对象
C语言中所有用到的数据都存储在内存中,从硬件方面来讲每个被存储的值都占用了一定的物理内存,C把这样的一块内存称为对象,对象和面向对象编程中的对象不是一个含义。
int a = 1;
之前提到过,对于编译后的程序而言,是不存在变量名的,这里的标识符 a 仅为了方便程序员操作内存,标识符代表了一块内存即对象,1 就是这块内存里的值,在接下来的程序中使用 a ,程序会认为你想要使用 a 所代表的内存里面的值。
指针
上面讲到,正常声明的标识符用来指定特定对象的内容,而不是特定对象。
int * pt = π
上面 pt 是一个标识符,它指定了一个存储地址(&pi的值)的对象(pt)。
对于指针而言,星号一般出现在两个场合,一个是指针定义的场合,一个是指针使用的场合。在定义指针的场合,就是在定义指针时前面加个星号而已
指针的定义
int * pt = π
变量名叫 p ,类型为 int ,pt 是标识符,int 代表 pt 只能存放 int 类型变量的地址,所以这是一种新的类型,pt 就是指针变量,乱赋值会报错
操作指针
#include <stdio.h>
int main(void) {
int* p;
int i = 3;
p = &i;
return 0;
}
p 和 i 本身没有区别,除了在定义时标识符兼职代表对象外,其他任何时候代表对象中的内容,这里也不例外,相当于编译器在我们使用标识符的时候替我们做了 操作。
*号代表指针取值操作,拿着指针变量中存的地址号去内存中找里面存的东西。
不同类型的指针
#include <stdio.h>
int main(void) {
int* p;
int i = 3;
double* q;
double j = 0.1;
p = &i;
q = &j;
printf("%ld",p);
printf("-");
printf("%ld",q);
return 0;
}
// 140734390721532-140734390721520
每个指针类型对应的指针大小都是一样的即每个内存的第一个字节的地址,编译器根据标识符对应的对象拿到第一个字节的地址后根据不同的类型,向后截取的几个类型字节的长度。
指针在内存中所在的字节数是由系统的寻址能力决定的,寻址能力就是CPU对于该数据范围处理的极限能力,理论上32位系统的寻址能力是2的32次(4GB,也就是现在的虚拟内存为4GB),也就是32bit,按照一个字节8bit来算就是4个字节。16位系统是2个字节,64位的就是8个字节。寻址能力是由系统的硬件决定的,也就是总线的位数。
地址和指针
CPU 直接操作内存条,64 位 CPU 代表可以操作 16G 内存,地址就是一个从 0 开始的非负整数,范围就是 64 位二进制的范围。
地址(address)和指针(pointer)并不是一种东西,指针还附带类型,这是编译器赋予的,比如我们想要获取 0xffffffff 的两个字节,单纯有地址是办不到的,需要指针来规定类型从而编译器可以截取。
“1000” 地址 可看作整型 值 1000
“1000” 指针 指针类型(int *) 值 1000
在接下来的程序中使用 1000 ,编译器会默认根据指针的类型来截取并读取,而地址只是一个地址而已,指针和地址的区别是编译器赋予的。
地址可以理解为一个 64 位常量,指针可以理解为存在一个变量里的地址,可以有多个变量指向同一个地址,指针的概念通常是带有指向含义的。
理解指针的局限
初始化一个变量后,这个变量的地址确定了,当程序结束后这个变量的地址改变了,指针是做不到这种操作的,也就是说变量地址是 immutable 的。
数组
数组在多数表达式中,隐式转换为指向其首元素的指针右值,称为数组退化。数组名被编译器看作一个常量(指针常量),即数组第一个元素的地址,但是数组名加下标被视为变量。
通常可以在和数组声明同一个块作用域下使用 sizeof(array)/sizeof(int) 的方法来获取数组长度信息, 但是如果你把数组传入到其他的块作用域下面,不管你情愿不情愿,不管你的函数参数声明是数组类型还是指针类型, 你的数组都再没办法这样来获得长度信息。
因为在同一个函数块内, 数组变量的类型其实是具有多大的容量的什么类型数组,在另一个函数块内,不管是参数是数组类型还是指针类型,传入的array都会变成一个指向 int* 的指针, 而没有长度信息。
所以叫退化, decay。decay 这种行为可以理解为 C 的发明者为了操作方便而建立的规则。
#include <stdio.h>
int main(void) {
int a[4] = {0};
printf("%#x\n",a);
printf("%#x\n",&a[0]);
printf("%#x\n",&a);
return 0;
}
//0x96afff80
//0x96afff80
//0x96afff80
a 和 &a[0] 是一个东西都是 int 类型的指针,但是 &a 是 int ()[4] 类型的指针代表指向长度为4的数组的指针
a 可以被看做指向数组首地址的指针。但是!这种说法在以下两种情况下不成立:
sizeof(a);
&a;
在这两种情况下,a是单独的类型,即「长度为4的int数组」类型,而不是「指向int的指针」类型。
求sizeof(a)得到的是数组的尺寸16。
求&a得到的不是「指向指针的指针」类型,而是「指向长度为4的数组的指针」类型,即 int (*)[4] 类型,此处4不能省略,因为「指向不确定长度数组的指针」是没有意义的,编译器若不知道该指针指向的类型,就无法编译指针的加减法运算(指针指向类型的长度未知,加减法的位移量就未知)
int (*)[4] 数组指针解引用等价于数组名
int a[4] = {0,88};
printf("%d\n",(a)[1]);
printf("%d\n",(&a[0])[1]);
printf("%d\n",(&a)[1]);
//88
//88
//1143774224
上面三个输出可以清楚地了解到数组的本质,换个说法编译器是怎么理解中括号的,就相当于中括号前的标识符所代表的必须是指针,中括号意味着对这个指针加一操作(根据指针运算指针的加减 n 会默认对 n 乘上类型长度的值,得出偏移后的地址),编译器把 标识符[] 视为一个变量那么就会自动取到地址上的存储的值。
这样就可以解释下面代码
a[5] == 5[a]
*(a + 5) == *(5 + a)
数组名的含义
如果一个表达式或标识符代表一个数组(代表指的是退化前的数组指针),例如数组名,int arr[2][5] 中的 arr[0] 和 arr[1],int (*t)[4] t 的解引用都是代表了一个数组。
// 类比其他语言中,arr 代表了一个数组
let arr = [];
此时这些表达式使用起来都和数组名一样(遭到decay)
指针的运算
指针不能相加相乘相除,如果两个指针指向一个连续存储空间的不同单元才可以相减
void*
void 能包容地接受各种类型的指针。也就是说,如果你期望接口能够接受任何类型的参数,你可以使用 void 类型。但是在具体使用的时候,你必须转换为具体的指针类型。例如,你传入接口的是 int ,那么你在使用的时候就应该按照 int 使用。
左值和右值
左值是内存空间,右值是个值,左值可以是一个表达式例如 p = &a ,p 就是左值,但是 p 也可以是右值。
right: storage
left: value
函数指针
函数是一段机器码,存放在程序的代码区,函数指针的实现就是保存那段机器码的地址,让我们感到迷惑的是函数指针可以用函数调用一样的语法来调用对应的函数。这是 C 的句法的问题。
returnType (*pointerName)(param list);
int (*pmax)(int, int);
maxval = (*pmax)(x, y);
int add(int a, int b); //函数定义
int (*add)(int a, int b); //函数指针
任何变量的指针就是定义时变量名换成 *变量名
函数名只有在作为 sizeof 或者单目 & 操作符的操作数时,它的类型才是函数;其它情况都会被转化为指向该函数的指针。
[
](https://link.zhihu.com/?target=https%3A//port70.net/~nsz/c/c99/n1256.html%236.3.2.1p4)
&func 根据上一条此处得到的就是函数指针,给函数指针赋值时,可以用&fun或直接用函数名fun。这是因为函数名被编译之后其实就是一个地址,所以这里两种用法没有本质的差别。
[
](https://link.zhihu.com/?target=https%3A//port70.net/~nsz/c/c99/n1256.html%236.5.3.2p3)
func 得到函数地址,是因为本来就有相关的规定,表达式 函数 的值是对应的函数指示符,于是参见第一条。 func 前面可以有无数个*
指针的步长
指针的步长有两个用处,指针 + 1 移动的距离,以及解引用指针取到的字节数。