前言
运行环境:centos7( Linux) 编译器: gcc 9.3.1 64位
指针是什么
地址是内存空间对应的编号。
指针就是地址!
指针作为c语言最难的语法之一,也是C语言和c++的特色之一。
指针变量(pointer)是一种基本数据类型。
指针变量和普通变量最本质的区别是——普通变量存放的是一个值,而指针变量存放的是一个内存地址。每一个变量具有两个意义,一个是值,一个是地址。
举个例子,变量就相当于一间公寓,值就是住在里面的人,地址就是公寓的门牌号。在生活中,我们既可以直接打电话将公寓中的朋友约出来见面,也可以通过寻找门牌号,在门牌对应的房间中见面,虽然过程不同,但结果都是见到了朋友。
同理,我们在访问变量的时候,既可以通过变量名直接访问,也可以通过它的地址间接访问。
定义指针
指针的声明与野指针
在实际操作中,人们往往将指针和指针变量通常为指针。
通过前面的学习,我们知道了如何定义一个变量: int a; 指针和这种定义很相似,如果我们定义一个指针p应该如何去写呢?我们在此使用了“*”符号,于是指针的格式就是int *p;当然,我们在定义的时候通常不会单独定义。我们前文说过,指针中存放的是一个地址,在没有初始化的情况下通常会随机指向一个地址,这称之为野指针(也被成为悬挂指针)。
野指针是一个非常危险的操作,如果其不慎指向了操作系统中一个重要的值,而我们又不慎更改,则可能导致电脑系统崩溃(当然现在的操作系统通常不会让你这样做),所以我们通常会采用这种定义方式int *p = NULL;让指针p置空就能避免出现野指针。
指针的*是跟随着变量名而非类型名,指针的大小是固定的,只与计算机本身有关,如果是32位操作系统,则指针位4个字节,如果是64位操作系统,则指针为8个字节。
指针定义的形式也是 存储类型 变量类型 指针名 ,通常在特别声明的情况外,也往往没有写存储类型,而由编译器自动补充,通常在函数内部会默认为auto。
不过要注意的一点是,野指针不仅仅会在初始化的时候可能出现,在程序运行的过程中也可能会出现,比如:指针指向的对象消亡但是指针依旧指向该地址或者是指针释放(free、delete)后没有置空。不过这属于知识点很靠后的内容,本文的正文暂时不涉及,如果感兴趣可以看一看本文末尾的“补充”板块。
所以我们在开发的过程中应该时刻注意指针所指向的地址。
我们回到正题,讲完了如何创建指针,我们再看看如何给指针赋值。
这个时候我们引入了取地址符号“&”,假设一个变量int a = 1; 那么要取a的地址操作就是&a;我们在给指针赋值的时候就会使用如下操作 int *p = &a;
不过我们也不一定非要在定义的时候赋初值,以下操作也是可以的。
int a = 1;
int *p = NULL;
p = &a;
同样,指针和普通的变量很相似,只不过指针存放的是地址,所以如下操作也是可以的。
int a = 1;
int *p1 = NULL;
int *p2 = NULL;
p1 = &a;
p2 = p1;
我们可以看到,p1的值赋给了p2;所以指针p1和p2都是存放的变量a的地址。
指针的解引用操作
讲完了如何定义指针,很容易就能联想到如何访问这个地址的值,这个时候我们使用的依旧是”*“,不过此时它的含义已经改变了,只有在定义的时候才是指针符号,这个时候它的操作叫做“解引用”。请让我们看一下以下的例子。
#include <stdio.h>
int main()
{
int a = 1;
int *p = &a;
printf("a = %d\n",a);
printf("*p = %d\n",*p);
}
运行结果
a = 1
*p = 1
我们可以看到对指针p进行解引用操作后得到了的值就是a的值,那么如果不对a解引用,p本身存放的值是什么呢?
#include <stdio.h>
int main()
{
int a = 1;
int *p = &a;
printf("&a = %p\n",&a);
printf("p = %p\n",p);
}
运行结果
&a = 0x7fffaf406e64
p = 0x7fffaf406e64
我们可以清楚的看到,p本身存放的是一串地址,而这串地址和就是a的地址。
看到了这里,读者或许心中有疑惑,我明明可以用a来获取值,为什么还要多此一举引入指针这个概念,其实很多人在初次接触的时候都会存在这样的疑问,不过很多相关的文章并没有很好的讲解过,但我没有留悬念的习惯,接下来我为大家举一个例子方便大家理解指针的重要性。
#include <stdio.h>
void swap(int a,int b){//这里创建一个值,作用是将a和b的值交换
int temp;
temp = a;
a = b;
b = temp;
}
int main()
{
int a = 1;
int b = 2;
swap(a,b);//交换a和b的值
printf("a = %d\n",a);
printf("b = %d\n",b);
}
读者请看一下这个例子,可以分析一下输出结果是什么。
运行结果
a = 1
b = 2
它的输出结果,和我们的预期完全相反,并没有进行交换。其原因就是——swap(int a,int b)中传入的参数其实是main函数中两个变量的副本,此a非彼a,此b非彼b,就像1班上和隔壁2班都有一个名字叫做张三的同学,但名字一样却不是一个人。同理,swap里面的a和main里面的a虽然都是a,并且值都是1,但是却不是一个a。
我们联想一下我想找1班的张三该怎么做,我们会找到一班所在的班级,再找到那个叫张三的人。也就是我们确定了地址是1班。那么接下来我们看一下下面更新后的代码。
#include <stdio.h>
void swap(int *pa,int *pb){//我们将main中a和b的地址传过来。
int temp;
temp = *pa;// 我们已经讲过*在此时已经是解引用操作,所以,*pa其实就相当 main中的a
*pa = *pb;
*pb = temp;//将pa和pb中存放的地址所对应的值进行了交换
}
int main()
{
int a = 1;
int b = 2;
swap(&a,&b);//将a和b的地址传入函数
printf("a = %d\n",a);
printf("b = %d\n",b);
}
运行结果
a = 2
b = 1
看,我们通过指针的方式成功将两个值进行了交换,看到这里,应该能感到指针的作用了吧
不过还有一个注意的一点,定义指针必须要和你想指向的变量相对应,比如int *对应的是int ,你不能用int *的指针去引用一个char类型的变量
指针的运算
说到指针的运算,就有一个不得不提的重要概念——指针的步长
指针的步长简单来说就是指针移动所跨越的长度。
我先来讲一下指针的四则运算——单目运算++、–双目运算+、-
内存中的地址是连续的,如果我们定义了一个变量 int a;那么它会在内存中开辟一个四个字节的空间,比如0x00,虽然它的地址是0x00,但是本质上它占用了0x00~0x03合计长度为四,所以从0x01到0x03中,其他数据是无法占用的。
那么我们再来看指针,假设有两个连续的变量int a和int b;它们的地址分别是0x00,0x04;那么指针的移动就是按照整个数据的长度移动,如int *p = &a; 那么p++之后,p中存放的地址就变成了0x04了,这个移动的长度就是指针的步长。
指针的步长就是按照指针所指的数据类型的长度来算的,例如char *pc,那么指针pc的步长就是char这个数据类型占用空间的大小,也就是一个字节。
说明白了步长,我们就可以开始正式学习四则运算。
- ++ :这个很好理解,和我们的变量一样,比如
int a = 0;a++之后的值就是1,但指针不一样,我们学习了步长的概念,指针的移动是按照步长来算的,如果p = &a;那么p++后就跳到了4个字节后的地址。也就是往后挪一个步长的长度。
- – – :和++类似,不过它相反,他是往前挪一个步长的长度,如果原地址是
0x04;那么- -后就是0x00
- + :和前面也是异曲同工,比如
int a = 0; int *p =&a;如果a的地址是0x00,那么p+10就是p往后挪了10个步长的长度,那么它的地址就是0x40
- – : 同上,前提也是int类型,如果p原地址是
0x40那么p-10就是往前挪10个步长的长度,新地址也就是0x00
多级指针
前面说过,一个变量是有两个意义,一个地址,一个数值。指针也不例外,它虽然值是存放的指针,但是它自身也存在地址,既然存在地址,那就不难有一个猜想——是否能用指针存放指针的地址。
答案是肯定的,这就是我们接下来要说的多级指针
先来说一说定义
#include <stdio.h>
int main()
{
int a = 1;//变量
int *p = &a;//一级指针
int **p2 = &p;//二级指针
printf("a = %d\n",a);//
printf("*p = %d\n",*p);//
printf("**p2 = %d\n",**p2);//
printf("&a = %p\n",&a);//a的地址
printf("p = %p\n",p);//p中存放的数据
printf("&p = %p\n",&p);//p本身的地址
printf("p2 = %p\n",p2);//p2中存放的数据
}
运行结果
a = 1
*p = 1
**p2 = 1
&a = 0x7ffe1d09e3b4
p = 0x7ffe1d09e3b4
&p = 0x7ffe1d09e3a8
p2 = 0x7ffe1d09e3a8
我们从代码上可以看到,二级指针使用了两个*号,这就是多级指针的表示方式,几级指针就用几个*号,同样在解引用的时候也一样。接下来我分析一下定义过程:
首先我们看到p2,我们可以把它当做一个一级指针,只不过p是一个存放地址的变量,这样再看是不是就清晰了很多,同样的,我们也可以引出int ***p3 =&p2;一样的遮住两个*号,就可以看成 int *p3=&p2只不过p2是一个存放地址的变量。
其实从另一个角度也可以理解,我们分开来看,int** *p3 = &p2,就可以理解成定义了一个指针p3,这个指针指向的类型是int**,而我们再看看 p2的定义,同理 int* *p2定义了一个指针p2,这个指针指向的类型是int*,最后看一下int *p,我们定义了一个指针p,这个指针指向的类型是int,这一个种分析方法不仅将多级指针联系了起来,就连指针和普通变量也联系了起来,两种方法读者都可以试一试。
我们再来看看解引用,同样使用的是*号,不过理解过程要稍微有点不同。
我们来看一看p2,它的解引用使用了**p2,我们沿着表达式逐步分析一下。
首先,我们在前文已经学习过*p能解引用出p指向的地址中的值。那我们将目光看到**p2,然后我们将*p2单独拎出来,就得到了* (*p2),这样就实现了逻辑上的降级,按照我们刚刚的想法,*p2解引用后就得到了p中存放的值,就是a的地址,然后我们将外面的*和被解引用后的值结合起来,就相当于对p进行了解引用,从而得到a的值:**p2-> * (*p2) ->*p ->a
总结一下,定义采用的是隔离思想,将每一个数据类型一个个剥离,解引用采用降级思想,一步步降低指针级数
void*型指针
void*型指针是一个特殊的指针类型,它的特点是可以承接任何指针,但是不能被解引用,不过这也好理解,因为这是一个空类型。
#include<stdio.h>
int main(){
int a = 10;
int *p = &a;
void *pv = p;
int *p2 = pv;
printf("a = %d\n",*p2);
return 0;
}
运行结果
a = 10
我们通过这个例子可以看到,pv可以被int*的指针传址,并传给p2,这也是void*型指针的主要用法。
当然这个void*指针一般也不会这样使用,通常这个传递是用在函数的返回值中。
指针常量和常量指针
前文我们已经初步了解了指针,现在我们开始详细介绍指针和各种概念的结合,可能初次理解有些困难,不过多看几次就能理解了。
我们在看到标题这一类的名词时要把握一个点,只看最后一个名词,前面的都不要看,然后这个标题就变成了“常量和指针”
所以我们知道了指针常量就是一个指针的常量,本质是一个常量,常量指针是一个指向常量的指针,本质是指针。
我们先来看一下两者的定义。
#include <stdio.h>
int main()
{
const int a = 1;//常量 line1
int b = 2;//变量 line2
const int *pa = &a;//常量指针 line3
int *const pb = &b;//指针常量 line4
}
文字定义很绕,如果直接换成代码这样看就清楚很多了。
我们先看line1,const 是一个常量标识符,常量就是定了就无法改变的量,所以这个a 就只能等于1 后续如果写一个a = 2;这样的赋值代码,编译器会抛出一个错误。
然后我们再看一看line3,我们分析一下它的类型是什么,先遮住pa,目光往前移动,能看到*,所以我们知道了pa是一个指针,那这个指针指向的数据类型是什么呢,接着往前看,是int类型,所以我们看到了pa是一个指向int类型的指针,但还没完,前面还有一个const,原来这个int还是一个常量,最终我们得出来一个结论,这是一个指针pa,它指向的是一个const int类型的值,也就是常量指针。
我们再来看一下line2,这是一个整型变量b,这没什么好讲的,我们将目光移到line4,还是一样的方法,我们遮住pb,剩下的就是一个int *const ,我们就近加上从const看一看,就是const pb,所以我们已经能看到pb被限制成了一个常量。那这是一个什么类型的常量呢,我们接着往前面看,这是一个*指针符号,所以我们已经能看出来了,pb是一个常量指针,这个指针常量指的是什么数据类型是什么呢,是int,最终我们得出来一个结论——这是一个指向int的指针常量。
我们将line3和line4分析清楚之后,就来讲一讲它们的性质。
常量指针
先来说说line3——常量指针,常量指针是它指向的一个值是常量,我们知道常量是不可更改的,所以当我们解引用的时候,这种操作是错的,*pa = *pa+1;但是这是指针指向的值被限制成了常量,我的指针本身没有限制,所以我完全可以改变指针pa中存放的地址,比如:
#include <stdio.h>
int main()
{
const int a = 1;//常量
const int a2 = 2;//常量
const int *pa = &a;//常量指针
pa = &a2;
}
我可以将pa中存放的地址从a的地址换成a2的地址。
我们来总结一下——常量指针本身的地址可以改变,但是它指向的值是一个常量所以不能改变
指针常量
接下来我们说说line4——指针常量,顾名思义是一个指针的常量,这个指针本身是个常量,常量不可更改,但是指针本身是常量关它指向的变量什么事,所以它的变量的值可以改变,比如:
#include <stdio.h>
int main()
{
int b = 2;//变量
int *const pb = &b;//指针常量
*pb = 3;//我可以将b的值从2改成3,所以这个操作是允许的
printf("b = %d",*pb);
}
运行结果
b = 3
我们看到b的值发生了改变,但是如果我们将pb本身存放的地址改一下呢?
#include <stdio.h>
int main()
{
int b = 2;//变量
int b2 = 3;
int *const pb = &b;//指针常量
pb = &b2;
}
运行结果
error: assignment of read-only variable ‘pb’
pb = &b2;
我们可以看到编译器抛出了一个错误,pb本身的地址不允许更改,因为它是一个常量,这就是指针常量,我们来总结一下——指针常量所指向的值可以更改,但是它本身的地址不可以更改
说到这里,我们如果想定义一个指向的值和地址都不能更改的指针该怎么做呢。
这就是常量指针常量,比如:
#include <stdio.h>
int main()
{
const int a = 1;
const int *const p = &a;
}
按照我之前提到的方法,请自行分析一下,相信你一定也能理解——指向常量的一个指针常量。
指针与数组以及指针数组与数组指针
对数组还不了解的读者可以先参考一下这篇文章论数组
这篇文章较为详细的讲解了指针和数组的关系,读者可以跳转过去,里面也详细讲解的数组指针和指针数组的区别。
相信大家看过了前面指针常量和常量指针的命名规则,也该也对这个标题的含义有了了解,其实这里可以拓展一下,我们根据命名规则可以衍生出很多概率,甚至可以无限叠加,比如:
数组指针数组——这是一个数组,数组里面存放的是几个指向其他数组的指针。
指针数组指针——这是一个指针,指向了一个存放着指针的数组。
指针数组指针数组——这是一个数组,数组中存放着几个指向存放指针的数组的指针。
指针数组指针数组指针——这是一个指针,它指向了一个数组,这个数组存放着很多指针,这些指针又分别指向了某些数组,这些数组中存放的内容是指针。
……
不过万变不离其宗,我们从后往前读,先全部遮住,然后一个一个和最后的那个名字叠加起来,就能很清晰的看懂这个概念的定义。
指针函数和函数指针
依旧是前面说过的思想,我们将最后一个名词保留,遮住前面的修饰词,然后就能清晰的看懂这个标题将的是“函数和指针”
指针函数
先来说一说指针函数。
按照我们之前的思想,我们在很容易推测出指针函数的格式。
指针函数顾名思义就是返回指针的函数,所以我们可以写一个函数名func(int a);然后加上它的返回值 int *func(int a),这并不是什么很神秘的东西,普通函数可以返回char 也可以返回int 指针函数只不过是返回一个指针罢了。
不过,指针函数的领域主要涉及到的是动态内存领域。在我们学习那个地方之前,我的建议是这一部分暂时做了解即可,因为初学者很容易造成内存泄漏和让指针变成野指针的操作。
不过此处我还是粗略的讲解一下指针函数。
#include <stdio.h>
#include <malloc.h>
int *func(int a){
int *data = (int*)malloc(sizeof(int));//在堆区开辟int大小的空间
*data = a+10;
return data;
}
int main()
{
int a;
int *p = func(a);指针接受返回的函数指针
printf("a = %d\n",*p);
free(p);//释放堆区空间
p = NULL;//置空指针,防止出现野指针
}
运行结果
a = 10
我们使用了一个int*的指针接住了func函数返回的堆区指针,然后操作,之后记得释放空间以及置空指针,否则会造成内存泄漏和指针悬挂,也就是野指针。在free后面紧跟着置空指针是一个好习惯,这个比较推崇读者学习。
这个内容对于初学者来说有点超前,只需要了解即可
函数指针
接下来看一看函数指针。
顾名思义,它是一个指针。
我们先来看它的基本格式:
int (*func)(int a)
这和指针函数很像,不过区别是函数指针多了一个小括号。这个原因涉及到了优先级的问题,()的优先级是大于*号的,所以func会先和(int a)结合,但是这样就成了指针函数了,所以我们加了括号,强行让*和func结合,这个变量就变成了指针。
#include <stdio.h>
int func(int a){
a+=10;
return a;
}
int main()
{
int a = 1;
int (*p)(int)=NULL;//函数指针
p = func;
a = p(a);
printf("a = %d\n",a);
}
运行结果
a = 11
这里需要补充一个知识点,函数名的本质也是一个指针,这个指针中存放了这个函数的地址。
这也是我们可以直接使用函数指针p = func而不需要取地址的原因。
然后我们的所有操作都可以通过p来进行,相当于p取代了func,当然func依旧可以使用。
函数指针的引用其实也比较广泛,在使用c语言写底层系统的时候,在多级菜单栏这些功能上常常会用到函数指针调用函数。
我们在学习C语言的时候,常常会因为老师或者书本没有给出实际运用的例子,而觉得某个语法无用,其实只不过是我们还未接触到罢了,这也是学C语言的一道坎。
指针和动态内存分配
这个模块就不展开讲了,因为这个是一个很大的知识板块,不过以下有一些常用的动态内存函数:
malloc():在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
calloc():在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是 。
realloc():该函数重新分配内存,把内存扩展到 newsize。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。
free():该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
补充
易发生野指针的情况
这里我们补充一下前文埋下的野指针的伏笔。
生命周期结束
首先我们来说一说指针所指的对象消亡的情况,我们来看一看下面的例子:
#include <stdio.h>
int* func(){
int num = 10;
return #
}
int main()
{
int *a = NULL;
a = func();
printf("a = %d\n",*a);
}
我们在这个例子中能看到 a 承接了num的地址,但是实际上,在函数func的生命结束后,num也就随之释放了,所以此时出现了所指对象消亡,那么a就顺其自然变成了野指针。
指向堆区的指针被释放
另一种情况就是free和delete释放内存后的情况,这里就用free作为例子
#include <stdio.h>
#include <malloc.h>
int *func(int a){
int *data = (int*)malloc(sizeof(int));//在堆区开辟int大小的空间
*data = a+10;
return data;
}
int main()
{
int a;
int *p = func(a);指针接受返回的函数指针
printf("a = %d\n",*p);
free(p);//释放堆区空间
p = NULL;//置空指针,防止出现野指针
}
这个指针函数的例子就很适合,我们这里看到了func返回了指针,然后被p接住了,free释放内存后,p也就成为了野指针,所以需要置空来消除野指针的隐患。
数据在内存中的存储
指针在内存中的最小可寻址单位通常是字节,也就是说一个int类型的数据在内存中其实包含了四个地址,而通常int*的指针指向的是数据的首地址。这个底层逻辑变给了我们更多的对数据的操作方式。
那么接下来就让我们解开内存的“庐山真面目”
#include <stdio.h>
int main(){
/*我们定义了一个变量a,int的大小是4个字节,那么我们可以在逻辑上将其分成四个区块,
*每个区块存一个数,如下,分别存储1、3、7、15,但这只是逻辑的,实际上a依旧表示一
*个整数,这个整数用16进制表示0x103070f
*/
int a=0x103070f;//0000001 00000011 00000111 00001111
// 1 3 7 15
int *p = &a;
printf(" p = %p\n",p);
printf("*p = %d\n\n",*p);
/*为了直观和容易理解,我此处选择使用比较繁琐的写法
*我们在这里定义了一个数组,数组就是相同类型的元素放在一起,b[4]的含义就是b里面放了4个元
*素,分别存放了p(我们将p看作a中四个字节中的首个元素的地址)的地址,以及p+1、p+2、p+3的
*地址,一共四个地址。
*/
long long int b[4] = {(long long int)p,(long long int)p+1,(long long int)p+2,(long long int)p+3};
//我们拿出数组中第一个地址,b[0]代表第一个元素,我们将p强制转换为地址。
//为什么使用char*,是因为char*刚好是一个字节,是最小的可寻址单位
char *p1 = (char*)(b[0]);
printf(" p1 = %p\n",p1);
printf("*p1 = %d \n\n",*p1);
char *p2 = (char*)(b[1]);
printf(" p2 = %p\n",p2);
printf("*p2 = %d \n\n",*p2);
char *p3 = (char*)(b[2]);
printf(" p3 = %p\n",p3);
printf("*p3 = %d \n\n",*p3);
char *p4 = (char*)(b[3]);
printf(" p4 = %p\n",p4);
printf("*p4 = %d \n\n",*p4);
return 0;
}
运行结果

