为啥我需要使用 type** 来指向 type*?

Posted

技术标签:

【中文标题】为啥我需要使用 type** 来指向 type*?【英文标题】:Why do I need to use type** to point to type*?为什么我需要使用 type** 来指向 type*? 【发布时间】:2015-07-18 23:25:29 【问题描述】:

这几天我一直在阅读 Learn C The Hard Way,但这是我想真正理解的内容。作者 Zed 写道,char ** 用于“指向(指向 char 的指针)的指针”,并说这是必要的,因为我试图指向二维的东西。

这是网页上写的确切内容

一个 char * 已经是一个“指向 char 的指针”,所以它只是一个字符串。但是,您需要 2 个级别,因为名称是二维的,这意味着您需要 char ** 作为“指向(char 的指针)”类型的指针。

这是否意味着我必须使用一个可以指向二维的变量,这就是为什么我需要两个**

稍微跟进一下,这是否也适用于n维?

这是相关代码

char *names[] =  "Alan", "Frank", "Mary", "John", "Lisa" ;
char **cur_name = names;

【问题讨论】:

仅供参考,char *names[] = /* five string literals */ ; 不是二维数组。它是一个char 指针数组。 如果你想要一个指向some_type类型变量的指针,你需要一个some_type *some_typeintchar * 还是 char *********** 无关紧要。如果您需要指向它的指针,请添加另一个 *。这只是语法。 如果T 是任何类型,那么T * 指向T。这就是类型系统的定义方式。 厌倦了所有垃圾教程和书籍。我发誓我花在不教错误练习上的时间比教正确练习上的时间要多。 @solitude names 在您的示例中是一个指针数组。它不是数组数组,cur_name 也不是。此外,如果您尝试修改“数组”中的一项,代码将崩溃并烧毁,因为以未定义的行为写入字符串文字。一个好的教程会教你将其声明为const char* names[]。没有理由不这样做。 【参考方案1】:

不,该教程质量有问题。不建议继续阅读。

char** 是一个指向指针的指针。它不是二维数组。 它不是指向数组的指针。 它不是指向二维数组的指针。

本教程的作者可能会感到困惑,因为有一种普遍存在的错误做法,即您应该像这样分配动态 2D 数组:

// BAD! Do not do like this!
int** heap_fiasco;
heap_fiasco = malloc(X * sizeof(int*));
for(int x=0; x<X; x++)

  heap_fiasco[x] = malloc(Y * sizeof(int));

然而,这不是一个二维数组,它是一个缓慢的、碎片化的查找表,分配在整个堆中。访问查找表中一项的语法heap_fiasco[x][y] 看起来就像数组索引语法,因此很多人出于某种原因认为这是您分配二维数组的方式。

动态分配二维数组的正确方法是:

// correct
int (*array2d)[Y] = malloc(sizeof(int[X][Y]));

您可以看出第一个不是数组,因为如果您这样做memcpy(heap_fiasco, heap_fiasco2, sizeof(int[X][Y])),代码将崩溃并烧毁。这些项目没有分配在相邻的内存中。

同样memcpy(heap_fiasco, heap_fiasco2, sizeof(*heap_fiasco)) 也会崩溃和烧毁,但出于其他原因:你得到的是指针的大小而不是数组。

虽然memcpy(array2d, array2d_2, sizeof(*array2d)) 可以工作,因为它是一个二维数组。

【讨论】:

@Zaibis 当您想要复制整个“数组”时,memcpy() 的行为将不符合预期,因为sizeof *heap_fiasco 将返回整数指针的大小,因为*heap_fiasco 实际上并不是数组类型。我相信声明 (*array2d)[Y]... 是从 6.7.5.2 开始的。 @Lundin 将二维数组作为参数传递给函数的正确方法是什么?在您的示例中,函数原型将是什么 Y 需要事先知道?您能否扩展您的答案以包括带有函数参数的二维数组?【参考方案2】:

指针花了我一段时间才理解。我强烈推荐绘制图表。

请阅读并理解this part of the C++ tutorial(至少就指针而言,这些图表确实对我有帮助)。

告诉你你需要一个指向二维数组的 char 指针的指针是谎言。您不需要它,但这是一种方法。

内存是连续的。如果你想像 hello 一样将 5 个字符(字母)放在一行中,你可以定义 5 个变量并始终记住使用它们的顺序,但是当你想保存一个单词时会发生什么有6个字母?你定义了更多的变量吗?如果只是将它们按顺序存储在内存中会不会更容易?

