1、指针
内存会被划分一个个的内存单元,每个内存单元的大小为1字节(8位),每个内存单元都有一个独一无二的地址值。
未使用的内存空间的地址值叫地址;已使用的内存空间的地址值叫指针。
变量的定义就是为这个变量在内存中开辟内存空间,初始化就是为这个内存空间赋值,变量在定义后,它所开辟的内存空间的地址值就成了指针。
C语言中没有对象的概念,但我们可以将任何数据类型定义的变量视作对象,所以我们可将指针或指针变量指向的内存空间中的数据视作一个对象。
指针具有数据类型,指针的数据类型决定了所指向的对象的数据类型及其量级。
1.1、取地址操作符&
取地址操作符又叫做取地址符:&
。
它的作用是获取一个已经定义的变量(即已经使用了内存空间)的首地址值(首个字节的地址),以及这个对象的数据类型及其量级,也就是获取这个变量的指针。
1.2、指针变量
指针变量和指针不是同一个东西,指针变量是用于容纳指针的变量。
指针变量用于存放地址值,所以指针变量本身的量级取决于地址值的精度,32位操作系统下指针为4字节,64位操作系统下指针为8字节。所以指针变量的数据类型也有量级,在64位操作系统下,所有类型的指针的量级都是8byte
。
所以当二级指针发生偏移时,是根据一级指针的量级来偏移的,可是所有的指针(不论级别)的量级都是统一的,故而任何类型的二级指针的偏移步长都是相同的。
当我们在定义指针变量时,例如:int* pa
,int*
就是指针变量pa
的数据类型,*
指明了pa
是一个指针变量,int
指明了这个指针变量所要指向的对象的数据类型及其量级。
例如:
char a = 1;
int b = 1;
char* pa = &a;
int* pb = &b;
上述代码中,指针变量pa
内置的信息为:所要指向的对象的数据类型应该为char
,量级为1byte
;指针变量pb
内置的信息为:所要指向的对象的数据类型应该为int
,量级为4byte
。
注意,数据类型的量级详见文章:数据类型的量级
故而我们可知:
一个指针变量在定义时,这个指针变量的数据类型内置了一个信息:这个指针变量所要指向的对象的数据类型及其量级。数据类型决定了这个指针变量在发生解引用时,所指向的内存空间里的二进制数据应该以怎样的逆存储方式进行转换;量级决定了这个指针变量所要指向的内存空间的大小,也就是这个指针变量在发生解引用时的访问权限,亦或者说是访问范围,还决定了指针偏移时“单位1”(步长)的大小。
实际上,这就是指针变量的数据类型的作用。
虽然指针也内置了信息,可是当将一个指针赋值给一个指针变量时,并不会将指针所指向的对象的信息传给指针变量,只会单纯地赋值地址值,在后续对指针变量的使用中,依然以指针变量的所指向的对象信息为基准。
若赋值时,指针和指针变量所规定指向的对象的信息相同那是再好不过;但是若不同,例如:
char a = 1;
int* pa = &a;
pa
在后续的使用中,内置的信息依然为:指向的对象的数据类型为int
,指向的对象的量级为4byte
。
所以我们可以得出结论:指针赋值给指针变量、一个指针变量赋值给另一个指针变量、调用函数时指针或指针变量作为实参传址给形参,都只是单纯地传递地址值,并不会传递所指向的对象信息。
1.3、解引用操作符*
解引用操作符*
,唯一作用于指针变量。
它的实际意义是:根据这个指针变量所规定指向的对象,以对象的量级作为访问范围来获得二进制数据,再根据对象的数据类型来决定解码二进制数据的方式(也就是整型家族与浮点型家族的逆存储方式),从而获得所指向的对象。
也就是说,例如int* p
,指针变量p
所规定指向的对象应该是一个int
类型对象,在发生*p
操作时,由于int
量级为4byte
,也就会提取变量p
存放的地址往后4byte
空间里的二进制数据,然后再将这段二进制数据视作int
类型对象的二进制数据,按照对应的逆存储方式(原码-反码-补码)进行解码。
例如:
char a = 1;
int b = 1;
float c = 3.14f;
char* pa = &a;
int* pb = &b;
float* pc = &c;
printf("%d %d %f",*a,*b,*c);
1、pa
的所指向的对象,数据类型是char
,量级为1byte
,char
类型属于整型家族且为有符号数,故转换方式为:原码-反码-补码
,所以*
的作用就是提取从pa
这个地址开始往后1byte
的内存空间中的二进制数据,并以原码-反码-补码
进行转换。
2、pb
的所指向的对象,数据类型是int
,量级为4byte
,int
类型属于整型家族且为有符号数,故转换方式为:原码-反码-补码
,所以*
的作用就是提取从pb
这个地址开始往后4byte
的内存空间中的二进制数据,并以原码-反码-补码
进行转换。
3、pc
的所指向的对象,数据类型是float
,量级为4byte
,float
类型属于浮点数家族,故转换方式为:IEEE 754标准
,所以*
的作用就是提取从pc
这个地址开始往后4byte
的内存空间中的二进制数据,并以IEEE 754标准
进行转换。
注意,即使一个指针变量存放的是相异类型的指针,在发生解引用时,也只会按照指针变量所规定指向的对象进行翻译解码。例如:
int n = 1;
float* pf = &n;
printf("%f\n", *pf);
以上代码中,
整型变量n
的内存视图为:00000000 00000000 00000000 00000001
。
指针变量pf
指向了n
的内存空间。
发生解引用*pf
时,先取得n的内存数据:00000000 00000000 00000000 00000001
。
再将其视作float
类型对象的二进制数据进行解码,也就是按照IEEE 754
进行转换,
也就是:0 00000000 00000000000000000000001
。
最终所得为0.000000
。
1.4、指针的偏移
指针的偏移表现格式为:指针/指针变量+-n
。
指针的偏移是指:指针或指针变量在进行加减n时,会根据所指向对象的量级作为偏移步长(单位1),从而改变所指向的位置。
1.5、void* 指针
void*
类型名为空类型指针,它与其他指针的唯一不同是:它不指向任何对象,也就是说它不具有所指向对象的数据类型和量级信息,故而它不能发生解引用,以及指针变量的偏移。但是可以使用类型转换符将它转换为有效类型的指针。
它的作用是:可存放任何类型的指针。
1.6、常量指针与指针常量
1、const修饰一般变量:
const
关键字修饰变量后将变量的值锁死是发生在编译层面,而指针变量发生解引用后对数据进行修改是发生在内存层面,故而通过解引用指针变量来修改所指向变量的值是不受const
关键字限制的。
这里也可看出,解引用指针变量来访问所指向的变量是一种间接访问方式。
2、常量指针
const int* pa = &a;
此时const
关键字限制的是*pa
,也就是说const
锁死的是指针变量pa
解引用后的内容,即指针变量pa
指向的对象。在此情况下:不能通过解引用指针变量(*pa
)的方法来修改所指向的对象的内容,但是指针变量pa
本身的值是可以改变的,pa
可以被修改来指向其他对象。
注意:
1、常量指针是将指针变量通过解引用来改变所指向的对象的功能锁死了,并不是将所指向的对象改变为常量。
2、在写代码时,权限为只读的数据的指针变量都用关键字const
修饰,以提高安全性。
3、指针常量
int* const pa = &a;
此时const
关键字限制的是pa
,也就是说const
锁死的是指针变量pa
本身的内容。在此情况下:可以通过解引用指针变量(*pa
)的方法来修改所指向的对象的内容,但是指针变量pa
本身的值是不能被改变的,此时的pa
唯一指向当前对象。
可参考文章:常量指针与指针常量
1.7、指针/指针变量的运算
指针与指针变量的运算规则是一样的,故只给出指针的运算规则:
1、指针 ± 数字
其实这种运算就是指针的偏移,运算式中的单位1
(步长)由指针内置信息中的数据类型量级决定。
2、指针 - 指针
只有指向同一块内存空间的指针之间才能发生运算(例如指向同一个数组的不同元素的两个指针),且指针之间只有相减运算,结果的绝对值是指针和指针之间的元素个数,也可以理解为相差的量级个数。
3、指针与指针之间的关系运算
指针与指针之间可比较大小,比较的依据是各自的地址值。
1.8、野指针
概念:野指针就是指针变量指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
野指针的成因:
1、指针变量未初始化,因为局部变量在未初始化时。
2、指针变量越界访问。
3、指针变量指向的空间释放。
如何规避野指针:
1、如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL
,NULL
是C语言中定义的一个标识符常量,值是0
,0
也是地址,这个地址是无法使用的,对赋值NULL
的指针变量进行解引用、读写该地址都会报错。
2、一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
3、指针变量不再使用时,及时置NULL,指针使用之前检查有效性。
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL
。因为约定俗成的一个规则就是:只要是NULL
指针就不去访问,同时使用指针之前可以判断指针是否为NULL
。
我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL
,其实就类似把野狗栓起来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL
,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。
4、避免返回局部变量的地址。因为一个函数在调用完毕后,内部的局部变量的空间会被释放掉。
1.9、传值调用和传址调用
1、函数的传址调用相比于传值调用更节省空间,因为传值调用的实参可能很大,这导致函数内创建的形参也很大,就会占用比较大的空间;而传址调用的实参和形参都是指针变量,只需要很小的空间。
2、在函数没有返回值的情况下,传值调用的函数是无法改变主调函数(调用别的函数的函数叫主调函数,被调用的函数叫被调函数)中的变量的,但是传址调用的函数可以,因为指针变量改变所指向的对象是在内存层面。