《Effective c++》 笔记
- c:说到底c++仍是以c为基础
- object-Oriented C++。 这部分也是C with Classed所诉求的。
- Template C++。这是c++的泛型编程部分,也是大多数程序员经验最少的部分。
- STL。STL是个template程序库。
- 对于对于单纯常量,最好以const对象或enums替换#defines。
- 对于形似函数的宏(macros),最好改用inline函数替换#define。
const成员函数
- const成员函数不可以更改对象内任何non-static成员变量
- 关键字mutable可以释放掉成员变量的bitwise constness约束;既此时const成员函数可以更改non-static成员变量。
代码:
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char*pText;
mutable std::size_t textLength; //这些成员变量可能总是
mutable bool lengthIsValid; //会被改变,即使在
}; //cosnt 成员函数内。
std::size_t CTextBlock::length() const
{
if(!lengthIsValid){
textLength=std::strlen(pText); //现在,可以这样,
lengthIsValid=true; //也可以这样。
}
return textLength;
}
const和non-cosnt成员函数中避免重复
- 当类中有函数重载,重载仅仅是const的区别那么会导致代码的大量的重复此时促使我们将常量性转移(casting away constness)
代码
class TextBlock
{
public:
...
const char& operator[](std::size_t postion) const//一如即往
{
...
...
...
return text[postion];
}
char& operatot[](std::size_t postion)//现在只调用const op[]
{
return
cosnt cast(
static_cast(*this)[postion]
);
}
...
};
如你所见,这份代码有两个转型动作,而不是一个。我们打算让non-cosnt operator[]调用其const兄弟,但non-const operator[]内部若只是单纯的调用operator[],会递归调用自己。那会大概......悟......进行一百万次。为了避免无穷递归,我们必须明确指出调用的是const operator[],但c++缺乏直接的语法可以那么做。因此这里将*this从其原始类型TextBlock&转型为const TextBlock。是的,我么使用转型操作符为它加上const!所以这里共有两次转型:第一次用来为*this添加const(这使接下来调用operator[]时得以调用const版本),第二次是从const operator[]的返回值中移除const。
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可以被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const可避免代码重复。
- 为内置型对象进行手工初始化,因为c++不能保证初始化他们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
- local static对象:函数内的static对象。
- non-local static对象:其他static对象。
- c++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。
- 编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment操作符,以及析构函数。
- 如果你打算在一个"内含reference成员(引用成员)"的class内支持赋值操作,你必须自己定义copy assignment操作符。
- 面都"内含const成员的classes,编译器的反应也一样,更改const成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。
- 如果某个base classes 将copy assignemt操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。
- 为驳回编译器(暗自)提供的机能,可以将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class 也是一种做法。
代码
class Uncopyable
{
protected:
Uncopyable() {};
~Uncopyable() {};
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
为阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
class HomeForSale:private Uncopyable{
...
}
- polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes 的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明vitrual析构函数。
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
代码
//如果close抛出异常就结束程序。通常通过调用abort完成:
DBConn::~DBConn()
{
try{db.close(); }
catch(...){
制作运转记录,记下对close的调用失败;
std::abort();
}
}
//吞下因调用close而发生的异常:
DBConn::~DBConn()
{
try{db.close();}
catch(...){
制作运转记录,记下对close的调用失败;
}
}
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
代码
class DBConn{
public:
...
void close()
{
db.close(); //供客户使用的新函数
closed=true;
}
~DBConn()
{
if(!closed){
try
{
db.close(); //关闭连接(如果客户不那么做的话)
}
catch(...){
制作运转记录,记下对close的调用失败;//记录下来并结束程序
... //或吞下异常
}
}
}
}
- 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)
(P51)一种做法是在class Transaction 内将 logTransaction 函数改为non-virtual,然后要求derived class 构造函数传递必要信息给Transaction 构造函数,而后那个构造函数便可以安全的调用 non-virtual logTransaction。
- 令赋值(assignment)操作符返回一个reference to *this。
代码
class Wideget{
public:
...
Widget& operator=(const Widget &ths)
{
...
return* this;
}
...
};
- 确保当对象自我赋值时operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy-and-swap。
- 确定任何函数如操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
- 重要思想:
- 智能指针:auto_ptr和shared_ptr均为智能指针,可以自动管理new 分配的内存。但两者有区别(自行百度)。
- 为防止资源泄露,请使用RAII(“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机” Resource Acquisition Is Initialization; RAII)对象->(我将其称为资源管理类对象)。
- 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳的选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
当一个RAII对象被复制,会发生什么事?大多数时候会选择一下几种可能:
- 禁止复制。许多时候允许RAII对象被复制并不合理。对一个像lock这样的class这是有可能的,因为很少能够合理拥有“同步化基础器物(synchronization primitives)”的复件(副本)。如果复制动作对RAII class并不合理,你便应该禁止之。条款6告诉你怎么做:将copying操作声明为private。对lock而言看起来是这样的:
代码
class Lock:private Uncopyable{
public:
...
}
- 对底层资源祭出“引用计数法”(reference-count)。 有时候我们希望保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该将资源的“被引用次数”递增。tr1::shared_ptr便是如此。 通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可实现出reference-counting copying 行为。如前述的Lock打算使用reference counting,它可以改变mutexPtr的类型,将它从Mutex*改为tr1::shared_ptr
。然而很不幸tr1::shared_ptr的缺省行为是“当引用次数为0时删除其所指物”,那不是我们所要的行为。当我们用上一个Mutex,我们想要的释放动作是解除锁定而非删除。 幸运的是tr1::shared_ptr允许指定所谓的“删除器”(deleter),那是一个函数或函数对象(function object),当引用次数为0时便被调用(此机能并不存在于auto_ptr——它总是将其指针删除)。删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数,所以代码看起来像这样:
代码
class Lock{
public:
explicit Lock(Mutex* pm) //以某个Mutex初始化
: mutexPtr(pm,unlock) //并以unlock函数为删除器
{
lock(mutexPtr.get()); //条款15谈到"get"
}
private:
std::tr1::shared_ptr mutexPtr; //使用shared_ptr
} //替换raw pointer
本例中Lock class不再声明析构函数。因为没有必要,条款5说过,class析构函数(无论是编译器生成的,还是用户自定的)会自动调用其non-static成员变量(本例为mutexPtr)的析构函数。
- 复制底部资源。有时候只要你喜欢,可以针对一分资源拥有其任意数量的复件(副本)。而你需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包含的资源。也就是说,复制资源管理对象时,进行的是“深度拷贝”。
- 转移底部资源的所有权。 某些罕见的场合下你可能希望确保永远只有一个RAII对象指向一个未加工资源(new resource),即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。一如条款13所述,这是auto_ptr奉行的复制意义。
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
- 普遍而常见的RAII class copying行为是:抑制copying,施行引用计数法(reference counting)。不过其他行为也都可能被实现。
- APIs往往要求访问原始(raw resources),所以每一个RAII class应该提供一个"取得其所管理之资源"的方法。
- 对原始资源的访问可能经由显示转换和隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。
- 显示转换:RAII class(资源管理类)应该提供一个类似auto_ptr类中的get()方法一样的函数,用于取得RAII class管理的资源。
- 隐式转化:将RAII class隐式转换为其管理的资源的类型。代码实现需要运算符重载。
代码
//给类添加隐式转换的代码
class Font{
public:
...
operator double()const{return f;}//将类隐式的转换为double类型
...
};
eg.Font ft(); double ftt=ft;
- 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
- 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。(详细原因见书p76)
代码
std::tr1::shared_ptr pw(new Widget); //在单独语句内以智能指针存储newed所得对象
processWidget(pw,priority()); //这个调用动作绝不至于造成泄露
tr1::shared_ptr的构造函数是一个explicit构造函数
- 假设为一个用来表现日期的class设计构造函数:
class Date {
public:
Date(int month,int day,int year);
...
}
这段代码乍见之下同情达理,但它的客户很容易犯下至少两个错误:
- 他们也许会以错误的次序传参数:
Date d(30,3,1995); //哇哦!应该是"3,30"而不是"30,3"
- 他们可能传递一个无效的月份或天数:
Date d(2,30,1995); //哇哦!应该是"3,30"而不是"2,30"
解决方案:
class Date{
public:
Date(const Month& m,const Day& d,const Year& y);
...
}
令Day,Month,和Year成为成熟且经充分锻炼的classes并封装数据。
- 一旦正确的类型就定位,限制其值有时候是通情达理的。例如一年只有12个有效月份,所以Month应该反映这一事实。办法之一是利用enum表现月份,但enmus不具备我们希望拥有的类型安全性,例如enums可被拿来当一个ints使用,比较安全的解法是预先定义所有有效的Months:
class Month{
public:
static Month Jan() {return Month(1);} //函数,返回有效月份。
static Month Feb() {return Month(2);} //稍后解释为什么。
... //这些是函数而非对象
static Month Dec() {return Month(12);}
... //其他成员函数
private:
explicit Month(int m); //阻止生成新的月份(构造函数声明为private)
... //这是月份专属数据
};
Date d(Month::Mar(),Day(30),Year(1995));
如果“以函数替换对象,表现某个特定的月份”让你觉的诡异,或许是因为你忘记了non-locas static对象的初始化次序有可能出问题。建议阅读一下条款四。
预防客户错误的另一个方法,限制类型内什么事可做,什么事不能做。常见的限制是加上const。例如条款3曾经说明为什么“以const修饰operator*的返回类型”可阻止客户因“用户自定义类型”而犯错:
if(a*b=c)//哦,愿意其实是要做比较动作!
条款13表明客户如何将createInvestment的返回值存储于一个智能指针如auto_ptr或tr1::shared_ptr内,因而将delete责任推给智能指针。但万一客户忘记使用智能指针怎么办?许多时候,较佳接口的设计原则是先发制人,就如factory函数返回一个智能指针:
std::tr1::shared_ptr
(shared_ptr可以自己指定“删除器”,它的构造函数的第二个参数)crateIvestment(); tr1::shared_ptr有一个特别好的特性是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的"cross-DLL-problem"。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁”。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。tr1::shared_ptr没有这个问题,因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。
- 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
- “促进正确使用”的办法包括接口的一致性,以及于内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes;见条款14)
设计class就是设计一个新的type(类型),设计class时应该考虑以下的问题:(P84)
- 新的type的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值有什么样的差别?
- 新type的对象如果被passed by value(以值传递),意味着什么?
- 什么是新type的“合法值”?
- 你的新type需要配合某个继承图系(inheritance graph)吗?
- 你的新type需要什么样的转换?
- 什么样的操作符和函数对新type而言是合理的?
- 什么样的标准函数应该驳回?
- 谁该取用新type的成员?
- 什么是新type的"未声明接口"(undeclared interface)?
- 你的新type有多么一般化?
- 你真的需要一个新type吗?
- class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条覆盖的所有讨论主题。
- 采用pass by reference to const的效率高。
- 以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。这实在不怎么让人惊讶,因为正是base class构造函数建立了它。
- 以pass by reference通常意味着真正传递的是指针。因此如果你有一个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高。对内置类型而言,当你有机会选择采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非没有道理。
- “小型的用户自定义类型不必然成为pass-by-value优良候选人”的另一个理由是,作为一个用户自定义类型,其大小容易有所变化。
- 一般而言,你可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL 的迭代器和函数对象。
- 尽量以pass-by-reference-to-const 替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
- 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
- 切记将成员变量声明为private。这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性。
- protected并不比public更具有封装性。
- 类的封装性:能后访问class内之private成分的函数数量。
- 如果你要在memeber和一个non-memeber,non-friend函数之间做抉择,而且两者提供相同机能,那么,导致较大封装性的是non-memeber non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量。
- 有两点值得注意:
- 这个论述只适用于non-member non-friend函数。friends函数对class private成员的访问权力和member函数相同,因此两者对封装的冲击力道也是相同的。从封装的角度看,这里选择的关键并不在memeber和non-member函数之间,而是在member和non-member non-friend函数之间。
- 封装性让函数“成为class的non-member”,并不意味着它“不可以是另一个class的memebr”。例如我们可以有一个工具类,可以让non-member non-friend函数成为这个工具类的static member函数。
- 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性,包裹弹性和机能扩充性。
- 不能够只因为函数不该成为memeber,就自动让它成为friend;
*如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
- 一般而言,重载function templates没有问题,但std是一个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的templates,但不可以添加新的templates(或classes或functions或其他任何东西)到std里头。std的内容完全有c++标准委员会决定。
- 如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。
- 如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:
- 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。这个函数绝不该抛出异常。
- 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
3.如果你正在编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。 - 最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap;
- 调用swap时针对std::swap使用using声明式,然后调用swap并且不带任何"命名空间资格修饰"。
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。