美文网首页C语言从 0 开始学习 C 语言系列程序员
从 0 开始学习 C 语言:详细分析常见「类型」在内存中的存储

从 0 开始学习 C 语言:详细分析常见「类型」在内存中的存储

作者: 登龙zZ | 来源:发表于2018-05-23 22:17 被阅读10次

    版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!

    这个系列的文章我会挑出我认为比较重要的 C 语言知识来分享给大家,尽量写的富有实践性,让你看完之后不会觉得枯燥无味,而是看完就有想动手调试的欲望。

    今天给大家分享的是 C 语言中「类型」这个概念,写过程序的同学对类型这个东西都不陌生,不管是什么编程语言都离不开类型。一些高级语言只用一个关键字就定义了所有的类型,比如 JS 中的 var,而 C 语言则为常见的类型都定义的对应的关键字。

    我们写程序,不管是面向过程还是面向对象,都要使用到类型。其实说白了,对类型的简单理解就是:「一个变量或者对象在内存中的步长,以及可以对该对象执行的操作」。

    不知道你有没有听过「步长」这个概念,字面意思可以理解为人走一步的长度,腿长的人跨度大,小短腿跨度小。把步长这个概念应用于理解类型上面,我觉得还是非常不错的,一个 32 位机器上的 char 类型的字节数为 1 Byte,则可以理解为该类型在内存中的步长为 1,而 int 字节数为 4 Bytes,则可以理解为步长为 4。

    如果你还不是太理解的话,看看下面具体的内存分析,目前把内存当成一个「字节数组」即可,这样理解类型其实非常简单。

    1. char 类型内存分析

    运行这个例子,打印出变量 c 的 ANSI 值和内存地址。

    #include <stdio.h>
    
    int main(void) {
      char c = 'A';
      printf("c ANSI = %d, c address = 0X%X", c, &c);
      // 这里断点调试
      getchar();
      return 0;
    }
    

    我的结果是:

    c ANSI = 65, c address = 0X15FCFB
    

    我们调试内存,输入 c 的内存地址 0X15FCFB,右击选择「显示 1 字节整数」,并只「显示 1 列」,结果如下图:

    [图片上传失败...(image-2571bc-1527085048097)]

    可以看到下面这一行:

    0x0015FCFB  41  A
    
    1. 0x0015FCFB 是变量 c 的内存地址
    2. 0x41 = 65 = A = 0100 0001 B

    这样在内存中就很直接的看到了变量 c 的存储,它的步长只占 1 个字节,后面的地址都不属于变量 c,并且 ANSI 值为 41,表示字符 A。

    2. int 类型内存分析

    int 占 4 个字节,你理解了上面的例子,举一反三后你应该已经知道了 int 在内存中的存储方式,如果还有点迷糊,继续看这个例子。

    #include <stdio.h>
    
    int main(void) {
      // 2147483647 = 0x 7F FF FF FF
      int i = 2147483647;
      printf("i address = 0X%X", &i);
      // 这里断点调试
      getchar();
      return 0;
    }
    

    我的结果如下:

    i address = 0X3BFECC
    

    继续调试内存,输入 i 的内存地址,仍然右击显示 1 字节整数,显示 1 列,如下图:

    int

    注意这 4 行:

    0x003BFECC  ff  .
    0x003BFECD  ff  .
    0x003BFECE  ff  .
    0x003BFECF  7f  .
    

    如果你理解了 char 的存储,相信你应该知道每一行是什么意思,这里还是要注意为什么是反的,这还是跟计算机的字节序有关,还不理解字节序的朋友可以看我上一篇文章中对字节序概念的介绍。

    这 4 行连起来就是 0X7FFFFF,即 2147483647 的 16 进制表示,至于为啥用这个数来举例,因为它很特殊,学习的时候最好记住它,这里就不多提了,以后再说,不是目前的重点。

    int 是不是很简单,那来个稍微复杂点的,分析下数组吧。

    3. 数组类型内存分析

    看下面这个简单的数组例子:

    #include <stdio.h>
    
    int main(void) {
      char c[3] = { 'a', 'b', 'c' };
      printf("c address = 0X%X, c[0] address = 0X%X", &c, &c[0]);
      // 这里断点调试
      getchar();
      return 0;
    }
    

    我的结果如下:

    c address = 0x3CFB94, c[0] address = 0x3CFB94
    

    继续调试内存,显示 1 字节整数,显示 1 列,结果如下:

    int

    注意这连续的 3 行,是一个 char 数组类型:

    0x003CFB94  61  a
    0x003CFB95  62  b
    0x003CFB96  63  c
    

    可以看到 a,b,c 在内存中顺序存储,且数组首地址和第一个数组元素的首地址相同,但是它们俩又有本质的区别。因为它们的步长(类型)不同,如果都对它们「取地址后加 1」,那么结果将千差万别,这个留给你自己去思考和调试。可不要小看这个问题,C/C++ 面试题很常见的,很多初学者都搞不明白。

    再来分析下很多同学头疼的「指针」!

    4. 指针类型内存分析

    首先要说明下指针的类型大小,在 32 位机器(项目)上指针是 4 个字节,64 位机器(项目)上指针是 8 个字节,这是为什么呢?

    其实可以把指针简单的「看作」地址,但是指针严格意义上不是地址,因为指针是变量,而地址是常量。当你把指针看作地址后,你调试内存就会发现 32 位机器上的地址是 32 位 = 4 Bytes,而 64 位机器上的地址是 64 位 = 8 Bytes,因此对应的指针就是 4 B 或者 8 B 了。

    如果看不太懂的话,直接看看这个指针例子,例子是 32 位的,我的电脑是 64 位:

    #include <stdio.h>
    
    int main(void) {
      // 2147483647 = 0x 7F FF FF FF
      int i = 2147483647;
      int *p = &i;
      printf("&p = 0X%X, p = &i = 0X%X, sizeof(p) = %d", &p, &i, sizeof(int *));
      // 这里断点调试
      getchar();
      return 0;
    }
    

    32 位配置下的运行结果如下:

    &p = 0x46F860, p = &i = 0x46F86C, sizeof(p) = 4
    

    要理解:「指针是一个变量,也有自己的内存地址」,这里 0x46F860 就是指针 p 的内存地址,这个指针是 4 字节大小,里面存储的内容是变量 i 的地址 0x46F86C,该地址也是 4 个字节。

    继续调试内存,输入指针地址,如下图:

    pointer

    继续调试内存,输入变量 i 的地址,如下图:

    于是就得出一张经典的指针模型图:

    pointer_i

    看到这里你应该能够理解指针了,建议还不理解的朋友一定要自己调试这个过程,多调试内存就能理解了。至于在 64 位下的指针分析,就留给你锻炼吧,把当前 VS 的项目属性配置成 x64 模式,然后重新按照上面的步骤调试即可,不要眼高手低,不理解指针的话一定要动手调试!

    至于二级指针,其实和一级指针是相同的,只不过第一级指针存储的内容是第二级指针的地址,第二级指针存储的内容才是实际变量的内存地址,建议你自己理解一级指针后,写个二级指针的例子,然后按照上面的方法重新调试 32 位和 64 模式。

    5. 函数指针内存分析

    在平常开发的过程中个,函数指针可以说是非常常用了,作为函数的参数用来回调是函数指针的一个经典用法,但是能够使用这项技术的前提是要完全理解函数指针。函数指针简单来说就是:「一个指向函数的指针,该指针存储的内容是一个函数的首地址,对该指针解引用,加上小括号() 就可以调用所指向的函数

    例如下面这个例子:

    #include <stdio.h>
    
    // 函数名其实是一个函数指针变量
    void fun(void) {
      printf("I'm fun.\n");
    }
    
    // &fun = fun
    int main(void) {
      void (*p1)(void) = fun;
      printf("&p1 = 0X%X, p1 = fun = 0X%X,&fun = 0X%X\n", &p1, fun, &fun);
      p1();
    
      void(*p2)(void) = &fun;
      printf("&p1 = 0X%X, p2 = fun = 0X%X,&fun = 0X%X\n", &p2, fun, &fun);
      (*p2)();
    
      // 这里断点调试
      getchar();
      return 0;
    }
    

    这是我的运行结果:

    &p1 = 0X3BF9E0, p1 = fun = 0X13811EF,&fun = 0X13811EF
    I'm fun
    &p2 = 0X3BF9D4, p2 = fun = 0X13811EF,&fun = 0X13811EF
    I'm fun
    

    可以看到这两种使用函数指针的方法都能正常运行,并且指向的 fun 函数地址都相同。注意:C 语言设计者为了方便允许使用第一种不带 * 直接调用函数的方式。

    同样,来以 p1 指针分析下函数指针的内存,如下图:

    fun_p

    可以看到 p1 指针的存储的内容为 fun 函数的地址,那 fun 函数在哪里呢?我们打开反汇编窗口:「调试 -> 窗口 -> 反汇编」,输入 fun 函数的地址 0X13811EF,定位如下位置:

    fun_p

    可以看到一行汇编代码,看不懂也不要紧,知道这行代码是要跳转到地址 0x013813C0 即可,我们继续跳到这个地址:

    fun_p

    是不是看到 fun 函数的汇编代码了,调试到此为止,不必完全理解所有的调试步骤,你只需要知道函数指针指向一个函数即可,演示这个调试步骤的「目的是」:为了让你直观的看到函数指针所指向的函数到底在哪里,实际调试出来给你看相信比口头说出来要更有说服力。

    请一定要实践!

    这次就分享这些吧,其实已经写了很多了,能耐心看下来的朋友相信对类型和指针一定有更加深刻的理解,刚入门 C 语言的同学相信也应该已经能够理解类型这个概念了,还不太理解的同学一定要动手调试这个过程,也许在你调试的时候就突然间恍然大悟,原来类型和指针这么简单啊!

    ok,下次见。

    相关文章

      网友评论

        本文标题:从 0 开始学习 C 语言:详细分析常见「类型」在内存中的存储

        本文链接:https://www.haomeiwen.com/subject/ydidjftx.html