#! https://zhuanlan.zhihu.com/p/537589122
Effective C++ 55
导读
- 声明式(declaration)
- 签名式(signature)
- 定义式(definition)
- 初始化(initialization)
- explicit关键字:阻止隐式类型转换
- 接口(interface)
- lhs与rhs
一 让自己习惯C++
1 视C++为一个语言联邦
2 尽量以const,enum,inline替换#define
class GamePlayer{
private:
enum { NumTurns = 5};
// static const int NumTurns = 5;
int scores[NumTurns];
};
3 尽可能使用const
4 确定对象被使用前已先被初始化
- 别混淆赋值和初始化,初始化效率高于赋值。eg.类的成员初始化列表与构造函数内部赋值
- 单例模式
二 构造/析构/赋值运算
5 了解c++ 默默编写并调用哪些函数
- 自动生成:构造函数、拷贝构造函数、拷贝赋值函数、析构函数
- 另外:移动构造函数、移动赋值函数也很重要
class Test{
public:
Test() = default;
Test(const Test& t) = default;
Test& operator=(const Test& t)= default;
~Test() = default;
Test(const Test&& t) noexcept{}
Test& operator=(const Test&& t) noexcept{}
};
6 若不想使用编译器自动生成的函数,就该明确拒绝
- 声明为私有,或者继承私有class,或者=delete
class Uncopyable{
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable{};
7 为多态基类声明virtual析构函数
- 基类析构函数不加virtual会造成“局部销毁”,eg.
Base* ptr = New Derived(); delete ptr; 中只会调用base的析构,而derived类很可能没有被销毁。 - 虚函数表及指针,X64占8字节,X86占4字节,参考
- 纯虚析构函数(
virtural ~Test()=0 ) = 抽象类 = 不能单独实例化 = 派生类必须要定义该函数(如果基类未定义)
8 别让异常逃离析构函数
9 绝不在构造和析构过程中调用virtual函数
10 令operator=返回一个*this引用
- 可实现连锁赋值(a=b=c=20)
- 只是个协议,并无强制性
class Widget{
Widget& operator=(const Widget& rhs){
...
return *this;
}
}
11 在operator=中处理“自我赋值”
class BitMap{};
class Widget{
Widget& operator=(const Widget& rhs){
if(this == &rhs) return *this;
delete pb;
pb = new BitMap(*rhs.pb);
return *this;
}
Widget& operator=(const Widget& rhs){
BitMap* pOring = pb;
pb = new BitMap(*rhs.pb);
delete pOring;
return *this;
}
Widget& operator=(const Widget& rhs){
Widget temp(rhs);
swap(temp);
return *this;
}
Widget& operator=(Widget rhs){
swap(rhs);
return *this;
}
void swap(Widget& rhs);
private:
BitMap* pb;
};
12 复制对象时勿忘其每一个成分
- 复制所有成员变量和基类的所有成元
- 深拷贝和浅拷贝
- 拷贝构造和拷贝赋值不要相互调用,共同机能放入第三个函数
三 资源管理
13 以对象管理资源
- c++11 智能指针:shared_ptr, unique_ptr, weak_ptr
- RAII(Resource Acquisition Is Initialization) 资源取得时便是初始化时刻
- 多个auto_ptr不能指向同一个对象(auto_ptr被销毁时会自动删除所指对象),unique_ptr已替代auto_ptr
- weak_ptr专门为避免shared_ptr环形引用问题而设计,算是shared_ptr的辅助工具
14 在资源管理类中小心copying行为
- 禁止复制:将类的copy函数声明为私有
- 对底层资源祭出“引用计数法”:共享指针
- 复制底部资源:深拷贝
- 转移底部资源的拥有权:auto_ptr,std::move()
15 在资源管理类中提供对原始资源的获取
- shared_ptr::get()
- shared_ptr/auto_ptr 重载->和* 运算符
16 成对使用new和delete时要采取相同形式
- new/delete 与 new[]/delete[]
17 以独立语句将newed对象置入智能指针
如下第二个有风险的函数调用,在调用之前,编译器必须创建代码:
- 执行new Widget
- 调用priority()
- 调用shared_ptr构造函数
以上三件事,c++编译器以什么次序完成并不知道,弹性很大,如果以上面顺序执行,在对priority()的调用导致异常,new Widget将会遗失,引发资源泄露。所以要分离语句。
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
processWidget(new Widget, priority());
processWidget(std::shared_ptr<Widget>(new Widget), priority());
std::shared_ptr<Widget> pw(new Widget)
processWidget(pw, priority());
四 设计与声明
18 让接口容易被正确使用,不易被误用
- STL容器的接口十分一致,size函数等等。相反java就比较混乱
- 年月日的示例,防止接口误用
class Date{
public:
Date(int month, int day, int year);
Date(const Month& m, const Day& d, const Year& y);
};
Investment* createInvestment();
std::shared_ptr<Investment> createInvestment();
19 设计class犹如设计type
注意class的重载、操作符、控制内存的分配和归还、传参、继承、类型转换、定义对象的初始化和终结等的设计,和默认类型一样设计。
20 宁以 传常引用 替换 传值
- 为避免不必要的重复内存构造和析构,传常引用。
- 内置类型(如int)、STL的迭代器和函数对象传值比传引用效率高。
21 必须返回对象时,谨慎返回其引用
- 函数内不能返回临时值的引用,当函数结束时,内存就被销毁了,返回引用就会报错
int& func(){
int a = 99;
return a;
int* a = new int(99);
return *a;
}
22 将成员变量声明为私有
- 保证封装性,成员变量私有更安全,可以设置setter和getter函数访问私有
- protected不比public更具封装性
23 宁以non-member、non-friend替换member函数
- 以网页浏览器class为例探讨删除缓存:
clearEverything 比clearBrowser 的封装性低。 - 越少代码可以看到数据,越多数据可以被封装。越多函数可以访问它,数据的封装性就越低。
- 成员函数可以访问所有成员,非成员非友元函数不能访问任何成员,但两者提供相同机能。所有后者封装性高
class WebBrowser {
public:
void clearCache() {}
void clearHistory() {}
void removeCookies() {}
void clearEverything() {
clearCache();
clearHistory();
removeCookies();
}
};
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
- 比较好的做法是类和非成员非友元函数在一个namespace中,同时可以在其他头文件中增加其他的功能函数,需要该功能调用头文件即可,减少编译的依赖关系
- 把所有便利功能函数放在多个头文件但同一个namespace中,意味着客户可以轻松扩展功能。和C++标准库一样的组织方式
#include "webbrowser.h"
namespace WebBrowserStuff {
class WebBrowser {
public:
void clearCache() {}
void clearHistory() {}
void removeCookies() {}
};
void clearBrowser(WebBrowser& wb) {
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
}
#include "webbrowserbookmarks.h"
namespace WebBrowserStuff {
}
24 若所有参数皆需类型转换,请为此采用non-member函数
- 设计有理数支持混合式运算,所以允许隐式转换。示例中有两种乘法设计方式,一种是成员函数,一种是非成员函数。
- 对于成员函数1,
res = 2 * one_half ,相当于2.operator*(one_half) ,中的2没有相应的class,也就没有operator* 成员函数。同事也没有非成员函数满足operator*(2, one_half) - 对于非成员函数2,满足
operator*(2, one_half) ,其中2进行了隐式转换Rational(2)
class Rational {
public:
Rational(int numerator = 0, int denominator = 1)
: _numerator(numerator), _denominator(denominator) {}
int numerator() const { return _numerator; }
int denominator() const { return _denominator; }
private:
int _numerator;
int _denominator;
};
const Rational operator*(const Rational& lhs, const Rational& rhs) {
Rational res(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
return res;
}
int main() {
Rational one_half(1, 2);
Rational res;
res = one_half * 2;
res = 2 * one_half;
return 0;
}
25 考虑写出一个不抛出异常的swap函数
- 标准程序库提供的算法如下,只需要T支持拷贝构造函数和赋值函数
namespace std{
template<typename T>
swap(T a, T b){
T tmp(a);
a = b;
b = tmp;
}
}
class WidgetImpl {
private:
int a, b, c;
std::vector<double> vec;
};
class Widget {
public:
Widget(const Widget& rhs) {}
Widget& operator=(const Widget& rhs) {
*pImpl = *(rhs.pImpl);
return *this;
}
void swap(Widget& other) { std::swap(pImpl, other.pImpl); }
private:
WidgetImpl* pImpl;
};
namespace std {
template <>
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b);
}
}
- 而如果Widget设计为模板类,类设计的没错,但是c++只允许
偏特化 类模板,不允许偏特化 函数模板。尤其是管理规则比较特殊的std内的模板,只支持全特化
template<typename T>
class WidgetImpl {
private:
std::vector<T> vec;
};
template<typename T>
class Widget {
...
private:
WidgetImpl<T>* pImpl;
};
namespace std {
template <typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
namespace WidgetStuff{
template <typename T>
class WidgetImpl;
template <typename T>
class Widget;
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
五 实现
- 适当提出class和function的声明
- 太快定义变量会拖延效率
- 过度使用转型(cast)可能导致代码变慢又难维护
- 未考虑异常导致资源泄露
- 过度inline导致代码膨胀
- 过度耦合导致编译时间冗长
26 尽可能延后变量定义式的出现时间
std::string encryptPassword(const std::string& password){
if(password.length() < 8){
throw logic_error("Password is too short");
}
std::string encrypted(password);
encrypt(encrypted);
return encrypted;
}
Widget w;
for(int i = 0; i < n; ++i){
w = 某个值;
}
for(int i = 0; i < n; ++i){
Widget w(某个值);
}
27 尽量少做转型动作
- 不要使用c旧式转型,使用c++新式转型
- const_cast 改变常量性和可变性
- dynamic_cast 可以将一个指向基类的指针或者引用转换成指向其派生类的指针或者引用。耗时很慢,尽量避免
- reinterpret_cast 将任意的指针类型转换成其他任意的指针类型,容易出错,不具有移植性
- static_cast 普通类型转换
28 避免返回handles指向对象内部成分
-
handles指包括引用、指针、迭代器。 -
返回指针指向某个成员函数
struct Point {
int x = 0;
int y = 0;
};
class Test {
public:
int& GetX() const { return _pt->x; }
const int& GetX() const { return _pt->x; }
private:
std::shared_ptr<Point> _pt;
};
struct Point {
int x = 99;
int y = 66;
};
class Test {
public:
Test() {
cout << "Test()" << endl;
_pt = new Point();
}
~Test() {
cout << "~Test()" << endl;
delete _pt;
}
const int &GetX() const { return _pt->x; }
private:
Point *_pt;
};
int main() {
const int *ptr = nullptr;
{
Test t;
auto x = &(t.GetX());
cout << "1 dir:" << x << " value:" << *x << endl;
ptr = x;
cout << "2 dir:" << ptr << " value:" << *ptr << endl;
}
cout << "3 dir:" << ptr << " value:" << *ptr << endl;
return 0;
}
- 所以一般情况下尽量避免返回handles(包括引用、指针、迭代器)指向对象内部,遵守该条款可增加封装性。
29 为异常安全而努力是值得的
- 当异常被抛出时,带有异常安全性的函数会:1. 不泄露任何资源, 2.不允许数据破坏。以表现夹带背景图案的class为例:
class Image{
public:
Image(const std::istream& img_src){}
};
class PrettyMenu{
public:
void ChangeBackground(std::istream& img_src){
mtx.lock();
delete bg_image;
++image_changes;
bg_image = new Image(img_src);
mtx.unlock();
}
void ChangeBackground2(std::istream& img_src){
std::lock_guard<std::mutex> auto_lock(mtx);
delete bg_image;
++image_changes;
bg_image = new Image(img_src);
}
private:
std::mutex mtx;
Image* bg_image;
int image_changes;
};
- 异常安全函数(Exception-safe functions)提供以下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
- 强烈保证:如果异常被抛出,程序状态不改变。函数调用成功则是完全成功,失败则要回复到调用之前的状态。
- 不抛掷(nothrow)保证。承诺绝不抛出异常,
- 使用智能指针管理,保证基本的异常安全性。
1基本承诺
class Image{
public:
explicit Image(const std::istream& img_src){}
};
class PrettyMenu{
public:
void ChangeBackground(std::istream& img_src){
std::lock_guard<std::mutex> auto_lock(mtx);
bg_image.reset(new Image(img_src));
++image_changes;
}
private:
std::mutex mtx;
shared_ptr<Image> bg_image;
int image_changes;
};
- copy and swap,和pimpl idiom手法(将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向新的实现对象,即副本)。
2强烈保证
class Image{
public:
explicit Image(const std::istream& img_src){}
};
struct PMImpl{
shared_ptr<Image> bg_image;
int image_changes;
};
class PrettyMenu{
public:
void ChangeBackground(std::istream& img_src){
std::lock_guard<std::mutex> auto_lock(mtx);
shared_ptr<PMImpl> p_new(new PMImpl(*p_impl));
p_new->bg_image.reset(new Image(img_src));
++p_new->image_changes;
std::swap(p_impl, p_new);
}
private:
std::mutex mtx;
shared_ptr<PMImpl> p_impl;
};
30 透彻了解inlining的里里外外
- 一个inline函数取决于 编译环境,取决于编译器
- 构造函数和析构函数不适合inline
- inline限制在小型、被频繁调用的函数上。使得调试和二进制升级更容易,使得代码膨胀问题最小化
- 不要因为函数模板出现在头文件,就声明为inline。(std::max是内联函数模板)
31 将文件间的编译依存关系降至最低
#pargma once
class A{
public:
A();
};
#include "a.h"
A::A(){}
#pargma once
#include "a.h"
class B{
public:
B();
A _a;
};
#include "b.h"
B::B(){}
#pargma once
#include "b.h"
class C{
public:
C();
B _b;
};
#include "c.h"
C::C(){}
#include "c.h"
int main(){
C c;
return 0;
}
-
接口是指.h ,实现是指.cpp 。c++编译分两步编译.o文件 和链接.o成执行文件(exe/其他) 。接口和实现改变都需要重新编译.o,但两者也有区别,最好只修改实现。 -
一般来说,我们改cpp也就是实现的地方,只会需要重新编译该文件的.o,而依赖该文件的接口也就是.h没变,所以依赖的文件是不需要重新编译的。 -
也就是说,修改a.cpp,只需要重新编译a.cpp.o 和链接成执行文件。这也是类设计的时候需要接口和实现分离两个文件的原因之一! -
但是当我们修改接口a.h 时,需要重新编译a.cpp.o 、b.cpp.o 、c.cpp.o 、main.cpp.o 和链接成执行文件。这就会导致浪费时间编译整个工程! -
编译依存最小化有两种方法:Handle classes 和Interface classes
(1) Handle classes
前置声明+指针
-
对于上面的示例可以更改为如下示例。主要改动有两点 -
一是在有依赖的接口前面去掉依赖的头文件,加上依赖类前置声明,在实现里加入依赖头文件:如b.h去掉了#include "a.h" ,前置声明了class A ,在b.cpp里面加入了#include "a.h" -
二是在有依赖的接口里面,使用指针声明成员变量:如B类里面使用了A* _a 声明成员。因为A只有声明,此时这个地方没有定义,编译器不知道_a具体分配多少内存,但是使用指针就可以。“将对象实现细目隐藏于一个指针背后的游戏”,这也是该方法的巧妙之处! -
此时我们修改a.h时,需要重新编译a.cpp.o 、b.cpp.o 和链接成执行文件,b.cpp.o 是因为b.cpp里面包含了A的接口,A接口变了,所以需要重新编译,但是B的接口没变,所以后面的两个都不需要重新编译!
#pargma once
class A{
public:
A();
};
#include "a.h"
A::A(){}
#pargma once
class A;
class B{
public:
B();
A* _a;
};
#include "b.h"
#include "a.h"
B::B(){}
#pargma once
class B;
class C{
public:
C();
B* _b;
};
#include "c.h"
#include "b.h"
C::C(){}
#include "c.h"
int main(){
C c;
return 0;
}
person.h
#ifndef PERSON_H
#define PERSON_H
#include <memory>
class PersonImpl;
class Person
{
public:
Person(const std::string& name);
std::string name() const;
private:
std::shared_ptr<PersonImpl> pImpl;
};
#endif
person.cpp
#include "person.h"
#include "personimpl.h"
Person::Person(const std::string& name) : pImpl(new PersonImpl(name)){}
std::string Person::name() const{
return pImpl->name();
}
personimpl.h
#ifndef PERSONIMPL_H
#define PERSONIMPL_H
#include <iostream>
class PersonImpl
{
public:
PersonImpl(const std::string& name);
std::string name() const;
private:
std::string _name;
};
#endif
personimpl.cpp
#include "personimpl.h"
PersonImpl::PersonImpl(const std::string& name):_name(name){}
std::string PersonImpl::name() const{
return _name;
}
- C++标准库里的内含iostream各组件的声明式,定义
(2) Interface classes
抽象基类+继承+指针
- 用基类指针指向派生类
- 只需要提供基类头文件person.h给使用者即可,PersonImpl类是完全封装的,别人看不见。和上面是一样的。
person.h
#ifndef PERSON_H
#define PERSON_H
#include <memory>
class Person
{
public:
virtual ~Person() = default;
static std::shared_ptr<Person> create();
virtual std::string name() const = 0;
};
#endif
person.cpp
#include "person.h"
#include "personimpl.h"
std::shared_ptr<Person> Person::create(){
return std::shared_ptr<Person>(new PersonImpl);
}
personimpl.h
#ifndef PERSONIMPL_H
#define PERSONIMPL_H
#include <iostream>
#include "person.h"
class PersonImpl : public Person
{
public:
PersonImpl();
~PersonImpl() = default;
std::string name() const;
private:
std::string _name;
};
#endif
personimpl.cpp
#include "personimpl.h"
PersonImpl::PersonImpl():_name("admin"){}
std::string PersonImpl::name() const{
return _name;
}
main.cpp
#include <iostream>
#include "person.h"
using namespace std;
int main()
{
std::shared_ptr<Person> per(Person::create());
cout << per->name() << endl;
return 0;
}
六 继承与面向对象设计
32 确定你的public继承塑模出is-a关系
class Base{
virtual ~Base(){}
};
class Derived : public Base{
~Derived(){}
}
- 除此之外,还有
has a 和is-implemented-in-terms-of (根据某物实现出)
33 避免遮掩继承而来的名称
- 继承时的名称遮掩问题可以使用
using声明式 和私有继承+转交函数 解决。
class Base{
virtura ~Base(){}
virtual void f1();
virtual void f1(int);
void f2();
void f2(float);
};
class Derived : public Base{
~Derived(){}
virtual void f1();
void f2();
}
Derived d;
d.f1();
d.f2();
d.f1(1);
d.f2(1.3f);
class Derived : public Base{
~Derived(){}
using Base::f2;
using Base::f1;
virtual void f1();
void f2();
}
d.f1(1);
d.f2(1.3f);
class Derived : private Base{
~Derived(){}
virtual void f1(){
Base::f1();
}
...
}
34 区分接口继承和实现继承
- public继承了两种:
函数接口 和函数实现 - 基类声明纯虚函数
virtual void f() = 0; ,派生类只继承函数接口,且需要强制性重新实现 - 基类声明纯虚函数
virtual void f(); ,派生类继承函数接口和默认函数实现 - 基类的纯虚函数也可以拥有实现
35 考虑virtual函数以外的其他选择
NVI + Strategy
-
使用非virtual 接口手法实现Template Method 模式 -
Template Method 模式之一:使用普通成员函数调用私有虚函数,non-virtual interface(NVI) 手法 -
派生类需要重新定义虚函数时,可以设为protected
class GameCharacter{
public:
int healthValue() const{
return doHealthValue();
}
protected:
virtual int doHealthValue() const{
return health_value;
}
int health_value;
};
- 使用
函数指针 实现Strategy 模式:就是构造函数使用函数指针传递,派生类继承基类,在初始化的时候可以使用不同的健康计算函数
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc) { return 1; }
typedef std::function<int(const GameCharacter&)> HealthCalCFunc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalCFunc hcf = defaultHealthCalc)
: healthFuncPtr(hcf) {}
virtual ~GameCharacter() {}
int healthValue() const { return healthFuncPtr(*this); }
private:
HealthCalCFunc healthFuncPtr;
};
class EvilBadGuy : public GameCharacter {
public:
explicit EvilBadGuy(HealthCalCFunc hcf = defaultHealthCalc)
: GameCharacter(hcf) {}
};
int main() {
GameCharacter gc(defaultHealthCalc);
EvilBadGuy ebg1(defaultHealthCalc);
cout << ebg1.healthValue() << endl;
return 0;
}
- 传统古典的
Strategy 模式是将健康计算函数设计成一个基类,不同的健康计算函数派生。然后用类指针替换任务里的函数指针,本质是一样的。设计模式里面的内容。
36 绝不重新定义继承来的non-virtual函数
- 非虚函数(普通成员函数)静态绑定,虚函数动态绑定。前面遮掩继承有提到过
class Base {
public:
virtual ~Base() {}
void f() { cout << "base f()" << endl; }
};
class Derived : public Base {
public:
void f() { cout << "Derived f()" << endl; }
};
int main() {
Derived x;
Base* pb = &x;
Derived* pd = &x;
pb->f();
pd->f();
return 0;
}
37 绝步重新定义继承而来的缺省参数值
class Base {
public:
virtual ~Base() {}
virtual void f(int num = 1) { cout << "base : " << num << endl; }
};
class Derived1 : public Base {
public:
virtual void f(int num = 2) { cout << "Derived1 : " << num << endl; }
};
class Derived2 : public Base {
public:
virtual void f(int num) { cout << "Derived2 : " << num << endl; }
};
int main() {
Derived1 d;
Base* pb1 = new Derived1();
Base* pb2 = new Derived2();
d.f();
pb1->f();
pb2->f();
delete pb1;
delete pb2;
return 0;
}
38 通过复合塑造出has-a 或根据某物实现出
is a 公有继承(私有继承不是)has a 复合(应用域:仅类成员)is-implemented-in-terms-of 复合(实现域:由类成员实现该类的内容,比如用一个链表成员实现平衡二叉树)、私有继承
39 明智而审慎地使用private继承
- 编译器不会自动把
私有继承的派生类 转换为基类 ,继承的成员全部变成私有属性
class Person {
public:
virtual ~Person() {}
virtual void f() const { cout << "person f()" << endl; }
};
class Student : private Person {
public:
virtual void f() const { cout << "Student f()" << endl; }
};
void doSomething(const Person& p) { p.f(); }
int main() {
Person p;
Student s;
doSomething(p);
doSomething(s);
return 0;
}
- 私有继承是
is-implemented-in-terms-of ,比复合的级别低,但是当派生类需要访问基类的protected成员,或者重新定义virtual函数时,这么设计是合理的。 - 与复合不同,私有继承可以造成empty base最优化,对象尺寸最小化
40 明智而审慎地使用多重继承
class A {
public:
void checkout() { cout << "A" << endl; }
};
class B {
public:
void checkout() { cout << "B" << endl; }
};
class C : public A, public B {};
int main() {
C c;
c.A::checkout();
c.B::checkout();
return 0;
}
class File {
public:
string file_name;
};
class InputFile : public File {};
class OutputFile : public File {};
class IOFile : public InputFile, public OutputFile {};
int main() {
IOFile io;
io.file_name;
return 0;
}
class File {
public:
string file_name;
};
class InputFile : virtual public File {};
class OutputFile : virtual public File {};
class IOFile : public InputFile, public OutputFile {};
int main() {
IOFile io_file;
io_file.file_name;
return 0;
}
- virtual继承的类,产生的对象体积大,访问成员变量速度慢等。virtual继承的代价。所以避免在virtual base class中放置数据。
- 虚拟继承(共享继承)是多重继承中特有的概念,解决多重继承中数据的二义性。
七 模板与泛型编程
41 了解隐式接口和编译器多态
- class和template都支持接口和多态
- class的接口是
显示 ,以函数签名为中心。多态通过virtual发生在运行期 - template的接口是
隐式 , 奠基于有效表达式。多态通过模板具现化和函数重载解析发生于编译期
template<typename T>
void doSomething(T& t){
t.size();
}
42 了解typename的双重意义
- 声明template参数时,class和typename都可以
template<class T> f(T& t);
template<typename T> f(T& t);
- 嵌套从属类型名称时要使用typename,其他名称不用
template<typename C>
void print2nd(const C& container){
if(container.size() >= 2){
typename C::const_iterator iter(container.begin());
++iter;
cout << *iter << endl;
}
}
template<typename C>
void f(const C& container,
typename C::iterator iter)
{}
- 基类列和成员初始列的嵌套类型不能使用typename
template<typename T>
class Base{
public:
Base() = default;
virtual ~Base() = default;
class Nested{
public:
explicit Nested(int x){}
Nested() = default;
};
};
template<typename T>
class Derived : public Base<T>::Nested{
public:
explicit Derived(int x) : Base<T>::Nested(x){
typename Base<T>::Nested temp;
}
};
43 学习处理模板化基类内的名称
- 派生类模板使用基类模板函数时要加
this 或者明确域 ,否则基类模板的函数会被派生类覆盖遮掩。
class CompanyA{
public:
void sendClearText(const string& msg){}
};
class CompanyB{
public:
void sendEncrypted(const string& msg){}
};
template<typename T>
class MsgSender{
public:
void sendClear(const string& info){
T company;
company.sendClearText(info);
}
};
template<>
class MsgSender<CompanyB>{
public:
void sendSecret(const string& info){
CompanyB company;
company.sendEncrypted(info);
}
};
template<typename T>
class LoggingMsgSender: public MsgSender<T>{
public:
void SendClearMsg(const string& info){
MsgSender<T>::sendClear(info);
}
};
44 将与参数无关的代码抽离templates
template<typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(size_t n, T* p_mem) : size(n), p_data(p_mem){}
void invert(size_t matrix_size){}
private:
size_t size;
T* p_data;
};
template<typename T, size_t n>
class SquareMatrix : private SquareMatrixBase<T>{
public:
SquareMatrix() : SquareMatrixBase<T>(n, data){}
void invert(){
SquareMatrixBase<T>::invert(n);
}
private:
T data[n*n];
};
45 运用成员函数模板接受所有兼容类型
- 以设计自定义的智能指针模板为例,支持基类和派生类指针的转换。因为具现化了不同的两个类,所以需要设计拷贝构造函数
class Base{
public:
virtual ~Base() = default;
};
class Derived : public Base{};
template<typename T>
class SmartPtr{
public:
explicit SmartPtr(T* ptr) : _ptr(ptr){}
template<typename U>
SmartPtr(const SmartPtr<U>& other){}
T* get(){return _ptr;}
private:
T* _ptr;
};
int main() {
Base* ptr = new Derived;
SmartPtr<Base> pt1 = SmartPtr<Derived>(new Derived);
return 0;
}
- 使用成员模板生成了
泛化拷贝构造 或泛化赋值操作 ,正常的拷贝构造和赋值操作函数还是需要声明的。
46 需要类型转换时请为模板定义非成员函数
- 使用
24 条的有理数例子进行模板化设计,为了支持模板的混合式算数运算,设计如下:
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1)
: _numerator(numerator), _denominator(denominator) {}
T numerator() const { return _numerator; }
T denominator() const { return _denominator; }
friend Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
private:
T _numerator;
T _denominator;
};
int main() {
Rational<int> one_half(1, 2);
Rational<int> result = one_half * 2;
return 0;
}
47 请使用traits classes表示类型信息
-
STL主要由容器、迭代器、算法等 的模板组成,也包括若干工具类模板,例如advance,用来将某个迭代器移动某个给定距离,但迭代器有5种,移动方式也不同:
- Input迭代器,只能向前移动,一次一步,只读
- Output迭代器,只能向前移动,一次一步,只写
- forward迭代器,继承Input迭代器,一次一步,读写
- Bidirectional迭代器,继承forward迭代器,双向单次移动。
- random access迭代器,继承Bidirectional迭代器,常量时间内向前或向后跳跃任意距离,包含上述功能
-
以上五种分类,C++标准程序库分别提供了专属卷标结构(tag struct)加以确认 ...\c++\bits\stl_iterator_base_types.h
struct input_iterator_tag { };
struct output_iterator_tag { };
struct forward_iterator_tag : public input_iterator_tag { };
struct bidirectional_iterator_tag : public forward_iterator_tag { };
struct random_access_iterator_tag : public bidirectional_iterator_tag { };
- 如果设计advance函数(用来将某个迭代器移动某个给定距离),就需要知道是什么类型迭代器。而
traits允许在编译器取得某些类型信息,Traits不是c++关键字或一个定义好的构建,是一种技术,是c++程序员共同遵守的协议 ,设计如下:
template<typename IterT, typename DistT>
void MyAdvance(IterT &iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag)) {
iter += d;
} else {
if (d > 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}
- 但是上面存在编译问题,类型可以在编译器确定,但是if else是在运行期确定,为什么要把编译器可以确定的事延到运行期呢?使用重载可以解决,如下:
template<typename IterT, typename DistT>
void MyAdvance(IterT &iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag)) {
iter += d;
} else {
if (d > 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}
template<typename IterT, typename DistT>
void DoAdvance(IterT &iter, DistT d, std::random_access_iterator_tag){
iter += d;
}
template<typename IterT, typename DistT>
void DoAdvance(IterT &iter, DistT d, std::bidirectional_iterator_tag){
if (d > 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
template<typename IterT, typename DistT>
void DoAdvance(IterT &iter, DistT d, std::input_iterator_tag){
if (d < 0) {
throw std::out_of_range("Negative distance");
}
while (d--) ++iter;
}
- 另外,来看c++官方的实现,和书上的代码是差不多的。
template<typename _InputIterator, typename _Distance>
inline _GLIBCXX17_CONSTEXPR void
advance(_InputIterator& __i, _Distance __n)
{
typename iterator_traits<_InputIterator>::difference_type __d = __n;
std::__advance(__i, __d, std::__iterator_category(__i));
}
template<typename _InputIterator, typename _Distance>
inline _GLIBCXX14_CONSTEXPR void
__advance(_InputIterator& __i, _Distance __n, input_iterator_tag)
{
__glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
__glibcxx_assert(__n >= 0);
while (__n--)
++__i;
}
template<typename _BidirectionalIterator, typename _Distance>
inline _GLIBCXX14_CONSTEXPR void
__advance(_BidirectionalIterator& __i, _Distance __n,
bidirectional_iterator_tag)
{
__glibcxx_function_requires(_BidirectionalIteratorConcept<
_BidirectionalIterator>)
if (__n > 0)
while (__n--)
++__i;
else
while (__n++)
--__i;
}
48 认识模板元编程
- Template meta programming(TMP,模板元编程)是编写基于模板的C++程序并执行于
编译期 的过程
- 模板元编程执行于编译期,原来在运行期才能侦查的错误,编译期就可以发现。(缺点是编译时间变长了,编译通过但是运行报错不容易找到bug)
- 模板元编程高效,较小的可执行文件、较短的运行期、较少的内存需求。
- 模板元编程的起手程序是在编译期计算阶乘。使用模板具现化每个阶乘的struct,并且使用enum hack(是模板元编程的基础技术)声明模板元编程的变量
template<unsigned n>
struct Factorial {
enum {
value = n * Factorial<n - 1>::value
};
};
template<>
struct Factorial<0> {
enum {
value = 1
};
};
int main() {
cout << Factorial<5>::value << endl;
cout << Factorial<10>::value << endl;
return 0;
}
- 模板元编程的内容很多很深,书上并没有多做深入,但是对于程序库的开发人员,模板元编程还是很重要的
八 定制new和delete
主要是C++内存管理,在《STL源码剖析》中的空间配置器(allocator)篇章也着重讲了内存管理的内容,回头发现这一章还是很重要的。
-
两个主角:分配例程 和归还例程 (allocation and deallocation routines),即operator new 和operator delete -
配角:new-handler -
多线程环境下的内存管理很重要。由于heap是一个可被改动的全局性资源,如果没有适当的同步控制,一旦使用无锁算法或精心防止并发访问,调用内存例程可能很容易导致管理heap的数据结构内容败坏。 -
operator new 和operator delete 分配单一对象,operator new[] 和operator delete[] 分配一组对象 -
STL容器所使用的的heap内存由容器自带或分配的allocator来管理,而不是new和delete直接管理,这也是《STL源码剖析》第二章的主要分析内容。 -
区分new 、operator new 和placement new ,delete亦是。
- new = operator new + 构造函数
- delete = 析构函数 + operator delete
- placement new是operator new的重载版本,解决内存分配慢的问题
49 了解new-handler的行为
- 当operator new无法满足某一内存分配需求时,会先调用客户端指定的错误处理函数(new-handler),再抛出异常。所以要调用
set_new_handler 函数传入函数指针,设置好错误处理函数(new-handler)
namespace std
{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler) throw();
}
void OutOfMem(){
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main{
std::set_new_handler(nullptr);
std::set_new_handler(OutOfMem);
int* p_big_data_array = new int[100000000000L];
return 0;
}
50 了解new和delete的合理替换时机
替换编译器提供的operator new和operator delete的理由如下:
- 用来检测运用上的错误:如new的内存delete时不幸失败会导致内存泄漏,new的内存多次delete会导致不确定行为等
- 为了强化效能:大块内存、小块内存、大小混合型内存的需求。存在破碎问题(总量足够但分散为小区块的自由内存),这时候无法满足大区块内存要求
- 为了收集使用上的统计数据
51 编写new和delete时需固守常规
52 写了placement new也要写placement delete
九 杂项讨论
53 不要轻忽编译器的警告
54 让自己熟悉包括在TR1在内的标准程序库
- 也就是现在C++11的一些新特性,智能指针、STL、lambda等
55 让自己熟悉Boost
- C++标准程序库的测试场或后花园,好的程序经过审核评估后会加入到C++标准程序库中
- 功能广泛,值得学习和使用
|