C——指针学习总结(下)


前言

  通过C指针学习总结(上)的介绍,我们初步了解了不同数据类型在内存中的存储方式,指针的两种类型,对指针的基本操作,使用指针形参,指针与二维数组等用法,在本篇总结中将着重介绍指针与动态内存,返回指针的函数、函数指针。以下是笔者根据《C Primer Plus》和b站视频整理的学习笔记,希望能对大家学习指针有所帮助。由于水平有限,如在文中出现错误,感谢大家及时指正。


提示:以下是本篇文章正文内容,下面案例在VS中均能运行,如果读者使用的是DEVC++,有些细节需要改动

一、指针和动态内存

1.1 内存中的分区(栈VS堆)

  系统将内存分为四个区域,分别是Code、Static/Global、Stack、Heap,它们用于存储不同的内容
(1)Code:存放需要执行的命令
(2)Static/Global:存放静态变量或全局变量,即不在函数中声明的变量
(3)Stack(栈):存放函数调用的所有信息和局部变量
(4)Heap(堆):在了解程序运行时上述三种内存划分是如何被使用后再进行对比讲解
//这里的栈和堆与数据结构中的栈和堆没有关系
在这里插入图片描述
下面让我们来通过一段代码并结合进程示意图来形象的演示这个过程

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int result;
int square(int x) {
	return x*x;
}
int SumOfSquare(int x, int y) {
	int sum = square(x + y);
	return sum;
}
int main(void) {
	int num1 = 2;
	int num2 = 3;
    result = SumOfSquare(num1, num2);
	printf("%d", result);
	return 0}

在这里插入图片描述

  注意:在程序运行开始,系统会为栈划定一个固定大小的内存空间,所以当执行一个不能跳出的错误递归函数,或划定的初始内存很小且运行的程序有很多层嵌套函数时,栈的空间会被填满,导致栈溢出,程序崩溃。
  为了解决栈的在程序开始运行时内存大小就被划定的问题,我们使用可以动态改变内存大小的堆来解决这个问题。(使用讲解在1.3)

1.2 void类型指针

  在《C——指针学习总结(上)》中介绍了指针的数据类型,在声明指针变量时就要确定指针的数据类型,在这里我们介绍void型指针。数据类型为void*的指针是一种通用指针类型的指针,即在语句
void *ptr中并没有确定指针的数据类型,所以void类型的指针可以指向任何的数据类型

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main(void) {
	int  a=1024;
	char* ptr;
	ptr = &a;//指针的数据类型与指针指向的数据类型不匹配,
	         //在内存读取的字节数不正确,不能正确解引用

	void* p;
	p = &a;//使用void*类型的指针,可以正确的取到地址和正确的长度

	return 0;
}

  注意: void*类型的指针只能得到它所指向的地址,不能对指针进行算数运算,也不能直接用*解引用。 如果我们调用的函数返回的是一个void*类型的指针,而我们又需要对指针进行运算操作或解引用,那么我们需要对void*类型的指针进行数据类型的强制转换

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
void* demo() {
	void* ptr;
	return ptr;
}
int main(void) {
	int  a = 1024;
	void* p=&a;
	//int temp = *p;  
	//对void*类型的指针不能直接解引用
	int* ptr = (int*)p;
	printf("%d", *ptr);//结果为1024
	
	int* pointer = (int*)demo;//需要对void*类型的指针进行强制转换,
	                          // 变为对应的数据类型
	return 0;
}

1.3动态内存的函数调用

1.3.1 堆的使用方法

  通过调用即将介绍的几个函数在堆上申请空间,并返回一个void*类型的指针,而这个指针就是指向堆中内存块的首地址

int main(void) {
	int a;//在栈中
	int* p;
	p = (int*)malloc(sizeof(int));
	*p = 10;
	//free(p);
	p = (int*)malloc(sizeof(int));
	*p = 20;
	return 0;
}

在这里插入图片描述
在这里插入图片描述  通过代码和图示可以看出首先在堆上开辟了一个int的空间,并对其赋值,随后我们重新划分了一块内存给指针p,这时p指向的地址变为0x12341234,而刚才用于存放整数10的内存仍然占据空间,我们需要手动的释放掉它,即添加上被注释掉的free(p)

1.3.2malloc

  常用的申请堆空间的函数有三个,分别是malloc,calloc,recalloc,不管使用哪一种都应该记住当空间使用完毕后,用free函数将其释放(在1.4中说明free()的用法)

