2026/10/8 19:43:02

C语言指针与内存

C语言指针与内存 在C语言的学习中指针总是一个让人头疼并且难以理解的概念但是通过建立指针和内存地址二进制字节数据的联系能帮助你很好地理解指针是什么数据在内存中长什么样为什么读取内存时CPU要知道数据的大小和类型一、实验环境Visual Studio 2026Windows11X86-64二、本期要用到的函数与结构体byte_array结构体包含数据源指针内存数据拷贝数据大小typedef struct { void* source; unsigned char* data; size_t data_size; }byte_array;构造函数源指针赋值与数据拷贝byte_array* to_byte_array(void* value, size_t size) { byte_array* arr (byte_array*)malloc(sizeof(byte_array)); if (arr NULL) { return NULL; } arr-data (unsigned char*)malloc(size); if (arr-data NULL) { free(arr); return NULL; } memcpy(arr-data, value, size); arr-source value; arr-data_size size; return arr; }内存打印函数附带起始地址打印内存数据或者按地址逐字节打印int byte_array_print(byte_array* arr,int no_address) { if (arr NULL || arr-data NULL) { return -1; } char* ptr (char*)arr-source; if (no_address) { printf(starting at 0x%p : ,(void*)ptr); } for (size_t i 0; i arr-data_size; i) { if (no_address) { printf(%02X , arr-data[i]); } else { printf(0x%p : %02X\n, (void*)ptr, arr-data[i]); ptr; } } if (no_address) { puts(); } return 0; }析构函数释放内存void byte_array_free(byte_array* arr) { if (arr ! NULL) { if (arr-data ! NULL) { free(arr-data); } free(arr); } }一个bit_cast宏//涉及指针强转不保证在所有情况下都能正常工作 //gcc/clang 建议开启 -fno-strict-aliasing #define bit_cast(type,src_ptr) (*(type*)(src_ptr))三、基本数据指针我们先定义并初始化一些常见的基本数据类型变量char char_var 0x10; short short_var 0x1020; int int_var 0x10203040; long long long_long_var 0x1020304050607080; float float_var 3.14f; double double_var 2.71828; //long在windows下为32位linux下为64位但是int和long long已经覆盖32位和64位故不再单独列出longchar*指针指向的内容通常占据sizeof(char) 1字节包含一个8位整数有无符号由实现定义msvc下通常为有符号printf(Value %d , char_var); byte_array* byte_of_char to_byte_array(char_var, sizeof(char_var)); byte_array_print(byte_of_char, true);编译并运行上面的代码控制台将打印Value 16 starting at 0x00000031061CFE80 : 10这里可以看到内存里面存的就是我们写的0x10按十进制打印输出16short* 指针和char* 不同从short开始单个数据的大小不再是一个单独的字节现在的数据大小是sizeof(short) 2 此时在不同CPU上打印出的内存数据会产生区别大小端printf(Value %d , short_var); byte_array* byte_of_short to_byte_array(short_var, sizeof(short_var)); byte_array_print(byte_of_short, true);编译并运行上面的代码控制台将打印Value 4128 starting at 0x000000E99D8FFEC4 : 20 10在x86的机器上打印出0x20 10因为x86是小端可以看到首字节0x10被放到最高位地址int*指针通常情况下int的数据大小为sizeof(int) 4个字节某些老机器上可能为sizeof(int) 2字节和short相同不同CPU上打印出的内存数据可能不一样下面不再说明printf(Value %d , int_var); byte_array* byte_of_int to_byte_array(int_var, sizeof(int_var)); byte_array_print(byte_of_int, true);编译并运行上面的代码控制台将打印Value 270544960 starting at 0x00000004AD31FC78 : 40 30 20 10和short类似只不过数据从2字节变成4字节long long*指针通常为sizeof(long long) 8个字节printf(Value %lld , long_long_var); byte_array* byte_of_long_long to_byte_array(long_long_var, sizeof(long_long_var)); byte_array_print(byte_of_long_long, true);编译并运行打印Value 1161981756646125696 starting at 0x00000004AD31FC80 : 80 70 60 50 40 30 20 10相比int从4字节提升到8字节float*指针4字节x86机器使用IEEE754标准来储存浮点数下面打印出的二进制内容是IEEE754的位模式printf(Value %f , float_var); byte_array* byte_of_float to_byte_array(float_var, sizeof(float_var)); byte_array_print(byte_of_float, true);输出Value 3.140000 starting at 0x00000004AD31FC7C : C3 F5 48 40可以看到0x 40 48 F5 C3就是我们3.14f在内存中的实际二进制表示double* 指针8字节和float相同x86使用IEEE754标准来储存双精度浮点数printf(Value %lf , double_var); byte_array* byte_of_double to_byte_array(double_var, sizeof(double_var)); byte_array_print(byte_of_double, true);输出Value 2.718280 starting at 0x00000004AD31FC88 : 90 F7 AA 95 09 BF 05 40基本数据的可读写范围对于CPU来说所有类型的数据都相当于一个二进制的字节数组相当于C语言的char[]每个类型对应的大小就是我们最多可以通过指针修改的地址数量1步进char 通常为1个字节short 2个字节int 通常为4个字节long long 8个字节float 4个字节double 8个字节代码例子float a 3.14f; memset(a,0,sizeof(float));//这里的第三个参数size可以填 4 3 2 1 0都是合法的 memset(a,0,sizeof(float) 1);//但你不能这样写内存访问越界其他类型相同四、数组C语言里的数组中的数据是连续储存的通过下面的代码我们可以很清楚地看出来这里的地址步进按1字节这里拿int数组来做例子其他数组的原理是一样的int arr[10] {0x10203040, 0x50607080, 0x90A0B0C0, 0xD0E0F000, 0x10203040, 0x50607080, 0x90A0B0C0, 0xD0E0F000, 0x10203040, 0x50607080}; byte_array* byte_arr to_byte_array(arr, sizeof(arr)); byte_array_print(byte_arr, false); byte_array_free(byte_arr);输出0x000000CD322FF7B0 : 40 0x000000CD322FF7B1 : 30 0x000000CD322FF7B2 : 20 0x000000CD322FF7B3 : 10 0x000000CD322FF7B4 : 80 0x000000CD322FF7B5 : 70 0x000000CD322FF7B6 : 60 0x000000CD322FF7B7 : 50 0x000000CD322FF7B8 : C0 0x000000CD322FF7B9 : B0 0x000000CD322FF7BA : A0 0x000000CD322FF7BB : 90 0x000000CD322FF7BC : 00 0x000000CD322FF7BD : F0 0x000000CD322FF7BE : E0 0x000000CD322FF7BF : D0 0x000000CD322FF7C0 : 40 0x000000CD322FF7C1 : 30 0x000000CD322FF7C2 : 20 0x000000CD322FF7C3 : 10 0x000000CD322FF7C4 : 80 0x000000CD322FF7C5 : 70 0x000000CD322FF7C6 : 60 0x000000CD322FF7C7 : 50 0x000000CD322FF7C8 : C0 0x000000CD322FF7C9 : B0 0x000000CD322FF7CA : A0 0x000000CD322FF7CB : 90 0x000000CD322FF7CC : 00 0x000000CD322FF7CD : F0 0x000000CD322FF7CE : E0 0x000000CD322FF7CF : D0 0x000000CD322FF7D0 : 40 0x000000CD322FF7D1 : 30 0x000000CD322FF7D2 : 20 0x000000CD322FF7D3 : 10 0x000000CD322FF7D4 : 80 0x000000CD322FF7D5 : 70 0x000000CD322FF7D6 : 60 0x000000CD322FF7D7 : 50可以很明显地看到第一个元素的最高位0x10下面接着的就是下一个元素的最低位0x80每个元素都占4个字节紧密地排在一起无任何填充地址是绝对连续的五、结构体结构体内存涉及填充无填充的结构体内存排布与数组类似不过访问元素需要通过固定的偏移无填充typedef struct { int a; float b; } nopadding_struct; //无填充结构体初始化并且打印一下nopadding_struct struct1 { 0x02030401, 2.0f }; printf(nopadding_struct: %d, %f\n, struct1.a, struct1.b); byte_array* arr1 to_byte_array(struct1, sizeof(struct1)); byte_array_print(arr1, false);输出很明显说明是连续排布并且无填充nopadding_struct: 33752065, 2.000000 0x000000430B6FF860 : 01 0x000000430B6FF861 : 04 0x000000430B6FF862 : 03 0x000000430B6FF863 : 02 0x000000430B6FF864 : 00 0x000000430B6FF865 : 00 0x000000430B6FF866 : 00 0x000000430B6FF867 : 40有填充的结构体这里在char后面有三个字节的填充typedef struct { char a; int b; double c; }padding_struct;这里初始化一个栈上结构体并打印padding_struct struct2 { 0xFF, 0x35249042, 3.14 }; printf(padding_struct: %d, %d, %f\n, struct2.a, struct2.b, struct2.c); byte_array* arr2 to_byte_array(struct2, sizeof(struct2)); byte_array_print(arr2, false);输出可以看到FF后面被填充了3个00padding_struct: -1, 891588674, 3.140000 0x000000430B6FF868 : FF 0x000000430B6FF869 : 00 0x000000430B6FF86A : 00 0x000000430B6FF86B : 00 0x000000430B6FF86C : 42 0x000000430B6FF86D : 90 0x000000430B6FF86E : 24 0x000000430B6FF86F : 35 0x000000430B6FF870 : 1F 0x000000430B6FF871 : 85 0x000000430B6FF872 : EB 0x000000430B6FF873 : 51 0x000000430B6FF874 : B8 0x000000430B6FF875 : 1E 0x000000430B6FF876 : 09 0x000000430B6FF877 : 40相同的结构体这一次我们在堆上创建padding_struct* struct2 (padding_struct*)malloc(sizeof(padding_struct)); struct2-a 0xFF; struct2-b 0x35249042; struct2-c 3.14; printf(padding_struct: %d, %d, %f\n, struct2-a, struct2-b, struct2-c); byte_array* arr2 to_byte_array(struct2, sizeof(padding_struct)); byte_array_print(arr2, false); free(struct2);release模式下的打印123的位置被填充为0x00padding_struct: -1, 891588674, 3.140000 0x0000013E761068C0 : FF 0x0000013E761068C1 : 00 0x0000013E761068C2 : 00 0x0000013E761068C3 : 00 0x0000013E761068C4 : 42 0x0000013E761068C5 : 90 0x0000013E761068C6 : 24 0x0000013E761068C7 : 35 0x0000013E761068C8 : 1F 0x0000013E761068C9 : 85 0x0000013E761068CA : EB 0x0000013E761068CB : 51 0x0000013E761068CC : B8 0x0000013E761068CD : 1E 0x0000013E761068CE : 09 0x0000013E761068CF : 40debug模式下的打印可以看到123的位置被填充了0xCDpadding_struct: -1, 891588674, 3.140000 0x000001CA6A1254A0 : FF 0x000001CA6A1254A1 : CD 0x000001CA6A1254A2 : CD 0x000001CA6A1254A3 : CD 0x000001CA6A1254A4 : 42 0x000001CA6A1254A5 : 90 0x000001CA6A1254A6 : 24 0x000001CA6A1254A7 : 35 0x000001CA6A1254A8 : 1F 0x000001CA6A1254A9 : 85 0x000001CA6A1254AA : EB 0x000001CA6A1254AB : 51 0x000001CA6A1254AC : B8 0x000001CA6A1254AD : 1E 0x000001CA6A1254AE : 09 0x000001CA6A1254AF : 40debug模式下改为callocpadding_struct* struct2 (padding_struct*)calloc(1,sizeof(padding_struct)); struct2-a 0xFF; struct2-b 0x35249042; struct2-c 3.14; printf(padding_struct: %d, %d, %f\n, struct2-a, struct2-b, struct2-c); byte_array* arr2 to_byte_array(struct2, sizeof(padding_struct)); byte_array_print(arr2, false); free(struct2);填充字节变为0x00padding_struct: -1, 891588674, 3.140000 0x000002226C114720 : FF 0x000002226C114721 : 00 0x000002226C114722 : 00 0x000002226C114723 : 00 0x000002226C114724 : 42 0x000002226C114725 : 90 0x000002226C114726 : 24 0x000002226C114727 : 35 0x000002226C114728 : 1F 0x000002226C114729 : 85 0x000002226C11472A : EB 0x000002226C11472B : 51 0x000002226C11472C : B8 0x000002226C11472D : 1E 0x000002226C11472E : 09 0x000002226C11472F : 40六、函数指针与其他指针不同函数指针指向的区域是只读的存储的也是二进制数据不过它是机器码可以被反汇编获得汇编代码这里我们尝试打印to_byte_array函数的机器码byte_array* arr2 to_byte_array(to_byte_array, 128); byte_array_print(arr2, true); byte_array_free(arr2);输出末尾的CC代表这是填充字节我们可以把0xCC前面的机器码给翻译为汇编starting at 0x00007FF66D301070 : 48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 48 8B F1 48 8B FA B9 18 00 00 00 FF 15 E0 0F 00 00 48 8B D8 48 85 C0 74 1B 48 8B CF FF 15 CF 0F 00 00 48 89 43 08 48 85 C0 75 1B 48 8B CB FF 15 C5 0F 00 00 33 C0 48 8B 5C 24 30 48 8B 74 24 38 48 83 C4 20 5F C3 4C 8B C7 48 8B D6 48 8B C8 E8 C9 0C 00 00 48 89 33 48 8B C3 48 8B 74 24 38 48 89 7B 10 48 8B 5C 24 30 48 83 C4 20 5F C3 CC CC CC反汇编输出与源C代码对应AI做的反汇编; to_byte_array 函数反汇编X86-64MSVC ; 对应源码byte_array* to_byte_array(void* value, size_t size) 48 89 5C 24 08 mov [rsp8], rbx ; 保存 rbx 到调用者栈帧 48 89 74 24 10 mov [rsp10h], rsi ; 保存 rsi 到调用者栈帧 57 push rdi ; 保存 rdi 48 83 EC 20 sub rsp, 20h ; 分配 32 字节影子空间 48 8B F1 mov rsi, rcx ; rsi value第一个参数 48 8B FA mov rdi, rdx ; rdi size第二个参数 B9 18 00 00 00 mov ecx, 18h ; ecx 24sizeof(byte_array) FF 15 E0 0F 00 00 call qword ptr [rip0xFE0] ; 调用 malloc 48 8B D8 mov rbx, rax ; rbx arrmalloc 返回值 48 85 C0 test rax, rax ; 检查 arr 是否为 NULL 74 1B jz short loc_ret_null ; 若为 NULL 则跳转返回 NULL 48 8B CF mov rcx, rdi ; rcx size FF 15 CF 0F 00 00 call qword ptr [rip0xFCF] ; 调用 malloc分配 data 48 89 43 08 mov [rbx8], rax ; arr-data 返回值 48 85 C0 test rax, rax ; 检查 data 是否为 NULL 75 1B jnz short loc_copy ; 若非 NULL 则跳转执行拷贝 48 8B CB mov rcx, rbx ; rcx arr FF 15 C5 0F 00 00 call qword ptr [rip0xFC5] ; 调用 free(arr) 33 C0 xor eax, eax ; eax 0返回 NULL 48 8B 5C 24 30 mov rbx, [rsp30h] ; 恢复 rbx 48 8B 74 24 38 mov rsi, [rsp38h] ; 恢复 rsi 48 83 C4 20 add rsp, 20h ; 回收栈空间 5F pop rdi ; 恢复 rdi C3 ret ; 返回 NULL loc_copy: 4C 8B C7 mov r8, rdi ; r8 sizememcpy 第三参数 48 8B D6 mov rdx, rsi ; rdx valuememcpy 第二参数 48 8B C8 mov rcx, rax ; rcx arr-datamemcpy 第一参数 E8 C9 0C 00 00 call memcpy ; 调用 memcpy 48 89 33 mov [rbx], rsi ; arr-source value 48 8B C3 mov rax, rbx ; rax arr返回值 48 8B 74 24 38 mov rsi, [rsp38h] ; 恢复 rsi 48 89 7B 10 mov [rbx10h], rdi ; arr-data_size size 48 8B 5C 24 30 mov rbx, [rsp30h] ; 恢复 rbx 48 83 C4 20 add rsp, 20h ; 回收栈空间 5F pop rdi ; 恢复 rdi C3 ret ; 返回 arr loc_ret_null: 33 C0 xor eax, eax ; eax 0返回 NULL 48 8B 5C 24 30 mov rbx, [rsp30h] ; 恢复 rbx 48 8B 74 24 38 mov rsi, [rsp38h] ; 恢复 rsi 48 83 C4 20 add rsp, 20h ; 回收栈空间 5F pop rdi ; 恢复 rdi C3 ret ; 返回 NULL七、CPU怎么读取内存中的二进制数据如果说数据本身是char[]那CPU是怎么把这些数据翻译为我们能看到的浮点数和整数或者字符串呢char data[8] {45,56,55,43,50,51,52,00}; printf(%s\n,data); printf(%d\n,bit_cast(char,data)); printf(%d\n,bit_cast(short,data)); printf(%d\n,bit_cast(int,data)); printf(%lld\n,bit_cast(long long,data)); printf(%e\n, bit_cast(float, data)); printf(%e\n, bit_cast(double, data));编译并运行上面的代码会输出由于浮点数太小这里选择用%e打印注意这里的data里面是十进制数据-87234 45 14381 725039149 14692989355374637 6.509262e-13 1.123661e-307通过不同类型的指针CPU把data的二进制内容给翻译为了这七行人类可读的数据通过不同类型的指针CPU读取不同的字节数把它翻译为人类可读的数据即使字节相同浮点型和整数也会被翻译为截然不同的数据。八、总结本文通过把指针与内存地址、二进制字节数据建立联系从底层视角重新理解了 C 语言中的指针。核心收获可以归纳为以下几点指针的本质指针保存的是内存地址而内存中存放的只是一串二进制字节指针的类型决定了 CPU 按多大的步长、以何种方式去解读这些字节。基本数据的内存形态char、short、int、long long、float、double 在内存中分别占据 1、2、4、8、4、8 字节且在小端机器上低字节存放在低地址浮点数按 IEEE754 位模式存储与整数的二进制表示完全不同。可读写范围每种类型对应的大小就是通过指针最多可安全修改的字节数超出该范围如 memset 多写 1 字节就会造成内存访问越界。数组的连续性数组元素在内存中紧密连续排列、无填充地址按 1 字节步进因此可以通过指针逐字节观察每个元素的二进制内容。结构体的填充无填充结构体与数组类似、连续排布有填充结构体则会在成员之间插入填充字节且填充内容受编译模式影响——release 下为 0x00debug 下为 0xCDcalloc 分配的内存填充为 0x00。函数指针与机器码函数指针指向只读的代码区存储的是机器码可以被反汇编为汇编指令从而把高级语言与底层指令对应起来。CPU 如何解读数据同一段二进制字节通过不同类型的指针会被翻译成截然不同的结果——整数、浮点数或字符串这正是类型系统在底层发挥作用的地方。理解了「指针 地址 类型」这一核心关系再结合内存字节的视角去观察数据就能更扎实地掌握 C 语言的内存模型也为后续学习更复杂的数据结构与底层调试打下基础。