一般来讲,编译器使用三块独立的内存:
1.存储静态变量的内存(关键字static)
2.存储自动变量的内存(关键字auto)
3.动态内存(关键字new)
计算机内存中又分为堆(heap)与栈(stack),前文已经讲过,栈是用于处理自动变量的。而堆就比较厉害了,堆负责动态内存分配。什么是动态内存呢?大部分的变量,在编译后就已经确定了自己在内存的位置;而有些变量则是在程序运行过程中确定自己的位置,也就是用你的时候再分配你,这样的变量叫动态分配变量。
C++中使用new关键字来在堆中分配出一块内存块,使用指针指向该内存块,进而操作该内存块。需要注意的是,在一些不健壮的操作系统中,请求大型内存块可能会导致程序结束后不会被自动释放,造成内存泄漏。因此为了编程规范与程序安全,每一块由new申请的内存都应当手动使用delete关键字进行释放。
OK现在我们知道了:动态内存由关键字new申请;动态内存靠指针操作;动态内存必须要由delete关键字释放。知道这三点,使用动态分配就比较简单了。但是在类对象的动态分配中,由于构造函数与析构函数的介入等,情况变得更加复杂。
在工程的头文件中定义了如下一个类:
#include <iostream>
#include <string>
#ifndef NEWTEST_H_
#define NEWTEST_H_
using namespace std;
namespace NEWTEST
{
class Newtest
{
private:
string words;//字符串数据变量
int number;//整形数据变量
public:
Newtest(const string &s,int n);//构造函数
~Newtest();//析构函数
void show() const;//显示函数
};
}
#endif
研究的对象是如何动态分配与释放类,因此该类的设计比较简单。类中仅有两个隐藏的数据变量,公共接口只有构造函数、析构函数和一个显示对象数据的函数。
按照编程规范,在另一个cpp文件中对类的成员函数进行定义:
#include <iostream>
#include <string>
#include "newTest.h"
namespace NEWTEST
{
Newtest::Newtest(const string &s,int n)
{
words=s;
number=n;
cout<<words<<" object created"<<endl;
}
Newtest::~Newtest()
{
cout<<words<<" object destroyed"<<endl;
}
void Newtest::show() const
{
cout<<"words: "<<words;
cout<<" number:"<<number<<endl;
}
}
在主程序之前,需要知道new和delete的一些用法。
new有两种主要的用法:普通new运算符与定位new运算符。普通的运算符是较为常见的用法,即不关心变量到底被分配到了哪里,我只是单纯的想分配一块内存而已。如下所示:
int *p1;
p1=new int;//p1指向一块动态内存,用于存储一个整形变量
定位new运算符,顾名思义,程序员需要指定该内存被分配的位置,格式如下:
char buffer[128];//定义了一块128字节的内存块,也称为内存池
int *p1;
p1=new (buffer) int;//p1指向一块动态内存,用于存储一个整形变量,该整形变量的地址就是内存池buffer的首地址
比较常见的搭配是,先使用动态分配分配出一块内存池,再利用内存池的地址来为动态分配的变量指定位置。
关键字delete用法比较简单,原则只有一个,就是格式必须与对应的new格式相同:
char *str1=new char[5];
delete [] str1;//创建用new [],释放用delete []
char *str2=new char;
delete str2;//创建用new,释放用delete
此外,在类对象的处理中,使用普通new运算符分配动态内存给对象会自动调用构造函数,使用delete会自动调用析构函数。普通new运算符创建对象和释放内存的语句如下:(把主程序拆了拆)
p2=new Newtest("number one",20);//普通new创建对象,自动调用构造函数进行初始化
delete p2;//删除p2,自动调用析构函数
麻烦的是定位new运算符。delete运算符可以和普通new运算符搭配,但是不能与定位new运算符搭配。原因是定位new运算符本质上没有开辟新内存,定位new运算符是基于开辟了一块内存池,才得以实现的。也就是说指向定位new运算符创建的对象的指针并没有收到new运算符返回的地址,因此使用delete操作会产生错误。
该问题的解决方案是:针对定位new运算符创建的对象,显示调用析构函数,这也是整个C++中需要显示调用析构函数的罕见情况之一。
定位new创建对象和释放内存的语句如下:
p1=new (buffer) Newtest("C++ is",10);//定位new创建对象,自动调用构造函数进行初始化
p1->~Newtest();//显式调用析构函数
主程序如下:
#include <iostream>
#include <new>
#include <string>
#include "newTest.h"
using namespace NEWTEST;
const int BUFF=512;
int main()
{
char *buffer=new char[BUFF];//分配一块内存区512字节
Newtest *p1,*p2,*p3;
p1=new (buffer) Newtest("C++ is",10);//定位new
p2=new Newtest("number one",20);//普通new
p3=new (buffer+sizeof(Newtest)) Newtest("without a doubt!",30);//定位new
cout<<"address of buffer:"<<endl<<(void*)buffer<<endl;//打印内存区的首地址
cout<<"memory contents:"<<endl;
cout<<p1<<": ";//显示p1地址
p1->show();//显示p1内容
cout<<p2<<": ";//显示p2地址
p2->show();
cout<<p3<<": ";//显示p3地址
p3->show();
delete p2;//删除p2
p3->~Newtest();//删除p3
p1->~Newtest();//删除p1 定位new删除顺序与创建顺序相反
delete [] buffer;//删除内存区
cout<<"program ends!"<<endl;
cin.get();
return 0;
}
程序运行结果如下:
主程序中p2指向一个普通new创建的内存,p1和p3指向两个定位new创建的内存,p3指向的内存相对p1偏移量为一个Newtest类的大小。
需要注意一点,定位new创建对象的删除顺序应该与创建顺序相反,因为晚创建的对象有可能依赖于早创建的对象。此外,当所有对象的内存被释放后,不要忘记释放内存池所占的内存空间。