前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C语言进阶(十二) - 动态内存管理

C语言进阶(十二) - 动态内存管理

作者头像
怠惰的未禾
发布2023-04-27 21:21:19
4230
发布2023-04-27 21:21:19
举报
文章被收录于专栏:Linux之越战越勇Linux之越战越勇

前言

C语言中最重要的知识点就是指针动态内存管理,这是检验C语言学习好坏的重要标准。


1. 动态内存分配出现的原因

我们首先接触到的向操作系统申请空间的方法往往是创建一个变量、数组的形式,这样申请的是固定的内存大小,往往不能够很好地满足需要。比如申请小了不够使用,申请大了存在浪费。动态内存很好地解决了这样的问题,我们可以先申请一块空间,小了就在申请大一点的空间,大了也可以申请小一点的空间。

代码语言:javascript
复制
int a = 10;//在栈空间上开辟四个字节的空间
char ch[20] = "Hello world!";//在栈空间开辟20个字节的空间

普通方式申请内存的特点:

  1. 申请内存的大小是固定的。
  2. 数组在定义时必须指明数组的长度(C99之前不支持变长数组),它所需要的内存在编译时分配。

变长数组(C99标准才支持)

是指用整型变量或表达式声明或定义的数组,数组的长度并不会随时变化。 变长数组的长度确定之后在生命周期内的长度是固定的。 变长数组定义时不能初始化。 变长数组必须在程序块的范围内定义,不能在文件范围内定义变长数组。 变长数组不能用staticextern修饰。 变长数组不能作为结构体或联合的成员,只能以独立的数组的形式存在。 变长数组的作用域是块的范围,生命周期也是块的范围。

例子:

代码语言:javascript
复制
#include <stdio.h>

int main(){
    int n = 0;
    scanf("%d", &n);
    int arr[n];
    int i = 0;
    for(i=0; i<n; i++){
        arr[i] = i;
    }
    
    for(i=0; i<n; i++){
       printf("%d ", arr[i]);
    }
    return 0;
}

2. 动态内存函数

普通开辟的变量的内存空间属于栈区。 动态开辟的内存空间属于内存的堆区。

image.png
image.png

2.1 malloc()和free()

malloc()函数声明

void* malloc(size_t size); 头文件<stdlib.h>

功能:向内存申请一块连续可用的空间,并返回指向这块空间指针。 如果开辟成功,返回指向这块空间的指针; 如果开辟失败,返回空指针NULL),应该检查malloc的返回值以防止返回的空指针; 函数参数是size,要申请的字节个数,类型时size_t,无符号整型。 返回值类型是无类型的指针void*,使用返回值时需要强制类型转换为需要的类型的指针。 size的值如果传入了0,**结果是返回空指针还是其他的值是不确定的,**C语言标准并未定义,取决于具体的编译器。

malloc()申请的空间不会自动初始化,是随机值,需要手动初始化。

free()函数声明

void free(void* ptr); 头文件<stdlib.h>

功能:用来释放动态开辟malloc()、calloc()、realloc()的内存块。 如果参数ptr指向的空间不是动态开辟的,则free()行为是未定义的。 如果参数ptr空指针NULL,则free()不执行任何操作。 一般释放完指针ptr指向的动态开辟的内存块后需要把指针ptr置为空指针。不然ptr就成了野指针,非常危险。

例子

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(){
    int n = 0;
    scanf("%d", &n);
    //申请4*n个字节的空间
    int* p = (int*)malloc(sizeof(int) * n);
    //检查p是否是空指针,如果是输出错误信息并返回
    if(p == NULL){
        printf("%s\n", strerror(errno));
        return 1;
    }
    int i = 0;
    for(i=0; i<n; i++){
        *(p+i) = i;
    }
    for(i=0; i<n; i++){
        printf("%d ", *(p+i));
    }
    //释放整型指针p指向的动态内存开辟(申请)的空间
    free(p);
    //指针p指向置为空(NULL)
    p = NULL;
    return 0;
}

2.2 calloc()

函数声明

void* calloc(size_t num, size_t size); 头文件<stdlib.h>

功能:为num个大小为size的元素分配一块空间,并把这块空间的每一位bit都初始化为0。 如果size0,那么返回值是未定义的(不一定是空指针),取决于具体的编译器。 与malloc()分配结果的区别仅仅在于calloc()会对分配空间的每一个bit初始化为0。

