深入剖析与实战演练:C/C++中的动态内存管理艺术

目录

为什么要有动态内存管理

malloc和free函数

1.malloc函数——开辟动态内存的函数:

2.free函数

calloc和realloc函数

1.calloc函数

2.realloc函数

常见动态内存错误

 1.对NULL的解引用操作

2.对动态内存的越界访问

3.对非动态内存的空间使用free

4.使用free释放动态内存的一部分

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

5.忘记free释放动态内存

区别

例题

例题1:计算字符串长度

解析:

例题2:动态分配二维数组

解析:

总结


为什么要有动态内存管理

int a = 10;


char arr [20]={0};

在以上代码中,我们一旦创建变量便向内存分别申请了4字节和20字节的空间,这种方式无法在对内存大小进行调整。

如此我们便会遇到一个问题:申请的内存空间不灵活。举个例子:假如我们想要记录一个班上同学的成绩,用以上方式不难想到创建一个数组。

int grade[30] = {0};

假设我们创建一个可存放30人成绩的数组,如果班上有26个人,我们将会浪费16字节的内存空间,假如班上有35人,将没有足够大小存下。

对此,我们想一下,有没有什么解决方案呢?答案是有!如果我们让程序员去根据自己的需要申请内存大小,这个问题将得以解决。在C/C++中,这个方法就是——动态内存分配。在C/C++中,给程序员足够的权限去申请内存大小,这种方式大大增加了灵活性,但同时也会带来风险。

为此,我将介绍四个动态内存相关的函数:malloc,free,calloc,realloc

mallocfree函数

1.malloc函数——开辟动态内存的函数:

void* malloc (size_t size);

 该函数的功能是向内存申请一块空间(单位字节),并返回该空间的地址。

  • 如果开辟成功,将返回该空间地址。
  • 如果开辟失败,将返回NULL(空指针),所以在使用前一定要做好检查。
  • 返回类型为void*,具体类型需要使用者调整。
  • 如果size为0,malloc的行为标准是未定义的,取决于编译器。

如果我们希望开辟一块20字节的空间来存放五个整形

#include <stdlib.h>
#include <stdio.h>

int main()
{
	int* p = (int*)malloc(20);//向内存申请20字节的空间,并创建p来存放地址
	if (p == NULL)
	{
		perror("malloc");
		return 1;//如果开辟失败,将报错并且返回1
	}
	for (int i = 0; i < 5; i++)
		*(p + i) = i;
	return 0;
}

在以上代码,我们向内存申请20个字节用来存放5个int类型的变量,如果申请失败将会报错

我们用内存的角度来看一下代码

上图可以看出我们申请的空间,返回的地址是空间的起始地址。

2.free函数

释放之前调用 calloc、malloc 或 realloc 所分配的内存空间,函数原型如下:

void free(void *ptr);
  • 如果 ptr 不是动态分配的,free 的行为标准是未定义的。
  • 如果 ptr 是NULL,free将没有任何行为。

使用该函数后,将申请的空间重置并把权限还给操作系统。

tips:我们使用free后,最好将ptr赋值为NULL。

我们可以看到使用free函数后,空间被重置为 0xdd 。

callocrealloc函数

1.calloc函数

分配所需的内存空间,并返回一个指向它的指针。malloc 和 calloc 之间的不同点是,malloc 不会设置内存为零,而 calloc 会设置分配的内存为零

原型如下:

void *calloc(size_t nitems, size_t size);
  • nitems个大小为size的元素开辟空间,并把空间每个字节初始化为0。

举个例子:

int main()
{
    int i, k, n;
    int* a;

    printf("要输入的元素个数:");
    scanf("%d", &n);

    a = (int*)calloc(n, sizeof(int));

    printf("开辟空间为初始化前\n");
    for (k = 0; k < n; k++)
    {
        printf("%d ", *(a + k));
    }
    printf("\n");

    printf("输入 %d 个数字:\n", n);
    for (i = 0; i < n; i++)
    {
        scanf("%d", &a[i]);
    }

    printf("输入的数字为:");
    for (i = 0; i < n; i++) 
    {
        printf("%d ", a[i]);
    }
    free(a);  // 释放内存
    return 0;
}

上述代码的运行示例:

要输入的元素个数:5
开辟空间为初始化前
0 0 0 0 0
输入 5 个数字:
1 2 3 4 5
输入的数字为:1 2 3 4 5

2.realloc函数

尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小,原型如下:

void *realloc(void *ptr, size_t size);
  • ptr为需调整空间的首元素地址,size是调整后的大小。
  • 返回值是调整内存后的起始地址。

在扩容过程中,我们会遇到三种情况:

1. 我们需要重新调整的空间未分配。正常在未分配空间申请

 

2. 需分配空间与已分配空间冲突。

        在情况2下:第一步,我们在堆区中找到一块满足需求的空间

                            第二步,将旧空间内容拷贝到新的空间

                            第三步,释放旧空间,返回新空间的起始地址

3.调整失败,返回空指针

下面是使用案例:

int main()
{
	int *p=malloc(20);//向内存申请空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}


	for (int i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}


	int* ptr = realloc(p, 40);//将空间调整为40字节
	if (ptr == NULL)
		perror("realloc");
	else
		p = ptr;
	return 0;
}

tips:在以上代码中,我们创建了一个新的整形指针ptr来存放调整后空间的起始地址,这是因为如果调整失败,原本的空间和数据还可以使用。

常见动态内存错误

 1.对NULL的解引用操作

这就是一种经典的错误,我们使用malloc函数后,并未对整形指针p进行判断,这样会带来风险。

正确的写法是:

