C\C++语言3|动态内存(堆内存)使用和管理以及动态数组、向量

在C或C++中,每个程序都需要用到几个变量在写程序前就应该知道,每个数组有几个元素也必须在写程序时就决定。有时在编程序时,我们并不知道需要多大的数组或需要多少变量,直到程序开始运行,根据某一个当前运行值才能决定。(或者通过交互确定。)

在定义数组时,我们建议按最大的可能值定义数组,每次运行时使用数组的一部分元素,当元素个数变化不是很大时,这个方案是可行的;但如果元素个数的变化范围很大时,就太浪费空间了。

这个问题一个更好的解决方案就是动态变量机制。所谓动态变量是指:在写程序时无法确定它们的存在,只有当程序运行起来时,随着程序的运行,根据程序的需求动态产生和消亡的变量。由于动态变量不能在程序中定义,也就无法给它们取名字,因此动态变量的访问需要通过指向动态变量的指针变量来进行间接访问。

计算机内存除了操作系统、各进程占用的空间以外,剩余的空间(加上硬盘的虚拟内存)都可以用作程序的动态内存(堆内存)。

要使用动态变量,必须定义一个相应类型的指针变量(存放一个内存单元地址的变量),然后通过动态变量申请的功能向系统申请一块空间,将空间的地址存入该指针变量。这样就可以间接访问动态变量了。当程序运行结束时,系统会自动回收指针占用的空间,但并不回收指针指向的动态变量的空间,动态变量的空间需要程序员在程序中地释放。因此要实现等实现动态内存分配,系统必须提供以下3个功能。

定义指针变量;
动态申请空间;
动态回收空间;

1 C语言的动态内存申请

在C语言中,malloc()函数的功能是在内存的动态存储区中分配size个字节的连续空间,它的返回值指向所分配的那一段空间的起始地址,若分配失败,则返回一个空指针(0)。

void *malloc(unsigned int size);
void *calloc(unsigned int n, unsigned int size);
void *realloc(void *mem_address, unsigned int newsize);

malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存。

malloc()函数接受一个参数:所需的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存块是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用这个指针变量访问这块内存。

malloc()函数返回的是一个void通用指针,可以通过强制类型转换来转换为特定的指针类型。

void* 类型表示未确定类型的指针。C、C++规定,void* 类型可以通过类型转换强制转换为任何其它类型的指针。

类型未确定,即无法确定对应内存空间的长度和解码方案,强制类型转换,即重新确定长度和解码方案。

malloc()一般需和free()函数配对使用,free()用于释放内存。

malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。

calloc()函数接受两个参数,第一个参数是所需的存储单元数量,第二个参数是存储单元的大小(以字节为单位)。另外,callo()函数会把块中的所有位都设置为0.

calloc()的功能是在内存的动态存储区中分配n个长度为size个字节的连续空间,它的返回值是指向所分配空间的起始地址,若分配失败,则返回一个空指针(0)。

realloc()可以确保一块足够大的连续空间,当空间或大或小时,可以重新分配,目的就是确保空间够用并连续。

void *realloc(void *mem_address, unsigned int newsize);

realloc()先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。

当今的操作系统都会给应用程序的每一个进程分配独立的“虚拟地址空间”。

程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到合适的物理内存地址上。这样一来,只要操作系统处理好虚拟地址到物理地址的映射关系,就可以保证不同的程序最终访问不同的区域,从而达到内存地址空间的隔离。

物理地址存在于物理内存中,同理,虚拟地址存在于虚拟地址空间中。要进行数据访问,必须由操作系统将虚拟地址转化为物理地址。我们在C语言和C++中看到的地址都是虚拟地址。用户是看不到物理地址的,物理地址由操作系统统一管理。

不其它的编程语言,C语言没有内存回收机制,为了避免内存泄露,C++使用了智能指针的机制。

2 C++的动态内存申请

