skaiuijing
理解指针
很多人经常使用指针,看到这个标题可能不屑一顾。但笔者想说,把指针当作理所当然而不去探索它的本质,这是不对的。
先问个问题:指针是地址还是变量?(int *)a +1 增加了多少?
如果说 int a代表一个int大小的a变量,那么 int *a代表什么呢?答案是指向一个int 变量的指针变量a。
既然a有大小,是int,那么int *a有没有大小呢?答案是:肯定有!可能有人会回答一个字的大小,其实多少有点不严谨,指针的大小取决于具体的计算机架构和编译器,例如在常见的MCU-stm32上,一个指针的大小是32位,对应一个字。
所以说,指针的本质也是变量,(int *)a + 1,其实是加了对应一个指针的大小。
为什么指针能够修改内存
笔者定义一个结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include<stdio.h>
struct store{ int a; int b; };
int main() { long long address; struct store *pointself;//此时pointself的值是随机的、无意义的 pointself = &address;
pointself->a = 10; pointself->b = 5; printf("address:%d,address-a: %d,address-b: %d\n",pointself,&(pointself->a),&(pointself->b));
printf("store: %lld, a:%d ,b = %d\n",address,(int)address,(int)(*(((int *)&address) + 1 )));
}
|
程序的运行结果:
1 2
| address:6422032,address-a: 6422032,address-b: 6422036 store: 21474836490, a:10 ,b = 5
|
x86-64为大端序,转为二进制就是:10100000000000000000000000000001010
假设架构为x86*64,那么内存布局如下:
| 变量 |
a (4 bytes) |
b (4 bytes) |
| 值 |
1010 |
101 |
| 地址 |
00000000 01100001 11111110 00010000 |
00000000 01100001 11111110 00010100 |
让我们思考一下,poinself->a = 10时,到底发生了什么?
答案是:机器通过pointself这个指针,找到了这个具体地址对应的空间,然后存放了a的值。
反汇编如下:(程序已经被gcc优化得非常好了,可能不足以解释这个过程,但是可以作为参考)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| main: push rbp mov rbp, rsp sub rsp, 32 mov QWORD PTR [rbp-8], 0 //把这块区域作为存储address的空间,初始化为0 lea rax, [rbp-8] mov QWORD PTR [rbp-16], rax //存储address的地址 mov DWORD PTR [rbp-8], 10 //存储a mov DWORD PTR [rbp-4], 5 //存储b mov rax, QWORD PTR [rbp-16] mov rsi, rax lea rdx, [rbp-8] lea rcx, [rbp-4] mov edi, OFFSET FLAT:.LC0 mov eax, 0 call printf mov rax, QWORD PTR [rbp-16] mov rax, QWORD PTR [rax] mov esi, eax mov rax, QWORD PTR [rbp-16] mov eax, DWORD PTR [rax+4] mov edi, OFFSET FLAT:.LC1 mov edx, eax mov eax, 0 call printf mov eax, 0 leave ret
|
- 内存分配:
sub rsp, 32 分配了 32 个字节的栈空间,用于存储 address 和 pointself。
mov QWORD PTR [rbp-8], 0 初始化 address 为 0。
lea rax, [rbp-8] 计算 address 的地址并存储到寄存器 rax。
mov QWORD PTR [rbp-16], rax 将 address 的地址存储到 pointself。
- 赋值操作:
mov DWORD PTR [rbp-8], 10 将值 10 存储到 pointself->a 对应的内存位置。
mov DWORD PTR [rbp-4], 5 将值 5 存储到 pointself->b 对应的内存位置。
- 打印操作:
mov rax, QWORD PTR [rbp-16] 将 pointself 的值加载到寄存器 rax。
mov rsi, rax 将 rax 的值移动到 rsi,作为 printf 的参数。
lea rdx, [rbp-8] 计算 pointself->a 的地址并存储到 rdx。
lea rcx, [rbp-4] 计算 pointself->b 的地址并存储到 rcx。
mov edi, OFFSET FLAT:.LC0 将格式字符串的地址加载到 edi。
call printf 调用 printf 函数。
mov rax, QWORD PTR [rbp-16] 将 pointself 的值加载到寄存器 rax。
mov rax, QWORD PTR [rax] 将 address 的值加载到寄存器 rax。
mov esi, eax 将 eax 的值移动到 esi,作为 printf 的参数。
mov rax, QWORD PTR [rbp-16] 将 pointself 的值加载到寄存器 rax。
mov eax, DWORD PTR [rax+4] 将 pointself->b 的值加载到寄存器 eax。
mov edi, OFFSET FLAT:.LC1 将格式字符串的地址加载到 edi。
mov edx, eax 将 eax 的值移动到 edx,作为 printf 的参数。
call printf 调用 printf 函数。
现在你是否明白为什么我们在使用指针时,常常要malloc?其实就是在给指针变量赋一个有意义的值,不然当我们使用->时,计算机会发现这块空间存储了莫名其妙的东西,或者是这块空间压根不存在。
既然理解了指针,那么该使用它了。
在使用函数时,我们常常使用return传递某些变量,但是,有时候我们希望函数能够传递多个元素,这时有什么好办法呢?
现在,是时候看看指针的伟大之处了。
1.使用指针修改对应地址指向的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h>
void getTwoValues(int *x, int *y) { *x = 10; *y = 20; }
int main() { int a, b; getTwoValues(&a, &b); printf("Returned values: a = %d, b = %d\n", a, b); return 0; }
|
2.返回指向数组的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h>
int* getTwoValues() { static int values[2]; // Static array so it persists after the function returns values[0] = 5; values[1] = 15; return values; }
int main() { int *values = getTwoValues(); printf("Returned values: values[0] = %d, values[1] = %d\n", values[0], values[1]); return 0; }
|
3.返回指向结构体的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <stdio.h> #include <stdlib.h>
struct TwoValues { int val1; int val2; };
struct TwoValues* getTwoValues() { struct TwoValues *result = (struct TwoValues *)malloc(sizeof(struct TwoValues)); result->val1 = 50; result->val2 = 60; return result; }
int main() { struct TwoValues *values = getTwoValues(); printf("Returned values: val1 = %d, val2 = %d\n", values->val1, values->val2); free(values); return 0; }
|
这里解释一下,定义变量,其实就是开辟一片内存空间。我们一般在函数中定义的变量,都是局部变量,被存储在栈中,而malloc开辟的空间,是在堆中。栈的空间在函数结束后会被回收,而堆不会,必须手动清理,否则它永远存在,并且会造成内存溢出等问题。所以,我们可以使用动态内存分配来存储变量的值。
当然,你要是对地址、空间、内存这些东西感到烦躁,也有别的方法。不一定只能使用指针。
直接使用结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <stdio.h>
struct TwoValues { int val1; int val2; };
struct TwoValues getTwoValues() { struct TwoValues result; result.val1 = 100; result.val2 = 200; return result; }
int main() { struct TwoValues values = getTwoValues(); printf("Returned values: val1 = %d, val2 = %d\n", values.val1, values.val2); return 0; }
|