漫谈C语言指针(一)

3,029 阅读11分钟

更多博文,请看音视频系统学习的浪漫马车之总目录

C内存与指针:

漫谈C语言内存管理

漫谈C语言指针(一)

漫谈C语言指针(二)

漫谈C语言指针(三)

上一篇漫谈C语言内存管理主要讲解了C语言内存管理相关的内容,今天在上一篇的基础上漫谈下C语言的一大精髓——指针。和上一篇一样,假设大家都是有基础的,谈一些比较本质的,一些偏进阶的内容。

什么是指针

C语言里,指针一直是一个难点,初学者容易混淆的地方,但是指针本身其实很简单,指针就是一个存放整数的变量。

C语言中,变量存放在内存中,而内存其实就是一组有序字节组成的数组,这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量起始地址的变量

如图为4GB 的内存的分布图: 4GB 的内存的分布图

什么叫做指向变量首地址呢?比如:

int a = 10;
int *p = &a;

用图表示就是: 在这里插入图片描述

为什么要使用指针

如果是写过类似Java、kotlin等高级语言的程序员,对引用肯定非常熟悉了,其实引用就是个简洁版的指针,因为指针实在太灵活,用得不好容易出事故,所以这些高级语言就简化为引用。

引用就像普通钥匙,我们可以用它去打开某种类型的门,其他类型数据无法打开,并且就算打开了门的具体编号(地址)也不会暴露给我们。引用不能通过移动位置(算数运算)去打开其他门,只能指定去打开某个门。

而指针虽然也有类型,但是却并不是要求一定要指向该类型的数据(至于指针的类型的作用后面会讲),指针有点像万能钥匙,虽然指定打开这某种门,但是其他门也是可以打开的(当然读取数据可能会有错误),更厉害的是可以移动任意位置(算术运算挪动指针指向)去打开其他门,门牌号(内存地址)也是暴露给我们的。这样会灵活很多,开发者的操作权限会很大,当然带来的风险也会高很多。

(上面的比喻可能不是很恰当或者难以理解,简单来说就是我可以使用指针直接操作内存,读也好,写也好,怎么样都好,内存的数据尽在我手中,内存的命运尽在我手中,我想读哪里的数据,我想往哪里写什么数据,都随我意。所以一旦操作不当,就会有程序事故,比如访问到没有访问权限的内存导致程序奔溃,比如常见的数组越界和野指针访问就是这个原因导致的奔溃)

总的来说,使用指针有如下好处:

1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;

2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;

3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。

4)可以通过算术运算灵活操作内存。

1和2比较容易理解,主要谈下3和4.

通过指针在函数内部修改函数外部变量值

我们知道C语言函数是传值的,所以不能在函数里直接修改函数外的变量的值,比如:

int changeValue(int x){
    x = 9;
    return x;
}

int main(){
    int x = 10;
    changeValue(x);
    printf("x: %d\n", x);
}

运行结果: x: 10

任你在函数里怎么修改,就是拿函数外的变量没办法,因为函数传参的时候,是拷贝了一个副本,然后在函数里面操作的,所以再怎么修改改的都是函数内部的一个副本,和外部的变量无关。我们可以通过打印出地址来验证:

void changeValue(int x){
    x = 9;
    printf("changeValue %#X\n", &x);
}

int main(){
    int x = 10;
    changeValue(x);
    printf("main %#X\n", &x);
}

打印结果: changeValue 0X62FDF0 main 0X62FE1C

可见函数内外的x并不是同一块内存区域的数据。

这时指针排上用场了,既然传参传的是值,那么如果参数为指针,那传的也是地址的值,同个地址对应的变量也是同一个,那就可以直接通过修改该地址指向的内存数据来修改函数外的变量了:

void changeValue(int* x){
    *x = 9;
}

int main(){
    int x = 10;
    changeValue(&x);
    printf("x: %d\n", x);
}

运行结果: x: 9

修改成功~~

指针算术运算

了解指针的算数运算,就要先了解指针的类型和指向的类型。

指针的类型

与指针类型相关的主要有2点: 1.指针自身类型 2.指针所指向的类型

指针自身类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。

比如

int * ptr;//指针的类型是int *

