C语言__指针总结__

1.什么是指针

指针是一种数据类型,使用它定义的变量叫指针变量,其值为另一个变量的地址,即内存位置的直接地址(内存地址的整数)

2.为什么要使用指针

A.解决函数之间无法通过传参来共享变量:

函数的形参变量属于被调用者,实参属于调用者,函数之间的变量名是可以互相重名的,因为他们的存放空间是各自独立的,互不相干,他们之间的数据传递都是值传递(内存拷贝、赋值)

B.优化函数之间的传参效率:

假设你有一个很大的数组需要传,如果只是单纯的用赋值、内存拷贝,效率是很低的,但如果你是用指针,传的是一个数组的首地址,接下来被调用者就会根据你给的内存地址去接收数据,因为只是传地址,所以效率会很高

C.使用堆内存:

因为C语言中没有提供管理内存的语句,只能依靠标准库中提供的函数来对堆进行申请和释放,而且堆内存无法与标识符建立联系,只能配合指针使用

3.如何使用指针

定义:类型* 变量名p
1.指针变量一般以p结尾,为了区别与普通变量
2.* 表示此变量是指针变量,一个* 只能定义一个指针变量,不能连续定义,如:

int* p1,p2,p3;		//p1是指针变量,p2,p3是int变量
int *p1,*p2,*p3;		//三个指针变量

3.类型表示的是存储的是什么类型变量的地址,它决定了当通过地址访问这块内存时要访问的字节数
4.指针变量的默认值是不确定的,一般初始化为NULL(空指针)
5.赋值:因为指针变量里面存放的是内存的地址,所以给指针变量也要用地址给他赋值,格式为:指针变量 = 地址,如:

栈地址赋值:
int num = 0;
int* p = NULL;
p = #
堆地址赋值:
int* p = NULL;
p = malloc(4);			// 向堆内存申请4个字节的空间

6.解引用(根据地址访问内存):指针只是一个地址,传来传去,但要想得到里面的值,这就需要一把"钥匙",这个"钥匙"就是* ,指针和变量的关系就像是这样:*指针变量名 <=> 变量,他俩是等价的。
根据指针变量中存储的内存编号去访问内存中多少字节,这就是由指针变量的类型所决定了;
如果指针变量中存放的地址出错,此时访问可能会发生段错误(赋值产生的错误)。

4.使用指针要注意的问题

指针的使用,主要会发生下面2个问题 。
1.空指针:

指针变量的为NULL(大多数为0,也有特殊情况为1),这种指针变量叫空指针,空指针是不能进行解引用的,因为NULL被操作系统当作复位地址(里面存储了系统重启所需要的数据),当操作系统察觉到程序试图访问NULL位置的数据时,系统就会向程序发送段错误的信号,程序就会死亡,说白了,你(程序)要想在我(操作系统)的地盘上运行,你就得遵守我的规矩,禁区(NULL)看看就行,知道有这个地方(可以初始化为NULL),但不要乱进(解引用)。
空指针还可被当作错误标志,如果一个函数返回值类型时指针类型时,但它实际上返回的值NULL,则说明函数执行失败或者出错。
在C语言中,应该杜绝对空指针进行解引用,当使用来历不明的指针(调用者提供的)时,应首先判断该指针是否为NULL,如:

if(NULL == p)
{

}
2.野指针:

指针变量的值是不确定的,或者都是无效的,这种指针叫野指针。使用野指针不一定会产生问题,可能产生的后果如下:
1.一切正常(运气好)
2.段错误
3.脏数据

上面说野指针不一定会产生问题,那是不是说明野指针会比空指针好一点,因为一旦对空指针进行解引用,就必然会出现段错误,而野指针运气好就不会出现,其实不然,野指针其实比空指针危害更大,因为野指针是无法判断出来的、也无法测试出来,也就意味着一旦产生,无法杜绝。
那有什么办法来解决野指针呢,虽然野指针无法判断也无法测试出来,但是所有的野指针都是人为制造出来的,最好的办法就是防范于未然,不产生野指针,方法有:
1.定义指针变量时,要对其进行初始化
2.不返回局部变量的地址
3.资源释放后,指向它的指针要及时置空

5.指针与数组的关系

数组名就是指针(常指针),数组名与数组首地址是映射关系(即数组名与数组首地址对应),而指针是指向关系
由于数组名就是指针,所以数组名可以使用指针的解引用运算符,如:

#include 

int main()
{
		char str[] = "hello";
		printf("%c",*str);
}
因为str是str这个数组的首地址,所以解引用出来的就是该数组首个字符
输出结果为:h

而指针也可以使用数组的[ ]运算符,如

#include 

int main()
{
	char str1[] = "hello";
	char* p = str1;
	printf("%c",p[0]);	
}
输出结果:h

