C语言内存管理是指系统内存的分配、创建和使用。 在内存管理中,由于是操作系统内存,使用不当终究会造成麻烦的结果。 本文将从系统内存的分配和创建入手,通过实例来说明内存管理不当的情况和解决方法。
1.内存
在计算机中,各个应用程序之间的内存是相互独立的。 通常情况下,应用A无法访问应用B,当然也有一些特殊的技巧可以访问,但本文不做详细说明。 例如,在计算机中,视频播放器程序和浏览器程序的内存是不能访问的,每个程序所拥有的内存都是分区管理的。
在计算机系统中,运行程序A会在内存中开辟程序A的内存区1,运行程序B会在内存中开辟程序B的内存区2,内存区1和内存区2在逻辑上是分开的。
1.1 内存的四个区域
程序A中开辟的内存区1会分成几个区域,就是内存的四个区域,内存的四个区域又分为栈区、堆区、数据区和代码区。
栈区是指存放一些临时变量的区域。 临时变量包括局部变量、返回值、参数、返回地址等,当这些变量超出当前范围时,会自动弹出。 栈的最大存储有一个大小,这个值是固定的,超过这个大小就会导致栈溢出。
堆区是指一块比较大的内存空间,主要用于动态内存的分配; 在程序开发中,分配和释放的一般是开发者。 如果在程序结束时没有释放,系统会自动回收。
数据区是指主要存放全局变量、常量和静态变量的区域,数据区可分为全局区和静态区。 全局变量和静态变量将存储在该区域中。
代码区更容易理解。 主要存放可执行代码,该区域的属性为只读。
1.2 使用代码确认四个内存区的底层结构
由于栈区和堆区的底层结构比较直观,这里使用的代码只是演示了这两个概念。 先看代码观察栈区的内存地址分配:
portant;overflow-wrap: break-word !important;">#include
int main()
{
int a = 0;
int b = 0;
char c='0';
printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
}
运行结果为:
我们可以观察到变量a的地址就是变量b的地址。 由于int的数据大小为4,所以两者的间隔为4; 再看变量c,我们发现变量c的地址是 ,变量b的地址区间是1,因为c的数据类型是char,类型大小是1。这里我们观察到我创建变量的时候,顺序是从a到b再到c,为什么它们之间的地址不是递增而是递减? 那是因为栈区的一个数据存储结构是先进后出的,如图:
首先栈顶是地址的“最小”索引,然后依次向下递增,但是由于栈的特殊存储结构,我们先存储变量a,再存储它的一个索引地址最大,依次递减;第二次存储的值为b,其地址索引小于a。 由于int的数据大小为4,所以a的地址减4。存储c时为char,其大小为1,地址为。 由于a、b、c三个变量属于同一个栈,所以它们地址的索引是连续的。 如果我创建一个静态变量怎么办? 上面的内容中说明了静态变量是存放在静态区的,现在我们来确认一下:
portant;overflow-wrap: break-word !important;">#include
int main()
{
int a = 0;
int b = 0;
char c='0';
static int d = 0;
printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
printf("静态变量d的地址是:%d\n", &d);
}
运行结果如下:
上面代码中创建了一个变量d,变量d是一个静态变量。 运行代码后,从结果可知,静态变量d的地址与普通变量a、b、c的地址不连续。 两者的内存地址是分开的。 接下来,在这里创建一个全局变量。 根据上面的内容,全局变量和静态变量都应该存放在静态区。 代码如下:
portant;overflow-wrap: break-word !important;">#include
int e = 0;
int main()
{
int a = 0;
int b = 0;
char c='0';
static int d = 0;
printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
printf("静态变量d的地址是:%d\n", &d);
printf("全局变量e的地址是:%d\n", &e);
}
运行结果如下:
从以上运行结果可以确认以上内容的真实性,同时也得到一个知识点。 栈区和数据区使用栈结构来存储数据。
在上面的内容中,也说明了一点栈的特点,就是容量有固定的大小,超过最大容量就会导致栈溢出。 查看以下代码:
portant;overflow-wrap: break-word !important;">#include
int main()
{
char arr_char[1024*1000000];
arr_char[0] = '0';
}
上面的代码定义了一个字符数组,并将大小设置为 1024*。 设置这个数据方便查看尺寸; 然后在数组的头部分配一个值。 运行结果如下:
这是程序运行的错误,原因是栈溢出。 如果平时开发需要大量内存,就需要用到堆。
堆不具有与栈相同的结构,也不具有与栈相同的先进后出。 需要人为分配和使用内存。 代码如下:
portant;overflow-wrap: break-word !important;">#include
#include
#include
int main()
{
char *p1 = (char *)malloc(1024*1000000);
strcpy(p1, "这里是堆区");
printf("%s\n", p1);
}
上面代码中,将“这里是堆区”的数据转移到手动开辟的内存空间p1中,手动开辟该空间以供使用。 申请空间大小为1024*。 栈空间这么大,肯定会造成栈溢出。 但是堆本身容量很大,所以不会出现这种情况。 然后输出打开内存的内容,运行结果如下:
这里要注意,p1是开辟的内存空间的地址。
二、免费
在C语言(不是C++)中,free是系统提供的函数,成对使用,用于从堆中分配和释放内存。 全称翻译为“动态内存分配”。
2.1 和使用免费
我们在开辟堆空间时使用的函数,在C语言中就是用来申请内存空间的。 函数原型如下:
portant;overflow-wrap: break-word !important;">void *malloc(size_t size);
函数中size表示需要申请的内存空间大小。 如果申请成功,则返回该内存空间的地址; 申请失败则返回NULL,申请成功则不会自动初始化。
细心的同学可能会发现这个函数的返回值被描述为void *,其中void *并不是指具体的类型,而是表示类型不确定,通过接收到的指针变量进行类型转换。 分配内存时,需要注意。 即使系统在程序关闭时自动回收手动申请的内存,也必须手动释放,以保证在不需要的时候能够将内存归还到堆空间,从而合理分配和使用内存.
使用free函数释放空间,函数原型如下:
portant;overflow-wrap: break-word !important;">void free(void *ptr);
free函数的返回值为void,没有返回值,接收的参数是指向已分配内存空间的指针。 一个完整的堆内存申请和释放示例如下:
portant;overflow-wrap: break-word !important;">#include
#include
#include
int main() {
int n, *p, i;
printf("请输入一个任意长度的数字来分配空间:");
scanf("%d", &n);
p = (int *)malloc(n * sizeof(int));
if(p==NULL){
printf("申请失败\n");
return 0;
}else{
printf("申请成功\n");
}
memset(p, 0, n * sizeof(int));//填充0
//查看
for (i = 0; i < n; i++)
printf("%d ", p[i]);
printf("\n");
free(p);
p = NULL;
return 0;
}
上面代码中,通过用户输入创建了指定大小的内存,判断该内存地址是否创建成功,并使用函数填充内存空间,然后使用for循环进行检查。 最后使用free释放内存,给p赋值NULL。 这个很重要。 指针不能指向未知地址,必须设置为NULL; 否则,后面的开发者会误认为它是一个普通的指针,有可能再次使用。 通过指针访问一些操作,但是此时指针是没有用的,指向的内存此时也不知道如何使用。 如果此时出现意外,将会造成难以预料的后果,甚至会导致系统崩溃。 在使用中需要更多。
2.2 内存泄漏和安全使用的例子和解释
内存泄漏是指在动态分配的内存中,内存没有释放或由于某些原因无法释放。 轻者会造成系统内存资源的浪费,重者会导致整个系统崩溃。
内存泄漏通常是隐蔽的,少量的内存泄漏不一定会造成难以承受的后果,但错误的累积就会导致系统整体性能下降或系统崩溃。 尤其是在比较大的系统中,如何有效的防止内存泄露等问题变得尤为重要。 例如,对于一些长时间的程序,如果在开始运行时出现少量的内存泄漏问题,可能不会出现,但随着运行时间的增加和系统业务处理的增加,内存泄漏就会累积起来; 这时候极大的会造成不可预知的后果,比如整个系统的崩溃,由此带来的损失将是难以承受的。 因此,防止内存泄漏对于底层开发人员来说尤为重要。
C程序员在开发过程中,不可避免地会面临内存操作的问题,尤其是在频繁申请动态内存时,极易造成内存泄漏事故。 比如申请了一块内存空间后,不初始化就读取内容,间接申请动态内存但不释放,释放一块动态申请的内存后继续引用内存内容; 泄漏的原因,往往这些原因在测试的时候可能因为太隐蔽而不能完全清楚,在项目上线时间长了之后会导致灾难性的后果。
下面是在一个子函数中申请内存空间,但是没有释放:
portant;overflow-wrap: break-word !important;">#include
#include
#include
void m() {
char *p1;
p1 = malloc(100);
printf("开始对内存进行泄漏...");
}
int main() {
m();
return 0;
}
上面代码中,申请了100个单位的内存空间后,一直没有释放。 假设当前系统中频繁调用m函数,每次使用都会导致100个单位的内存空间没有释放,久而久之就会造成严重的后果。 合理的做法是在p1用完之后加上free去:
portant;overflow-wrap: break-word !important;">free(p1);
下面演示读取文件时的一个不规则操作:
portant;overflow-wrap: break-word !important;">#include
#include
#include
int m(char *filename) {
FILE* f;
int key;
f = fopen(filename, "r");
fscanf(f, "%d", &key);
return key;
}
int main() {
m("number.txt");
return 0;
}
上面的文件都没有被读取,此时会产生多余的内存。 一次可能还好,多次内存就会翻倍。 可以使用循环调用,然后可以在任务管理器中查看程序。 时间占用的内存大小,代码为:
portant;overflow-wrap: break-word !important;">#include
#include
#include
int m(char *filename) {
FILE* f;
int key;
f = fopen(filename, "r");
fscanf(f, "%d", &key);
return key;
}
int main() {
int i;
for(i=0;i<500;i++) {
m("number.txt");
}
return 0;
}
可以查看加循环后的程序和加循环前的程序的内存占用对比,可以发现两者之间加循环的代码会增加开销和占用容量。
未初始化的指针也可能导致内存泄漏,因为未初始化的指针指向不可控,例如:
portant;overflow-wrap: break-word !important;">int *p;
*p = val;
包括错误的空闲内存空间:
portant;overflow-wrap: break-word !important;">pp=p;
free(p);
free(pp);
free后使用,导致悬空指针。 申请完动态内存后,我们使用指针指向内存。 使用完后,我们通过free函数释放申请的内存,该内存将允许其他程序申请; 但是我们使用后的动态内存指针还是指向这个地址。 ,假设下一秒其他程序申请了这个区域的内存地址并执行了操作。 当我仍然使用已经释放的指针进行下一步操作,或者进行计算时,结果将大相径庭,或者其他灾难性的后果。 因此,这些指针在生命周期结束后也应该设置为空。 看个例子,因为free释放后指针还在使用,所以计算结果有很大的不同:
portant;overflow-wrap: break-word !important;">#include
#include
#include
int m(char *freep) {
int val=freep[0];
printf("2*freep=:%d\n",val*2);
free(freep);
val=freep[0];
printf("2*freep=:%d\n",val*2);
}
int main() {
int *freep = (int *) malloc(sizeof (int));
freep[0]=1;
m(freep);
return 0;
}
上面代码申请了一块内存后,将值传给1; 函数中,先用val值接收freep的值,将val乘以2,然后释放free,重新赋值给val,最后再次用val乘以2,导致结果变化很大,最可怕的是错误很难发现,隐匿性很强,造成的后果却不堪设想。 运行结果如下:
三、新和
C++ 使用 new and 从堆中分配和释放内存。 new 和 是运算符,不是函数,而且是成对使用的(后面会解释为什么要成对使用)。
new/ 不仅仅是分配和释放内存(使用 /free),因此在 C++ 中使用 new/ 代替 /free。
3.1 新建与使用
new的一般格式如下:
C++中new的三种用法包括:plain new、new和new。
Plain new是最常用的new方法,在C++中定义如下:
portant;overflow-wrap: break-word !important;">void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete( void *) throw();
plain new 在分配失败的情况下抛出异常 std:: 而不是返回 NULL,因此检查返回值是否为 NULL 是没有用的。
portant;overflow-wrap: break-word !important;">char *getMemory(unsigned long size)
{
char * p = new char[size];
return p;
}
void main(void)
{
try{
char * p = getMemory(1000000); // 可能发生异常
// ...
delete [] p;
}
catch(const std::bad_alloc & ex)
{
cout << ex.what();
}
}
new 是不抛出异常的 new 形式。 new 在失败时返回 NULL。 它的定义如下:
portant;overflow-wrap: break-word !important;">void * operator new(std::size_t, const std::nothrow_t&) throw();
void operator delete(void*) throw();
portant;overflow-wrap: break-word !important;">void func(unsinged long length)
{
unsinged char * p = new(nothrow) unsinged char[length];
// 在使用这种new时要加(nothrow) ,表示不使用异常处理 。
if (p == NULL) // 不抛异常,一定要检查
cout << "allocte failed !";
// ...
delete [] p;
}
new 的意思是“放置”,这个 new 允许在已成功分配的一块内存上重建一个对象或一个对象数组。 new 不用担心内存分配失败,因为它根本不分配内存,它唯一做的就是调用对象的构造函数。 它的定义如下:
portant;overflow-wrap: break-word !important;">void* operator new(size_t, void*);
void operator delete(void*, void*);
new的主要目的是重复使用一大块动态分配的内存来构造不同类型的对象或者它们的数组。 由 new 构造的对象或它们的数组必须通过显式调用它们的析构函数来销毁,并且不得使用。
portant;overflow-wrap: break-word !important;">void main()
{
using namespace std;
char * p = new(nothrow) char [4];
if (p == NULL)
{
cout << "allocte failed" << endl;
exit( -1 );
}
// ...
long * q = new (p) long(1000);
delete []p; // 只释放 p,不要用q释放。
}
p和q只是起始地址相同,构造的对象可以是不同的类型。 “放置”的空间要小于原来的空间,以防发生意外。 当“place new”超出应用范围时,在Debug版本会崩溃,但是可以运行不崩溃!
这个算子的作用是:只要第一次分配成功,就不再担心分配失败。
portant;overflow-wrap: break-word !important;">void main()
{
using namespace std;
char * p = new(nothrow) char [100];
if (p == NULL)
{
cout << "allocte failed" << endl;
exit(-1);
}
long * q1 = new (p) long(100);
// 使用q1 ...
int * q2 = new (p) int[100/sizeof(int)];
// 使用q2 ...
ADT * q3 = new (p) ADT[100/sizeof(ADT)];
// 使用q3 然后释放对象 ...
delete [] p; // 只释放空间,不再析构对象。
}
注意:对于使用该操作符构造的对象或数组,必须显式调用析构函数,不能替换析构函数,因为新对象的大小不再与原来的空间相同。
portant;overflow-wrap: break-word !important;">void main()
{
using namespace std;
char * p = new(nothrow) char [sizeof(ADT)+2];
if (p == NULL)
{
cout << "allocte failed" << endl;
exit(-1);
}
// ...
ADT * q = new (p) ADT;
// ...
// delete q; // 错误
q->ADT::~ADT(); // 显式调用析构函数,仅释放对象
delete [] p; // 最后,再用原指针来释放内存
}
new的主要目的是重复使用申请成功的内存空间。 这样可以避免应用失败的徒劳,避免使用后释放。
特别要注意的是,一定不能调用new,因为这个new只是别人申请的时候用的。 释放内存是new的事情,也就是用原来的指针释放内存。 free/ 不要重复调用,会立即被系统重用,再次free/很可能会释放不属于自己的内存,导致异常甚至崩溃。
上面说了new/比/free做的事情更多。 与new相比,它会做一些额外的初始化工作,而与free相比,它会做更多的清理工作。
portant;overflow-wrap: break-word !important;">class A
{
public:
A()
{
cont<<"A()构造函数被调用"< }
~A()
{
cont<<"~A()构造函数被调用"< }
}
在主函数中,添加如下代码:
portant;overflow-wrap: break-word !important;">A* pa = new A(); //类 A 的构造函数被调用
delete pa; //类 A 的析构函数被调用
可以看出,使用new生成类对象时,系统会调用类的构造函数,使用类对象时,系统会调用类的析构函数。 能够调用构造函数/析构函数意味着new和具有初始化和释放堆分配的内存的能力,而free和free则没有。
2.2 与[]的区别
在c++中,new和[]申请的内存有两种释放方式,两者有什么区别?
我们通常会在课本上看到这样的说明:
portant;overflow-wrap: break-word !important;">int *a = new int[10];
delete a; //方式1
delete[] a; //方式2
对于简单类型,使用new分配的内存空间,无论是数组还是非数组,都可以有两种使用方式:
portant;overflow-wrap: break-word !important;">int *a = new int[10];
delete a;
delete[] a;
本例中的效果是一样的,原因是:在分配类型内存时,内存大小已经确定,系统可以记忆和管理,析构时系统不会调用析构函数。
它可以通过指针直接获取实际分配的内存空间,即使是数组内存空间(在分配过程中,系统会记录分配内存的大小等信息,这些信息存储在结构体中。详见VC安装目录.cpp)。
对于类Class,这两种方法体现了具体的区别
当您通过以下方式分配类对象数组时:
portant;overflow-wrap: break-word !important;">class A
{
private:
char *m_cBuffer;
int m_nLen;
`` public:
A(){ m_cBuffer = new char[m_nLen]; }
~A() { delete [] m_cBuffer; }
};
A *a = new A[10];
delete a; //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
delete[] a; //调用使用类对象的析构函数释放用户自己分配内存空间并且 释放了a指针指向的全部内存空间
所以综上所述,如果ptr代表new申请的内存返回的一个内存空间地址,也就是所谓的指针,那么:
ptr是用来释放内存的,它只用来释放ptr指向的内存。 []rg用于释放rg指向的内存,! ! 还打电话! 对于数组中的每个对象一个一个! !
对于int/char/long/int*/等简单数据类型,由于没有对象,所以和[]一样! 但是如果是C++对象数组就不一样了!
关于new[]和[],有两种情况:
对于(1),上面给出的程序证明了[]和等价。 但是对于(2),情况发生了变化。
我们来看下面的例子,通过例子来学习C++中and[]的使用
portant;overflow-wrap: break-word !important;">#include
using namespace std;
class Babe
{
public:
Babe()
{
cout << \"Create a Babe to talk with me\" << endl;
}
~Babe()
{
cout << \"Babe don\'t Go away,listen to me\" << endl;
}
};
int main()
{
Babe* pbabe = new Babe[3];
delete pbabe;
pbabe = new Babe[3];
delete[] pbabe;
return 0;
}
结果发现:
portant;overflow-wrap: break-word !important;">Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Babe don\'t go away,listen to me
Babe don\'t go away,listen to me
可以看到,只有一个Babe不走,只用的时候给我,三个Babe不走,用[]的时候给我。 但是不管是used还是[],这三个对象在内存中都被删除了,存储位置都标记为可写,但是使用的时候只会调用pbabe[0]的析构函数,而pbabe的析构函数[0] 在对 3 个 Babe 对象使用 [] 析构函数时被调用。
你肯定会问,反正存储空间都释放了,有什么区别。
答:关键在于调用析构函数。 该程序的类不使用操作系统的系统资源(如:、File等),因此不会造成明显的不良影响。 如果你的类使用了操作系统资源,那么简单地将类的对象从内存中删除是不合适的,因为如果不调用对象的析构函数,系统资源是不会被释放的,而这些资源的释放必须依赖于这些类的分析。 构造函数。 因此,在使用这些类生成对象数组时,使用[]释放才是王道。 释放它可能没有问题,也可能后果很严重,这取决于类的代码。
最后祝大家保持良好的代码编写规范,减少严重错误的发生。
- EOF -