所以你要做的就是向操作系统请求 5 个字符(每个字符恰好是一个字节),系统会返回给你一个内存地址,你的 5 个字符序列开始的地方。您获取此地址并将其存储在我们称为指针的变量中,因为它指向您的内存。

指针的问题在于它们只是地址。你怎么知道那个地址存储了什么?是 5 个字符还是 8 个字节的大二进制数?或者它是您加载的文件的一部分?你怎么知道的?

这就是像 C 这样的编程语言试图通过给你类型来帮助你的地方。类型告诉你变量存储的是什么,指针也有类型,但它们的类型告诉你指针指向什么。因此,char * 是一个指向内存位置的指针,该位置保存单个charchars 序列。可悲的是,您需要记住自己有多少chars。通常,您将该信息存储在一个变量中,以提醒您那里有多少个字符。

那么当你想要一个二维数据结构时,你如何表示呢?

最好用一个例子来解释。让我们做一个矩阵:

1  2  3  4
5  6  7  8
9 10 11 12

它有 4 列和 3 行。我们如何存储它?

好吧,我们可以制作 3 个序列,每个序列有 4 个数字。第一个序列是1 2 3 4,第二个是5 6 7 8,第三个也是最后一个序列是9 10 11 12。因此,如果我们要存储 4 个号码,我们将要求系统为我们保留 4 个号码并给我们一个指向它们的指针。这些将是指向数字的指针。然而,由于我们需要其中的 3 个,我们将要求系统给我们 3 个指向 pointers numbers 的指针。

这就是你最终得到建议的解决方案的方式......

另一种方法是意识到您需要 4 乘以 3 的数字,然后要求系统将 12 个数字按顺序存储。但是,您如何访问第 2 行和第 3 列中的数字?这就是数学的用武之地,但让我们在我们的示例中尝试一下:

1  2  3  4
5  6  7  8
9 10 11 12

如果我们将它们彼此相邻存储,它们将如下所示:

offset from start:  0  1  2  3    4  5  6  7    8   9  10  11   
numbers in memory: [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]

所以我们的映射是这样的:

row | column | offset | value
 1  |   1    |   0    |   1
 1  |   2    |   1    |   2
 1  |   3    |   2    |   3
 1  |   4    |   3    |   4
 2  |   1    |   4    |   5
 2  |   2    |   5    |   6
 2  |   3    |   6    |   7
 2  |   4    |   7    |   8
 3  |   1    |   8    |   9
 3  |   2    |   9    |  10
 3  |   3    |  10    |  11
 3  |   4    |  11    |  12

我们现在需要制定一个简单易用的公式来将行和列转换为偏移量...当我有更多时间时,我会回到它...现在我需要回家(对不起)...

编辑:我有点晚了,但让我继续。要从行和列中找到每个数字的偏移量,您可以使用以下公式:

offset = (row - 1) * 4 + (column - 1)

如果你注意到这里的两个-1 并考虑一下,你就会明白这是因为我们的行和列编号以 1 开头,我们必须这样做,这就是为什么计算机科学家更喜欢从零开始的原因偏移量(因为这个公式)。但是,使用 C 语言中的指针,当您使用多维数组时,语言本身会为您应用此公式。因此,这是另一种方式。

【讨论】:

【参考方案3】:

从您的问题中,我了解到您在问为什么需要 char ** 作为声明为 *names[] 的变量。所以答案是当你简单地写names[]时,它是数组的语法,而数组基本上是一个指针。

所以当你写 *names[] 时,这意味着你指向一个数组。由于数组基本上是一个指针,所以这意味着您有一个指向指针的指针,这就是为什么如果您编写编译器不会抱怨

char ** cur_name = 名称;

在上面一行中,你声明了一个指向字符指针的指针,然后用指向数组的指针初始化它(记住数组也是指针)。

【讨论】:

以上是关于为啥我需要使用 type** 来指向 type*?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 getConstructor 反射 API 需要 int.class 参数?

为啥 Oracle odbc 驱动程序没有为整数和数字类型提供 TYPE_NAME?

IOS CoreData,使用哪个 NSManagedObjectContextConcurrencyType,为啥?

为啥 ostream::write() 在 C++ 中需要“const char_type*”而不是“const void*”?

为啥`object`是`type`的实例而`type`是`object`的实例?

为啥`object`是`type`的实例而`type`是`object`的实例?