1.介绍指针
指针是C语言、C++中一个非常重要的工具,它赋予了程序员直接对内存访问和操作的能力,使得我们对内存的操作具有了高效性和灵活性。
2.什么是指针?
2.1.指针是内存中一个最小单元(一个字节)的编号,和我们平常所理解的地址是相同的,我们利用这个编号可以直接对内存中的数据进行操作。
2.2.我们平常口语中经常提到的指针是指针变量,它是存储、使用指针的载体,它在内存中同样也需要存储,大小是固定的4/8个字节,这个大小受到不同机器的影响(32位/64位)。
3.指针类型是什么?它们有什么意义?
引言:我们都知道,变量有不同的类型,同样的,指针变量也有不同的类型,接下来我们开始研究它们的类型以及这些不同的类型都有什么不同的意义。
3.1.都有哪些指针类型?
讨论这个问题前,我们先看以下代码:
int num = 10;
p = #
首先对‘&’做一个解释:‘&’叫做取地址操作符,它是一个单目操作符,它的作用是从内存中取出它后面变量的地址。
那么,观察以上的代码,我们将num的地址取出并保存在p中,它的类型是什么呢?
我们需要给出不同的类型:
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
float* pf = NULL;
double* pd = NULL;
......
通过上面的代码可以看出,定义指针的语法格式是:type* + 变量名,其中的‘*’说明它后面的变量是指针变量,同时指针变量的类型是多种多样的,甚至可以是结构体。
3.2.指针变量有什么意义?
3.2.1.指针加减整数:
我们可以将指针加减整数的结果打印出来再进行分析:
可以看到,不同的指针类型指向的位置是相同的,但不同的类型决定了加减整数时跳过的步长是多少,一般来说跳过的步长和”type*“中的type在内存中所占的字节相同。
3.2.2.指针的解引用:
首先要了解:‘*’在指针变量使用时,叫做解引用操作符,可以拿到指针所指向的内容。
从上图可以看出,不同的指针解引用后,打印的结果是不一样的,这其实是因为,不同的指针变量在解引用时所访问的字节数也是不同的,访问的大小与指针加减整数时的步长相同。
3.2.3.指针比较大小与相减:
从上图可以看出,指针是可以相减的,结果是它们中间有多少个单位的步长,同时这也可以说明指针是可以比较大小的。
4.什么叫野指针?如何避免野指针?
4.1.什么叫野指针?
野指针顾名思义就是所指向的位置是不确定的,或者没有明确限制的指针都叫野指针,野指针是非常危险的,会让程序产生不可控的运行结果,下面是几个野指针的例子:
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
上面的指针越界访问了数组外的空间,当它指向了不属于数组的位置时,它就是野指针了。
int main()
{
int* p;//局部变量未初始化
*p = 20;
return 0;
}
上面的指针没有初始化就成为了野指针,同时在之后,用这个野指针访问了内存空间,这是非常危险的行为。
4.2.如何避免出现野指针?
(1). 指针初始化
(2). 小心指针越界
(3). 指针指向空间释放即使置NULL
(4). 避免返回局部变量的地址
(5). 指针使用之前检查有效性
5.指针与数组
对于指针与数组需要特别说明:
(1).从上图可以看出,数组名表示数组首元素地址,两种情况除外,它们是:
(1).1.数组名单独出现在sizeof中
(2).2.对数组名进行取地址操作,此时取出的是数组的地址,它与数组首元素的地址的区别是跳过的步长不一样
6.字符指针:
在指针中有一种叫字符指针char*,一般的用法是这样的:
int main()
{
char ch = 'x';
char* pc = &ch;
return 0;
}
此时pc指向了ch,这是显而易见的,但它还有以下的用法:
需要注意的是,以上的这种情况看似是将”haha“这个字符常量赋值给了pstr,但其实是将这个字符常量首元素的地址赋值给了pstr,而打印的原理是从这个指针开始向后一直找到了结束标志:‘\0’。
以上的代码及运行结果更加说明了将字符常量赋值给指针变量的做法实际上是将地址赋给了指针变量,同时也说明了当str3和str4两个指针所指向的对象不能发生改变时,内存将这两个变量指向了同一个位置。
7.指针数组:
指针数组是一个数组,它里面所有的元素都是指针。
例如:
int* arr1[10]//数组中的元素都是int*类型
char* arr2[4]//数组中的元素都是char*类型
char** arr3[5]//数组中的元素都是char**类型
8.数组指针:
8.1.什么是数组指针?
数组指针是一个指针,它指向了一个数组,它在加减整数和访问空间时都访问一个数组的大小,首先我们先通过一个例子来认识一下数组指针:
int (*p)[10]
分析一下上面的代码:
‘*’与p结合说明p是一个指针,将*p拿走不看时,剩下的“int [10]”就是它所指向的数组类型了,这个类型说明了该数组中的元素都为int类型,其中共有10个元素。
通过以上的例子,我们认识了数组指针。
8.2.数组指针之-------数组名与取地址数组名:

