Effective C++ 读书笔记
item2:
宏坑多,尽量用const和enum。类中定义的 static const int val = 0; 只是声明,如果要取它的地址必须在实现文件中定义,定义不能有赋值,因为声明里面已经赋值了。
item3:
const std::vector<int>::iterator iter // acts like a T * const
std::vector<int>::const_iterator citer // acts like a const T *
重载运算操作符的返回值最好设成const。函数的const和非const是不一样的,可以重载。重载[]操作符最好重载两个版本。可以用下面这个方式来减少两个重载函数的重复代码
const char& operator[](std::size_t position)const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position];
)
}
const成员函数不能保护成员指针指向的东西,如果要保护成员指针指向的东西,要用template写一个wrapper类,重载->操作符(具体看看二楼答案)。
用mutable声明的成员变量即使在const对象里面也能被修改。
item4:
用初始化列表来初始化成员函数效率高。
用函数中的static对象来解决一个全局对象的初始化依赖另外一个文件的全局对象,而我们不能控制全局对象初始化顺序的问题,为了效率可以把这个函数inline。
item6:禁止拷贝的方法:将拷贝构造函数和赋值操作符设为private防止类外调用,不实现它防止friend调用,将它们声明在基类可以把错误信息从链接期移到编译期,可以直接继承boost的noncopyable。不过如果因此而多重继承,noncopyable这个空基类的空间优化可能会失去。
item7:凡是用了虚函数的类一定要有虚析构函数。反正用了虚函数都要插多一个vptr,多个虚析构也不会增加成本。
std::string, vector, list, set, tr1::unordered_map没有虚构造,所以不要继承他。
item8:不要在析构函数里面抛异常。如果遇到必须抛的情况,要处理在析构过程中抛异常的情况,应该留接口给类用户在析构抛异常的时候处理好析构。这种做法很丑,没有优雅解决办法的根源是,析构函数的语义就假设析构过程一定是没问题的。
item9:不要在构造函数和析构函数里面调用虚函数,因为那个时候vptr指向基类的虚函数表。如果构造函数和析构函数是通过另外一个函数来调用虚函数,这种错误的做法不会被编译器检查出来。
item10:赋值操作符要返回*this引用,因为这是赋值运算符对于内置类型的语义。
item11:
Object& Object::operator=(const Object& rhs)
{
Object *temp = p_obj;
p_obj = new Object(*rhs.p_obj);
delete temp;
return *this;
}
Object& Object::operator=(const Object& rhs)
{
Object temp(rhs);
swap(temp);
return *this;
}
Object& Object::operator=(const Object rhs)
{
swap(rhs);
return *this;
}
注意swap是自己定义的成员函数。上面这三种做法能够防止自己给自己赋值,因为delete放在后面,也可以防止new抛异常以后,p_obj指向空资源。
item12:记得在拷贝构造函数和赋值操作符里面调用 成员对象和基类 的拷贝构造函数和赋值操作符,不然编译器会给成员对象和基类调用默认拷贝构造函数而且不调用他们的赋值操作符。拷贝构造函数和赋值操作符有相同代码的时候,不应该让他们互相调用,而是将重复代码放在第三个函数里面。
item13:
用构造函数和析构函数的语义来管理资源,以防止多处return或者函数中间抛异常而引起资源泄漏的情况(RAII,Resources Acquisition Is Initialization)。
auto_ptr不能用于STL容器,因为如果拷贝装着auto_ptr的容器,原来的容器里面的auto_ptr全部被设成null。但shared_ptr就没问题。但两个智能指针都不能用于数组。
item14:对于管理资源的类,要好好考虑他被拷贝的情况。可以禁止拷贝,可以用引用技术。可以把资源也拷贝过去。可以改变所有权(像auto_ptr)。
item15:
有时候一些函数只能以裸指针作为参数。这个时候可以像auto_ptr和shared_ptr那样提供一个get方法,也可以定义隐式类型转换。
顺带一提,造智能指针的时候有应该重载指针的应有的api,这些api有 * -> ->*
item17:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
当该statement以这样的顺序执行:1 new Widget. 2 运行priority() 3 把new出来的对象转换成shared_ptr然后调用shared_ptr的拷贝构造函数,而priority又抛异常,那new出来的资源就泄漏了。所以应该把new出来的资源交给智能指针的语句单独放在一行里面。
item18: 如果接口容易用错(例如传错参数),尝试用C++内置的类型检查让编译器来为我们检查这些错误(引入新类型,用const)。尽量让接口保持一致(命名一致,同名函数功能相似,重载操作符行为与内置的一致)。
item22:书中说把成员变量设成private。其实我觉得这不重要,关键还是接口应该设计得够简单而且不那么需要改来改去。
item23:不要把跟类有关的函数都塞进类里面。类应该紧凑,这样写类的时候脑子不会爆掉。
item24:像封装数学运算,由于要支持两种类型的二元运算,所以运算函数不能是成员函数,因为这样会限制实现二元运算的交换率。
item25:如果要定制std::swap,最好像STL容器那样先定义一个public的swap为成员函数,然后std命名空间下定义一个swap来调用这个成员函数swap。如果这个swap也需要模板参数,由于模板函数不支持partial specialization, 所以我们应该重载swap函数,而且要把非成员版本放在该类的命名空间下(因为不能在std里面加template)
template<typename T>
void swap(Widget<T> &a, Widget<T>&b) // 这里原来是 T &a, T & b
{
a.swap(b);
}
item26:变量应该在用的时候才定义。
item27:尽可能不用cast。即使用也尽可能用C++新的cast。注意不能通过下面的方式调用基类函数,因为cast会产生新的临时对象,函数调用不会在当前对象中起作用
static_cast<Base>(*this).memfun();
item28:对象方法返回一个指向对象内部成员的指针,引用,迭代器时,要小心外部可以通过这个对象方法来修改这些成员(可以通过返回const来解决),更要小心临时对象调用这个函数而让一个外部指针,引用,迭代其指向一个被析构对象的内部成员。
item29:非常精华和重要的有一节!
如果一个函数调用到一半抛异常后,Exception-safe有三个层次的保证:
1:程序仍然处于一个valid的状态。但是具体处于什么常态不清楚。
2:程序处于未调用该函数前的状态。
3:程序压根不抛异常。
如果资源在函数开头分配,在函数结束时释放,应该用RAII。如果要将某个指针指向一个新资源,析构原来的资源,可以像shared_ptr那样用reset。还可以用copy and swap,或者保证先分配好资源并且配置妥当,再释放原来的资源(这样即使分配资源或者配置状态失败的时候,不会改变原来的状态)。
struct PMImpl
{
std::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu
{
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream & imagSrc)
{
using std::swap;
Lock m1(&mutex);
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->reset(new Image(imagSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
基本的想法是,如果资源分配好,变量也配置好,就交换整个状态。
如果一个函数不是Exception-safe,那么它在抛异常以后,状态的不确定就导致整个程序的状态不确定。
item30:调用虚函数的函数不能是inline。
item31: "pimpl idiom"(把一个类分成两个,一个负责实现,一个通过提供指向实现类对象的private指针和调用实现类方法的public函数提供接口。为的是防止一个头文件修改导致n个文件要重新编译)的做法真的好丑。不过,当一个类仅仅用于组合其它类的话,尽量用指针确实是个不错的选择。
在声明函数时,即使对象以值传递的方式传给函数或者从函数返回,这个文件也不需要该类定义,只需要类声明。因为是函数声明而不是实现,不需要知道类的大小。
三种方式减少编译文件之前的依赖:
1 如果可以,在头文件中用 指针或者引用。
2 在头文件中进可能用类声明,而不是类定义。
3 将声明和定义类的放在不同的头文件里面。
可以使用工厂函数返回对象指针可以减少编译文件间的依赖。
item32:继承的语义是,子类拥有任何基类的行为。有时候会发现子类不应该拥有基类某个方法或者在完成一项任务的时候应该拥有与基类不一样的函数。这不是设计得好不好的问题,而是继承的语义本来就没那么通用。
item33:可以在public: 里面using Base::memfun; 来让被自己重载函数隐藏的基类同名函数重见天日。
item34:纯虚函数的语义是定义通用接口(子类必须自己定义行为)。虚函数的语义是定义通用接口还有默认行为。非虚函数的语义是子类不应该改变的行为。
纯虚函数可以定义,但是子类必须显示调用(Base:memfun())。这种用法可以用来提供一个通用接口和默认行为,但是使用默认行为的时候必须显示掉用,这样又可以提醒需要自定义行为的子类重新定义该方法。
item35:
1 基类不直接定义public的虚函数,而是把它设为private,而让一个public的非虚函数调用这个虚函数。好处是通过在public的非虚函数里面可以放一些设置配置调用环境的代码来保证子类在调用这个虚函数时处于正确的调用环境中。
2 将部分逻辑写在类外的一个函数里面,在构造函数中通过传入这个函数的方式,能让没一个类的对象有不同的行为。而且可以在运行期改变对象的行为。或者用boost的function和bind。
3 跟2差不多,只不过将类外的函数写进一个用类包裹的继承体系里面(喔,类也能这么灵活。不过相比与lambda和boost的function,这种做法就显得有点臃肿了)。
item36:不要重定义基类的非虚函数。
item37:不要在改变虚函数里面用默认参数。因为这里有个陷阱,一个基类指针在调用子类虚函数的时候会用基类的默认参数!
item39:private继承表达的是以什么类来实现(只重用实现,不重用接口)。通过将一个虚函数放进内部类里面,子类就不能修改这个虚函数(子类是可以override基类的private虚函数的)。private继承空基类的时候,会有empty base optimization(EBO), 这样编译器不会为空类插入一个char还有padding。private继承还可以让子类override基类虚函数还有调用基类protected的成员。
item40:从对象模型来看,要实现多重继承和虚拟继承的语义,坑实在是太多了。指针要转来转去,运行的成本很大。如果不在意效率,通过public继承接口和private继承实现确实很方便。
item41:类与模板都支持接口和多态(虽然我脑子里的模板多态指static polymorphism)。模板的implicit接口真的非常强大,特别是加上C++11的auto(好吧,这对动态语言来说再平常不过)。
item42:nested dependent name在默认情况下不会被编译器认为是类型,而是一个类中的成员,因此需要在类型前面加typename说明。但是他不能用于存在与继承列表和初始化列表的基类的nested dependent name。
item43:在继承一个模板基类的时候(同时子类也是模板类),由于这个模板基类可能存在specialization,对于不同的模板参数,基类的实现可能完全不一样!一个函数可以在general的实现有,在specialization的版本没有。如果子类调用了这个函数(直接使用memfun();),编译器会报错说没有这个函数(因为可能真的没有)。解决方法有三个:
1 把memefun(); 改成 this->memfun(); 这样编译器会假设有,如果没有,编译器是在模板被instantiated 的时候报错。
2 像item33那样 using Base<T>::memfun(); 他让编译器在instantiation之前就去搜索这个函数。
3 把memfun(); 改成Base<T>::memfun(); 注意这种做法会让虚函数失效。
item44:模板中与模板参数无关的代码随着各种版本instantiate,会让编译出来的程序膨胀。解决办法是可以把这些参数放进函数里面。是否会让程序变快变慢要看实际。
item45: 非常tricky
template<typename T>
class SmartPtr
{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other):heldPtr(other.get())
{...}
T* get()const{ return heldPtr; }
private:
T *heldPtr;
};
这样,只有U版本的heldPtr能够隐式转换成T版本的heldPtr,编译才会通过,这样就可以通过内置的隐式类型转换,让自己构造的类也能隐式类型转换。这种做法可以用于赋值操作符,拷贝构造函数,也可以用于类型转换操作符。不过当T 和 U一样的时候,编译器不会造一个普通赋值操作符,拷贝构造函数!需要自己重新写一个。
item46:
当不用模板的时候(模板函数加上模板类),在调用参数的时候参数可以通过构造函数隐式类型转换来得到函数所需要的参数类型。但是在模板参数类型推导的时候,编译器不能让通过构造函数的隐式类型转换发生。