数组,也就是数据块而已----小话c语言(6)

本文代码编写编译运行的环境:[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


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值