例子

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main() {
	int n = 10;
	int* p1 = (int*)malloc(sizeof(int) * n);
	if (p1 == NULL) {
		printf("%s\n", strerror(errno));
		return 1;
	}
	int* p2 = (int*)calloc(n, sizeof(int));
	if (p2 == NULL) {
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < n; i++) {
		printf("%d ", *(p1 + i));
	}
	printf("\n");
	for (i = 0; i < n; i++) {
		printf("%d ", *(p2 + i));
	}
	return 0;
}

运行结果:

image.png
image.png

2.3 realloc()

函数声明

void* realloc(void* ptr, size_t size); 头文件<stdlib.h>

功能:指针ptr不为空指针时,重新分配内存块。 有两种情况:

  1. 将要重新申请的空间大于原来的空间且原来空间的后面有足够大的空间将要重新申请的空间小于原来的空间

realloc()函数直接在原来空间的后面再申请追加新的空间原来空间的数据不发生变化函数返回值是旧空间得起始地址,是指针ptr的值。

  1. 将要重新申请的空间大于原来的空间原来空间后面没有足够大的空间

realloc()函数在堆区的合适的地方申请足够大的连续空间作为新的空间函数返回值是一个新空间的起 始地址,与ptr的值不同

新分配的部分的值时不确定的,即是随机值ptr是要调整的内存地址。 size时调整之后新的大小。 返回值是调整之后的内存块的起始位置或空指针。 realloc()函数在调整原来内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。 当传入的**ptr**是空指针时,realloc函数相当于malloc函数的功能。

.png
.png

