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

    未完,待续…

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

  • 相关阅读:
    Qt编写安防视频监控系统(界面很漂亮)
    Qt编写数据可视化大屏界面电子看板系统
    Qt开源作品35-秘钥生成器
    Qt开源作品34-qwt无需插件源码
    Qt开源作品33-图片开关控件
    Qt开源作品32-文本框回车焦点下移
    Qt开源作品31-屏幕截图控件
    Qt开源作品30-农历控件
    Qt开源作品29-NTP服务器时间同步
    Qt开源作品28-邮件发送工具
  • 原文地址:https://www.cnblogs.com/beanocean/p/3246986.html
Copyright © 2011-2022 走看看