C 指针与内存地址

发布于 2022-01-17  4 次阅读


PS: 本文主讲 C Language的 Pointer和Memory. 内容全凭个人理解而写,若有不同见解可以评论讨论


Pointer

在了解指针是什么东西之前,首先来了解一个特殊符号 &。一般情况下我们在日常交流或者表达&用于表达and,但在编程语言中有很多不同的表达

  • 对于一个变量 a 来说 使用 &a 表达的是这个变量的地址
  • 同时可以用a&b 做变量 ab 的合取(AND)位运算
  • 而对比较而言的and在c中的表示为 &&
  • PS: 不知道啥是 合取的可以看 逻辑语言入门 一节

通俗的理解指针就是,得到想要的地址,有了地址就可以上门做很多事情大雾

  • 例如,我知道一个人是谁,但是我无法改变这个人的状态。但如果我知道他的地址,我就可以直接上门给他来一拳,让他好好学习,不争气的东西弥天大雾
  • 在 编程语言中,有很多变量可能是primitive的也就是不可更改的,也就是说当你更改一个primitive变量时,在内存中的运作实际上是,创建了一个新的value并将你变量指向新的value. 如果我们知道初始值的地址的话,直接将这个地址上的值更改,而变量还是指向这个地址,但是其值已经被我们更改过了 而非是通过直接更改变量值得到的新地址.

下面来看个赋予值的例子

int a = 1;
int * address_a = &a;

在此处

  • *表示指针的申明,
  • int *表示该指针指向的数据类型为int
  • *address_a的值与 a相等 因为本质是同一个地址
  • *address_a的运作 实际是解析 address_a = &a 这个地址, 从而得到 a的值,这里的*表示解析

指针的运算

接着上面的例子,以及*的定义
试想一下 执行*address_a ++a 的值为多少?

  • 2?
  • 1?

结果是2。未免有点纳闷,这计算和直接执行a++一样,为什么我们还需要用到指针呢?
这就不得不再重复重复过很多遍的点

  • primitive variable cant be immutable 原始数据不可变

举个例子

void func(int num){
num++;
}

运行func(a)后,我们会发现num并没有变化这是因为int为原始数据类型
但是

int num;
void func(int* num_ptr){
(*num_ptr)++;
}

运行func(&num)后,我们会发现num竟然神奇的加了1,这就是指针的妙用

地址的运算

书接上面的例子,加上下面几句

int *b;
b = address_a + 1;

上面我们说过 指针的变量本身是一个地址,对于地址的运算会产生什么变化呢?
在这里 我们将b赋予address_a + 1的值,但实际上我们运行出来会发现 地址实际上 + 4,这是因为int类型的大小为4bytes。有时候我们会发现变量地址,与赋予的地址不一样,这是因为不同visualizer的显示方式可能有所不同,但是pointer的值永远指向的是绝对地址
这里列出不同类型的大小

  • int: 4 bytes
  • char: 1 byte
  • short: 2 bytes
  • long: 8 bytes
  • pointer: 8 bytes

数组与指针与数组的指针

当我们将数组本身当成一个地址时, 例如

int num_array[5] = {1,2,3,4,5};
func(num_array);

另一个例子

int sum(int * a, int size){
    int sum = 0;
    for (int i=0; i<size; i++){
        sum += a[i];
    }
    return sum;
}
int main(){
    int a[3] = {0, 1, 2}
    int result = sum(a, 3);
}

此处调用sum方法时我们写入的为数组a, 此时就被当作a[0]地址使用

在执行过后,num_array[0] 将会加1。这说明了数组不加[]是可以当作指针来使用,而默认的数组(没有[])表达第一位值的地址。但要注意,数组与指针 还是有很多不同
在举个指针与数组的例子

int c[3] = {1,2,3};
int * address_c = c;
  • address_c[0]在这里等同于c[0]
  • 数组[i]的地址 = 数组[0]的地址 + i
    • *address_c在这种情况为1即c[0]
    • *(address_c + 1)在这种情况为1即c[1]
    • address_c[k] == *(address_c + k)

C数组可以out of index?

书接上面的例子,我们可以尝试给c[4]赋予一个值,学过其他编程语言的人可能会意识到out of index error,在c里可能会有部分ide报错,可以成功运行。
在上述情况下,程序内存是这样的(我随机捏造2个初始地址):

  • 0x250c
  • 0x690address_c

在上述条件下,两者的大小分别为:

  • 0x250 - 0x25c 12 bytes,因为有3个int
  • 0x690 - 0x698 8 bytes, 因为指针的大小为8

在执行c[4] =3后,在内存中:

  • 0x25d - 0x260 会被写入值3,如果在该区域有值,亦会被覆盖

函数的指针

我们可以指针指向变量,指向数组,当然也可以指向函数。那么怎么样才能去指向一个函数呢,让我们从一个例子开始。

int func(){
    return 1;
}
int main(){
    int (*x)() = func;
    return x();
}

在上述例子中,我们将 x 作为指针指向了 func,具体的

  • * x 声明 变量x是一个指针
  • () 声明 所指向函数需要的参数

有没有发现一件事,我们可能有许多函数所需的参数类型与参数数量都一样,也就是说我们这个指针可以指向任意一个满足该条件的函数。也就是说我们可以利用指针当接口来调用不同的函数。

内存地址

很多人对内存的分配不是很了解笑死我也不了解,在这里,代码运行一般从靠近0但不能为0的地址开始, 对c程序的内存地址的分配浅析一下
一般的内存组成如下:

  • Buffer 缓冲
  • Code 代码
  • Global Data 全局数据(静态 static区 和 常量区)
  • Heap 堆
  • Stack 栈
  • OS 系统

Code 代码

代码也会占有内存

Global Data 全局数据

  • 绝大多数全局变量都储存在各个函数外(包括main), 这使得每一个函数方法都可以调用这个变量,这同时意味着,栈内是不会有全局变量(在其他编程语言可以通过申明来使变量全局化)
  • 一些例如 字符串"String literals" 也会被存入全局变量被调用
  • 所有static数据亦会存入里面

Heap 堆

执行动态内存分配类的函数时,所被分配变量会被存在堆中

  • malloc函数是一个很好的例子,其分配所需的内存空间,并返回一个指向它的指针。这一类动态地址的不会像栈的内容一样被覆盖或清除掉

malloc函数

当我们查看api时,会发现malloc的前缀时void *,这是因为

  • malloc 返回指向已分配内存的地址的指针,它不需要知道访问该内存所需的指针类型。(因为调用的时候会告诉地址大小,所以不需要在意具体是什么类型)
  • malloc 可以用于为许多不同的数据类型分配内存。
  • free 函数可以有效 解决内存溢出问题(memory leak) - 无法调用堆中的值

Stack 栈

根据程序所含的函数/方法而将内存分成不同的栈(stack)

  • 一般的我们会在main函数中调用其他函数/方法,从而调用其所对应的栈。
  • 局部变量之所以是局部变量因为其储存在栈中只会为一次函数调用所储存,并且用完就会被该栈会被pop out。下次在调用这片地址的可能是不同函数。

我内存学的有点垮,等我学业有成再回来修改


浊酒情殇逝,失心狂傲往。 无情者伤人,有情者自伤。