C语言指针:何时使用&和*符号?

403
我刚刚开始学习指针,有些困惑。我知道&表示一个变量的地址,而在指针变量前加上*可以得到指针所指对象的值。但是当你处理数组、字符串或者用指针拷贝来调用函数时,情况会有所不同。这其中的逻辑模式不易看出。
何时应该使用&*

5
请说明你如何看待事情有时会出现不同的工作方式。否则,我们就得猜测什么让你感到困惑。 - Drew Dormann
1
同意Neil Butterworth的观点。从书本中直接获取信息可能会更加详细,而且K&R的解释非常清晰。 - Tom
1
虽然这是一个难以搜索的话题,但在各种形式下已经被反复讨论。以下内容均来自http://stackoverflow.com/search?q=c+pointer+basic ,可能与本问题相关(我确定有重复,但我没有找到):https://dev59.com/AnVD5IYBdhLWcg3wWaVh https://dev59.com/UXNA5IYBdhLWcg3wmfK5 https://dev59.com/cXVC5IYBdhLWcg3w2lGI https://dev59.com/qUbRa4cB1Zd3GeqP0Gka - dmckee --- ex-moderator kitten
223
我不同意那些认为在stackoverflow上提这些问题不是一个好主意的人。SO已成为在谷歌搜索时排名第一的资源。你们没有为这些答案给予足够的认可。请阅读丹·奥尔森(Dan Olson)的回答。这个答案真正有深度,对新手非常有帮助。“RTFM”是无用的,而且坦率地说非常粗鲁。如果你没有时间回答问题,那么请尊重那些花时间回答这些问题的人。我希望我能@“anon”,但显然他/她没有时间有任何有意义的贡献。 - SSH This
39
SSH This所说的是绝对正确的。有些人会大喊“去Google一下”,但如今情况已经颠倒了:“只需要在StackOverflow上查找”。这个问题对许多人都有用。(因此有很多赞和没有踩。) - MC Emperor
3
https://beej.us/blog/data/c-pointers/ - MmmHmm
10个回答

740
你有指针和值:
int* p; // variable p is pointer to integer type
int i; // integer value

你可以使用*将指针转换为值。
int i2 = *p; // integer i2 is assigned with integer value that pointer p is pointing to

您可以使用&将一个值转换为指针。
int* p2 = &i; // pointer p2 will point to the integer i

编辑: 在数组的情况下,它们被视为指针。 如果您将它们视为指针,可以使用 * 来访问其中的值,就像上面解释的那样,但还有另一种更常见的方法,即使用 [] 操作符:
int a[2];  // array of integers
int i = *a; // the value of the first element of a
int i2 = a[0]; // another way to get the first element

获取第二个元素:
int a[2]; // array
int i = *(a + 1); // the value of the second element
int i2 = a[1]; // the value of the second element

所以[]索引运算符是*运算符的一种特殊形式,它的工作方式如下:
a[i] == *(a + i);  // these two statements are the same thing

6
数组是特殊的,可以透明地转换为指针。这凸显了从指针到值的另一种方法,我会将其添加到上面的解释中。 - Dan Olson
8
如果我理解正确的话,例子 int *bX = &aX; 不起作用是因为 aX 已经返回了 aX[0] 的地址(即 &aX[0]),所以 &aX 将得到一个地址的地址,这没有意义。这样理解正确吗? - Pieter
8
你说得没错,但在某些情况下,你可能需要获取指针的地址。这种情况下,你需要声明 int** bX = &aX,不过这是一个更高级的话题。 - Dan Olson
5
@Dan,给定int aX[] = {3,4};int **bX = &aX;是错误的。 &aX的类型是“指向数组[2]的int的指针”,而不是“指向int的指针的指针”。具体来说,对于一元运算符&,数组的名称不被视为指向其第一个元素的指针。您可以这样做:int (*bX)[2] = &aX; - Alok Singhal
2
“将指针转换为值”是一个糟糕的描述。您可以使用 * 找到指针引用的值。指针是一个包含值的变量,它所包含的值是某个内存位置的地址。没有“转换”,指针也不会“转换为”它所引用的值。同样地,& 也不会“将值转换为指针”。&v 求出 v 的地址。没有转换。 - William Pursell
显示剩余4条评论

