本文代码编写编译运行的环境:[Mac-10.7.1 Lion Intel-based]
Q: 有的时候总是发现一个数组的字符串可以修改,但是如果使用字符串字面量就不能修改,这是为什么?
#include <stdio.h>
int main()
{
char buf[] = "hello";
char *str = "hello";
buf[0] = 'a';
str[0] = 'a';
return 0;
}
代码运行:
而且是运行到str[0] = 'a'; 的时候挂掉的。
A: 这是因为buf数组的数据存放在栈中,而str指向的字符串数据保存在全局只读数据区域。str[0] = 'a'; 修改了不能被修改的数据块。
Q: 怎么才能知道char *str = "hello";这句代码中的hello字符串被保存在全局只读数据区域里呢?
A: 我们可以使用strings命令来得到上面代码编译成的可执行文件里面的可打印字符串。
假设上面的代码保存为char_string.c, 编译: gcc -o char_string char_string.c
先看下strings程序的作用:
接着用strings char_string得到char_string可执行文件内部的可打印字符串:
Q: 那么怎么证明char buf[] = "hello"; 中buf保存的数据hello在栈中呢?
A: 使用gcc -S char_string.c得到它的汇编形式:
movb L_.str(%rip), %al
movb %al, -14(%rbp)
movb L_.str+1(%rip), %al
movb %al, -13(%rbp)
movb L_.str+2(%rip), %al
movb %al, -12(%rbp)
movb L_.str+3(%rip), %al
movb %al, -11(%rbp)
movb L_.str+4(%rip), %al
movb %al, -10(%rbp)
movb L_.str+5(%rip), %al
movb %al, -9(%rbp)
leaq L_.str(%rip), %rax
movq %rax, -24(%rbp)
movb $97, -14(%rbp)
其中L_.str为:
L_.str:
.asciz "hello"
可以看到,上面的汇编代码中的%rbp即为和堆栈基址相对应的寄存器。
Q: 常常看到,如果arr是个数组,arr[i]和*(arr + i)是等同的, 这里的i可以为负数吗?
A: 是的。数组就是个数据块,至于是取arr更高地址还是更低地址的数据,这有程序员决定。当然,arr表示数组的初始地址,取比它更低的地址可能不是程序员的本来意图,小心为之。
#include <stdio.h>
#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));
int main()
{
int arr[] = {1, 2};
int n = 3;
PRINT_D(arr[0])
PRINT_D(arr[-1])
return 0;
}
编译运行:
可以看到,arr[0]打印预期的1, arr[-1]输出数组arr地址更低4个字节(笔者的平台的int是4个字节)空间数据的整形值。依据栈的原理,变量n正好保存在这个位置,所以会输出3.
Q: 经常看到多维数组的形式,发现它的形式还有关于多维数组相关变量的地址,不是很好地分析,如何很好地认识?
A: 如下例子:
#include <stdio.h>
#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));
int main (int argc, const char * argv[])
{
int arr[2][3] = {1, 2, 3, 4, 5, 6};
PRINT_P(arr)
PRINT_P(&arr)
PRINT_P(arr[0])
PRINT_P(&arr[0])
PRINT_P(arr[1])
PRINT_P(&arr[1])
return 0;
}
运行结果:
可以看到, arr和&arr是一致的,因为arr就是代表此数据的地址;它的地址依然和它一样;而且arr的虚拟地址是在编译阶段即可确定。arr[0]也是一个地址,因为arr是二维数组,所以&arr[0]和arr[0]也是一致的;arr[0]即是arr初始一块数据的地址,所以它们也是一致的;同理可分析,arr[1]和&arr[1].
Q: 对于多维数组的形式,有的时候也有些不好理解;先拿简单的一维数组来说,数组作为参数的形式是怎么样的?
A: 数组作为参数,只需要将首地址传入即可,当然一般还可能需要数组元素个数的参数。形如:
#include <stdio.h>
#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));
#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));
void print_arr(int *arr, int size)
{
int i = 0;
for(; i < size; ++i)
PRINT_D(arr[i])
}
int main (int argc, const char * argv[])
{
int arr[] = {1, 2, 3};
print_arr(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
因为数组元素是整形的,所以数组地址为整形指针,即为上面的int *, 还有个参数size表示数组的大小。
Q: 既然数组的个数可以用sizeof(arr) / sizeof(arr[0])来表示,那么参数size不就是不必要的吗,print_arr函数里面直接用这个表达式不就可以得到数组的大小了么?
A: 测试下。
#include <stdio.h>
#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));
#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));
void print_arr(int *arr)
{
int i = 0;
for(; i < sizeof(arr) / sizeof(arr[0]); ++i)
PRINT_D(arr[i])
}
int main (int argc, const char * argv[])
{
int arr[] = {1, 2, 3};
print_arr(arr);
return 0;
}
运行:
Q: 为何数组arr的个数不正确了?
A: 我们可以看看print_arr函数的汇编形式:
0x0000000100000dd0 <print_arr+0>: push %rbp
0x0000000100000dd1 <print_arr+1>: mov %rsp,%rbp
0x0000000100000dd4 <print_arr+4>: sub $0x10,%rsp
0x0000000100000dd8 <print_arr+8>: mov %rdi,-0x8(%rbp)
0x0000000100000ddc <print_arr+12>: movl $0x0,-0xc(%rbp)
0x0000000100000de3 <print_arr+19>: movslq -0xc(%rbp),%rax
0x0000000100000de7 <print_arr+23>: cmp $0x2,%rax
0x0000000100000deb <print_arr+27>: jae 0x100000e14 <print_arr+68>
0x0000000100000ded <print_arr+29>: lea 0xa4(%rip),%rdi # 0x100000e98
0x0000000100000df4 <print_arr+36>: movslq -0xc(%rbp),%rax
0x0000000100000df8 <print_arr+40>: mov -0x8(%rbp),%rcx
0x0000000100000dfc <print_arr+44>: mov (%rcx,%rax,4),%esi
0x0000000100000dff <print_arr+47>: mov $0x0,%al
0x0000000100000e01 <print_arr+49>: callq 0x100000e6e <dyld_stub_printf>
0x0000000100000e06 <print_arr+54>: mov %eax,-0x10(%rbp)
0x0000000100000e09 <print_arr+57>: mov -0xc(%rbp),%eax
0x0000000100000e0c <print_arr+60>: add $0x1,%eax
0x0000000100000e0f <print_arr+63>: mov %eax,-0xc(%rbp)
0x0000000100000e12 <print_arr+66>: jmp 0x100000de3 <print_arr+19>
0x0000000100000e14 <print_arr+68>: add $0x10,%rsp
0x0000000100000e18 <print_arr+72>: pop %rbp
0x0000000100000e19 <print_arr+73>: retq
可以看到第七行位置的cmp指令和第八行的jae指令,表示如果循环变量i大于或者等于2,那么跳到结束位置。也就是说,这个函数里面,把arr数组大小看成了2, 这是为什么呢?这是因为编译器编译print_arr函数代码时,根本不知道传入int * arr参数的到底是个数组还是指针,所以sizeof(arr) / sizeof(arr[0])得到的是sizeof(int *) / sizeof(int)的值(笔者平台得到的是2)。其实这也是数组名作为参数的一个可能引发错误的地方。
Q: 那么如果把参数形式int *arr改为int arr[]就能将arr当成数组了,代码就会正确执行?
A: 很可惜,c语言的语法决定了任何代码都会编译成确定指令的东西,和上面说的一样,print_arr依然不知道外部传入参数arr的数组或者指针到底有多大,sizeof(arr) / sizeof(arr[0])最终又被得到一个诡异的数据。
Q: 那该怎么办?
A: 就另外传入一个参数为传入数组的元素个数即可。
Q: 关于二维数组,经常看到一些很诡异的样式,到底怎么很好地理解?
A: 形如如下代码:
#include <stdio.h>
#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));
#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));
int main (int argc, const char * argv[])
{
int arr[] = {1, 2};
int (*p_arr)[2] = &arr;
PRINT_D(**p_arr)
return 0;
}
运行结果:
可以看到, p_arr是一个指针,它指向一个包含2个整形元素的数组;arr数组正好满足要求,所以p_arr可以指向它。这里需要注意,p_arr的值是arr的地址,所以使用它的时候需要解引用。*p_arr表示数组arr, *(*p_arr)表示*arr, 也就是arr[0], 所以最后输出数值1.
Q: 这里使用p_arr太浪费了,直接用arr比它简单多了!
A: 是的, 数组指针更多地可以用到二维或者多维数组更能体现价值。如下代码:
#include <stdio.h>
#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));
#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));
int main (int argc, const char * argv[])
{
int arr[][2] = {{1, 2}, {3, 4}};
int (*p_arr)[2] = &arr[0];
PRINT_D(p_arr[1][1])
p_arr = &arr[1];
PRINT_D(p_arr[0][1])
return 0;
}
运行结果:
可以看到,arr是二维数组,arr[0]是一个一维数组, &arr[0]是一维数组指针,p_arr是个指针,它需要指向一个包含2个元素的数组, &arr[0]正好符合,所以int (*p_arr)[2] = &arr[0]; 代码ok; 紧接着,p_arr[1]表示p_arr指向的一维数组为单位的下一个数组,也就是arr[1]所在的数组; p_arr[1][1]也就等同于arr[1][1], 所以结果打印4;我想,你可以分析后两句代码的意图了。
同时,你也可以看到,上面两段代码,同是int (*p_arr)[2],可以指向单纯的一维数组,同时也可以指向二维数组中的一维数组,这就是指针,只要类型ok,就能指。
Q: 下面使用二维数组以及它的指针,为什么会挂掉?
#include <stdio.h>
#define PRINT_P(pointer) printf("%10s is %p\n", #pointer, (pointer));
#define PRINT_D(intValue) printf(#intValue" is %d\n", (intValue));
int main (int argc, const char * argv[])
{
int arr[][2] = {{1, 2}, {3, 4}};
int **p_arr = (int **)arr;
PRINT_D(p_arr[0][0])
return 0;
}
运行结果:
A: arr是二维数组,arr[0][0]是没问题的;p_arr是二级指针,它指向arr,也就是p_arr的值是arr.所以p_arr[0]就是以地址p_arr为地址的数据,也就是arr数组的第一个元素,即p_arr[0] 等于1.那么p_arr[0][0]就是地址为1的一个整形数据,这能保证不挂么?
Q: 有的时候,发现下面这样的声明实在太难解读了,有什么好的方法么?
int (*p[3])(int *arg);
int (*(*func)(int *p))[3];
int (*(*func)[3])(int *p);
A: 要读懂这些函数,需要掌握优先级,函数指针的知识。
一一解析:
第一个:(*p[3]),[]优先级比*高,所以p是一个数组,含有3个元素,*表示数组元素都是指针;接着,看到右边(int *arg)表明前面的是个函数,参数是int *类型, 最左边的int表示返回值为整形;最后得到:p是一个数组,它含有3个元素,每个元素都是函数指针,函数指针的格式是: int (*)(int *arg);
由上,代码例子:
#include <stdio.h>
int (*func1)(int *arg);
int (*func2)(int *arg);
int (*func3)(int *arg);
int main (int argc, const char * argv[])
{
int (*p[3])(int *arg) = {func1, func2, func3};
return 0;
}
第二个:*func表示func是一个指针,后面的int *p表示,它是一个函数指针,参数为int *p, 左侧一个星号,表示返回值是个指针,右侧[3]表示返回值是个3个元素的数组,每个元素都是指针,最左侧的int表示返回值的数组元素为整形。总结下:func是个函数指针,参数为int *p, 返回值为包含5个元素的数组,且为指针。
第三个: 类似第一个,不过func多了一个指针类型。总结:func是一个指针,它指向一个数组,数组元素个数为3,每个元素都是一个函数指针,函数指针参数为int *p, 返回值为int.
xichen
2012-5-14 12:46:50