Effective C++
explicit声明
class B { public: explicit B(int x = 0, bool b = true) //默认构造函数,要么没有参数,要么所有参数都有默认值 { m_x = x; } public: int m_x; }; void doSomething(B bObject) { std::cout << bObject.m_x << endl; } int main() { doSomething(B()); //ok 0 doSomething(B(12)); //ok 12 //doSomething(12); //error system("pause"); return 0; }
通过在构造函数前加explict,用以避免隐式类型转换。
条目1:把C++看成多种语言的联合体
C++是一门多范型编程语言,可以将C++看成是一个由若干门语言组成的联合体:
- C
- 面向对象的C++
- 包含模板的C++
- STL
条目2:多用const、enum、inline,少用#define
#define ASPECT_RATIO 1.653替换为定义一个常量
const double AspectRatio=1.653;
对于类内部的常量,为了限制常量的份数超过一份,必须将该常量声明为static成员。
class GamePlayer { private: static const int NumTurns = 5; //常量声明 int scores[NumTurns]; };
const int GamePlayer::NumTurns; //常量定义
类内部的为常量声明,而非定义。类内部的静态常量如果是整形(整数、字符型、布尔型)则不需要定义。只要你不需要得到它们的地址,你可以声明它们、调用它们而不需要定义。
对于简单的常量,多用const对象或者枚举类型数据,少用#define
多用内联函数来代替带参宏
条目3:尽可能使用const
char greeting[] = "Hello"; char *p1 = greeting; //非const指针,非const数据 const char *p2 = greeting; //非const指针,const数据 char *const p3 = greeting; //const指针,非const数据 const char *const p4 = greeting; //const指针,const数据
const成员函数
const成员函数可以被具有相同参数列表的非const成员函数重载:
1 class TextBlock 2 { 3 private: 4 std::string text; 5 public: 6 TextBlock(std::string s) 7 { 8 text = s; 9 } 10 char& operator[](std::size_t position) 11 { 12 return text[position]; //用于非const对象 13 } 14 const char& operator[](std::size_t position) const 15 { 16 std::cout << "const func" << endl; 17 return text[position]; //用于const对象 18 } 19 }; 20 21 int main() 22 { 23 TextBlock tb("Hello"); 24 std::cout << tb[0] << endl; 25 const TextBlock ctb("World"); 26 std::cout << ctb[0] << endl; 27 return 0; 28 }
关于const成员函数的两种说法:
- 按位恒定:当且仅当一个成员函数对其所在对象的所有数据成员(static数据成员除外)都不做改动时,才需要将成员函数声明为const.但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。
- 逻辑恒定:如果某个对象调用了一个const成员函数,则这个成员函数可以对对象作出内部改动,但仅仅是客户端无法察觉的方式进行。
条目4:确保对象在使用前得到初始化
读取未初始化的数据将导致未定义行为。在一些语言平台中,通常情况下读取未初始化的数据仅仅是使你的程序无法运行罢了。更典型的情况是,这样的读取操作可能会得到内存中某些位置上的半随机的数据,这些数据将会“污染”需要赋值的对象,最终,程序的行为将变得十分令人费解,你也会陷入烦人的除错工作中。
解决这类不确定的问题的最好途径是:总是在使用对象之前进行初始化。对于内置类型的非成员对象,需要手动完成这一工作。注意赋值和初始化的区别:
C++约定:一个对象的数据成员要在进入构造函数内部之前得到初始化。在进入ABEntry构造函数内部之前,这些数据成员的默认构造函数应该自动得到调用。注意对于numTimesConsulted成员不成立,因为其为内置类型,对其而言,在被赋值之前,无法确保其得到了初始化。
更好的方法是使用初始化表,这样效率会更高:
C++对象中数据成员的初始化顺序为其在类中声明的顺序,而不是成员初始化列表中的顺序。为了使读者不至于陷入困惑,应保证初始化表中的顺序与声明时的顺序保持一致。
条目5:要清楚C++后台为你书写和调用了什么函数
对于一个类来说,如果不自己手动声明一个复制构造函数、赋值运算符、析构函数,编译器会自动声明这些函数,没有声明构造函数的话,编译器也会为你声明一个默认构造函数。所有这些函数都是public和inline的。
条目6:要显式禁止编译器为你生成不必要的函数
通常情况下,如果你希望一个类不支持某种特定的功能,你需要做的仅仅是不去声明那个函数。然而这一策略对复制构造函数和拷贝赋值运算符就失效了,这是因为,即使你不做声明,而一旦有人尝试调用这些函数,编译器就会为你自动声明它们(参见条目 5)。解决问题的关键是,所有编译器生成的函数都是公共的。为了防止编译器生成这些函数,将复制构造函数和赋值运算符声明为私有的。通过显式声明一个函数,你就可以防止编译器去自动生成这个函数,同时,通过将函数声明为private的,你便可以防止人们去调用它。同时为了防止其他成员函数或者友元函数访问这些private函数,可将这些private成员函数只声明而不进行定义。
class HomeForSale { public: HomeForSale() {} private: HomeForSale(const HomeForSale&);//只有声明,无定义 HomeForSale& operator=(const HomeForSale&); //只有声明,无定义 };
条目7:要把多态基类的析构函数声明为虚函数
C++有明确的规则:如果希望通过一个基类类型的指针来删除一个派生类对象,并且基类的析构函数为非虚析构函数,则结果是未定义的。典型的后果是,运行中派生类中新派生出的部分将得不到销毁,基类部分会被销毁掉,这样就产生了一个古怪的“部分销毁”的现象。
排除这一问题的方法很简单:为基类提供一个虚拟的析构函数,这样删除一个派生类对象,程序就可以精确地按照需要进行了,这个对象都会得到销毁。任何有虚函数的类几乎都需要一个虚析构函数,如果一个类不包含虚函数,则通常情况下意味着它将不作为基类使用。当一个类不作为基类使用时,将它的析构函数声明为虚函数不是个好主意。
应该为多态基类声明虚析构函数。一旦一个类包含虚函数,它就应该包含一个虚析构函数。
如果一个类不用作基类或者不需具有多态性,便不应该为它声明虚析构函数。
条目8:防止因异常中止析构函数
条目9:永远不要在构造或者析构的过程中调用虚函数
创建一个派生类的对象时,基类的构造函数优先于派生类的构造函数运行,在基类构造函数运行的时候,派生类的数据成员还未得到初始化。对于一个派生类的对象来说,在其进行基类部分构造工作的时候,这一对象的类型就是基类的。不仅仅虚函数会解析为基类的,而且 C++中“使用运行时类型信息”的部分(比如 dynamic_cast(参见条目 27)和typeid)也会将其看作基类类型的对象。
对于析构过程可以应用同样的推理方式。一旦派生类的析构函数运行完毕,对象中派生类的那一部分数据成员将取得未定义的值,所以 C++会认为它们不再存在。在进入基类的析构函数时,这个对象将成为一个基类对象,C++的所有部分——包括虚函数、dynamic_cast 等等——都会这样对待该对象。
条目10:让赋值运算符返回一个指向*this的引用
int x, y, z; x = y = z = 15; //一连串的赋值操作
这种实现的本质是:赋值时,返回一个指向运算符左边对象的引用。当为你的类实现赋值运算符时,应遵守这一惯例,这一惯例对所有的赋值运算符同样适用。
1 class Widget 2 { 3 public: 4 Widget() {} 5 Widget& operator=(const Widget& rhs) 6 { 7 //other code 8 return *this; //返回运算符左边的对象 9 } 10 Widget& operator+=(const Widget& rhs) 11 { 12 //other code 13 return *this; //返回运算符左边的对象 14 } 15 };
条目11:在operator=中要处理自赋值问题
Widget& operator=(const Widget& rhs) { if (this == &rhs) return; this->a = rhs.a; return *this; }
条目12 要复制整个对象,不要遗漏任一部分
当没有手动定义拷贝成员函数时(拷贝构造函数和拷贝赋值运算符),编译器将自动生成拷贝函数,且自动生成的拷贝函数可以精确地按你所期望的方式运行,当前正在赋值的所有对象都会得到复制。然而当自己声明拷贝函数时,如果拷贝函数内只进行部分复制,编译器不会给出任何警告和错误。通过继承,这一问题可以带来更加严重却隐蔽的危害。
1 void logCall(const std::string& msg) 2 { 3 /// 4 } 5 class Customer 6 { 7 public: 8 Customer():name("unknown"){} 9 Customer(const std::string& s) :name(s) {} 10 Customer(const Customer& rhs):name(rhs.name) 11 { 12 logCall("Customer copy constructor"); 13 } 14 Customer& operator=(const Customer& rhs) 15 { 16 logCall("Customer copy assignment operator"); 17 name = rhs.name; 18 return *this; 19 } 20 private: 21 std::string name; 22 }; 23 //////////////////////////////////////// 24 class PriorityCustomer :public Customer 25 { 26 public: 27 PriorityCustomer() :priority(0) {} 28 PriorityCustomer(const PriorityCustomer& rhs); 29 PriorityCustomer& operator=(const PriorityCustomer& rhs); 30 private: 31 int priority; 32 }; 33 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :priority(rhs.priority) 34 { 35 logCall("PriorityCustomer copy constructor"); 36 } 37 PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) 38 { 39 logCall("Customer copy assignment operator"); 40 priority = rhs.priority; 41 return *this; 42 }
拷贝时,PriorityCustomer对象从基类继承而来的成员始终没有得到复制。一旦你亲自为一个继承类编写了拷贝函数,你必须同时留心其基类的部分。当然这些部分通常情况下是私有的,所以你无法直接访问它们。取而代之的是,派生类的拷贝函数必须调用这些私有数据在基类中相关的函数。
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :Customer(rhs), //调用基类的拷贝构造函数 priority(rhs.priority) { logCall("PriorityCustomer copy constructor"); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall("Customer copy assignment operator"); Customer::operator=(rhs); //为基类部分赋值 priority = rhs.priority; return *this; }
条目13:要使用对象来管理资源
为了确保createInvestment()所返回的资源总能得到释放,我们需要将这类资源放置在一个对象中,以来C++对默认析构函数的自动调用来确保资源及时得到释放。标准库中的auto_ptr是类似于指针的对象(智能指针),其析构函数可以自动对其所指内容执行delete。
由于当一个 auto_ptr 被销毁时,它将自动删除其所指向的内容,所以永远不存在多个 auto_ptr 指向同一个对象的情况,这一点很重要。如果存在的话,这个对象就会被多次删除,这样你的程序就会立即陷入未定义行为。为了防止此类问题发生,auto_ptr 有一个不同寻常的特性:如果你复制它们(通过拷贝构造函数或者拷贝赋值运算符),它们就会被重设为 null,然后资源的所有权将由复制出的指针独占!
引用计数智能指针是auto_ptr的替代品,它可以跟踪有多少个对象指向了一个特定的资源,同时在没有指针再指向这一资源时,智能指针会自动删除该资源。可以看出引用计数智能指针的行为和垃圾回收器相似。
auto_ptr和tr1::shared_ptr在析构函数中使用的是delete语句,而不是delete[]。这就意味着对于动态分配的数组使用auto_ptr和tr1::shared_ptr不是一个好主意。但是遗憾的是,这样的代码会通过编译。
条目14:要注意资源管理类中的复制行为
条目15:要为资源管理类提供对原始资源的访问权
条目16:互相关联的new和delete要使用相同的形式
使用new语句时,将会分配内存和为这段内存调用一个或者多个构造函数;当使用delete语句时,将会为分配的内存调用一个或多个析构函数,释放内存。
std::string *stringPtr1 = new std::string; std::string *stringPtr2 = new std::string[100]; delete stringPtr1; delete[] stringPtr2;
对stringPtr1调用delete[],或者对stringPtr2调用delete,都会导致未定义的行为。这里的规则很简单:如果你在一个 new 语句中使用了[],那么你必须在相关的delete 语句中也使用[]。如果你在一个 new 语句中没有使用[],那么在相关的delete 语句中也不应使用[]。
条目17:用智能指针存储由new创建的对象时要使用独立的语句
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
在编译器能够生成对processWidget的调用前,必须对传入的参数进行处理,编译器必须自动生成代码来解决下面三件事情:1.调用priority(),2.执行new Widget,3.调用tri::shared_ptr的构造函数。C++编译器对这三项任务完成的顺序要求的很宽松,调用priority()可能出现在第2步,如果调用priority抛出异常的话,则第一步new Widget返回的指针将会消失,这种情况下,processWidget()可能会造成资源泄露。这是因为:在资源被创建(通过 new Widget)以后和将这个资源转交给一个资源管理对象之前的这段时间内,有产生异常的可能。防止这类问题发生的办法很简单:使用单独的语句,创建 Widget 并将其存入一个智能指针,然后将这个智能指针传递给 processWidget。
条目18:要让接口易于正确使用,而不易被误用
优秀的接口应该易于正确使用,而不易误用。你应该力争让你所有的接口都具备这一特征。
增加易用性的方法包括:让接口保持一致性,让代码与内建数据类型保持行为上的兼容性。
防止错误发生的方法包括:创建新的数据类型,严格限定类型的操作,约束对象的值,主动管理资源以消除客户的资源管理职责。
条目19:要像设计类型一样设计class
条目20:传参时要多用“引用常量”,少用传值
默认情况下,C++为函数传入和传出对象是采用传值方式的(这是从C语言继承而来的特征)。除非你明确使用其他方法,否则函数的形参总是通过复制实参的副本来创建,而且,函数的调用者得到的也是函数返回值的副本。这些副本是由对象的拷贝构造函数创建的。
bool validateStudent(const Student& s);
通过引用传参也可以避免“截断问题”。当一个派生类的对象以一个基类对象的形式传递(传值方式)时,基类的拷贝构造函数就会被调用,此时,这一对象的独有特征——使它区别于基类对象的特征会被“截掉”。剩下的只是一个简单的基类对象,这并不奇怪,因为它是由基类构造函数创建的。通过传递常量引用,可以避免截断问题。
C++编译器中,引用通常是以指针的形式实现的,所以通过引用传递实质是传递一个指针。传递一个内置数据类型的对象,传值会比传递引用更为高效,迭代器和 STL 中的函数对象也是如此,这是因为它们设计的初衷就是能够更适于传值,这是 C++的惯例。
条目21:在必须返回一个对象时,不要尝试返回一个引用
这个函数会返回一个指向result的引用,但result为一个局部对象,局部对象在函数退出时会被销毁。事实上,任何返回局部对象的引用的函数都是灾难性的(任何返回指向局部对象的指针的函数也是灾难性的)。
条目22:将数据成员声明为私有的
protected并不会带来比public更高的封装性
条目23:多用非成员非友元函数,少用成员函数
多用非成员非友元函数,少用成员函数。这样做可以增强封装性,以及包装的灵活性和功能的扩展性。
条目24:当函数所有参数都需要进行类型转换时,要将其声明为非成员函数
条目25:最好不要让swap抛出异常
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using namespace std; 5 6 class WidgetImpl 7 { 8 public: 9 WidgetImpl() :a(1), b(2), c(3), d(4) {} 10 WidgetImpl(int _a, int _b, int _c, int _d) :a(_a), b(_b), c(_c), d(_d) {} 11 private: 12 int a, b, c, d; 13 }; 14 class Widget 15 { 16 public: 17 Widget(WidgetImpl* pp) :pImpl(pp) {} 18 Widget(const Widget& rhs) 19 { 20 *pImpl = *rhs.pImpl; 21 } 22 Widget& operator=(const Widget& rhs) 23 { 24 if (this == &rhs) return *this; 25 *pImpl = *rhs.pImpl; 26 return *this; 27 } 28 void swap(Widget& other) 29 { 30 using std::swap; 31 swap(pImpl, other.pImpl);//交换两个Widget,只需交换其pImpl指针 32 } 33 private: 34 WidgetImpl *pImpl; 35 }; 36 ////////////////// 37 namespace std 38 { 39 //将Widget的std::swap特化 40 template<> 41 void swap<Widget>(Widget& a, Widget& b) 42 { 43 cout << "swap widget..." << endl; 44 a.swap(b); 45 } 46 } 47 48 49 50 int main() 51 { 52 WidgetImpl imp1(1, 2, 3, 4); 53 WidgetImpl imp2(4, 5, 6, 7); 54 Widget wd1(&imp1); 55 Widget wd2(&imp2); 56 swap(wd1, wd2); 57 system("pause"); 58 }
如果默认的 swap 实现并不够高效(大多数情况下意味着你的类或模板正在运用 pimpl 惯用法),请按下面步骤进行:
1. 提供一个公用的 swap 成员函数,让它可以高效的交换你的类型的两个对象的值。理由将在后面列出,这个函数永远不要抛出异常。
2. 在你的类或模板的同一个名字空间中提供一个非成员的 swap。让它调用你的swap 成员函数。
3. 如果你正在编写一个类(而不是类模板),要为你的类提供一个 std::swap的特化版本。同样让它调用你的 swap 成员函数。
最后,如果你正在调用 swap,要确保使用一条 using 声明来使 std::swap 对你的函数可见,然后在调用 swap 时,不要做出任何名字空间的限制。
条目26:定义变量的时机越晚越好
条目27:尽量少用转型操作
const_cast用于脱去对象的恒定性
dynamic_cast 主要用于进行“安全的向下转型”
static_cast 可以用于强制隐式转换
条目28:不要返回指向对象内部部件的“句柄”
条目29:力求代码做到“异常安全”
抛出异常时,异常安全的代码应该能够做到:不泄漏资源,不能让数据结构遭到破坏。
条目30:深入探究内联函数
inline是对编译器的一次请求,而不是一条命令,这种请求可以显式提出,也可以隐式提出,隐式提出的途径是在类定义的内部定义函数,显式方法为在函数定义前加inline关键字。
inline是对编译器的请求,但编译器可能会忽略它,大多数编译器如果认为当前的函数过于复杂(比如包含循环或递归的函数),或者这个函数是虚函数(即使是最平常的虚函数调用),就会拒绝将其内联。
条目31:尽量减少文件间的编译依赖
接口类实现方法
//Person.h #pragma once #include <string> #include <memory> class Person { public: virtual ~Person() {}; virtual std::string name() const= 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; public: static std::shared_ptr<Person> create(const std::string& _name, const std::string& _birth, const std::string& _addr); }; //RealPerson.h #pragma once #include "Person.h" class RealPerson : public Person { public: RealPerson(const std::string& name, const std::string& birthday, const std::string& addr) : theName(name), theBirthDate(birthday), theAddress(addr) {} virtual ~RealPerson() {} std::string name() const; // 这里省略了这些函数的具体实现, std::string birthDate() const; // 但是很容易想象它们是什么样子。 std::string address() const; private: std::string theName; std::string theBirthDate; std::string theAddress; }; //RealPerson.cpp #include "RealPerson.h" std::string RealPerson::name() const { return theName; } std::string RealPerson::birthDate() const { return theBirthDate; } std::string RealPerson::address() const { return theAddress; } std::shared_ptr<Person> Person::create(const std::string& _name, const std::string& _birth, const std::string& _addr) { return std::shared_ptr<Person>(new RealPerson(_name, _birth, _addr)); } //test.cpp #include <iostream> #include <string> #include <vector> #include "Person.h" using namespace std; int main() { std::shared_ptr<Person> pp(Person::create("aaa", "2019", "beijing")); cout << pp->name() << " " << pp->birthDate() << " " << pp->address() << endl; system("pause"); }
句柄类实现方法
//PersonImpl.h #pragma once #include <string> class PersonImpl { public: PersonImpl(const std::string& name, const std::string& birthday, const std::string& addr) : theName(name), theBirthDate(birthday), theAddress(addr) {} ~PersonImpl() {} std::string name() const; // 这里省略了这些函数的具体实现, std::string birthDate() const; // 但是很容易想象它们是什么样子。 std::string address() const; private: std::string theName; std::string theBirthDate; std::string theAddress; }; //PersonImpl.cpp #include "PersonImpl.h" std::string PersonImpl::name() const { return theName; } std::string PersonImpl::birthDate() const { return theBirthDate; } std::string PersonImpl::address() const { return theAddress; } //Person.h #pragma once #include "PersonImpl.h" #include <memory> class Person { public: Person(const std::string& name, const std::string& birthday, const std::string& addr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::shared_ptr<PersonImpl> pImpl; //指向实现的指针 }; #include "Person.h" #include "PersonImpl.h" Person::Person(const std::string& name, const std::string& birthday, const std::string& addr) :pImpl(new PersonImpl(name,birthday,addr)) { } std::string Person::name() const { return pImpl->name(); } std::string Person::birthDate() const { return pImpl->birthDate(); } std::string Person::address() const { return pImpl->address(); } //test.cpp #include <iostream> #include <string> #include <vector> #include "Person.h" using namespace std; int main() { Person pp("aaa", "2019", "beijing"); cout << pp.name() << " " << pp.birthDate() << " " << pp.address() << endl; system("pause"); }
条目32:确保公共继承以“A是一个B”形式进行
公共继承意味着“A 是一个 B”关系。对于基类成立的一切都应该适用于派生类,因为派生类的对象就是一个基类对象。
条目33:避免隐藏继承而来的名字
当我们在一个派生类的成员函数中企图引用基类的某些内容(比如成员函数、typedef、或者数据成员等等)时,编译器能够找出我们所引用的内容,因为派生类所继承的内容在基类中都做过声明。这里真正的工作方式实际上是:派生类的作用域嵌套在基类的作用域中。
即使同一函数在基类和派生类中的参数表不同,基类中该函数依然会被隐藏,而且这一结论不会因函数是否为虚函数而改变。
派生类中的名字会将基类中的名字隐藏起来。在公共继承体系下,这是我们永远不希望见到的。
条目34:区分清接口继承和实现继承
声明纯虚函数的目的是让派生类仅仅继承函数接口。为纯虚函数提供一个定义并没有被C++所禁止,但是在调用这种函数时,需要加上类名。
声明简单虚函数(非纯虚函数)的目的是让派生类继承函数接口的同时,继承一个默认的具体实现。
声明一个非虚函数的目的是让派生类继承这一函数接口,同时强制继承其固定的具体实现。
条目35:虚函数的替代方案
条目36:避免对派生的非虚函数进行重定义
class B { public: void mf() { cout << "B mf()" << endl; } }; class D:public B { public: void mf() { cout << "D mf()" << endl; } }; int main() { D x; B *pb = &x; D *pd = &x; pb->mf(); //B mf() pd->mf(); //D mf() system("pause"); }
条目37:避免对函数中继承得来的默认参数值进行重定义
可以继承的函数可以分为两种:虚拟的和非虚拟的。然而,由于重定义一个派生的非虚函数始终是一个错误(参见条目 36),因此我们可以放心地将此处的讨论范围缩小至以下情况:继承一个含有默认参数值的虚函数。
虚函数是动态绑定的,而默认参数值是静态绑定的。
一个对象的静态类型就是在对其进行声明时赋予它的类型;一个对象的动态类型是通过它当前引用的对象的类型决定的,动态类型表明了它应具有怎么样的行为。
Shape *pc = new Circle;
pc的静态类型为Shape*,动态类型为Circle*
虚函数是动态绑定的,意味着,对于一个特定函数的调用,其调用对象的动态类型将决定调用这一函数的哪个版本。
class Shape { public: enum ShapeColor{Red,Green,Blue}; virtual void draw(ShapeColor color = Red) const = 0; }; class Rectangle :public Shape { public: virtual void draw(ShapeColor color = Green) const { cout << "use color:" << color << endl; } }; int main() { Shape* pr = new Rectangle(); pr->draw(); //use color:0 system("pause"); }
条目38:使用组合来表示“A拥有一个B”、“A以B的形式实现”
class Address {}; class PhoneNumber {}; class Person { private: std::string name; Address address; PhoneNumber voiceNumber; PhoneNumber faxNumber; };
条目39:审慎使用私有继承
如果类之间的层次关系是私有继承的话,那么编译器一般不会将派生类对象(比如 Student)直接转换为一个基类对象(比如 Person)。
派生类中继承自私有基类的成员也将成为私有成员,即使他们在基类中用 public 或 protected 修饰也是如此。
私有继承意味着“A 以 B 的形式实现”。通常它的优先级要低于组合,但是当派生类需要访问基类中受保护的成员,或者需要重定义派生的虚函数时,私有继
承还是有其存在的合理性的。
条目40:审慎使用多重继承
条目41:理解隐式接口和编译时多态
类的接口是显式的,基于函数签名。多态通过虚函数产生于运行时。模板参数的接口是隐式的,基于逻辑表达式。
条目42:理解typename的双重含义
在声明模板参数时,class 和 typename 可以互换。使用 typename 来指定嵌套从属类型名字。
条目43:了解如何访问模板化基类内的名字
要在派生类模板中调用基类模板内部的名字,可以通过“this->”前缀,通过using 声明,或者通过一个显式的基类限定来实现。
条目44:将参数无关的代码从模板中分离
类模板的成员函数只有在被使用时才会被隐式初始化
条目45:使用成员函数模板来接纳“所有兼容类型”
条目46:在需要进行类型转换时要将非成员函数定义在模板内部
在编写一个类模板时,如果需要提供一个与该模板相关的、对所有参数支持隐式类型转换的函数,要在类模板内部将其定义为友元。
template<typename T> class Rational { public: Rational(const T& numerator = 0,const T& denominator = 1) :num(numerator), den(denominator) { } const T numerator() const { return num; } const T denominator() const { return den; } friend const Rational operator*(const Rational& lhs,const Rational& rhs) { Rational<T> result(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator()); return result; } private: T num; T den; }; int main() { Rational<int> a(1,2); Rational<int> result = a*2;//隐式类型转换 return 0; }
条目47:使用traits类来描述类型的相关信息
条目48:留意模板元编程