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

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

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

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

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

在之前的文章里其实我们简单的介绍过C语言中的内存划分。

大致可以分为:栈区,堆区和静态区:

3fb10b95d661477aae77a3b9b9e1c167.png那今天,我们来更加细致的细致的讲解一下C/C++程序的内存开辟。

首先,我们来看一张图:


0d608287daa240eaba2b87018842da1b.png

这张图更细致的划分了一下内存,接下来,我们就一个一个的就看一下:

现阶段的学习中我们主要了解一下栈,堆,数据段和代码段就行了。

  1. 1.内核空间
  2. 首先第一个我们先来看内核空间,这块空间是用户代码不能读写的,也就是说,我们自己写的代码是不能访问这块空间的。
  3. 2.栈
  4. 这里的栈其实就是我们之前提到的栈区,栈区一般用来存放局部变量、函数的形参、调用函数时的返回值等临时变量。

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

  1. 3.内存映射段
  2. 内存映射段用来存放文件映射、动态库、匿名映射等内容。
  3. 4.堆
  4. 堆就是之前提到的堆区,堆区是用来进行动态内存分配的,像malloc、calloc、realloc这些动态内存函数开辟的空间就是在堆区上的,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  5. 5.数据段(静态区)
  6. 数据段其实就是我们之前所说的静态区,静态区主要用来存放一些全局变量以及静态数据(如static修饰的静态变量)等。程序结束后由系统释放。

     6.代码段

     代码段存放的是可执行代码(函数体、类成员函数和全局函数的二进制代码。)和只读常量。


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


实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。

所以生命周期变长。

2. 柔性数组

2.1 柔性数组的定义

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


也许大家可能没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。

C99 标准中,结构体中的最后一个元素允许是未知大小的数组,这个成员就叫做『柔性数组』成员


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

struct S
{
  int a;
  double b;
  int arr[];
};

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

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

struct S
{
  int a;
  double b;
  int arr[0];
};

这时两种不同的写法,可能有的编译器支持这种,有的支持那种。

当然还要注意这中语法是C99 标准中才引入的。

2.2 柔性数组的特点

既然它叫柔性数组,呢这个“柔”怎么体现呢?

接下来我们就来了解一下柔性数组的特点:

  1. 结构体中的柔性数组成员前面必须至少有一个其他成员
struct S
{
  int arr[0];
};

也就是说你不能写成像上面这样,柔性数组成员前面至少要有一个其它成员。

struct S
{
  int a;
  //.....;(至少一个其它成员)
  int arr[0];
};
  1. sizeof 返回的这种结构体的大小不包括柔性数组的内存大小
  2. 什么意思呢?

就是我们用sizeof去计算这种包含柔性数组成员的结构体的大小时,不会加上柔性数组成员的大小。

况且柔性数组没有指定数组大小,真要计算好像也没法算啊!

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

#include <stdio.h>
struct S
{
  int a;//对齐数4
  double b;//对齐数8
  int arr[];
};
int main()
{
  printf("%d", sizeof(struct S));
  return 0;
}

这个结构体大小,如果只看前两个成员,考虑对齐的话,应该是16个字节。

我们打印出来看看:3286b0bb205c4b9b870f9e70efc613d6.png

确实是16个字节,没有包含柔性数组的内存大小。


包含柔性数组成员的结构体应该用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

什么意思呢?


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


比如像这样:

#include <stdio.h>
struct S
{
  int a;
  double b;
  int arr[];
};
int main()
{
  struct S s1;
  return 0;
}

这段代码中struct S是一个包含柔性数组成员的结构体变量,但这里还是像普通的结构体一样创建了一个结构体变量。但这样其实是错误的用法。


为什么这样不行呢?


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


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


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


比如:

struct S
{
  int a;
  double b;
  int arr[];
};

我们就还拿这个包含柔性数组的结构体来说,假如我们想使用这个柔性数组,去存放4个整型数据。

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

#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 置空。

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,打印一下看看:

e485b969871d4db7a7dbbaf411068053.png

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

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

大家想一下,我们刚才为柔性数组开辟了4个字节空间,如果我们不使用柔性数组,

直接定义一个这样的结构体:

struct S
{
  int a;
  double b;
  int arr[4];
};

直接包含一个int arr[4]这样的数组,那它是不是也能放4个整型啊。

但是,这样的话,它的大小是不是就固定死了,就能放4个整型,想多放一个都不行。

而我们使用柔性数组的话,是使用malloc为它开辟空间的,那我们跟据自己的需求,是不是可以使用realloc再调整柔性数组这块空间的大小啊。

struct S* ptr=(struct S*)realloc(ps,sizeof(struct S) + sizeof(int) * 10);
assert(ptr);
ps=ptr;

这次调整,它就可以放10个整型了。

可大可小,是不是有点那种所谓的“柔性”的意思了。

2.4 柔性数组的优势

那讲完柔性数组的使用,大家可能会想:

柔性数组说到底,不就是搞了一个可大可小的数组嘛,那想要实现这样的功能,非得用柔性数组嘛。

我们是不是也可以这样搞:

struct S
{
  int a;
  double b;
  int* arr;
};

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


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

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(使用了柔性数组)的第一个优势:


方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。

所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

而方法1使用柔性数组就达到了这样的效果。


有利于提高访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(不过可能也高不了多少)


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


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

084052c07b114123bfe2254797e35b77.png

目录
相关文章
|
4月前
|
NoSQL 测试技术
内存程序崩溃
【10月更文挑战第13天】
165 62
|
1月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
|
3月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
99 1
|
3月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
252 4
|
3月前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
4月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
271 22
|
4月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
710 1
|
4月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(三)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
3天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19