文章目录
了解C++默默编写并调用了哪些函数
一个空类会声明的函数:copy构造函数, copy assignment操作符、析构函数
如果没有声明构造函数:编译器声明默认的default 构造函数
编译器声明函数的目的:给编译器来放置调用基类的非静态变量的构造与析构函数(编译器产生的析构函数是non-virtual,除非该类的基类自身声明有virtual析构函数。)
不想使用编译器自动生成函数, 明确拒绝
如果不希望编译器自动生成函数,有以下方法:
-
将其声明到private中并不定义, 缺点:member函数和friend函数依旧可以调用
-
为阻止拷贝设计一个基类,在基类的private中声明拷贝函数
为多态基类声明virtual析构函数
-带多态性质(polymorphic)的基类应该声明一个virtual的析构函数。如果任何class带有任何virtual函数,他应该拥有一个virtual析构函数。
原因:当derived class的对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,那么通常该成分的derived成分没有被销毁,可能造成资源泄露。
任何类只要有virtual函数几乎都应该有一个virtual析构函数,只有类内至少含有一个virtual函数,才声明virtual析构函数
同理,如果一个类不包含基类,那么其析构函数也不应该是virtual的。
原因:一个virtual函数的实现,对象必须携带信息来决定运行期来调用拿个virtual函数,该信息由一个(vptr, virtual table pointer)指针指出,vptr指向一个由函数指针构成的数组,成为vbtl(virtual table),每一个带有virtual的类都有一个vbtl来查询决定适当的函数指针。
不分青红皂白的声明虚析构函数会降低代码的可移植性,因为vptr占存储空间。
当希望拥有抽象class, 没有纯虚函数:
为希望成为抽象的那个类声明一个纯虚析构函数
class AWOV{
public:
virtual ~AWOV( ) = 0;
};
必须为这个纯虚虚构函数提供一份定义:
AWOV::~AWOV() { }
不让异常逃离析构函数
析构函数吐出异常会导致程序过早结束或出现不明确行为,C++没有禁止析构函数抛出异常,但是不提倡。
解决办法:
1.如果抛出异常就结束程序,调用abort
DBConn::~DBConn( )
{
try { db.close(); }
catch (...) {
制作运转记录,记录close的调用失败;
std::abort;
}
}
2.吞下因调用出问题函数而产生的异常
DBConn::~DBConn( )
{
try { db.close(); }
catch (...) {
记录close的调用失败;
}
}
3.重新设计DBConn接口,让客户有机会对可能出现的问题作出反应,这样异常的抛出就会在一个普通函数中执行此操作。
class DBConn {
public:
...
void close( )
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed) {
try {
db.close();
}
catch (...) {
记录失败记录;
}
}
}
private:
DBConnection db;
bool closed;
};
不在构造和析构过程中调用virtual函数
只是c++与java的不同之处
原因:基类会先于derived class之前被调用,导致调用的是基类里的构造函数,建立的对象就编程了一个隶属基类的类型,一旦虚的构造和析构函数开始执行,子类中的派生数据就被忽略。
确定构造函数和析构函数没有调用virtual函数
其它的解决办法:
在基类中将对应的函数声明为非虚, 其后构造函数可以安全调用
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(parameters)
: Transaction(creatLogString(parameters))
{...}
...
private:
static std::string createLogString(parameters);
};
operator=返回一个reference to *this
x = y = z = 15;
//等价于
x = (y = (z = 15));
为了实现连锁赋值, 赋值操作符必须返回一个reference指向操作符的左侧实参
例如:
class Widget{
public:
...
Widget& operator=(const Widget& rhs)
{
...
return* this;
}
}
在operator= 中处理“自我赋值”
对象可能自我赋值,例如:
a[i] = a[j] //如果i==j, 则自我复制
别名(aliasing):有一个以上的方法指代某对象。
假如尝试自行管理资源,可能“在停止使用资源之前释放了它”,可以加入“认同测试”(identity test)来解决这个问题
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this
}
具备异常安全性往往自动获得自我复制安全性。“许多时候一群精心安排的语句就可以导出异常安全”。例如,我们只需注意在复制pb所指东西之前别删除pb
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
更加聪明的解决办法:
‘认同测试会引入额外成本",可以使用“copy and swap”技术。
class Widget{
...
void swap(Widget& rhs);
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
复制对象勿忘其每一个成份
一个设计良好的对象系统回将对象的内部封装,只留两个函数负责对象拷贝(复制), 即copy构造函数和copy assignment操作符,也称之为copying函数。编译器会在必要时为我们的类创造copying函数,并说明这些编译器生成版的行为-将对象所有的成员变量都做一份拷贝。
如果你为class添加一个成员变量, 你必须同时修改copying函数。如果忘记,编译器不太可能做出提醒。任何时候只要为“derived class”撰写copying函数, 就必须小心地复制其基类成分(往往是private)
举例
class Date {...}
class Customer {
public:
...
private:
std::string name;
Date lastTransaction;
};
class PriorityCustomer : public Customer{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), //调用基类的copy构造函数
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); //对基类成分进行复制操作
priority = rhs.priority;
return *this;
}
编写一个copying函数的的注意事项:
- 复制所有local成员变量
- 调用所有基类内的适当的copying函数
不要尝试以某个copying函数实现另外一个copying函数, 应将共同机制放到第三个函数中,并由两个coping函数共同调用
不要用copy assignment操作符调用copy构造函数–试图构造一个已经存在的对象。
不要用copy构造函数调用copy assignment操作符:构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象上。对一个尚未构造好的对象赋值没有意义。