F}6KELZ8E%BZQI9$%%X(~~W.png
F}6KELZ8E%BZQI9$%%X(~~W.png

例子

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main() {
	int n = 0;
	scanf("%d", &n);
	int* ptr = (int*)calloc(n, sizeof(int));
	if (ptr == NULL) {
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < n; i++) {
		printf("%d ", *(ptr + i));
	}
	printf("\n");
	//重新分配内存块大小
	scanf("%d", &n);
	int* p = (int*)realloc(ptr, sizeof(int) * n);
	if (p != NULL) {
		ptr = p;
	}

	for (i = 0; i < n; i++) {
		printf("%d ", *(p + i));
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

动态内存申请空间会有时间上的开销,多次动态申请空间将会花费时间,导致程序效率降低,并且产生较多的内存碎片。采用内存池一次申请足够的内存空间,程序自己管理内存池,不在多次向操作系统申请空间,很好的解决了前面的问题。


3. 常见的动态内存错误

3.1 对空(NULL)指针的解引用操作

动态开辟内存之后不对其返回的指针进行检查就直接使用,可能会出现内存开辟失败的情况,此时返回的是空指针。

错误举例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int main(){
    int* p = (int*)malloc(sizeof(int) * 10);
    *p = 10;
    free(p);
    
    return 0;
}

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

一次动态开辟的空间大小是确定的,对动态开辟的空间进行访问操作时不注意对边界的控制,可能会导致越界访问,成为野指针,导致程序出错。

错误举例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int main() {
    int* p = (int*)malloc(INT_MAX);
    //检查p是否是空指针
    if (p == NULL) {
        printf("%s\n", strerror(errno));
        return 1;
    }
    int i = 0;
    for (i = 0; i <= 10; i++) {
        //i等于10的时候越界访问
        *(p + i) = i + 1;
    }
    //释放指针p所指向的空间
    free(p);
    p = NULL;
    return 0;
}

3.3 使用free()释放非动态开辟的内存空间

free()只能释放动态开辟的内存空间。动态开辟的内存空间属于堆区,而非动态开辟的空间在栈区、静态区等内存区域。

错误举例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int main(){
    int a= 10;
    int* p = &a;
    //释放局部变量a所在的栈区空间,出错
    free(p);
    p = NULL:
    return 0;
}

3.4 使用free()释放一块动态开辟内存空间的一部分

在使用malloc()、calloc()、realloc()函数成功在堆区申请一块内存空间后会返回这一块内存空间的起始地址。我们一般用某种类型的指针ptr来接收这个地址,这都是正常操作。 但在接下来对这块空间的使用中,可能会使指针ptr指向这块内存空间的其它非起始地址处,并且使用者没有注意到这一点就直接对ptr指向的动态开辟的内存空间的一部分进行了释放,导致出错。

错误举例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(){
    int* p = (int*)malloc(sizeof(int) * 10);
    //检查p是否是空指针
    if(p == NULL){
        printf("%s\n", strerror(errno));
    }
    int i = 0;
    for(i=0; i<10; i++){
        *p = i;
        p++;
    }
    //释放指针p所指向的空间
    free(p);
    p = NULL;
    return 0;
}

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

指针ptr指向的malloc()或calloc()或realloc()申请的内存空间在使用完后多次通过指针ptr释放这块内存空间也会导致程序出错。 第一次使用free()释放指针ptr指向的内存空间是符合要求的正常操作;但第一次free()并没有及时把ptr置为NULL此时ptr是指向了已经被释放的内存空间,这块内存空间已经不属于本程序了。ptr此时是野指针,再次对ptr进行free()属于非法访问内存,导致出错。

错误举例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(){
    int* p = (int*)malloc(sizeof(int) * 10);
    if(p == NULL){
        printf("%s\n", strerror(errno));
        return 1;
    }
    //多次释放p所指向的内存空间
    //第一次是正常释放
    free(p);
    //第二次p是野指针,非法访问内存,此处就已经出错,程序停止
    free(p);
    free(p);
    return 0;
}

3.6 动态开辟的内存空间忘记释放(内存泄漏)

内存泄漏常常导致程序运行变慢的罪魁祸首,我们虽然不会有意主动写出产生内存泄漏的代码,但内存泄漏在我们逻辑出现漏洞时还是会悄然出现,导致程序出现随运行时间增加而显现的问题。

动态开辟的内存空间没有释放,也没有使用,因为是忘记释放了或者有逻辑问题而没有释放,结果便是这块内存空间虽然还在那里,但是程序本身没有使用(程序已经使用完了),由于这块空间系统已经分配给程序了,所以系统也没有办法使用,相当于这块内存被丢掉了,直到程序停止操作系统自动回收这块内存空间,内存空间才能再次参与使用,才相当于被找到。

错误举例:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void test() {
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL) {
        printf("%s\n", strerror(errno));
        return 0;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i;
    }
    for (i = 0; i < 10 ; i++) {
        printf("%d ", p[i]);
    }
    //没有释放p所指向的动态开辟的内存空间
}
int main() {
    while (1) {
        test();
    }

    return 0;
}

4. C/C++程序的内存开辟

image.png
image.png

C/C++程序内存分配的区域说明

栈区stack: 在执行函数时,函数内局部变量的储存单元都可以在栈上创建,函数执行结束时这些储存单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数返回数据、返回地址等。 栈区是从高地址低地址扩展,是一块连续的内存区域,遵循先进后出后进先出的原则。

堆区heap一般由程序员分配释放,如果程序员不释放,程序结束时可能由OS(操作系统)回收。 分配方式类似于链表。是可以不连续的。 堆区是由低地址高地址扩展,与栈区相反,遵循先进先出、后进后出的原则。 开辟出空间的首地址在栈区。

静态区**static**存放全局变量、静态数据。程序结束后空间由系统释放。 程序执行期间(生命周期)一直存在。 static修饰的局部变量生命周期不再是当前的代码块,而是整个程序运行期间;但是作用域还是当前代码块。

常量区: 存放常量,整个程序执行期间都存在,且不能被改变。 系统管理空间。

代码区: 存放函数体(类成员函数和全局函数)的二进制代码。


5. 柔性数组(flexible array)

柔性数组使用的情况较少,但也有着使用途径。 C99中说道:一个结构体中的最后一个元素允许是未知大小的数组,这样的数组叫做柔性数组成员。

5.1 柔性数组的声明

代码语言:javascript
复制
struct student{
    char name[20];
    int arr[0];
};

或者:

代码语言:javascript
复制
struct student{
    char name[20];
    int arr[];
};

  • 结构中的柔性数组成员的前面必须至少有一个其他成员。
  • 一个结构体中的柔性数组成员只能有一个
  • sizeof()返回的这种结构的大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构体用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。

观察结构体的大小:

代码语言:javascript
复制
#include <stdio.h>

struct student {
    char name[20];
    int arr[];
};

int main(){
    int ret = sizeof(struct student);
    printf("%d\n", ret);

    return 0;
}

运行结果:

image.png
image.png

5.2 柔性数组的使用

例子:

含柔性数组成员的结构体使用malloc()进行动态内存的分配,分配的大小包含结构体本身的大小 + 柔性数组的大小。

动态开辟内存示意图:

image.png
image.png
代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

struct S{
    int i;
    int arr[];
};

int main(){
    //含柔性数组成员的结构体使用malloc()进行动态内存的分配
    //分配的大小包含结构体本身的大小 + 柔性数组的大小
    struct S* p = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
    //检查p是否是空指针
    if(p == NULL){
        printf("%s\n", strerror(errno));
        return 1;
    }
    //使用柔性数组
    int i = 0;
    for(i=0; i<10; i++){
         p->arr[i] = i;   
    }
    for(i=0; i<10; i++){
        printf("%d ", p->arr[i]);
    }
    //释放p指向的动态开辟的内存空间并将p置为空指针
    free(p);
    p = NULL;
    return 0;
}

运行结果:

image.png
image.png

5.3 柔性数组的优势

小节5.2中的柔性数组完成的任务,我们会想到在结构体中使用一个整型指针也可以完成相同的任务。事实确实如此,让我们来试一下,并尝试找出二者的不同之处。 动态开辟内存示意图:

image.png
image.png
代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

struct S{
    int i;
    int *arr;
};

int main(){
    //先动态内存分配一个结构体大小的空间
    struct S* p = (struct S*)malloc(sizeof(struct S));
    //检查p是否是空指针
    if(p == NULL){
        printf("%s\n", strerror(errno));
        return 1;
    }
    //再动态分配结构体中整型指针指向的内存空间
    p->arr = (int*)malloc(sizeof(int) * 10);
    //检查p是否是空指针
    if(p->arr == NULL){
        printf("%s\n", strerror(errno));
        return 1;
    }                                
    //使用
    int i = 0;
    for(i=0; i<10; i++){
         *(p->arr + i) = i;   
    }
    for(i=0; i<10; i++){
        printf("%d ", p->arr[i]);
    }
    //释放p指向的动态开辟的内存空间并将p置为空指针
    free(p->arr);
    p->arr = NULL;
    free(p);
    p = NULL;
    return 0;
}

小节5.2中结构体与柔性数组的动态开辟均是在堆区进行的,要想达到相同的效果,需要在堆上动态开辟两次内存。 第一次动态开辟的是一个结构体的大小,包含了一个指针成员。 第二次动态开辟的是指针成员指向的内存。

既然在堆上动态开辟了两次内存,在结束使用时就要释放两次动态开辟的内存。并且两次动态开辟有一定的包含关系,所以要注意释放的先后顺序,先开辟的后释放,后开辟的先释放。 因为如果先释放第一次开辟的内存,那么指针成员所在的内存就被回收了。这时指针储存的内容是什么就不好说了,不一定还是第二次动态开辟的空间的起始地址了。此时释放指针成员指向的内存时就是非法访问。

运行结果:

image.png
image.png

柔性数组优势

  1. 方便内存释放

柔性数组动态开辟内存时只需要开辟一次,也只需要释放一次。很是方便。

  1. 可以提高访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。


结语

本文主要介绍了动态内存管理中的函数malloc()、calloc()、realloc()和使用方法;接着介绍了在动态内存开辟和使用中可能会出现的问题;最后介绍了柔性数组的概念,虽然它并不常使用,但还是有相应的使用空间的。


END

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1. 动态内存分配出现的原因
    • 变长数组(C99标准才支持)
    • 2. 动态内存函数
      • 2.1 malloc()和free()
        • malloc()函数声明
        • free()函数声明
        • 例子
      • 2.2 calloc()
        • 函数声明
        • 例子
      • 2.3 realloc()
        • 函数声明
        • 例子
    • 3. 常见的动态内存错误
      • 3.1 对空(NULL)指针的解引用操作
        • 3.2 对动态开辟的空间越界访问
          • 3.3 使用free()释放非动态开辟的内存空间
            • 3.4 使用free()释放一块动态开辟内存空间的一部分
              • 3.5 对同一块动态内存多次释放
                • 3.6 动态开辟的内存空间忘记释放(内存泄漏)
            • 4. C/C++程序的内存开辟
              • C/C++程序内存分配的区域说明
              • 5. 柔性数组(flexible array)
                • 5.1 柔性数组的声明
                  • 5.2 柔性数组的使用
                    • 5.3 柔性数组的优势
                      • 柔性数组优势
                  • 结语
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档