三、总结指针知识
1 笔试积累
- 指针的全称为:指针变量。
- 指针访问时需要注意其指向的内存,避免野指针和悬空指针的情况出现。
- const修饰的变量并非一定无法更改。
2 理解和思想
2.1 指针变量是外挂
C/C++
语言提供的指针变量类型是高级语言访问内存中的数据(变量或常量等其他数据)的外挂手段。一般的高级语言,如Java
,python
等都只能通过变量符号或其他与数据绑定的符号
进行数据访问。
2.2 指针变量的本质
- 平时称呼时要牢记其本质是个
变量
,唯一区别只是这个变量的意义被编译器限制为存储其需要指向的内存地址
。 - 指针变量中存放的是内存地址,准确的说是匹配的数据类型的变量的内存地址,一般使用符号
&变量
来得到该变量的地址。 - 通过指针操作其指向的这个地址处的内存数据,使用解引用符
*
来访问该数据,例如定义一个指针变量int *p
,若其指向与指针变量匹配的类型int a = 4321; p=&a
则解引用给变量a
写入数据1234
时需要使用*p=1234
。
2.3 指针变量非法操作
-
注意使用解引用时,是以该指针匹配的类型来解释该内存数据的,例如上面提到的指针变量若更改为
double *p
,则编译器会报类型不匹配的警告,强行通过的话,在*p=1234
赋值操作后,会非法操作该段内存,虽然以double
类型去解引用这段内存可能会因为访问到非法的内存区域而发生段错误(见代码段中注释1
和结果1
),且破坏了原来的int a
的存储结构,再次读取变量a
时,将无法得到正确的结果(见代码段中注释2
和结果2
)。代码及运行结果如下:#include <stdio.h> int main() { int a = 4321; double *p = &a; /*解引用操作前打印结果*/ printf("a is [%d]\n",a); printf("*p is [%lf]\n",*p); /*解引用操作后打印结果*/ *p = 1234; printf("a is [%d]\n",a);/*注释2*/ printf("*p is [%lf]\n",*p);/*注释1*/ return 0; } /**************************** 运行结果为: a is [4321] *p is [-146615434496.000000] 结果2:a is [0] 结果1:Segmentation fault (core dumped) ****************************/
2.4 野/悬空指针
- 野指针是在定义指针变量时没有及时初始化该变量,即没有手动为该指针分配一个需要指向的地址,此时该指针被称作为野指针。因为野指针指向的地址是不可预知的,所以使用野指针会带来意想不到的危害。
- 若该野指针指向不被允许的地址,例如操作系统的内核空间地址,则在运行时会发生段错误。手动强制转换数字4为对应的指针变量类型后分配给指针,运行时访问该地址也会发生段错误,若不将数字4强制转换,则编译器报错,运行时段错误。小提示:由于NULL在C语言中数值为0,若分配指针变量指向数字0,则编译不会报错,运行时仍然段错误。
- 若该野指针指向被允许的地址,但该地址是程序正在使用的地址,例如存放着某个正在使用的变量,则会篡改这个变量的值,使得程序发生莫名其妙的问题,也大大增加了排查问题的难度。
- 悬空指针是在使用完该指针指向的内存后(即释放了该内存,例如free接口),没有及时将该指针指向NULL,导致下次使用该指针时仍然指向了之前的内存,最终非法访问该段内存。
2.5 NULL的本质
- 首先需要确认的是,
NULL
符号的本质就是数值0
。 - 但是在C语言中,编译器对NULL有类型检查的机制,规定其类型必须为
(void *)
,即整体为(void*)0
。 - 另外在C++语言中,对
NULL
符号的定义即为数值0,默认int,没有类型检查。
2.6 const与指针
-
首先,const修饰的变量在编译器层面是常量,
gcc编译器
(注意某些单片机编译器并非如此)要求凡是被const修饰的变量不允许被更改。注意,如果通过指针这种外挂方式操作const变量,是可以骗过编译器从而实现运行时对该变量进行修改的目的(根本原因是该变量存放的地方和普通变量一样)。 -
const与指针组合共四种情况,其中有两种情况意义一致。分析时抓住const是修饰指针本身还是指针指向的空间来区分,从符号来说,对应着指针变量符号以及对指针变量的解引用两种情况:
1、const int *p:该情况说明const修饰了一个整体,该整体是对指针的解引用,故指针本身不受const限定,而其指向的变量受const限定。
2、int const *p:这种情况仍然是const修饰指针指向的变量,即情况同第一种。
3、int * const p:该情况显然是修饰指针变量本身的,即指针变量受const限制,而其指向的变量不受const限定。
4、const int * const p:这种情况是两种情况的结合,即指针和指针指向的变量均受到const的限定。
-
虽然,编译器会报警告“ 警告:初始化丢弃了指针目标类型的限定”,这个意思是,
b失去了对目标对象的const的限定
。但是通过,并且,可以通过指针b
更改它们共同指向的空间。const int a=10; int * b=&a;
-
要想骗过编译器警告,也有办法,上面之所以报警告是因为,
指针b
初始化时被赋值了一个带const
限定的地址,而其本身却没有携带该限定。只要在操作时不要直接操作带限定的指针去赋值就可以完美躲避编译器警告。如下代码所示:将a
的地址强制转换成一个int
类型的数值,并赋值给一个不带限定的变量并将该变量再转换成一个指针并赋值给最终的指针c
。const int a=10; int b=(int)&a; int * c=(int *)b;
-
强制类型转换警告:
地址到数值不报警告
(因为数值类型并不能访问内存空间),数值到地址不报警告
(编译器本身就允许这样做)。
2.7 数组和指针
-
程序员操作数组有两种方式:数组下标(数组名[下标]从0开始)和指针解引用(*(指针+偏移量))。两种方法的本质都是通过指针的运算获取到变量的最终地址,进而操作变量的。
-
根据数组定义可知,其在一块内存中连续分配了一批相同数据类型的变量,这也为使用数组下标进行数据操作提供了可能。因为编译器能根据数据类型大小和数组下标以及首元素地址直接计算出需要访问的变量的地址,从而实现下标访问的功能,提高了开发程序的效率。
-
数组相关的地址有三种情况,以
int a[10]
为例:1、&a:是一个常量,故只能做右值,表示整个数组的地址,数值上和数组首元素的首地址相等(见下面代码中
查看各符号数值
)。,但意义不同,主要体现在编译器的类型检查(见下面代码中做类型匹配
)以及指针运算时的步进值不同(见下面代码中做指针运算
)。2、&a[0]:也是常量,故只能做右值,表示数组首元素的首地址,数值上和整个数组的地址相等,但意义不同,主要体现在编译器的类型检查以及指针运算时的步进值不同,具体见代码段中所示。
3、a:是数组名称,因数组只能单个元素操作,故其没有做左值的情况,即不存在
a = {1,2,3}
,只能在初始化时使用int a[10]={1,2,3}
。做右值时意义和&a[0]相同,均表示数组首元素的首地址。具体见代码段中所示。#include <stdio.h> int main() { int a[10] = {0}; int *p = NULL; int *q = NULL; int *m = NULL; int (*n)[10] = NULL; //定义一个数组的指针类型 /*1、查看各符号数值*/ printf("&a[%p]\n", &a); printf("&a[0][%p]\n", &a[0]); printf("a[%p]\n", a); /*2、做类型匹配*/ //p = &a; //warning: assignment from incompatible pointer type //指针类型不匹配 q = &a[0]; m = a; n = &a; //没有警告 类型匹配 /*3、做指针运算*/ printf("\n ++q[%p]\n", ++q); //0xa4-0xa0 = 4 printf("a+1[%p]\n", a+1); //0xa4-0xa0 = 4 printf("++m[%p]\n", ++m); //0xa4-0xa0 = 4 printf("++n[%p]\n", ++n); //0xc8-0xa0 = 40 printf("&a+1[%p]\n", &a+1); //0xc8-0xa0 = 40 return 0; } /**************************** 运行结果为: &a [0x7fffcb0953a0] &a[0] [0x7fffcb0953a0] a [0x7fffcb0953a0] ++q [0x7fffcb0953a4] a+1 [0x7fffcb0953a4] ++m [0x7fffcb0953a4] ++n [0x7fffcb0953c8] &a+1 [0x7fffcb0953c8] ****************************/
-
指针变量进行运算时的步进值由该指针
指向的内存块大小
决定,即由指向的数据的类型决定。若指向int
类型,则步进值为sizeof(int)=4
,指针运算时,则以4个字节为单位,例如:int *p = NULL,p=p+2
,即指针p
实际移动了sizeof(int)*2 =8
个字节。若指向数组类型,则步进值为sizeof(数组名称)
,同理可得指针运算时移动的字节数。
2.8 指针与函数参数
- 数组名作为形参时,按上述分析可知,传入的是首元素的首地址,即传入的是一个指针本身,指针本身的大小在确定的平台上是固定的,例如32平台下:
int (*n)[10] = NULL;
则sizeof(n)
的值为4
,但n所指向的数据类型的大小为4*10=40
。数组名不作形参时,对其进行sizeof
运算,得到的结果是整个数组的大小。 - 为什么数组名作为形参时,传入的是一个首元素首地址的指针?效率!对!省去了普通变量作为形参时,函数内部实参需要拷贝的操作。当传入的数据量特别庞大时,例如数组和多成员的结构体变量等,拷贝操作将牺牲不小的效率!
- 普通变量作为形参时,函数内部在复制完传入的实参变量后,便不再使用实参,因此无法通过函数内部去修改实参变量,且形参变量在函数弹栈后将被立即释放。
- 指针作为形参传入后,相当于将访问内存的
外挂方式
开放给了函数内部,若担心该操作会带来风险,请使用const对其所指向的内存数据加以限定。 - 输入型参数和输出型参数:当该参数加了const限定或传入的是一个普通变量,我们一般认为该参数为
输入型参数
,而当该参数为指针类型,则一般认为该参数为输出型参数
。注意这里的输入输出是相对于被调用的函数而言的。由于函数的返回值只能有一个,当需要对外输出多个返回值时,可以采用输出型参数
。普遍的做法是,将函数返回值仅作为函数调用后的状态返回,而将需要输出参数通过输出型参数
对外输出。