在使用C语言中的**时有哪些不同之处?

45

我最近开始学习C语言,但理解指针语法有问题,例如当我写下以下代码行:

int ** arr = NULL;

如何判断:

  • arr 是一个整数指针的指针

  • arr 是一个指向整数指针数组的指针

  • arr 是一个指向整数数组指针数组的指针

这些不都是 int ** 吗?


同一问题的另一个问题:

如果我有一个函数接收 char ** s 作为参数,我想把它作为字符串数组的指针引用,意思是指向字符指针数组的数组指针,但它也是指向 char 的指针吗?


18
int ** arr中,只有一个陈述是正确的:"arr是指向整型指针的指针",另外两个则是错误的。 - alk
5
我认为alk是在讨论int **p(指向指针)和int (*p)[3](指向长度为3的数组的指针)之间的区别:http://stackoverflow.com/questions/25080413/difference-between-pointer-to-pointer-and-pointer-to-2d-array - Klas Lindbäck
7
@BoPersson,我认为你在这里是错误的。 int * a 指向单个整数 - 总是如此。虽然您可以使用指针算术来访问连续布局的整数,但这并不改变 int * a 是指向单个整数的指针的事实。它永远不会是指向数组的指针。 - Support Ukraine
5
只要它能工作,就没有关系。只是术语非常令人困惑。你的动态分配可能看起来正好像一个一维数组 - 但是你的 a 不是一个数组 - 它是指向 int 的指针。当你想动态分配一个二维数组时,差异变得更加明显。在这里,你很快会发现 int **a 并不像 int a[4][4] - Support Ukraine
4
@Random832说:虽然不是很常见,但指向数组的指针仍然有其用途,提醒原帖作者应该知道这一点。而“指向数组的指针”确实是int (*)[N]。这并非小题大做,而是精确明确。在这里不准确只会增加混淆,而你对术语的更改并没有起到任何帮助。如果正确理解,这些注释非常有用。 - too honest for this site
显示剩余25条评论
11个回答

44

不是都是 int ** 吗?

你刚刚发现了类型系统中可能存在的缺陷。你所提到的每个选项都有可能是正确的。这实际上源自程序内存的扁平视图,其中单个地址可以用于引用各种逻辑内存布局。

C程序员自C诞生以来一直采用的方法是制定公约。例如,对于接受此类指针的函数要求size参数,并记录有关内存布局的假设。或者要求使用特殊值终止数组,从而允许指向缓冲区的指针的“锯齿形”缓冲区。


我觉得需要进行一定的澄清。当您查阅其他很好的答案时,您会发现数组绝对不是指针。但在足够的上下文中,它们确实会衰减为指针,这已经导致教学方面存在几十年的错误(但我跑题了)。

我最初写的是以下代码:

void func(int **p_buff)
{
}

//...

int a = 0, *pa = &a;
func(&pa);

//...

int a[3][10];
int *a_pts[3] = { a[0], a[1], a[2] };
func(a_pts);

//...

int **a = malloc(10 * sizeof *a);
for(int i = 0; i < 10; ++i)
  a[i] = malloc(i * sizeof *a[i]);
func(a);
假定 func 和每个代码片段都在单独的翻译单元中编译。 除了我可能写错之外,每个示例都是有效的C语言。当作为参数传递时,这些数组将衰减为“指向指针”的形式。从其参数类型的定义上来看, func 如何知道它究竟传递了什么?答案是它不知道。 p_buff 的静态类型是 int ** ,但它仍然允许 func 间接访问具有极不同的有效类型(部分)对象。