40
处理数组和函数时有一种模式,刚开始可能有点难以理解。
处理数组时,有一个有用的记忆方法:当大多数情况下使用数组表达式时,表达式的类型会从“T类型的N个元素数组”隐式转换为“指向T类型的指针”,并且该表达式的值被设置为指向数组中第一个元素。例外情况是当数组表达式作为 &sizeof 运算符的操作数时,或者它是在声明中用作初始化器的字符串字面量时。
因此,当您将数组表达式作为参数调用函数时,函数将接收一个指针而不是一个数组:
int arr[10];
...
foo(arr);
...

void foo(int *arr) { ... }

这就是为什么在scanf()中,不要使用&运算符来对应"%s"参数的原因:

char str[STRING_LENGTH];
...
scanf("%s", str);

由于隐式转换,scanf() 接收一个指向 str 数组开头的 char * 值。对于任何使用数组表达式作为参数调用的函数(几乎所有的 str* 函数、*scanf*printf 函数等),都是如此。
实际上,在实践中,您可能永远不会使用 & 运算符调用带有数组表达式的函数,例如:
int arr[N];
...
foo(&arr);

void foo(int (*p)[N]) {...}

这种代码并不常见,你需要在函数声明中知道数组的大小,并且该函数仅适用于特定大小的数组指针(指向10个元素的T数组的指针与指向11个元素的T数组的指针是不同的类型)。
当一个数组表达式作为“&”运算符的操作数出现时,生成的表达式的类型是“指向N元素的T数组”的指针,或者是T (*)[N],这与指针数组(T *[N])和基本类型的指针(T *)不同。
处理函数和指针时,要记住的规则是:如果您想改变参数的值并在调用代码中反映出来,则必须传递指向要修改的内容的指针。再次提醒,数组会使事情有些复杂,但我们先处理正常情况。
请记住,C通过值传递所有函数参数;形式参数接收实际参数中值的副本,对形式参数的任何更改都不会反映在实际参数中。常见的例子是交换函数:
void swap(int x, int y) { int tmp = x; x = y; y = tmp; }
...
int a = 1, b = 2;
printf("before swap: a = %d, b = %d\n", a, b);
swap(a, b);
printf("after swap: a = %d, b = %d\n", a, b);

你将得到如下输出:
交换前:a = 1,b = 2
交换后:a = 2,b = 1
形式参数 xy 是与 ab 不同的对象,因此对 xy 的更改不会反映在 ab 中。由于我们想要修改 ab 的值,所以必须向swap函数传递指向它们的指针。
void swap(int *x, int *y) {int tmp = *x; *x = *y; *y = tmp; }
...
int a = 1, b = 2;
printf("before swap: a = %d, b = %d\n", a, b);
swap(&a, &b);
printf("after swap: a = %d, b = %d\n", a, b);

现在你的输出将是:
交换之前: a = 1, b = 2
交换之后: a = 2, b = 1
请注意,在交换函数中,我们没有改变 xy 的值,而是改变了 xy 所指向的值。写入 *x 不同于写入 x;我们不是更新 x 中的值本身,而是从 x 获取一个位置并更新该位置中的值。
如果我们想修改指针值,这同样适用;如果我们写入:
int myFopen(FILE *stream) {stream = fopen("myfile.dat", "r"); }
...
FILE *in;
myFopen(in);

然后我们修改了输入参数 stream 的值,而不是 stream 指向的内容,所以改变 streamin 的值没有影响;为了让这个方法起作用,我们必须传递指向指针的指针:

int myFopen(FILE **stream) {*stream = fopen("myFile.dat", "r"); }
...
FILE *in;
myFopen(&in);

再次提到,数组会对程序造成一些影响。当你将一个数组表达式传递给一个函数时,函数接收到的是一个指针。由于数组下标操作符的定义方式,你可以像在数组上一样使用指针上的下标操作符:

int arr[N];
init(arr, N);
...
void init(int *arr, int N) {size_t i; for (i = 0; i < N; i++) arr[i] = i*i;}

请注意,数组对象可能无法分配;也就是说,您不能执行以下操作:
int a[10], b[10];
...
a = b;

所以当你处理指向数组的指针时要小心;比如下面这样的代码:

void (int (*foo)[N])
{
  ...
  *foo = ...;
}

无法工作。


32

简单来说

  • &表示取地址符,在函数占位符中可以看到这一点,用于修改参数变量。在C语言中,参数变量是通过值传递的,使用取地址符意味着按引用传递。
  • *表示指针变量的解引用,即获取该指针变量的值。
int foo(int *x){
   *x++;
}

int main(int argc, char **argv){
   int y = 5;
   foo(&y);  // Now y is incremented and in scope here
   printf("value of y = %d\n", y); // output is 6
   /* ... */
}

以上示例说明如何使用按引用传递的方式调用函数foo,与此相比,请看:
int foo(int x){
   x++;
}

int main(int argc, char **argv){
   int y = 5;
   foo(y);  // Now y is still 5
   printf("value of y = %d\n", y); // output is 5
   /* ... */
}

这是一个使用解引用的示例:

int main(int argc, char **argv){
   int y = 5;
   int *p = NULL;
   p = &y;
   printf("value of *p = %d\n", *p); // output is 5
}

上面的示例展示了我们如何获取变量 y地址并将其赋值给指针变量p。然后我们通过在*前面加上它来解引用p以获取p的值,即*p

20

在C/C++中,*被用于多种不同的目的,因此这可能会很复杂。

如果*出现在已经声明的变量/函数前面,则意味着:

  • a) 如果该变量的类型是指针类型或重载了*运算符,则*可以访问该变量的值。
  • b) 如果*表示乘法运算符,则必须在*左侧有另一个变量。

如果*出现在变量或函数声明中,则表示该变量是指针:

int int_value = 1;
int * int_ptr; //can point to another int variable
int   int_array1[10]; //can contain up to 10 int values, basically int_array1 is an pointer as well which points to the first int of the array
//int   int_array2[]; //illegal, without initializer list..
int int_array3[] = {1,2,3,4,5};  // these two
int int_array4[5] = {1,2,3,4,5}; // are identical

void func_takes_int_ptr1(int *int_ptr){} // these two are identical
void func_takes_int_ptr2(int int_ptr[]){}// and legal

如果在变量或函数声明中出现 &,通常表示该变量是对该类型变量的引用。

如果在已经声明的变量前面出现 &,它会返回该变量的地址。

此外,当将数组传递给函数时,除非数组类似于以0结尾的c字符串(char数组),否则您总是需要传递该数组的大小。


1
@akmozo s/func_takes int_ptr2/func_takes_int_ptr2/(空格无效) - PixnBits

10
我浏览了所有冗长的解释,最终通过观看新南威尔士大学的视频得到了帮助。这里是一个简单的解释:如果我们有一个地址为x,值为7的单元格,间接询问值为7的地址的方法是&7,而间接询问地址为x的值的方法是*x。因此,(cell: x, value: 7) == (cell: &7, value: *x)。另一种理解方式是:John坐在第七个座位上。*第七个座位将指向John,而&John将给出第七个座位地址/位置。这个简单的解释对我很有帮助,希望也能对其他人有所帮助。这是优秀视频的链接:点击这里。 这里是另一个例子:
#include <stdio.h>

int main()
{ 
    int x;            /* A normal integer*/
    int *p;           /* A pointer to an integer ("*p" is an integer, so p
                       must be a pointer to an integer) */

    p = &x;           /* Read it, "assign the address of x to p" */
    scanf( "%d", &x );          /* Put a value in x, we could also use p here */
    printf( "%d\n", *p ); /* Note the use of the * to get the value */
    getchar();
}