需要注意的是,当使用数组当函数的参数时,数组会蜕变成指针,长度也就丢失了,因此需要额外再添加一个参数用来传递数组的长度:

#include 
#include 

void func(char a[])
{
	printf("%d",sizeof(a));
}

int main()
{
	char a[] = "hello world";
	printf("%d",sizeof(a));
	puts("");
	func(a);
	return 0;
}
输出:12
	  4
第一行输出12没问题,
第二行输出4是因为将数组作为参数传过去是,传的是数组的首地址,没有把长度也传过去

6.指针的运算

指针的本质就是个整数,因此从语法上来说整数能使用的运算符它都能使用,但也不是所有的运算符对指针运算都是有意义的
指针 + 整数 <=> 指针 + 宽度 * 整数,向右移动,如

#include 
#include 

int main()
{
	int num = 5;
	int* p = #
	printf("%x,%x",p,p+1);
	return 0;
}
输出结果:bfba6ff8,bfba6ffc	
因为内存地址是随机分配的,所以每次执行的地址都有可能是不同的,但他们两个地址的差都是4(一个int类型的大小)

指针 + 整数 <=> 指针 - 宽度 * 整数,向左移动,例子跟上面的差不多,可自行实验
指针 - 指针 <=> 指针 - 指针/宽度 计算出两个指针之间相隔多少个元素。如:

#include 
#include 

int main()
{
	char str1[] = "hello";
	char str2[] = "world";
	char* p1 = str1;
	char* p2 = str2;
	printf("%x,%x",p1,p2);
	puts("");
	printf("%d",p2 - p1);
	return 0;
}
输出结果:bfd90e90,bfd90e96
		 6
因为char类型是1字节,所以中间隔了6个元素

7.指针与const配合

在了解配合之前,先讲一下我的理解,因为p是一个指针变量,相当于一个宝箱,没有 * 这个钥匙,就打不开宝箱,所以const加在 * 前还是 * 后,就能决定保护的是地址里面的值,还是保护地址的值
const int * p:保护指针指向的数据,不能通过指针解引用修改内存的值。
int const * p:保护指针指向的数据,不能通过指针解引用修改内存的值。(因为const加在 * 前,说明这个宝箱已经被打开,所以保护的是里面的宝物)
int * const p:保护指针变量,指针变量初始化之后不能再显式的赋值。(因为const加在 * 后,说明这个宝箱还没被打开,所以保护的是宝箱)
const int const * p:既不能修改指针的值,也不能修改内存的值。
int const * const p:既不能修改指针的值,也不能修改内存的值。(因为const既加在 * 前和 * 后,说明及保护宝箱也保护宝物)

8.什么是二级指针、什么情况下使用

说到一级指针,就会有二级指针,一级指针指向的是存放变量的内存地址,而二级指针则指向的是存放一级指针变量的内存地址,即指向指针的指针。
就好像挖宝,你挖到第一个宝箱,打开一看,里面放的是另一张藏宝图,然后你根据藏宝图找到了第二个宝箱,里面放的就是你要的宝藏,那么第一个宝箱就像是二级指针,第二个宝箱就像是一级指针,而你想要的宝藏,就像是普通变量。

那么要在什么情况下使用呢,一级指针是为了再调用函数的,能共享变量,以及优化传参效率。那么二级指针就可知道了,那就是为了调用函数时,能共享一级指针变量。也就是说,当你调用的函数要对一级指针进行修改时,传参就需要传递二级指针。或者当你要传递一级指针数组时(数组里面存放的是指针变量),传参时也需要二级指针(跟一级指针传数组是一样的)

总的来说,二级指针和一级指针是差不多一样的,只不过一级指针指向的是普通变量,二级指针指向的是指针变量,理论上来讲,也有三级指针,四级指针,但是基本用不到,估计也就学术上,考试上用到三级指针。

9.函数指针

函数指针本身就是指针变量,所以在使用上跟指向变量的指针差不多,那么函数指针要怎么样指向这个函数呢
C语言在编译时,每个函数都有一个入口地址,那么函数指针就是指向这个入口地址的指针。
那么函数指针的用途是什么,主要有两点,一个是调用函数,另一个时作为函数的参数
函数指针可以体现在回调函数这一块内容。

10.数组指针

数组指针,就是指向数组的指针,指的是数组的首地址。
定义方法:类型 (*p)[数组大小]
下面举例说明:

int (*p)[10]; // 因为()优先级较高,所以p是一个指针,指向一个整形的一维数组,10就是这个p的跨度(就像int的跨度为4,char为1)

由上面的例子得到二位数组的定义
定义方法:类型 (*p)[数组大小]
下面定义一个a[5][10]来举例说明:

