您的位置:首页 > 编程语言 > C语言/C++

Effective C++第一章

2017-06-21 16:45 197 查看

为了理解C++,将其分为四个次语言:

次语言
面向对象class(析构函数,构造函数)、封装、继承、多态、virtual函数….
Template C++C++泛型编程部分
STL是一个template程序库,容器、迭代器、算法以及函数对象..
C区块(blocks)、语句、预处理器、内置数据类型、数组。指针等统统来自C。但C语言的局限:没有模板、没有异常、没有重载…
当从某次语言切换到另一个,可能高效编程守则会要求你改变策略。例如对内置类型而言,pass by value通常比pass by reference高效,但当你从C到面向对象时,由于用户自定义构造函数和析构函数的存在,pass by reference to const往往更好,运用template C++尤其如此。但是STL中,迭代器和函数对象都是在C指针上塑造出来的,所以对STL的迭代器和函数对象而言,旧式的C pass by value守则再次适用。

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

例如
#define RATIO 1.653
可以替换为
const double Ratio = 1.653;
因为#define不被视为语言的一部分,所以记号名称
RATIO
可能没进入记号表,但作为语言常量的
Ratio
肯定会被编译器看到,当然就会进入记号表。

以常量替换#define,有两种特殊情况

定义常量指针(指向常量的指针(变量)),由于常量定义式通常被放在头文件内,因此有必要将指针(不只是指针所指之物)声明为const。

关键词const出现在
*
左边,被指物是常量;在
*
号右边指针是常量;如果出现在
*
两边则被指物和指针都是常量。

在C/C++中,常量指针(non-const pointer,const data)这样声明:

1)const int *p;
2)int const *p;
//两种写法意义相同


在C/C++中,指针常量(const pointer , non-const data)这样声明:

int a;
int *const b = &a; //const放在指针声明操作符的右侧


还有指物和指针都是常量:

const char* const p;        //const pointer , const data


class专属常量

为了将常量的作用域限制于class内,你必须让它成为class的一个成员,而为确保此常量至多只有一份实体,必须让其成为一个static成员。

class GamePlayer{
private:
static const int NumTurns = 5;       //常量声明式(非定义式)
int scores[GameTurn];
...
};


使用
enum hack
的技巧,其思想就是把
GameTurn
定义为一个枚举常量。上面的代码可以写为:

class GamePlayer {
private:
enum {GameTurn = 10};
int scores[GameTurn];
};


enum hack的优点:

enum定义的常量,并不是在预编译的时候进行替换,而是在编译时,从enum定义的常量存储区取定义的常量值。因此,不会导致 “不必要的内存分配”。

enum hack的行为某方面比较像#define而不像const。例如,取const地址是合法的,但是取值enum的地址是不合法的,#define的地址也不合法,如果你不想让别人获得一个point或reference指向你的某个整数常量,enum实现这个约束。

对于形似函数的宏,最好改用inline函数替换#defines

#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
//宏中所有的实参加上小括号


可以改用inline函数

template<typename T>
inline void CallWithMax(const T& a,const T& b){
f(a>b? a:b);
}


强大的const

指针常量,常量指针

迭代器(因为STL迭代器以指针为根据塑模出来的,声明迭代器为const就像声明指针为const一样)

std::vector<int> vec1;
...
const std::vector<int>::iterator iter = vec1.begin();//像T* const
*iter = 10;                                          //ok
++iter;                                              //wrong ! iter is const
...
std::vector<int>::const_iterator cIter = vec1.begin();//像const T*
*cIter = 10;                                          //wrong ! *cIter is const
++cIter;                                              //ok


函数声明

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而不至于放弃安全性和高效性;const参数就像local const对象一样。

class rational{...};
const rational operator*(const rational& lhs,const rational& rhs);


将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这类成员函数的重要性在于:

可知哪个函数可以改动对象内容,而哪个不可以

使得“操作const对象”成为可能

两个成员函数如果只是常量性不同,可以被重载,如下:

class TextBlock{
public:
...
const char& operator[](std::size_t position) const{return text[position];}
//operator[] for const对象
char& operator[](std::size_t position)      {return text[position];}
//operator[] for non-const对象
private:
std::string text;
};

