C++中动态分配对象的内存有着很微妙的地方,下面就来简单说一下:
结论:如果在类中动态分配了内存,那么就应该编写自己的复制构造函数以及赋值运算符,来提供深层次的内存复制。
动态分配对象内存的好处:有时候在程序运行之前,我们无法知道具体需要多少内存空间,比如编写一个类时,不知道类的某个对象需要占多少内存,这个时候我们就需要动态分配对象内存空间了。动态分配内存使我们能够在想要一块内存的时候就去分配一块我们想要的大小的内存空间。
动态分配对象内存需要注意的地方:
1. 释放内存:我们要在不需要动态分配的内存时释放它们,可以在析构函数中进行释放操作。看下面类的定义:
#include "Square.h" /**假如是实现一个类似九宫格的样式, 每个格子都是一个Square对象**/ class Collect{ public: Collect(int width, int height); protected: int mWidth, mHeight; Square ** mSquare; };
Collect类包含了一个Square**变量,原因是Collect对象的样式可能不同(除了9宫格, 还可能是其他样子),因此类的构造函数必须能够根据用户指定的宽度和高度去动态的创建一个二维数组, 构造函数可以如下:
#include "Collect" Collect::Collect(int width, int height):mWidth(width), mHeight(height) { mSquare = new Square * [width]; for (int i=0; i<width; ++i) { mSquare[i] = new Square[height]; } }
析构函数可能如下所示:
Square :: ~Square() { for (int i=0; i<mWidth; ++i) { delete [] mSquare[i]; } delete [] mSquare; mSquare = nullptr; // C++ 11 }
2. 对象复制:
#include "Collect" void printCollect (Collect c) { // do some thing } int main () { Collect c1(4, 5); // 复制c1以初始化c printCollect (c1); return 0; }
Collect 类包含了一个指针变量 mSquare,当执行printCollect (c1) 时,向目标对象c提供了一个mSquare指针的副本,但是没有复制底层的数据,最终导致c和c1都指向了同一块内存空间,如图所示:
collect_1 中的mSquare和 collect_2中的mSquare都指向了同一块堆内存,这将导致的后果:
1. collect_1 修改了mSquare所指的内容,这一改动也会在collect_2中表现出来
2. 当函数printCollect()退出的时候,会调用c的析构函数,从而释放mSquare所指的内存,那么c1就变成了野指针,访问时可能会造成程序崩溃
如果编写了下面代码:
Collect c1(4, 5), c2(4, 2); c1 = c2;
首先c1, c2 中的mSquare指针变量都指向了一块内存空间,当执行c1=c2时,c1中的mSquare指向了c2中mSquare指向的内存,并且在c1中分配的内存将泄漏。这也是在赋值运算符中首先要释放左边引用的内存,然后再进行深层复制的原因。
至此,我们已经了解到,依赖C++默认的复制构造函数或者赋值运算符未必是个好主意。
Collect 的复制构造函数:
// Collect.h class Collect { public: Collect (int width, int height); Collect (const Collect & src); }; // Collect.cpp Collect :: Collect (const Collect & src) { mWidth = src.mWidth; mHeight = src.mHeight; mSquare = new Square * [mWidth]; for (int i=0; i<mWidth; ++i) { mSquare[i] = new Square[mHeight]; } for (int i=0; i<mWidth; ++i) { for (int j=0; j<mHeight; ++j) { mSquare[i][j] = src.mSquare[i][j]; } } }
需要注意的是,复制构造函数复制了所有的数据成员,包括mWidth和mHeight,而不仅仅是指针数据成员。其余的代码对mSquare动态分配的二维数组进行了深层复制。
3. 对象赋值
对象赋值其实就是重载了赋值运算符,看下面代码:
// Collect.h class Collect { public: Collect & operator= (const Collect & rhs); }; /* 当对象进行赋值的时候已经被初始化过了, 所以必须在分配新的内存之前释放动态分配的内存, 可以将赋值运算看成析构函数以及复制构造函数的结合 */ // Collect.cpp Collect & Collect :: operator= (const Collect & rhs) { if (this == &rhs) return *this; for (int i=0; i<mWidth; ++i) { delete [] mSquare[i]; } delete [] mSquare; mSquare = nullptr; mWidth = rhs.mWidth; mHeight = rhs.mHeight; mSquare = new Square * [mWidth]; for (int i=0; i<mWidth; ++i) { mSquare[i] = new Square[mHeight]; } for (int i=0; i<mWidth; ++i) { for (int j=0; j<mHeight; ++j) { mSquare[i][j] = rhs.mSquare[i][j]; } } return *this; }
注意:只要类中动态分配了内存空间,就应该自己编写析构函数、复制构造函数以及重载赋值运算符。
在这里顺便提一句,如果想在const方法中改变对象的某个数据成员,那么可以在成员变量前添加“mutable”关键字,例如:
// Test.h class Test { double mValue; mutable int mNumAccesses = 0; }; // Test.cpp double Test :: getValue () const { mNumAccesses ++; return mValue; }