IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C/C++】基础:动态内存管理 -> 正文阅读

[C++知识库]【C/C++】基础:动态内存管理

【C/C++】基础:动态内存管理

摘要:本文将介绍关于动态内存开辟的由来与优势,再介绍动态内存开辟的方法,最后会列举常见错误与相应练习帮助大家巩固。


一.概述

在以往学习的过程中,我们通过声明来开辟内存空间,可是这样开辟空间有两个特点:

  • 空间大小的开辟是固定的;
  • 数组在声明的时候必须是指定的数组长度,它在需要的内存在编译时分配;

可是在现实情况下,会发现在需要开辟内存过程中,有时无法去区分内存开辟的大小,有时内存的大小的差值需要较大,如果只是静态的设定好内存,可能造成内存不足也可能造成内存浪费,非常的不灵活。因此C语言提供了关于动态内存开辟的方法,需要注意的是这些开辟空间的内存在堆区进行动态内存分配,相应函数分别为malloc、calloc、recalloc等。记住必须要对其进行内存释放,并置空指针。

二. 动态内存函数介绍

2.1 malloc

//函数定义
void* malloc (size_t size);

作用:分配内存块,向内存申请一块连续可用的空间,并返回指向这块空间的指针。

参数:size表示需要开辟的内存块的字节数,记住为参数size的类型为无符号数。

返回值:如果成功开辟内存空间,返回开辟成功后的内存块的首地址,地址类型为空指针;如果开辟失败了,将会返回空指针。

解释:

  • 开辟后的内存时不进行初始化的;
  • size的参数可以为0,但返回类型取决于特定的库函数实现,并且返回后的指针不能被解引用。
  • 需要包括头文件 stdlib.h
  • 由于开辟空间返回类型为void*指针,因此在开辟后需要强制类型转换一下类型。
  • 开辟空间后,需要及时的释放,否则会出现内存泄露等问题。

官方示例:

/* malloc example: random string generator*/
#include <stdio.h>      /* printf, scanf, NULL */
#include <stdlib.h>     /* malloc, free, rand */

int main(){
    int i, n;
    char* buffer;

    printf("How long do you want the string? ");
    scanf("%d", &i);
    //开辟内存空间
    buffer = (char*)malloc(i + 1);
    //报错说明
    if (buffer == NULL) exit(1);
    //输入随机数到申请内存空间中
    for (n = 0; n < i; n++)
        buffer[n] = rand() % 26 + 'a';
    buffer[i] = '\0';
    //输出
    printf("Random string: %s\n", buffer);
    free(buffer);

    return 0;
}

补充说明:以下代码的注释为使用malloc的关键。

#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main(){
	//声明:向内存申请了40个字节,并且就要记住进行强制类型转换,
	int* p = (int*)malloc(10*sizeof(int));
	//注意:通过临时指针,进行操作,便于释放内存时使用ptr指针
	int* ptr = p;
	//注意:当开辟空间失败时,打印错误信息
	if (p == NULL){
		printf("%s\n", strerror(errno));
		return 1;
	}
	//通过使用指针来访问空间案例
	int i = 0;
	for (i = 0; i < 10; i++){
		*ptr = i;
		ptr++;
	}
	//释放并置空
	free(p);
	p = NULL;
	ptr = NULL;
	return 0;
}

2.2 free

//函数定义
void free (void* ptr);

作用:对于动态内存的释放和回收

参数:ptr指向先前用malloc、calloc或realloc分配的内存块的指针

解释:

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
  • 如果参数 ptr 是NULL指针,则函数不行操作
  • 当释放空间时,不得从开辟指针地址后中间开始释放,否则程序会崩溃
  • 当释放空间后,需要将指针赋空,否则会出现野指针的情况
  • 需要包括头文件stdlib.h

官方案例:

/* free example */
#include <stdlib.h>     /* malloc, calloc, realloc, free */

int main ()
{
  int * buffer1, * buffer2, * buffer3;
  buffer1 = (int*) malloc (100*sizeof(int));
  buffer2 = (int*) calloc (100,sizeof(int));
  buffer3 = (int*) realloc (buffer2,500*sizeof(int));
  free (buffer1);
  free (buffer3);
  return 0;
}

2.3 calloc

//函数定义
void* calloc (size_t num, size_t size);

作用:分配内存块并进行初始化

参数:num表示申请类型的数量,size表示要申请的类型

返回值:如果申请空间成功后,将会返回申请空间的内存块的首地址,返回类型为空指针,因此在申请后需要对其进行强制类型转换;如果申请空间失败,将会返回空指针。

