【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

目录
相关文章
|
1月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
1天前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
3月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
215 68
|
1月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
113 0
|
2月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
|
4月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
110 1
|
1月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
9天前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
37 16
|
2天前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
2天前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。

热门文章

最新文章