指针声明中星号的放置位置

134

我最近决定必须学习C/C++,但有一件事情我不太理解,那就是指针或者更准确地说是它们的定义。

这些例子怎么样:

  1. int* test;
  2. int *test;
  3. int * test;
  4. int* test,test2;
  5. int *test,test2;
  6. int * test,test2;

现在,据我所知,前三种情况都是一样的:Test 不是一个 int,而是指向 int 的指针。

第二组例子有点棘手。在 Case 4 中,test 和 test2 都将成为指向 int 的指针,而在 Case 5 中,只有 test 是指针,而 test2 是一个 "真正" 的 int。Case 6 呢?和 Case 5 一样吗?


18
在C/C++中,空格不会改变其含义。 - Sulthan
37
这是一个声明一个名为test的指向整型(int)变量的指针(pointer)的语句。 - Jin Kwon
4
+1 是因为我只考虑了 1-3。读到这个问题让我对 4-6 有了新的认识。 - vastlysuperiorman
4
@AnorZaken 你说得对,那是一个相当古老的评论。有多种情况下,空格会改变含义,例如,递增 ++ 运算符不能用空格分开,标识符也不能用空格分开(编译器可能仍然合法但运行时行为未定义)。考虑到 C/C++ 的语法混乱,确切的情况很难定义。 - Sulthan
5
我不明白为什么人们总是说这只是“美学”、“风格”或“观点问题”。int* test,test2;不能按照你的期望工作,这意味着它是错误的,是对语言的误解的结果,而int *test,test2;才是正确的。 - endolith
显示剩余5条评论
15个回答

154

4、5和6是相同的东西,只有test是一个指针。如果你想要两个指针,你应该使用:

int *test, *test2;

或者,更好的是(为了让一切更清晰明了):
int* test;
int* test2;

7
案例4实际上是个死亡陷阱吗?是否有任何规范或进一步的阅读材料解释为什么int* test,test2只让第一个变量成为指针? - Michael Stum
13
这是C++,你真的认为有逻辑解释吗? - Joe Phillips
8
阅读K&R(C程序设计语言),它非常清楚地解释了所有这些内容。 - Ferruccio
10
案例4、5和6是“死亡陷阱”。这是为什么许多C/C++风格指南建议每个语句只有一个声明的原因。 - Michael Burr
15
在C编译器中(忽略预处理器),空格是无关紧要的。因此,无论在星号及其周围有多少空格或没有空格,其意义都完全相同。 - ephemient
显示剩余4条评论

56

星号周围的空格没有意义。以下三个表达式都表示相同的意思:

int* test;
int *test;
int * test;
"int *var1;int var2;"
int *var1;
int var2;

3
星号前后的空格仅仅是美观方面的问题。然而,Google编码标准采用int *test的写法(http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Pointer_and_Reference_Expressions)。请保持一致。 - user2489252
@SebastianRaschka 《Google C++ Style Guide》(http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Pointer_and_Reference_Expressions)明确允许星号的放置方式。也许自你上次阅读以来已经有所改变了。 - Jared Beck
1
@JaredBeck https://google.github.io/styleguide/cppguide.html#Pointer_and_Reference_Expressions - endolith

45

许多编码指南建议你每行只声明一个变量,这样可以避免任何混淆以及你在提问之前遇到的问题。我与之合作的大多数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

1
虽然“每行一个变量”似乎很有用,但我们仍然没有完全解决星号更靠左或更靠右的情况。我相当确定,在野外的代码中,一种变体占主导地位;有点像某些国家开车在右侧,而其他国家开车在错误的方向上,比如英国。;-) - shevy
不幸的是,在我的野外冒险中,我看到了许多这两种风格。在我的团队中,我们现在使用clang-format,并采用了一种我们都同意的样式。这至少意味着我们团队制作的所有代码都有相同的空格放置样式。 - Scott Langham

35

使用“顺时针螺旋规则”帮助解析C/C++声明;

遵循以下三个简单步骤:

  1. 从未知元素开始,沿着螺旋/顺时针方向移动; 遇到以下元素时,请将其替换为对应的英文语句:

    [X][]: 数组大小为 X 或 数组大小未定义...

    (type1, type2): 函数传递类型 type1 和 type2 并返回...

    *: 指向... 的指针(s)

  2. 保持在一个螺旋/顺时针方向上持续执行,直到覆盖所有记号。
  3. 始终优先解决括号中的内容!

此外,在可能的情况下(这在绝大多数情况下都是正确的),声明应该分成单独的语句。


