写在前面:
最近在看csapp这本书,对于一个非科班出身的我来说,犹如神书!在理解了计算机底层原理后,从头理解一下之前让我爱了又恨的指针,想来也会有一些别样的收获。
之后我会将我在学习计算机系统的过程中的感悟和笔记,放在专栏里,如果感兴趣,Please Follow me!
1. 指针映射到机器代码的原则
初识指针,对他的认识大概就停留在 “指针的值是对象的地址”,至于地址是啥?在计算机中是如何表示的?ps: 之后会另起篇幅讲解.对于为什么要这样表示更是一头雾水。
细细想来,指针,作为C/C++语言的一大特色,它以一种统一的方式对不同的数据结构中的元素产生引用,譬如,基本的数据类型(int,float …),数组,结构体,甚至于函数!都可以用指针的形式引用他们。是不是很奇妙,对于不同的数据结构,有统一的表达形式,这是多么美妙的事情呀!当然,这绝非偶然,统一的表达形式必然对应的是统一的底层逻辑。
那么将指针映射到机器代码需要遵循什么原则呢:
1.1. 每个指针都对应着一个类型
这个类型表明了该指针指向的是哪个类型的对象;
int *p;
char ** cpp
void * v
变量p是一个指向int类型的指针,而cpp指向的对象本身就是一个指向char类型的指针。
特殊的,void * 类型代表的是通用指针,比如说 malloc
函数返回的就是void *类型的指针,有类型的指针可以直接赋值给void *类型的指针,而void *指针需要经过强制类型转换将其转化为目标类型的指针。
int a = 1;
p = &a;
v = &a;
p = (int*)v;
指针类型不是机器代码的一部分,是C/C++提供的高级抽象,以帮助程序员寻址错误。
1.2. 每个指针都有一个值
这个值是指针对应的指定类型的地址,特殊的,NULL(0)没有指向任何地方。
1.3. 指针用 ‘&’ 运算符创建
机器代码中的leaq(load effective address)是用来计算数据内存引用的地址,一般被用来当做指针的实现,即:
# %rdi 为value
leaq((%rdi), %eax) # load %rdi address to %eax
1.4. ‘*’ 运算符用来间接引用
其结果是一个值,它的类型与该指针的类型一致
1.5. 指针指向数组
C/C++可以产生指向数组元素的指针,并进行运算。
若数据类型为T,常数N,有
T A[N]; // 起始地址为$M_A$
该声明有两个效果:
- 在内存中开辟了大小为 L ∗ N L * N L∗N的连续内存空间,其中L为类型T的字节大小。
- 声明了一个标识符A,使其指向这个连续内存空间的首地址 M A M_A MA。
则任意数组元素 i i i的地址为: M A + L ∗ i M_A + L * i MA+L∗i。
同理,当我使用
A
[
i
]
A[i]
A[i]来访问数组中某个元素时,做的事情其实是 *(M_A + L * i)
,对应的机器代码为:
# %rdi store M_A; %rsi store i
movq((%rdi, %rsi, L), %rax) # get value in (M_A + L * i) to %rax
1.6. 将指针从一种数据类型强制转换为另一种数据类型。只改变他的类型,不改变他的值。
例如:p是一个char*的指针,则表达式(int *) p + 7
为p的首地址
M
p
M_p
Mp,向后寻址28个字节M_p + 4*7
,而(int *)(p + 7) = M_p + 7
。
1.7 指针指向函数
函数指针提供了一种,向代码传递函数引用的功能,例如回调函数等。
我们有一个函数,其函数原型为:
int fun(int x, int *p);
然后声明一个指针,使其指向这个函数,代码如下:
int (*fp)(intx, int *p);
fp = fun;
嗯?怎么把函数名赋值给了函数指针?难道说函数名fun是指针类型?和数组A一样?别着急,我们一会儿再解释这个问题,带着疑问,先往下看。
接下来用这个指针fp
去调用这个函数。
int y = 1;
int res = fp(3, &y);
咦?
fp
不是指针吗?难道函数调用是用的是函数的地址吗?
函数指针的值是,该函数机器代码中的第一行的地址。
2. 函数指针
在上一节的最后我们留下了一些问题,是关于函数指针和函数名的,随之我们提出了一些猜想,即,函数名到底是什么?
现在我们带着疑问,写脚本测试一下, 代码如下:
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
int main() {
printf("[max]: %p\n", max);
printf("Size of max: %ld, Void: %ld \n", sizeof(max), sizeof(void));
printf("[max_pointer]: %p\n", &max);
printf("Size of max pointer: %ld, Void *: %ld \n", sizeof(&max), sizeof(void *));
int(*p)(int, int); // 函数指针
int(*pp)(int, int);
p = max; // 不同的赋值方式
pp = &max;
printf("[p]: %p, size: %ld\n", p, sizeof(p));
printf("[*pp]: %p, size: %ld\n", pp, sizeof(pp));
int a = 1;
int b = 2;
printf("*p func: %d\n", p(a, b));
printf("*pp func: %d\n", pp(a, b));
}
Return:
[max]: 0x55b1269e564a
Size of max: 1, Void: 1
[max_pointer]: 0x55b1269e564a
Size of max pointer: 8, Void *: 8
[p]: 0x55b1269e564a, size: 8
[*pp]: 0x55b1269e564a, size: 8
*p func: 2
*pp func: 2
测试发现,通过函数名就可以得到函数地址,按照&运算符本来的意义,它要求其操作数是一个对象,但函数名不是对象(函数是一个对象),本来&max是非法的,但很久以前有些编译器已经允许这样做,c/c++标准的制定者出于对象的概念已经有所发展的缘故,也承认了&max的合法性。
对于 max 和 &max 应该这样理解:
- max 是函数的首地址,它的类型是 void ()
- &max 表示一个指向函数 fun 这个对象的地址, 它的类型是 void (*)()
因此 max 和 &max 所代表的地址值是一样的,但类型不一样。
最后,在阅读文章时,有何问题或者建议,欢迎评论或私信指正!