飞道的博客

[计算机系统]:理解指针

344人阅读  评论(0)


写在前面:

最近在看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$

该声明有两个效果:

  1. 在内存中开辟了大小为 L ∗ N L * N LN的连续内存空间,其中L为类型T的字节大小。
  2. 声明了一个标识符A,使其指向这个连续内存空间的首地址 M A M_A MA

则任意数组元素 i i i的地址为: M A + L ∗ i M_A + L * i MA+Li

同理,当我使用 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 所代表的地址值是一样的,但类型不一样。


最后,在阅读文章时,有何问题或者建议,欢迎评论或私信指正!


转载:https://blog.csdn.net/qq_41897558/article/details/127969913
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场