29
你指定的每个选项都可以是正确的,嗯,不是的。只有一个选项是正确的:指向指针的指针类型(pointer to pointer to int)。 - alk
7
实际上,它不能这样做,因为指向指针的指针类型与数组指针不兼容。但可以通过查找表来模拟二维数组,这种做法大多数情况下是可疑的做法。 - Lundin
12
一个指针数组会衰变成一个指向指针的指针。实质上,它指向数组的第一个元素,根据标准,这个元素与整个数组共享一个地址。事实上,甚至不能对数组进行索引(必须先衰变为指针)。标准竭尽所能地混淆了它们之间的区别。那么它们之间有什么明显的区别呢? - StoryTeller - Unslander Monica
3
@KeineLust:1)如果兼容的话,对于int **p,*a [10];sizeof(p) == sizeof(a)将保证为真。但事实并非如此!2)int *a[10]是指针数组,而不是指向数组的指针(请注意,在英语中通常省略“-”)。 - too honest for this site
3
@Olaf - 这有些居高临下。我从未误导过任何人。我在回答中并没有混淆指针和数组指针,并且受到问题中我认为重要的限制,所以我不觉得有必要把标准中有关数组和指针的所有内容都放进来。如果你不同意,那么你的批评已经被记录下来了,你可以自己发布答案。 - StoryTeller - Unslander Monica
显示剩余16条评论

27

声明 int **arr 表示:"将 arr 声明为指向整数的指针的指针"。如果有效,它指向一个指向单个整数对象的单个指针。由于可以使用任一间接级别进行指针算术运算(即*arr 等同于 arr[0]**arr 等同于arr[0][0]),因此该对象可用于访问您所提问的3种情况中的任何一种(即,对于第二种情况,访问指向整数的指针数组,并且对于第三种情况,访问指向整数数组的第一个元素的指针数组),前提是这些指针指向数组的第一个元素...


然而,arr 仍然被声明为指向单个指向单个整数对象的指针。还可以声明指向定义维度的数组的指针。在这里,a 被声明为指向具有10个元素的指针数组的10元素数组的指针:

cdecl> declare a as pointer to array 10 of pointer to array 10 of int;
int (*(*a)[10])[10]

在实践中,数组指针最常用于将多维常量大小的数组传递到函数中,并且用于传递可变长度数组。声明变量为指向数组的指针的语法很少见,因为每当它们被传递到函数中时,使用"未定义大小的数组"类型的参数会更容易一些,所以不需要声明。

void func(int (*a)[10]);

可以使用

void func(int a[][10])

可以传递一个包含多个包含10个整数的数组的多维数组。或者,可以使用typedef减轻头痛。


3
指向数组的指针并不晦涩难懂。它们是传递多维数组的唯一方式。 - too honest for this site
@Olaf 好的,那就这样吧,但是也许指向指针数组的指针有点晦涩 :D - Antti Haapala -- Слава Україні
@AnttiHaapala:确实。对于绝大多数代码来说,这样复杂的结构信号意味着接口设计不良。其中一个原因就是指针没有长度信息。在整个调用树中始终传递每个维度的长度信息是有问题的,因此我们会使用中间的“struct”。对于“指向指针数组”的情况,可以使用2D数组(所有“内部”数组的长度相等)或指向struct {length,pointer to data}或指向后者的struct的指针。 - too honest for this site

9
如何确认以下问题:
  • arr是指向整数的指针的指针
它总是指向指向整数的指针的指针。
  • arr是指向整数指针数组的指针
  • arr是指向整数数组指针数组的指针
它永远不可能是这样。指向整数指针数组的指针应该声明为:
int* (*arr)[n]

看起来你被糟糕的老师/书籍/教程欺骗,使用int**几乎总是不正确的做法,详见这里这里以及(有关数组指针的详细解释)这里
编辑
最后写了一篇详细的文章,解释了什么是数组,什么是查找表,为什么后者不好,以及你应该使用什么:正确分配多维数组

4
“绝不可能”和“不正确的做法”是非常不同的概念。如果你正在编写代码,它们几乎是等价的;但如果你在阅读代码,你需要意识到编写代码的人可能犯错 - IMSoP
2
顺便说一句:声明不应该将arr声明为指向n个整数数组的指针,而是应该声明为指向指针数组的指针。 - Antti Haapala -- Слава Україні
3
指向包含n个整型指针的数组的指针将是int *(*arr)[n] - Antti Haapala -- Слава Україні
2
@MooingDuck 不是,它是一个指向指针的指针。也就是指向指针数组的第一个元素的指针。你示例代码工作的唯一原因是由于数组衰减机制。int ** arr = &t; 会给你一个编译器错误。 - Lundin
1
我一直想写一个社区维基的FAQ,这是一个正在进行中的工作。 - Lundin
显示剩余9条评论

8
仅有变量声明是无法区分这三种情况的。有人仍然可以讨论是否应该使用像 int *x[10] 之类的语句来表示一个指向整数的十个指针数组,但是 int **x 可以 - 由于指针算术运算,以三种不同的方式使用,每种方式都假设不同的内存布局,并有可能产生错误的假设。
考虑以下示例,其中 int ** 以三种不同的方式使用,即 p2p2i_v1 作为指向单个 int 的指针的指针,p2p2i_v2 作为指向整数指针数组的指针的数组,以及 p2p2i_v3 作为指向整数数组的指针的指针。请注意,您不能仅通过类型区分这三个含义,对它们中的任何一个错误访问都会产生不可预测的结果,除了访问第一个元素:
int i1=1,i2=2,i3=3,i4=4;

int *p2i = &i1;
int **p2p2i_v1 = &p2i;  // pointer to a pointer to a single int

int *arrayOfp2i[4] = { &i1, &i2, &i3, &i4 };
int **p2p2i_v2 = arrayOfp2i; // pointer to an array of pointers to int

int arrayOfI[4] = { 5,6,7,8 };
int *p2arrayOfi = arrayOfI;
int **p2p2i_v3 = &p2arrayOfi; // pointer to a pointer to an array of ints

// assuming a pointer to a pointer to a single int:
int derefi1_v1 = *p2p2i_v1[0];  // correct; yields 1
int derefi1_v2 = *p2p2i_v2[0];  // correct; yields 1
int derefi1_v3 = *p2p2i_v3[0];  // correct; yields 5

// assuming a pointer to an array of pointers to int's
int derefi1_v1_at1 = *p2p2i_v1[1];  // incorrect, yields ? or seg fault
int derefi1_v2_at1 = *p2p2i_v2[1]; // correct; yields 2
int derefi1_v3_at1 = *p2p2i_v3[1]; // incorrect, yields ? or seg fault


// assuming a pointer to an array of pointers to an array of int's
int derefarray_at1_v1 = (*p2p2i_v1)[1]; // incorrect; yields ? or seg fault;
int derefarray_at1_v2 = (*p2p2i_v2)[1]; // incorrect; yields ? or seg fault;
int derefarray_at1_v3 = (*p2p2i_v3)[1]; // correct; yields 6;

好的回答。小修正:我认为 *p2p2i_v3[0] 产生了 5,而不是您评论中所建议的 1。 - Peter - Reinstate Monica
@Peter A. Schneider:你是对的,已经更正了答案。 - Stephan Lechner

7
如何判断以下情况之一: 1. arr是一个整数指针的指针 2. arr是一个指向整数指针数组的指针 3. arr是一个指向整数数组指针的指针
你无法确定。它可能是其中任何一种情况,具体取决于你如何分配/使用它。
因此,如果你使用这些代码,请记录你对它们的操作,将大小参数传递给使用它们的函数,并确保在使用之前确定你所分配的内容。

6

指针不会保留它们指向单个对象还是数组中的对象的信息。此外,在指针算术中,单个对象被视为由一个元素组成的数组。

考虑以下声明:

int a;
int a1[1];
int a2[10];

int *p;

p = &a;
//...
p = a1;
//...
p = a2;

在这个例子中,指针 p 处理的是地址。它不知道它存储的地址是指向像 a 这样的单个对象,还是指向只有一个元素的数组 a1 的第一个元素,或者是指向具有十个元素的数组 a2 的第一个元素。

我喜欢你解释中的单指针方法,因为它捕捉了问题的本质。 - Klas Lindbäck
1
指针始终指向单个元素。你的意思是,如果将数组传递给函数,它似乎会在传递过程中“丢失”大小信息。但实际上并非如此,因为数组会隐式转换为指向第一个元素的指针,这就是我评论中第一句话的原因。 - too honest for this site

6

这种类型的

int ** arr;

