Effectice C++01
条款01:视C++为一个语言联邦
区块(blocks)、语句(statements)、预处理(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)
classes(包括构造和析构函数)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual(动态绑定)
泛型编程、模板元编程
容器、迭代器、算法、函数对象
条款二:尽量以const、enum、inline、替换#define
以编译器替换预处理器
#define ASPECT_ROTIO 1.653 // 可能并未记录到符号表 获取编译错误信息会提到 1.653
// 目标码会出现多份1.653
const double AspectRotio = 1.653
// 常量指针
const char* const authorName = "Scott Meyers"
// class 专属常量
class GamePlayer {
private:
// 通常需要在外面有定义式
// static 且为整数类型 int char bool 只要不取它们的地址,可以提供声明式而无需提供定义式
static const int NumTurn = 5; //声明常量=======================================
int scores[NumTUr];
}
const int GamePlayer::Numturns; // Numturns的定义 放进一个实现文件
class GamePlayer {
private:
enum {NumTurns = 5}; // the enum hack 无法取地址
int score[NumTruns];
}
- 对于单纯常量,最好以const对象或enum替换#define
- 对于形似函数的宏,最好改用inline函数替换#define
条款03:尽可能使用const
STL迭代器以指针为根据塑模出来,所以迭代器的作用就像一个T*指针
。声明迭代器为const对象就像声明指针为const一样(即声明一个T* cosnt
指针)——不得指向不同的东西,它所指的东西的值是可以改变的。
const T*
指针 => const_iterator
const 可以和函数返回值、各参数、函数自身(成员函数)产生关联
const和non-const成员函数中避免重复
令non-const版本调用const版本可避免代码重复。
class TextBlock {
public:
const char& operator[] (std::size_t position) const
{
//.....
return text[position];
}
char& operator[] (std::size_t position) {
return
const_cast<char&>( // 返回const对象 转化为非const对象
static_cast<const TextBlock&>(*this) // 转化为const对象 为调用cosnt函数
[position]
);
}
}
条款04:确定对象使用之前已被初始化
C part of C++ 不保证发生初始化
array(来自C part of C++)不保证其内容被初始化, vector(STL part of C++)却有此保证
除内置类型,初始化责任落在构造函数(constructors)身上
别混淆了赋值(assignment)和初始化(initialization)
class PhoneNumber{ //... };
class ABEntry { // 通讯簿
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
// 首先调用default构造函数为theName、theAddress和thePhones设初值
// 然后立即再对他们赋予新值 浪费了default构造函数做的一切
AbEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
{
theName = name; // 这些都是赋值(assignments)
theAddress = address; // 而非初始化 (initializations)
thePhones = phones;
numTimesConsulted = 0; // 内置对象,初始化和赋值的成本相同
}
AbEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones) // 现在都是初始化
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{ }
初始化次序:
base classes更早与其derived被初始化
class的成员变量总是以其声明的次序初始化
static对象,其寿命从被构造出来直到程序结束为止
函数内的static对象称为local static对象,其它的static对象称为non-local-static对象
class FileSystem {
public:
std::size_t numDisks() const;
//...
};
static FileSystem tfs; //预备给客户使用的对象
class Directory {
public:
Directiry(params);
// ...
};
Directory::Directory(params) {
//...
std::size_t disks = tfs.numDisks(); // 使用tfs对象
}
// 在不同的编译单元 无法保证 tfs在tempDir之前被初始化
Directory tempDir(params);
解决方案:用local static对象替换non-local static对象
class FileSystem {//...}
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
Directory& tempDir() {
static Directory td;
return td;
}
- 为内置对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初值列。初值列中的成员变量,其排列次序应该和它们在class中的声明次序相同
- 为避免跨单元之初始化次序, 以local static 对象替换non-local static对象
构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
M条款17:理解特种成员函数的生成机制
- 唯有这些函数被需要(被调用),它们才会被编译器创建出来
- 编译器产出的析构函数是个non-virtual
class Empty {};
class Empty {
public: // inline
Empty() {……} // default ctor
Empty(const Empty& rhs) {……} // copy ctor
~Empty() {……} // dctor
Empty& operator=(const Empty& rhs) {……} // copy=
}
//C++11
class Widget {
public:
……
Widget(Widget&& rhs);// 移动构造函数
Widget& operator=(Widget&& rhs); // 移动赋值运算符
}
- 并不能保证移动操作真的会发生。“按成员移动”实际上更像是按成员的移动请求,因为那些不可移动的型别(C++98遗留),将通过复制移动实现“移动”。
-
两种复制操作是彼此独立的:声明了其中一个,并不会阻止编译器生成另一个。
-
两种移动操作并不彼此独立。——原因在于自己声明的一个move函数与编译器自动生成的有不同,另一个极有可能与编译器自动生成的不同。——涉及到资源的管理方式
- 一旦显式的声明了复制操作,这个类也就不会再生成移动操作了。——成员适用于复制操作,极有可能不适用移动操作。
- 一旦声明了移动操作,编译器就会废除复制操作。——理由同上
三大律
C++98: 如果你声明了copy-ctor、copy=、dctor中的任意一个,你就得同时声明所有的这三个。——如果有改写复制操作的要求,往往意味着该类需要执行某种资源管理。
推论:如果用户声明了析构函数,则复制操作就不该自动生成。C++98未严格遵守;C++11:只要用户声明了dctor函数,就不会生成移动操作。
移动操作的生成条件(如果需要生成)
- 该类未声明任何复制操作
- 该类未声明任何移动操作
- 该类未声明任何析构函数
C++11:在已经存在复制操作或析构函数的条件下,仍然自动生成复制操作已经成为了被废弃的行为。
成员函数模板在任何情况下都不会抑制特种成员函数的生成。
// 编译器会始终生成Widget的复制和移动操作
class Widget {
……
template<typename T>
Widget(const T& rhs);
template<typename T>
Widget operator=(const T& rhs);
}
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
M条款11:优先选用删除函数,而非private未定义函数
C++98为了阻止一些特种成员函数被使用,采用的做法是声明其未private,并且不去定义它们。
//C++98
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:
……
private:
basic_ios(const basic_ios& ); //not defined
basic_ios& operator=(const basic_ios& ); // not defined
}
// 链接期才会诊断出错误
//C++11
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:
……
public:
basic_ios(const basic_ios&)= delete;
basic_ios& operator=(const basic_ios& ) = delete;
}
任何函数都能成为删除函数
bool isLucky(int number);
// 下面的调用都可以
if (isLucky('a'));
if (isLucky(true));
if (isLucky(3.5));
//如果幸运数必须是整数
bool isLucky(int number);
bool isLuncky(char) = delete;
bool isLuncky(bool) = delete;
bool isLuncky(double) = delete; // 拒绝double和float型别 float优先选择转型为double
指针中的两个异类:
void*
无法对其执行提领、自增、自减操作
char*
表示的C风格的字符串
template<typename T>
void processPointer(T* ptr);
template<>
void processPointer<void*> = delete;
template<>
void processpointer<char*> = delete;
模板特化必须在名字空间作用域而非类作用域内来撰写
//C++98
class Widget {
public:
……
template<typename T>
void processPointer(T* ptr)
{……}
///
private:
template<>
void processPointer<void>(void); // 错误=======================
///
}
template<>
void Widget::processPointer<void> = delete;
条款07:为多态基类声明virtual析构函数
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
……
};
class AtomicClock: public TimeKeeper {……}// 原子钟
class WaterClock: public TimeKeeper {……}// 水钟
class WristWatch: public TimeKeeper {……}// 腕表
// 工厂函数 返回的对象必须位于heap中
TimeKeeper* getTimeKeeper();
Timekeeper* ptk = getTimeKeeper();
……
delete ptk; // 依赖客户执行delete动作,基本上便带有某种错误倾向。
derived class对象经由一个bass class指针被删除。——若base classer的析构函数为non-virtual,结果未定义,实际执行时通常发生的是对象的derived成分没有被销毁。
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
只有当class内至少含一个virtual函数,才将它声明为virtual析构函数
Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不该声明virtua析构函数。
条款08:别让异常逃离析构函数
C++不喜欢析构函数吐出异常。
//class负责数据库连接
class DBConnection {
public:
……
static DBConnection create();
void close(); // 关闭连接,失败则抛出异常
};
// 用来管理DBConnection资源的class
class DBConn {
public:
……
~DBConn() { db.close();} // 确保数据库连接总是会被关闭
}; // 析构函数传播异常,允许它离开这个析构函数,会出现难以驾驭的麻烦
两种解决方案:
// close抛出异常就结束程序。通常通过abort完成
DBConn::~DBConn() {
try{db.close();}
catch (……) {
制作运转记录,记下对close的调用失败
std::abort();
}
}
// 吞下因调用close而发生的异常
DBConn::DBConn() {
try {db.close();}
catch (……){
制作运转记录,记下对close的调用失败。
}
}
让客户自己调用close函数,对可能出现的问题作出反应
class DBConn {
public:
……
void close() {db.close(); close = true;}
~DBConn() {
if (!closed) {
try { db.close();}
catch(……) {记录下来并结束程序, 或吞下异常 }
}
}
private:
DBConnection db;
bool closed;
}
- 析构函数绝不要吐出异常。如果析构函数调用的函数可能抛出异常,析构函数应该捕抓任何异常,然后吐下它们或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
条款09绝不在构造和析构过程中调用virtual函数
class Transaction { // 所有交易的base classes
public:
Transaction();
virtual void logTransaction() const = 0; // 做出一份因类型不同而不同的日志记录
};
Transaction::Transaction() {
……
logTransaction();
}
class BuyTransaction : public Transaction { // derived class
public:
virtual void logTransaction() const; // 记录此类交易日志
……
};
class SellTransaction : public Transaction { // derived class
public:
virtual void logTransaction() const; // 记录此类交易日志
……
};
Buytransaction b;
——首先Transaction构造函数调用,调用的是Transaction::logTransaction
的版本。 ——base classes构造期virtual函数绝不会下降到derived classes阶层。
在base class 构造期,virtual函数不是virtual函数。在derived class 对象的 base class 构造期间,对象的类型是base class 而不是 derived class。对象在derived class 构造函数开始执行前不会成为一各derived class 对象。
解决方案
class Transaction {
public:
explict Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // 如今是个non-virtual函数
……
}
Transaction::Transaction(const std::string& logInfo) {
……
logTransaction(logInfo);
};
class BuyTransaction : public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters))
{……}
private:
static std::string createLogString(parameters);
};
你无法使用virtual函数从base classes向下调用,在构造期间,你可以借由 令drived classes 将必要的构造信息向上传递base class 构造函数替换之加以弥补。
在构造期和析构期不要调用virtual函数,因为这类调用从不下降至derived class。
条款10:令operator= 返回一个reference to *this
为了实现连续赋值
class Widget {
public:
……
Widget& operator=(const Widget& rhs) {
……
return *this; // 返回左侧对象
}
……
};
适用于所有赋值相关运算
class Widget {
public:
……
Widget& operator+=(const Widget& rhs) {
……
return *this;
}
Widget& operator=(int rhs) {
……
return *this;
}
};
条款11:在operator中处理"自我赋值"
// 自我赋值
class Widget {……};
Widget w;
……
w = w;
a[i] = a[j] // 潜在的自我赋值
*px = *py;
class Bitmap {……};
class Widget{
……
private:
Bitmap* pb;
}
//================================
Widget& Widget::operator(const Widget& rhs) {
delete pb; // 如果 *this 和 rhs是同一各对象 则数据被销毁无法再恢复
pb = new Bitmap(*rhs.pb);
return *this;
}
传统做法:证同测试
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this;
delete pd;
pb = new Bitmap(*rhs.pb); // 如果抛出异常 pb会指向一块被删除的Bitmap;
return *this;
}
另一种做法:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb); // 如果抛出异常 pb恢复原状
delete pOrig;
return *this;
}
copy and swap技术
class Widget {
……
void swap(Widget& rhs); // 交换*this和rhs的数据
};
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs);
swap(temp);
return *this;
}
- 确保对象自我赋值的时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
- 任何函数如果可以操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个部分
class Customer {
public:
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
private:
std::string name;
}
Customer::Customer(const Customer& rhs):name(rhs.name) {}
Customer& Customer::operator=(const Custormer& rhs) {
name = rhs.name;
return *this;
}
class Date {……};
class Customer {
public:
……
private:
std::string name;
Date lastTransaction; // 如果copy函数不改变,lastTransaciton就不会被复制,编译器也不会提醒
}
一旦发生继承
class PriorityCustomer: public Customer {
public:
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCusomer& rhs);
private:
int priority;
}
//复制了PriorityCustomer声明的成员变量,但是Customer的成员变量却未被复制。
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{}
PriorityCustome& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
priority = rhs.priority;
return *this;
}
解决方案:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), //调用base class的copy构造函数
priority(rhs.priority)
{}
PriorityCustome& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
Customer::operator=(rhs); // 对base class 成分进行赋值动作
priority = rhs.priority;
return *this;
}
copy=操作符调用copy构造函数是不合理的——这就像在试图构造一个已经存在的对象
同样copy-ctor函数调用copy=同样无意义。
如果copy-ctor和copy=有相近的代码,消除重复的方法是,建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名未init。
- Copying函数应该确保复制 对象的所有成员变量 以及 所有base class 成分。
- 不要尝试以某个copy函数实现另一copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
条款20:pass-by-reference-to-const
和pass-by-value
slicing(对象切割)
class Window {
public:
std::string name() const; // 返回窗口的名称
virtual void display() const; // 显示窗口和其内容
}
class WindowWithScrollBars: public Window {
public:
virtual void display() const;
}
// pass by value
void printNameAndDisplay(Window w) {
std::cout << w.name();
w.dispaly();
}
// w 会构造一个Window对象,总是会调用Window::dispaly()
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
// 正确方法 pass by reference to const
void printNameAndDisplay(const Window& w);
- 尽量以pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题
- 内置类型,以及
STL的迭代器
和函数对象
pass-by-value比较恰当。