解释:

  • 与malloc的区别就是calloc会进行初始化;
  • 在使用calloc时,也要注意判断是否会开辟空间失败;
  • 对于calloc来说,也需要进行free,并置为空指针;
  • 头文件为stdlib.h

官方示例:

#include <stdio.h>      /* printf, scanf, NULL */
#include <stdlib.h>     /* calloc, exit, free */

int main()
{
    int i, n;
    int* pData;
    printf("Amount of numbers to be entered: ");
    scanf("%d", &i);
    pData = (int*)calloc(i, sizeof(int));
    if (pData == NULL) exit(1);
    for (n = 0; n < i; n++)
    {
        printf("Enter number #%d: ", n + 1);
        scanf("%d", &pData[n]);
    }
    printf("You have entered: ");
    for (n = 0; n < i; n++) printf("%d ", pData[n]);
    free(pData);
    return 0;
}

补充说明:以下代码的注释为使用calloc的细节

int main(){
	//空间申请:40个字节 - 10个整型 = malloc(40);
	int* p = (int*)calloc(10, sizeof(int));
	//注意进行申请失败的判断
	if (p == NULL){
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//释放
	free(p);
	p = NULL;

	return 0;
}

2.4 realloc

void* realloc (void* ptr, size_t size);

作用:再次申请内存空间,有时我们进行动态内存申请开辟后,可能在后续操作需要更大的空间,这时我们可以通过realloc来完成对空间的再次开辟

参数:ptr为动态申请空间后的指针,size表示调整后的新大小,其单位为字节

返回值:如果成功申请,返回值为申请内存块的首地址,注意不是新申请后的位置的首地址;如果失败,这会返回一个空指针。

解释:

  • 一般情况下,会定义一个临时指针来接受地址,在赋值给原指针,原因是,如果内存开辟失败,会返回空指针,这样会导致原内存被丧失,无法释放也无法使用。
  • 如果ptr赋予了一个空指针,其作用于malloc相同
  • 由于返回类型为空指针,因此需要对其进行强制类型转换为需要的指针类型

官方案例:

/* realloc example: rememb-o-matic */
#include <stdio.h>      /* printf, scanf, puts */
#include <stdlib.h>     /* realloc, free, exit, NULL */

int main()
{
    int input, n;
    int count = 0;
    int* numbers = NULL;
    int* more_numbers = NULL;

    do {
        printf("Enter an integer value (0 to end): ");
        scanf("%d", &input);
        count++;

        more_numbers = (int*)realloc(numbers, count * sizeof(int));

        if (more_numbers != NULL) {
            numbers = more_numbers;
            numbers[count - 1] = input;
        }
        else {
            free(numbers);
            puts("Error (re)allocating memory");
            exit(1);
        }
    } while (input != 0);

    printf("Numbers entered: ");
    for (n = 0; n < count; n++) printf("%d ", numbers[n]);
    free(numbers);

    return 0;
}

补充说明:以下代码的注释为使用realloc的细节

int main(){
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
	int i = 0;
	for (i = 0; i < 10; i++){
		*(p + i) = i;
	}
	//增加空间
	int* ptr = (int*)realloc(p, 80);
	//当realloc开辟失败的是,返回的是NULL,因此我们不选择直接赋值,而是开辟成功后赋值
	if (ptr != NULL){
		p = ptr;
		ptr = NULL;
	}
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

对于realloc的内存大小开辟也是有注意的地方,因为realloc开辟的也是一段连续的空间,当空间足够时,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。但当空间不足时,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。画图示例如下:

下面对此细节进行我们在通过一个临时的变量保存返回地址时,需要记得把地址赋值到原指针,不然当内存不足以连续追加开辟时,原指针被释放,会出现一些出乎意料的问题。

二. 动态内存开辟常见错误

2.1 对于NULL指针的解引用操作

void test(){
	int* p = (int*)malloc(INT_MAX);
	*p = 20;
	free(p);//如果p的值是NULL,就会有问题
}

查看以上代码,malloc内存时可能会返回空指针,这时候由于空指针是不可以姐应用的,因此会发生错误,造成对NULL指针解引用的误操作,正确修改代码如下:

void test()
{
	int* p = (int*)malloc(INT_MAX);
    //增加对空指针的判断
	if (p == NULL) {
    	perror("malloc");
		return;
	}
	*p = 20;
	free(p);
}

2.2 对动态开辟空间的越界访问

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p){
		exit(EXIT_FAILURE);
	}
	for (i = 0; i <= 10; i++){
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
}

以上代码开辟了10个int类型内存空间的内存大小,在访问时,出现了访问11个int类型大小的内存空间,发生了内存越界访问的错误,这样会使程序发生错误。正确修改如下:

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p){
		exit(EXIT_FAILURE);
	}
	for (i = 0; i < 10; i++){
		*(p + i) = i;
	}
	free(p);
}

