一、指针的基本常识
1.什么是指针?
对于指针需要掌握以下几个基本的概念:
<1>指针是一种变量类型,也是具有空间、内容、地址三个属性。空间属性值得是:在定义指针时,内存会给指针变量分配指定类型大小的空间,空间的大小为四个字节(32位平台下)。内容属性指的是:指针变量内部存储的数据,即存放的是地址。因此,通常也会说指针就是地址,地址就是指针,要注意的是这里的指针指的是指针的内容。
<2>指针是有类型的,其类型决定了指针访存时的步长,即指针解引用操作时的权限。比如:char * 类型的指针在通过指针间接访存(对其解引用)时的步长为1字节,而 int * 、float * 类型的指针为4字节,double * 为8字节。
<3>一个地址唯一标识一块内存空间。
2.正确规避野指针(悬垂指针)
什么是野指针?野指针指的是指针指向不明确的指针。
野指针形成的原因:<1>定义指针时未初始化。
<2>指针访问内存时,进行 越界访问。
<3>指针指向的空间被释放(malloc函数开辟空间后,使用结束时会free掉开辟的空间)。
正确规避野指针:<1>定义指针时要初始化,没有明确的指向空间时,赋值为NULL。
<2>使用指针对内存进行访问时,注意防止越界。
<3>指针指向的空间被释放后,及时将指针置为NULL。
<4>使用指针前,检查指针的有效性(在使用指针进行函数传参时,在函数内部使用assert()函数对传入的指针进行校验)。
3.指针的运算
<1>指针与整数的加减:对指针变量加上或者减去一个整数,实际上加/减去的是其所指向类型的大小。
type *a + n == a + sizeof(type),这里n是常数。、
<2>指针减指针:我们通常对指针进行减法运算,是在同一个字符串或者数组下进行,其结果代表的是两个指针之间元素的个数。
<3>指针的大小关系比较:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的那个内存位置的指针进行比较,同时不能直接对指向末位元素后的内存进行赋值。
4.指针和数组
#include <stdio.h>
#include <windows.h>
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
system("pause");
return 0;
}
由结果可以看出,打印的三个地址的结果在数值上是完全相等的。
arr(数组名)代表的是数组首元素的地址,因此前两个打印的结果相同;而 &arr 代表的是整个数组的地址,它在数值上是与数组首元素的地址是相等的。如何能快速鉴别两个的差别,我们可以对其+1,结果如下:
从结果可以看出,对 arr+1 ,其地址之间的差值为4byte,正好是数组中一个元素的大小;而对 &arr+1,其地址之前的差值为40byte,正好是整个数组的大小。究其原因是由于两个(地址)指针的类型不相同,arr是一个整形指针,而&arr是数组指针,下边将对数组指针加以说明,这也与我们上边所说的对指针+1,加的是其所指向类型的大小。
二、指针数组、数组指针、函数指针、多级指针
1.指针数组、数组指针
指针数组、数组指针,这两个到底是怎么一回事呢?
在C语言中,这是两种不相同的变量类型。数组指针是指针,通常在数组传参的时候会应用到此概念;而指针数组是数组,是用来存储指针的数组,其元素为指针。
<1>指针数组
定义指针数组的方式为(以整型指针数组为例):int *arr[5];
解释:arr先和[]结合,说明arr是一个数组,其类型为 int *。
如下图所示,是一个包含5个元素的整型数组,其元素类型为:int * 。
<2>数组指针
①定义数组指针的方式为:int (*arr)[5];
解释:arr先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
注:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
②数组指针的应用:数组指针的应用场景多为数组作为参数传递给另一个函数。我们都知道,在函数传参时有两种方式:传值传参、传址传参。当数组作为参数传递时是传址传参,数组降维成内部元素类型的指针。
一维数组传参:以整型为例,降维成 int * 类型的指针。
二维数组传参:降维成 int (*arr)[size] 类型的数组指针。这里需要注意的是,二维数组可以看成是一个内部元素为一维数组的一维数组。以此类推,三维数组可看成是一个内部元素为二维数组的一维数组。
③二维数组降维传参,形参定义的集中形式,以及访问数组元素的几种方式
void example1(int(*arr)[5], int x,int y){
for (int i = 0; i < x; i++){
for (int j = 0; j < y; j++){
printf("%d\n", arr[i][j]);//建议使用
printf("%d\n", *(*(arr + i) + j));
printf("%d\n", *(arr[i] + j));
}
}
}
void example2(int arr[][5]){
}
2.函数指针
如上所述,指针是用来唯一标识一块内存空间的,空间的大小由指针的类型决定。在我们练习写代码的过程中,如果你去打印一个变量的地址,这样会得到一个唯一的地址,当你打开内存查看变量占空间的情况时,你会发现打印出来的地址与该变量所占用的内存中最低的地址的值相等(起始地址)。所以当CPU进行运算,从内从中读取变量的内容时,是通过变量的地址(即这里所打印出来的地址)唯一的标识出变量所在的空间,你可以理解为相当于门牌号。
函数指针也是一样,也用来标识一块唯一的内存空间。这里就需要介绍函数和函数名,函数的本质是一个代码块,会包含多组的代码,也就决定了一个连续的地址序列;函数名则是用来标识当前函数的内存空间,它的值在大小上等于该函数所开辟连续空间中地址最低的序列。函数指针则是用来指向该连续空间的起始地址,即函数名。
函数指针变量的定义与初始化:
#include <stdio.h>
void example(int(*arr)[5], int x,int y){
for (int i = 0; i < x; i++){
for (int j = 0; j < y; j++);
}
}
int main()
{
int arr[][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
void (*p)(int (*)[5],int ,int) = example;
return 0;
}
如上代码所示,我们定义了一个函数指针变量 p ,它所指向的函数接受三个参数,分别是一个数组指针和两个整型值。
如何通过函数指针去调用函数,将例子中的数组打印出来?以下介绍两种形式:
void (*p)(int (*)[5],int ,int) = example;
//example(arr, 2, 5);
(*p)(arr, 2, 5); //1
p(arr, 2, 5); //2
首先是第一种 ,对指针p进行间接访问操作,把函数指针转换成一个函数名,该语句在效果上与注释掉的语句是一样的,但是这个转换是不必要的,编译器在执行函数调用时又会将它转换回去。第二种与第一种的效果相同,间接访问操作并非必需,因为编译器需要的是一个函数指针来标识空间。
3.多级指针
这里通过一个面试题来具体说明问题。
int main()
{
char *c[] = { "ENTER", "New", "POINT", "FIRST" };
char **cp[] = { c + 3, c + 2, c + 1, c };
char ***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
如上所示,分别定义了一个字符型数组指针、一个二级数组指针、三级指针。
注:字符型指针的指向有两种:①字符,②字符串。当指向字符串时,拿到的是字符串的起始地址,对其进行打印输出时比较特殊,不需要解引用,从起始地址开始往后读取打印,遇见 '\0' 停止。
①**++cpp:开始时,cpp 指向 cp[0],自增后指向 cp[1],第一次解引用得到 c[2] 的地址,第二次解引用得到字 c[2] 的内容,即符串 "POINT" 的起始地址,打印输出为:POINT。
②*--*++cpp + 3:可以看成是 (*(--(*(++cpp)))) + 3 第一次打印cpp自增指向 cp[1],此时再次自增则指向 cp[2],解引用得到 c[1] 的地址,然后再自减得到 c[0] 的地址,解引用得到"ENTER" 的起始地址,然后 +3 得到 "ENTER" 中字符E的地址,打印输出为:ER。
③*cpp[-2] + 3:可以转换成 *(*(cpp-2))+3; 经过②之后,cpp指向 cp[2],减2后指向 cp[0],解引用得到 c[3] 的地址,再次解引用得到 "FIRST" 的起始地址,+3 得到S的地址,打印输出为:ST。需要注意的是这里执行完之后,cpp的指向仍为 cp[2]。
④cpp[-1][-1] + 1:可以转换成 *(*(cpp-1)-1)+1。cpp-1指向 cp[1],解引用得到 c[2] 的地址,-1后得到 c[1] 的地址,解引用得到 "New" 的起始地址,+1得到E的地址,打印输出为:ew。
总结: *(三级指针) = 二级指针的内容 = 一级指针的地址;
*(二级指针) = 一级指针的内容 = 变量的地址;
*(一级指针) = 变量的内容;
注:指针与数组的部分操作
int a[5] = { 1, 2, 3, 4, 5 };
//&a:表示数组指针
int *ptr = (int *)&a;
//ptr+1:指向的是元素 2,+1加的是ptr的类型大小
//ptr[-1] <----> *(ptr-1)
//p[4][2] <----> *(*(p+4)+2)
//*(p+4) 该解引用相当于另一个数组名