int (*p)[10]  // 同一维数组定义一样,只不过可以对p进行加减
p++;
printf("%d",p[5]) // p[5] <=>a[1][5];因为p指向的是首地址,当加一后,加的是这个数组一行的长度,所以p就指向了第二行,
					所以p[5]等价于啊a[1][5]

11.指针数组

指针数组,就是存储指针的数组,这个数组里面存放的都是指针变量。
定义方法:类型 *p[n]

因为[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。
就是一个数组里面,存的都是指针变量。
那么如何要将二维数组赋给一指针数组呢:
就是该指针数组的第一个元素,指向二维数组的的一行,第二个元素指向第二行,以此类推

int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i]=a[i]
这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]
所以要分别赋值。

这样指针数组和数组指针的区别就好理解了,数组指针只是一个指针变量,指向的是一个数组。而指针数组是一个数组里面存放个指针变量。

主要怎么区别指针数组和数组指针,需要看它是怎么定义的,就需要了解一些符号的优先级:()>[]>*

12.结构体指针

结构体指针也是一种指针,用法在我感觉上,跟二维数组指针有点像,如:

#include 

typedef struct Student
{
	char name[20];
	int age;
}Stu;

int main()
{
	Stu stu[3] = {{"Dragon",21},{"Lin",21},{"Shan",21}};
	Stu *p = &stu[0];
	for(int i = 0;i < 3;i++,p++)
	{
		printf("姓名:%s,年龄:%d\n",p->name,p->age);
	}
}
输出:姓名:Dragon,年龄:21
	 姓名:Lin,年龄:21
	 姓名:Shan,年龄:21

13.结构体成员指针

当结构体中,有一个成员是指针时,指向的就是该成员所占内存段的起始地址,如果2个结构体成员指针共同指向一个地方的,当其中一个结构体指针内容跟改变,另一个也会相应改变,如

#include 
#include 

typedef struct Student
{
	char name[20];
	int* age;
}Stu;

int main()
{
	Stu stu_a;
	Stu stu_b;
	int stu_age = 20;
	strcpy(stu_a.name,"Dragon");
	stu_a.age = &stu_age; 
	printf("姓名:%s,年龄:%d\n",stu_a.name,*stu_a.age);
	stu_b = stu_a;
	*stu_b.age = 25;
	printf("姓名:%s,年龄:%d\n",stu_a.name,*stu_a.age);
}
输出结果:姓名:Dragon,年龄:20
		 姓名:Dragon,年龄:25
原因:因为有赋值语句在,所以stu_a里面的成员一个个拷贝到stu_b中,所以这两个结构体里的int* age指向了同一块地方,所以当对其中任
何一个结构体修改年龄时,另一个的年龄也会相应改变

14.指针与堆内存配合

指针与堆内存的关系还是很密切的,因为C语言中没有提供管理内存的语句,只能依靠标准库中提供的函数来对堆进行申请和释放,而且堆内存也无法对标识符建立联系,所以想要使用堆内存,还得靠使用指针,只能靠指针来配合使用堆内存。
那么要如何使用呢,首先我们要先向堆内存申请一块空间
语法:
类型* p = NULL;
p = malloc(你想申请的大小);
接下来如果申请成功,指针变量p就指向你想使用的那块堆内存,你就可以根据p来对来进行操作。
需要注意的是,如果对堆内存使用不当,会产生下列2个问题:
A.内存泄漏:
在程序运行期间由于管理失误导致内存无法被释放,这种情况叫作内存泄漏(当一个程序结束后,属于它的所有资源都会释放,包括泄漏的内存,但有些程序不能停止 7 * 24(一天执行24小时,一周执行7天))。
如休防止内存泄漏:
1、指针不轻易改变指向的位置(类型 * const p)。
2、写完申请内存的语句,要立即写释放内存的语句(谁申请,谁释放)。
3、要保证内存释放语句初始执行。
如果出现内存泄漏怎么办:
GDB调试、打断点,监控程序的行为。

B.内存碎片
内存碎片:已经被释放了,但无法继续使用的内存叫内存碎片(在程序运行过程中由于频繁的申请、释放小块的内存,导致一些内存虽然被释放但不能形成连续的大块内存而导致无法使用)。
内存碎片归根结底是由于释放时间与申请时间不协调造成的,因此不可能完全消除(只能尽量减少),有时候也会自动修复。
1、不要频繁的申请和释放内存。
2、尽量申请大块的连接的内存。
3、优先选择栈内存存储数据。

15.总结

虽然有很多种指针的类型,但是归根结底,他们的用法都是相同的,无非是对地址内的变量进行操作,只要知道这一点,任它千变万化,我自岿然不动。

你可能感兴趣的:(C,指针)