首先我们来看malloc函数在源代码中的定义 void *malloc (unsigned int size)
可以发现它返回的数据类型是一个void*的指针,所以在使用时注意要进行强制类型转换,其次它的形参是一个待确定的size长度,即被划分的内存空间,如需要给5个整数划分内存,那么传入的形参为5*sizeof(int),注意不要人为的将int类型的长度写为4字节,因为在不同的电脑中int的长度可能不同(4字节或8字节)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int size = 10;
int main(void) {
	int* ptr = (int*)malloc(size *sizeof(int));
	for (int i = 0; i < 5; i++) {//只对一部分数据进行初始化
		*(ptr+i) = i + 1;
	}
	for (int i = 0; i < size; i++) {
		printf("第%d个元素是%d\n", i,ptr[i]);
	}
	return 0;
}

在这里插入图片描述
  可以看出在使用malloc函数调用堆内存时,不会对调用的整块内存进行初始化,我们没有人为进行初始化的部分中存储的是没有意义的随机数值(脏数据)

1.3.3 calloc

  calloc与malloc类似,返回值也是一个void*类型的指针,但是形参变为两部分,即指定数据类型的个数,和指定数据类型的长度,例如
int* ptr = (int*)calloc(5 , sizeof(int));

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int size = 10;
int main(void) {
	int* ptr = (int*)calloc(size , sizeof(int));
	for (int i = 0; i < 5; i++) {
		*(ptr+i) = i + 1;
	}
	for (int i = 0; i < size; i++) {
		printf("第%d个元素是%d\n", i,ptr[i]);
	}
	return 0;
}

在这里插入图片描述  通过结果我们可以看出,calloc函数初始化数据是会将空间初始为默认值,假如是int型的数据,就初始化为0,假如是指针就初始化为NULL
这也是malloc和calloc的一个主要区别。

1.3.4 realloc

  realloc函数用于对已有的堆上空间大小进行更新,函数形参分为两部分,第一部分为已有的void*类型指针,第二部分为新申请内存空间的大小(与malloc相同)
void *realloc(void *ptr, unsigned int size)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int size = 5;
int main(void) {
	int* ptr = (int*)calloc(size ,sizeof(int));
	for (int i = 0; i < 4; i++) {
		*(ptr+i) = i ;
	}
	for (int i = 0; i < size; i++) {
		printf("第%d个元素是%d\n", i,ptr[i]);
	}
	printf("调用recalloc函数\n");
	int* p = (int*)realloc(ptr, 2 * size * sizeof(int));
	for (int i = 0; i < 2*size; i++) {
		printf("第%d个元素是%d\n", i, p[i]);
	}
	return 0;
}

在这里插入图片描述
  通过观察结果我们发现对一块没有用完的原堆上内存调用realloc函数,实际是先对原来的值进行拷贝,并且与malloc相同realloc也不会进行初始化。例如我们先使用calloc申请了一块存放5个整数的空间,使用了4块空间后调用realloc函数,通过结果可以看出,calloc初始化过而未使用的空间在新的内存块中仍然是被初始化过的。

1.4内存泄漏与free()

  通过上文讲解我们知道每调用一次malloc函数,都会在堆上划分一块内存,而当我们使用结束后系统并不会自动释放这一块内存空间,这样就会造成内存空间的浪费,称为内存泄漏。下面这个例子就严重的浪费了内存空间

int size = 10;
void test() {
	int* p = (int*)malloc(10 * sizeof(int));
	for (int i = 0; i < size; i++) {
		*(p + i) = 1;
	}
}
int main(void) {
	int i = 0;
	while (i++ < 10) {
		test();
	}
	return 0;
}

  为了避免造成内存泄漏,每当使用完一块空间后一定要free释放掉这块内存
free()的使用方法:
free()函数的形参为一段内存空间的首地址free(void* ptr);,例如

int main(void) {
	int* ptr = (int*)malloc(size *sizeof(int));
	for (int i = 0; i < 5; i++) {//只对一部分数据进行初始化
		*(ptr+i) = i + 1;
	}
	free(ptr);
	return 0;
}

二、函数返回指针

  构造函数时传入的形参既可以是整数,又可以指数的指针,同样函数的返回值既可以是一个普通的变量,也可以是一个指针。下面我们通过代码实例来解释函数返回一个普通的变量和返回一个指针的区别。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int Add(int* a, int* b) {
	int c = (*a) + (*b);
	return c;
}
int* AddFalse(int* a, int* b) {//错误示范
	int c = (*a) + (*b);
	return &c;
}
int* AddTrue(int* a, int* b) {//正确示范
	int* c = (int*)malloc(sizeof(int));
	*c = (*a) + (*b);
	return c;
}
int main(void) {
	int x = 2, y = 5;
	int result1= Add(&x, &y);
	int result2 = *(AddFalse(&x, &y));
	int result3 = *(AddTrue(&x, &y));
	printf("返回整数的函数结果为%d\n", result1);
	printf("返回指针的函数错误结果为%d\n", result2);
	printf("返回指针的函数正确结果为%d\n", result3);
	return 0;
}

