在C中混淆指针
我有一个以上的疑问所以请耐心等待。 有人能告诉我为什么这段代码失败了吗?
#include 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 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
的方式访问内容,它也失败了。
我意识到过去曾经问过类似的问题,但我似乎无法找到我需要的东西。 若有,有人可以发布相关链接。
你应该使用char *list[]={"aaa","bbb","ccc"};
而不是char **list={"aaa","bbb","ccc"};
。 你使用char* list[] = {...};
声明指针数组,但是使用char**
将指针传递给一个或多个指向函数的指针。
-
T* x[]
=指针数组 -
T** x
=指向指针的指针
PS响应ejohn:我只能想到一个用于创建指针指针的用法(作为实际声明的变量,而不是函数参数或由一元&
运算符创建的临时变量): 句柄 。 简而言之,句柄是指向指针的指针,其中句柄e由用户拥有,但它指向的指针可以根据需要由OS或库改变。
在旧的Mac OS中广泛使用句柄。 由于Mac OS是在没有虚拟内存技术的情况下开发的,因此保持堆快速碎片化的唯一方法是在几乎所有内存分配中使用句柄。 这让操作系统根据需要移动内存来压缩堆并打开更大,连续的可用内存块。
事实是,这种策略充其量只是“少吸”。 有一大堆缺点:
- 一个常见的错误是程序员取消引用指针的句柄,并将该指针用于多个函数调用。 如果这些函数调用中的任何一个移动了内存,则指针有可能变为无效,并且取消引用它会破坏内存并可能导致程序崩溃。 这是一个阴险的错误,因为取消引用坏指针不会导致总线错误或分段错误,因为内存本身仍然存在且可访问; 它刚好不再被您使用的对象使用。
- 出于这个原因,编译器必须格外小心,并且不能采用一些常见的Subexpression Elimination优化(公共子表达式是对指针的句柄解引用)。
- 因此,为了确保正确执行,几乎所有通过句柄的访问都需要两次间接访问,而不是一次使用普通旧指针。 这可能会影响性能。
- 操作系统或任何库提供的每个API都必须指定它是否可能“移动内存”。 如果你调用了其中一个函数,那么通过句柄获得的所有指针现在都是无效的。 没有办法让IDE为您执行此操作或检查您,因为移动内存调用和变为无效的指针可能甚至不在同一源文件中。
- 性能变得不确定,因为你永远不知道操作系统何时会暂停以压缩你的内存(这涉及很多
memcpy()
工作)。 - multithreading变得困难,因为一个线程可以移动内存而另一个线程正在执行或阻塞,使其指针无效。 请记住,几乎所有内存分配都必须使用句柄来防止对堆进行分段,因此即使线程不使用任何Mac OS API,线程仍可能需要通过句柄访问内存。
- 有一些函数调用来锁定和解锁由句柄指向的指针,但是,过多的锁定会损害性能并使堆碎片化。
我忘记了可能还有几个。 请记住,所有这些缺点仍然比仅使用指针和快速分割堆更加可口,特别是在第一台只有128K RAM的Mac上。 这也让我们深入了解为什么Apple完全乐于抛弃所有这些并转向BSD然后他们有机会,一旦他们的整个产品线都有内存管理单元。
首先,让我们把挑剔的东西放在一边。 main
返回int ,而不是void。 除非您的编译器文档明确声明它支持void main()
,否则请使用int main(void)
或int main(int argc, char **argv)
。
现在让我们回过头来讨论指针和数组之间的差异。 要记住的第一件事是数组和指针是完全不同的东西 。 您可能已经听过或读过某个数组只是指针的地方; 这是不正确的。 在大多数情况下,数组表达式的类型将从“N元素数组T”隐式转换为“指向T的指针”(类型衰减为指针类型),其值设置为指向数组中的第一个内容,例外情况是数组表达式是sizeof
或address-of( &
)运算符的操作数,或者数组表达式是用于初始化另一个数组的字符串文字。
数组是一个存储块,其大小可以容纳类型为T的N个元素; 指针是一个内存块,其大小可以保存类型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";
然后类型没有转换; 文字仍被视为一个数组,并且数组的内容被复制到a
(并且a
是隐式大小以保存字符串内容加上0终结符)。 结果大致相当于写作
char a[4]; strcpy(a, "aaa");
当类型为T a[N]
的数组表达式是sizeof
运算符的操作数时,结果是整个数组的大小(以字节为单位):N * sizeof(T)。 当它是地址( &
)运算符的操作数时,结果是指向整个数组的指针,而不是指向第一个元素的指针(实际上,这些是相同的值 ,但类型不同):
声明:T a [N]; 表达类型“衰变”为价值 ---------- ---- ----------- ------ a [0]的T [N] T *地址 &a的T(*)[N]地址 sizeoft中的size_t字节数 (N * sizeof(T)) a [i] T [i]的值 &[i] T *地址[i] sizeof a [i] size_t [i]中的字节数(sizeof(T))
请注意,数组表达式a
衰减为T *
类型或指向T的指针。这与表达式&a[0]
类型相同。 这两个表达式都会产生数组中第一个元素的地址。 表达式&a
是T (*)[N]
,或指向T的N元素数组的指针,它产生数组本身的地址,而不是第一个元素。 由于数组的地址与数组的第一个元素的地址相同,因此a
, &a
和&a[0]
都会产生相同的值 ,但表达式的类型并不完全相同。 这在尝试将函数定义与函数调用匹配时很重要。 如果要将数组作为参数传递给函数,例如
int a[10]; ... foo(a);
那么相应的函数定义必须是
void foo(int *p) { ... }
foo
收到的是指向int的指针,而不是int数组。 请注意,您可以将其称为foo(a)
或foo(&a[0])
(甚至foo(&v)
,其中v
是一个简单的int变量,但是如果foo
期望一个会导致问题的数组)。 请注意,在函数参数声明的上下文中, int a[]
与int *a
相同,但仅在此上下文中为true。 坦率地说,我认为int a[]
forms导致很多关于指针,数组和函数的混淆思考,应该不鼓励使用它。
如果要将指向数组的指针传递给函数,例如
int a[10]; foo(&a);
那么相应的函数定义必须是
void foo(int (*p)[10]) {...}
当您想要引用特定元素时,必须在应用下标之前取消引用指针:
for (i = 0; i < 10; i++) (*p)[i] = i * i;
现在让我们把一个猴子扳手投入到工作中并为数组添加第二个维度:
声明:T a [M] [N]; 表达类型“衰变”为价值 ---------- ---- ----------- ------ a [0]的T [M] [N] T(*)[N]地址 &a的T(*)[M] [N]地址 sizeoft a中的字节数(M * N * sizeof(T)) [i] [0]的[i] T [N] T *地址 和[i]的[i] T(*)[N]地址 sizeof a [i] size_t [i]中的字节数(N * sizeof(T)) a [i] [j] a [i] [j]的T值 &a [i] [j] T [*] [j]的地址
注意,在这种情况下, a
和a[i]
都是数组表达式,因此在大多数情况下它们各自的数组类型将衰减为指针类型; a
将从“T的N元素数组的M元素数组”转换为“指向T的N元素数组的指针”,并且a[i]
将从“N元素数组T”转换为“指向T“的指针。 同样, a
, &a
, a[0]
, &a[0]
和&a[0][0]
都将产生相同的值 (数组开头的地址),但不是所有相同的类型 。 如果要将二维数组传递给函数,例如:
int a[10][20]; foo(a);
那么相应的函数定义必须是
void foo(int (*p)[20]) {...}
请注意,这与将指针传递给1-d数组(除了示例中数组的大小不同)相同。 但是,在这种情况下,您可以将下标应用于指针,例如
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 T *地址 * p T类型的另一个对象的T值 &p T **指针的地址 sizeof p size_t指针中的字节数(取决于类型和平台, 常见桌面架构上4到8之间的任何位置) sizeof * p size_t T中的字节数 sizeof&p size_t指向指针的字节数(再次,取决于 在类型和平台上)
这一切都非常简单。 指针类型保存另一个类型为T的对象的地址; 取消引用指针( *p
)产生该地址的值,并获取指针的地址( &p
)产生指针对象的位置(指向指针的指针)。 将sizeof
应用于指针值将产生指针中的字节数,而不是指针指向的字节数。
现在,假设你已经做到这一点并且还没有死于ennui,让我们看看所有这些都适用于你的代码。
你想要创建一个指向char的指针数组并用三个字符串文字初始化它,所以你要声明它为
char *list[] = {"aaa", "bbb", "ccc"};
list
数组隐式resize以容纳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 ......他做完了吗?
是的,我认为他已经完成了。
char ** x指向一个char指针数组,但这可能不是你的编译器在内存中存储{“aaa”,“bbb”,“ccc”}的方式。 无论编译器如何存储指针数组,char * x []都将生成正确的代码。
学习C的复杂性的最佳来源是Peter van der Linden的书籍Expert C Programming( http://www.amazon.co.uk/Expert-Programming-Peter-van-Linden/dp/0131774298 )。
这本书的名称具有误导性,因为我认为它很容易被初学者阅读。
“…假设它与指针指针有关,我不明白这一点。”
指针数组如何工作?