看完这篇你还能不懂C语言/C++内存管理?
The following article is from C语言与CPP编程 Author 自称一派123
一、内存
在计算机中,每个应用程序之间的内存是相互独立的,通常情况下应用程序 A 并不能访问应用程序 B,当然一些特殊技巧可以访问,但此文并不详细进行说明。例如在计算机中,一个视频播放程序与一个浏览器程序,它们的内存并不能访问,每个程序所拥有的内存是分区进行管理的。在计算机系统中,运行程序 A 将会在内存中开辟程序 A 的内存区域 1,运行程序 B 将会在内存中开辟程序 B 的内存区域 2,内存区域 1 与内存区域 2 之间逻辑分隔。1.1 内存四区
在程序 A 开辟的内存区域 1 会被分为几个区域,这就是内存四区,内存四区分为栈区、堆区、数据区与代码区。
栈区指的是存储一些临时变量的区域,临时变量包括了局部变量、返回值、参数、返回地址等,当这些变量超出了当前作用域时将会自动弹出。该栈的最大存储是有大小的,该值固定,超过该大小将会造成栈溢出。
堆区指的是一个比较大的内存空间,主要用于对动态内存的分配;在程序开发中一般是开发人员进行分配与释放,若在程序结束时都未释放,系统将会自动进行回收。
数据区指的是主要存放全局变量、常量和静态变量的区域,数据区又可以进行划分,分为全局区与静态区。全局变量与静态变量将会存放至该区域。
代码区就比较好理解了,主要是存储可执行代码,该区域的属性是只读的。
1.2 使用代码证实内存四区的底层结构
由于栈区与堆区的底层结构比较直观的表现,在此使用代码只演示这两个概念。首先查看代码观察栈区的内存地址分配情况:
#include<stdio.h>
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 的地址是 2293324 变量 b 的地址是 2293320,由于 int 的数据大小为 4 所以两者之间间隔为 4;再查看变量 c,我们发现变量 c 的地址为 2293319,与变量 b 的地址 2293324 间隔 1,因为 c 的数据类型为 char,类型大小为 1。在此我们观察发现,明明我创建变量的时候顺序是 a 到 b 再到 c,为什么它们之间的地址不是增加而是减少呢?那是因为栈区的一种数据存储结构为先进后出,如图:
首先栈的顶部为地址的“最小”索引,随后往下依次增大,但是由于堆栈的特殊存储结构,我们将变量 a 先进行存储,那么它的一个索引地址将会是最大的,随后依次减少;第二次存储的值是 b,该值的地址索引比 a 小,由于 int 的数据大小为 4,所以在 a 地址为 2293324 的基础上往上减少 4 为 2293320,在存储 c 的时候为 char,大小为 1,则地址为 2293319。由于 a、b、c 三个变量同属于一个栈内,所以它们地址的索引是连续性的,那如果我创建一个静态变量将会如何?在以上内容中说明了静态变量存储在静态区内,我们现在就来证实一下:
#include<stdio.h>
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 的地址并不存在连续,他们两个的内存地址是分开的。那接下来在此建一个全局变量,通过上述内容得知,全局变量与静态变量都应该存储在静态区,代码如下:
#include<stdio.h>
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);
}
运行结果如下:
#include<stdio.h>
int main()
{
char arr_char[1024*1000000];
arr_char[0] = '0';
}
以上代码定义了一个字符数组 arr_char,并且设置了大小为 1024*1000000,设置该数据是方便查看大小;随后在数组头部进行赋值。运行结果如下:
#include<stdio.h>
#include<string.h>
#include <malloc.h>
int main()
{
char *p1 = (char *)malloc(1024*1000000);
strcpy(p1, "这里是堆区");
printf("%s\n", p1);
}
以上代码中使用了strcpy 往手动开辟的内存空间 p1 中传数据“这里是堆区”,手动开辟空间使用 malloc,传入申请开辟的空间大小 1024*1000000,在栈中那么大的空间必定会造成栈溢出,而堆本身就是大容量,则不会出现该情况。随后输出开辟的内存中内容,运行结果如下:
在此要注意p1是表示开辟的内存空间地址。
二、malloc 和 free
在 C 语言(不是 C++)中,malloc 和 free 是系统提供的函数,成对使用,用于从堆中分配和释放内存。malloc 的全称是 memory allocation 译为“动态内存分配”。
2.1 malloc 和 free 的使用
void *malloc(size_t size);
void free(void *ptr);
free 函数的返回值为 void,没有返回值,接收的参数为使用 malloc 分配的内存空间指针。一个完整的堆内存申请与释放的例子如下:
#include<stdio.h>
#include<string.h>
#include <malloc.h>
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;
}
2.2 内存泄漏与安全使用实例与讲解
#include<string.h>
#include <malloc.h>
void m() {
char *p1;
p1 = malloc(100);
printf("开始对内存进行泄漏...");
}
int main() {
m();
return 0;
}
如上代码中,使用 malloc 申请了 100 个单位的内存空间后,并没有进行释放。假设该 m 函数在当前系统中调用频繁,那将会每次使用都将会造成 100 个单位的内存空间不会释放,久而久之就会造成严重的后果。理应在 p1 使用完毕后添加 free 进行释放:
free(p1);
以下示范一个读取文件时不规范的操作:
#include<stdio.h>
#include<string.h>
#include <malloc.h>
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;
}
以上文件在读取时并没有进行 fclose,这时将会产生多余的内存,可能一次还好,多次会增加成倍的内存,可以使用循环进行调用,之后在任务管理器中可查看该程序运行时所占的内存大小,代码为:
#include<stdio.h>
#include<string.h>
#include <malloc.h>
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;
}
int *p;
*p = val;
包括错误的释放内存空间:
pp=p;
free(p);
free(pp);
释放后使用,产生悬空指针。在申请了动态内存后,使用指针指向了该内存,使用完毕后我们通过 free 函数释放了申请的内存,该内存将会允许其它程序进行申请;但是我们使用过后的动态内存指针依旧指向着该地址,假设其它程序下一秒申请了该区域内的内存地址,并且进行了操作。当我依旧使用已 free 释放后的指针进行下一步的操作时,或者所进行了一个计算,那么将会造成的结果天差地别,或者是其它灾难性后果。所以对于这些指针在生存期结束之后也要置为 null。查看一个示例,由于 free 释放后依旧使用该指针,造成的计算结果天差地别:
#include<stdio.h>
#include<string.h>
#include <malloc.h>
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;
}
三、 new 和 delete
3.1 new 和 delete 使用
指针变量名 = new 类型标识符; 指针变量名 = new 类型标识符(初始值); 指针变量名 = new 类型标识符[内存单元个数];
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete( void *) throw();
plain new 在分配失败的情况下,抛出异常 std::bad_alloc 而不是返回 NULL,因此通过判断返回值是否为 NULL 是徒劳的。
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();
}
}
nothrow new 是不抛出异常的运算符new的形式。nothrow new在失败时,返回NULL。定义如下:
void * operator new(std::size_t, const std::nothrow_t&) throw();
void operator delete(void*) throw();
void func(unsinged long length)
{
unsinged char * p = new(nothrow) unsinged char[length];
// 在使用这种new时要加(nothrow) ,表示不使用异常处理 。
if (p == NULL) // 不抛异常,一定要检查
cout << "allocte failed !";
// ...
delete [] p;
}
placement new 意即“放置”,这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:
void* operator new(size_t, void*);
void operator delete(void*, void*);
palcement new 的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组。placement new构造起来的对象或其数组,要显示的调用他们的析构函数来销毁,千万不要使用delete。
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释放。
}
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; // 只释放空间,不再析构对象。
}
注意:使用该运算符构造的对象或数组,一定要显式调用析构函数,不可用 delete 代替析构,因为 placement new 的对象的大小不再与原空间相同。
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; // 最后,再用原指针来释放内存
}
class A
{
public:
A()
{
cont<<"A()构造函数被调用"<<endl;
}
~A()
{
cont<<"~A()构造函数被调用"<<endl;
}
}
在 main 主函数中,加入如下代码:
A* pa = new A(); //类 A 的构造函数被调用
delete pa; //类 A 的析构函数被调用
2.2 delete 与 delete[] 的区别
delete 释放 new 分配的单个对象指针指向的内存 delete[] 释放 new 分配的对象数组指针指向的内存 那么,按照教科书的理解,我们看下下面的代码:
int *a = new int[10];
delete a; //方式1
delete[] a; //方式2
针对简单类型 使用 new 分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:
int *a = new int[10];
delete a;
delete[] a;
针对类 Class,两种方式体现出具体差异
当你通过下列方式分配一个类对象数组:
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指针指向的全部内存空间
(1) 为基本数据类型分配和回收空间; (2) 为自定义类型分配和回收空间;
#include <iostream>
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;
}
结果是:
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 don’t go away,listen to me
,而使用 delete[] 的时候出现 3 个 Babe don’t go away,listen to me
。不过不管使用 delete 还是 delete[] 那三个对象的在内存中都被删除,既存储位置都标记为可写,但是使用 delete 的时候只调用了 pbabe[0] 的析构函数,而使用了 delete[] 则调用了 3 个 Babe 对象的析构函数。点分享
点点赞
点在看