前言
指针是C语言的精华,但我对它一直有种敬而远之的感觉,因为一个不小心就可能让你的程序陷入莫名其妙的麻烦之中。所以,在处理字符串时,我总是能用数组就尽量用数组。但是,数组有个缺点:不能动态地分配内存的大小。
终于下定决定克服这个心里障碍,探一探指针的究竟,却发现了很多自己之前没有认识到的,甚至是认识有错误的地方。这里,把这两天学到的新知识做了一个简单的整理,并记录下来。因为水平有限,若有疏漏之处,还希望大家及时指正,以免祸害他人。
基础知识 & Questions
指针
约等于是一个地址(这个地址是某个对象的存储空间的首地址),它是一个以当前系统寻址范围为取值范围的无符号整数(unsigned int),在32位系统中长度为32bits。
指针变量
首先它是一个变量(它存储的内容是可以改变的),其次这个变量是用来存放某个对象存储空间的首地址(指针)的。它的类型便是它指向的对象的类型,并且允许强制类型转换。它指向的对象可以是:空(NULL)、通用类型(void)、简单类型(int,char…)、结构体(struct)、类(Class)甚至是函数(function)!
其实对于指针和指针变量这俩名词我总是用着用着就搞混了,刚刚捋顺清楚,没一下午的功夫就又糊涂了…不过我觉得心里明白其中的含义就好,会用就好,不必在名词使用的正误上追究太多。
不过,要是非得打个比方来说,感觉指针就像是一个整数,指针变量则是一个整型的变量,指针只不过是一个指针变量某一时刻的取值。
指针符号『*』和地址符号『&』
& : 取对象的地址(即:取得该对象存储空间对于的首地址)
* : 取对应首地址存放的对象的内容(即:值)
相对地址 内容 0xf1110000 00 0xf1110002 00 0xf1110003 00 0xf1110004 15 0xf1110005 … ……….
0xf1110100 … 假设粗体字中表示整型变量 num 的存储空间,那么num的值是:21(=16 + 5)
1: int * pNum = #2: * pNum = 22;
在上述代码中,&取出的是num的首地址:0xf1110000,而 * 取出的却是num对应的整个内存。
这里我想问一个问题,也是一直困扰我很久的一个问题:(Q1)指针pNum只占用了4B的存储空间,并没有额外的存储空间存储它指向对象的类型信息,它是怎么知道num占用了4B的存储空间的呢?这个问题我们稍后解答。
注:本例中采用小端序(Little End)。
声明:
1: //pattern: type * pv1, * pv2, v3, ... ;
2: //attention: v3 is not a pointer!
3: char * ptr;
初始化:
1: //Initialization when declaration
2: char * pointer = NULL;
3: char Msg[] = "I Love U";
4: char * pMsg = Msg; //init by array
5: char * ptr = "I'm not a good boy"; //init by C string
6: int age = 21;
7: int * pAge = &age; //init by refference
- Q2:code 4中看不出数组和指针有什么区别,是不是数组跟指针是一样的呢?
- Q3:code 5中是否可以通过ptr[0] = 'l' ;的方式修改字符串呢?
赋值:
1: //assignment
2: struct Woman{
3: int age;
4: int weight;
5: int height;
6: };
7: struct Woman laura, * pWoman;
8: int * pAge;
9: pWoman = &laura;
10: pAge = &pWoman->age;
用指针访问结构体或者类的成员时,一定要使用:“->”,用“.”是不合法的。
探索
好吧,说到这我又把C语言指针的最基本的用法回顾了一遍,同时,也提出了3个简单的问题。接下来,我就本着探索的精神来解决了一下这里的疑惑,另外还会在下一篇博客中陆续提出几个新的问题。
探索工具
代码面前没有秘密可言,要窥探程序的运行机制,当然非汇编代码莫属了。下面我们将从汇编的角度来解决上面遇到的3个问题。
首先,我们要知道如何产生高级语言对应的汇编代码。GNU gcc的编译指令是:gcc –masm=intel –S source.c –o source.s,这里 -masm是产生intel风格的汇编代码,如果要是不加这句话,则产生AT&T风格的汇编代码。另外,Window平台下的编译器也同样提供了产生中间汇编代码的编译选项。(参考:AT&T与Intel汇编语言的比较)
另外,补充一点儿汇编的知识:(更多的汇编知识请点击这里)
ebp | 基址寄存器 base pointer R |
esp | 堆栈寄存器 stack pointer R |
eax, ebx, ecx, edx | 数据寄存器 |
esi | source index register |
edi | destination index register |
Q1
我写了一个简单的测试程序:
1: #include <stdio.h>
2:
3: int main()
4: {
5: int num = 21;
6: int * pNum = #
7: printf("num: %d pNum: %d ", num, *pNum);
8:
9: * pNum = 22;
10: printf("num: %d pNum: %d ", num, *pNum);
11:
12: return 0;
13: }
程序的输出为:
输出它的汇编代码为:(这里我只保留了code 9对应的汇编代码)
1: mov eax, DWORD PTR [esp+28]
2: mov DWORD PTR [eax], 22
看到这,我突然明白:原来指针的类型信息仅仅是保存在了我们自己写的源代码里,在进行编译时,编译器已经将类型信息转换成对应的操作了。比如:“* pNum = 22;”这句代码,因为pNum是int型的指针变量,
所以先:mov eax, DWORD PTR [esp+28],从堆栈中取出pNum中存储的地址;
然后再:mov DWORD PTR [eax], 22,将立即数22赋值给eax寄存器存储的地址后连续的双字内存空间,即将22复制到num的内存空间。
如果num是short int型,pNum是short int型的指针,那么这句就应该会被编译成:
mov WORD PTR [eax], 22,即将22复制到一个单字内存空间。
Q2
很长一段时间以来,我一直认为数组和指针是一回事儿。确实也是因为他们太像了,很多时候可以混着用。但是看看下面这段程序输出的结果,我便陷入了迷惑之中…
1: #include <;stdio.h>
2:
3: int main()
4: {
5: char Msg[] = "I Love U";
6: char * ptr = "I'm not a good boy";
7: printf("%s %s ", Msg, ptr);
8: printf("%d %d ", sizeof(Msg), sizeof(ptr));
9: return 0;
10: }
输出结果:
Msg中共有8个字符,加上字符串尾部的 ' '刚刚好9个字符,这没问题。但是为什么ptr指向的字符串就值输出了4呢?这输出的不可能是它指向字符串的长度,而很可能是它自身占用的内存大小。
下面来看汇编代码,希望能从这里找到答案:
1 .file "test.c" 2 .def ___main; .scl 2; .type 32; .endef 3 .section .rdata,"dr" 4 LC1: 5 .ascii "I'm not a good boy " 6 LC2: 7 .ascii "%s12%s12 " 8 LC3: 9 .ascii "%d %d12 " 10 LC0: 11 .ascii "I Love U " 12 .text 13 .globl _main 14 .def _main; .scl 2; .type 32; .endef 15 _main: 16 LFB6: 17 ; 汇编和连接信息 18 .cfi_startproc 19 pushl %ebp 20 .cfi_def_cfa_offset 8 21 .cfi_offset 5, -8 22 movl %esp, %ebp 23 .cfi_def_cfa_register 5 24 pushl %edi 25 pushl %esi 26 pushl %ebx 27 andl $-16, %esp 28 subl $32, %esp 29 .cfi_offset 3, -20 30 .cfi_offset 6, -16 31 .cfi_offset 7, -12 32 call ___main 33 ; 将字符串常量复制到Msg的存储空间中 34 leal 19(%esp), %edx ; &Msg -> edx,将Msg的地址复制给edx 35 movl $LC0, %ebx ; &LC0 -> ebx,将标号LC0的地址复制给ebx 36 movl $9, %eax ; $9 -> eax,将立即数9传给eax 37 movl %edx, %edi ; 将Msg的地址传给目的寄存器 38 movl %ebx, %esi ; 将LC0的地址传给源索引寄存器(source index) 39 movl %eax, %ecx ; 将eax中的9传给ecx 40 rep movsb ; 开始在esi和edi之间传输数据,每传一个eci减一,直到eci为0 41 ; 将Msg和ptr对应字符串的地址当做参数传给printf(),并打印 42 movl $LC1, 28(%esp) ; Msg占据9B,从28开始是ptr的地址 43 movl 28(%esp), %eax ; 将LC1的地址赋给eax 44 movl %eax, 8(%esp) ; 将eax中存放的地址(LC1)赋给8(偏移地址) 45 leal 19(%esp), %eax ; 将Msg的地址赋给eax 46 movl %eax, 4(%esp) ; 将eax中存放的地址(Msg)赋给4(偏移地址) 47 movl $LC2, (%esp) 48 call _printf 49 ; 将sizeof()计算出的结果当立即数传给printf()的参数,并打印 50 movl $4, 8(%esp) 51 movl $9, 4(%esp) 52 movl $LC3, (%esp) 53 call _printf 54 ; 程序退出 55 movl $0, %eax 56 leal -12(%ebp), %esp 57 popl %ebx 58 .cfi_restore 3 59 popl %esi 60 .cfi_restore 6 61 popl %edi 62 .cfi_restore 7 63 popl %ebp 64 .cfi_def_cfa 4, 4 65 .cfi_restore 5 66 ret 67 .cfi_endproc 68 LFE6: 69 .def _printf; .scl 2; .type 32; .endef
从上面的汇编代码中可以看出,程序将 "I Love U " 这9个字符从 .section .rdata (只读数据段)中拷贝到了运行时的堆栈中,并且可以断定 28(%esp)既是字符数组Msg的首地址。但可怜的字符串”I'm not a good boy “就没那么幸运了,它之后依然呆在数据段中(常量,不可被修改)。
看到这,我想我明白了数组和指针最本质的差别。在编译器进行编译时,并没有刻意地开辟4B的内存保存数组的初始地址,而是将数组名直接翻译成了数组对应的存储空间的首地址,它是一个彻彻底底的地址常量!就像一个代表地址的立即数一样!上例中,第34行汇编代码:leal 9(%esp), %edx, 在堆栈中,9~27这9B的内存都是数组的内存空间,所以在对数组初始化时,直接使用了其首地址。注意:这里用的是leal(将源操作数的地址赋给目的操作数),而不是movl(将源操作数的内容赋给目的操作数)。
但是指针变量ptr却拥有自己的在堆栈中拥有属于自己的内存空间:28~31。
从之前对指针的定义来看,数组名也可以算作是指针常量(它本身标记的就是一个地址而已),记住:数组名不是指针变量,它只能用作右值!
Q3
解决了Q2,Q3的答案也便一目了然了。因为char * ptr = "I'm not a good boy";在被编译时,编译器将字符串当做常量存放在了data段中(只读),而指针变量ptr只是在自己的存储空间中存放了这个字符串常量的首地址。常量当然不允许被修改啦~
未完,待续…
啰啰嗦嗦的写了这么多,这篇博客就先写到这里吧,希望这种分析方式可以为大家解决问题提供一个新的视角,下一篇我将写一写在使用指针过程中遇到的细节问题