在C++中动态内存的分配技术可以保证程序在允许过程中按照实际需要申请适量的内存,使用结束后还可以释放,这种在程序运行过程中申请和释放的存储单元也称为堆。 申请和释放过程一般称为建立和删除。
在C++程序中,建立和删除堆对象使用两个运算符:new和delete。
1.运算符new的功能是动态分配内存,或者称为动态创建堆对象,语法形式为:
new 数据类型(初始化列表参数);
该语句在程序运行过程中申请分配用于存放指定类型数据的内存空间,并根据初始化参数列表中给出的值进行初始化。如果内存申请成功,new运算符便返回一个指向新分配内存首地址的类型的指针,可以通过这个指针对该内存空间进行访;如果申请失败,会抛出异常。
(1)对于基本类型的对象
如果建立的对象是一个基本类型的变量,初始化过程就是赋值,例如:
int *p;
p=new int(2);
动态分配了用于存放int型数据的内存空间,并将初值2存入该空间中,然后将首地址赋给指针p。
【注意】对于基本数据类型,如果不希望在内存分配后设定初值,可以把括号省去,例如:
int *p=new int;
如果保留括号,但括号中不写任何数值,则表示用0对该对象初始化,例如:
int *p=new int();
(2)对于类类型的对象
要根据初始化参数列表的参数类型和个数调用该类的构造函数。
在用new建立一个对象时,如果该类存在用户自定义的默认构造函数,则“new T”和“new T()”这两种写法的效果时相同的,都会调用这个默认的构造函数。但若用户没有定义默认的构造函数,使用“new T”创建对象时,会调用系统生成的隐含的默认构造函数;使用“new T()”创建对象时,系统除了执行默认构造函数会执行的操作外,还会为基本数据类型和指针类型的成员用0赋初值,而且这一过程是递归的。也就是说,如果该对象的某个成员对象也没有用户自定义的默认构造函数,那么对该成员的基本数据类型和指针类型的成员,同样会被以0赋初值。
2.运算符delete是用来删除由new建立的对象,释放指针所指向的内存空间。
格式为:
delete 指针名;
如果删除的是对象,该对象的析构函数将被调用。对于new建立的对象,只能用delete进行一次删除操作,如果同一内存空间多次使用delete进行删除将会导致运行错误。
【注意】用new分配的内存,必须用delete加以释放,否则会导致动态内存分配的内存无法回收,使得程序占据的内存越来越大,这叫做“内存泄漏”。
【例】动态创建对象
class Point
{
public:
Point() :x(0), y(0)
{
cout << "调用默认构造函数" << endl;
}
Point(int x, int y) :x(x), y(y)
{
cout << "调用构造函数" << endl;
}
~Point()
{
cout << "调用析构函数" << endl;
}
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY)
{
x = newX;
y = newY;
}
private:
int x, y;
};
int main()
{
cout << "创建第一个对象:" << endl;
Point* p1 = new Point;//动态创建对象,没有给出参数列表,因此调用默认的构造函数
delete p1;//删除对象,自动调用析构函数
cout << "创建第一个对象:" << endl;
Point* p2 = new Point(1,2);//动态创建对象,并给出参数列表,因此调用有参数的构造函数
delete p2;//删除对象,自动调用析构函数
return 0;
}
运行结果:
3.使用运算符new也可以创建数组对象,这时需要给出数组的结构说明。用new运算符动态创建一维数组的语法形式为:
new 类型名[数组长度];
其中数组长度指出了数组元素的个数,它可以是任何能够得到正整数值得表达式。
用new动态创建一维数组时,在方括号后面仍然可以加小括号“()”,但小括号内不能带参数。是否加“()”得区别在于,不加“()”,则对数组每个元素得初始化,与执行“new T”时所进行得初始化方式相同;加“()”,则与执行“new T”所进行初始化得方式相同。例如,如果这样动态生成一个整型数组:
int *p=new int[10]();
则可以方便地为动态创建的数组用()初始化。
如果是用new建立得数组,用delete删除时在指针名前面要加“[]”,格式如下:
delete []指针名;
【例】动态创建对象数组
class Point
{
public:
Point() :x(0), y(0)
{
cout << "调用默认构造函数" << endl;
}
Point(int x, int y) :x(x), y(y)
{
cout << "调用构造函数" << endl;
}
~Point()
{
cout << "调用析构函数" << endl;
}
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY)
{
x = newX;
y = newY;
cout <<"(" <<x <<"," << y<<")" << endl;
}
private:
int x, y;
};
int main()
{
Point* p = new Point[2];
p[0].move(5, 10);
p[1].move(15, 20);
cout << "删除对象:" << endl;
delete[]p;
return 0;
}
运行结果:
这是利用动态内存分配操作实现了数组得动态创建,使得数组元素得个数可以根据运行时得需要而确定。但是建立和删除数组得过程使得程序略显繁琐,更好得方法是将数组得建立和删除封装起来,形成一个动态数组类。
另外,在动态数组类中,通过类得成员函数访问数组元素,可以每次在访问之间检查一下下标是否越界,使得数组下标越界得错误能够及早发现。这种检查,可以通过C++的assert来进行。assert的含义是“断言”,它是标准C++的cassert头文件中定义的一个宏,用来判断一个条件表达式的值是否为true,如果不为true,则程序会中止,并且报错,这样就很容易将错误定位。一个程序一般可以以两种模式编译——调试(debug)模式和发行(release)模式,assert只在调试模式下生效,而在发行模式下不执行任何操作,这样兼顾了调试模式的调试需求和发行模式的效率需求。
【注意】由于assert只在调试模式下生效,一般用assert只是检查程序本身的逻辑错误,而用户的不当输入造成的错误,则应当用其他方式加以处理。
【例】动态数组类
class Point
{
public:
Point() :x(0), y(0)
{
cout << "调用默认构造函数" << endl;
}
Point(int x, int y) :x(x), y(y)
{
cout << "调用构造函数" << endl;
}
~Point()
{
cout << "调用析构函数" << endl;
}
int getX() { return x; }
int getY() { return y; }
void move(int newX, int newY)
{
x = newX;
y = newY;
cout << "(" << x << "," << y << ")" << endl;
}
private:
int x, y;
};
//动态数组类
class Arr
{
public:
Arr(int size) :size(size)
{
points = new Point[size];
}
~Arr()
{
cout << "删除对象:" << endl;
delete[]points;
}
//获得下标为index的数组元素
Point& element(int index)
{
assert(index >= 0 && index < size);//如果数组下标越界,程序中止
return points[index];
}
private:
Point* points;//指向动态数组的首地址
int size;//数组大小
};
int main()
{
int count;
cout << "请输入要创建的对象的个数:";
cin >> count;
Arr points(count);//创建对象数组
points.element(0).move(5,0);//访问数组元素的成员
points.element(1).move(15,20);
return 0;
}
运行结果:
在main函数中,只是建立一个Arr类的对象,对象的初始化参数size指定了数组元素的个数,创建和删除对象数组的过程都由Arr类的构造函数和析构函数完成。