Home

cdeveloper

读书,写作

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


版权声明:本文为 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 列」,结果如下图:

char

可以看到下面这一行:

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,定位如下位置:

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

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

请一定要实践!

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

ok,下次见。

欢迎关注我的公众号:「cdeveloper」

cdeveloper at 05/17/18