10
看起来令人望而生畏且相当可怕,很抱歉这么说。 - Joe Phillips
7
可以,但似乎这是解释一些更复杂结构的很好的解释。 - Michael Stum
1
@d03boy:毫无疑问 - C/C++ 的声明可能会让人崩溃。 - Michael Burr
3
“螺旋”一词毫无意义,更不用说“顺时针”了。我宁愿称之为“右左规则”,因为语法不会让你看起来像从右到下再到左再到上,只是从右到左。 - Ruslan
我学到的是“右左右”规则。C++程序员通常喜欢假装所有类型信息都在左边,这导致了int* x;风格而不是传统的int *x;风格。当然,空格对编译器来说并不重要,但它确实会影响人类。否认实际语法会导致样式规则,这可能会使读者感到恼怒和困惑。 - Adrian McCarthy
4
它既不是“螺旋”也不是“左右”,也不是任何其他特定的模式:它只是根据括号、优先级和相应的评估顺序(从左到右或从右到左)应用运算符,就像在相应的表达式中一样,该表达式将内置类型转换为左侧。在 int *arr [1] [2] [3] [4] 中,你的螺旋或左右在哪里? - Peter - Reinstate Monica

34
这个谜题有三个部分。
第一部分是,在C和C++中,空白通常不重要,除了用于分隔无法区分的相邻标记。
在预处理阶段,源文本被分解成一系列“标记” - 标识符、标点符号、数字文字、字符串文字等。这些标记序列稍后会被分析语法和含义。标记器是“贪婪”的,会构建最长的有效标记。如果你写了类似这样的东西:
inttest;

分词器只看到两个标记 - 标识符inttest后面是标点符号;。它在这个阶段不会将int识别为一个单独的关键字(这在后续过程中发生)。因此,为了将该行读作一个名为test的整数声明,我们必须使用空格来分隔标识符标记。
int test;
*字符不是任何标识符的一部分;它是一个独立的标记(标点符号)。因此,如果你写成:
int*test;

编译器将这段代码分解为4个独立的标记 - int*test;。因此,在指针声明中,空格并不重要,并且所有的
int *test;
int* test;
int*test;
int     *     test;

被解释的方式是相同的。
拼图的第二部分是C和C++中声明的实际工作方式¹。声明被分为两个主要部分 - 一系列的声明说明符(存储类说明符、类型说明符、类型限定符等),后面是逗号分隔的(可能初始化的)声明符列表。在声明中,
unsigned long int a[10]={0}, *p=NULL, f(void);

声明说明符是unsigned long int,而声明符是a[10]={0}*p=NULLf(void)。声明符引入了被声明的事物的名称(apf),以及关于该事物的数组性、指针性和函数性的信息。声明符还可以有一个相关的初始化器。 a的类型是"10个元素的unsigned long int数组"。这个类型由声明说明符和声明符的组合完全确定,并且初始值由初始化器={0}指定。同样,p的类型是"指向unsigned long int的指针",同样由声明说明符和声明符的组合指定,并且初始化为NULL。根据同样的推理,f的类型是"返回unsigned long int的函数"。
这是关键-没有"指向"的类型说明符,就像没有"数组"类型说明符一样,就像没有"返回函数"的类型说明符一样。我们无法声明一个数组为
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++.

然后你有 信号

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`为指向非常量`int`的`const`指针;你可以对`p`指向的对象进行写操作。
int x = 1;
int y = 2;
int * const p = &x;

*p = 3;

但你不能将p设置为指向不同的对象:

p = &y; // constraint violation, p is const

这就引出了谜题的第三个部分 - 为什么声明是这样结构化的。
意图是,声明的结构应该与代码中表达式的结构紧密对应("声明模仿使用")。例如,假设我们有一个指向整数 int 的指针数组,名为 ap,我们想要访问由第 i 个元素指向的 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]))。

作为另一个例子,假设我们有一个指向名为 paint 数组的指针,并且我们想要访问第 i 个元素的值。我们会这样写:

printf( "%d", (*pa)[i] );

表达式(*pa)[i]的类型是int,因此声明应写为
int (*pa)[N];

再次强调,优先级和结合性的规则仍然适用。在这种情况下,我们不想解引用`pa`的第`i`个元素,而是想访问`pa`所指向的对象的第`i`个元素,因此我们必须明确地将`*`运算符与`pa`分组。
`*`、`[]`和`()`运算符都是代码中表达式的一部分,因此它们也是声明符的一部分。声明符告诉您如何在表达式中使用该对象。如果您有一个声明如`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;

