在指针声明中放置星号
Posted
技术标签:
【中文标题】在指针声明中放置星号【英文标题】:Placement of the asterisk in pointer declarations 【发布时间】:2010-09-15 21:00:53 【问题描述】:我最近决定我必须最终学习 C/C++,关于指针,或者更准确地说,我不太了解它们的定义。
这些例子怎么样:
int* test;
int *test;
int * test;
int* test,test2;
int *test,test2;
int * test,test2;
现在,据我了解,前三种情况都在做同样的事情:Test 不是一个 int,而是一个指向 int 的指针。
第二组示例有点棘手。在案例 4 中,test 和 test2 都是指向 int 的指针,而在案例 5 中,只有 test 是指针,而 test2 是“真正的”int。案例6呢?同案例5?
【问题讨论】:
在 C/C++ 中,空格不会改变含义。 7.int*test;
?
+1 因为我只想问 1 - 3。阅读这个问题教会了我一些我从未想过的关于 4 - 6 的事情。
@AnorZaken 你是对的,这是一个相当古老的评论。有多种情况下空格会改变含义,例如,增量 ++
运算符不能被空格分割,标识符不能被空格分割(结果对于编译器仍然是合法的,但具有未定义的运行时行为)。考虑到 C/C++ 的语法混乱,确切的情况很难定义。
我不明白为什么人们一直说这只是“美学”或“风格”或“见仁见智”。 int* test,test2;
没有达到您的预期,这意味着它是错误的,是对语言的误解,而 int *test,test2;
是正确的。
【参考方案1】:
4、5、6是同一个东西,只是test是一个指针。如果你想要两个指针,你应该使用:
int *test, *test2;
或者,甚至更好(让一切都清楚):
int* test;
int* test2;
【讨论】:
那么案例4实际上是一个死亡陷阱吗?是否有任何规范或进一步阅读可以解释为什么 int* test,test2 只使第一个变量成为指针? @Michael Stum 是 C++ 所以你真的认为有一个合乎逻辑的解释吗? 阅读 K&R(C 编程语言)。它非常清楚地解释了这一切。 案例 4、5 和 6 是“死亡陷阱”。这就是为什么许多 C/C++ 风格指南建议每个语句只声明一个的原因之一。 空格对于 C 编译器来说是微不足道的(忽略预处理器)。所以无论星号和周围有多少个空格,它的含义都是一样的。【参考方案2】:星号周围的空白没有意义。这三个意思是一样的:
int* test;
int *test;
int * test;
“int *var1, var2
”是一种邪恶的语法,只是为了迷惑人们,应该避免。它扩展为:
int *var1;
int var2;
【讨论】:
星号前后的空格只是美观问题。但是,Google 编码标准与int *test
(google-styleguide.googlecode.com/svn/trunk/…) 一致。保持一致
@SebastianRaschka Google C++ Style Guide 明确允许放置星号。也许自从你阅读它以来它已经改变了。【参考方案3】:
许多编码指南建议您只声明每行一个变量。这避免了您在问这个问题之前所遇到的任何混淆。与我共事过的大多数 C++ 程序员似乎都坚持这一点。
我知道有点旁白,但我发现有用的是向后阅读声明。
int* test; // test is a pointer to an int
这开始工作得很好,尤其是当您开始声明 const 指针时,很难知道它是指针是 const,还是指针指向的东西是否是 const。
int* const test; // test is a const pointer to an int
int const * test; // test is a pointer to a const int ... but many people write this as
const int * test; // test is a pointer to an int that's const
【讨论】:
虽然“每行一个变量”看起来很有用,但我们仍然没有完全解决星号更靠左或更靠右的情况。我很确定,在野外代码中,一种变体占了上风。有点像一些国家在右侧行驶,而另一些国家则在错误的方向行驶,例如英国。 ;-) 不幸的是,从我到野外的冒险中,我看到了很多两种风格。在我的团队中,我们现在使用我们已经同意的风格的 clang 格式。这至少意味着我们团队生成的所有代码对于空格所在的位置都具有相同的样式。【参考方案4】:使用"Clockwise Spiral Rule" 帮助解析C/C++ 声明;
只需三个简单的步骤:
从未知元素开始,沿螺旋/顺时针方向移动 方向;当遇到以下元素时,将它们替换为 对应的英文语句:
[X]
或[]
:数组 X 大小...或数组未定义大小...
(type1, type2)
: 函数传递 type1 和 type2 返回...继续以螺旋/顺时针方向执行此操作,直到覆盖所有标记。 始终先解决括号中的任何问题!
*
:指向...的指针
此外,声明应尽可能在单独的语句中(绝大多数情况下都是如此)。
【讨论】:
这看起来令人生畏而且非常可怕,很抱歉。 确实如此,但对于一些更复杂的结构来说,这似乎是一个很好的解释 @d03boy:毫无疑问 - C/C++ 声明可能是一场噩梦。 “螺旋”没有任何意义,更不用说“顺时针”了。我宁愿把它命名为“左右规则”,因为语法不会让你看起来是右下左上,只是右左。 我将其作为“右-左-右”规则学习。 C++ 人通常喜欢假装所有类型信息都在左侧,这导致了int* x;
样式而不是传统的int *x;
样式。当然,间距对编译器无关紧要,但它确实会影响人类。否认实际语法会导致样式规则使读者感到烦恼和困惑。【参考方案5】:
正如其他人提到的,4、5 和 6 是相同的。通常,人们使用这些示例来证明*
属于变量而不是类型。虽然这是一个风格问题,但对于您是否应该这样思考和编写它存在一些争论:
int* x; // "x is a pointer to int"
或者这样:
int *x; // "*x is an int"
FWIW 我属于第一个阵营,但其他人提出第二种形式的理由是它(大部分)解决了这个特殊问题:
int* x,y; // "x is a pointer to int, y is an int"
这可能具有误导性;相反,你会写要么
int *x,y; // it's a little clearer what is going on here
或者如果你真的想要两个指针,
int *x, *y; // two pointers
就个人而言,我说每行保留一个变量,那么你喜欢哪种风格并不重要。
【讨论】:
这是假的,你叫什么int *MyFunc(void)
? *MyFunc
是一个返回 int
的函数?不。显然我们应该写int* MyFunc(void)
,并说MyFunc
是一个返回int*
的函数。所以对我来说这很清楚,C 和 C++ 语法解析规则对于变量声明来说是完全错误的。他们应该将指针限定作为整个逗号序列的共享类型的一部分。
但是*MyFunc()
是 int
。 C 语法的问题是混合 prefix 和 postfix 语法 - 如果使用 only 后缀,则不会混淆。
第一个阵营与语言的语法作斗争,导致像int const* x;
这样的结构令人困惑,我发现它与a * x+b * y
一样具有误导性。【参考方案6】:
这个谜题分为三个部分。
第一部分是 C 和 C++ 中的空格通常不重要,除了分隔在其他情况下无法区分的相邻标记。
在预处理阶段,源文本被分解为一系列标记 - 标识符、标点符号、数字文字、字符串文字等。稍后分析该标记序列的语法和含义.分词器是“贪婪的”,并且会构建尽可能长的有效令牌。如果你写类似
inttest;
标记器只看到两个标记 - 标识符 inttest
后跟标点符号 ;
。在此阶段,它不会将 int
识别为单独的关键字(在此过程中稍后会发生)。因此,要将该行读取为名为@987654325@ 的整数的声明,我们必须使用空格来分隔标识符标记:
int test;
*
字符不是任何标识符的一部分;它本身就是一个单独的标记(标点符号)。所以如果你写
int*test;
编译器看到 4 个单独的标记 - int
、*
、test
和 ;
。因此,空白在指针声明中并不重要,所有的
int *test;
int* test;
int*test;
int * test;
以同样的方式解释。
第二个难题是声明在 C 和 C++ 中的实际工作方式1。声明分为两个主要部分 - 声明说明符序列(存储类说明符、类型说明符、类型限定符等),然后是逗号分隔的列表(可能已初始化)声明符。在声明中
unsigned long int a[10]=0, *p=NULL, f(void);
声明说明符是unsigned long int
,声明符是a[10]=0
、*p=NULL
和f(void)
。声明器引入了被声明事物的名称(a
、p
和 f
)以及有关该事物的数组、指针和函数的信息。声明器也可能有一个关联的初始化器。
a
的类型是“unsigned long int
的 10 元素数组”。该类型完全由声明说明符和声明符的组合 指定,初始值由初始化程序=0
指定。类似地,p
的类型是“指向unsigned long int
的指针”,并且该类型再次由声明说明符和声明符的组合指定,并初始化为NULL
。而f
的类型也是“函数返回unsigned long int
”,同理。
这是关键 - 没有“指针”类型说明符,就像没有“array-of”类型说明符,就像没有“函数返回”类型说明符一样.我们不能将数组声明为
int[10] a;
因为[]
运算符的操作数是a
,而不是int
。同样,在声明中
int* p;
*
的操作数是 p
,而不是 int
。但是因为间接操作符是一元的并且空格不重要,如果我们这样写,编译器不会抱怨。但是,它总是被解释为int (*p);
。
因此,如果你写
int* p, q;
*
的操作数是p
,所以会被解释为
int (*p), q;
因此,所有的
int *test1, test2;
int* test1, test2;
int * test1, test2;
做同样的事情 - 在这三种情况下,test1
是 *
的操作数,因此类型为“指向int
的指针”,而test2
的类型为 int
。
声明符可以任意复杂。你可以有指针数组:
T *a[N];
你可以有指向数组的指针:
T (*a)[N];
你可以让函数返回指针:
T *f(void);
你可以有指向函数的指针:
T (*f)(void);
您可以拥有指向函数的指针数组:
T (*a[N])(void);
你可以让函数返回指向数组的指针:
T (*f(void))[N];
你可以让函数返回指向指针数组的指针,这些函数返回指向T
的指针:
T *(*(*f(void))[N])(void); // yes, it's eye-stabby. Welcome to C and C++.
然后你有signal
:
void (*signal(int, void (*)(int)))(int);
读作
signal -- signal
signal( ) -- is a function taking
signal( ) -- unnamed parameter
signal(int ) -- is an int
signal(int, ) -- unnamed parameter
signal(int, (*) ) -- is a pointer to
signal(int, (*)( )) -- a function taking
signal(int, (*)( )) -- unnamed parameter
signal(int, (*)(int)) -- is an int
signal(int, void (*)(int)) -- returning void
(*signal(int, void (*)(int))) -- returning a pointer to
(*signal(int, void (*)(int)))( ) -- a function taking
(*signal(int, void (*)(int)))( ) -- unnamed parameter
(*signal(int, void (*)(int)))(int) -- is an int
void (*signal(int, void (*)(int)))(int); -- returning void
这只是触及了可能的表面。但请注意,数组、指针和函数始终是声明符的一部分,而不是类型说明符。
需要注意的一点——const
可以同时修改指针类型和指向类型:
const int *p;
int const *p;
以上两个都将p
声明为指向const int
对象的指针。您可以向p
写入一个新值,将其设置为指向不同的对象:
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
但您不能写入指向的对象:
*p = 3; // constraint violation, the pointed-to object is const
然而,
int * const p;
将p
声明为const
指向非常量int
的指针;你可以写信给p
指向的东西
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
但您不能将p
设置为指向不同的对象:
p = &y; // constraint violation, p is const
这给我们带来了第三个难题 - 为什么声明是这样构造的。
其目的是声明的结构应密切反映代码中表达式的结构(“声明模仿使用”)。例如,假设我们有一个指向int
的指针数组,名为ap
,并且我们想要访问i
'th 元素指向的int
值。我们将按如下方式访问该值:
printf( "%d", *ap[i] );
表达式 *ap[i]
的类型为int
;因此,ap
的声明写成
int *ap[N]; // ap is an array of pointer to int, fully specified by the combination
// of the type specifier and declarator
声明符*ap[N]
与表达式*ap[i]
具有相同的结构。运算符*
和[]
在声明中的行为方式与它们在表达式中的行为相同——[]
的优先级高于一元*
,因此*
的操作数是ap[N]
(它被解析为*(ap[N])
)。
再举一个例子,假设我们有一个指向名为pa
的int
数组的指针,并且我们想要访问i
'th 元素的值。我们会把它写成
printf( "%d", (*pa)[i] );
表达式(*pa)[i]
的类型是int
,所以声明写成
int (*pa)[N];
同样,优先级和关联性规则同样适用。在这种情况下,我们不想取消引用pa
的i
'th 元素,我们想访问pa
中的i
'th 元素指向,所以我们必须明确地将*
运算符与pa
分组。
*
、[]
和 ()
运算符都是代码中 表达式 的一部分,因此它们都是 declarator 的一部分宣言。声明器告诉您如何在表达式中使用对象。如果你有一个像int *p;
这样的声明,它告诉你代码中的表达式*p
将产生一个int
值。通过扩展,它告诉您表达式p
产生一个类型为“指向int
”或int *
的值。
那么,像 cast 和 sizeof
表达式这样的东西,我们使用像 (int *)
或 sizeof (int [10])
这样的东西呢?我如何阅读类似的内容
void foo( int *, int (*)[10] );
没有声明符,*
和[]
运算符不是直接修改类型吗?
嗯,不——仍然有一个声明符,只是带有一个空标识符(称为抽象声明符)。如果我们用符号 λ 表示一个空标识符,那么我们可以将这些内容读作(int *λ)
、sizeof (int λ[10])
和
void foo( int *λ, int (*λ)[10] );
并且它们的行为与任何其他声明完全一样。 int *[10]
代表一个包含 10 个指针的数组,而 int (*)[10]
代表一个指向数组的指针。
现在是这个答案的固执部分。我不喜欢将简单指针声明为的 C++ 约定
T* p;
并认为它是不好的做法,原因如下:
-
与语法不符;
它引入了混淆(正如这个问题所证明的那样,这个问题的所有重复项、关于
T* p, q;
含义的问题、那些问题的所有重复项等等);
内部不一致 - 将指针数组声明为 T* a[N]
在使用中是不对称的(除非您有编写 * a[i]
的习惯);
它不能应用于指向数组的指针或指向函数的指针类型(除非您创建一个 typedef 以便可以干净地应用 T* p
约定,这...否 );
这样做的原因 - “它强调对象的指针性” - 是虚假的。它不能应用于数组或函数类型,我认为这些品质同样值得强调。
最后,它只是表明对这两种语言的类型系统如何工作的困惑。
有充分的理由单独申报物品;解决不好的做法 (T* p, q;
) 不是其中之一。如果您正确地编写声明符 (T *p, q;
),则不太可能引起混淆。
我认为这类似于故意将所有简单的 for
循环编写为
i = 0;
for( ; i < N; )
...
i++;
句法有效,但令人困惑,意图可能被误解。然而,T* p;
约定在 C++ 社区中根深蒂固,我在我自己的 C++ 代码中使用它,因为代码库之间的一致性是一件好事,但每次这样做都会让我很痒。
- 我将使用 C 术语 - C++ 术语略有不同,但概念大致相同。
【讨论】:
这是这个问题的最佳答案。它应该得到更高的投票。【参考方案7】:#include <type_traits>
std::add_pointer<int>::type test, test2;
【讨论】:
#include <windows.h>LPINT test, test2;
【参考方案8】:
在 4、5 和 6 中,test
始终是指针,test2
不是指针。空格在 C++ 中(几乎)不重要。
【讨论】:
【参考方案9】:C 语言的基本原理是您以使用它们的方式声明变量。例如
char *a[100];
说*a[42]
将是char
。和a[42]
一个字符指针。因此a
是一个字符指针数组。
这是因为最初的编译器编写者希望对表达式和声明使用相同的解析器。 (语言设计选择不是一个非常明智的理由)
【讨论】:
然而写char* a[100];
also 推断*a[42];
将是一个char
和a[42];
一个字符指针。
好吧,我们都得出相同的结论,只是顺序不同。
引用:“说 *a[42] 将是一个字符。而 a[42] 是一个字符指针”。你确定不是反过来吗?
如果你更喜欢另一种方式,比如a[42]
是一个char
指针,*a[42]
是一个字符。【参考方案10】:
我会说最初的约定是将星号放在指针名称一侧(声明的右侧
在 c 编程语言 中由 Dennis M. Ritchie 编写,星星位于声明的右侧。
通过查看https://github.com/torvalds/linux/blob/master/init/main.c 的 linux 源代码 我们可以看到星星也在右边。
您可以遵循相同的规则,但如果您将星标放在字体一侧,这没什么大不了的。 请记住,一致性很重要,因此无论您选择哪一边,始终要让明星站在同一边。
【讨论】:
嗯 - 解析器似乎允许任何一种变体,但如果丹尼斯和莱纳斯说它应该在右侧,那是非常引人注目的。但是,我们仍然缺乏一些理由,然后也解释为什么要这样做。这有点像制表符与空格的情况 - 除了一个得到了解决,因为使用空格而不是制表符的人可以赚更多的钱,根据 *** ... :-)【参考方案11】:在我看来,答案是两者皆有,视情况而定。 通常,IMO,最好将星号放在指针名称旁边,而不是类型旁边。比较例如:
int *pointer1, *pointer2; // Fully consistent, two pointers
int* pointer1, pointer2; // Inconsistent -- because only the first one is a pointer, the second one is an int variable
// The second case is unexpected, and thus prone to errors
为什么第二种情况不一致?因为例如int x,y;
声明了两个相同类型的变量,但该类型在声明中只提及一次。这创造了一个先例和预期的行为。而int* pointer1, pointer2;
与此不一致,因为它声明pointer1
是一个指针,而pointer2
是一个整型变量。显然容易出错,因此应该避免(通过将星号放在指针名称旁边,而不是类型旁边)。
但是,有一些例外情况,您可能无法将星号放在对象名称旁边(以及放置位置很重要)而不得到不希望的结果——例如:
MyClass *volatile MyObjName
void test (const char *const p) // const value pointed to by a const pointer
最后,在某些情况下,将星号放在 type 名称旁边可能会更清晰,例如:
void* ClassName::getItemPtr () return &item; // Clear at first sight
【讨论】:
【参考方案12】:指针是类型的修饰符。最好从右到左阅读它们,以便更好地理解星号如何修改类型。 'int *' 可以读作“指向 int 的指针”。在多个声明中,您必须指定每个变量都是指针,否则它将被创建为标准变量。
1,2 和 3) 测试属于 (int *) 类型。空格无关紧要。
4,5 和 6) 测试属于 (int *) 类型。 Test2 是 int 类型。同样,空格是无关紧要的。
【讨论】:
【参考方案13】:一个很好的经验法则,很多人似乎通过以下方式掌握这些概念:在 C++ 中,很多语义是通过关键字或标识符的左绑定派生的。
举个例子:
int const bla;
const 适用于“int”字。指针的星号也是如此,它们适用于它们左侧的关键字。和实际的变量名?是的,这是由它的剩余部分声明的。
【讨论】:
这不能回答问题。更糟糕的是,如果我们试图从中推断一个答案,那么它意味着星号绑定到它左边的类型,正如其他人所说的那样,这是错误的。它绑定到右侧的单个变量名。以上是关于在指针声明中放置星号的主要内容,如果未能解决你的问题,请参考以下文章
clang-format:将指针声明的星号 (*) 与变量名对齐