explicit声明

class B
{
public: 
    explicit B(int x = 0, bool b = true) //默认构造函数,要么没有参数,要么所有参数都有默认值
    { 
        m_x = x; 
    }
public:
    int m_x;
};

void doSomething(B bObject)
{
    std::cout << bObject.m_x << endl;
}
int main()
{
    doSomething(B());  //ok   0
    doSomething(B(12)); //ok   12
    //doSomething(12);  //error 
    system("pause");
    return 0;
}

通过在构造函数前加explict,用以避免隐式类型转换。

条目1:把C++看成多种语言的联合体

 C++是一门多范型编程语言,可以将C++看成是一个由若干门语言组成的联合体:

  • C
  • 面向对象的C++
  • 包含模板的C++
  • STL

条目2:多用const、enum、inline,少用#define

#define ASPECT_RATIO 1.653替换为定义一个常量

const double AspectRatio=1.653;

对于类内部的常量,为了限制常量的份数超过一份,必须将该常量声明为static成员。

class GamePlayer
{
private:
    static const int NumTurns = 5; //常量声明
    int scores[NumTurns];
};
const int GamePlayer::NumTurns; //常量定义

类内部的为常量声明,而非定义。类内部的静态常量如果是整形(整数、字符型、布尔型)则不需要定义。只要你不需要得到它们的地址,你可以声明它们、调用它们而不需要定义。

对于简单的常量,多用const对象或者枚举类型数据,少用#define

多用内联函数来代替带参宏

条目3:尽可能使用const

char greeting[] = "Hello";
char *p1 = greeting;  //非const指针,非const数据
const char *p2 = greeting; //非const指针,const数据
char *const p3 = greeting; //const指针,非const数据
const char *const p4 = greeting; //const指针,const数据

const成员函数

const成员函数可以被具有相同参数列表的非const成员函数重载:

Effective C++Effective C++
 1 class TextBlock
 2 {
 3 private: 
 4     std::string text;
 5 public:
 6     TextBlock(std::string s)
 7     {
 8         text = s;
 9     }
10     char& operator[](std::size_t position)
11     {
12         return text[position]; //用于非const对象
13     }
14     const char& operator[](std::size_t position) const
15     {
16         std::cout << "const func" << endl;
17         return text[position]; //用于const对象
18     }
19 };
20 
21 int main()
22 {
23     TextBlock tb("Hello");
24     std::cout << tb[0] << endl;
25     const TextBlock ctb("World");
26     std::cout << ctb[0] << endl;
27     return 0;
28 }
View Code

关于const成员函数的两种说法:

  • 按位恒定:当且仅当一个成员函数对其所在对象的所有数据成员(static数据成员除外)都不做改动时,才需要将成员函数声明为const.但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。
  • 逻辑恒定:如果某个对象调用了一个const成员函数,则这个成员函数可以对对象作出内部改动,但仅仅是客户端无法察觉的方式进行。

条目4:确保对象在使用前得到初始化

读取未初始化的数据将导致未定义行为。在一些语言平台中,通常情况下读取未初始化的数据仅仅是使你的程序无法运行罢了。更典型的情况是,这样的读取操作可能会得到内存中某些位置上的半随机的数据,这些数据将会“污染”需要赋值的对象,最终,程序的行为将变得十分令人费解,你也会陷入烦人的除错工作中。

解决这类不确定的问题的最好途径是:总是在使用对象之前进行初始化。对于内置类型的非成员对象,需要手动完成这一工作。注意赋值和初始化的区别:

Effective C++

C++约定:一个对象的数据成员要在进入构造函数内部之前得到初始化。在进入ABEntry构造函数内部之前,这些数据成员的默认构造函数应该自动得到调用。注意对于numTimesConsulted成员不成立,因为其为内置类型,对其而言,在被赋值之前,无法确保其得到了初始化。

更好的方法是使用初始化表,这样效率会更高:

Effective C++

 C++对象中数据成员的初始化顺序为其在类中声明的顺序,而不是成员初始化列表中的顺序。为了使读者不至于陷入困惑,应保证初始化表中的顺序与声明时的顺序保持一致。

条目5:要清楚C++后台为你书写和调用了什么函数

对于一个类来说,如果不自己手动声明一个复制构造函数、赋值运算符、析构函数,编译器会自动声明这些函数,没有声明构造函数的话,编译器也会为你声明一个默认构造函数。所有这些函数都是public和inline的。

Effective C++

条目6:要显式禁止编译器为你生成不必要的函数

通常情况下,如果你希望一个类不支持某种特定的功能,你需要做的仅仅是不去声明那个函数。然而这一策略对复制构造函数和拷贝赋值运算符就失效了,这是因为,即使你不做声明,而一旦有人尝试调用这些函数,编译器就会为你自动声明它们(参见条目 5)。解决问题的关键是,所有编译器生成的函数都是公共的。为了防止编译器生成这些函数,将复制构造函数和赋值运算符声明为私有的。通过显式声明一个函数,你就可以防止编译器去自动生成这个函数,同时,通过将函数声明为private的,你便可以防止人们去调用它。同时为了防止其他成员函数或者友元函数访问这些private函数,可将这些private成员函数只声明而不进行定义。

