实际上对于指针,总有人说听不太懂。但对于我来说,我更喜欢从数据类型的角度理解指针。而在汇总文章的时候,我发现自己写的指针篇幅真的不少,汇总难度有点大,我会尽力不太啰嗦。(而实际上,被汇总的文章虽然讲得详细,但是可阅读性很差,这点我自己承认,毕竟当初写博客只是为了梳理学习思路而已)注:不会解释结论是为什么,个别会演示结论效果,详细的结论效果都在被汇总的文章内。
目录
1·指针的简单理解
注:这段话是我自己学习指针的理解。
其实指针(变量)是一种数据类型,和int、long、char之类的是一样的作用,用来给数据分类储存的。只不过int、long等是用来储存整型数据,folat、double用来储存浮点数据,而指针是用来储存地址数据的。(所以,存放在指针中的值都被当成地址处理。)
而实际上地址又是什么?我们总说地址是编号(且是十六进制形式的编号,例如:0x00000000),那既然是数字编号应该是int类型的整型数据,为什么地址还要分出来指针类型来储存?
所以我把地址理解为,向内存申请住宿空间时候的证书(或者说是钥匙)。而这个证书上写着自己想要的证书编号(就是起始房间门牌号,也就是起始地址)、房间大小以及房间住客的类型。
地址理解:
例如:以数组指针为例子(存放数组地址的指针)
int arr[5] = {0,1,2,3,4}; //arr类型int[5]
int(*p)[5]=&arr; //p类型和&arr类型:int[5]*
arr整个数组 申请住宿空间的过程:
证书编号(也就是地址编号):0x88888888为起始地址,起始地址是证书编号
房间大小(也就是数据类型大小):int[5],20个byte大小(会占用20个门牌号)
房间住客(也就是数据内容):0,1,2,3,4。每个住客占用4byte大小的房间(也就是4个门牌号),实际上数组中的每一个住客也会有自己的证书,这里不过多介绍,只提示是int*类型的证书(也就是地址)。
申请完之后,就形成了地址,地址类型(钥匙类型)是int[5]*。
而指针在保存地址的时候,保存的也正是地址(证书)的信息。所以你会看到【一个int[5]类型的整个数组地址】在【使用指针进行地址保存的时候】需要用int[5]*类型的指针变量来对应保存。(p指针保存的是arr地址的证书编号,也就是arr数组的起始地址)
此时的指针变量p和&arr数据类型相同,*p和arr数据类型相同。
指针的大小问题:
指针保存的是地址,32位系统中的地址编号是8位16进制数字,正好在内存中占用4byte,所以32位系统中指针是4byte大小。所以指针的大小取决于地址编号。(32位系统中指针是4byte,64位系统中指针是8byte)
二级指针问题:
接续上面那段代码的解释。前面说到过,数组中的每个住客也会有自己的证书,那该如何保存住客的地址呢?如下演示。
例如:以数组指针为例子(存放数组地址的指针)
int arr[5] = {0,1,2,3,4}; //arr类型int[5]
int(*p)[5]=&arr; //p类型和&arr类型:int[5]*
int* pa = &arr[0]; //pa类型和&arr[0]:int*
(注:&arr[0]=arr; 数组名是首元素的地址,在运用的时候类型是int*)
住客0 的住宿空间申请过程:
证书编号(也就是地址编号):和数组起始地址编号一样,0x88888888为起始地址
房间大小(也就是数据类型大小):int,4个byte大小(会占用4个门牌号)
房间住客(也就是数据内容):0
地址申请完毕,地址类型(钥匙类型)是int*
住客1 的住宿空间申请过程:
证书编号(也就是地址编号):0x8888888C,比起始地址编号多4
房间大小(也就是数据类型大小):int,4个byte大小(会占用4个门牌号)
房间住客(也就是数据内容):1
地址申请完毕,地址类型(钥匙类型)是int*
其他住客依次向后推,房间大小不变,只有证书编号和房间住客会改变。
所以,整个数组的地址中,会有5个int*类型的地址。
整个数组的证书(也就是地址)保存了每个住客的地址,所以整个数组的地址实际上就是使用二级指针类进行保存的,保存了其他地址的地址就是二级指针。
你可以把整个数组的地址理解为一个宾馆,每个住客住进去都需要申请数组(宾馆)内的住宿空间,而申请后会给你名为地址的钥匙来打开房间。整个数组的地址(也就是宾馆)会记录下每个住客的地址,那么 数组的地址 需要用二级指针来保存。(虽然我们经常会叫它数组指针,但它本质上和二级指针没什么大的差别。类似的其他各种级别的指针类型,本质上也就是宾馆里有几个住客以及住客的钥匙由谁保管的问题。)
而我们常说的指针指向谁的问题,实际上讨论的就是这个指针保存的地址所对应的房间住客是谁的问题。比如:int* a = 5; 此时,这个地址所申请的内存空间中住了一位类型为int、内容为5的住客。我们说指针 a 指向(住客)5,(住客)5的(房间)类型是int类型,占用4byte大小的房间(占用了4个门牌号)。指针指向谁,解引用的时候就会访问谁。
至此,这就是我对指针的全部理解了,我觉得指针就是这么简单。尽力了,为了讲的形象化一些,我还是啰嗦了。
2·编译器中如何查看地址?
现在,我们查看上面那段代码我说的对不对。
int arr[5] = { 0,1,2,3,4 };
int(*p)[5] = &arr;
我们前面说:p指针保存的是arr地址的证书编号,也就是arr数组的起始地址。
所以你会发现,我说的没错,p指针保存的是arr地址的证书编号,也就是arr数组的起始地址。
具体查看地址中的细节不再过多解释,详情看(注:只看地址相关内容,这文章里我对指针的认识有些偏差,但是地址没错的。)从0开始学c语言-09-指针及指针大小、*、&、地址_阿秋的阿秋不是阿秋的博客-优快云博客
3·如何快速写出指针类型
注:在辨别出指针类型后,应该想这个指针类型意味着什么?
比较简单的指针类型如下:去掉指针变量p,剩下的是指针类型
char *p = NULL; //p类型:char*
int *p = NULL; //p类型:int*
short *p = NULL; //p类型:short*
long *p = NULL; //p类型:long*
float *p = NULL; //p类型:float*
double *p = NULL; //p类型:double*
指针数组:去掉数组名,剩下的是数组类型
数组指针:去掉指针变量p,剩下的是指针类型
//指针数组
int *arr[10]; //arr的类型:int*[10]
char *arr[4]; //arr的类型:char*[4]
char **arr[5]; //arr的类型:char**[5]
int (*parr[10])[5]; //p的类型:int[5]*[10]
//数组指针
int (*p)[10]; //p的类型:int[10]*
int (*p)[4]; //p的类型:int[4]*
int (*p)[3][4]; //p的类型:int[3][4]*
另外我们知道数组的类型大多是 type+[常量表达式] ,如:int[5]、char[2]等,所以这里有一种特殊的看法,把(*p)看做数组名来判断*p的类型(注:只有*p被括号括起来才可以这样看待)。
(*p)看做数组名:去掉数组名,剩下的是数组类型,也是p指针变量指向的对象类型。
//数组指针
int (*p)[10]; //*p的类型:int[10]
int (*p)[4]; //*p的类型:int[4]
int (*p)[3][4]; //*p的类型:int[3][4]
对于函数指针,我想说函数名其实就是函数地址,也就是说 函数名=&函数名 。这一点和数组名是不一样的。
函数指针:去掉函数名,剩下的是函数指针类型
函数指针数组:去掉函数指针数组名,先写函数指针类型,后写数组。
指向函数指针数组的指针:去掉(*指针变量名),剩下的是函数指针数组类型,在这个类型后加*
//函数指针
void (*pfun)(); //pfun的类型:void(*)()
void (*pfun)(const char*); //pfun的类型:void(*)(const char*)
//函数指针数组
int (*parr[10])(); //parr的类型:int(*)()[10]
void (*pfunArr[5])(const char* str); //pfunArr的类型:void (*)(const char* str)[5]
//指向函数指针数组pfunArr的指针ppfunArrs
void (*(*ppfunArrs)[5])(const char*) = &pfunArr;
//去掉(*ppfunArrs),剩下void (*)(const char*)[5]
//ppfunArrs的类型void (*)(const char* str)[5]*
大概这么多,嵌套的则需要对于指针更加理解才能自己推理出来。
4·指针类型有什么用?以及指针规定
实际上,如果你懂了我一开始说的指针理解,就很快能够理解指针类型有什么用。
1·指针的类型决定了指针向前或者向后走一步有多大(距离)。 指针加减整数代表指针指向的地址向后或者向前挪动。
2· 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
3·指针的类型取决于指针本身,和它指向谁没有关系。
4·两个指针相减,指针必须指向一段连续空间,减完之后的结构代表两个指针之间相差元素的个数。(指针 - 指针 = 指针之间的元素个数)或者在数组中理解为下标-下标,高地址-地地址为正数,反过来为负数。
相关规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
const修饰指针:
1·放在*左边,修饰的是*p,*p表示指针指向的内容,也就是说不能通过*p来改变指针指向的内容了,但是指针变量p可以改变。
2·放在*右边,修饰的是指针变量p,指针变量存储的地址不能被改变,但是*p(指针指向的内容)可以改变。
3· *的左边和右边可以都放const。
//*左边
const int *p=#
*p=20;//err
p=&n;//ok
//*右边
int * const p=#
*p=20;//ok
p=&n;//err
5·字符指针与字符串
//指向字符的指针
char ch = 'w';
char *pc = &ch;
*pc = 'w';
//指向字符串的指针
const char* pstr = "hello bit.";//保存首字符h的地址
//字符数组
char ch[20] = "asd"; //ch是首字符a的地址
指向字符的指针不能直接指向,需要像上面一样先用一个char变量储存这个字符。
char* ch = ‘w’; //单个字符的类型是int,对应ASCII码值
6·一些学习结论
判断 数组和指针 指向对象类型:
1·指针去掉*来判断指针指向谁
2·数组去掉[ ]来判断数组名指向谁
数组名与&数组名:int arr[10];
arr的类型是int[10] ——>数组去掉[ ]——>指向int——>本身类型int*
&arr的类型是int[10]* ——>指针去掉*——> 指向10个int——>本身类型int[10]*
判断步长:
arr的类型是int[10] ,指向int,步长int(跳过一个int元素)
&arr是int[10]*,指向int[10],步长int[10](跳过10个int元素)
补充结论:arr[ i ] 和*(arr+i ) 相当于 arr 这个指针向后挪动 i 步长后并访问住户。
7·如何规避野指针?
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因:指针未初始化、指针越界访问(注意访问不可少!)、指针指向的空间释放
规避措施:
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
int main()
{
/*避免野指针
不知道初始化什么,就等于NULL*/
int* p = NULL;
/*有效性检查*/
if (p != NULL)
return 9;
return 0;
}
8·汇总链接
从0开始学c语言-20-指针与地址、指针类型、野指针、指针运算_阿秋的阿秋不是阿秋的博客-优快云博客
从0开始学c语言-21-指针和数组、二级指针、指针数组_阿秋的阿秋不是阿秋的博客-优快云博客
从0开始学c语言-27-字符指针,指针数组和数组指针_阿秋的阿秋不是阿秋的博客-优快云博客
从0开始学c语言-28-qsort函数、 数组和指针参数、函数指针数组(转移表)、回调函数_阿秋的阿秋不是阿秋的博客-优快云博客
9·指针练习
从0开始学c语言-30- 指针不练习?还真觉得自己会了~_阿秋的阿秋不是阿秋的博客-优快云博客
10·指针传参
我们在数组汇总篇说过这段代码。
//形参数组形式
void test1(int arr[]) //10可写可不写,因为传过来是首元素的地址
{
}
//形参指针形式
void test2(int* arr) //这两种写法都可以,本质上都是指针,只不过是两种书写方式
{
}
int main()
{
int arr[10] = { 0 };
test1(arr);
test2(arr);
return 0;
}
现在你应该知道,数组名是首元素的地址,数组名在作为参数传参的时候实际上传递的是数组首元素的地址,类型是int*,所以写函数参数的时候可以写为int *类型来接收这个地址。
类似其他的传参形式不在这里解释,只写上代码例子。(学完指针后,能够自己判断需要什么类型的参数并能够写出来是基本能力)
//1·普通变量地址传参
test(int* a){}
int main()
{
int a = 0; //a是int类型
test(&a); //&a是int*类型
}
//2·字符传参
test(char* c){}
int main()
{
char a = 'w'; //a是char类型
char b[20] = "aaaa"; //b是char[20]类型,运用时候是char*类型,代表首元素地址
test(&a); //&a是char*
test(b); //b是char*类型
}
//3·指针数组传参
test(int*(*arr)[10]) //[]内代表数组大小,不写默认为数组内0下标的地址
{
//若写90超出警告为: warning C4048: “int *(*)[90]”和“int *(*)[10]”数组的下标不同
}
int main()
{
int* arr[10] = {0};
test(&arr);
}
//4·数组指针传参
test(int(*a)[10]) //[]内代表数组大小,不写默认为数组内0下标的地址
{
}
int main()
{
int arr[10] = { 0 };
test(&arr);
}
//5·函数指针传参
test(int(*p)(int))
{
}
int main(int a)
{
int(*p)(int) = main;
test(p);
}
示范到这里把,我示范再多,也不如你自己写学得快。