2.3 对非动态开辟内存使用free释放

void test(){
	int a = 10;
	int* p = &a;
	free(p);//ok?
}

以上代码对非动态开辟的空间进行了free内存释放,根据对free的标准来看,free是未定义的,因此程序会对此出现错误。

2.4 使用free释放一块动态开辟内存的一部分

void test(){
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

以上代码对p通过自增操作,发生指针的偏移,不再指向动态开辟空间的一部分。当我们对其进行释放时,就是动态开辟内存的一部分进行释放时,会使程序崩溃,出现错误。一般做法是通过一个临时指针来完成操作,而不对原指针直接拿来使用。

2.5 对同一块动态内存多次释放

void test(){
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

以上代码对已经释放的p指针进行了两次释放,这时相当于释放了一个野指针,同2.3出现的问题一致,会导致程序崩溃,而处理这种问题的方法就是养成良好的习惯,便是在每次释放完后置指针为空,这样free对空指针的使用是什么都不做,从而避免问题的出现。

2.6 动态开辟内存忘记释放(内存泄漏)

int* test(){
    int *p = (int*)malloc(40);
    return p;
}
int main(){
    int *ptr = test();
    //……
    return 0;
}

内存泄露为动态内存开辟的非常常见也是比较严重的问题,它是在我们开辟空间后,没有做到及时的释放。如以上代码,在申请内存空间后,并没有对内存进行返回,这样就造成了内存泄露。内存泄露的问题在一个小程序上体现不明显,可是当我们为一个服务器时,长期处于运行状态,若内存一直泄露,将会使得内存一直占用,直到最后发生卡死。因此一定要记住释放内存。正确示例:

int* test(){
    int *p = (int*)malloc(40);
    return p;
}
int main(){
    int *ptr = test();
    //……
    free(ptr);
    ptr = NULL;
    return 0;
}

三. 经典例题

3.1 练习1

void GetMemory(char* p) {
	p = (char*)malloc(100);
}
void Test(void) {
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

请问运行Test函数会有什么样的结果?

解析:在使用GetMemory时,传递的是形式参数,而不是通过指针的方式将地址指向开辟好的空间,因此str指针指的地址仍然是NULL,再对str进行strcpy函数,将会发生断言,程序会出错。而关于在本题的内存泄露问题等就不先讨论,示意图如下:

正确代码书写应该为传递指针的方式,函数参数更改为二级指针,用于接收一级指针的地址,这时为地址传递,而不是拷贝构造,而是对str作用。代码如下:

void GetMemory(char** p) {
	*p = (char*)malloc(100);
}
void Test(void) {
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
    str = NULL;
}

3.2 练习2

char* GetMemory(void) {
	char p[] = "hello world";
	return p;
}
void Test(void) {
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

请问运行Test函数会有什么样的结果?

解析:在函数GetMemory为定义一个p数组,赋值为一个字符串,最后返回给指针str,但是在函数栈帧的创建与销毁中,我们知道,函数会出栈,因此就不会有p数组的内容,因此str接受之后变成了一个野指针,可能会导致程序输出错误。

再看一下相似程序练习:

int Test1() {
	int a = 10;
	return a;
}
int* Test2() {
	int a = 10;
	return &a;
}
int main() {
	int temp1 = Test1();
	int* temp2 = Test2();
	printf("%d\n",temp1);
	printf("%d\n", *temp2);
}

解析:对于temp1,来说输出值为10,因为在函数栈帧出栈时会将a的值存到寄存器中,而temp2解引用后输出的值为随机值,因为函数栈帧出栈后,空间就被释放了,不再使用。

3.3 练习3

void GetMemory(char** p, int num) {
	*p = (char*)malloc(num);
}
void Test(void) {
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

请问运行Test函数会有什么样的结果?

解析:这个函数与练习1的改进相似,通过指针传递地址,完成堆内存开辟的地址传递,从而开辟空间。输出结果为hellow

3.4 练习4

void Test(void) {
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL){
		strcpy(str, "world");
		printf(str);
	}
}
int main() {
	Test();
}

请问运行Test函数会有什么样的结果?

解析:以上代码对开辟空间后的str赋值hellow后,进行空间释放,释放之后对其再次进行strcpy,这时会出现程序错误,因为这时的str为野指针。而要对其进行正确修改则需要养成对空间释放后,对指针赋空。这样就不会进行对野指针的赋值。


补充:

  1. 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:33:07  更:2022-09-30 00:33:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 4:37:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码