Effective C++ Note
Effective C++ Note
基础部分
1. c++四个次语言
c,objective-orinted c++,Template C++,STL
2. 尽量用const,enum, inline 替换 #define
例如
1 | #define ASPECT_RATIO 1.652 /* 预编译器 */ |
而当对于一个字符串常量
1 | const char* const str = "stockdean";/* 常量字符串*/ |
而对于类内变量
1 | class GamePlayer{ |
此时NumTurns为声明式而非定义式,如果必须提供定义式,则需要在.cc文件内定义。
const int GamePlayer::NumTurns ;
无法使用#define 来创建一个class 专属常量,因为#define并不重视作用域,却不提供封装性。如private。但是使用const 可以提供封装性。
下面提供一个例子,如果老式编译器不支持在声明式赋予初值,则如下
1 | /** |
但是,如果后面的数组必须在编译期间指导数组大小怎么办?1
2
3
4
5
6class GamePlayer{
private: //封装性
enum { NumTurns = 5 }; //使用enum可以充当int
int scores [NumTurns]; //使用该常量
};
一.
enum hack
行为更像#define(比如可以取const的地址,但是不能取enum的地址,也不能取#define的地址)
二. 实用主义
macros的实现引发的错误,如下面的例子
1 |
|
为了避免macro引发的错误,template inline函数可以避免1
2
3
4
5template<typename T>
inline void callWithMax(const T& a,const T& b)
{
f(a>b?a:b);
}
小节:常量避免#define,macro实用template inline func 来替代。
3. 尽可能使用const
例子
1 | char* p ="stockdean";/* 字符串*/ |
两种形式1
2void f1(const Widget* w);
void f2(Widget const* w); //都是指向不变的Widget类对象
对于STL迭代器有同等适用的规则[2]
1 | std::vector<int> vec; |
对于重载的operator有同样的妙用
1 | class Rational { .. }; |
const 对于成员函数
- 使class容易理解,标明什么是可以改的
- 使操作const 对象成为可能(见[20])
1 | class TextBook{ |
bitwise const 和logical const
假如1
2
3
4
5
6
7
8
9
10
11
12
13class TextBook{
public: const char& operator[](std::size_t position)const//operator for const 对象
{return text[position];}
char& operator[](std::size_t position)//operator for non-const 对象
{return text[position];}
private: char* text;
};
const TextBook ctb("hello");
char* pc = &ctb[0];
*pc = 'J';//这是允许的
因为只改变了内部值,这是允许的
假如1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class TextBook{
public: std::size_t length() const;
private: const char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t TextBook::length() const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText);//错误const成员函数内不能修改属性
lengthIsValid = true; //错误
}
return textLenght;
}
想要解决这个问题使用mutable类型(可变的)来修改non-static成员变量bitwise const限定
在const和non-const成员函数间避免重复
1 | class TextBook{ |
4. 确定对象使用前被初始化
- 要为内置对象手动初始化,因为C++不一定初始化(c part可能不会初始化,其他可能会)
- 构造函数使用初始值列表,而不要使用构造函数的赋值操作,排列顺序最好与声明顺序同
- 未免受”跨编译单元初始化次序”干扰,用
local static
替换non-local static
对象
对于第一条举例:STL的vector会保证初始化,而c的array需要手动初始化。
对于非内置类型,类的初始化和member的初始化一般交由构造函数。
举例
1 | class PhoneNumber{...}; |
有些情况下对于内置类型也一定要初始化,例如const和reference的
member field
C++的初始化顺序一般与声明的顺序相同,但是有些操作一定要按顺序来(如数组大小要在数组前初始化)。
不同编译单元内定义之non-local static 对象次序
编译单元:
local 对象:函数内的对象。
non-local static对象:global或者namespace内的或者class和file作用域内被声明为static的对象
例:
1 | class FileSystem{ |
此时
1 | class Directory{ |
所以需要用另外一种方式去实现。
这里使用的是单例模式Singleton
即用local static 替换non-local static。使用return T&类型的func来实现。
理由:C++保证,函数内的non-local对象会在该函数被调用期间和首次遇到该对向定义式时初始化。所以使用函数调用替换”直接访问non-local 对象”
修改后1
2
3
4
5
6
7
8
9
10
11
12
13class FileSystem{
public:
std::size_t numDisks() const;
FileSystem& tfs();
};
/**
local static 替换non-local static
*/
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
1 | class Directory{ |
构造/析构/赋值运算
5. 了解C++编译器默默做了什么
1 | class Empty{//实质上cpp编译器生成的是下面的 |
编译器产出的析构函数是non-virtual类型的,除非base class自身声明
而对于拷贝构造函数和拷贝赋值运算符将non-static 对象拷贝到目标对象。
1 | template<typename T> |
但是生成copy assignment operator的条件必须满足:1.代码合法2.有适当机会证明有意义
下面一种情况
1 | template<class T> |
因为这里string& name 不能变更,因为c++不允许”让reference改变向不同对象”
如果某个base classes将copy assignment operator声明为private,那么编译器会拒绝为其derived classes 生成CAO。因为无权调用。
6. 阻止编译器生成拷贝构造函数和拷贝构造运算符
两种方法:
- 声明但是不定义
- 继承一个uncopyable class
1 | 只声明不定义 |
或者定义一个base class
1 | class Uncopyable{ |
7. 为多态基类声明virtual析构函数
- 如果一个(多态性质)作为base class的类有member函数为virtual,那么它的析构函数应为virtual
- 如果一个类不打算作为base class不要使用virtual修饰析构函数
如果存在一个Factory类
1 | class Animal{ |
改为1
2
3
4
5
6
7
8
9
10
11
12
13class Animal{
public: Animal();
// ~Animal();
virtual ~Animal();
....
};
class Dog:public Animal{
...
};
Animal* p = new Dog();
delete p;//会导致局部销毁,只销毁部分。所以
如果一个class不作为base class类但是声明了virtual member func会怎么样?
由于对象会保存一个vptr指向虚函数表(virtual function table)(指针数组),所以导致对象臃肿。
如果继承一个没有virtual函数的class也会出现问题。如STL的string等(析构函数non-virtual)
生成一个抽象类
1 | class AWOV{ |
8. 别让异常逃离析构函数
- 析构函数绝对不要出现异常
- 如果对异常要处理,使用普通函数包装
这里假设vector销毁时,析构出现问题(大于一个出现异常,导致不明确行为)。
可以抛出异常处理
例
1 | class DBConn{ |
这时如果写下面的代码
如果调用close()失败
1 | DBConn::~DBConn() |
无法对抛出异常做出反应,可以使用普通函数处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 class DBConn{
public: ~DBConn();
void close()
{
db.close();
closed = true;
}
private: DBConnnection db;
bool closed;
};
DBConn::~DBConn()
{
//db.close();
if(!closed)
{
try{
db.close();
}
catch(...)
{
}
}
}
9. 绝不在构造和析构函数中调用virtual函数
- 绝不在构造和析构函数中调用virtual函数,因为不会下降到derived层
假如
1 | class Transaction{ |
现在
1 | BuyTransaction b; |
使用新一层包装问题可能更难发现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class Transaction{
public: Transaction(){
init(); //non virtual
}
virtual void LogTransacition()const =0;
private: void init()
{
LogTransaciton(); //virtual
}
};
Transaction::Transaction()
{
LogTransaction();
}
class BuyTransaction:public Transaction{
public:
virtual void LogTransacition() const;
};
class SellTransaction:public Transaction{
public:
virtual void LogTransacition() const;
};
所以更好地是避免使用virtual1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Transaction{
public: explicit Transaction(const std::string& logInfo)
void LogTransacition(const std::string& logInfo) const;
};
Transaction::Transaction(const std::string& logInfo)
{
LogTransaction(logInfo);
}
class BuyTransaction:public Transaction{
public:
BuyTransaction(Params):Transaction(createLogString(params))
{
...
}//log传给base构造函数(向上传递弥补不能向下传递)
private:
static std::string createLogString(params);
};
10. 令operator=返回一个referfence to *this
- 这是个习惯
1 | class Widget |
11. 在operator=中处理自我赋值
例子
如果
1 | class BitMap{ |
这种做法保证了异常安全
1 | class BitMap{ |
12. 复制对象勿忘每一个成分
如果在自我实现copy 构造函数和COA后,添加了成员变量,注意要修改这两个函数,否则会引起局部copy(采用default构造函数初始化)。
1 | PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority) |
资源管理
13. 以Object管理资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Investment{
};
Investment* createInvestment();//Factory模式
void f()
{
Investment* iv = createInvestment();
... //如果...内有return,会提前结束
delete iv;
}
为确保正常释放,把资源放入对象,利用析构函数自动释放
或者
void f()
{
std::auto_ptr<Investment> apiv(createInvestment());//auto_ptr的析构函数会自动释放
}
1 | std::auto_ptr<Investment> priv1(createInvestment()); |
使用shared_ptr比auto_ptr更好
14. 在资源管理类中小心copying行为
1
2
3
4
5
6
7
8
9
10
11class Lock{
public: explicit Lock(Mutex* pm):mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{unlock(mutexPtr);
}
private: Mutex* mutexPtr;
};
1 | Lock l1(&m); |
重新实现1
2
3
4
5
6
7
8
9
10
11class Lock{
public: explicit Lock(Mutex* pm):mutexPtr(pm,unlock)//这里以unlock函数作为删除器
{
lock(mutexPtr.get());
}
/* ~Lock()
{unlock(mutexPtr);
}*/析构函数不再需要声明
private: std::tr1::shared_ptr<Mutex> mutexPtr;
};
15.在资源管理类提供对原始资源的访问
1
std::tr1::shared_ptr<Investment> pInv(createInvestment());
这时候有两种方法可以解决这个问题。
显式转换1
int days = daysHeld(pInv.get());
隐式转换
16. 成对使用new和delete使用相同形式
1
2
3
4
5std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete[] stringPtr2
17. 以独立语句将newed对象置入只能指针
1
2int priority();
void processWidget(std::shard_ptr<Widget>pw,int priority);
如果
1 | processWidget(new Widget,priority()); //不能通过编译 |
如果以下面的顺序
一旦priority()
调用出现问题,则会引发异常。
所以
1 | std::shard_ptr<Widget>pw (new Widget) ; |
设计与声明
18. 让接口容易被正确使用
19. 设计class犹如设计type
20. 宁以pass-by-reference-to-const替代pass-by-value
继承与OO-Design
32. 确定public继承是is-a的关系
考虑下面的例子
1 | void eat(const Person& person); |
下面更符合事实
1 | class Bird{ |
讨论下面这个例子1
2
3
4
5
6class Rectangle{
public: virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int Height()const;
virtual int Width()const;
};
1 | void makeBigger(Rectangle& r) |
33. 避免遮掩继承来的名称
1 | class Base{ |
下面这个例子
1 | class Base{ |
为了解决这个问题,一方面可以使用using声明来解决。
1 | class Base{ |
利用转交函数forward function可以选择性继承Base的virtual函数。
1 | class Base{ |
34. 区分接口继承和实现继承
模板与泛型编程
41. 隐式接口和编译器多态
- classes和template都支持接口和多态
- OOP支持的是显式接口,可以找到对应代码。virtual函数实现了运行期多态,根据对象的动态类型决定调用哪一个函数。
- template 支持的是隐式接口,例如w无论是什么类型,都需要支持doProcessing内的函数操作。而且是编译器多态,在编译的时候决定实际运行的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
};
void doProcessing(Widget& w)
{
if(w.size()>10&&w!=someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
//改写成template 接口
template<typename T>
void doProcessing(T& w)
{
if(w.size()>10&&w!=someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
42. 了解typename的双重意义
1 | template<typename C> |