TextBlock tb("hello");
std::cout << tb[0];               //调用non-const TextBlock::operator[]
tb[0] = 'x';                      //ok

const TextBlock ctb("world");
std::cout << ctb[0];               //调用const TextBlock::operator[]
ctb[0] = 'x';                      //wrong


其中,当const和non-const成员函数有实质等价的实现时,

利用non-const operator[]调用const operator[]是可以避免代码重复的安全做法,过程中需要转型将常量性转除。

但是const成员函数调用non-const 成员函数是一种错误行为,因为const成员函数承诺不改变其对象的逻辑状态,但non-const成员函数没有这种承诺,如果const函数内调用non-const函数可能会使得承诺不改动的对象被改动。

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

通常如果使用C part of C++而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入non-C parts of C++,规则有些变化。这就是为什么array(来自C part of C++)不保证其内容被初始化,但vector(STL part of C++)却有此保证。

读取未初始化的值会导致不明确的行为。

最佳处理方法:对象被使用前先初始化:

对于无任何成员的内置类型,必须手工完成此事。

内置类型以外的任何其它,初始化责任落在构造函数身上。确保每一个构造函数都将对象的每一个成员初始化。

class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address, const std::list<int>& phones);
//声明式
private:
std::string theName;
std::string theAddress;
std::list<int> thePhones;
int num;  //num是内置类型
};

//(1)利用copy赋值法(首先调用default构造函数为theName、theAddress、thePhones设初值,然后再赋予新值。)
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<int>& phones){
theName = name;
theAddress = address;
thePhones = phones;
num = 0;
}

//(2)成员初值列(初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造变量的实参)
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<int>& phones){
theName(name);//theName以name为初值进行copy构造法
theAddress(address);//同上
thePhones(phones);//同上
num(0);
}
//对大多数类型而言,(2)是比(1)高效的。但对于内置类型对象(1)和(2)成本相同,但为了一致性,最好也通过成员初值列来初始化。如果成员变量是const或reference,它们就一定需要初值,不能被赋值。


建议总是使用成员初始值列,并总是列出所有成员变量。

class可能拥有多个构造函数,每个构造函数有自己的成员初值列,多份成员初值的存在就会导致不受欢迎的重复。这种情况下,可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在“成员变量的初值由文件或数据库读入”时特别有用。

C++有十分固定的成员初始化次序:父类早于子类class的成员变量总是以声明次序被初始化,即使在初值列中以不同于声明次序出现,也不会有影响。

为免除“跨编译单元之初始化次序”问题,请以local static 对象替换non-local static对象。

所谓static对象,包括

global对象

定义于namespace作用域的对象

在class内、函数内、在file作用域内被声明为static的对象。

其中,函数内的static对象成为local static对象,其它static对象称为non-local static对象。static对象的寿命从被构造出来直到程序结束时被自动销毁,也就是它们的析构函数在main函数结束时被自动调用。

所谓编译单元

当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。这个编译单元会被编译成为一个与cpp 文件名同名的目标文件(.o或是.obj) 。简单说,一个编译单元就是一个经过预处理的cpp文件。

C++对定义于不同编译单元内的non-local static 对象的初始化并无明确定义

class FileSystem{                        //编译单元1
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs;                  //预备给编译单元2使用的对象


class Directory{                         //编译单元2
public:
Directory(param);
...
};
Directory::Directory(param){
...
std::size_t disks = tfs.numDisks();   //使用tfs对象
...
}


现在编译单元2创建一个Directory对象,

Directory tempDir(param);


tfs和tempDir是定义于不同编译单元内的non-local static 对象,因此问题在于tfs是否在tempDir之前先被初始化?

解决方法:将每一个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个指向local static对象的reference。调用这些函数来替换“直接访问non-local static对象”。这是Singleton模式的一个常见实现手法。

class FileSystem{...};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}

class Directory{...};
Directory::Directory(param){
...
std::size_t disks = tfs().numDisks();
//上面是std::size_t disks = tfs.numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}


C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇见该对象之定义式”时被初始化。

但另一角度看,这些函数“内含static对象”的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。处理的一种做法就是:在程序的单线程启动阶段手工调用所有reference-return函数,这可消除与初始化有关的“竞速形势”
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息