我们从结果可以看出,输出结果和我们的预期一模一样,a的地址是它的四个地址块的首元素地址每个地址块都存放了相应的数据。
不过细心的读者已经发现了一个问题——为什么地址的方向和数据的方向是相反的。
这就得提到一个概念——字节序
字节序就是内存中数据的顺序,分为大端字节序和小端字节序。
小端字节序
说字节序之前我们先说一个概念,数据的高位低位和地址的高位低位。
先说一说地址的高位低位,为了便于理解,我们直接通过以下例子来讲解,假设有两个地址0x0000和0x0001这个地址的低位就是0x0000,高位就是0x0001。
再说一说数据高位,这个和地址恰恰相反,假设有一个16进制数,0x12345678,它的高位是12然后是34等等,至于为什么是两个数,因为一个字节是8位,最多的存储也就是11111111,换算成16进制是ff,显然不管是12还是34都是小于ff的。
接下来谈谈小段字节序,小段字节序就是数据低位放在地址低位,数据高位放在地址高位,所以如果地址是0x0000、0x0001、0x0002、0x0003,在实际的存放过程中,12 34 56 78存放顺序因该是78 56 34 12。
作者使用的x86平台则是采用的小端字节序
大端字节序
大端字节序是高位字节放在低位地址中,低位字节放在高位地址中,和小端字节序相反。
我们依旧采用上面的例子,如果在大端字节序存储,若地址是0x0000、0x0001、0x0002、0x0003,在实际的存放过程中,12 34 56 78存放顺序因该是12 34 56 78。
评论