Effective C++Effective C++
class HomeForSale
{
public:
    HomeForSale() {}
private:
    HomeForSale(const HomeForSale&);//只有声明,无定义
    HomeForSale& operator=(const HomeForSale&); //只有声明,无定义
};
View Code

条目7:要把多态基类的析构函数声明为虚函数

C++有明确的规则:如果希望通过一个基类类型的指针来删除一个派生类对象,并且基类的析构函数为非虚析构函数,则结果是未定义的。典型的后果是,运行中派生类中新派生出的部分将得不到销毁,基类部分会被销毁掉,这样就产生了一个古怪的“部分销毁”的现象。

排除这一问题的方法很简单:为基类提供一个虚拟的析构函数,这样删除一个派生类对象,程序就可以精确地按照需要进行了,这个对象都会得到销毁。任何有虚函数的类几乎都需要一个虚析构函数,如果一个类不包含虚函数,则通常情况下意味着它将不作为基类使用。当一个类不作为基类使用时,将它的析构函数声明为虚函数不是个好主意

条目8:防止因异常中止析构函数

Effective C++

Effective C++

 条目9:永远不要在构造或者析构的过程中调用虚函数

创建一个派生类的对象时,基类的构造函数优先于派生类的构造函数运行,在基类构造函数运行的时候,派生类的数据成员还未得到初始化。对于一个派生类的对象来说,在其进行基类部分构造工作的时候,这一对象的类型就是基类的。不仅仅虚函数会解析为基类的,而且 C++中“使用运行时类型信息”的部分(比如 dynamic_cast(参见条目 27)和typeid)也会将其看作基类类型的对象。

对于析构过程可以应用同样的推理方式。一旦派生类的析构函数运行完毕,对象中派生类的那一部分数据成员将取得未定义的值,所以 C++会认为它们不再存在。在进入基类的析构函数时,这个对象将成为一个基类对象,C++的所有部分——包括虚函数、dynamic_cast 等等——都会这样对待该对象。

Effective C++

条目10:让赋值运算符返回一个指向*this的引用

int x, y, z;
x = y = z = 15; //一连串的赋值操作

这种实现的本质是:赋值时,返回一个指向运算符左边对象的引用。当为你的类实现赋值运算符时,应遵守这一惯例,这一惯例对所有的赋值运算符同样适用。

Effective C++Effective C++
 1 class Widget
 2 {
 3 public:
 4     Widget() {}
 5     Widget& operator=(const Widget& rhs)
 6     {
 7         //other code
 8         return *this; //返回运算符左边的对象
 9     }
10     Widget& operator+=(const Widget& rhs)
11     {
12         //other code
13         return *this; //返回运算符左边的对象
14     }
15 };
View Code

条目11:在operator=中要处理自赋值问题

条目12:要复制整个对象,不要遗漏人一部分

当没有手动定义拷贝成员函数时(拷贝构造函数和拷贝赋值运算符),编译器将自动生成拷贝函数,且自动生成的拷贝函数可以精确地按你所期望的方式运行,当前正在赋值的所有对象都会得到复制。然而当自己声明拷贝函数时,如果拷贝函数内只进行部分复制,编译器不会给出任何警告和错误。通过继承,这一问题可以带来更加严重却隐蔽的危害。

Effective C++Effective C++
 1 void logCall(const std::string& msg)
 2 {
 3     ///
 4 }
 5 class Customer
 6 {
 7 public:
 8     Customer():name("unknown"){}
 9     Customer(const std::string& s) :name(s) {}
10     Customer(const Customer& rhs):name(rhs.name)
11     {
12         logCall("Customer copy constructor");
13     }
14     Customer& operator=(const Customer& rhs)
15     {
16         logCall("Customer copy assignment operator");
17         name = rhs.name;
18         return *this;
19     }
20 private:
21     std::string name;
22 };
23 ////////////////////////////////////////
24 class PriorityCustomer :public Customer
25 {
26 public:
27     PriorityCustomer() :priority(0) {}
28     PriorityCustomer(const PriorityCustomer& rhs);
29     PriorityCustomer& operator=(const PriorityCustomer& rhs);
30 private:
31     int priority;
32 };
33 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :priority(rhs.priority)
34 {
35     logCall("PriorityCustomer copy constructor");
36 }
37 PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
38 {
39     logCall("Customer copy assignment operator");
40     priority = rhs.priority;
41     return *this;
42 }
View Code

拷贝时,PriorityCustomer对象从基类继承而来的成员始终没有得到复制。一旦你亲自为一个继承类编写了拷贝函数,你必须同时留心其基类的部分。当然这些部分通常情况下是私有的,所以你无法直接访问它们。取而代之的是,派生类的拷贝函数必须调用这些私有数据在基类中相关的函数。

Effective C++Effective C++
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    :Customer(rhs),  //调用基类的拷贝构造函数
    priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("Customer copy assignment operator");
    Customer::operator=(rhs); //为基类部分赋值
    priority = rhs.priority;
    return *this;
}
View Code

 条目13:要使用对象来管理资源

Effective C++

为了确保createInvestment()所返回的资源总能得到释放,我们需要将这类资源放置在一个对象中,以来C++对默认析构函数的自动调用来确保资源及时得到释放。标准库中的auto_ptr是类似于指针的对象(智能指针),其析构函数可以自动对其所指内容执行delete。

Effective C++

由于当一个 auto_ptr 被销毁时,它将自动删除其所指向的内容,所以永远不存在多个 auto_ptr 指向同一个对象的情况,这一点很重要。如果存在的话,这个对象就会被多次删除,这样你的程序就会立即陷入未定义行为。为了防止此类问题发生,auto_ptr 有一个不同寻常的特性:如果你复制它们(通过拷贝构造函数或者拷贝赋值运算符),它们就会被重设为 null,然后资源的所有权将由复制出的指针独占

引用计数智能指针是auto_ptr的替代品,它可以跟踪有多少个对象指向了一个特定的资源,同时在没有指针再指向这一资源时,智能指针会自动删除该资源。可以看出引用计数智能指针的行为和垃圾回收器相似。

Effective C++

auto_ptr和tr1::shared_ptr在析构函数中使用的是delete语句,而不是delete[]。这就意味着对于动态分配的数组使用auto_ptr和tr1::shared_ptr不是一个好主意。但是遗憾的是,这样的代码会通过编译。

条目14:要注意资源管理类中的复制行为

条目15:要为资源管理类提供对原始资源的访问权

条目16:互相关联的new和delete要使用相同的形式

std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
delete stringPtr1; 
delete[] stringPtr2;

对stringPtr1调用delete[],或者对stringPtr2调用delete,都会导致未定义的行为。这里的规则很简单:如果你在一个 new 语句中使用了[],那么你必须在相关的delete 语句中也使用[]。如果你在一个 new 语句中没有使用[],那么在相关的delete 语句中也不应使用[]。

条目17:用智能指针存储由new创建的对象时要使用独立的语句

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

在编译器能够生成对processWidget的调用前,必须对传入的参数进行处理,编译器必须自动生成代码来解决下面三件事情:1.调用priority(),2.执行new Widget,3.调用tri::shared_ptr的构造函数。C++编译器对这三项任务完成的顺序要求的很宽松,调用priority()可能出现在第2步,如果调用priority抛出异常的话,则第一步new Widget返回的指针将会消失,这种情况下,processWidget()可能会造成资源泄露。这是因为:在资源被创建(通过 new Widget)以后和将这个资源转交给一个资源管理对象之前的这段时间内,有产生异常的可能。防止这类问题发生的办法很简单:使用单独的语句,创建 Widget 并将其存入一个智能指针,然后将这个智能指针传递给 processWidget。

Effective C++

 条目18:要让接口易于正确使用,而不易被误用

 条目19:要像设计类型一样设计class

条目20:传参时要多用“引用常量”,少用传值

默认情况下,C++为函数传入和传出对象是采用传值方式的(这是从C语言继承而来的特征)。除非你明确使用其他方法,否则函数的形参总是通过复制实参的副本来创建,而且,函数的调用者得到的也是函数返回值的副本。这些副本是由对象的拷贝构造函数创建的。

bool validateStudent(const Student& s);

通过引用传参也可以避免“截断问题”。当一个派生类的对象以一个基类对象的形式传递(传值方式)时,基类的拷贝构造函数就会被调用,此时,这一对象的独有特征——使它区别于基类对象的特征会被“截掉”。剩下的只是一个简单的基类对象,这并不奇怪,因为它是由基类构造函数创建的。通过传递常量引用,可以避免截断问题。

C++编译器中,引用通常是以指针的形式实现的,所以通过引用传递实质是传递一个指针。传递一个内置数据类型的对象,传值会比传递引用更为高效,迭代器和 STL 中的函数对象也是如此,这是因为它们设计的初衷就是能够更适于传值,这是 C++的惯例。

条目21:在必须返回一个对象时,不要尝试返回一个引用

Effective C++

这个函数会返回一个指向result的引用,但result为一个局部对象,局部对象在函数退出时会被销毁。事实上,任何返回局部对象的引用的函数都是灾难性的(任何返回指向局部对象的指针的函数也是灾难性的)

条目22:将数据成员声明为私有的

条目23:多用非成员非友元函数,少用成员函数

多用非成员非友元函数,少用成员函数。这样做可以增强封装性,以及包装的灵活性和功能的扩展性。

Effective C++

条目24:当函数所有参数都需要进行类型转换时,要将其声明为非成员函数

条目25:最好不要让swap抛出异常

条目26:定义变量的时机越晚越好

条目27:尽量少用转型操作

Effective C++

 条目28:不要返回指向对象内部部件的“句柄”

  条目29:力求代码做到“异常安全”

 

相关文章: