读了读《Effective C++》甚至感觉这本书就是专门给我这种弟弟写的,本文也是此书的阅读笔记
作为一个写了两年C#和Shader被商业引擎惯坏了的程序员,看见老朋友甚至有点陌生
把C++当做由四个次语言组成的联邦,从一个次语言到另一个次语言时,守则可能改变。
这四个次语言分别是:C、Object-Oriented C++、Template C++、STL
#define ASPECT_RATIO 255
开发者极有可能被它所带来的编译错误感到困惑,编译器可能提到255而不是ASPECT_RATIO,也许该语句被其他预处理干掉,追踪它会浪费时间。
解决办法就是用常量替换宏
const int AspectRatio = 255;
着重说明
由于常量经常定义于头文件内,因此有必要将指针(而不是指针所指之物)声明为const
const char* const authorName = "TOSHIRON";
对于Class专属常量,为了确保他只有一份实体,必须使其成为static成员
class GamePlayer
{
private:
static const int NumTurns = 5;
...
}
如果不想让别人获取到指向某个常量的指针,因为取const地址是合法的,所以可以用enum取代
class GamePlayer
{
private:
enum{
NumTurns = 5 };
...
}
用内联函数替代宏,以获得相同的效率和功能
#define MAX(a,b) f((a) > (b) ? (a) : (b))
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
const出现在*左边,被指物是常量
const出现在*右边,指针自身是常量
const出现在*两边,指针和所指事物都是常量
着重说明
令一个函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性
我感觉这几乎主要是为了单独避免一种情况
class Rational{
...};
const Rational operator* (const Rational& lhs, const Rational& rhs);
...
Rational a,b,c;
...
if(a*b = c){
...}
找错麻烦?因为常量不允许赋值所以会直接报错
利用常量性(constness)不同,重载函数
class TextBlock
{
public:
...
const char& operator[] (std::size_t position) const //第二个const是其重载的依据
{
return text[positon];
}
char& operator[] (std::size_t position)
{
return text[position];
}
private:
std::string text;
}
TextBlock tb("Hello");
std::cout << tb[0]; //调用non-const
Const TextBlock ctb("World");
std::cout << ctb[0]; //调用const
const成员函数不可以更改对象内任何non-static成员变量;解决办法就是用 mutable 关键字修饰,使变量总是可更改的,及时在const成员函数内。
在 const 和 non-const 成员函数中避免重复,常量性重载往往伴随着大量重复代码,这时,我们需要让non-const 利用 const 函数。
class TextBlock
{
public:
...
const char& operator[] (std::size_t position) const
{
...
...
return text[positon];
}
char& operator[] (std::size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
}
首先,将 *this 转型为 const,调用const成员函数,再移除const
这是当然了,至少我使用过的任何编程语言都有要求这一点
int x;
x有可能被初始化为0
class Point
{
int x, y;
};
...
Point p;
p的成员变量有时候会初始化为0,有时候不会,所以手动初始化很有必要。
着重说明
C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前。
class PhoneNumber{
...};
class ABEntry
{
public:
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones);
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
int numTimeConsulted;
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
{
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
书上的说法是 构造函数中那四行四赋值,而不是初始化
构造函数应该改为
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
{
}
为解决两个不同编译单元内的初始化次序问题,使用local static 替代 non-local
class FileSystem
{
public:
...
size_t numDisks() const;
...
};
extern FileSystem tfs;
替代为
class FileSystem
{
public:
...
size_t numDisks() const;
...
};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
这样,在调用时才不用在乎初始化顺序的问题
因为GC的存在,很长时间没有用析构函数了
编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。
class Empty{
};
相当于
class Empty
{
public:
Empty(){
...}
Empty(const Empty& rhs){
...}
~Empty(){
...}
Empty& operator = (const Empty& rhs){
...}
};
着重说明
如果想要驳回编译器自动提供的函数,可以将成员函数声明为 private 并且不与实现。
class Uncopyable
{
protected:
Uncopyable(){
}
~Uncopyable(){
}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator = (const Uncopyable&);
};
然后之后的类可以继承Uncopyable,反正C++可以多继承,但是多继承会阻止empty base class optimization,慎重
class Abc:private Uncopyable{
...}
class TimeKeeper
{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
class WaterClock:public TimeKeeper{
...};
class AtomicClock:public TimeKeeper{
...};
这时,才会正确摧毁整个对象
WaterClock wc;
...
delete &wc;
然而,没有多态用途的类,尽量避免使用 virtual。
在析构函数中用 try-catch 把错误盖住,至少要比草率结束程序要好,比如在构造函数中建立了数据库链接(?谁会干这种傻事)
~DBconn()
{
try{
db.close();}
catch(...)
{
记录错误日志
{
}
对上面的方法进行改良的话,就是用一个bool记录是否关闭过,如果是,就不必再close了
void close()
{
db.close();
closed = true;
}
~DBconn()
{
if(!closed)
{
try{
db.close();}
catch(...)
{
记录错误日志
}
}
}
据说像我这种C#过来的,更应该重视这一点。
主要是因为,首先会调用基类的版本。理由就是:base class构造函数会优先于其派生类的构造函数,这时,派生类的变量什么的还没初始化。不会下降到派生类的重写版。
这是一个协议?也包括*= += -= /=等。
class Widget
{
public:
...
Widget& operator = (const Widget& rhs)
{
...
return *this;
}
...
};
大概是这么个情况
class Bitmap{
...};
class Widget
{
...
private:
Bitmap* pb;
};
...
Widget w;
...
w = w;
主要为了避免在delete的时候把=左边的也删除了,主要解决办法有:
证同测试
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
第二个做法是先复制一份pb,再删除之前的,但我感觉有点浪费,所以觉得好像不是一个好方法。
如果为class添加了成员变量,那必须同时修改copying函数,以及operator=的重写。
好吧,我被GC惯坏了
看起来没毛病的操作
void f()
{
Investment* pInv = createInvestment();
...
delete pInv;
}
竟然考虑到在…提前return的情况。使用auto_ptr 智能指针
void f()
{
auto_ptr<Investment> pInv(createInvestment());
...
}
auto_ptr在销毁时会自动销毁它的所指之物,但是要注意不能让多个auto_ptr指向同一个对象,如果利用copying来复制,那么将会得到"剪切"的效果。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
std::auto_ptr<Investment> pInv2(pInv); //pinv 设为null,pInv2 指向原对象
...
}
解决办法就是用shared_ptr 替代,“引用计数型智慧指针” 持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。(有点GC的意思)
void f()
{
std::tr1::shared_ptr<Investment> pInv(createInvestment());
std::tr1::shared_ptr<Investment> pInv2(pInv); //pInv 和 pInv2 指向同一个对象
...
}
所以建议用 shared_ptr。
首先思考:被复制时会发生什么?可能会重复锁定同一个资源,在析构的时候可能重复销毁同一个资源。
解决方法
class Lock
{
public:
explicit Lock(Mutex* pm): mutexPtr(pm,unlock) ///unlock为删除资源的函数
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
}
就像 auto_ptr.get() 从智能指针中获取原始指针那样
int daysHeld(const Investment* pi);
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int days = daysHeld(pInv.get());
提供一个函数(显示转换),或者提供隐式转换
class FontHandle{
...}
class Font
{
public:
...
operator FontHandle() const
{
return f;
}
private:
FontHandle f;
}
std::string* stringArray = new std::string[100];
...
delete stringArray;
看起来好像没什么毛病,但是实际上只删除了第一个string。
应该使用
delete[] stringArray;
如果 new 表达式中使用 [],对应的 delete 表达式中也使用 []。
如果 new 表达式中不使用 [],对应的 delete 表达式中一定不要使用 []。
int A();
void B(std::tr1::shared_ptr<CClass> pw, int a);
务必不要直接
B(std::tr1::shared_ptr<CClass>(new CClass), A());
因为,编译器执行次序不定,如果A()导致异常,可能导致难以察觉的错误
所以至少要把智能指针的创建分离出来
pc = std::tr1::shared_ptr<CClass>(new CClass);
B(pc, A());
Date a(1998,12,28);
//为了防止无效日期,增加Day,Month,Year类,对Int封装,做有效性限制
Date a(Year(1998),Month(12),Day(28));
但我觉得这十分麻烦,或许只是这个例子不好,比如2001.02.29这个日期,感觉还是在函数内检验比较好std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment *>(0), getRidOfInvestment);
retVal = ...;
return retVal;
}
因为值传递会调用 copy 构造函数带来不必要的构造和析构,所以可以
bool validateStudent(const Student& s);
这样,因为const不允许更改,函数内编写时也会自律不去修改
注意
这并不适用于内置类型,STL 迭代器 和函数对象。
为了正常的 delete 和析构,在返回reference 和 object之间做出选择。不要忘记可能同时需要多个引用或指针指向的对象,而在内存释放上出现问题。
class AccessLevels
{
public:
...
int getReadOnly() const {
return readOnly;}
void setReadWrite(int value) {
readWirte = value;}
int getReadWrite() const {
return readWrite;}
void setWriteOnly(int value) {
writeOnly = value;}
private:
int noAccess;
int readOnly;
int readWrite;
int writeOnly;
}
好吧,C#过来的感到震惊
class WebBrowser
{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
现在想要做一个同时调用 WebBrowser里三个清理函数的函数,下面两种做法那种好
class WebBrowser
{
public:
...
void clearEverthing();
...
};
void clearBrowser(WebBrowser& wb);
居然是2好,因为更具有封装性。它的逻辑大概是这样
首先,成员变量声明为 private 就意味着有更少的函数能访问它,如果不是 private 那么就有很多函数可以访问它,它就不具有封装性。那么,越少函数能访问 private,那封装性越好。所以2好。
令 classes 支持隐式类型转换通常是个糟糕的主意。
请记住:如果需要为某个函数的所有参数进行类型转换,那么这个函数必须是个 non-member.
有些细节我从来没有考虑过
定义一个带有构造和析构函数类型的变量,就要承担其构造和析构带来的消耗。
好吧,抠得真细,说来惭愧,这是我从来没有考虑过的事情。
再加上 通过 default 构造函数构造出一个对象然后对它赋值 比 直接在构造时指定初值 效率差。可以看情况选择在必要时利用其 copy 构造函数初始化。
遇到循环时,我还是觉得应该在循环外声明,构造代价小于赋值 个人认为情况很少。
两种形式的 “旧式转型”,c风格
(T)expression
T(expression)
c++的新式转型
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
注意
class Window{
...};
class SpecialWindow: public Window
{
public:
void blink();
...
};
typedef
std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
{
spw->blink();
}
}
所以还是尽量在基类里提供虚函数比较好handles 包括 references、指针、迭代器
既然成员变量是private,就不能把它的指针return
可以参考3的做法用const
异常安全函数提供以下三个保证之一:
强烈保证往往用 copy-and-swap 的方法完成:修改对象数据的副本,然后如果不抛出异常,就将修改后的数据和原件置换。
至少完成基本承诺。
热衷于 inlining 会造成程序体积太大,inline造成的代码碰撞会导致额外的换页行为,降低指令告诉缓存设置的击中率
编译器通常不对“通过函数指针而进行的调用”实施inlining
inline void f(){
...}
void(* pf)() = f; //指针pf指向f
...
f(); //被inlined
pf(); //不被inlined
inline 函数无法随着程序库的升级而升级。如果改变inline 函数 f,那所有用到f的客户端程序都要重新编译
大多数 inlining 限制在小型、频繁调用的函数身上
目的是降低修改实现导致不必要的编译
如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects。
如果可以,尽量以 class 声明式替换 class 定义式。注意:当声明一个函数而它用到某个 class 时,你并不需要该 class 的定义
class Date; //class 声明式
//下面两个在不使用的情况下不会用到定义式,关键点在于,并不是每个人都会调用它
Date today;
void clearAppointments(Date d);
声明式和定义式提供不同的头文件。
#include "classafwd.h" //此头文件中申明,但没有定义 ClassA
ClassA Fun1();
void Fun2();
C++ 允许在Interfaces 内实现成员变量或成员函数。来自C#玩家的震惊
程序库头文件应该以 “完全且仅有声明式”的形式存在。
依赖于声明式,不依赖于定义式的两个手段
#include "Person.h" //定义式头文件
#include "PersonImpl.h" //实现类的头文件,接口相同
Person::Person(const string& name. const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name,birthday,addr)){
}
std::string Person::name() const
{
return pImpl->name();
}
书上的例子很生动 -_-||
public 继承 意味着 is-a。适用于 base classes 身上的每一件事一定也适用于 derived classes 身上。因为每一个 derived class 对象也都是一个 base class 对象。
只要重写一个虚函数,基类的重载也无效了
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
...
};
Derived d;
int x;
...
d.mf1(); //调用 Derived:mf1
d.mf1(x); //错误,被掩盖
d.mf2(); //调用 Base::mf2
d.mf3(); //调用 Derived::mf3
d.mf3(x); //错误,被掩盖
如何解决呢?使用 using 声明式
class Derived: public Base
{
public:
using Base::mf1; //让Base class内名为 mf1 和 mf3 的所有东西可见
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
...
};
如果我们只想让上例中 Derived 只继承 mf1无参版本,应该怎么办? 定义转交函数
class Derived: public Base
{
public:
virtual void mf1()
{
Base::mf1();
}
...
};
class Base
{
public:
virtual void draw() cosnt = 0;
...
}
class A:Base{
...}
...
Base* a = new A;
a->draw();
a->Base::draw(); //调用基类的虚函数
使用 non-virtual interface 手法,以 public non-virtual 成员函数包裹较低访问性(private 或 protected) 的 virtual 函数。和模板模式很像
将 virtual 函数替换为“函数指针成员变量” 或 用tr1::function 成员变量替换 virtual函数
将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数
tr1::function 对象行为就像一般函数指针,可以接纳“与给定之目标签名式兼容”的所有可调用物
typedef std::tr1::function<int (const ClassA&)> TypeA;
类型TypeA接受任何兼容 ClassA& ,返回 int
可以在构造时赋值,之后使用
class A;
int DealFunction(const A& a);
class A
{
public:
typedef std::tr1::function<int (const ClassA&)> TypeDealA;
explicit A(TypeDealA tda = DealFunction) : dealFun(hcf){
}
int Deal() const
{
return dealFun(*this);
}
...
private:
TypeDealA dealFun;
};
在C#里通过Action或delegate来传递函数,C++可以直接这样做
其实这本身就符合条例32,不重新 non-virtual 就对了
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数——你唯一应该复写的东西是动态绑定
class Shape
{
public:
enum ShapeColor
{
Red,
Green,
Blue
};
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle:public Shape
{
public:
virtual void draw(ShapeColor color = Green) const;
...
};
class Rectangle:public Shape
{
public:
virtual void draw(ShapeColor color) const;
...
};
这个时候
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
pc->draw(); // color == Red 自动调用缺省
pr-draw(); // color == Red 因为pr 的静态类型是 Shape 所以调用 基类的缺省参数值
实例中仿佛在讲如何处理 set 和 list 的关系。
class Widget
{
private:
class WidgetTimer:public Timer
{
public:
virtual void onTick() const;
...
}
WidgeTimer timer;
...
}
情况说明
class File{
...};
class InputFile: public File{
...};
class OutputFile: public File{
...};
class IOFile: public InputFile, public OutputFile{
...};
如果File Class有个成员变量 fileName,在IOFile中调用 fileName,就会出现歧义性。class File{
...};
class InputFile: virtual public File{
...};
class OutputFile: virtual public File{
...};
class IOFile: public InputFile, public OutputFile{
...};
让我看看有什么不一样
在 template 声明式中,class和typename没有什么不同
template<class T> class Widget;
template<typename T> class Widget;
我觉得,书上的例子太极端了。
template<typename T>
class Derived: public Base<T>::Nested //不允许
{
public:
explicit Derived(int x) : Base<T>::Nested(x) //不允许
{
typename Base<T>::Nested temp; //必须以 typename 修饰
...
}
...
};
class CompanyA
{
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(cosnt std::string& msg);
...
};
class CompanyB
{
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(cosnt std::string& msg);
...
};
...
class MsgInfo{
...};
template<typename Company>
class MsgSender
{
public:
...
void sendClear(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendClearText(msg);
}
void sendSecret(const MsgInfo& info)
{
...}
};
因为不知道 LoggingMsgSender 继承什么样的Class,他继承自模板,所以调用sendClear会报错
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
sendClear(info); //报错
}
...
};
解决办法
在 base class 函数调用动作之前加上“this->”
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
this->sendClear(info);
}
...
};
使用 using 声明式
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear; //告诉编译器,假设sendClear 位于 base class
...
void sendClearMsg(const MsgInfo& info)
{
sendClear(info);
}
...
};
明白支持被调用的函数位于 base class内
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
MsgSender<Company>::sendClear(info);
}
...
};
template<class T>
class shared_ptr
{
public:
//构造
shared_ptr(shared_ptr const& r);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
//copy
shared_ptr& operator = (shared_ptr const& r);
template<class Y>
shared_ptr& operator = (shared_ptr<Y> const& r);
...
当我们编写一个 class template,而它所提供之 “于此 template 相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend 函数”。
template<typename T>
class Rational
{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
cosnt T numerator() cosnt;
const T denominator() const;
...
};
template<typename T>
const Rational<T> operator* (cosnt Rational<T>& lhs, const Rational<T>& rhs){
...}
然后
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2; //报错,无法通过编译
因为编译器不知道T是什么,所以找不到正确的 operator*
必须先有相关函数推导出参数类型,声明 operator* 为友元函数可以化简这个过程
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational
{
public:
...
friend const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs);
{
return doMultiply(lhs, rhs);
}
...
};
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
现在执行不会报错了,因为,当对象 oneHalf 被声明为一个 Rational
,classRational
于是被具现化出来,而作为过程的一部分,friend 函数operator*
也就被自动声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数。
吃鲸,还有这种操作
template<unsigned n>
struct Factorial
{
enum
{
value = n * Factorial<n-1>::value
};
};
template<>
struct Factorial<0>
{
enum
{
value = 1
};
};
在main中
std::cout<<Factorial<5>::value; //打印 5! 120
还能这样?这只是“hello world”而已
STL容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,不是被 new 和 delete 直接管理。所以本章并不讨论
可以理解为专门catch operator new 的函数
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[1000000000L];
...
}
当 无法为 100000000个整数分配足够的空间,outOfMem会被调用。
一个设计良好的 new-handler 函数必须做以下事情:
替换编译器提供的operator new 或 operator delete 主要有三个理由:
C++标准程序库包括
TR1 详细描述了 14 个新组件
tr1::shared_ptr
和tr1::weak_ptr
智能指针tr1::function
可表示任何符合签名的函数和函数对象tr1::bind
绑定器tr1::unordered_set
,tr1::unordered_multiset
,tr1::unordered_map
以及 tr1::unordered_multimap
哈希表tr1::array
支持成员函数的数组tr1::mem_fn
类成员函数指针功能tr1::referene_wrapper
让引用行为更新对象?tr1::result_of
template,用来推导函数调用的返回类型C++开发者社区
感觉C++具有高自由度,我不得不说一下我的感触。用C#的时候,就仿佛用SRP写管线,一些底层的东西你没有参与其中(比如为物体维护光源索引),这可能也是SRP迷人的地方,轻轻松松让Unity按照你的想法渲染;用C++感觉像是直接拿着大写开头的DX API写管线,你要考虑所有细节。
阅读此书给我打开了新世界。
书中的一部分内容我还没有看懂,之后如果在使用过程中产生感触,会继续补充。
下一步我要更加熟悉C++标准库(正如54所说),下一本书不出意外是《Effective STL》