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

Effective C++读书笔记一

2015-11-23 20:24 295 查看

一.让自己习惯C++

条款01:视C++为一个语言联邦

为了更好的理解C++,我们将C++分解为四个主要次语言:

C。说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。编程效率不高。

Object-Oreinted C++。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等…

Template C++。这是C++泛型编程部分。

请记住:

这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

条款02:尽量以const,enum,inline替换#define

这个条款或许可以改为“宁可 以编译器替换预处理器”。即尽量少用预处理。

编译过程:.c文件--预处理-->.i文件--编译-->.o文件--链接-->bin文件

预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,将所有的#define删除,并且展开所有的宏定义。预处理过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是以#号开头的代码行。

将被包含的文件插入到该预编译指令的位置

例:#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未进入记号表(symbol table)。当出现编译错误信息是会导致追踪错误麻烦。
替换:const double AspectRatio = 1.653;
好处应该有:多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。


常量替换#define两点注意:

1.定义常量指针:(由于常量指针通常放在头文件内,因此有必要将指针声明为const)

const char *const authorName = “Shenzi”;//指针变量和指向的内容都是不变的。

const std::string authorName(“Shenzi”);

2.类专属常量:(为将常量的作用域限制在class内,必须让它成为class的一个成员。为确保常量至多只有一个实体,你需让它成为一个static成员)

classA{

private:

static const int NumTurns = 5;//static 静态常量 所有的对象只有一份拷贝。此处是声明

}

此时已经给予初值。在后面的定义是就不需要设定初值。in-class初值设定只允许对整型常量进行。除上语法,也可以在头文件类内声明,在实现文件中类外定义并赋初值。

万一你编译器不允许“static整数型class常量”完成“in class初值设定”(即在类的声明中设定静态整形的初值),我们可以通过枚举类型予以补偿(一个属于枚举类型的数值可权充 ints 被使用):

enum { NumTurns = 5 };

*取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获取一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。

例:#define CALL_WITH_MAX(a,b)    f((a) > (b)) ? (a) : (b))
上面的宏有太多缺点。
宏看起来像函数,但不会招致函数调用带来的额外开销,而是一种简单的替换。
替换:
template<typename T>
inline void callWithMax(cosnt T &a, cosnt T &b)
{
f(a > b ? a : b);
}
callWithMax是个真正的函数,它遵循作用于和访问规则。


请记住:

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

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

条款03:尽可能使用const

const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。

关键字const多才多艺:

例:

char greeting[] = “Hello”;

char *p = greeting; //指针p及所指的字符串都可改变;

const char *p = greeting; //指针p本身可以改变,如p = &Anyother;p所指的字符串不可改变;

char * cosnt p = greeting; //指针p不可改变,所指对象可改变;

const char * const p = greeting; //指针p及所致对象都不可改变;

const 用法说明:

如果关键字const出现在星号左边,表示被指物是常量。const char *p和char const *p两种写法意义一样,都说明所致对象为常量;

如果关键字const出现在星号右边,表示指针自身是常量。

如果出现在星号两边,表示被指物和指针两者都是常量。

STL(标准模板库)例子,迭代器的作用就像T*指针,声明迭代器为常量就是声明指针本身为const一样,这一点和:

const std::vector<int>::interator iter = vec.begin();  //iter作用像T *const,
++iter ;    // 错误:iter是const
*iter = 10  //正确,改变iter所指物
std::vector<int>::const_iterator cIter = vec.begin();  //作用像const T*
*cIter = 10 错误:*cIter是const
注:std::vector<int>::interator iter  表示将iter声明为std::vector<int>的迭代器


关于迭代器:

迭代器就像指针,可以访问容器里面的元素,但是还能完成其他操作。STL为每个容器都定义了迭代器,运用模板,这样函数就可以不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。迭代器有5种类型,相应的就有不同的功能。

const用于函数开头

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

例:const Rational operator* (const Rational &lhs, cosnt Rational &rhs);

引用作为返回值,必须遵守以下规则:

(1)不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了”无所指”的引用,程序会进入未知状态。

(2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

(3)可以返回类成员的引用,但最好是const。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

const成员函数

举例

const char& operator[](std:size_t position) const

const成员函数使class接口比较容易被理解,它们使“操作const对象”称为可能;

说明:声明为const的成员函数(在函数名后加const),不可改变non-static成员变量,在成员变量声明之前添加mutable(可变的)可让其在const成员函数中可被改变。

const_cast<char &>(static_cast<const TextBlock &>(*this))[position];
//static_cast 将TextBlock &转为const TextBlock &;
//const_cast将返回值去掉const约束;


请记住:

将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;

编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的车辆”(conceptual constness);

当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。但是不能用const成员函数调用non-const成员函数

条款04:确定对象被使用前已先被初始化

永远在使用对象之前先将它初始化。对于无任何成员的*内置类型*,你必须手工完成此事。至于内置类型以外的任何其它东西,初始化责任落在构造函数身上,确保每一个构造函数都将对象的每一个成员初始化。


区分赋值和初始化:

C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。所以应将成员变量的初始化置于构造函数的初始化列表中。

ABEntry::ABEntry(const std::string& name, const std::string& address,

const std::list& phones)

{

theName = name; //这些都是赋值,而非初始化

theAddress = address; //这些成员变量在进入函数体之前已调用默认构造函数,接着又调用赋值构造函数,

thePhones = phones; //即要经过两次的函数调用。

numTimesConsulted = 0;

}

ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
: theName(name),                    //这些才是初始化
theAddress(address),                //这些成员变量只用相应的值进行拷贝构造函数,所以通常效率更高。
thePhones(phones),
numTimesConsulted(0)
{    }
所以,对于非内置类型变量的初始化应在初始化列表中完成,以提高效率。而对于内置类型对象,如numTimesConsulted(int),其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化表来初始化。


举例:

无参构造函数

ABEntry::ABEntry( )

: theName(), //调用thename的默认构造函数

theAddress(), //

thePhones(),

numTimesConsulted(0)

{ }

如果成员变量时const或reference,它们就一定需要初值,不能被赋值。见条款五

所以避免麻烦,简单的做法是总是使用成员初值列

C++有着十分固定的“成员初始化次序”。基类总是在派生类之前被初始化,而类的成员变量总是以其说明次序被初始化。所以:当在成员初始化列表中列各成员时,最好总是以其声明次序为次序。

讨论 non-local static 对象的初始化次序

所谓static对象,其寿命从被构造出来直到程序结束为止,这种对象包括global对象,定义于namespace作用域内的对象,在class内,函数内,以及在file作用域内声明为static的对象。

函数内的static对象称为local static对象,其他static对象称为non-local static对象,程序结束时,static对象会被自动销毁。

对于“跨编译单元之non-local static初始化次序”,non-local static 对象的初始化动作使用了另一个编译器单元内的某个non-local static对象,它使用的这个对象可能尚未被初始化。解决方法是:将每个non-local static对象搬到自己的专属函数内。这些函数将返回一个reference指向它所含的对象。

class filesysterm{};
filesysterm & tfs()  //这个函数用来替换tfs对象,它在filesysterm class 中可能是个static 
{
static filesysterm fs; //定义并初始化一个local static对象返回一个reference指向上述对象
return fs;
}


请记住:

为内置对象进行手工初始化,因为C++不保证初始化它们;

构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;

为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  c语言