但是指针本身的类型意义不大,关键是要拿到指针所指向的类型 。

指针所指向的类型

从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。 比如:

(1)int * ptr; //指针所指向的类型是int

(2)char * ptr; //指针所指向的的类型是char

(3)int * * ptr; //指针所指向的的类型是int*(指针的指针,俗称二级指针)

(4)int( * ptr )[3]; //指针所指向的的类型是int()[3](数组也可以看成一种类型)

(5)int * (  * ptr)[4]; //指针所指向的的类型是int *(数组指针也可以看成一种类型)[4]

//更加复杂的情况

为什么说指针所指向的类型很重要呢?

一方面,当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

比如:

int a = 10;
int *p = &a;
printf("*p=%d\n", *p);

当执行指针取值的时候,cpu知道了p指向的类型是int ,所以接下来会读取接下的size(int)个字节并按照按照int的格式解析得到对应的值。

另一个方面,指针所指向的类型决定了指针的步长。指针的步长是什么呢?就是指针在做算术运算的时候,加减1,在实际内存中的加减地址数。

int main(){
 	printf("int length: %d\n", sizeof(int));
    int a = 10;
    int *p = &a;
    printf("p=%#X\n", p);
  	p++;
    printf("p=%#X\n", p);
    return 0;
}

运算结果: int length: 4 p=0X62FE14 p=0X62FE18

注意到虽然对指针p加了1,但是打印出来指针p的值实际上是加了4,正好是sizeof(int)的值。如果把a改为其他类型,你会发现p增加的量都是等于a的类型对应的空间大小。

为什么要这样说设计呢?因为指针是一种特殊的变量,它专门存放其他类型变量的地址,如果指针加1也是像其他变量一样在数值(即地址上)加1,指针会指向一个变量的中间,那实际上没有意义。

比如刚才的例子,p原来指向

在这里插入图片描述 p++后假如p仅在数值上加1: 在这里插入图片描述 这样p取值得到的数值会很诡异,对于程序开发来说没有什么意义。

我们要的效果是p++后,从a的数据移动到下一个同样类型的数据: 在这里插入图片描述 指针算术运算的意义是什么呢?比如读取一段内存缓冲区里面的数据,类似遍历一个数组,这时候指针指向的类型将决定读取的数据是怎样的,比如a就是一段内存缓冲区,因为a是一个字符串组,我们一般用char*的指针地遍历读取:

int main(){
 	char *a = "abcdefgh\0";
    char *ptr=a; 
    while (*ptr != 0){
        printf("char %c\n", *ptr);
        ptr++;
    }
    return 0;
}

运行结果: char a char b char c char d char e char f char g char h

假如把ptr指向的类型改为short:

int main(){
 	char *a = "abcdefgh\0";
    short *ptr= (short *)a; //强制类型转换并不会改变a 的类型
    while (*ptr != 0){
        printf("char %c\n", *ptr);
        ptr++;
    }
    return 0;
}

运行结果: char a char c char e char g

可以清楚看到,遍历的时候,是每次跳2个字节(当前环境short 2个字节)进行遍历的,所以指针的步长会决定如何读取一段内存的数据。

总结下,指针算术运算中,指针的值每次加1(减1),就是在原来值基础上加(减)sizeof(指针指向的类型)个字节。

所以,每当遇到指针,都要发出灵魂的拷问:每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?想清楚了,很多问题就迎刃而解了~

异常指针

空指针

写Java的童鞋对于空指针简直是老熟人了,C语言就更有拥有空指针的权力了(毕竟它才有真正的指针),不过Java由于属于高级语言,屏蔽掉了很多底层细节,所以空指针就是一个没有指向任何对象的引用,C语言也类似,一般我们会将一个指针指向NULL表示空指针,单实际上空指针并不是完全没有指向的指针,而是指向了不可读写的内存地址的指针,NULL的定义如下:

#define NULL ((void *)0)

实际上在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,所以指向这段地址的指针不能进行操作。C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来。理论上NULL可以指向保留区中的任何地址。

野指针

野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。野指针的一般场景:

1,指针变量未初始化: 任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指。所以指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

2.指针释放后未置空 有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

下一篇:漫谈C语言指针(二)