0.为什么使用指针
假如我们定义了 char a=’A’ ,当需要使用 ‘A’ 时,除了直接调用变量 a ,还可以定义 char *p=&a ,调用 a 的地址,即指向 a 的指针 p ,变量 a( char 类型)只占了一个字节,指针本身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。
但如果要引用的是占用内存空间比较大东西,用指针也还是 4 个字节即可。
使用指针型变量在很多时候占用更小的内存空间。 变量为了表示数据,指针可以更好的传递数据,举个例子:
第一节课是 1 班语文, 2 班数学,第二节课颠倒过来, 1 班要上数学, 2 班要上语文,那么第一节课下课后需要怎样作调整呢?方案一:课间 1 班学生全都去 2 班, 2 班学生全都来 1 班,当然,走的时候要携带上书本、笔纸、零食……场面一片狼藉;方案二:两位老师课间互换教室。
显然,方案二更好一些,方案二类似使用指针传递地址,方案一将内存中的内容重新“复制”了一份,效率比较低。
在数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。
一个数据缓冲区 char buf[100] ,如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int ,使用如下语句进行赋值:
*(short*)&buf[0]=DataId; *(short*)&buf[2]=DataType; *(int*)&buf[4]=DataValue;
数据转换,利用指针的灵活的类型转换,可以用来做数据类型转换,比较常用于通讯缓冲区的填充。
指针的机制比较简单,其功能可以被集中重新实现成更抽象化的引用数据形式
函数指针,形如: #define PMYFUN (void*)(int,int) ,可以用在大量分支处理的实例当中,如某通讯根据不同的命令号执行不同类型的命令,则可以建立一个函数指针数组,进行散转。
在数据结构中,链表、树、图等大量的应用都离不开指针。
1. 指针强化
1.1 指针是一种数据类型
操作系统将硬件和软件结合起来,给程序员提供的一种对内存使用的抽象,这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。
内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有唯一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范围就是 0 ~ 4294967296 ,十六进制表示就是 0x00000000~0xffffffff ,当程序使用的数据载入内存时,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。
1.1.1 指针变量
指针是一种数据类型,占用内存空间,用来保存内存地址。
void test01(){ int* p1 = 0x1234; int*** p2 = 0x1111; printf("p1 size:%d\n",sizeof(p1)); printf("p2 size:%d\n",sizeof(p2)); //指针是变量,指针本身也占内存空间,指针也可以被赋值 int a = 10; p1 = &a; printf("p1 address:%p\n", &p1); printf("p1 address:%p\n", p1); printf("a address:%p\n", &a); }
1.1.2 野指针和空指针
1.1.2.1 空指针
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。
对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。
如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。 不允许向NULL和非法地址拷贝内存:
void test(){ char *p = NULL; //给p指向的内存区域拷贝内容 strcpy(p, "1111"); //err char *q = 0x1122; //给q指向的内存区域拷贝内容 strcpy(q, "2222"); //err }
1.1.2.2 野指针
在使用指针时,要避免野指针的出现:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
什么情况下会导致野指针?
指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
指针释放后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
void test(){
int* p = 0x001; //未初始化
printf("%p\n",p);
*p = 100;
}
操作野指针是非常危险的操作,应该规避野指针的出现:
初始化时置 NULL
指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。
释放时置 NULL
当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。
1.1.2.3 void*类型指针
void是一种特殊的指针类型,可以用来存放任意对象的地址。一个void指针存放着一个地址,这一点和其他指针类似。不同的是,我们对它到底储存的是什么对象的地址并不了解。
double a=2.3; int b=5; void *p=&a; cout<<p<<endl; //输出了a的地址 p=&b; cout<<p<<endl; //输出了b的地址 //cout<<*p<<endl;这一行不可以执行,void*指针只可以储存变量地址,不可以直接操作它指向的对象
由于void是空类型,只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。
1.1.2.4 void*数组和指针
同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的。指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在32位平台下,无论指针的类型是什么,sizeof(指针名)都是 4 ,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8 。
数组名作为右值的时候,就是第一个元素的地址
int main(void) { int arr[5] = {1,2,3,4,5}; int *p_first = arr; printf("%d",*p_first); //1 return 0; }
指向数组元素的指针 支持 递增 递减 运算。p= p+1意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。在数组中相邻内存就是相邻下标元素。
1.1.3 间接访问操作符
通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。
注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。
int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]&arr1
1)在指针声明时,* 号表示所声明的变量为指针
2)在指针使用时,* 号表示操作指针所指向的内存空间
*相当通过地址(指针变量的值)找到指针指向的内存,再操作内存
*放在等号的左边赋值(给内存赋值,写内存)
*放在等号的右边取值(从内存中取值,读内存)
//解引用 void test01(){ //定义指针 int* p = NULL; //指针指向谁,就把谁的地址赋给指针 int a = 10; p = &a; *p = 20;//*在左边当左值,必须确保内存可写 //*号放右面,从内存中读值 int b = *p; //必须确保内存可写 char* str = "hello world!"; *str = 'm'; printf("a:%d\n", a); printf("*p:%d\n", *p); printf("b:%d\n", b); }
1.1.4 指针的步长
指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。
思考如下问题:
int a = 0xaabbccdd; unsigned int *p1 = &a; unsigned char *p2 = &a; //为什么*p1打印出来正确结果? printf("%x\n", *p1); //为什么*p2没有打印出来正确结果? printf("%x\n", *p2); //为什么p1指针+1加了4字节? printf("p1 =%d\n", p1); printf("p1+1=%d\n", p1 + 1); //为什么p2指针+1加了1字节? printf("p2 =%d\n", p2); printf("p2+1=%d\n", p2 + 1);
1.1.5 函数与指针
1.1.5.1 函数的参数和指针
C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。
void change(int a) { a++; //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。 } int main(void) { int age = 60; change(age); printf("age = %d",age); // age = 60 return 0; }
有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。
传递变量的指针可以轻松解决上述问题。
void change(int* pa) { (*pa)++; //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时, //会直接去内存中找到age这个数据,然后把它增1。 } int main(void) { int age = 160; change(&age); printf("age = %d",age); // age = 61 return 0; }
比如指针的一个常见的使用例子:
#include <stdio.h> #include <stdlib.h> #include <string.h> void swap(int *,int *); int main() { int a=5,b=10; printf("a=%d,b=%d\n",a,b); swap(&a,&b); printf("a=%d,b=%d\n",a,b); return 0; } void swap(int *pa,int *pb) { int t=*pa;*pa=*pb;*pb=t; }
在以上的例子中,swap函数的两个形参pa和pb可以接收两个整型变量的地址,并通过间接访问的方式修改了它指向变量的值。在main函数中调用swap时,提供的实参分别为&a,&b,这样就实现了pa=&a,pb=&b的赋值过程,这样在swap函数中就通过pa修改了 a 的值,通过pb修改了 b 的值。因此,如果需要在被调函数中修改主调函数中变量的值,就需要经过以下几个步骤:
定义函数的形参必须为指针类型,以接收主调函数中传来的变量的地址;
调用函数时实参为变量的地址;
在被调函数中使用*间接访问形参指向的内存空间,实现修改主调函数中变量值的功能。