条款 5 了解C++ 默默编写并调用哪些函数
class Empty { };
1、编译器会默认为空类声明copy构造函数、default构造函数、析构函数、copy assignment操作符。相当于:
class Empty {
public:
Empty() {...}//default构造函数
Empty(const Empty& rhs) {...}//copy构造函数
~Empty() {...} //析构函数。编译器产出的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
Empty& operator=(const Empty& rhs) {...}//copy assignment操作符
};
2、如果自己声明了构造函数,编译器会选择自己构造的那个构造函数,而非默认的。
template<typename T>
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(const string& name, const T& value);
...
private:
string nameValue;//两个参数
T objectValue;
};
更改了copy 构造函数。使用这个版本而非默认版本。
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);
copy构造函数的调用过程:编译器生成的copy构造函数会以no1.nameValue和no1.objectValue为初值来设定no2.nameValue和no2.objectValue。
3、赋值操作符operator=自己定义
template<typename T>
class NamedObject {
public:
NamedObject(string& name, const T& value);
...
private:
string &nameValue;
const T objectValue;
};
string newDog("Persephone");
string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s;//错误!!!
C++编译器会拒绝编译那一行赋值操作!因为默认情况下reference不能指向不同的对象!同样更改const是不合法的!
解决办法:自己定义copy assignment操作符
#include <iostream>
using namespace std;
class Nameobject{
public:
Nameobject(string& name);//声明copy构造函数
Nameobject& operator=(const Nameobject& rhs);//声明成员函数operator=
private:
string& namevalue;
};
Nameobject::Nameobject(string& name) :namevalue(name){}//定义copy构造函数
Nameobject& Nameobject::operator=(const Nameobject& rhs)//定义赋值构造函数,前面的Nameobject&类似于int,是operator=返回的类型
{
namevalue = rhs.namevalue;
return *this;
}
int main(){
string newDog("ppppp");
string oldDog("sssss");
Nameobject p(newDog);
Nameobject s(oldDog);
p = s;
return 0;
}
条款6 若不想使用编译器自动生成的函数,要明确拒绝
总结:
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private,并且只声明不定义。
使用像Uncopyable这样的base class也是一种做法。
引出原因:当你想阻止对象copying时,他会使用默认生成的构造函数和赋值操作符起作用,这是我们不想用的。
class HomeForsale {};
HomeForsale h1;
HomeForsale h2;
HomeForsale h3(h2);//使用了默认构造函数,错误!!
h1=h2//使用了默认赋值操作符,错误!!
方法一:把copy构造函数跟copy assignment操作符在private中声明,并且不要定义!
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&); //只有声明,不能定义!
};
这样的话,当用户企图拷贝HomeForSale对象的时候,编译器会报错!因为他们没法调用private函数。
方法二:我们设计一个专门为了阻止copying动作而设计的base 类:class Uncopyable
class Uncopyable {
public:
Uncopyable() { }
~Uncopyable() { }
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable {
... //这个时候该class就不要再声明copy构造函数或copy assignment操作符了!
}
尝试拷贝HomeForSale对象的时候,编译器就是试着生成HomeForSale的copy构造函数跟copy assignment操作符!这个时候就要先调用base class的copy构造函数跟copy assignment操作符!这时候编译器会拒绝这样的调用!因为其base class的拷贝函数是private!
备注:
boost::noncopyable 的基本思想是把构造函数和析构函数设置protected权限,这样子类可以调用,但是外面的类不能调用,那么当子类需要定义构造函数的时候不至于通不过编译。但是最关键的是noncopyable把复制构造函数和复制赋值函数做成了private,这就意味着除非子类定义自己的copy构造和赋值函数,否则在子类没有定义的情况下,外面的调用者是不能够通过赋值和copy构造等手段来产生一个新的子类对象的。
条款7 为多态基类声明virtual析构函数
总结:
1)polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。
如果class带有任何virtual函数,它也应该拥有一个virtual析构函数!
(2)Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性!就不该声明virtual析构函数。
1、为多态基类声明虚析构函数
引出原因:non-virtual析构函数,derived class对象被删除,通常其bass class被销毁,但是derived class 存留了下来。
#include <iostream>
using namespace std;
class A
{
public:
A(){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
//virtual ~A(){ cout << "~A()" << endl; }//虚析构函数
};
class B :public A
{
public:
B(){ cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
};
int main(){
A *pa = new B;//new一个B类对象,
delete pa;
return 0;
}
问题出现在:基类A的指针指向了派生类B的对象,A的析构函数调用了,但是派生类B的析构函数没有被调用。于是这是一个诡异的“局部销毁”的问题,会导致内存泄露,败坏数据结构,在调试时浪费时间。
解决办法:为基类添加一个virtual析构函数,这样,通过基类指针销毁派生类对象就会将会调用派生类的构造函数,那么会将整个对象销毁。
virtual ~A(){ cout << "~A()" << endl; }
顺序:
构造函数:先Base再Derived;析构函数:先Derived再Base。
2、如果类目的不是作为基类或不是为了具备多态性,就不要声明虚析构函数
但并不是所有基类设计的目的都是为了多态用途。如表中string和STL容器都不是设计作为基类使用,更不要提多态了。因此他们不需要虚析构函数。如果你试图继承一个没有任何虚析构函数的类,包括STL容器如vector,list,unordered_map等,容易导致错误
3、纯虚析构函数
为你希望成为抽象的那个class声明一个pure virtual 析构函数。除了声明,还要定义。
class A{
public:
virtual ~A()=0; // 声明纯虚析构函数
}
A::~A() {} // 定义纯虚析构函数
条款9 绝不在构造和析构过程中调用virtual函数
总结:
在构造和析构期间不要调用virtual函数,因为它会停留在base class,而不会下降至derived class。
虚函数virtual:
在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,以共同的方法,但因个体差异,而采用不同的策略。
虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;
实现多态性,通过指向派生类的基类指针或引用(两个都是指向基类的指针)!!!,访问派生类中同名覆盖成员函数。
#include<iostream>
using namespace std;
class A
{
public:
void print()
{
cout<<"This is A"<<endl;
}
};
class B : public A
{
public:
void print()
{
cout<<"This is B"<<endl;
}
};
int main()
{
//为了在以后便于区分,我这段main()代码叫做main1
A a;
B b;
a.print();
b.print();
return 0;
}
分别是“ThisisA”、“ThisisB”。但这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
现在把main()处的代码改一改,保证指针类型相同。运行一下看看结果,结果却是两个This is A(错)。
int main()
{
//main2
A a;
B b;
A *p1 = &a;//改为相同类型指针A*
A *p2 = &b;//之前相当于B *p2=&b
p1->print();
p2->print();
return 0;
}
应该更改为基类成员函数为virtual虚函数。
class A
{
public:
virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
public:
void print(){cout<<"This is B"<<endl;}
};
现在重新运行main2的代码,这样输出的结果就是This is A和This is B了。
1、构造函数中出现的virtual成员函数,在调用成员函数时仍然是Base class内的版本
我们原本认为构造子类对象时,如果在父类的构造函数中调用虚函数就会调用子类的虚函数,然而编译器会调用父类的虚函数
编译器对这种在父类中调用虚函数的做法的一个解决方案是:继承类对象的基类对象构造期间对象的类型是基类而不是继承类。
合理的解释: 在基类构造函数执行时,继承类的成员变量尚未初始化,如果使用这些未初始化的成员将导致不明确行为。
class Transaction { //所有交易的base class
public:
Transaction();
virtual void logTransaction() const = 0; //做出一份因类型不同而不同的日志记录(log entry)
...
};
Transaction::Transaction() //base class构造函数之实现
{
...
logTransaction(); //最后动作是志记这笔交易
}
class BuyTransaction: public Transaction { //derived class
public:
virtual void logTransaction() const; //志记(log)此型交易
...
};
class SellTransaction: public Transaction { //derived class
public:
virtual void logTransaction() const; //志记(log)此型交易
...
};
现在执行以下语句:
BuyTransaction b;
Transaction的构造函数最后一行调用logTransaction();这时候调用的logTransaction();是Transaction版本(基类Bass)而非BuyTransaction(派生类Derived class)。
2、将 Transaction (基类)中的 logTransaction 转变为一个 non-virtual函数,然后要求派生类构造函数将必要的信息传递给 Transaction 构造函数,而后那个函数就可以安全地调用 non-virtual的 logTransaction。
class Transatcion {
public:
explicit Transaction(cosnt std::string& logInfo);
void logTransaction(const std::string& logInfo) const; //non-virtual
};
Transaction::Transaction(const std::string& logInfo) {
logTransaction(logInfo); //non-virtual调用
}
class BuyTransaction:public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters)) {} //将log信息传递给基类构造函数
private:
static std::string createLogString(parameters);
};
这句话,令派生类BuyTransaction将必要的构造信息向上传递至基类构造函数Transaction
BuyTransaction(parameters) : Transaction(createLogString(parameters)) {}
条款10 令operator=返回一个reference to *this
总结:
重载赋值运算符(包括所有赋值=相关的运算)、前自增和前自减运算符(++a、--a)都返回*this的引用。而后自增和后自减(a++、a--)返回的是对象。
1、连锁赋值的情况。赋值运算符必须返回一个指向引用指向操作符左侧的实参, *this指向左侧对象。
stu& operator=(const stu& s);
class stu
{
public:
int _x;
public:
stu(int x) :_x(x){};
stu& operator=(const stu& s)
{
_x = s._x;
return *this;
}
};
int main()
{
stu s(1);
stu s2(2);
stu s3(3);
s = s2= s3;
return 0;
}
可以实现连锁赋值。
stu& operator=(const stu& s)
{
...
return *this;//返回左侧对象
}
*this 是自身对象,this是当前对象的指针。
2、前自增和前自减运算符(++a、--a)都返回*this的引用;后自增和后自减(a++、a--)返回的是对象
class CheckedPtr {
public:
CheckedPtr& operate++(); //前自增运算符重载
CheckedPtr& operate--();
...
};
//前自增
CheckedPtr& CheckedPtr::operator++() {
...
++curr;
return *this; //注意返回值,返回的是对象的引用
}
//后自增
CheckedPtr& CheckedPtr::operator++() {
...
CheckedPtr ret(*this);
++*curr;
return ret; //注意返回值,返回的是当前对象
}
后自增、自减的效率较低。
条款11 在operator=中处理“自我赋值”
总结:
确保当对象自我赋值时operator = 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap.
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
1、自我赋值不安全和异常不安全版本:
class Bitmap {
};
class Widget {
private:
Widget(Bitmap*pb):p(pb){ }
Bitmap* p; //指向一个从heap分配而得的对象。
Widget& operator = (const Widget&rhs);
};
Widget& Widget::operator=(const Widget&rhs) {
delete p;
p = new Bitmap(*(rhs.p));
return *this;
}
/*
这里的this(赋值的目标)和 rhs 可能是同一个对象。果真如此 delete 不仅会销毁当前对象的bitmap,
也会销毁 rhs 的 bitmap。在函数的结尾,Widget(原本不该被自我赋值动作改变的)
发现自己持有一个指向已删除对象的指针。
*/
delete会先删除自身对象,最终发现自己持有一个指针指向一个已被删除的对象。
2、自我赋值安全,但不是异常安全的版本,先加上 identity test(证同测试)
if (this == &rhs) return *this;
如果是自我赋值,就不做任何事
3、异常安全+自我赋值安全版本
我们只要注意复制pb所指东西之前别删除pb。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig=p;//指向原先的p对象
p=new Bitmap(*rhs.p);
delete pOrig;
return *this;
}
4、copy and swap" 技术
class Widget {
...
void swap(Widget& rhs); // 交换*this和rhs数据
...
};
Widget& Widget::operator=(constWidget& rhs)
{
Widget temp(rhs); // 为rhs数据制作一份副本
swap(temp); // 将*this数据和上述复件的数据交换
return *this;
}
条款12 复制对象时勿忘其每一个成分
总结:
Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
以下规则是在声明定义自己的copying 函数时出现的问题。
当我们编写一个copying函数,请确保
(1)复制所有local成员变量!(2)调用所有base classes内的适当的copying函数。
1、添加新的成员变量,需要同时修改copying函数
如果你声明自己的copying函数(拷贝构造,拷贝赋值),那么编译器在你做出错误的动作时不会告诉你,下面定义的copying函数没有对Defau赋值或初始化,而编译器不会管。
// 第一个类
class Defau {
public:
int i = 10;
};
// 第二个类
class A {
public:
A() = default;
A(const A&rhs):name(rhs.name){}
A& operator = (const A&rhs) {
this->name = rhs.name;
}
string name;
// 用到了第一个类
Defau u;
};
int main() {
A a;
a.u.i = 15;
A b(a);
cout << b.u.i;
}
在这里,已有的Copying函数只进行了局部拷贝:它们复制了 第二个类A 的 name,但是没有复制第一个类Defau 的对象 u。然而,大部分编译器即使是在最高的警告级别也不出任何警告。
结论显而易见:如果你为一个类增加了一个数据成员,你务必要做到更新拷贝函数,你还需要更新类中的全部的构造函数以及任何非标准形式的 operator=。
2、发生继承,只有derived自定义的变量,base 的只能用缺省的默认初始化
任何时候只要我们承担起“为derived class撰写copying函数”的重责大任,必须很小心的也复制其base class成分。
但是那些成分往往是private,所以我们无法直接访问它们,所以我们应该让derived class的copying函数调用相应的base class函数:
PriorityCustomer::PriorityCustomer(constPriorityCustomer& rhs)
:priority(rhs.priority)
{logCall("PriorityCustomer copy constructor");}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return*this;
}
现在只有复制了local成员变量,对于base class的成员变量只能缺省的默认构造函数。
3、正确做法:derived class 的copying函数调用base class的构造函数和赋值函数。
PriorityCustomer::PriorityCustomer(const PriorityCustomer &rhs) : Customer(rhs), priority(rhs.priority)
{
}
PriorityCustomer& PriorityCustomer::operator =(const PriorityCustomer &rhs)
{
Customer::operator=(rhs);
priority = rhs.priority;
return *this;
}
当我们编写一个copying函数,请确保(1)复制所有local成员变量!(2)调用所有base classes内的适当的copying函数。