当前位置:   article > 正文

C 语言基础—动态内存_c语言动态内存

c语言动态内存


1、为什么要用动态内存

如果需要定义一个比较大的数组时,如下代码:

// 定义一个1M(兆)大小的内存
int main()
{
	char arr[1024*1024];
	printf("定义好了!\n");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这个程序存在什么问题呢?它在运行时会直接崩溃掉,为什么呢?

局部变量分配在栈中,该区域在 windows 平台默认为 1M

虽然 C99 支持利用变量作为长度定义数组,但是 vs 系列的编译器都不支持,而又必须通过变量来定义数组长度该如何处理呢?

这两种情况都需要用到【动态内存】,因为动态内存分配在堆中,该区域非常大,在 windows 平台默认大于 1.2G

  • 1.需要通过变量定义数组长度
  • 2.需要定义大容量的数组

例如,通过帅选法求 n 以内的素数

筛选法具体做法是:先把 N 个自然数按次序排列起来。1 不是素数,也不是合数,要划去。第二个数 2 是质数要保留下来,而把 2 后面的所有能被 2 整除的数都划去。2 后面第一个没被划去的数是 3,把 3 保留下来,再把 3 后面所有能被 3 整除的数都划去。3 后面第一个没被划去的数是 5,把 5 保留下来,再把 5 后面的所有能被 5 整除的数都划去。这样一直做下去,就会把不超过 N 的全部合数都筛掉,保留下来的就是不超过 N 的全部质数

这里比较麻烦的事是如何把不是素数的数字划去呢?一个好办法就是给每个数字一个标记,如果要把它划去就把它标记一改就好了。那么 n 个数就需要 n 个标记,当然需要定义 n 个长度的数组

代码如下:

// 利用筛选法求素数
void SiftPrime(int n)
{
	int* pArr = (int*)malloc(n * sizeof(n));   // 动态申请 n 个整型空间
	assert(pArr != NULL);
	for (int i = 0; i < n; ++i) {   // 假定都是素数
		pArr[i] = 1;
	} 
	pArr[1] = pArr[0] = 0;   // 0 和 1 不是素数
	for (int i = 2; i <= sqrt(1.0 * n); ++i)
	{
		for (int j = i + 1; j < n; ++j)
		{
			if ((pArr[j] != 0) && (j % i == 0)) {   // j 是 i 的倍数
				pArr[j] = 0;
			}
		}
	}
	for (int i = 2; i < n; ++i) {   // 输出素数
		if (pArr[i] != 0) {
			printf("%d  ", i);
		}
	}
	printf("\n");
	free(pArr);   // 释放内存
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

2、创建动态内存

C 语言创建动态内存的有三个函数:malloc、calloc、realloc。都需要引入头文件:stdlib.h

  • malloc:
               函数原型 void* malloc (unsigned int size);
               作用:给函数传递一个非负的整数 size,那么函数在内存的动态存储区(堆)中分配一个长度为 size 字节的连续空间
               返回值:成功时返回动态内存的首地址。失败时返回 NULL。失败只有一种情况就是申请的内容太大,超出堆能提供的最大连续空间
               一种比较保守的编程方式是:每次都验证动态申请内存的返回值是否为空
               void*:不特指某一种类型的指针,只用来保存地址值

例如需要动态申请 n 个字节的程序:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));   // 注意乘以 sizeof(int) 大小
	assert(p != NULL);
	for (int i = 0; i < n; ++i) {   // 模拟数组的使用
		p[i] = 1;
	}
	free(p);   // 使用完,需要释放内存
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

例:复制一个字符串 str:

char* tmp;  strcpy(tmp, str);   // 方法1,错误
char tmp[100];  strcpy(tmp, str);   // 方法2,有可能出错
char* tmp = (char*)malloc((strlen(str) + 1) * sizeof(char));   // 方法3,正确
  • 1
  • 2
  • 3

将字符串 str 复制给 tmp,方法 1 肯定是错误的,因为 tmp 没有指向任何有效的内存,典型的野指针问题。方法 2 有可能出错,是典型的有 bug 的代码,如果 str 长度不超过 100,这句代码是可以的,如果超过 100,则出现数组越界的问题。所以不要使用这种假设和这种代码。方法 3 最好,不仅正确,而且没有浪费一个字节,不管 str 多长,都是刚刚好。动态内存在数据结构的不定长顺序表、链表、队列、树等多种结构中都有大量的使用。

注意: malloc 申请的内存,默认为随机值
            calloc 申请的内存,默认值为 0

  • calloc:
               函数原型 void* calloc(unsigned int num, unsigned int size);
               参数含义:num:申请单元个数;size:每个单元占用的字节数
               作用:在内存的动态存储区(堆)中分配一个长度为 num 乘以 size 字节的连续空间
               返回值:成功时返回动态内存的首地址,失败时返回 NULL,失败只有一种情况就是:申请的内存太大,超出堆能提供的最大连续空间
               calloc 和 malloc 最大的区别就是:calloc 会将每个元素的值置为 0
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <memory.h>
int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));
	assert(p != NULL);
	memset(p, 0, 10*sizeof(int));   // 将每个字节设置为 0
	for (int i = 0; i < n; ++i) {
		printf("%d  ", p[i]);
	}
	printf("\n");
	free(p);   // 使用完,必须释放内存

	// 等同于上面的代码
	int* q = (int*)calloc(n, sizeof(int));
	assert(q != NULL);
	for (int i = 0; i < n; ++i) {
		printf("%d  ", q[i]);
	}
	printf("\n");
	free(q);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

memset 函数:将内存设置为指定的数据。总是用来清除内存数据(置 0)

如果第一次申请了 10 个整形单元,在使用的过程中,发现申请的少了,实际需要 20 个整形单元。这时该如何处理呢?看下面的代码:

int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));
	assert(p != NULL);
	for (int i = 0; i < n; ++i) {
		p[i] = i;   // 模拟 p 被使用
	}
	// 发现 p 的申请的空间太少,于是重新申请
	int* q = (int*)malloc(2 * n * sizeof(int));   // 1. 申请新的内存
	for (int i = 0; i < n; ++i) {
		q[i] = p[i];   // 2. 将原数据移动到 q 中
	}
	free(p);   // 3. 释放原来的 p 的内存
	p = q;   // 4. p 接收新的内存
	q = NULL;
	// ...... 后面可以继续使用 p
	free(p);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上面的1到4个步骤非常的重要,当需要进行扩容时就要如此操作,只是这样是不是太麻烦些呢?有没有更好的办法呢?

  • realloc:
               该函数主要用于扩展内存
               函数原型 void* realloc( void* memblock, unsigned int size );
               参数的含义:memblock,旧内存的地址;size,新申请的内存大小,以字节为单位
               作用:在原有的动态内存的基础上进行扩容
               返回值:成功时返回新的动态内存的首地址。失败时返回NULL,失败只有一种情况就是申请的内存太大,超出堆区能提供的最大连续空间

           接着上一个问题:如果第一次申请了10个整形单元,使用过程中,发现申请的少了,实际需要20个整形单元。利用 realloc 实现的代码如下:

int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));
	assert(p != NULL);
	for (int i = 0; i < n; ++i) {
		// 模拟 p 被使用
		p[i] = i;
	}
	// 发现 p 申请的太少,于是重新申请
	p = (int*)realloc(o, 2 * sizeof(int));
	// ... 后面可以继续使用 p
	free(p);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

3、释放动态内存

           动态内存如果只是申请,使用后不释放,则会出现内存泄漏的问题。即内存被申请出去,但是一直不还,导致能用的内存越来越少。C语言中有两大棘手问题:**1. 数组越界;2. 内存泄漏。**数组越界的问题是越界后把数组周边的数据非法修改了,而这个数据是什么并不统一,那么出现的问题千奇百怪,不容易分析。内存泄漏就是能用的内存它不会让程序直接奔溃,而像温水煮青蛙,程序呈现越来越慢的问题,当你能直观察觉到慢时可能需要几个月,几年的时间(比如你的手机)。程序员找问题都是需要调试,而当程序重新开始调试时意味着程序又重新开始运行了,这时它健步如飞,没有任何问题。内存泄漏这类问题无法通过调试来分析。

           泄漏的内存是否永远都不能再被使用呢?不是,程序泄漏的内存在如下情况是会被还回来的。

  • 1.整个程序(进程)退出,那么这个程序(进程)分配的内存全部会被回收回来
  • 2.操作系统关机,那么所有的内存都被回收

           所以平常我们写程序很难察觉内存泄漏这个问题就是因为程序运行的时间不够长,即使有内存泄漏也察觉不到

            free (void* memblock);
           C 语言中用来释放动态申请内存的函数。参数只有一个就是动态申请的内存
           由一个工具可以用来检查内存泄漏——“ visual leak detector (vld)

4、注意事项

           在使用 free 释放内存经常会出现程序奔溃的问题,主要原因如下:

  • 1.数组越界,代码如下:
// 数组越界导致程序奔溃
int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));
	assert(p != NULL);
	for (int i = 0; i < n; ++i) {
		p[i] = 0;
	}
	free(p);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

           数组越界还有一种常见的情况就是:申请内存时忘记乘以 sizeof(数据类型),导致申请的空间不足

  • 2.移动指针首地址。代码如下:
// 移动指针导致程序奔溃
int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));
	assert(p != NULL);
	for (int i = 0; i < n; ++i) {
		*p = 0;
		p++;   // 移动指针
	}
	free(p);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 3.重复释放同一段内存。代码如下:
// 重复释放同一段内存导致程序奔溃
int main()
{
	int n = 10;
	int* p = (int*)malloc(n * sizeof(int));
	assert(p != NULL);
	int* q = p;
	free(p);
	free(p);   // 重复释放同一段内存
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

真题演练

           1、求两个字符串的最长的连续公共子串,如 “abcdefg” 和 “adefgwgwweg” 的最长公共子串为 “defg” (子串必须是连续的)

string LCS(string str1, string str2) {
	int len1 = str1.size();
	int len2 = str2.size();
	vector<vector<int> >dp(len1 + 1, vector<int>(len2 + 1));
	int index = 0, maxlen = 0;
	for (int i = 1; i <= len1; i++)
	{
		for (int j = 1; j <= len2; j++)
		{
			if (str1[i - 1] == str2[j - 1])
			{
				dp[i][j] = dp[i - 1][j - 1] + 1;   // 状态转移方程
				if (dp[i][j] > maxlen)
				{
					maxlen = dp[i][j];
					index = i - 1;
				}
			}
		}
	}
	return str1.substr(index - maxlen + 1, maxlen);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

           2、有 n 个人围城一圈,顺序排号。从第 1 个人开始报数,从 1 到 3 报数,凡是报到 3 的人退出圈子,问最后留下的是原来的几号?

           3、输入一行字符串,里面有数字和非数字字符,例如:“as123adf4567erer88,90”,将其中连续的数字作为一个整数,依次存放在一个数组中,例如:123存放在arr[0],4567存放在arr[1],…。统计共有多少个整数,并返回整个数组

           3、给定一行字符串,获取其中最长单词


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/592181
推荐阅读
相关标签
  

闽ICP备14008679号