C++new(delete)与malloc(free)的主要区别一是可以自动确认数据类型的内存大小,二是除了分配与释放内存,还会自动调用构造函数与析构函数。对于数组的操作,需要使用如delete [] stringParr;

double *p, *q, *t;
p new double;
q = new double(1.0); // q指向的单元被初始化为1.0
t = new double[10]; // 为t分配一个长度为10的一维数组;

如果分配失败,则返回一个空指针;

符号[]是告诉编译器,我这里需要释放的是一个整个数组的指针,这样编译器就会逐个释放每个元素的内存占用。

使用malloc函数需要指定内存分配的字节数并且不能初始化对象,New会自动调用对象的构造函数。delete会调用对象的destructor,而free不会调用对象的destructor.

同时,malloc需要计算类型的字节数,而new可以自动推算类型的字节数。

new、delete返回的是所分配对象(变量)的指针,而malloc、free返回的是void指针,所以需要做强制类型转换;

//为简单变量动态分配内存,并作初始化
#include <iostream.h>
int main()
{ 
 int *p;
 p = new int(99);
 //动态分配内存,并将99作为初始化值赋给它
 cout<< *p;
 delete p;
 return 0;
}//new 不能初始化动态数组
p = new int(5);//申请单个空间,并赋默认值为5
q = new char[10]; //申请10个空间

用new(或malloc)开辟的内存单元没有名字,指向其首地址的指针是引用其的唯一途径,若指针变量重新赋值,则用new(或malloc)开辟的内存单元就在内存中“丢失”了,别的程序也不能占用这段单元。

当我们在程序中使用new运算符(或malloc)在堆中开辟一个空间之后,在数据使用完后,一定要释放在堆中开辟的空间,否则会出现内存泄露。需要注意的是,释放以后并没有完事,此时的指针并没有自动置NULL,此时指针指向的就是“垃圾”内存,是一个“野指针”。如果继续使用,会出现不可预料的错误。

因为没有明确的所属关系,一些在多个模块中共亨的资源指针很容易成为“野指针”,从而导致多种内存汸问的问题。首先,因为共享某个内存块指针的多个模块并不拥有这个指针,它们只是使用这些指针,并不负责释放这些指针所指向的内存资源,这可能会导致程序的内存泄露。其次,因为这些指针被多个模块所共享,这可能会导致某个指针所指向的内存资源巳经被释放,但是另外的模块又试图访问这个指针所指向的内存资源,最终导致内存访问错误。

为此,C++使用了智能指针的办法,详细介绍可见:

C++|智能指针为何智能?

3 动态数组

动态数组是写程序时不指定长度的数组,它的长度在程序运行时确定。

普通数组声明时必须指定长度。但有的时候,除非程序实际运行,否则不好确定长度。例如,数组可能容纳了一个学生ID 列表,但程序每次运行时,班级的学生数都可能发生变化。沿用传统做法,就必须预估数组可能的最大长度,并希望那个长度足够大,能适应所有情况。但这样做有两个问题。首先,你估计的长度可能还是太小,造成程序不能适应所有情况。其次,由于数组可能包含许多未使用的位置,所以会浪费计算机内存。动态数组避免了所有这些问题。用动态数组容纳学生ID,可在程序运行时输入班级的学生数。然后,程序会创建刚好那么大的动态数组。动态数组用操作符new 创建。和许多人想象的不同,动态数组的创建和使用其实非常简单。由于数组变量其实就是指针变量,所以可以用操作符new 创建被用作数组的动态变量,并像使用普通数组那样使用动态数组变量。例如,以下语句创建一个动态数组变量,其中含有10 个double 类型的数组元素:

double* p;
p = new double[10];

由于程序马上就要终止,所以并不是真的需要这个delete 语句。但如果程序准备用动态变量做其他事情,就应该包括这个delete 语句,将动态数组占用的内存还给自由存储。为动态数组执行的delete 语句类似于以前介绍过的delete 语句,只是在动态数组的情况下,必须包括一对空的方括号,如下所示:

