开源中国

#@date:			2014-06-16
#@author: gerui
#@email: forgerui@gmail.com

  前几天买了好几本书,其中有一本是《Effective C++》,准备好好学习一下C++.书中提出了55条应该遵循的条款,下面将逐一学习。点击查看evernote原文

一、让自己习惯C++

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

  将C++分为4个次语言。即:C, Objective-Oriented C++, Template C++, STL.

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

  宁可以编译器替换预处理器:

  1) 预处理器`#define N 1.653' 将所有出现N的地方替换为1.653,当出现错误报的是1.653导致目标有问题,而不是N。如果使用变量,则可轻易地判断。此外,替换会造成代码在多处出现,增加代码量。所以尽量定义为常量,const double N = 1.653;

  2) 如果在数组初始化的时候,编译器需要知道数组的大小,这样,不可以使用变量进行数组初始化,这时#define可以,但我们最好使用enum{N=3;}来替代define.

  3) 使用#define定义一个三目运算符也会产生问题,如果你想获得高效,建议使用inline内联函数。

  但#include,以及#ifdef/#ifndef都是必需的,但我们要尽量限制预处理器的使用。

3. 尽可能使用 const

  1) const 表示不可以改变,如果修饰变量,则表示这个变量不可变,如(a);如果修饰指针,表示指针指向的位置不可改变,如(b)。

const char * p = "hello"; //(a) *p的hello不可变, 与char const * p = "hello"等价
char * const p = "hello"; //(b) 表示p的值不可变,即p不能指向其它位置

  2) STL迭代器的const

std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //类似T* const
*iter = 10; //没问题,改变iter所指物
++iter; //错误!iter是const
std::vector<int>const_iterator cIter = vec.begin(); //类似const T*
*iter = 10; //错误,*iter是const
iter++; //没问题,可以改变iter

  3) 使函数返回一个常量值,可以避免意外错误。如下代码,错把==写成=,一般程序对*号之后进行赋值会报错,但在自定义操作符面前不会(因为自定义*号后返回的是Rational对象实例的引用,可以拿来赋值,不会报错)。如果*不写成const,则下面的程序完全可以通过,但写成const之后,再对const进行赋值就出现问题了。

class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
Rational a, b, c;
if(a * b = c); //把==错写成=,比较变成了赋值

  4) 函数的参数,如果无需改变其值,尽量使用const,这样可以避免函数中错误地将==等于符号误写为=赋值符号,而无法察觉。

  5) const作用于成员函数,两个作用,a)可以知道哪些函数可以改变成员变量,哪些函数不可以;b)改善C++效率,通过reference_to_const(即const对象的引用)方式传递对象。下面是常量函数与非常量函数的形式:

class TextBlock{
public:
...
const char& operator[] (std:size_t position) const{
return text[position];
}
char& operator[] (std:size_t position) {
return text[position];
}
private:
std::string text;
}; /**
*使用operator[]
*/
TextBlock tb("hello"); //non-const 对象
cout<<tb[0]<<endl; //调用的是non-const TextBlock::operator[]
tb[0] = 'x'; //没问题,写一个non-const对象 const TextBlock cTb("hello"); //const 对象
cout<<cTb[0]<<endl; //调用的是const TextBlock:operator[]
cTb[0] = 'x'; //错误,写一个const对象

  6) bitwise const主张const成员函数不可以改变对象内任何non-static成员变量;logical const主张成员函数可以修改它所处理的对象内的某些bits,但要在客户端侦测不出的情况下才得如此。编译器默认执行bitwise。如果想要在const函数中修改non-static变量,需将变量声明为mutable(可变的)

class TextBlock{
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
public:
...
std::size_t length() const;
}; std::size_t TextBlock::length() const{
if (!lengthIsValid){
textLength = std::strlen(pText); //加上mutable修饰后,便可以修改其值
lengthIsValid = true;
}
}

  7) 避免const和non-const成员函数重复

  思想很简单,如果const和non-const成员函数功能相当时,就用non-const函数去调用const函数(不能反过来...o_O)。

class TextBlock{
public:
const char& operator[](std:size_t position) constP
...
return text[position];
} char& operator[] (std:size_t position){
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
};

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

  1) 对内置类型(基本类型)手动进行初始化。

int x = 0;
const char* p = "Hello World";
double d;
std:cin >> d;

  2) 内置类型以外的类型,初始化要靠构造函数。类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操作。初值列成员变量的排列顺序与其声明顺序相同。

class PhoneNumber { ... };
class ABEntry {
public:
ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numberTimesConsulted;
}; ABEntry::ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones){
theName = name; //这些都是赋值,而非初始化
theAddress = address;
thePhones = phones;
numberTimesConsulted = 0;
} /**
*使用成员初值列,效率更高
*/
ABEntry::ABEntry(const std:string& name, const std::string& address, const std:list<PhoneNumber>& phones)
:theName(name), theAddress(address), thePones(phones), numberTimesConsulted(0) //成员初值列
{
...
}

  3) 为避免"跨编译单元之初始化次序"问题,以local static对象替换non-local static对象。

//FileSystem源文件

class FileSystem{

public:

...

std::size_t numDisks() const;

};

extern FileSystem tfs;

//Directory源文件,与FileSystem处于不同的编译单元
class Directory{
public:
Directory(params);
...
};
Directory::Directory(params){
...
//调用未初始化的tfs会出现错误
std::size_t disks = tfs.numDisks();
}

  这样的话,Directory类会调用一个non-local的tfs,而这个tfs未必经历了初始化处理。我们要有效避免这个情况,使获取的tfs对象保证是初始的,可以使用如下的一个函数获取,这就像Singleton(单例)模式一样。

class FileSystem { ... };
FileSystem& tfs(){
static FileSystem fs;
return fs;
} class Directory { ... };
Directory::Directory(params){
std::size_t disks = tfs().numberDisks();
} Directory& tempDir(){
static Directory td;
return td;
}

  经过上面的处理,将non-local转换了local对象,这样做的原理是:函数内的local static 对象会在"该函数被调用期间","首次遇上该对象之定义式"时被初始化,这样就保证了对象被初始化。这样做的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。

二、构造/析构/赋值运算

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

  1) 编译器会自动为class创建default构造函数、copy构造函数、copy assignment操作符、以及析构函数。

  2) 如果用户声明了一个构造函数,则编译器则不会再为它声明default构造函数。

  3) 拷贝构造函数可以通过=()实现。默认的拷贝构造函数对指针进行地址的复制,这样会产生多个对象共用一块地址,会产生问题,可以自己实现拷贝构造函数,实现值的复制。

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

  1) 不允许用户进行对象的拷贝。一般编译器会提供默认拷贝,可将相应的成员函数声明为private并且不予以实现。但这是有个问题,member(成员)函数和friend(友元)函数仍然可以调用。

  2) 在不想实现的函数中不写函数参数的名称。

class HomeForSale{
public:
...
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};

  3) 将错误移至编译期,更早地发现错误往往更好。定义一个Uncopyable的基类,其它类继承该类,当执行拷贝时,要调用基类拷贝构造函数,就会出现问题。

class Uncopyable{
protected:
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};

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

  1) polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数。这样,每个派生类都要实现析构函数,防止指向derived classes的对象没有析构函数。

  2) 如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

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

  4) 含有纯虚函数的类是抽象类。

class AWOV{
public:
virtual ~AWOV() = 0; //纯虚函数
};

  5) 不是所有类都是被设计作为基类来使用的。如string类和STL容器类,所以这些类不需要声明为virtual。

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

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

class DBConnection{
public:
static DBConnection create();
void close();
}; class DBManager{
public:
~DBManager(){
db.close(); //析构函数关闭数据库连接
} private:
DBConnection db;
}; //调用析构函数时可能会发生异常
DBManager dbM(DBConnection::create());

  上面的代码为了帮助忘记关闭数据库连接的客户关闭连接,在析构函数中调用了close函数,但这个函数可能出现异常,这种必须调用可能产生异常的函数时,需要进行异常捕获。如下:

DBManager::~DBManager(){
try { db.close(); }
catch(...){
//可以记录错误后退出程序
std::abort();
}
}

  2) 上面这个问题还不是完善的方案,即使析构函数捕获到异常,客户也无法处理异常,客户需要对某个函数运行期间抛出的异常进行反应,那么class应该提供一个普通函数来执行该操作。

class DBManager(){
public:
void close(){
db.close();
closed = true;
}
~DBManager(){
if (!closed){
try { db.close(); }
catch(...) {
//错误日志...
}
}
}
private:
bool closed;
DBConnection db;
};

  这里面加了一个close函数,客户可以自己调用close函数,当发生异常时,进行异常处理。如果客户没有调用close函数,则可以在析构函数中自动调用。所以,在写程序时,一定要将会发生异常的函数作为一个普通函数,这样可以提供更多的选择。

###学习《Effective C++》的更多相关文章

  1. 鸟哥的LINUX私房菜基础篇第三版 阅读笔记 一

    1. Linux的档案权限与目录配置      一.基础知识:             a.分为三类,拥有者(owner).群组(group).其他人(other)             b.三个核 ...

  2. 鸟哥的LINUX私房菜基础篇第三版 阅读笔记 四 档案的文件系统的压缩和打包

    1.压缩文件案的用途与技术     a.用途,简单来说,就是节约磁盘空间.如果从传输角度讲,占用宽带也会小很多(Apache就有自动压缩的功能,节省宽带资源,提升网站的输出能力)     b.压缩技术 ...

  3. 鸟哥的LINUX私房菜基础篇第三版 阅读笔记 三 Linux磁盘与文件系统管理

    一.认识EXT2文件系统:     a.硬盘的组成:转动小马达+存储的磁盘+读写的机械臂     b.磁盘的一些概念              扇区为最小的物理储存单位,每个扇区为512B       ...

  4. 鸟哥的LINUX私房菜基础篇第三版 阅读笔记 二

    Linux档案与目录管理 1.一些比较特殊的目录,需要用力的记下来 .         代表当前层目录 ..        代表上一层目录 -        代表前一个工作目录   (这个好屌!其他的 ...

  5. 《鸟哥的Linux私房菜--基础篇》学习

    第四章 显示日期与时间的指令:date 输入: (base) liyihuadeMacBook-Pro:~ liyihua$ date 输出: Thu Jun 6 08:44:02 CST 2019 ...

  6. 拒绝从入门到放弃_《鸟哥的 Linux 私房菜 — 基础学习篇(第三版)》必读目录

    目录 目录 前言 关于这本书 必看知识点 最后 前言 相信部分刚进入这个行业的新同学会对一个问题感到疑惑,为什么从培训学校出来的学员不被欢迎? 这里记录下一些我个人的看法(博主也曾有面试新员工的经历) ...

  7. 每周一书-《鸟哥的Linux私房菜基础学习篇(第四版)》台湾原版,你想要吗?

     首先说明,本周活动有效时间为2016年10月19日到2016年10月31日.   目在介绍这本书之前,首先要感谢QQ号为:1084830483(路在远方),来自哈尔滨工程大学的同学赠送给玄魂工作室的 ...

  8. 【Linux】鸟哥的Linux私房菜基础学习篇整理(一)

    最近,一直在写PPC的模拟器和汇编器,也在做设计.所以重新看了看<鸟哥的Linux私房菜>,还是有好多命令不太熟悉.就打算写几篇blog记下来. 1. nl [-bnw] filename ...

  9. 《鸟哥的Linux私房菜-基础学习篇(第三版)》(三)

    第2章 Linxu怎样学习         1. Linux当前的应用角色 当前的Linux常见的应用可略分为企业应用和个人应用双方面. 首先谈了企业环境的利用. 1)网络server. 2)关键任务 ...

  10. 鸟哥的Linux私房菜 基础学习篇读书笔记(9):Linux磁盘与文件系统管理(2)

    上一篇文章主要从理论上分析了Linux的Ext2文件系统.这一篇主要解说怎样查看Linux的文件系统的容量以及解说Linux文件系统中的连接文件. 能够通过df和du命令来查看磁盘与文件夹的容量.df ...

随机推荐

  1. iOS block并发

    多核运算 在iOS中concurrency编程的框架就是GCD(Grand Central Dispatch), GCD的使用非常简单.它把任务分派到不同的queue队列来处理.开发者把任务代码装到一 ...

  2. C++中实现从std::string类型到bool型的转换

    利用输入字符串流:std::istringstream bool b; std::string s = "true"; std::istringstream(s) >> ...

  3. JavaScript要点 (六) 函数参数

    JavaScript 函数对参数的值没有进行任何的检查. 函数显式参数(Parameters)与隐式参数(Arguments) 函数的显式参数: functionName(parameter1, pa ...

  4. 理解shared_ptr<T>

    1.shared_ptr<T>解决什么问题? auto_ptr有个局限,拥有权转移.这往往不符合我们的需求,有时候我们期望,多个资源管理对象可以共享一个资源,当引用计数为0的时候,执行de ...

  5. zookeeper使用场景【转】

    分布式网站架构后续:zookeeper技术浅析   Zookeeper是hadoop的一个子项目,虽然源自hadoop,但是我发现zookeeper脱离hadoop的范畴开发分布式框架的运用越来越多. ...

  6. MongoDB入门简单介绍

    有关于MongoDB的资料如今较少,且大多为英文站点,以上内容大多由笔者翻译自官网,请翻译或理解错误之处请指证.之后笔者会继续关注MongoDB,并翻译“Developer Zone”和“Admin ...

  7. js上传文件获取客户端地址

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  8. valgrind 生成mysqld调用图之 select now()跟踪

    1.mysqld起动方式: 1.mysqld以root用户运行 valgrind --tool=callgrind --separate-threads=yes  --trace-children=y ...

  9. [转]详述DHCP服务器的三种IP分配方式

    DHCP就是动态主机配置协议(Dynamic Host Configuration Protocol),它的目的就是为了减轻TCP/IP网络的规划.管理和维护的负担,解决IP地址空间缺乏问题.这种网络 ...

  10. MySQL的数据类型(转)

    MySQL的数据类型 1.整数 TINYINT: 8 bit 存储空间 SMALLINT: 16 bit 存储空间 MEDIUMINT: 24 bit 存储空间 INT: 32 bit 存储空间 BI ...