在翻阅《Effective C++》第三版,记录一下读到的第二章部分。
p34有一段阐述了这样一个现象"什么时候empty class(空类)不再是个empty class 呢?当C++处理过它之后。是的,如果你自己没声明,编译器就会为它声明(编译器版本的)一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline"
编译器生成的默认copy构造函数和默认copy assignment操作符效果如下
#include <iostream>
class apple {
public:
apple(int num):height(num) {
};
void showHeight() {
std::cout << "当前苹果的重量是" << height << std::endl;
}
private:
int height;
};
int main()
{
apple a(10);
apple b(20);
b.showHeight();
b = a;
b.showHeight();
apple c(a);
c.showHeight();
return 0;
}
程序输出结果如下
接下来在类中添加私有指针成员变量,测试编译器自动生成的函数
#include <iostream>
#include <string>
class color {
public:
color(std::string _str) : _color(_str) {};
std::string showColor() {
return _color;
};
private:
std::string _color;
};
class apple {
public:
apple(int num,color* _color):height(num),theColor(_color) {
};
~apple() {
delete theColor;
}
void showHeight() {
std::cout << "当前苹果的重量是" << height << std::endl;
}
void showColor() {
std::cout << "水果颜色是" << theColor->showColor() << std::endl;
}
void showAddr() {
std::cout << "元素位置是" << theColor << std::endl;
}
private:
int height;
color *theColor;
};
int main()
{
color *redColor = new color("红色");
apple firstApple(10, redColor);
firstApple.showHeight();
firstApple.showColor();
firstApple.showAddr();
std::cout << std::endl;
color* greenColor = new color("绿色");
apple secendApple(20, greenColor);
secendApple.showHeight();
secendApple.showColor();
secendApple.showAddr();
std::cout << std::endl;
secendApple = firstApple;
//delete redColor; //删除设置的颜色
std::cout << "使用默认copy重载运算符后输出信息为" << std::endl;
firstApple.~apple();
std::cout << std::endl;
secendApple.showHeight();
secendApple.showColor();
secendApple.showAddr();
return 0;
}
上面这段程序运行的时候会报错,因为firstApple和secendApple使用的是同一个颜色对象。即两个类里的指针指向了同一块内存。firstApple析构的时候会析构私有变量中的颜色对象,所以secendApple内指向颜色color对象的指针成了悬挂指针。所以会报错。
如果不使用裸指针,改为使用智能指针会发生什么呢?
代码如下:
#include <iostream>
#include <string>
#include <memory>
class color {
public:
color(std::string _str) : _color(_str) {};
//显示颜色
std::string showColor() {
return _color;
};
//改变颜色
void changeColor(std::string str) {
_color = str;
}
private:
std::string _color;
};
class apple {
public:
apple(int num,std::shared_ptr<color>& _color):height(num),theColor( _color) {
};
~apple() {
}
void showHeight() {
std::cout << "当前苹果的重量是" << height << std::endl;
}
void showColor() {
std::cout << "水果颜色是" << theColor->showColor() << std::endl;
}
void showAddr() {
std::cout << "元素位置是" << theColor << std::endl;
}
//改变颜色
void changeColor(std::string str) {
theColor->changeColor(str);
}
private:
int height;
std::shared_ptr<color> theColor;
};
int main()
{
std::shared_ptr<color> redColor = std::make_shared<color>("红色");
apple firstApple(10, redColor);
firstApple.showHeight();
firstApple.showColor();
firstApple.showAddr();
std::cout << std::endl;
std::shared_ptr<color> greenColor = std::make_shared<color>("绿色");
apple secendApple(20, greenColor);
secendApple.showHeight();
secendApple.showColor();
secendApple.showAddr();
std::cout << std::endl;
secendApple = firstApple;
std::cout << "红色的引用次数" << redColor.use_count() << std::endl;
secendApple.showHeight();
secendApple.showColor();
secendApple.showAddr();
std::cout << "\n" << std::endl;
firstApple.changeColor("褐色"); //修改苹果一的颜色,观察苹果二的颜色
secendApple.showColor();
return 0;
}
将裸指针改为使用智能指针后,程序不在出现悬挂指针现象。但是观察指针指向的地址,为同一地址。现测试,先在给类添加更改颜色功能,更改第一个苹果颜色为褐色,然后观察第二个苹果的颜色。程序输出如下。
结果显示苹果2的颜色随着苹果1颜色的改变而改变了,观察程序输出,是两个类中的智能指针地址相同,指向同一块内存,修改一个对象,会影响另一个对象得到结果。所以使用智能指针虽然编译器不再报错,但是并没有解决上面的问题。所以类的成员变量中如果存在动态分配资源的指针和引用时,还是需要重写copy构造函数和重载运算符操作。
重载运算符版本如下。
#include <iostream>
#include <string>
#include <memory>
class color {
public:
color(std::string _str) : _color(_str) {};
//显示颜色
std::string showColor() {
return _color;
};
//改变颜色
void changeColor(std::string str) {
_color = str;
}
private:
std::string _color;
};
class apple {
public:
apple(int num,std::shared_ptr<color>& _color):height(num),theColor( _color) {
};
~apple() {
};
apple(const apple& other) {
this->height = other.height;
this->theColor = std::make_shared<color>(*(other.theColor.get()));
};
apple& operator = (const apple& other)
{
if (this != &other)
{
height = other.height;
theColor = std::make_shared<color>(*(other.theColor.get()));
}
return *this;
}
void showHeight() {
std::cout << "当前苹果的重量是" << height << std::endl;
}
void showColor() {
std::cout << "水果颜色是" << theColor->showColor() << std::endl;
}
void showAddr() {
std::cout << "元素位置是" << theColor << std::endl;
}
//改变颜色
void changeColor(std::string str) {
theColor->changeColor(str);
}
private:
int height;
std::shared_ptr<color> theColor;
};
int main()
{
std::shared_ptr<color> redColor = std::make_shared<color>("红色");
apple firstApple(10, redColor);
firstApple.showHeight();
firstApple.showColor();
firstApple.showAddr();
std::cout << std::endl;
std::shared_ptr<color> greenColor = std::make_shared<color>("绿色");
apple secendApple(20, greenColor);
secendApple.showHeight();
secendApple.showColor();
secendApple.showAddr();
std::cout << std::endl;
secendApple = firstApple;
apple thridApple(firstApple);
std::cout << "红色的引用次数" << redColor.use_count() << std::endl;
secendApple.showHeight();
secendApple.showColor();
secendApple.showAddr();
std::cout << "\n" << std::endl;
firstApple.changeColor("褐色"); //修改苹果一的颜色,观察苹果二的颜色
secendApple.showColor(); //测试重载运算符
thridApple.showColor(); //测试重写copy构造函数
return 0;
}
代码运行结果如下:
观察结果发现指针指向的地址已经不同了,即编译器自动实现的版本时浅拷贝,这里实现了深拷贝。同时通过测试重载运算符和copy构造函数均正常运行。
重载运算符时因为又连续赋值的情况,即 a = b = c; 所以需要传出*this,即指针的解引用。同时上面代码中实现重载运算符时,进行了自我赋值判断。进行自我赋值判断,可以有效防止自我赋值导致的资源开销,这在管理动态内存,文件句柄,网络连接时开销会很大。
但是另一方面,在类使用的过程中会经常使用到自我赋值吗?如果不使用自我赋值,则每次调用时均要多支付一次if判断的额外开销。所以自我赋值的开销如果不大的话,可以省略这里的自我赋值判断。省略时如果没有裸指针,可直接赋值,有裸指针,先声明一个临时指针指向类本身的内存地址,然后然后将类本身的内存地址指向根据传入对象生成的新地址,然后删除临时指针指向的内存地址。代码如下
widget& widght::operator=(const widget& rhs)
{
string *pTemp = pb; //记住原先的pb
pb = new string(rhs.pb); //令pb指向pb的一个附件
delete pTemp; //删除原先的pb
return *this;
}
上面这段代码是《Effective C++》第三版p55中一段代码。delete要放在后面,为了放置new时抛出异常,直接跳过。放在前面会导致不仅没有赋值成功,还删除了原先的资源。所以前面会引入一个临时指针指向需要被删除的资源。我认为加不加这个判断,根据自己实际情况来吧。因为if判断的开销对现代计算机来说也不会很大,自我赋值有时消耗资源很大。
最后还有一点是跟类继承相关的。在《C++ Primer Plus》 P421有这样一段描述这个现象。"继承是怎样与动态内存分配(使用new 和delete进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性。如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧。下面来看看这两种情况。"
两种情况分别是在基类使用类的动态内存分配的情况下,派生类是否使用了动态内存分配。已知基类使用了动态内存分配,即基类重新定义了复制构造函数和重载运算符。
情况一,派生类不使用动态内存分配,即派生类使用编译器自动生成的默认复制构造函数和默认重载运算符即可。因为其会自动调用基类重写的复制构造函数和重载运算符。
情况二,派生类使用了动态内存分配,则需要重写派生类的复制构造函数和重载运算符。重载的时候需要使用手动调用基类的复制构造函数的重载运算符,完成基类中资源的复制。
代码如下
//基类是动态分配资源,且实现了重载拷贝构造函数和运算符
//此时如果派生类也使用了动态分配资源。则应重写拷贝构造函数
//如果不是动态分配资源,则不需要重写,因为编译器自动生成的会根据情况自动调用基类的拷贝构造函数和重载赋值运算符
#include <iostream>
#include <cstring>
class BaseClass {
public:
BaseClass(const std::string& _label = "null"){
label = new std::string(_label);
};
//复制构造函数
BaseClass(const BaseClass& other)
{
label = new std::string(*other.label);
}
BaseClass& operator = (const BaseClass& other)
{
if (this != &other)
{
std::string* tempStr = label;
label = new std::string(*other.label);
delete tempStr;
}
return *this;
}
virtual ~BaseClass() {
delete label;
}
void baseShow()
{
std::cout <<"标签是 " << *label << std::endl;
}
private:
std::string* label;
};
class DrivedClass :public BaseClass{
public:
DrivedClass(const std::string& _name = "nullName", const std::string& _label = "null") :BaseClass(_label) {
name = new std::string(_name);
};
DrivedClass(const DrivedClass& other):BaseClass(other)
{
name = new std::string(*other.name);
}
DrivedClass& operator=(const DrivedClass& other)
{
if (this != &other)
{
BaseClass::operator=(other);
name = new std::string(*other.name);
}
return *this;
}
virtual ~DrivedClass() {
delete name;
}
void drivedShow() {
std::cout << "名字是 " << *name << std::endl;
}
private:
std::string* name;
};
int main()
{
DrivedClass test1("苹果", "红富士");
DrivedClass test2;
test2 = test1;
test2.baseShow();
test2.drivedShow();
return 0;
}
在main函数中测试实例化test1对象,然后通过重载的=运算符实例化test2对象,之后调用test对象的成员函数观察基类的输出,如果不是null,则表明复制成功。
输出结果如下
注释注释掉BaseClass::operator=(other);后程序输出如下
测试结果表明,当处于第二种情况时要主动调用基类重写的复制函数。
还有一些细节,比如实例化DrivedClass test2 = test1; 和另一种写法DrivedClass test2; test2 = test1;这两种写法前者调用复制构造函数,后者调用重载运算符。