在这里插入图片描述
  代码实例中一共定义了三种加法的函数,第一种int Add(int *a, int *b)传入的形参为两个指针,函数返回值为整数;函数int* AddFalse(int* a, int* b)int* AddTrue(int* a, int* b)传入的形参和函数返回值都是指针,从程序运行的结果发现这三个函数的运行结果都是正确的。但是从函数定义的名字可以看出int* AddFalse(int* a, int* b)是一种错误的函数定义方式,那么为什么它能返回正确的结果呢,下面我们还是通过程序运行时在栈和堆中的存储形式来对比两个返回指针的函数,并找到逻辑错误。
在这里插入图片描述

  错误示例:我们可以看出当main()主函数调用AddFalse()时,c被压入栈中,在该函数中有a、b两个指针变量和整数变量c,函数运行结束返回的c的地址,这时AddFalse()出栈,内存被释放,该内存位置可以被划给其它函数。而这时result2中存储的还是在执行AddFalse()时划分给变量c的地址,所以实际上它现在存储的是一个没有意义的地址。那么为什么我们还能得到正确的结果呢,实际上这是偶然事件,因为存储c的内存被释放,但并没有被初始化,当该位置再次被划给其他函数并在上面写入新的值时才会被修改。而该测试代码简单,在AddFalse()后运行的函数没有在该位置上进行修改,所以偶然的获得了正确答案。为了正确返回指针,我们需要使用堆。
在这里插入图片描述  正确示例:因为在堆中申请的变量不会别系统自动释放,需要我们显式的free释放,所以AddTrue()执行结束出栈后,存储变量c的内存并没有被释放,我们得到的还是一个有意义的地址。所以将返回的变量声明在堆中可以解决该问题。

三、函数指针

  到目前位置,指针都用于指向一个数据的地址,数据可能是一个常量也可能是一个变量,事实上指针还可以指向函数,即用指针来调用函数,为更好的理解函数指针,我们还是先通过内存中的分块图来简述函数在内存中的存放。
  我们使用C或其他高级语言编写程序,编写出的程序以.c文件存储在电脑中,但计算机并不能直接运行.c文件,而是需要先将它编译成计算机能执行的0和1机器编码,并以.exe文件存储在电脑中。当程序运行时,编好码的指令会被读入到内存中的Code,机器会通过解读指令的内容来执行声明变量或者调用函数等操作(Code中的指令指挥计算机在内存上其他三块区域进行操作)。而指针调用函数就是在指针变量里面存入可以回调函数指令的首地址。
在这里插入图片描述声明一个函数指针与定义一个函数格式相似,注意区别

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int Add(int a, int b) {
	return a + b;
}
int main(void) {
	int c;
	int (*p)(int, int);
	p = &Add;//&取地址符也可以不写
	c = (*p)(2, 3);
	printf("%d", c);
	return 0;
}

  在这段代码中int (*p)(int, int);声明了一个函数指针,p = &Add;来取函数入口的地址,(在这里&取地址符可以不写),在c = (*p)(2, 3);中*表示对p解引用,与int (*p)(int, int);作用不同。
声明函数指针的格式:
  与声明函数对比,int表示函数的返回值类型,p为函数指针变量名,*表示声明的是一个指针变量,(int ,int)表示函数的形参是两个int类型的数据,与定义函数不同,声明函数指针只需要写形参的数据类型而不需要写形参的变量名。使用已经声明的函数指针时与使用函数相似,用一个变量名接受函数返回的值,(int ,int)的位置写入传进函数的形参变量即可。注意:(*p)一定要有括号,否则是定义一个返回指针的函数,而不是声明函数指针。 读者可以根据上述例子进行类比,来申明其他函数指针。


总结

  在本篇总结中介绍了动态内存的堆的调用方法,它可以在一定程度上解决使用栈的弊端,使用时一定要注意用free()释放内存,还介绍了返回的指针的函数和函数指针。
  在后续的文章中将介绍链表(单链表、双向链表、循环链表)、栈、队列、树(二叉搜索树、B+树、红黑树、AVL树)等数据结构的实现和使用。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值