通过以上的代码,我们发现数组名和取地址数组名打印的结果是相同的,难道这两者相同吗?
事实是否定的,我们之前提到过,在对数组名取地址时,取出的是数组的地址,而除了一些
特殊情况外,数组名指的是数组首元素的地址,接下来,我们用以下的代码来验证:
从上面的图片我们可以看出数组名和对数组名取地址表面上时一样的,但实际上访问内存是的步长不同。
9.函数指针:
9.1.什么是函数指针?
通过上面对各种指针的讨论,我们可以知道函数指针就是指向函数的指针,首先通过一段代码认识以下函数指针:
通过上面的指针可以发现函数确实是有地址的,同时事实上,函数名和对函数名取地址是完全相同的,这也很好理解,我们只想用函数指针调用函数,不需要用它访问内存,在使用有的库函数比如q_sort时,给它传入的比较函数cmp就是函数指针。
9.2.函数指针该怎么存储?
通过前面对各种指针的讨论,我们很容易可以看出pfun1与‘*’在一起,所以它有能力存放函数指针,事实的确如此,我们观察一下pfun1可以发现,函数指针的定义就是首先将‘*’与变量名结合,表示它是一个指针,接下来在它的前面写上指向的函数的返回值,后面写上参数就可以了。
9.3.函数指针有什么用?
在上面已经提到在函数调用时会在参数部分传入函数名,这就是对于函数指针的使用,被传入的指针就被称为回调函数。
10.解析各种指针的相关问题(均是在32位环境下):
10.1.一维数组:

(1).数组名单独放在sizeof里面表示的是整个数组,结果是16;
(2).数组名并不是单独放在sizeof里面而是加上了0,此时数组名表示首元素地址,加0表示向右移动0个单位,所以此时求得是指针大小,结果是4;
(3).数组名表示首元素地址,解引用则拿到了数组首元素,int类型大小为4;
(4).数组名表示首元素地址,加一表示向右移动一个步长,仍为指针,结果是4;
(5).a[1]为访问数组下标为1的元素,int类型结果为4;
(6).对数组名取地址取出的是数组的地址,但仍为地址,大小为4;
(7).对数组名取地址取出的是数组的地址,解引用则是拿到了整个数组,结果为16;
(8).对数组名取地址取出的是数组的地址,加一向右偏移16个字节的大小,但仍为地址,结果为4;
(9).取出了a[0]的地址,是地址,结果为4;
(10).取出a[0]的地址,向右偏移一个单位,是地址,结果为4;
结果如下:
10.2.字符数组
(1).数组名单独出现在sizeof里面表示的是整个数组,结果为6;
(2).数组名不是单独出现表示首元素地址,加0表示向右偏移一个步长,是指针,结果是4;
(3).数组名表示首元素地址,解引用拿到了首元素,char类型大小为1;
(4).访问arr[1]的位置的元素,大小为1;
(5).对数组名取地址取出的是数组的地址,是地址结果为4;
(6).对数组名取地址取出的是数组的地址,加一是指向右偏移一个数组的步长,偏移6个字节,仍为地址,是地址结果为4;
(7).取出arr[0]的地址,加一表示向右偏移一个cahr类型的步长,仍为地址,是地址结果为4;
结果如下:
10.3.字符数组与strlen:

