Item05: 了解C++默默编写并调用哪些函数
-
编译器会自动为空类empty class声明一个copy构造函数、一个copy assignment操作符和一个析构函数。此外,若无声明任何构造函数,编译器也会自动声明一个default构造函数;若已声明构造函数,则编译器不会再声明一个default构造函数。
class Empty {};
-
编译后相当于: class Empty {
public:
Empty() {...}
Empty(const Empty& rhs) {...}
~Empty() {...}
Empty& operator=(const Empty& rhs) {...}
}
Item06: 若不想使用编译器自动生成的函数,就该明确拒绝
-
上述已述,编译器会自动为class生成copy assignment和copy构造函数(如果程序员并未直接声明的话)。但是某些情况,并不需要编译器自动生成的函数。例如HomeForSale类不应有copy assignment和copy构造函数: HomeForSale h3(h1); // 不予编译 HomeForSale h3 = h2; // 不予编译 -
解决办法:为HomeForSale设计一个专门为了阻止copying动作而设计的base class内。【其实也可以在HomeForSale类的private函数声明copy assignment和copy构造函数,以阻止编译器自动生成,来阻止这两个操作】。 class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
}
class HomeForSale: private Uncopyable {
...
}
任何对象,甚至member函数、friend函数尝试copy HomeForSale对象,编译器便试着生成一个copy构造函数和copy assignment操作符。这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,那些调用会被编译器拒绝,因为其base class的拷贝函数是private。
补充:public、protected、private继承的区别 (1)public、protected、private访问标号的访问范围 -public:可以被1)该类中的函数;2)子类的函数;3)友元函数;4)该类的对象访问; -protected:可以被1)该类中的函数;2)子类的函数;3)友元函数【包括设为友元的非成员函数、设为友元的其他类的成员函数、设为友元类的所有成员函数】访问; -private:可以被1)该类中的函数;2)其友元函数访问。 (2)类的继承后方法属性变化 -private属性不能够被继承。 -使用private继承(默认为private继承),父类的所有属性在子类中变为private; -使用protected继承,父类的public和protected属性在子类中变为protected; -使用public继承,父类的所有属性不发生改变。
- 请记住:
- 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
Item07:为多态基类声明virtual析构函数
引用1:https://blog.csdn.net/qq_43118572/article/details/112553344 引用2:https://www.cnblogs.com/liushui-sky/p/5824919.html
尽管派生类中定义了析构函数来释放其申请的资源,但是并没有得到调用。原因是基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏。
- 请记住:
- polymorphic(带多态性质的)基类应该声明一个virtual 析构函数。如果class 带有任何virtual函数,它就应该拥有一个virtual析构函数。
- 反过来,如果Classes的设计目的不是作为base classes使用,或不是为了具备多态性,就不应该声明virtual析构函数。
Item08:别让异常逃离析构函数
为了安全,”析构函数尽可能的不要抛出异常“。如果非抛不可,语言也提供了方法,就是自己的异常,自己给吃掉。但是这种方法不提倡,我们提倡有错早点报出来(参考:https://www.cnblogs.com/zhyg6516/archive/2011/03/08/1977007.html)
class DBConn {
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try {
db.close();
}
catch(...) {
}
}
}
private:
DBConnection db;
bool closed;
}
- 请记住:
- 析构函数千万不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。
Itme09:绝不在构造和析构过程中调用virtual函数
-
在base class 构造/析构 期间,virtual函数不是virtual函数。【即在base class构造/析构期间,调用的virtual函数依然是base class的函数】 -
Example: class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
...
};
Transaction::Transaction()
{
...
logTransaction();
}
class BuyTransaction: public Transaction {
public:
virtual void logTransaction() const;
...
}
class SellTransaction: public Transaction {
public:
virtual void logTransaction() const;
...
}
BuyTransaction b;
-
当执行构造函数BuyTransaction b时,首先Transaction构造函数会被更早调用。Transaction构造函数的最后一行调用virtual函数logTransaction,而它调用的是Transaction【基类】的logTransaction版本,而不是BuyTransaction 【子类】 的版本。原因:子类BuyTransaction尚未初始化,所以面对它们,最安全的做法是视它们不存在。 -
析构函数:一旦子类析构函数开始执行,对象内的子类成员变量便呈现未定义值。同理。
-
解决办法:如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?一种做法是在class Transaction内将logTransaction函数改为non-virtual,然后要求子类构造函数传递必要信息给基类Transaction构造函数。 class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
}
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo);
}
class BuyTransaction: public Transaction {
public:
BuyTransaction(params) : Transaction(createLogString(params)) {...}
...
private:
static std::string createLogString(params);
}
-
Note:1)explicit的使用: google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换,缺点无。所以google约定所有单参数的构造函数都必须是显示的。2)static的使用: 避免子类构造期间,意外指向“初期未成熟之BuyTransaction对象内尚未初始化的成员变量”。
Item10:令operator=返回一个reference to *this
- 这是个共同遵守的协议,并无强制性。 【令赋值操作符返回一个reference to *this】
class Widget {
public:
...
Widget& operator=(const Widget& rhs)
{
...
return *this;
}
Widget& operator+=(const Widget& rhs)
{
...
return *this;
}
}
Item11:在operator=中处理“自我赋值”
-
C++禁止对象“自我赋值”,但不禁止以by value方式传递一份副本
a[i] = a[j];
*px = *py;
- 这里的不安全是若pb指向rhs.pb,那么delete pb会导致rhs.pb也被删除,而导致pb = new Bitmap(*rhs.pb)会报错。
class Bitmap { ... };
class Widget {
...
private:
Bitmap* pb;
}
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
- 欲解决这种错误,在operator=前面进行“证同测试”,达到“自我赋值的检验目的”:【但它不具备异常安全性:如果"new Bitmap"导致异常,Widget最终会持有一个指针指向一块被删除的bitmap】
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
- 让operator=具备“异常安全性”往往会自动获得“自我赋值安全”的回报。因此,一个更好的解决办法是,生成一个副本,然后赋值,最后删除这个副本:【现在即使new Bitmap产生异常,pb也不至于指向null】
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this;
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
- 也可以使用copy and swap技术:【by value的传递方式避免了自我赋值】,但它牺牲了代码的清晰性。
class Widget {
...
void swap(Widget& rhs);
}
Widget& Widget::operator=(const Widget& this)
{
Widget temp(rhs);
swap(temp);
return *this;
}
Item12:复制对象时别忘其每一个成分
- 最常见的错误就是:只复制了子类声明的成员变量,而忘记复制其继承父类的成员变量:
class PriorityCustomer: public Customer {
public:
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
int priority;
}
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer::PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy constructor");
priority = rhs.priority;
return *this;
}
-
任何时候只要你写子类的copying函数,必须很小心地复制其base class成分。那些成分往往是private,所以你无法直接访问它们,应该用子类的copying函数调用相应的base class函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer::PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy constructor");
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
|