附加说明: 在使用指针之前,始终要对其进行初始化。如果不这样做,指针将指向任何内容,可能会导致程序崩溃,因为操作系统会阻止您访问它知道您不拥有的内存。但是,通过简单地将 p = &x;,我们可以为指针分配一个特定的位置。


7

实际上,你已经掌握了它,没有更多需要知道的了 :-)

我只想补充以下几点:

  • 这两个操作是光谱的两端。&取一个变量并给出地址,*取一个地址并给出变量(或内容)。
  • 当你将数组传递给函数时,它们会“降级”为指针。
  • 你实际上可以有多个间接级别(char **p表示p是指向指向char的指针。

至于事情是否有所不同,实际上并不是:

  • 如前所述,当传递数组到函数时,它们会降级为指针(指向数组中的第一个元素),它们不保留大小信息。
  • C语言中没有字符串,只有字符数组,按照约定,它们表示以零(\0)字符结尾的字符序列。
  • 当你将变量的地址传递给函数时,你可以解引用指针来改变变量本身(通常变量是按值传递的(除了数组))。

6
我认为你有点困惑。你应该阅读一本关于指针的好教程/书籍。
这个教程非常适合初学者(清楚地解释了&*是什么)。还有,别忘了读肯尼思·里克(Kenneth Reek)的书《C语言指针》。 &*之间的区别非常明显。
例如:
#include <stdio.h>

int main(){
  int x, *p;

  p = &x;         /* initialise pointer(take the address of x) */
  *p = 0;         /* set x to zero */
  printf("x is %d\n", x);
  printf("*p is %d\n", *p);

  *p += 1;        /* increment what p points to i.e x */
  printf("x is %d\n", x);

  (*p)++;         /* increment what p points to i.e x */
  printf("x is %d\n", x);

  return 0;
}

4
当您声明指针变量或函数参数时,请使用*:
int *x = NULL;
int *y = malloc(sizeof(int)), *z = NULL;
int* f(int *x) {
    ...
}

注意:每个声明的变量都需要自己的 *。
当您想要获取一个值的地址时,请使用 &。当您想要读取或写入指针中的值时,请使用 *。
int a;
int *b;
b = f(&a);
a = *b;

a = *f(&a);

数组通常被视为指针。当您在函数中声明数组参数时,您可以轻松地将其声明为指针(这意味着相同的事情)。当您将数组传递给函数时,实际上是传递了指向第一个元素的指针。

函数指针是唯一不完全遵循规则的东西。您可以获取函数的地址而不使用&,并且可以调用函数指针而不使用*。


1

好的,看起来你的帖子被编辑了...

double foo[4];
double *bar_1 = &foo[0];

看看如何使用&来获取数组结构的开头地址?以下是

Foo_1(double *bar, int size){ return bar[size-1]; }
Foo_2(double bar[], int size){ return bar[size-1]; }

将会做同样的事情。


该问题已被标记为C而不是C ++。 - Prasoon Saurav
1
我已经删除了有问题的 cout << - wheaties

1

理解指针起初是很复杂的,你需要做练习并且多加实践。 不要期望在第一次迭代中就能掌握它,也不要期望通过阅读解释就能理解, 因为很可能你并没有真正理解...

如果你想获得更多不仅仅是理论上的理解,我建议你参加斯坦福大学CS107课程并完成给定的练习, 至少要学习前三节课程,其中讲解了指针。

Jerry Cain的斯坦福大学CS107课程

另一个非常有价值的工具是gdb,它提供了一个交互式的shell,就像你在Python中使用的那样。 通过gdb,你可以进行游戏和实验:

 (gdb) x pp.name
0x555555555060 <_start>:        0x8949ed31
(gdb) x &pp.name
0x7fffffffdc38: 0x55555060
(gdb) p &pp.name
$4 = (char **) 0x7fffffffdc38
(gdb) p *pp.name
$5 = 49 '1'

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