Effective C++学习笔记

1.视c++为一个语言联邦

  1. C

  2. 面向对象的C++

  3. Template C++ 泛型编程

  4. STL

2.尽量以const,enum,inline替换 #define

宁可以编译器换预处理器

1.#define错误

#define ASPECT_RATIO 1.111

错误:你所使用的名称可能并未进入记号表

解决:const double AspectRatio = 1.111//大写通常用于宏

2.特殊情况

1.定义常量指针( constant pointers)。

由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。

const char* const authorName = "scott Meyers"; 
const std:: string authorName ( "scott Meyers");
2.class专属常量

将常量的作用域限制于class内

class GamePlayer{
private:
static const int NumTurns=5;
enum{ NumTurns=5};
int scores[NumTurns];
}

2.实现宏错误

#define CALL_WITH_ MAX(a,b) f((a) > (b)? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX (++a,b);
//a被累加二次
CALL_WITH_MAX (++a,b+10);
//a被累加一次

解决:
template<typename T>
//由于我们不知道
inline void callwithMax (const T& a,const T& b)
//T是什么,所以采用
//pass by reference-to-const.
f(a > b ? a : b);
//见条款20

3.小结

有了consts、enums和 inlines,我们对预处理器(特别是#define)的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但你应该明确地给予它更长更频繁的假期。

请记住:

  1. 对于单纯常量,最好以const对象或enums替换#defines。

  2. 对于形似函数的宏(macros),最好改用inline函数替换#defines。

3.尽可能使用const

1.const规则

1.指针

const语法虽然变化多端,但并不莫测高深。如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

2.const修饰函数参数

表示参数不可变,若参数为引用,可以增加效率(引用传递而不用值拷贝)

3.const 修饰函数返回值

避免返回值被修改

如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。

例如函数

const char * GetString(void);
//如下语句将出现编译错误:
char *str = GetString();
//正确的用法是
const char *str = GetString();
4.const修饰类的成员变量(class)

表示成员变量不能被修改,同时只能在初始化列表中赋值

5.const修饰类的成员函数(class)
  1. 不能修饰全局函数,因为全局函数没有this指针

  2. 该函数不能修改成员变量,方法:在变量前加 mutable ,可以更改

  3. 不能调用非const成员函数,因为任何非const成员函数会有修改成员变量的企图

6.const修饰类对象(class)
  1. 对象的任何成员都不能被修改

  2. const类对象只能调用const成员函数

7.类中的所有函数都可以声明为const函数吗。哪些函数不能?(class)
  1. 构造函数不能

  2. 因为const修饰的成员函数不能修改成员变量。但是构造函数恰恰需要修改类的成员变量

  3. static静态成员函数不能

  4. static静态成员是属于类的,而不属于某个具体的对象,所有的对象共用static成员。this指针是某个具体对象的地址,因此static成员函数没有this指针。而函数中的const其实就是用来修饰this指针的,表示this指向的内容不可变,static静态成员却没有this指针,所以const不能用来修饰static成员函数

2.STL迭代器

iterator和const_iterator

std: : vector<int> vec;...
const std: :vector<int>::iterator iter =vec.begin ( );//iter的作用像个T*const
*iter = 10;
//没问题,改变iter所指物
++iter;
//错误! iter是const
std: :vector<int> :: const_iterator citer =vec.begin ( );//cIter的作用像个const T*
*cIter = 10;
//错误!*cIter是const
++cIter;
//没问题,改变cIter.

3.const成员函数

1.它们使 class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。

2.它们使“操作const对象”成为可能。这对编写高效代码是个关键,因为如条款20所言,改善C+程序效率的一个根本办法是以pass byreference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。

4.小结

  1. 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  2. 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”( conceptual constness)。
  3. 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。(P55)

4.确定对象被使用前已确定先被初始化

1,构造函数初始化

ABEntry: :ABEntry (const std::string& name,const std::string& address,
const std: : list<PhoneNumber>& phones){

theName = name;

theAddress = address;

thePhones = phones;
//这些都是赋值(assignments) 而非初始化(initializations)
numTimesConsulted = 0;
}
  1. C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。

  2. 在 ABEntry构造函数内,theName,theAddress和 thePhones 都不是被初始化,而是被赋值。

  3. 初始化的发生时间更早,发生于这些成员的default 构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。

  4. 但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。

ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作:

ABEntry : :ABEntry(const std::string& name,const std::string& address,
const std: : list<PhoneNumber>& phones)
:theName (name) ,
theAddress (address) ,
thePhones (phones) ,
//现在,这些都是初始化(initializations)
nunTimesConsulted (o){ }
//现在,构造函数本体不必有任何动作
{}

这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本(本例第一版本)首先调用default构造函数为theName, theAddress和thePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列( member initialization list)的做法避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的theName以nane为初值进行copy构造,theAddress 以 address为初值进行copy构造,thePhones 以 phones为初值进行copy构造。

2.小结

  1. 为内置型对象进行手工初始化,因为C+t不保证初始化它们。
  2. 构造函数最好使用成员初值列( member initialization list),而不要在构造函数本体内使用赋值操作( assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
  3. 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-localstatic对象。

5.了解C++默默编写并调用了哪些函数

class Empty { };
//这就好像你写下这样的代码:
class Empty {
public:
Empty() { ... }
//default构造函数
Ermpty(const Empty& rhs) { ... }
//copy构造函数
~Empty( ) { ... }
//析构函数
Empty& operator=(const Emptys rhs){ ... }
//copy assignment操作符.
};

6.若不想使用编译器自动生成的函数,就应该明确拒绝

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。

private: Uncopyable (const Uncopyable&) ; //但阻止 copying Uncopyable& operator=(const Uncopyable&) ;

7.为多态基类声明virtual析构函数

1.情况

这是一个引来灾难的秘诀,因为CH+明白指出,当derived class对象经由一个baseclass指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived 成分没被销毁。如果getTimeKeeper返回指针指向一个AtomicClock 对象,其内的 AtomicClock 成分(也就是声明于Atomicclock class内的成员变量〉很可能没被销毁,而atomicClock的析构函数也未能执行起来。然而其 base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。这可是形成资源泄漏、败坏之数据结构、在调试器上浪费许多时间的绝佳途径喔。

2.方法

消除这个问题的做法很简单:给base class一个virtual析构函数.此后删除derivedclass对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class成分

3.建议

  1. 有不同的实现码。任何 class只要带有 virtual函数都几乎确定应该也有一个virtual析构函数。

  2. 如果class不含virtual函数,通常表示它并不意图被用做一个base class。

  3. 当class不企图被当作base class,令其析构函数为virtual往往是个馒主意。

4.抽象类

class AwOV {

public:

virtual ~AWOV { ) = 0;

};

这个class有一个pure virtual函数,所以它是个抽象class,又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。

然而这里有个窍门:你必须为这个pure virtual析构函数提供一份定义: AwOV : : ~AWOV() { }

5.小结

  1. polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

  2. Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性( polymorphically),就不该声明virtual析构函数。

8.别让异常逃离析构函数

小结

  1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

  2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

9.绝不在构造和析构过程中调用virtial函数

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层))。

#include <iostream> 
using namespace std;

class Base
{
public:
Base();
virtual ~Base();
virtual void print();
};
Base::Base()
{
cout << "Base is Constructed" << endl;
print();
}
Base::~Base()
{
cout << "Base is Destroyed" << endl;
print();
}
void Base::print()
{
cout << "print Base" << endl;
}

class Derived :public Base
{
public:
Derived();
~Derived();
void print();
};
Derived::Derived()
{
cout << "Derived is Constructed" << endl;
}
Derived::~Derived()
{
cout << "Derived is Destroyed" << endl;
}
void Derived::print()
{
cout << "print Derived" << endl;
}

int main()
{
Derived der;
return 0;
}

10.令operator = 返回一个reference to *this

int x, y, z; x = y = z = 3; class widget { public: widget& operator+= (const widget& rhs) { … return *this; }

11.在**operator = 中处理“自我赋值”**

widget& widget : :operator= (const widget& rhs){
if (this == &rhs) return *this);//证同测试(identity test)
delete pb;
pb = new Bitmap (*rhs.pb);return *this;
}

小结

  1. 确保当对象自我赋值时operator=有良好行为。

  2. 其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。

  3. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

12.复制对象时勿忘其每一个成分

任何时候只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是 private(见条款22),所以你无法直接访问它们,你应该让 derived class 的 copying函数调用相应的base class函数:

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer (rhs) ,
//调用base class的copy构造函数
priority (rhs.priority)
{}
PriorityCustomer&
PriorityCustomer: :operator=(const PriorityCustomer& rhs){
customer : :operator=(rhs) ;
//对base class 成分进行赋值动作
priority = rhs.priority;
return *this;
}
  1. 复制所有local 成员变量
  2. 调用所有base classes 内的适当的copying函数。

13.以对象管理资源

1.情况

Investment* createInvestment();
void f( )
{
Investment* pInv = createInvestment ( ) ;
...
delete pInv;
}

…中可能return,continue,抛出异常,使得delete语句不执行

2.解决

标准程序库提供的auto_ptr 正是针对这种形势而设计的特制产品。

auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。

void f( ){
std::auto_ptr<Investment> pInv (createInvestment ( ));
std::trl::shared_ptr<Investment> pInv (createInvestment ( ));//可以复制
}
  1. 由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。

  2. 如果真是那样,对象会被删除一次以上,而那会使你的程序搭上驶向“未定义行为”的快速列车上。

  3. 为了预防这个问题,auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!

  4. trl: :shared ptr和 auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

14.在资源管理类中小心coping行为

Coping函数(包括copy构造函数和 copy assignmen 操作符)有可能被编译器自动创建出来,因此除非编译器所生版本做了你想要做的事(条款5提过其缺省行为),否则你得自己编写它们。

小结:

  1. 复制RAII 对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

  2. 普遍而常见的RAI class copying 行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。

15.在资源管理类中提供对原始资源的访问

  1. 资源管理类(resource-managing classes)很棒。它们是你对抗资源泄漏的堡垒。排除此等泄漏是良好设计系统的根本性质。在一个完美世界中你将倚赖这样的classes来处理和资源之间的所有互动,而不是玷污双手直接处理原始资源( rawresources)。但这个世界并不完美。许多APIs直接指涉资源,所以除非你发誓(这其实是一种少有实际价值的举动)永不录用这样的APIs,否则只得绕过资源管理对象(resource-managing objects)直接访问原始资源( raw resources)。

  2. APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供-个“取得其所管理之资源”的办法。

  3. 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

16.成对使用new和delete时要采取相同形式

规则:如果你调用new时使用[ ],你必须在对应调用delete时也使用[ ]。如果你调用new时没有使用[ ],那么也不该在对应调用delete时使用[ ]。

std::string*stringPtr1 = new std::string;
std::string*stringPtr2 = new std::string [ 100];
delete stringPtr1;
//删除一个对象
delete [ ] stringPtr2;
//删除一个由对象组成的数组

17.以独立语句将newed对象置入智能指针

待续—————————