【问题标题】:Confusing pointers in CC中令人困惑的指针
【发布时间】:2010-01-21 11:46:24
【问题描述】:

我有不止一个疑问,所以请多多包涵。 谁能告诉我为什么这段代码会失败?

#include<stdio.h>
void main(int argc,char **argv) /*assume program called with arguments aaa bbb ccc*/
{
    char **list={"aaa","bbb","ccc"};

    printf("%s",argv[1]);/*prints aaa*/
    printf("%s",list[1]); /*fails*/ 
}

我认为它与指向指针的指针有关,我不太清楚。所以我尝试了:

#include<stdio.h>
void main()
{
char **list={"aaa","bbb","ccc"};
char *ptr;
ptr=list;
printf("%s",ptr);/*this prints the first string aaa*/
    /* My second question is how do i increment the value
       of ptr so that it points to the second string bbb*/
}

char *list[]char **list 有什么区别,在什么情况下都适合使用? 让我感到困惑的另一件事是 argv 特殊?当我将char **list 传递给另一个函数时,假设它可以让我以使用argv 的方式访问内容,它也失败了。

我意识到过去有人问过类似的问题,但我似乎找不到我需要的东西。如果可以的话,有人可以发布相关链接。

【问题讨论】:

    标签: c pointers


    【解决方案1】:

    您应该使用char *list[]={"aaa","bbb","ccc"}; 而不是char **list={"aaa","bbb","ccc"};。您使用char* list[] = {...}; 声明指针数组,但您使用char** 将指针传递给一个或多个指向函数的指针。

    • T* x[] = 指针数组
    • T** x = 指向指针的指针

    附:回复 ejohn:我只能想到一种用途来创建指向指针的指针(作为实际声明的变量,而不是作为函数参数或由一元 &amp; 运算符创建的临时变量):handle。简而言之,句柄是指向指针的指针,其中句柄归用户所有,但它指向的指针可以根据操作系统或库的需要进行更改。

    句柄在旧的 Mac OS 中被广泛使用。由于 Mac OS 是在没有虚拟内存技术的情况下开发的,所以防止堆快速碎片化的唯一方法是在几乎所有内存分配中使用句柄。这让操作系统可以根据需要移动内存以压缩堆并打开更大、连续的空闲内存块。

    事实是,这种策略充其量只是“少吸”。有很多缺点:

    • 一个常见的错误是程序员将句柄取消引用到一个指针,并将该指针用于多个函数调用。如果这些函数调用中的任何一个移动了内存,那么指针就有可能变得无效,并且取消引用它会破坏内存并可能使程序崩溃。这是一个潜在的错误,因为取消引用坏指针不会导致总线错误或分段错误,因为内存本身仍然存在并且可以访问;它只是不再被您正在使用的对象使用。
    • 因此,编译器必须格外小心,并且无法进行一些公共子表达式消除优化(公共子表达式是指针的句柄取消引用)。
    • 因此,为了确保正确执行,几乎所有通过句柄进行的访问都需要两次间接访问,而不是一次使用普通的旧指针。这可能会影响性能。
    • 操作系统或任何库提供的每个 API 都必须指定它是否可以“移动内存”。如果您调用其中一个函数,则通过句柄获得的所有指针现在都无效。没有办法让 IDE 为您执行此操作或检查您,因为移动内存调用和无效的指针甚至可能不在同一个源文件中。
    • 性能变得不确定,因为您永远不知道操作系统何时会暂停以压缩您的内存(这涉及到 很多 memcpy() 工作)。
    • 多线程变得困难,因为一个线程可能会在另一个线程正在执行或阻塞时移动内存,从而使其指针无效。请记住,几乎所有内存分配都必须使用句柄,以防止堆碎片化,因此线程仍然可能需要通过句柄访问内存,即使它们不使用任何 Mac OS API。
    • 存在用于锁定和解锁句柄指向的指针的函数调用,但是,过多的锁定会损害性能并导致堆碎片化。

    可能还有几个我忘记了。请记住,所有这些缺点仍然比只使用指针和快速分割堆更容易接受,尤其是在第一台只有 128K 内存的 Mac 上。这也让我们了解了为什么 Apple 非常乐意放弃所有这些并转向 BSD,然后他们就有机会,一旦他们的整个产品线都有内存管理单元。

    【讨论】:

    • 所以 char** 仅用于将指针传递给函数?如果那是真的,那肯定会消除我的疑虑
    • 指针的指针非常有用,例如,当您需要一个二维“数组”时,行数和列数在编译时未知。
    • @Alok:我认为这更像是指向malloc() 数组的指针,即T** ptrList = (T**)malloc(sizeof(T*)*N);T* ptrArray[N]; T** ptrList = ptrArray;T* ptr = (T*)malloc(sizeof(T)); T** ptrToPtr = *ptr; 更相似,但我明白你的意思。
    • @Mike:回到过去。
    • 再见。必须知道我不能从中断服务例程中调用哪些例程已经够糟糕了。
    【解决方案2】:

    首先,让我们摆脱那些挑剔的东西。 main 返回 int,不是 void。除非您的编译器文档明确声明它支持void main(),否则请使用int main(void)int main(int argc, char **argv)

    现在让我们退后一步,谈谈指针和数组之间的区别。首先要记住的是数组和指针是完全不同的东西。您可能在某处听说或读到数组只是一个指针。这是不正确的。在大多数情况下,数组 表达式 将其类型从“T 的 N 元素数组”隐式转换为“指向 T 的指针”(类型 decays 为指针类型) 并将其值设置为指向数组中的第一件事,例外情况是当数组表达式是 sizeof 或地址 (&amp;) 运算符的操作数时,或者当数组表达式是字符串时用于初始化另一个数组的文字。

    数组是一块内存,大小可以容纳 N 个类型为 T 的元素;指针是一块内存,其大小可以保存 T 类型的单个值的地址。您不能将新值分配给数组对象;即,以下是不允许的:

    int a[10], b[10];
    a = b;
    

    注意字符串字面量(如“aaa”)也是一个数组表达式;类型是 char 的 N 元素数组(C++ 中的 const char),其中 N 是字符串的长度加上终止的 0。字符串文字具有静态范围;它们在程序启动时分配并一直存在到程序退出。它们也是不可写的(尝试修改字符串文字的内容会导致未定义的行为)。例如,表达式“aaa”的类型是具有静态范围的 char 的 4 元素数组。与其他数组表达式一样,字符串文字在大多数情况下从数组类型衰减为指针类型。当你写类似的东西时

    char *p = "aaa";
    

    数组表达式“aaa”从char [4]衰减到char *,其值为数组第一个'a'的地址;该地址随后被复制到p

    如果使用字面量来初始化一个 char 数组,那么:

    char a[] = "aaa";
    

    然后类型不转换;文字仍然被视为一个数组,并且数组的 contents 被复制到 a (并且 a 隐式调整大小以容纳字符串内容加上 0 终止符)。结果大致相当于写了

    char a[4];
    strcpy(a, "aaa");
    

    T a[N] 类型的数组表达式是sizeof 运算符的操作数时,结果是整个数组的大小(以字节为单位):N * sizeof(T)。当它是 address-of (&amp;) 运算符的操作数时,结果是指向整个数组的指针,而不是指向第一个元素的指针(实际上,它们是相同的,但类型不同):

    声明:Ta[N]; 表达式类型“衰减”为值 ---------- ---- ----------- ------ a T [N] T * a[0] 的地址 &a T (*)[N] a 的地址 sizeof a size_t a 中的字节数 (N * sizeof(T)) a[i] a[i] 的 T 值 &a[i] T * a[i] 的地址 sizeof a[i] size_t a[i] 中的字节数 (sizeof (T))

    请注意,数组表达式a 衰减为类型T *,或指向T 的指针。这与表达式&amp;a[0] 的类型相同。这两个表达式都产生数组中第一个元素的地址。表达式&amp;a 的类型为T (*)[N],或指向T 的N 元素数组的指针,它产生数组本身的地址,而不是第一个元素。由于数组的地址与数组的第一个元素的地址相同,a&amp;a&amp;a[0] 都产生相同的,但表达式不同所有相同的类型。这在尝试将函数定义与函数调用匹配时很重要。如果您想将 array 作为参数传递给函数,例如

    int a[10];
    ...
    foo(a);
    

    那么对应的函数定义一定是

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

    foo 接收的是一个指向 int 的指针,而不是一个 int 数组。请注意,您可以将其称为foo(a)foo(&amp;a[0])(甚至foo(&amp;v),其中v 是一个简单的int 变量,尽管如果foo 期望一个会导致问题的数组)。请注意,在函数参数声明的上下文中,int a[]int *a 相同,但在此上下文中为真。坦率地说,我认为int a[] 表单是造成对指针、数组和函数的许多困惑思考的原因,应该不鼓励使用它。

    如果你想将一个指向数组的指针传递给一个函数,比如

    int a[10];
    foo(&a);
    

    那么对应的函数定义一定是

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

    当你想引用一个特定的元素时,你必须在应用下标之前取消引用指针

    for (i = 0; i < 10; i++)
      (*p)[i] = i * i;
    

    现在让我们把活动扳手扔到工作中并为数组添加第二个维度:

    声明:Ta[M][N]; 表达式类型“衰减”为值 ---------- ---- ----------- ------ a[0] 的 T [M][N] T (*)[N] 地址 &a T (*)[M][N] a 的地址 sizeof a size_t a (M * N * sizeof(T)) 中的字节数 a[i] T [N] T * a[i][0] 的地址 &a[i] T (*)[N] a[i] 的地址 sizeof a[i] size_t a[i] 中的字节数 (N * sizeof(T)) a[i][j] a[i][j] 的 T 值 &a[i][j] T * a[i][j] 的地址

    请注意,在这种情况下,aa[i] 都是数组表达式,因此它们各自的数组类型在大多数情况下都会衰减为指针类型; a 将从“T 的 N 元素数组的 M 元素数组”类型转换为“T 的 N 元素数组的指针”,a[i] 将从“T 的 N 元素数组”类型转换为“指向 T 的指针”。同样,a&amp;aa[0]&amp;a[0]&amp;a[0][0] 都将产生相同的 (数组开头的地址),但不是所有相同的类型。如果要将二维数组传递给函数,例如:

    int a[10][20];
    foo(a);
    

    那么对应的函数定义一定是

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

    请注意,这与将指针传递给一维数组相同(除了示例中的数组大小不同)。但是,在这种情况下,您可以对指针应用下标,例如

    for (i = 0; i < 10; i++)
      for (j = 0; j < 20; j++)
        p[i][j] = i * j;
    

    在这种情况下,您不必显式取消引用 p,因为表达式 p[i] 隐式地引用它 (p[i] == *(p + i))。

    现在让我们看看指针表达式:

    声明:T *p; 表达式类型值 ---------- ---- ------ p T * 另一个 T 类型对象的地址 *p T 类型的另一个对象的 T 值 &p T ** 指针地址 sizeof p size_t 指针中的字节数(取决于类型和平台, 在常见的桌面架构上介于 4 到 8 之间) sizeof *p size_t T 中的字节数 sizeof &p size_t 指向指针的字节数(同样,取决于 在类型和平台上)

    这一切都很简单。指针类型保存另一个 T 类型对象的地址;取消引用指针 (*p) 会产生该地址的值,并获取指针的地址 (&amp;p) 会产生指针对象的位置(指向指针的指针)。将sizeof 应用于指针值将产生指针中的字节数,而不是指针指向的字节数。

    现在,假设您已经做到了这一点并且还没有因为厌倦而死,让我们看看所有这些如何应用于您的代码。

    您想要创建一个指向 char 的指针的 数组 并使用三个字符串文字对其进行初始化,因此您可以将其声明为

    char *list[] = {"aaa", "bbb", "ccc"};
    

    list 数组的大小已隐式调整为容纳 3 个 char * 类型的元素。尽管字符串字面量“aaa”、“bbb”和“ccc”出现在初始化程序中,但它们并没有用于初始化 char 数组;因此,它们从char [4] 类型的表达式衰减为char * 类型。这些指针值中的每一个都被复制到list 的元素中。

    当您将list 传递给函数时,例如

    foo(list);
    

    列表的类型从“指向char的4元素数组”(char *[4])衰减为“指向指向char的指针”(char **),所以接收函数必须有一个定义

    void foo(char **p) {...}
    

    由于下标是根据指针算术定义的,您可以在指针上使用下标运算符就好像它是char *的数组:

    for (i = 0; i < 3; i++)
      printf("%s\n", p[i]);
    

    顺便说一句,这就是main 接收argv 的方式,作为指向char 的指针(char **),而不是作为指向char 的指针数组。请记住,就函数参数声明而言,a[]*a 相同,因此char *argv[]char **argv 相同。

    现在,因为我似乎无法停止输入并重新开始工作(追逐死锁不好玩),让我们来探索使用指针和动态分配的内存.

    如果您想在运行时动态分配列表(即,您不会提前知道列表中有多少个字符串),您可以将 list 声明为指向 char 的指针,然后调用malloc 为它实际分配内存:

    char **list;
    size_t number_of_strings;
    ...
    list = malloc(number_of_strings * sizeof *list);
    list[0] = "aaa";
    list[1] = "bbb";
    list[2] = "ccc";
    ...
    

    由于这些是赋值而不是初始化,文字表达式会衰减为指向 char 的指针,因此我们将“aaa”、“bbb”等的地址复制到list。在这种情况下,list 不是数组类型;它只是一个指向分配在其他地方的一块内存的指针(在这种情况下,来自 malloc 堆)。同样,由于数组下标是根据指针算术定义的,因此您可以将下标运算符应用于指针值就好像它是一个数组。表达式list[i] 的类型是char *。无需担心隐式转换;如果你把它传递给一个函数

    foo(list)
    

    那么函数定义就是

    void foo(char **list) {...}
    

    你会为列表下标就好像它是一个数组。

    pssst……他说完了吗?

    是的,我想他已经完成了。

    【讨论】:

    • 感谢您花时间写这么长的解释!有些东西告诉我,这不是我最后一次提到这个,我相信这会帮助很多像我这样的 c 新手。至于我,我只是将其保存为 c_pointers_explained.txt 。
    • @John Bode:很好的答案。
    【解决方案3】:

    char **x 指向一个 char 指针数组,但是这可能不是您的编译器在内存中存储 {"aaa","bbb","ccc"} 的方式。无论编译器如何存储指针数组,char *x[] 都会导致生成正确的代码。

    【讨论】:

    • 是的,当我尝试假设三个字符串连续存储时,我得到了垃圾值。谢谢你澄清。
    【解决方案4】:

    学习 C 复杂性的最佳资源是 Peter van der Linden (http://www.amazon.co.uk/Expert-Programming-Peter-van-Linden/dp/0131774298) 所著的 Expert C Programming 一书。

    这本书的名字有误导性,因为我认为它很容易被初学者阅读。

    【讨论】:

    • 它是如何回答这个问题的?
    • 这实际上很好地回答了这个问题。本书有一整章专门讨论 C 中指针和数组之间的区别,解释了它们存在的原因以及它们的后果。大约 15 年前我读过它,我记得那是我第一次真正理解这里发生了什么。
    • OP 提出了一个关于指针的问题,而不是关于书籍的问题。如果你不能或不想回答这个问题 - 不要打扰
    【解决方案5】:

    "...假设它有事可做 用指向指针东西的指针, 我不是很清楚。”

    How does an array of pointers to pointers work?

    【讨论】:

      猜你喜欢
      • 2019-04-25
      • 2016-06-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多