delete [] a;

方括号告诉C++要销毁的是动态数组变量,所以系统会检查数组的长度,并删除刚好那么多的索引变量。如省略方括号,相当于告诉计算机只销毁一个int 类型的变量。例如:

delete a;

虽然上述语句不合法,但大多数编译器都检测不到这个错误。ANSI C++标准规定,这种情况下发生的事情是“未定义”的,意味着编译器的作者可以做他觉得方便的任何事情——

相对于上述的静态数组,动态数组在运行时使用malloc()或new来申请需求数量的内存空间。

首先是声明一个指针变量,然后用这个指针变量指向内存中的动态数组,并被用作动态数组名称:double * a;

调用new 使用操作符new 创建一个动态数组:a = new double[arraySize];动态数组长度在方括号内给出。可用int 变量或其他int 表达式给出长度。arraySize 可以是值在运行才确定的int 变量。

动态数组可以和普通数组一样使用,指针变量(比如a)可以像普通数组那样使用。例如,可采用标准方式书写索引变量,比如a[0],a[1],等等。不能再为指针变量赋其他任何指针值。相反,它应该像数组变量那样使用。

调用delete [] 用完动态数组后使用delete、一对空的方括号和指针变量来销毁动态数组,将其占用的内存还给自由存储以便重用。例如:delete [] a;

一个数组是一段连续的内存空间的命名,一个按上述流程在堆中申请的也是一段连续的内存空间,并也被赋值给了一个指针变量。
指针是内存地址,变量可以对计算机内存中的地址进行命名,而指针提供了一种间接的变量命名方式。
动态变量是程序运行时创建(和销毁)的变量。动态数组是其长度在程序运行时确定的数组。动态数组被实现为数组类型的动态变量。
动态变量要占用计算机内存的一个特殊区域,这个区域称为自由存储。程序结束动态变量的使用后,应该将动态变量占用的内存还给自由存储,以便重用;这是用delete 语句来完成的。

4 向量

向量用法类似于数组,但向量长度不固定。如需更大容量来存储更多元素,它的容量就会自动扩充。向量在库中定义,位于std 命名空间。所以,在程序中使用向量需包括以下语句(或其他类似语句):

#include <vector>
using namespace std;

给定了Base_Type(基类型)的向量类要写成vector。下面是两个示范向量声明:

vector v; // 默认构造函数生成一个空向量
vector record(20); //用AClass 类的默认构造函数初始化20 个元素

在向量添加元素要用成员函数push_back,如下例所示:

v.push_back(42);

一个元素位置获得了它的第一个值之后(无论是通过push_back,还是通过构造函数来初始化),以后就可使用方括号记号法来访问那个元素位置,和访问普通数组元素一样。

向量在任何时候都有一个容量,即当前分配了内存的元素的数量。成员函数capacity()返回向量的容量。不要混淆向量的容量和长度。长度是向量中元素的个数,容量是实际分配了内存的元素的个数。容量通常大于长度,且肯定大于或等于长度。

一旦向量容量不够,并需要空间来容纳一个附加成员,容量就会自动增加。不同C++实现对每次增加的容量有不同规定,但通常都会超过马上就要用到的容量。一个常见的方案是倍增当前需要的容量。由于增加容量是一项复杂的任务,所以相较于频繁分配许多小的内存块,一次性分配一个较大的内存块显得更有效率。

向量的长度是指向量中的元素个数,容量是当前实际分配了内存的元素的个数。对于向量v,可分别用成员函数v.size()和v.capacity()判断其长度和容量。

一般可忽略向量的容量,这不会对程序行为产生任何影响。但如果必须考虑效率问题,可考虑自己管理容量,而不是接受每次都使容量倍增的默认行为。可用成员函数reserve来显式增大向量的容量。例如以下语句将容量设为至少32 个元素:

v.reserve(32);

以下语句将容量设为向量当前元素个数加10:

v.reserve(v.size() + 10);

-End-

举报
评论 0