首先内存会划分为小的内存单元,每个内存单元都有一个编号,这个编号就被称为地址,我们把**地址也叫指针**,如下图所示:
注意:指针指的是地址,但是我们口语中的指在这里插入图片描述
针通常指的是指针变量
指针变量就是用来存放地址的变量
int a=10; //定义一个整型a
int* pa=&a;//定义一个指针pa指向a
*pa=9; //就可以通过修改指向指针的值来修改a的值
其中修改a的值的过程与a=9等价
但是两个**’‘的意义不同,int表示的是指针指向的类型是整型,而第二个‘***’是解引用操作符。
关于指针的大小:在32位和64位的大小不同,并不会因为指针类型的改变而改变,无论是整形指针(int*)还是字符指针(char*)在32位的平台下都是4个字节因为32bit=4byte,64位也是同理大小为8字节。
指针类型决定了,指针在被解引用的时候所访问的权限,例如:
整型指针解引用会访问4个字节
字符指针解引用会访问1个字节
下面来具体证明这两个类型的指针的访问权限:
a位16进制0x11223344,在监视页面a与*pa相同,都是数值;&a与pa相同都是地址;
在a的地址中,发现内存存储位44 33 22 11,这里就是小端序,然后再运行*pa=0后,我们再看内存可知:
a被改为00 00 00 00,4个字节都改为0了
与整形指针的顺序不变,代码顺序都不变,就改变了指针类型
int a=0x11223344;
char* pa=&a;
*pa=0;
跟整型指针都没差什么。就唯一个点是指针访问权限;
字符指针类型,解引用智能改变2个16进制位,也就是8个bit位,1个byte位
具体意思是指针变量加或减一,它的地址变化的量,如下所示;
这段代码定义了整形指针变量pa和字符指针变量pb,用%p打印地址,观察运行结果可知pa和pb的地址相同,因为这两个指针都指向a的地址,但是pa+1相对于pa加了4,而pb+1相对于pb加了1,从这个能看出来指针的类型能决定指针向前或向后走一步能走多大的距离。
**野指针(Dangling pointer)**是指一个指针指向的内存地址已经被释放或者不再有效,但是该指针仍然保留着这个无效的地址。当我们使用一个野指针时,由于其指向的内存已经被释放,可能会导致程序崩溃、数据损坏或其他不可预测的行为。
野指针就是指针指向位置是不可知的
1.野指针的成因
这种问题非常常见,具体看以下代码;
int main(){
int* p;
*p=10;
return 0;
}
运行后报错就会显示这种错误
因为代码int* p没有初始化,所以p变量中存的地址并没有指向我们当前程序的空间,而是指向内存中随机的空间,所以就会报错
②指针的越界访问
这个问题十分常见,我们在遍历数组中,一不小心就会遇到这样的问题;
这个是因为我们设定了指针变量p指向数组array首元素的地址,数组有5个元素,但是循环了6次,多循环了一次,**导致非法访问内存,**因此第六个数打印出来是随机数,这也就是野指针的问题了
③指针所指向的空间释放
int* test(){
int x=20;
return &x;
}
int main(){
int* p=test();
*p=30;
return 0;
}
test的返回值是x的地址,main函数中用指针变量p接受x的地址,但是x变量进入text函数就创建,出了test函数就会被销毁,这是再改变*p的值,就用的是x的地址,则是非法访问内存了,也会造成野指针的问题
2.如何规避野指针
①指针要初始化;
int a=10;
int* p=&a;
②要注意指针越界的问题
注意元素个数和循环次数
在我们不使用指针变量p时,int* p=NULL;把它变为空指针,在接下来使用p时,用if语句;if(*p!=NULL)
④避免返回局部变量的地址
1.常量指针
常量指针的概念是:指针所指空间的值不能发生改变,不能通过指针解引用修改指针所指向空间的值,但是指针的指向是可以改的
2.指针常量
指针常量的概念是:指针本身就是一个常量,即指针的指向不能发生改变,但是指针所指向空间的值是可以发生改变的,可以通过解引用改变指针所指向空间的值
具体怎么更好的理解呢?
大家先看常量指针这个名字,以指针结尾,所以是具有指针的性质的,因此可以改变指向,又因为是常量指针,所以指向的值是常量的,即是不能改变的
再看指针常量这个名字,以常量结尾,应该具有常量的一些性质,所以自然不能改变指针的指向,只能修改指针所指向的值了
eg:
常量指针:const int *pa = &a;、int const *pa = &a;
指针常量:int *const pa = &a;
1.指针±整数
指针变量p是数组第一个元素的地址,用for循环打印数组中的的各个元素,因为*的优先级高于++,所以每次执行 *p打印完数组中元素后,再用p++指向数组下一个元素,从而循环5此打印出数组中5个元素
2.指针-指针
指针-指针的操作得到的是指针和指针之间元素的个数,当然前提是两个指针必须指向同一块空间
指针的关系运算也就是指针之间进行大小的比较,从而实现某些功能,例如:
规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较(如上方的例子),但是不允许与指向第一个元素之前的内存位置的指针比较
字符指针就是存着字符的地址,就是讲字符变量的地址放到字符指针里面去,举例如下:
定义指针比那辆pa存a的地址,改变*pa的值,a也会随之改变
int main(){
const char* p1="abcde";
const char* p2="abcde";
char p3[]="abcde";
char p4[]="abcde";
return 0;
}
这是一道非常经典的例题:大家可以思考一下,p1和p2,p3和p4是否相等
答案是:p1和p2相等,p3和p4不相等
因为”abcde“是常量字符串,储存在常量区,所以p1和p2都是指向这个常量字符串的,储存的都是a的地址,所以p1和p2是相等的
而p3和p4分别是两个数组的初始化,所以p3和p4分别表示两个数组的首元素地址,所以p3和p4是不同的
整型数组就是存储整型的数组,字符数组也跟整型数组一样,以此类推指针数组的定义就很简单了,就是存储指针的数组eg:int* arr[3]={&a,&b,&c},数组有3个元素,数组中每个元素都是指针,每个元素的类型都是int*,这样的数组就叫做指针数组
arr数组中每一个元素都是int*类型,那么arr[i]就是表示arr数组中的元素,也就是arr数组中的3个整型数组的首元素地址,arr [i] [j] 就是3个整型数组中的下标为j的整型元素,所以这个指针数组的应用相当于一个二维数组
另一种表示方法是*(arr[i]+j),arr[i]就是表示arr数组中的元素,即3个整型数组的首元素地址,再+j就是指从首元素地址向后移j个元素的地址,再解引用打印出来,也是整型数组的元素
指针数组是数组,那么数组指针就是指针了,int* p=&a,是指向整型的指针,存放整型变量的地址;而字符指针是指向字符的指针,存放字符变量的地址;那么数组指针自然就是指向数组的指针了
首先对于整型指针,int a=5;int* pa=&a,给出一个整型变量a,取a的地址放到pa里面这里的指针变量pa的类型是int*,可以认为是int * pa去掉pa这个变量名后,剩下的就是这个指针的类型
那么同理,int arr[3]={0}; int (* parr) [3] = &arr ;给出一个整型数组arr,里面有3个元素,那么取数组的地址就自然应该放在数组指针parr里面,那么问题来了,数组指针parr的类型是什么呢?很简单,也是int (* parr) [3]去掉指针变量名字即可得到,即int (*) [3],根据类型可以得到,这样的一个指针,指向一个数组,数组有3个元素,每个元素的类型是int
下面举个例子
代码中需要将数组和指针传给函数,涉及到了数组函数,指针参数的概念
void test1()
{}
void test2()
{}
int main(){
int arr1[5]={0};
int* arr2[15]={0};
test1(arr1);
test2(arr2);
return 0;
}
2.二维数组传参
void test()
{}
int main(){
int arr[2][3]={0};
test(arr);
return 0;
}
3.一级指针传参
用一级指针传参,就用一级指针接收
void test(int* ptr)
{}
int main(){
int arr[10]={0};
int* p=arr;
test(p);
return 0;
}
4.二级指针传参
void test(int** ptr)
{}
int main(){
int a=0;
int* pa=&a;
int** ppa=&pa;
test(ppa);
return 0;
}
//同样的,如果test的参数是int**ptr,那么函数可以接受以下三种参数
//test(ppa);
//test(&pa);
//test(arr);
函数指针就是指向函数的指针,下面举函数指针例子
int add(int a,int b){
return a+b;
}
int main(){
int(*p)(int,int)=&add;
return 0;
}
这里的p就是函数指针
首先p和*在括号中先结合,表明p是指针,后面括号中是该函数的参数类型
int print(int* ptr)
{}
int main(){
int(*p)(int*)=print;
return 0;
}
这里一样p是函数指针,的那这里是print而不是&print,是因为这两种表示的意义相同
调用时,上面两种都可以调到函数
函数指针重命名
如果一个函数指针类型是int (* )(int,char),觉得太长太复杂,可以用typedef重命名,但这里的重命名和以前的命名格式不同,所起的名字是要放在* 号后面的,即:typedef int (*p)(int,char),代码意思就是给函数指针类型int ( * )(int,char)重新起名叫p
函数指针数组是一个数组,数组中存的是函数指针,如下所示
int test1(int a,int b)
{}
int test2(int a,int b)
{}
int test3(int a,int b)
{}
int main(){
int(*p1)(int,int)=&test1;
int(*p2)(int,int)=&test2;
int(*p3)(int,int)=&test3;
int(*ptr[3])(int,int)={test1,test2,test3};
return 0;
}
(int,char),代码意思就是给函数指针类型int ( * )(int,char)重新起名叫p
函数指针数组是一个数组,数组中存的是函数指针,如下所示
int test1(int a,int b)
{}
int test2(int a,int b)
{}
int test3(int a,int b)
{}
int main(){
int(*p1)(int,int)=&test1;
int(*p2)(int,int)=&test2;
int(*p3)(int,int)=&test3;
int(*ptr[3])(int,int)={test1,test2,test3};
return 0;
}
int(*ptr[3])(int,int)就是函数指针数组的运用,ptr是函数指针数组,去掉数组名ptr和[3],可知数组中每个元素是int( * )(int,int)的函数指针,所以称为函数指针数组a