c语言指针详解通俗易懂,C语言指针详解
在的第一篇博文中
1.什么是指针?我们知道数据存储在内存中。当我们想要访问内存中的特定区域时,我们需要使用指针。例如,我创建了一个局部变量int a=4,如果我想直接操作这个数所在的内存空间,我会改变它。还有指针是怎么实现的?
我们知道地址的概念存在于内存中。什么是地址?地址就是地址。
当然,这不是废话,而是我们可以简单理解为字面意思的称呼。假设记忆是一个在建的城镇。小镇按照一个字节的大小被整齐的划分成区域。我们对这些区域进行了顺序编号,这些编号就是对应区域的地址。地址就是指针。
当我们需要一个空间来盖房子或做其他事情时,我们需要从内存中申请一个空间。
现在我们有了一个指针,如何使用它呢?这时,我们需要一个指针变量来存储我们的指针。
二。什么是指针变量int main(){ int a=8;int* p=返回0;}这个P是我们用来存储指针a的指针变量。
当我们要修改A时,只需要用我们的解引用操作符*解引用P,就可以直接操作内容了。
int main(){ int a=8;int * p=* p=2;返回0;}此时,我们A中存储的值更改为2。
简单来说,我们假设A的地址是0x175864BF,我们在内存中创建的内存空间是一个百宝箱。
地址是这个宝箱的号码。
(我自己画的宝箱。太丑了。请原谅我。)
这个数字存储在指针P中,我们通过这个数字找到了宝箱。
当我们把p放在p前面的时候,我们就可以找到这个宝箱,把它拿走或者放进去。
既然我们提到指针变量本身也是存储的一组数据(地址),那么指针变量本身就有地址。
那么这个地址可以存储在指针变量中吗?当然有可能。
那么存储指针变量的指针也是二级指针。
以上面的百宝箱为例。既然宝箱有编号,我们可以把这个编号写在纸上,把这张纸放在另一个宝箱里。那么存放这张纸的宝箱一定也有自己的编号(地址),这个存放宝箱编号的宝箱编号(地址)就是二级指针。
这里当然只是为了更形象的说明,举了这样一个例子。实际存储中没有纸质介质,数据直接存在于内存空间中。
int main(){ int a=8;int * b=int * c=* * c=2;printf(%d ,a);返回0;}
有一次解报价的时候,在宝箱C的位置发现了一个地址,是另一个宝箱的号码。找到第二个报价中名为A的宝箱,将其中的2替换为8。
当然,这些描述对于熟悉C语言的人来说可能感觉比较糟糕,因为实际上指针,也就是地址,实际上是一块空间中的第一个(或者低位)地址,它的访问权限是根据指针的类型来决定的。这个时候,我们的指针类型的意义就要提到了。
四。指针类型1的含义。指针的访问权限我们已经知道一个int开辟了一个4字节的空间,我们每个字节都有自己的地址。那么当我们使用int*指针变量的时候,它是怎么存储地址的呢?
假设我们开辟一个空间来存储数据A,如图所示。如果此时我们给P一个指针变量来存储A的地址,那么4个字节中3的地址就存储在P中。
而且类型决定了我们会从3的左端访问几个字节,比如int,我们会从3的左端访问四个字节,而使用char,我们只会从左端访问一个字节,也就是字节3。
2.指针1的操作。指针的类型也决定了我们指针的操作。
以下面的代码为例
int main(){ char ch=5;int a=5;int* pa=char* pc=printf(%p\n ,pa);printf(%p\n ,pa 1);printf(%p\n ,PC);printf(%p\n ,pc1);}
我们可以看到pa 1跳过了一个int的大小4个字节,而pa 1跳过了一个char的大小1个字节。
3.如果指针减去指针会怎么样?第一,如果指针减去指针,两个指针必须指向一个连续的空格比如arr[6],其中arr[0]到arr[6]是连续空格。
我们可以通过下面的代码来分析指针减指针。
#包含stdio.h
int main()
{
int arr[10]={ 0,1,2,3,4,5,6,7,8,9 };
printf( %d\n,arr[0]-arr[9]);
printf( %d\n,arr[9]-arr[0]);
}
动词(verb的缩写)指针变量的大小既然P存储了我们的地址,那么P本身就是一组数据,自然会在内存中开辟一个空间。
这个空间的大小在32位平台和64位平台之间是不同的。
这是因为32位系统的最大寻址空间是2的32次方=4294967296(位)=4(GB),64位系统的最大寻址空间是2的64次方。因此,指针的大小被改变。在32位x86环境中,指针变量的大小是4字节(32位),而在x64环境中是8字节。
6.百搭指针1。什么是野指针?野指针是在内存中没有明确指针的指针。
这可能是因为指针未初始化。
int main(){ int * p;返回0;}数据存储空间已被破坏。例如,函数返回局部变量的地址。
int * test(){ int a=0;return } int main(){ int * ptr=test();}或者使用动态内存分配时指针指向的空间已经被释放等原因。
在实际的代码编写中,如果不小心使用了通配符指针,可能会导致很难发现的bug。举个例子,当我们读取野指针指向的区域时,我们只是没有报错,同时,我们意外地得到了一个符合我们预期的数字。恐怕我们会迷茫很久。
2.如何避免野指针1?第一,注意指针的初始化,避免指针初始化不完全,使用时将释放空间的指针设置为空指针。
2.避免指针越过int main()
{
int arr[5]={0,1,2,3,4 };
int p=arr
(arr 5)=1;
返回0;
}
请注意,指针越界了。
3.当指针指向的空间被释放后,设置为空指针# includestdlib . h int main(){ int * p=malloc(40);免费(p);p=NULL}malloc是动态内存分配。我们给他一个40,他就给我们开辟一个40字节的空间。返回值是指向这个空格开头的指针。当然,这里不会详细解释malloc
free的功能是释放内存。当我们空闲(p)时,这个块的内存空间被释放。这个时候指针没有明确的指向,所以我们用p=null将其设置为空指针,以避免其成为通配符指针。
4.避免返回局部变量的地址int * fun(){ int a=8;return } int main(){ int * p=fun();}像这样里面的指针P也是野指针。我们知道A是一个局部变量。当它离开它的作用域时,它的生命周期就结束了,指向A的空间此时已经被释放了。
七。字符指针在指针类型中,我们知道有一种指针类型是char*
int main(){ char ch= w ;char * pc=* pc= w返回0;}还有一种写法,我们可以放一个字符串进去。
# include stdio . h int main(){ const char * arr= hello word ;printf(%s ,arr);返回0;}这个const-modified字符串称为常量字符串,不能修改。
当然,不能修改不代表不能观察,就像银行柜台的钱只能看不能动一样。你甚至可以数一数柜台里有多少人,几个验钞机,但是你数不出来。下面这样的一段代码是没有问题的。但是注意,可以把arr的内容放入arr2,但是不能把arr2放入arr。改变常量字符串的内容是初学者容易犯的错误。
# include stdio . h # include string . h int main(){ const char * arr= hello word ;char arr 2[]= # # # # # # # # # # # # # ;strcpy(arr2,arr);printf(%s ,arr 2);返回0;}
注意arr并不存储所有的地址,只存储字符串的第一个地址。
我们来看一个之前出现过的面试问题,排除可能的认知错误。
# include stdio . h int main(){ char str 1[]= hello word。;char str2[]=hello word。;const char *str3=hello word ;const char *str4=hello word ;if(str1==str2) printf(str1和str2相同\ n );else printf(str1和str2不相同\ n );if(str3==str4) printf(str3和str4相同\ n );else printf(str3和str4不相同\ n );返回0;}
这里str3和str4指向同一个常量字符串。C/C将常量字符串作为几个指针存储在单独的内存区域。当指向同一个字符串时,它们实际上指向的是同一块内存。但是,当不同的数组用相同的常量字符串初始化时,会创建不同的内存块。所以str1和str2不一样,str3和str4也不一样。其实比较的是他们的内存空间是不是同一个地方。
8.注意区分指针数组和数组指针1。指针数组指针数组是用来存储指针的数组。它本质上是一个数组,只是其中存储的内容是相应类型的指针。
int * arr 1[6];//整数指针数组char * arr 2[6];//一级字符指针的数组char * * arr 3[6];//第二个字符指针2的数组。数组指针1。什么是数组指针?数组指针是指向数组的指针,本质上是指针。
我们来判断一下下面两个哪个是数组指针。
1.int * P1[6];2.int(* p2)[6];这就涉及到int和p谁先组合的问题。比如1中的1先和int结合,所以是一个存储int* type(整数指针)的数组。
而且我们在里面加了括号,先把它和p2组合起来,这说明p2是一个指针类型的变量,指向一个6个整数的数组。
因为[]的优先级比*高,如果我们不用()限定它,它会优先于数组的方向而不是指针。
2.arr和arrin tar[10];
我们已经知道,数组名代表第一个元素的地址,在各种使用和传输过程中传递的就是第一个元素的地址。
那么数组名arr和arr有区别吗?
这就涉及到上一篇文章提到的指针类型问题。
看一下下面的代码。
# include stdio . h int main(){ int arr[10]={ 0 };printf(arr=%p\n ,arr);printf( arr=%p\n ,arr);printf(arr 1=%p\n ,arr 1);printf( arr 1=%p\n ,arr 1);返回0;}
可以看到,都是存储第一个元素地址,但是加1的时候,arr跳过了4个字节的大小,也就是一个int类型,而arr加1跳过了整个数组的大小。所以这个地址取数arr实际取出的是整个数组的地址,它的实际指针类型是数组指针int(*)[10];
九。指针参数的传递我们在使用指针的过程中经常使用指针来传递或接受参数,那么函数的参数如何设计呢?
1.一维数组传递int main(){ int arr[10]={ 0 };测试(arr);返回0;}像这样,我们创建一个一维数组arr,并将其作为参数传递给我们创建的测试函数。那么test应该用什么类型来接受它呢?
void test(Intarr[]){ } void test(Intarr[10]){ } void test(Int * arr){ }作为一个一维数组,我们当然可以用一维数组类型来接受。同时我们也知道数组的名字其实是第一个元素的地址,所以用指针类型接受也是可以的。
2.二维数组参数int main(){ int arr[3][5]={ 0 };测试(arr);}我们已经知道如何向一维数组传递参数。二维数组呢?
void test(Intarr[3][5]){ } void test(Intarr[][])/{ } void test(Intarr[][5]){ }如果我们已经学过数组,应该也知道当我们把二维数组传递给函数时,接收二维数组的数组行可以省略,但是列不能省略。如果要用指针接收,怎么做呢?
void test(int * arr)//1{ } void test(int * arr[5])//2{ } void test(int(* arr)[5])//3 { } void test(int * * arr)//4{ }如果我们
Int* arr[5]是一个大小为五个元素的指针数组。自然不能用来接受我们的二维数组。
而int(*arr)[5]是指向数组的指针。5表示它所指向的数组每行有多少个元素,所以是正确的。
Int**arr是一个二级指针,用来存储指针变量。自然,它不能用来接收二维数组。
X.函数指针# includes stdio . h void test(){ printf( hello );}int main(){ printf(%p\n ,test);printf(%p\n ,test);返回0;}
测试和使用中的测试没有区别。和下面的代码一样,test和test都是一样的。
# include stdio . h void test(int a){ printf( % d \ n ,a);} int main(){ test(5);void(* pt)(int)=test;pt(6);返回0;}
可以看出,当我们要创建一个函数指针时,首先要告诉它函数返回值的类型。然后写出指针变量的名字,再写出函数接收值的类型。注意,( pt)的括号不能去掉。如果被移除,它们将与void组合来表示返回值为void的函数。
同时也要注意,我们直接用pt的时候,和pt是一样的,可以流畅的调用函数。
你已经知道了函数指针,所以看看这段代码并解释一下。
(*(void(*)())0)();其实这段代码并没有看起来那么“地狱”。让我们一点一点地把它拆开。首先
Void(*)()我们可以清楚的看到它是一个函数指针,在一个数字前面加()意味着强制类型转换。
(void()())0是将0强制转换为void()()类型。
((void ()()) 0)()表示在调用地址为0的函数之前,先转换0的强类型。
当然,我们在日常生活中一般不会使用这样“怪异”的代码,也不能只访问0平台的一些地址。
void (*signal(int,void(*)(int)))(int);当我们看这些代码时,我们可以一步一步地把它们拆开。首先,使用void( )(int)。这样,它就是一个普通的函数指针。返回值类型是void,它接收一个int类型的参数。说明函数信号的返回类型是函数指针信号(int,void()(int))
函数名为signal,其参数为int类型和函数指针类型。
这一段其实和int test(int a)有关;这类代码没有本质区别,都是函数声明,只是返回类型和参数看起来比较复杂。
我们的int测试(int a,char ch);声明一个函数,其返回类型为int,参数为int和char。
上面的代码声明了一个函数,它的返回类型是函数指针,它的参数是int和函数指针。
一时理解不了也没关系。以后看到更多特殊的返回类型你一般会慢慢理解的。
XI。函数指针的使用回调函数*说了这么多,函数指针在实际使用中有什么作用?这涉及到一个重要的内容回调函数。
回调是由函数指针调用的函数。如果你把一个函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用它所指向的函数时,我们说它是一个回调函数。回调函数不是由函数的实现者直接调用,而是在特定事件或条件发生时由另一方调用。
假设我们需要一个可以加减乘除的计算器,那么我们可以写下面的代码。
#include stdio.h int add(int a,int b){ return a b;}int sub(int a,int b){ return a-b;}int mul(int a,int b){ return a* b;}int div(int a,int b){ return a/b;}int main(){ int x,y;int input=1;int ret=0;do { printf( * * * * * * * * * * * * * * * * * * * * * * * \ n );printf( 1:add 2:sub \ n );printf( 3:mul 4:div \ n );printf( * * * * * * * * * * * * * * * * * * * * * * * * * \ n );Printf(请选择:);scanf(%d ,输入);switch(input){ case 1:printf( input操作数:);scanf(%d %d ,x,ret=add(x,y);printf(ret=%d\n ,ret);打破;2: printf(输入操作数:);scanf(%d %d ,x,ret=sub(x,y);printf(ret=%d\n ,ret);打破;3: printf(输入操作数:);scanf(%d %d ,x,ret=mul(x,y);printf(ret=%d\n ,ret);打破;情况4: printf(输入操作数:);scanf(%d %d ,x,ret=div(x,y);printf(ret=%d\n ,ret);打破;0: printf(退出程序\ n );打破;默认值:printf(错误选择\ n );打破;} } while(输入);返回0;}但是这段代码有些问题。我们在每一段中都写了它们
Printf(输入操作数:
scanf( %d %d,x,
ret=add(x,y);
这种类似但重复的内容。
而且这些函数的返回值和参数类型都是一样的,我们可以写下面的代码。
void counter(int(*con)(int,int)){ int x=0,y=0;Printf(输入操作数:);scanf(%d %d ,x,printf(ret=%d\n ,con(x,y));}上面的add、sub、mul、div函数,它们的返回值类型都是int,参数类型都是(int,int),所以我们可以用这样的函数指针来接收它们,然后调用它们。
int(*con)(int,int)
上面的计算器被改造成下面的。
#include stdio.h int add(int a,int b){ return a b;}int sub(int a,int b){ return a-b;}int mul(int a,int b){ return a* b;}int div(int a,int b){ return a/b;}void counter(int(*con)(int,int)){ int x=0,y=0;Printf(输入操作数:);scanf(%d %d ,x,printf(ret=%d\n ,con(x,y));}int main(){ int x,y;int input=1;int ret=0;do { printf( * * * * * * * * * * * * * * * * * * * * * * * \ n );printf( 1:add 2:sub \ n );printf( 3:mul 4:div \ n );printf( * * * * * * * * * * * * * * * * * * * * * * * * * \ n );Printf(请选择:);scanf(%d ,输入);开关(输入){情况1:计数器(添加);打破;案例二:计数器(子);打破;案例三:计数器(mul);打破;案例四:柜台(div);打破;0: printf(退出程序\ n );打破;默认值:printf(错误选择\ n );打破;} } while(输入);返回0;不是简单多了吗?当然,返回函数的使用并不止于此。
我们有一个void*类型的指针,它可以接受任何类型的指针。当然,在使用的时候,必须转换成其他类型才能使用,因为void是非类型化的。
# include stdio . h # include string . h int test(void * x,void*y,const char * ch){ return *(int *)x-*(int *)y;}int main(){ int a=1,b=2;printf(%d\n ,ret);}上面的代码充满了随机性,但是不用担心。我只是想给你展示一下void类型的用法。
此代码编译器没有报告错误或警告。当然,写这段代码只是为了说明我们可以用void接受任何类型,然后在使用时强制类型转换为我们需要的类型。
有了这样一个特性,我们就可以用更少的代码完成一些功能,比如写一个排序函数,可以对任何类型进行排序。
当然,这些只是回调函数的一些基本用法。
十二。函数指针数组
函数指针数组,数组是相同元素的集合。函数指针作为一个元素,当然可以放入数组中。
比如我们上面写的计算器代码add,sub,mul,div,其返回类型与参数相同,也可以看作是一类相同的元素。
int (*arr[5])(int x,int y)={0,add,sub,mul,div };既然是数组,自然可以用下标调用。
#include stdio.h int add(int a,int b){ return a b;}int sub(int a,int b){ return a-b;}int mul(int a,int b){ return a* b;}int div(int a,int b){ return a/b;}int main(){ int x,y;int input=1;int ret=0;int(*p[5])(int x,int y)={ 0,add,sub,mul,div };//transfer table while(input){ printf( * * * * * * * * * * * * * * * * * * * * * * * * * * * \ n );printf( 1:add 2:sub \ n );printf( 3:mul 4:div \ n );printf( * * * * * * * * * * * * * * * * * * * * * * * * * \ n );Printf(请选择:);scanf( %d ,输入);if((input=4 input=1)){ printf( input操作数:);scanf( %d %d ,x,ret=(*p[input])(x,y);} else printf(输入错误\ n );printf( ret=%d\n ,ret);}返回0;}汇总指针的基本语法和用法这里基本介绍完了,当然也只是基本内容。更深入的内容要看后续的使用需求和学习情况。
原创作品来自 blogger 1001,转载请联系作者取得转载授权,否则将追究法律责任。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。