int main()
{
	int* p = (int*)malloc(20);
	assert(p);  //需包含头文件assert.h
	*p = 20;
	free(p);
}

2.对动态内存的越界访问

int main()
{
	int* p = malloc(10 * sizeof(int));

	if (p == NULL)
	{
		exit(EXIT_FAILURE);
	}

	for (int i = 0; i <= 10; i++)//越界访问
	{
		*(p + i) = i;
	}

	free(p);
	return 0;
}

上述是错误的代码,我们将for循环中的i<=10改成i<10便解决了这一问题。

3.对非动态内存的空间使用free

int main()
{
	int i = 20;
	int* p = &i;

	free(p);
	p = NULL;

	return 0;
}

变量在栈区申请空间,而我们的free函数只能释放向动态内存申请的空间,所以造成了错误。

4.使用free释放动态内存的一部分

int main()
{
	int* p = malloc(100);
	assert(p);

	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
		p++;
	}//for循环执行完毕后,p向后移动了10次,不再是动态内存申请的起始位置

	free(p);
	p = NULL;

	return 0;
}

for循环执行完毕后,p指向的内存不在是动态内存的空间,会出现以下错误:

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

int main()
{
	int* p = malloc(20);

	free(p);
	free(p);
	return 0;
}

上述代码同样出现以下错误:

5.忘记free释放动态内存

void test()
{
	int* p = malloc(20);
	assert(p);
	*p = 20;
}

int main()
{
	test();
	while (1);
	return 0;
}

在调用test函数后,申请的动态内存没有及时释放,且test执行完毕后,栈帧被销毁,无法再释放动态内存,这种现象叫内存泄漏。其实malloc / realloc / calloc申请动态内存后,可以使用free释放内存,即使没有释放,操作系统也会回收这一部分内存。

区别

  1. 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。
  2. 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零。
  3. 函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void类型.void表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针。
  4. realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失**.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址**.相反,realloc返回的指针很可能指向一个新的地址。
  5. realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动。

例题

例题1:计算字符串长度

题目要求:编写一个C程序,定义一个函数int strLength(char *str),用于计算传入字符串(以空字符'\0'结尾)的长度。在main()函数中,测试该函数。

#include <stdio.h>

// 函数声明
int strLength(char *str);

int main() {
    char input[] = "Hello, World!";
    int length = strLength(input);
    printf("Length of the string \"%s\" is: %d\n", input, length);
    return 0;
}

// 函数定义
int strLength(char *str) {
    int len = 0;
    while (*str != '\0') {
        len++;
        str++;
    }
    return len;
}
解析
  • strLength函数接受一个指向字符串的指针char *str作为参数。
  • 定义一个整型变量len初始化为0,用于记录字符串的长度。
  • 使用while循环遍历字符串,条件为当前字符不等于结束符\0。在循环体内:
    • 增加len的值,表示遇到一个新的字符。
    • 将指针str向前移动一位,使其指向下一个字符。
  • 当遍历完字符串,即遇到\0时,跳出循环。此时len已累计了字符串的长度,将其作为函数返回值。
  • main()函数中,定义了一个字符串input,调用strLength(input)获取其长度,并打印结果。

例题2:动态分配二维数组

题目要求:编写一个C程序,定义一个函数int** createDynamicMatrix(int rows, int cols),该函数创建并返回一个动态分配的rowscols列的整型二维数组。在main()函数中,测试该函数,创建一个3x5的矩阵,为其赋随机值,并在使用后释放内存。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 函数声明
int** createDynamicMatrix(int rows, int cols);

void freeDynamicMatrix(int** matrix, int rows);

int main() {
    int rows = 3, cols = 5;
    int** dynamicMatrix = createDynamicMatrix(rows, cols);

    srand(time(NULL));
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            dynamicMatrix[i][j] = rand() % 100;
            printf("%d ", dynamicMatrix[i][j]);
        }
        printf("\n");
    }

    freeDynamicMatrix(dynamicMatrix, rows);
    return 0;
}

// 函数定义
int** createDynamicMatrix(int rows, int cols) {
    int** matrix = (int**)malloc(rows * sizeof(int*));
    for (int i = 0; i < rows; ++i) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
    }
    return matrix;
}

void freeDynamicMatrix(int** matrix, int rows) {
    for (int i = 0; i < rows; ++i) {
        free(matrix[i]);
    }
    free(matrix);
}
解析
  • createDynamicMatrix函数接受两个整数参数rowscols,分别表示要创建的二维数组的行数和列数。
  • 首先,使用malloc分配一个包含rows个整型指针的数组,并将其地址赋给int** matrix
  • 然后,使用嵌套循环对每一行进行处理,为每一行分配一个包含cols个整型元素的数组,并将新分配内存的地址赋给matrix[i]
  • 函数返回创建好的matrix指针,表示动态分配的二维数组。
  • freeDynamicMatrix函数负责释放之前动态分配的二维数组内存。它接受矩阵指针matrix和行数rows作为参数。
  • 使用外层循环遍历每一行,对每一行使用free释放内存。
  • 最后,释放包含行指针的一维数组matrix本身。

main()函数中,我们创建了一个3x5的动态二维数组,为其赋随机值,并在使用后调用freeDynamicMatrix释放内存。

总结

动态内存管理是C/C++程序员必须掌握的核心技能之一。理解其基本原理,熟练运用相关函数与运算符,遵循最佳实践,能够有效避免内存泄漏、悬挂指针等问题,提升程序的稳定性和资源效率。随着C++11引入智能指针等现代内存管理工具,动态内存管理变得更加安全便捷。善用这些工具,将使您的代码更具可维护性和可靠性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值