咳咳,上次写读书笔记居然已经是五个月前的事了,吐槽一下自己的懒_(:з)∠)_正好这两个星期报名参加了公司一个关于C++的培训,再次认识到自己对于基础知识的欠缺,所以还是勤快一些多多学习吧,既是为了自身的成长也是为了不被淘汰。用社长的话来说,“自己的未来靠自己的双手去开拓!”
好的那么这一次我们来学习C++中对象的四大件:构造函数,析构函数,拷贝构造函数以及赋值语句,培训的老师还讲了两个是移动拷贝构造函数和移动赋值语句,不过书里暂时没有涉及所以这里就先不讲了。这些函数可以说是对象不可或缺的组成部分,只要接触对象就不可避免的需要跟它们打交道,所以保证它们被正确的编写,或者说能正确的使用它们是非常重要的。
ITEM 5: KNOW WHAT FUNCTIONS C++ SILENTLY WRITES AND CALLS
这个item很清晰的说明了C++对象的这几个函数的运作方式,如果你定义了一个空的类
class Empty{};
看起来它什么都没有,但是你仍然可以初始化它(也因此可以析构它)、用它构造其他对象和赋值给其他对象,这是因为如果你没有自己显式地定义这些函数,当你写了调用这些函数的代码时,C++编译器会帮你自动生成一个,可能是因为几乎所有的类都会用到这些函数所以比起报错让你自己去编写一个默认的,C++很人性化的帮你处理了,这也是为什么有时候我们感觉不到除了构造函数以外的三个函数,毕竟大家通常都会自己编写构造函数但不一定会写其他的函数。所以下面的代码会触发编译器自动生成相应的函数:
Empty a; // 构造函数、析构函数
Empty b(a); // 拷贝构造函数
b = a; // 赋值语句
对于自动生成的这些函数,默认构造函数和析构函数其实就做了些“幕后”的事,比如调用基类的构造/析构函数以及初始化非静态成员变量等,而拷贝构造函数和赋值语句也就是简单的把源对象的成员拷贝到目标对象中。
而反过来说,如果你自己定义过了这些函数,编译器就不会再为你自动生成这些函数了,你不必担心自己定义的签名和逻辑会因为默认函数的存在而无法生效,但也意味着你必须按照自己定义的方式使用这些函数。
例如我们定义以下的类:
template<typename T>
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};
它定义了两个带两个参数的构造函数,那么初始化这样的对象时就需要相应的传入两个参数。它没有定义拷贝构造函数和赋值语句,因此编译器会自动帮你生成,看下面一段代码:
NameObject<int> no1("Smallest prime number", 2);
NameObject<int> no2(no1);
默认拷贝构造函数会使用no1.nameValue
和no1.objectValue
来初始化no2.nameValue
和no2.objectValue
。nameValue
是std::string
类型,所以会调用它的拷贝构造函数,objectValue
是int
内置类型所以会用位拷贝的方式。默认赋值语句跟拷贝构造函数类似,但是它只有当生成的函数合法且有意义时才能生效,否则编译器就会报错,假设我们作了如下的改动:将nameValue
定义为引用,将objectValue
定义为常量
template<typename T>
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string& nameValue;
const T objectValue;
再考虑如下的代码
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2); // when I originally wrote this, our
// dog Persephone was about to
// have her second birthday
NamedObject<int> s(oldDog, 36); // the family dog Satch (from my
// childhood) would be 36 if she
// were still alive
p = s; // what should happen to
// the data members in p?
p和s的nameValue
都是std::string
类型的引用,分别指向newDog
和oldDog
,这时候把s的nameValue
赋给p会怎样呢——C++规定了引用一旦初始化后就不能再更改地址,所以不可能让p的nameValue
重新引用s的,那难道是对引用的值进行赋值吗?这会导致赋值语句间接对不相干的对象造成了更改,让newDog
变成了“Satch”。所以C++也不知道这个默认的赋值语句应该怎么写:
对于常量成员变量也是一样的道理,C++不知道应该如何处理常量类型的赋值,所以以上两种情况你必须自己定义赋值语句,否则编译器会报错。最后还有一点,如果基类的赋值语句被声明为private
,而派生类中又不定义,编译也是无法通过的,毕竟给派生类赋值时必须保证基类可以work,而这时候它又没法调用基类的函数
总结:
编译器会隐式地帮你生成构造函数、析构函数、拷贝构造函数以及赋值语句
ITEM 6: EXPLICITLY DISALLOW THE USE OF COMPILER-GENERATED FUNCTIONS YOU DO NOT WANT
这个item说的是如何不让编译器帮你自动生成这些函数,比如我有个对象是小熊,她应该是独一无二的,我不希望这个对象有拷贝构造函数和赋值语句,应该怎么做?本来对于一般的函数来说,只要你不声明,那就不会有这个函数,调用的时候就会报错,但现在的情况是如果你不声明,调用的时候编译器自己帮你生成了一个,所以还是能调用。总之,不管你是否声明了拷贝的函数,这个对象都将支持拷贝的功能,怎样才能让它不支持拷贝呢?
作者告诉我们,编译器自动生成的函数都是public
类型,所以只要我们将拷贝构造函数和赋值语句声明为private
不就行了吗?这样既防止了编译器帮我们生成,又防止了别人调用它们。确实如此,但是还差了那么一点,因为你的成员函数和友元还是能调用这些函数,于是聪明的你一定能想到,只要我不去实现它们就可以了,这样一来当有的地方试图调用它们时,就会报link error。事实上,C++标准库中很多地方也用到了这个技巧,来防止对象被拷贝。
运用这个技巧,就可以写出不能被拷贝的小熊了:
class LHE {
public:
...
private:
...
LHE(const LHE&); // declarations only
LHE& operator=(const LHE&);
};
注意下面两个函数的参数名都被省略了,毕竟你既不会实现它们,也没人能调用到它们。最后还有一点可以改进的地方,我们可以将链接错误提前到编译错误,早报晚报都是报,我们当然是希望错了就早点告诉我们错了,省的浪费时间。具体做法就是将函数声明成private
这件事放到一个基类去做,这个类的意义就是防止对象被拷贝,然后让小熊去继承它就可以了。
class Uncopyable {
protected: // allow construction
Uncopyable() {} // and destruction of
~Uncopyable() {} // derived objects...
private:
Uncopyable(const Uncopyable&); // ...but prevent copying
Uncopyable& operator=(const Uncopyable&);
};
class LHE : private Uncopyable { // class no longer
... // declares copy ctor or
}; // copy assign. operator
正如上一节讲的,这里基类的拷贝构造和赋值都是私有的而派生类又没有自己定义,所以别的地方想要调用的话编译器会尝试生成一个默认的并去调用基类的函数,然后发现不能调用于是就会报错,而这正是我们希望看到的结果
总结:
想要防止编译器自动生成某些函数,就将它们定义成私有的并且不要实现它们
ITEM 7: DECLARE DESTRUCTORS VIRTUAL IN POLYMORPHIC BASE CLASSES
面试时也经常可能问到的一道题,这里再巩固一下。在需要应用多态的基类中需要将析构函数定义为虚函数,因为如果一个基类指针指向的是一个派生类对象,而这个基类的析构函数又不是虚函数,那么在析构这个指针对应的对象时,结果是无法预测的。基类的析构函数很可能只释放了基类的数据成员,对于派生类的部分没有进行释放,派生类的析构函数也不会被调用,形成一种“部分析构”的现象,造成内存的泄露。解决方法也很简单,只需要将基类的析构函数定义为虚函数即可,这样在析构时派生类的部分也会被释放。
如果一个类已经有一个虚函数,通常说明它需要运用到多态的特性,那么它也应该有一个虚的析构函数。而如果一个类没有虚函数,它一般就不适合作基类,也就不应该有虚的析构函数,作者举了一个例子来说明这样做的目的,考虑一个二维坐标系的Point类:
class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果一个int
变量有32位,那么一个Point对象可以存放在一个64bit的寄存器中,它也可以用64bit的量被传递到其他语言的函数中(C、FORTRAN)。如果析构函数被定义成虚函数的话,情况就会不一样了
首先我们需要知道虚函数是如何运作的,虚函数的实现需要对象携带信息来表明运行时应该调用哪一个虚函数,这是通过一个虚函数表指针(vptr,virtual table pointer)来实现的,它指向了一个函数指针的数组,这个数组称为虚函数表(virtual table)。每个有虚函数的类都有它自己相关联的虚函数表,于是当对一个对象调用虚函数时,实际调用的函数就通过虚函数表指针找到虚函数表,再从表中找到合适的函数。
知道了这个之后再来考虑析构函数为虚函数的Point对象,由于需要携带虚函数表指针,它所占用的空间也会变大,在32位操作系统中会变成96bit,而在64位操作系统中回变成128bit,仅仅是将析构函数声明为虚函数就使得占用空间增加了50%~100%。而且也不能以相同的方式传递到其他语言中了,因为没有虚函数表指针。
总的来说,将所有析构函数声明成虚函数跟从来不定义成虚函数一样都是错误的,而目前总结的最佳实践就是:当且仅当一个类已经至少有一个虚函数时,才将析构函数声明成虚函数。这里还有一些要注意的事情:不要继承std::string
或者容器类,因为它们没有虚析构函数。
有时候将析构函数声明为纯虚函数是个不错的选择,这样一来这个类就会变成抽象类,也就是无法被实例化的类。当你希望某个类是抽象类,你又没有任何纯虚函数时就可以将它的析构函数声明为纯虚函数,因为抽象类就是应当作为基类使用的,而纯虚函数又能保证它是抽象类,只不过你必须手动给它一个实现。
最后说一点,虚析构函数只适用于多态的场景,对于不涉及多态的基类,我们还是应当将析构函数声明为非虚函数,例如std::string
,STL容器类,以及之前例子中的Uncopyable类,它们虽然是基类,但并不会有多态的特性,也就不需要虚的析构函数。
总结:
1. 有多态性的基类应当将析构函数声明为虚函数。如果一个类有虚函数它也应该有虚析构函数
2. 不被用作基类的类或者没有多态性的基类不应该将析构函数声明为虚函数
ITEM 8: PREVENT EXCEPTIONS FROM LEAVING DESTRUCTORS
这个item主要是讲不要在析构函数中抛出异常,虽然C++允许这么做但是这么做是不好的,例如:
class Widget {
public:
...
~Widget() { ... } // assume this might emit an exception
};
void doSomething()
{
std::vector<Widget> v;
...
} // v is automatically destroyed here
当v
被销毁时,它会负责销毁它所包含的所有Widget
,假设其中一个抛出了异常,剩下的对象还是需要被释放,否则就会造成内存泄露,如果这之中又有某个抛出了异常,就会造成同时有两个异常存在的现象,C++的行为就会无法定义,所以建议不要在析构函数中抛出异常。
那么问题来了,如果我的析构函数确实需要进行某个可能抛出异常然后失败的操作呢
例如我有一个数据库连接类:
class DBConnection {
public:
...
static DBConnection create(); // function to return
// DBConnection objects; params
// omitted for simplicity
void close(); // close connection; throw an
}; // exception if closing fails
为了保证客户端不会忘记关闭连接,通常我们会需要一个资源管理类来管理它,比如DBConnection Manager,这个类负责在自己的析构函数中关闭数据库的连接:
class DBConn { // class to manage DBConnection
public: // objects
...
~DBConn() // make sure database connections
{ // are always closed
db.close();
}
private:
DBConnection db;
};
那么客户端的代码就可以这么写:
{ // open a block
DBConn dbc(DBConnection::create()); // create DBConnection object
// and turn it over to a DBConn
// object to manage
... // use the DBConnection object
// via the DBConn interface
} // at end of block, the DBConn
// object is destroyed, thus
// automatically calling close on
// the DBConnection object
这样一来一出作用域dbc
就会自动被销毁,然后调用析构函数时去关闭数据库的连接。如果关闭的操作能成功那么万事大吉,但是如果抛出了异常,那么DBConn
会往上抛,然后允许它离开这个析构函数。但如我们刚才所说这样做是会带来麻烦的,解决的方法主要有两种:
- 如果抛出异常直接终止程序
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
make log entry that the call to close failed;
std::abort();
}
}
这个做法在发生错误后程序无法正常运行时是合理的,它的好处就是防止造成无法预期的结果
- 忽略抛出的异常
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
make log entry that the call to close failed;
}
}
一般来说这样做是不太好的,因为你隐瞒了某些操作失败了这样一个重要的信息,但是有时候相比于过早的终止程序或者未定义的行为来说,这样做更加可取。当然前提是程序必须保证在遇到异常并且忽略它之后仍然能正常运行。
这两种做法都不是那么尽如人意,因为它们无法在引起异常的第一时间就及时响应。一个更好的解决方案就是修改DBConn
的接口使得客户端可以对此作出响应,比如提供一个close
接口来让客户端有机会处理这个操作造成的异常,并且用一个变量来标识数据库连接是否已经关闭,然后没关闭的话就在析构函数中关闭它
class DBConn {
public:
...
void close() // new function for
{ // client use
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try { // close the connection
db.close(); // if the client didn't
}
catch (...) { // if closing fails,
make log entry that call to close failed; // note that and
... // terminate or swallow
}
}
private:
DBConnection db;
bool closed;
};
虽然把close
的操作从析构函数中拿出来放到客户端主动去调用(析构函数还是有一层备用的close
)造成了不必要的负担,但是如果一个操作可能失败并且需要处理这个异常,那它就应当被放到一个非析构函数中去调用,因为抛出异常的析构函数是危险的(重要的话说三次)。在这个例子中,让客户端主动调用close
并不是加重了它的负担,而是给了它一个处理异常的机会,如果客户端足够自信,它也可以不处理这个异常,就靠DBConn
的析构函数来帮它关闭连接,也是可以的。当然如果那时候close
失败了,DBConn
就会要么终止程序要么忽略异常,而客户端也不能怪它,毕竟是你有这个机会去处理但是你没有珍惜(狗头)
总结:
1. 析构函数不应该抛出异常,如果析构函数中调用了可能抛出异常的函数,它应当捕获这个异常然后决定是终止程序还是忽略它
2. 如果客户端希望响应某个操作中可能抛出的异常,那么类中应该提供一个非析构函数的方法来执行这个操作
ITEM 9: NEVER CALL VIRTUAL FUNCTIONS DURING CONSTRUCTION OR DESTRUCTION
简单来说,不要在构造函数中调用虚函数,因为它不会像你想的那样运行,具体我自己也写了个测试程序:
原因就是创建派生类时,会先调用基类的构造函数,这时候去调用虚函数的话,由于派生类的成员变量还没有初始化,所以如果调用派生类的虚函数而这个函数中又用到了它的成员变量,就会造成无法预测的后果,所以这时候只能调用基类的虚函数,当作派生类的成员变量不存在。不仅仅是虚函数,在这个时候这个对象的类型也只能是基类,一个对象只有当运行了派生类的构造函数后才可能变成派生类对象。
同样的道理,当析构一个对象时,先调用了派生类的析构函数,因此我们认为派生类的数据成员已经被释放了,所以这时候调用虚函数的话也只可能调用基类的虚函数。
最后,既然不能通过虚函数来实现这样的功能,应该怎么做呢?作者给的例子是先保证调用函数不是虚函数,然后在派生类的构造函数的参数中传入一些信息,具体可以参考下面的代码:
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // now a non-
// virtual func
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // now a non-
} // virtual call
class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters )
: Transaction(createLogString(parameters )) // pass log info
{ ... } // to base class
... // constructor
private:
static std::string createLogString( parameters );
};
这样一来创建派生类时打印的信息就会跟基类时不同,注意这里createLogString
被声明为了静态方法,这样就防止了访问未初始化的派生类成员变量的危险
总结:
不要在构造函数或者析构函数中调用虚函数,因为这次调用不会深入到它的派生类中
ITEM 10: HAVE ASSIGNMENT OPERATORS RETURN A REFERENCE TO *THIS
赋值语句一个有趣的地方在于你可以连着写很多个赋值语句,因为它是右结合的:
int x, y, z;
x = y = z = 15; // chain of assignments
等同于
x = (y = (z = 15));
这是因为赋值语句返回了一个左值的引用,当我们自己编写赋值语句时也应当遵循这个规则
class Widget {
public:
...
Widget& operator=(const Widget& rhs) // return type is a reference to
{ // the current class
...
return *this; // return the left-hand object
}
...
};
不仅是赋值语句,对其他的自增自减运算符等也应当这么做:
class Widget {
public:
...
Widget& operator+=(const Widget& rhs // the convention applies to
{ // +=, -=, *=, etc.
...
return *this;
}
Widget& operator=(int rhs) // it applies even if the
{ // operator's parameter type
... // is unconventional
return *this;
}
...
};
这只是一个约定俗成的规律,即使不这么写编译器也不会报错,但是几乎所有的标准库和内置类型都遵循了这一规律,所以照做肯定不会错的
总结:
让赋值语句返回一个对*this的引用
ITEM 11: HANDLE ASSIGNMENT TO SELF IN OPERATOR=
首先我们需要知道赋值给自己是个什么情况:
class Widget { ... };
Widget w;
...
w = w; // assignment to self
简单来说就是赋值语句的左右两边是同一个对象,虽然这看起来很蠢但却是合法的语句,所以在编写赋值语句的时候你应当考虑到这个情况,而这也确实是一个容易疏忽的地方,毕竟我自己写代码有时候就是想当然的写没有考虑那么多。除了上面这种很容易看出来是自赋值的情况以外,还有些不那么容易发现的:
a[i] = a[j]; // potential assignment to self
*px = *py; // potential assignment to self
那么我们来看看不考虑自赋值的话可能会出现什么问题,下面这一段代码看起来是很合理的实现:
class Bitmap { ... };
class Widget {
...
Widget&
Widget::operator=(const Widget& rhs) // unsafe impl. of operator=
{
delete pb; // stop using current bitmap
pb = new Bitmap(*rhs.pb); // start using a copy of rhs's bitmap
return *this; // see Item 10
}
private:
Bitmap *pb; // ptr to a heap-allocated object
};
但是当发生自赋值的情况时,当释放原来的pb
时其实现在的pb
也一起被释放了,于是最后就会变成这个对象的pb
指向了一块被释放的区域。传统的解决方法就是在最前面加一行判断来单独处理自赋值的情况:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; // identity test: if a self-assignment,
// do nothing
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这样做可以解决对自赋值不安全的问题,但其实还存在对异常不安全的问题:假设在调用new Bitmap
的时候发生了异常,创建失败,就会导致pb
指向了一片被释放的区域。通常来说现在在写代码时越来越注重对异常的安全,因为保证了对异常安全时,往往也能顺便保证对自赋值安全。所以对以上的代码,我们其实只需要更改一下语句的顺序就能解决这个问题:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // remember original pb
pb = new Bitmap(*rhs.pb); // point pb to a copy of rhs's bitmap
delete pOrig; // delete the original pb
return *this;
}
也就是先用一个临时的指针存放原来的pb
,试图将它拷贝给现在的对象,最后再释放它,这样即使中间发生了异常,我们的pb
也没有发生变化。而如果是自赋值的情况,就相当于拷贝了自己的值给自己,也是没问题的。你可能会觉得这样做是不是有点浪费,影响效率,你当然也可以在最前面加一个identity test,但是还是应该衡量一下,自赋值发生的概率,毕竟加的这行代码也是会影响效率的,总之应该
综合考虑。
还有一种做法是“拷贝替换”,也就是先交换左右两边的值,然后返回左边:
class Widget {
...
void swap(Widget& rhs); // exchange *this's and rhs's data;
... // see Item 29 for details
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // make a copy of rhs's data
swap(temp); // swap *this's data with the copy's
return *this;
}
根据C++的两个特性(1)复制语句可以使用值传递的方法(2)采用值传递时会自动调用拷贝构造函数
我们可以更简化一些:
Widget& Widget::operator=(Widget rhs) // rhs is a copy of the object
{ // passed in — note pass by val
swap(rhs); // swap *this's data with
// the copy's
return *this;
}
总结:
1. 保证赋值语句在自赋值情况下可以正常工作。技巧包括比较源和目标的地址、仔细安排语句的顺序以及拷贝替换法
2. 保证对多个对象的操作在其中两个或多个是相同对象时仍然可以正常工作
ITEM 12: COPY ALL PARTS OF AN OBJECT
当我们编写我们自己的拷贝函数(拷贝构造函数和复制语句)时,我们需要注意拷贝所有的成员变量,这时候即使你忘了拷贝编译器也是不会提醒你的,比如下面的例子:
void logCall(const std::string& funcName); // make a log entry
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) // copy rhs's data
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name; // copy rhs's data
return *this; // see Item 10
}
目前为止一切顺利,但是如果给Customer
类加上一个成员变量呢:
class Date { ... }; // for dates in time
class Customer {
public:
... // as before
private:
std::string name;
Date lastTransaction;
};
如果不修改原有的复制语句,新加的Date
类型成员就不会被拷贝,编译器甚至不会告诉你你忘了拷贝(谁让你不喜欢我帮你生成的函数,非要自己写呢,哼😕)所以你必须自己记得在所有拷贝函数中给它加上。更隐蔽的情况发生在继承时:
class PriorityCustomer: public Customer { // a derived class
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 assignment operator");
priority = rhs.priority;
return *this;
}
看起来这个派生类的拷贝构造函数已经拷贝了所有的成员变量,但其实它的基类的两个成员变量都忘记了拷贝,这两个变量将通过它们的默认构造函数进行初始化(如果没有,编译器就会报错)。在赋值语句中也是一样的,基类的两个成员变量不会发生变化。因此当你自己编写派生类的拷贝函数时,一定不要忘记拷贝基类的部分,当然基类的很多成员变量可能是私有的,所以你应该主动去调用基类的拷贝函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // invoke base class copy ctor
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // assign base class parts
priority = rhs.priority;
return *this;
}
所以总之就是记住两点:(1)不要漏拷贝任何成员变量(2)记得调用基类的拷贝函数
最后还有一点,在实际工作中,我们通常都会追求代码的复用,减少重复代码,而这两个拷贝函数其实做的事情又差不多,所以你可能会想在某个函数中调用另外一个,这是很不好的一个做法。
(1)在赋值语句中调用拷贝函数❌这意味着你尝试在一个已经存在的对象上新建一个对象,不合理
(2)在拷贝函数中调用赋值语句❌这意味着你尝试把一个值赋给还未存在的对象,不合理,too
总之,不要尝试在一个拷贝函数中调用另一个,如果你希望减少重复,你可以将它们公共的部分提取成一个私有的成员函数,然后让两个拷贝函数都调用它即可。这点其实在工作中我也有用过,比如在写槽函数的时候,我们通常希望用on作为函数名的前缀,但有时候在别的地方我们可能也会需要这个槽函数的逻辑,直接调用这个槽函数也不是不行,但是看着就不合理,毕竟这时候其实没有信号触发它,我们就可以把这段逻辑放到一个私有的函数中,然后在槽函数中调用这个函数即可,既复用了代码又保证了代码的可读性。
总结:
1. 拷贝函数应该保证能拷贝所有的成员,包括它的基类
2. 不要尝试用一个拷贝函数去实现另一个,把公共逻辑放到私有方法中