`
java-mans
  • 浏览: 11414664 次
文章分类
社区版块
存档分类
最新评论

数组,也就是数据块而已----小话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


分享到:
评论

相关推荐

    传智博客-C语言 PPT

    里面包含了如何去创建第一个C语言,讲到了C语言的关键字、注释、标识符、数据类型、常量、变量、函数、数组、字符串、指针、结构体、预处理指令、static、extern关键字等等

    c++ 面试题 总结

    块式管理:把主存分为一大块、一大块的,当所需的程序片断不在主存时就分配一块主存空间,把程 序片断load入主存,就算所需的程序片度只有几个字节也只能把这一块分配给它。这样会造成很大的浪费,平均浪费了50%的...

    嵌入式C语言查表法在项目中的应用

    之前写过上面这个标题的一篇文章,讲的是以位移的方式去遍历表中的数据,效率非常高,但是,如果要实现一个乱序的流水灯或者跑马灯的话,思考一个这样的算法是不可取的,很费时间,也很费脑力,于是,今天就说一说...

    C和C++头文件对比一览

    经过了标准委员会如此大规模手术后,在98年以前出品的C++编译器(BC3.0,BC5.0)上能顺利通过编译的源文件,在支持新标准的编译器上可能无法顺利通过编译也就是很正常的事了。 [起因] 在回过头来看看标准程序库...

    基于485串口的发送接收程序

    基于485的串口发送接收程序,RS485和RE232的程序设计差不多,只有差了一个使能端而已,同样可以用。加入了自己的协议,协议可以自己随便设置。程序可以用调试助手测试。很好很强大,条例很清晰。

    c/c++ 学习总结 初学者必备

    答: 数组:数据顺序存储,固定大小; 链表:数据可以随机存储,大小可动态改变; 21、为什么数组名作为参数,会改变数组的内容,而其它类型如int却不会改变变量的值? 答: 当数组名作为参数时,传递的实际上是地址。而...

    最新名企标准通用C++面试题,

    private 也被集成,只事派生类没有访问权限而已。virtual可加可不加。子类的空间里有父类的所有变量(static除外)。同一个函数只存在一个实体(inline除外)。子类覆盖它的函数不加virtual ,也能实现多态。在子类的空间...

    PHP基础教程 是一个比较有价值的PHP新手教程!

    如果想要强行转换变量类型,可以使用与C语言相同的函数settype()。 2.5 变量与常量 可能你已经注意到,变量都有一个美元符号($)的前缀。所有变量都是局部变量,为了使得定义的函数中可以使用外部变量,使用...

    LABVIEW源程序

    方法多看例子,多动手!...入门主要是熟悉基于数据流的编程方法、控件、数组、簇、程序结构、子VI等等,入门后就可以根据你的需要跳着看了.书最好是纸质的,电子版让人头晕,我电脑里堆满了电子版书籍,但没

    自己动手写操作系统(含源代码).part2

    不过我得坦白,在写作《自己动手写操作系统》的时候,我并不敢期待它能引起多少反响,一方面因为操作系统并不是时尚的话题,另一方面我也是走在学习的路上,或许只是比读者早走了一小步而已。然而出乎我的意料,它...

    自己动手写操作系统(含源代码).part1

    不过我得坦白,在写作《自己动手写操作系统》的时候,我并不敢期待它能引起多少反响,一方面因为操作系统并不是时尚的话题,另一方面我也是走在学习的路上,或许只是比读者早走了一小步而已。然而出乎我的意料,它...

Global site tag (gtag.js) - Google Analytics