(1).数组名表示首元素地址,strlen会从该地址向后一直统计字符个数,直到'\0'为止,但该字符数组结尾没有'\0',所以结果为随机值;
(2).同上,结果为随机值;
(3).数组名表示首元素地址,解引用则拿到了首元素,但strlen会将‘a’的ascII值作为地址访问内存,从该地址向后一直统计字符个数,直到'\0'为止,但以97为地址的内存空间不可访问,所以程序会崩,结果部分不演示;
(4).访问arr[1],同上;
(5).对数组名取地址取出的是数组的地址,strlen会从该地址向后一直统计字符个数,直到'\0'为止,但该字符数组结尾没有'\0',所以结果为随机值;
(6).同上,形成越界访问;
(7).同上,随机值;
结果如下:
10.4.字符指针:

(1).p是指向字符串首元素的指针,是指针结果是4;
(2).同上,结果是4;
(3).对p解引用,拿到了字符串首元素,结果为1;
(4).访问p[0],char类型,结果为1;
(5).对指针取地址,是一个二级指针,是指针,结果为4;
(6).同上,结果为4;
(7).对p[0]取地址取出的是字符串首元素的地址,加一则是指向字符串中'b'的地址,是指针结果为4;
结果如下:
10.5.字符指针与strlen:

(1).p是指向字符串首元素的指针,strlen会从该指针开始向后统计字符的个数,直到'\0'结束,结果为6;
(2).p是指向字符串首元素的指针,加一表示指针向后偏移一个步长,strlen会从该指针开始向后统计字符的个数,直到'\0'结束,结果是5;
(3).p是指向字符串首元素的指针,解引用则拿到了字符串首元素,以'a'的ascII值作为地址,从该地址开始向后统计字符的个数,直到遇到'\0',但'a'的ascII值97为地址的内存空间不能访问,所以程序会崩溃。
(4).同上,程序会崩溃,结果部分不演示;
(5).对p取地址取出的是p的地址,是一个二级指针,结果是一个随机值;
(6).同上,结果是个随机值;
(7).对p[0]取地址,取出的是字符串首元素的地址,加一表示指针向后偏移一个步长,则访问了字符串中'b'的地址,strlen会从该指针开始向后统计字符的个数,直到'\0'结束,结果为5;
结果如下:
10.6.二级指针:

(1).数组名单独在sizeof里面,表示的是整个数组,结果为48;
(2).访问a[0][0]的位置,int类型结果为4;
(3).a[0]为二维数组第一行的数组名,单独放在sizeof里面表示的是整个数组,结果为16;
(4).a[0]为第一行数组名表示首元素地址,加一表示向右偏移一个步长,是指针,结果为4;
(5).a[0]为第一行数组名表示首元素地址,加一表示向右偏移一个步长,指针指向第一行第二个元素,解引用表示拿到了这个元素,int类型结果为4;
(6).数组名表示首元素地址,加一表示向右偏移一个步长,为指向第二行的指针,是指针结果为4;
(7).数组名表示首元素地址,加一表示向右偏移一个步长,为指向第二行的指针,解引用表示拿到第二行,结果为16;
(8).a[0]为数组第一行数组名,取地址表示对整个数组取地址,加一表示向右偏移一个步长,仍是指针,是指针结果为4;
(9).a[0]为数组第一行数组名,取地址表示对整个数组取地址,加一表示向右偏移一个步长,为指向第二行的指针,解引用拿到了第二行,结果为16;
(10).a为数组名,表示数组首元素的地址解引用拿到了第一行,结果为16;
(11).a[3]为数组第4行的数组名,(注意:虽然数组没有第4行,但是sizeof是根据类型推断结果,所以仍然有结果),单独放在sizeof里面表示整个数组,结果为16;
结果如下:
11.结语:
这就是本期关于指针的所有内容了,希望对大家有所帮助,感谢各位于晏、亦菲的阅读,欢迎大家和我一起讨论、进步。