考虑到以下原因,我们认为这种做法是不好的:
1. 它与语法不一致; 2. 它会引起混淆(正如这个问题所证明的,所有关于这个问题的重复问题,关于T* p, q;含义的问题,以及所有关于那些问题的重复问题等); 3. 它在内部不一致 - 声明一个指针数组为T* a[N]与使用方式不对称(除非你习惯写* a[i]); 4. 它不能应用于指向数组或函数的指针类型(除非你创建一个typedef来清晰地应用T* p约定,但...不推荐这样做); 5. 这样做的原因 - "它强调了对象的指针性质" - 是站不住脚的。它不能应用于数组或函数类型,而我认为这些特性同样重要需要强调。
总之,这只是表明对两种语言的类型系统工作方式存在困惑的思维。
有很多好的理由来单独声明变量;绕过不良实践(T* p, q;)并不是其中之一。如果你正确地书写你的声明符号(T *p, q;),你就不太可能引起混淆。
我认为这类似于故意将所有简单的for循环都写成
i = 0;
for( ; i < N; ) 
{ 
  ... 
  i++; 
}

语法上是有效的,但容易混淆,意图很可能被误解。然而,在C++社区中,“T* p;”的约定已经根深蒂固,并且我在自己的C++代码中使用它,因为代码库的一致性是一件好事,但每次这样做都让我感到不舒服。
我将使用C术语 - C++术语略有不同,但概念基本相同。

5
这是这个问题的最佳答案,应该有更高的投票数。 - Lou
1
这意味着引用声明也是如此:int &ref = x; - webninja

15

正如其他人提到的,4、5和6是相同的。通常,人们使用这些示例来论证*应该与变量而不是类型相关联。虽然这是一种风格问题,但是否应该以这种方式思考和编写仍存在一些争议:

int* x; // "x is a pointer to int"

或者这样:

int *x; // "*x is an int"

顺便提一句,我属于第一种观点,但其他人为第二种形式辩护的原因是它(大多数时候)解决了这个特定问题:

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

就我个人而言,我建议每行只定义一个变量,这样就不用担心你喜欢哪种风格了。


8
这是无效的,你如何称呼 int *MyFunc(void)?是一个返回 int 的函数吗?不是的。显然我们应该写成 int* MyFunc(void),并说 MyFunc 是一个返回 int* 的函数。所以对我来说很清楚,C 和 C++ 的语法解析规则在变量声明上是错的。它们应该将指针资格作为整个逗号序列的共享类型的一部分。 - v.oddou
3
但是 *MyFunc() 是一个 int。C 语法的问题在于混合使用前缀和后缀语法 - 如果仅使用后缀语法,就不会有混淆。 - Antti Haapala -- Слава Україні
6
第一个阵营反对语言的语法结构,导致出现令人困惑的格式,比如 int const* x;,我认为这种写法会误导人,就像 a * x+b * y 一样。 - Adrian McCarthy

11
#include <type_traits>

std::add_pointer<int>::type test, test2;

#include <windows.h>LPINT test, test2; - Stefan Dragnev
对于那些好奇的人:在这种情况下,testtest2都是int*类型。 - undefined

5

在4、5和6中,test始终是一个指针,而test2不是指针。在C++中,空格(几乎)从不重要。


4
C语言的基本原则是在使用变量时就声明它们。例如:
char *a[100];

这段话说的是,*a[42] 将会是一个 char 类型。而 a[42] 则是一个指向 char 类型的指针。因此,a 是一个 char 指针数组。

这是因为最初的编译器作者希望在表达式和声明中使用相同的解析器。(这并不是一个非常明智的语言设计选择)


然而,编写char* a[100];也可以推断出*a[42];将是一个char,而a[42];是一个字符指针。 - yyny
我们都得出了相同的结论,只是顺序不同。 - Michel Billaud
“说*a [42]将是一个字符。而a [42]是一个字符指针。”你确定不是反过来吗? - deLock
如果您更喜欢另一种方式,可以说a[42]是一个char指针,而*a[42]是一个字符。 - Michel Billaud

3
我认为最初的惯例是将星号放在指针名称侧(声明的右侧)。
在Dennis M. Ritchie的《C编程语言》中,星号位于声明的右侧。
通过查看https://github.com/torvalds/linux/blob/master/init/main.c上的Linux源代码,我们可以看到星号也在右侧。
您可以遵循相同的规则,但如果您将星号放在类型侧也没有关系。请记住,一致性很重要,因此请始终将星号放在同一侧,无论您选择哪一侧。

好吧,解析器似乎允许使用任一变量,但如果Dennis和Linus说它应该在右侧,那就非常有说服力了。但是我们还缺少一些理由,以及为什么要这样做的解释。这有点像选用哪种缩进方式-除了一个已经被解决了,因为使用空格而不是制表符的人,根据StackOverflow赚更多的钱... :-) - shevy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接