只有一种有效的解释。它是:
arr is a pointer to a pointer to an integer

如果你所拥有的信息就只有上面那个声明,那么你能知道的也只有这些。也就是说,如果arr可能已经初始化了,它指向另一个指针,而这个指针如果可能已经初始化,则指向一个整数。
假设已经正确初始化,使用它的唯一保证有效的方法是:
**arr = 42;
int a = **arr;

然而,C语言允许您以多种方式使用它。
• arr可以用作指向整数的指针的指针(即基本情况)。
int a = **arr;

• arr可以用作指向整数数组的指针的指针。

int a = (*arr)[4];

• arr可以用作指向整数指针数组的指针
int a = *(arr[4]);

• arr可以用作指向整数数组的指针的数组的指针

int a = arr[4][4];

在最后三种情况下,看起来好像你有一个数组。然而,类型并不是数组。类型始终只是指向整数的指针的指针 - 解引用是指针算术。它与2D数组完全不同。
要知道哪个对手头的程序有效,需要查看初始化arr的代码。 更新 关于问题的更新部分:
如果你有:
void foo(char** x) { .... };

你唯一可以确定的是,**x将提供一个char字符,而*x将提供一个char指针(在这两种情况下,假设x已经被正确初始化)。
如果你想以另一种方式使用x,例如使用x[2]来获取第三个char指针,那么调用者需要初始化x,使其指向至少有3个连续char指针的内存区域。这可以描述为调用foo的契约。

这甚至不是真的。 "正确初始化" 包括一个(超出末尾)指针值,而这样的指针值并不指向任何东西。唯一正确的事情是它具有指向整数的指针到指针的类型。但是,这种类型的有效指针值可以指向内存中零可用元素的位置。 - Ben Voigt

3

在使用指针时,有一个技巧是从右往左阅读:

int** arr = NULL;

你得到的是:arr**int,因此数组是指向指向整数的指针。

int **arr;int** arr; 是一样的。


int s[2] = {}; int* t[3] = {s. s. s}; int ** arr = t; 看看这个!arr 是 _指向整数数组指针的数组指针_! - Mooing Duck
@MooingDuck 不,arr是一个指向指针的指针。表达式"t"从指向int类型的指针数组衰变为指向指针的指针数组,这样一个指向t的第一个元素的值被赋给了arr。你正在将“数组指针”定义为“指向数组第一个元素的指针”。但是“数组指针”已经作为C类型有了意义(没有值),而且它不是那个意思。 - philipxy

3

C语言语法很有逻辑性。在声明中,标识符前面的星号表示该变量类型的指针,两个星号表示该变量类型的指向指针的指针。

在这种情况下, arr 指向整数类型指针的指针

双重指针有几种用法。例如,您可以使用指向指针向量的指针来表示矩阵。该向量中的每个指针都指向矩阵本身的行。

还可以使用它创建二维数组,如下所示:

 int **arr=(int**)malloc(row*(sizeof(int*)));  
 for(i=0;i<row;i++) {
 *(arr+i)=(int*)malloc(sizeof(int)*col); //You can use this also. Meaning of both is same. //
  arr[i]=(int*)malloc(sizeof(int)*col); }

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Paul Ogilvie
@Paul Ogilvie,我没有理解你想说什么!能否请您详细阐述一下? - minigeek
1
严格来说,“你也可以称之为指向指针一维数组的指针”是不正确的。指向int数组的指针看起来像这样:int (*p)[42]。这是一个指向int[42]数组的指针。在这里,int * p仍然是指向int的指针,没有更多或更少。无论p是否指向int数组的第一个元素取决于上下文,但仍然不会使p指向数组,而只会指向其第一个元素。 - alk
我猜@PaulOgilvie的意思是,如果指针为NULL,那么它就不指向任何东西,因此它“指向零个整数”。 - underscore_d
1
@underscore_d,这部分是我所指的。然而,malloc会通过返回一个有效指针来满足对零字节内存的请求,但该指针将指向零个整数。 - Paul Ogilvie

2
int ** arr = NULL;

这句话告诉编译器:arr是一个整数的双指针,并将其赋值为NULL


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