【C进阶】——C/C++程序的内存开辟 及 柔性数组详解

C/C++
151
0
0
2024-05-04
这篇文章我们一起来学习一下C/C++程序的内存开辟以及柔性数组!!!

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

C和C++的内存开辟方式是非常类似的,这篇文章我们就来学习一下C/C++程序的内存开辟。

在之前的文章里其实我们简单的介绍过C语言中的内存划分。 大致可以分为:栈区,堆区和静态区:

在这里插入图片描述

那今天,我们来更加细致的细致的讲解一下C/C++程序的内存开辟。 首先,我们来看一张图:

在这里插入图片描述

这张图更细致的划分了一下内存,接下来,我们就一个一个的就看一下: 现阶段的学习中我们主要了解一下栈,堆,数据段和代码段就行了。
  1. 内核空间
首先第一个我们先来看内核空间,这块空间是用户代码不能读写的,也就是说,我们自己写的代码是不能访问这块空间的。
这里的栈其实就是我们之前提到的栈区,栈区一般用来存放局部变量、函数的形参、调用函数时的返回值等临时变量。 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  1. 内存映射段
内存映射段用来存放文件映射、动态库、匿名映射等内容。
堆就是之前提到的堆区,堆区是用来进行动态内存分配的,像malloc、calloc、realloc这些动态内存函数开辟的空间就是在堆区上的,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  1. 数据段(静态区)
数据段其实就是我们之前所说的静态区,静态区主要用来存放一些全局变量以及静态数据(如static修饰的静态变量)等。程序结束后由系统释放。
  1. 代码段
代码段存放的是可执行代码(函数体、类成员函数和全局函数的二进制代码。)和只读常量。

有了这幅图,我们就可以更好的理解之前在《初始C语言——关键字static的作用》中讲的static关键字修饰局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。 所以生命周期变长。

2. 柔性数组

2.1 柔性数组的定义

接下来我们再来学习一个新知识——柔性数组。

也许大家可能没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。 C99 标准中,结构体中的最后一个元素允许是未知大小的数组,这个成员就叫做『柔性数组』成员

什么意思呢?那接下来我们就来举个例子:

代码语言:javascript

复制

struct S
{
	int a;
	double b;
	int arr[];
};
我们看struct S这个结构体类型,它就包含了一个柔性数组成员int arr[],它的大小是未知的,我们并没有指定它的大小。

如果你这样写了,在你的编译器上报错了无法编译,那可能是你的编译器不支持这种写法,你可以换成这种写法:

代码语言:javascript

复制

struct S
{
	int a;
	double b;
	int arr[0];
};
这时两种不同的写法,可能有的编译器支持这种,有的支持那种。 当然还要注意这中语法是C99 标准中才引入的。
2.2 柔性数组的特点

既然它叫柔性数组,呢这个“柔”怎么体现呢? 接下来我们就来了解一下柔性数组的特点:

  1. 结构体中的柔性数组成员前面必须至少有一个其他成员

代码语言:javascript

复制

struct S
{
	int arr[0];
};
也就是说你不能写成像上面这样,柔性数组成员前面至少要有一个其它成员。

代码语言:javascript

复制

struct S
{
	int a;
	//.....;(至少一个其它成员)
	int arr[0];
};
  1. sizeof 返回的这种结构体的大小不包括柔性数组的内存大小
什么意思呢? 就是我们用sizeof去计算这种包含柔性数组成员的结构体的大小时,不会加上柔性数组成员的大小。 况且柔性数组没有指定数组大小,真要计算好像也没法算啊!

我们来计算一个包含柔性数组的结构体的大小看看:

代码语言:javascript

复制

#include <stdio.h>
struct S
{
	int a;//对齐数4
	double b;//对齐数8
	int arr[];
};
int main()
{
	printf("%d", sizeof(struct S));
	return 0;
}
这个结构体大小,如果只看前两个成员,考虑对齐的话,应该是16个字节。

我们打印出来看看:

在这里插入图片描述

确实是16个字节,没有包含柔性数组的内存大小。
  1. 包含柔性数组成员的结构体应该用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

什么意思呢?

包含柔性数组成员的结构体应该用malloc ()函数进行内存的动态分配,这句话意味着我们不能像普通的结构体那样直接拿我们创建好的结构体类型创建结构体变量

比如像这样:

代码语言:javascript

复制

#include <stdio.h>
struct S
{
	int a;
	double b;
	int arr[];
};
int main()
{
	struct S s1;
	return 0;
}
这段代码中struct S是一个包含柔性数组成员的结构体变量,但这里还是像普通的结构体一样创建了一个结构体变量。但这样其实是错误的用法。

为什么这样不行呢?

我们上面已经讲了,sizeof 返回的这种结构体的大小不包括柔性数组的内存大小,那我们直接像这样创建一个结构体变量,这个柔性数组成员是没有属于自己的空间的,那我们就没法使用它啊。

那对于这种包含柔性数组成员的结构体,我们应该怎样正确的为它开辟空间,使得我们可以使用这个柔性数组呢?

那就是上面说的,应该用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

比如:

代码语言:javascript

复制

struct S
{
	int a;
	double b;
	int arr[];
};
我们就还拿这个包含柔性数组的结构体来说,假如我们想使用这个柔性数组,去存放4个整型数据。

