《Effective C++》笔记

  • A+
所属分类:C++面试题汇总

序言

声明:告诉编译器某个东西的名称和类型,但略去细节。
定义:提供编译器一些声明所遗漏的细节,对对象而言,定义是编译器为其分配内存的地方。
初始化:给予对象初值的过程。

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

初看题目,对这个语言联邦甚是不解,看完条款1,恍然大悟。所谓语言联邦,即C++由多种编程范式组成,包括:

  1. C。C++说到底还是在C基础上发展的,但是在C++中依然可以使用C,但是C中没有模板、异常、重载。
  2. Object-Oriented C++。也称之为C with classes。简而言之,封装、继承、多态。
  3. Template C++。泛型编程,STL中常用的手法。再往外延伸一点就是模板元编程。
  4. STL。标准模板库,包括容器、算法、迭代器、适配器、仿函数、分配器。

C++高效编程守则视状态而变化,取决于你是使用C++的哪一部分。

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

请记住:

  • 对于单纯常量,最好以const对象或者enum替换#define
  • 对于形似函数的宏,最好改用inline函数替换#define
  1. “宁可用编译器替换预处理器”。宏定义出错溯源困难的原因是:宏定义所使用的名称可能并未进入符号表。宏定义编译时报错,给的错误提示是它所替代的真实值,如果这个宏定义在其他头文件中,查找错误无疑是耗时的。#define就无法做到创建类专属常量。

2.以常量替换#define有两种特殊情况:

  • 定义常量指针:由于常量定义通常放在头文件中,所以有必要讲指针声明为const
  • class专属常量:通常类专属常量要被static const修饰,因为要保证这个常量在所有类对象中指存在一份,所以用static修饰,而又要求是常量,所以用const修饰;只要不取这个常量的地址,无需提供定义,只需在类中声明,例如static const int num = 5;,但是,如果坚持要获得这个常量的地址,那么需要在类外提供定义,例如const int Base::num;,由于class常量已经在声明的时候获得初值,所以在类外定义的时候无需再设初值。
  1. 在类中还可以使用enum来完成第2点中的任务。且enum#define很相似,因为都无法取地址。

  2. 通常不同编译器的规则不太一样,所以使用static const还是enum要根据编译器来定。

条款03:尽可能使用const

  1. 令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。例如,如果返回类型没有const,那么可能出现(a*b)=c;的情况。一个“良好的用户自定义类型”的特征是他们避免无端地与内置类型不兼容。

  2. 如果两个成员函数只是常量性不同,可以被重载。将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象。

  3. bitwise constness和logical constness

  • bitwise constness:成员函数只有在不更改对象的任何成员变量时才可以说是const的
  • logical constness:一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦察不出的情况才能如此操作。想要让客户端侦察不出,就需要对可以接受修改的变量加上mutable修饰

请记住:

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

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

请记住:

  • 为内置型对象进行手工初始化,因为C++不保证初始化他们
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用复制操作。初值列列出的成员变量,其排列次序应该和他们在类中的声明次序相同
  • 为避免“跨编译单元初始化次序”问题,使用local static对象替换non-local static对象(还有一种技术是:在新建一个类,专门用于初始化)
  1. 需要着重强调的是:在构造函数中,初值列中才叫初始化,函数体内是叫赋值!这么也就可以理解,为什么使用初值列比在函数体中“初始化”高效了!而且,对于成员变量是const或者reference的,不能进行复制操作,只能在初值列初始化。

  2. C++有着十分固定的“成员初始化次序”,次序总是相同的:基类早于派生类初始化,类中的成员变量总是以声明的次序被初始化。


条款05:了解C++默默编写并调用哪些函数

  1. 如果你自己没有声明,编译器会为一个类声明一个copy构造函数、copy赋值操作符、析构函数,如果你没有声明任何构造函数,编译器会为你声明一个默认构造函数。这些编译器声明的函数都是public inline的。不过,唯有当这些函数呗需要,或者说是被调用时,他们才会被编译器创造出来。

  2. 编译器产出的析构函数默认是non-virtual的,除非这个类的基类自身声明有虚析构函数。

  3. 如果你打算在一个内含引用成员的类内支持赋值操作,你必须自己定义一个拷贝赋值操作符。因为,如果你不指定,编译器生成的默认拷贝赋值操作符会导致你的引用被另一个对象所修改。

请记住:

  • 编译器可以暗自为class创建默认构造函数,copy构造函数,copy赋值操作符,以及析构函数。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

请记住:

  • 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。继承Uncopyable这样的基类也是一种做法。

条款07:为多态基类声明virtual析构函数

请记住:

  • 带多态性质的基类应该声明一个虚析构函数。如果类带有任何virutal函数,他就应该拥有一个virtual析构函数
  • 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不应该声明为virtual析构函数
  1. 对于请记住中的第二点,因为类中存在虚函数就会生成对应的虚函数表,对象实例的时候就会有一个虚函数指针,这个指针是占用内存的,对于一些简单的类,如果有这个虚函数指针,可能导致寄存器不能一次性地取到整个类对象实例内存,所以会影响效率。

条款08:别让异常逃离析构函数

请记住

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下他们或结束程序
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作
  1. 所谓的不吐出异常,指的是,在析构函数中就把该异常给处理掉了。

  2. 为什么不能让异常逃离析构函数?举个例子,对于一个vector</Widget/>的vector,vector负责析构容器中的每个Widget,当析构第一个元素期间抛出异常,析构函数没有处理,第二个元素析构又抛出异常,此时有两个异常,会导致程序不明确的行为。

条款09:绝不在构造和析构过程中调用virtual函数

请记住:

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类
  1. 无论是在构造还是析构过程,当前对象内并不能看到派生类的信息(构造先构造父类,析构先析构派生类)。

  2. 对于在构造或者析构中间接调用的虚函数危害比较大,因为它通常不会引起任何编译器和连接器抱怨。

条款10:令operator=返回一个reference to *this

请记住:

  • 令赋值操作符返回一个reference to *this
  1. 可以实现x = y = z = 15;这样的语句

条款11:在operator=中处理“自我赋值”

请记住:

  • 确保当对象自我赋值时,operator=有良好行为。其中技术包括比较“对象来源”和“目标对象”地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而区中多个对象是同一个对象时,其行为仍然正确
  1. 自我赋值可能存在的问题是:在停止使用资源之前意外地释放了该资源。

  2. 证同测试

Widget& Widget::operator=(const Widget& rhs){
   
	if(this == rhs) return *this;
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}
  1. 复制所指东西之前别删除
Widget& Widget::operator=(const Widget& rhs){
   
	Bitmap* pOrig = pb;
	pb = new Bitmap(*rhs.pb);
	delete pOrig;
	return *this;
}
  1. copy-and-swap
Widget& Widget::operator=( Widget rhs){
    // 注意,是值传递
	Wodget tmp(rhs);
	swap(tmp);	// 交换rhs和this的数据
	return *this;
}

条款12:复制对象时别忘其每一个成分

请记住:

  • copy函数应该确保复制“对象内的所有成员变量”及“所有基类成分”
  • 不要尝试以某个copy函数实现另一个copy函数。应该讲共同机能放进一个第三方函数中(init),并由两个copy函数共同调用。
  1. 如果你为class添加一个成员变量,你必须同时修改copy函数。
w3cjava

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: