关于C语言指针的一些新认识(1)

Technorati 标签: ,,,

前言 


    指针是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
  1. Q2:code 4中看不出数组和指针有什么区别,是不是数组跟指针是一样的呢?
  2. 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 = &num;
   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: }

程序的输出为:

image 

输出它的汇编代码为:(这里我只保留了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: }

输出结果:

image

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只是在自己的存储空间中存放了这个字符串常量的首地址。常量当然不允许被修改啦~

未完,待续…

啰啰嗦嗦的写了这么多,这篇博客就先写到这里吧,希望这种分析方式可以为大家解决问题提供一个新的视角,下一篇我将写一写在使用指针过程中遇到的细节问题

原文地址:https://www.cnblogs.com/beanocean/p/3246986.html