那我们就可以这样为它开辟空间:

代码语言:javascript

复制

#include <stdio.h>
struct S
{
	int a;
	double b;
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
	return 0;
}
前面sizeof(struct S)就是前面两个成员的大小,后面又加了一个sizeof(int) * 4就是为柔性数组开辟的空间,因为我们想往里放4个整型数据。
2.3 柔性数组的使用

那开辟好空间,我们就可以使用了:

我们现在就给这个结构体的成员赋个值,然后打印一下看看,当然记得malloc的返回值我们还是要判断一下,使用完释放一下,把ps 置空。

代码语言:javascript

复制

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
	assert(ps);
	ps->a = 100;
	ps->b = 5.5;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		scanf("%d", &(ps->arr[i]));
	}
	printf("%d %lf\n", ps->a, ps->b);
	for (i = 0; i < 4; i++)
	{
		printf("%d", ps->arr[i]);
	}
	free(ps);
	ps = NULL;
	return 0;
}

给柔性数组输入1,2,3,4,打印一下看看:

在这里插入图片描述

没问题,这样做就成功为柔性数组开辟了空间,并且可以使用它。

那讲到这里,大家是有没有对柔性数组的这个“柔性”有了一点自己的理解呢?

大家想一下,我们刚才为柔性数组开辟了4个字节空间,如果我们不使用柔性数组, 直接定义一个这样的结构体:

代码语言:javascript

复制

struct S
{
	int a;
	double b;
	int arr[4];
};
直接包含一个int arr[4]这样的数组,那它是不是也能放4个整型啊。 但是,这样的话,它的大小是不是就固定死了,就能放4个整型,想多放一个都不行。 而我们使用柔性数组的话,是使用malloc为它开辟空间的,那我们跟据自己的需求,是不是可以使用realloc再调整柔性数组这块空间的大小啊。

代码语言:javascript

复制

struct S* ptr=(struct S*)realloc(ps,sizeof(struct S) + sizeof(int) * 10);
assert(ptr);
ps=ptr;
这次调整,它就可以放10个整型了。 可大可小,是不是有点那种所谓的“柔性”的意思了。
2.4 柔性数组的优势
那讲完柔性数组的使用,大家可能会想: 柔性数组说到底,不就是搞了一个可大可小的数组嘛,那想要实现这样的功能,非得用柔性数组嘛。 我们是不是也可以这样搞:

代码语言:javascript

复制

struct S
{
	int a;
	double b;
	int* arr;
};
我们定义一个int* arr这样一个成员变量,它指向的空间我们可以使用malloc为它开辟啊,如果大小不合适,我们就再使用realloc调整大小,这样是不是也可以达到上面柔性数组的效果啊。

那为了和上面的代码保持一致,我们这里创建一个结构体变量是不是也把他所有的成员放到堆区上,那这里我们可以这样搞:

代码语言:javascript

复制

struct S
{
	int n;
	double s;
	int* arr;
};
int main()
{
	struct S*ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
		return 1;

	ps->n = 100;
	ps->s = 5.5;
	
	int* ptr = (int*)malloc(4 * sizeof(int));
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	//使用
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		scanf("%d", &(ps->arr[i]));
	}

	//调整
	//realloc(ps->arr, 10*sizeof(int));
	
	//打印
	printf("%d\n", ps->n);
	printf("%lf\n", ps->s);
	for (i = 0; i < 4; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//释放
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}
大家仔细看看这段代码。这样确实也是可以的。 上述这两个代码可以达到同样的效果。

那既然这样也可以,我们为什么还要搞一个柔性数组呢?

因为柔性数组是有一些自己独有的优势的。

那接下来,我们就对比一下这两段代码,看看柔性数组存在哪些优势:

我们先来看一下第二段代码,仔细观察我们发现第二段代码用了两次malloc: 第一次我们是定义了一个struct S*类型的指针ps,将它赋值为(struct S*)malloc(sizeof(struct S)),这样它指向的空间就是在堆区了,它指向的结构体变量就也是在堆区了(和上面代码保持一致) 第二次我们malloc是为ps->arr,也就是为柔性数组开辟空间。

那这样开辟了两次,有没有什么不好之处呢?

那就是这两次开辟的空间有可能不是连续的,不连续的话它们之间就有可能形成内存碎片,而这些残留的空间以后也不太好被有效的利用起来了,这样可能就导致内存的利用率就下降了。

而第一种我们使用柔性数组的方法:

我们只malloc了一次,使得前两个成员和柔性数组成员放在了一块连续的空间。

除此之外:

第一种方法我们malloc开辟了两次,那我们就要free释放两次,除了要释放结构体指针指向的那块空间,是不是还要释放结构体指针指向的柔性数组成员所在的那块malloc开辟的空间啊。 如果我们忘记释放了某一个,那是不是就造成内存泄漏了。

所以通过这一点就体现了方法1(使用了柔性数组)的第一个优势:

  1. 方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。 所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。 而方法1使用柔性数组就达到了这样的效果。
  1. 有利于提高访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(不过可能也高不了多少)

总的来说,第一段代码使用了柔性数组,在某些方面还是比第二段代码更好一些的。

好了,以上就是这篇文章的全部内容了,欢迎大家指正!!!