#include <iostream>
#include <memory>
#include <vector>
#include <mutex>
#include <functional>
#include <list>
namespace effective_cpp {
// 条款01:视c++为一个语言联邦
namespace term01 {
// - C
// - Objective-Oriented C++
// - Tempalte C++
// - STL
// 当关注点从一个次语言转到另一个次语言时,为了高效编程,你可能需要改变一些策略。
// 比如对内置类型而言,值传递(一般)比引用传递更高效。
// 但对于用户自定义类,由于构造函数及虚构函数的存在,const型引用传递可能更加高效。
// 总之,应深入了解每个次语言的特性进行有针对性的操作。
}
// 条款02:尽量以const,enum,inline,替换#define
namespace term02 {
// 对于单纯常量,最好以const对象或者enum代替#define
#define STR "giggle"
const char* const pStr = "giggle";
const std::string str("giggle");
// constexpr char* pStr = "giggle";
// 对于形似函数的宏(macros),最好改用template inline函数代替#define。
template<typename T>
void f(const T& t) {}
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
template<typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a : b);
}
// enum的一种用法但实际没啥卵用
class A {
private:
enum { Length = 10 };
int array[Length];
};
}
// 条款03:尽可能的使用const
namespace term03 {
// 如果关键字const在*左边,表示被指物是常量;const在*右边,则指针本身是常量,const std::iterator 被指物是常量;std::const_iterator 自身是常量。
// 将某些东西声明为const,可帮助编译器侦测某些错误。
// 将const作用于成员函数,是为了确认该成员函数可作用于const对象身上,此时函数不能更改对象中任何non-static成员变量,
// 且两个成员函数常量性不同可以被重载。为了实现代码复用,可以用non-const成员函数调用const成员函数,为了成功调用正确的重载函数,可能需要类型转换。
// 总之,该用const的地方就要用上。
}
// 条款04:确定对象被使用前已先被初始化
namespace term04 {
// 注意初始化与赋值的区别,成员变量的初始化要使用初始化列表,并注意顺序要与声明次序一致。
// 为了避免不同编译单元的non-local static对象初始化顺序的错误,可使用Singleton单例模式转化为local static对象。
// 函数内含static对象可能导致多线程系统中的不稳定性(不太懂)。
}
// 条款05:了解C++默认编写并调用了哪些函数
namespace term05 {
class CBase {
public:
// 未编写任何构造函数时,系统才生成默认构造函数,拷贝构造函数或拷贝赋值运算符不存在时,
// 系统始终生成相应函数,且只是将来源对象的每个non-static成员变量拷贝到目标对象。
// 编译器生成的这些函数都是public且inline的,生成的析构函数是non-virtual的。
CBase() : m_str() {
std::cout << "CBase()" << std::endl;
}
CBase(const std::string& str) : m_str(str) {
std::cout << "CBase(const std::string&)" << std::endl;
}
CBase(const CBase& right) : m_str(right.m_str) {
std::cout << "CBase(const CBase&)" << std::endl;
}
const CBase& operator = (const CBase& right) {
// 处理自我赋值。
if (&right == this) {
std::cout << "Copy self" << std::endl;
return *this;
}
m_str = right.m_str;
std::cout << "CBase operator = (const CBase&)" << std::endl;
return *this;
}
virtual ~CBase() {
std::cout << "~CBase()" << std::endl;
}
private:
std::string m_str;
};
class CDerived : public CBase {
// 子类构造时,若未显式调用基类构造函数,系统会尝试首先隐式调用基类默认构造函数,若基类不存在默认构造函数,则出错。
// 子类析构时,总是在最后隐式调用基类析构函数。
public:
CDerived() : m_num(0) {
// 首先自动调用CBase()。
std::cout << "CDerived()" << std::endl;
}
CDerived(int num) : m_num(num) {
// 首先自动调用CBase()。
std::cout << "CDerived(int)" << std::endl;
}
CDerived(const std::string& str) : CBase(str), m_num(0) {
// 需要显式调用基类相应构造函数。
std::cout << "CDerived(const std::string&)" << std::endl;
}
CDerived(const std::string& str, int num) : CBase(str), m_num(num) {
// 需要显式调用基类相应构造函数。
std::cout << "CDerived(const std::string&, int)" << std::endl;
}
CDerived(const CDerived& right) : CBase(right), m_num(right.m_num) {
// 需要显式调用基类拷贝构造函数。
std::cout << "CDerived(const CDerived&)" << std::endl;
}
const CDerived& operator = (const CDerived& right) {
// 处理自我赋值。
if (&right == this) {
std::cout << "Copy self" << std::endl;
return *this;
}
CBase::operator = (right); // 基类部分赋值,显式调用。
m_num = right.m_num; // 派生部分赋值。
std::cout << "CDerived operator = (const CBase&)" << std::endl;
return *this;
}
~CDerived() {
std::cout << "~CDerived()" << std::endl;
// 最后自动调用~CBase()。
}
private:
int m_num;
};
void run() {
CDerived derive(std::string("giggle"), 2333);
}
}
// 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
namespace term06 {
// 为了驳回编译器暗自提供的机能,可将相应的成员函数声明为private并且不予实现。
class A {
private:
A(const A&);
A& operator = (const A&);
};
// 或者:
class B {
public:
B() = default;
B(const B&) = delete;
B& operator=(const B&) = delete;
};
}
// 条款07:为多态基类声明virtual析构函数
namespace term07 {
// 当派生对象经由一个基类指针删除时,若基类为non-virtual析构函数,那么派生部分通常无法被销毁。
// 任何class只要带有virtual函数几乎都确定应有一个virtual析构函数,如果class不含virtual函数,
// 通常表示他并不意图被作为一个base class,当然也不应该有virtual析构函数,一个理由是vfptr会增加空间占用。
// 所有的标准STL容器都没有virtual析构函数,别把它当作base class。
// 析构函数运作方式是最深层派生的class的析构函数最先被调用,而后是其每个base class的析构函数被依次调用,所以纯虚析构函数需要被定义。
}
// 条款08:别让异常逃离析构函数
namespace term08 {
// 析构函数绝对不要吐出异常,如果析构函数调用的函数吐出异常,那么析构函数应该捕获任何异常,然后吞下他们或者结束程序。
// 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而不是在析构函数中)执行该操作。
// C++ 11默认析构函数是noexcept(true)的,故不允许抛出异常(抛出异常时abort()掉从而阻止异常传播而导致不明确的行为)。
// 当显式说明析构函数是noexcept(false)或者成员变量或者基类的析构函数是noexcept(false)时,
// 允许抛出;delete默认是noexcept(true)的,同样其操作对象指明为noexcept(false)析构则可以抛出异常。
class A {
public:
~A() {
throw "~A";
}
};
class B {
public:
~B() noexcept(false) {
throw "~B";
}
};
class C {
B b;
};
class D : public B {};
void function() {
A a;
}
void run() {
try {
B* b = new B;
delete b;
}
catch (const char* e) {
std::cout << e << std::endl;
}
try {
C* c = new C;
delete c;
}
catch (const char* e) {
std::cout << e << std::endl;
}
try {
D* d = new D;
delete d;
}
catch (const char* e) {
std::cout << e << std::endl;
}
try {
function();
}
catch (const char* e) {
std::cout << e << std::endl;
}
}
// 输出为~B、~B、~B、然后被终止。
}
// 条款09:绝不再构造和析构过程中调用virtual函数
namespace term09 {
// 在构造或者析构期间不要调用virtual函数(间接调用也不行),因为这类调用从不下降至derived class。
class Base {
public:
Base() { logName(); }
virtual void logName() const {
std::cout << "Base" << std::endl;
}
};
class Derive : public Base {
public:
virtual void logName() const {
std::cout << "Derive" << std::endl;
}
};
void run() {
Derive derive;
}
// 输出“Base”
// derive class构造时,首先执行base class构造函数,这期间对象只会被看作base class而去调用base class中的函数。
// 如果此期间将virtual函数下降到derived class,要知道此时的derived class成员变量尚未初始化,
// 而derived class的函数几乎必定取用那些成员变量,故这是不被允许的。
// 析构函数中类似(base class析构函数执行时,derived class成员已处于未定义状态)。
}
// 条款10:令operator= 返回一个reference to *this
namespace term10 {
// 只是一个协议,并无强制性,连续使用=时比较方便。
}
// 条款11:在operator= 中处理“自我赋值”
namespace term11 {
// 确定任何函数如果操作一个以上的对象,而其中多个对象是同一对象时,其行为仍然正确。
class A {};
class B {
public:
// right和*this为同一对象时出错
// const B& operator= (const B& right) {
// delete a;
// a = new A(*right.a);
// return *this;
// }
// 加入证同测试
const B& operator= (const B& right) {
if (this == &right) {
return *this;
}
delete a;
a = new A(*right.a);
return *this;
}
// 由于new/delete可能发生异常,那么可以在异常安全性处理中顺便解决自我赋值。
void swap(B& right) {
// 交换*this与right的数据,详见条款29
}
B& operator= (B right) { // 利用值传递复制一份right
swap(right);
return *this;
}
private:
A* a;
};
}
// 条款12:复制对象时勿忘其每一部分
namespace term12 {
// 详见条款05的例子。
// 如果拷贝构造函数与拷贝赋值操作符有着相同代码,可以通过private一个init函数消除重复。
}
// 条款13:以对象管理资源
namespace term13 {
// 获得资源后立刻放进管理对象内;管理对象运用析构函数确保资源被释放。
// 两个例子是std::auto_ptr和std::tr1::shared_ptr,但是注意auto_ptr在拷贝构造和拷贝
// 赋值时,原指针变为NULL,所得指针获得唯一使用权。两者都在其析构函数中做delete而不是delete[],
// 所以std::auto_ptr<std::string> aps(new std::string[10])是错误的。
}
// 条款14:在资源管理类中小心Copying行为
namespace term14 {
// 一些不适合智能指针管理的资源需要自己编写资源管理类,此时需要考虑管理类拷贝时该如何操作。
// 1.禁止拷贝(类似unique_ptr)
// 2.引用计数(类似shared_ptr)
// 3.复制资源(类似std容器)
// 4.转移资源(类似auto_ptr)
}
// 条款15:在资源管理类中提供对原始资源的访问
namespace term15 {
// 有些时候必须拿到底层资源,所以需要提供对原始资源的访问,比如使用一些旧的API。
}
// 条款16:成对使用new和delete时要采用相同的格式
namespace term16 {
// 最好不要对数组进行typedef动作。
}
// 条款17:以独立语句将newed对象置入智能指针
namespace term17 {
void function(std::shared_ptr<int> sptr, int number) {}
int getNumber() {
return 2333;
}
void run() {
// 多参数的准备次序不确定,若先调用new int(5),再调用getNumber(),
// 最后调用std::shared_ptr的构造函数,假设getNumber()产生异常,那么newed资源将会泄漏。
function(std::shared_ptr<int>(new int(5)), getNumber());
//正确的使用:
auto sptr = std::shared_ptr<int>(new int(100));
function(sptr, getNumber());
}
}
// 条款18:让接口容易被正确使用,不易被误用
namespace term18 {
// 需要考虑到用户可能犯的错误并加以阻止。
class Date {
public:
Date(int month, int day, int year) {}
};
// 上述构造函数容易搞混各个参数的顺序,下述方式可避免。
struct Day {
explicit Day(int value) : day(value) {}
int day;
};
struct Month {
explicit Month(int value) : month(value) {}
int month;
};
struct Year {
explicit Year(int value) : year(value) {}
int year;
};
class DateEx {
public:
DateEx(const Month& month, const Day& day, const Year& year) {}
};
void run() {
auto date = DateEx(Month(12), Day(21), Year(2002));
}
// 更好的方法,enum class不能隐式转换为int了。
enum class MonthEx : int {
Jar = 1,
Feb = 2,
// ...
Dec = 12
};
// 再比如你想让用户避免资源泄漏而使用智能指针那么应该直接使用下面第二种。
Date* newDate1(){
return new Date(1, 2, 3);
}
std::shared_ptr<Date> newDate2(){
return std::shared_ptr<Date>(new Date(1, 2, 3));
}
}
// 条款19:设计class犹如设计type
namespace term19 {
/*
需要考虑的:
1.新type的对象应该如何被创建和销毁
2.对象的初始化和对象的赋值该有什么样的差别
3.新type的对象如果被passed by value,意味着什么
4.什么是新type的合法值
5.你的新type需要配合某个继承图系吗
6.你的新type需要什么样的转换
7.什么样的操作符和函数对此新type而言是合理的
8.什么样的标准函数该被驳回
9.谁该取用新type的成员
10.什么是新type的未声明接口
11.你的新type有多么一般化
12.你真的需要一个新type吗
*/
}
// 条款20:以const引用代替值传递
namespace term20 {
// 1.对象以值传递时,至少需要一次拷贝构造和一次析构函数的调用
// 2.当子类以值传递却被当作基类时,将发生错误(切割问题)
// 3.STL容器迭代器和函数对象以值传递比较恰当
}
// 条款21:必须返回对象时,别妄想返回其引用
namespace term21 {
// 1.避免引用指向stack对象,这将可能导致无定义行为。
// 2.引用指向heap对象时,考虑谁该将其删除,下属代码有此问题。
class Number {
public:
Number(int value) : value(value) {}
friend const Number& operator* (const Number& l, const Number& r);
private:
int value;
};
const Number& operator* (const Number& l, const Number& r) {
auto number = new Number(l.value * r.value);
return *number;
}
void run() {
Number num1(1);
Number num2(2);
Number num3(3);
// 此时将发生内存泄漏。
Number num4 = num1 * num2 * num3;
}
}
// 条款22:将成员变量声明为private
namespace term22 {
// 1.安全性
// 2.修改时的破坏性
// 3.protected并不比public封装性强
}
// 条款23:宁以non-member、non-friend代替member函数
namespace term23 {
// 更大的封装度:如果以访问到私有成员的函数数量来评价封装性。
// 更大的包裹弹性:将同一对象关于不同功能的non-member、non-friend函数写到不同文件的同一命名空间中。
}
// 条款24:若所有的参数均需要类型转换,请为此采用non-member函数
namespace term24 {
class Number {
public:
Number(int value) : value(value) {}
int getValue()const {
return this->value;
}
const Number operator*(const Number& right)const {
return Number(this->value * right.value);
}
private:
int value;
};
// 推荐的方式:
// const Number operator*(const Number& left, const Number& right) {
// return Number(left.getValue() * right.getValue());
// }
void run() {
Number num1(1);
// 此时由于Number构造函数为noexplicit的,所以2隐式转化为Number。
Number num2 = num1 * 2;
// 错误,使用non-member函数之后正确。
// Number num3 = 2 * num1;
}
// 考虑operator*是否应该成为friend函数,答案是否定的。
}
// 条款25:考虑写出一个不抛出异常的swap函数
namespace term25 {
// 典型的std::swap实现缺乏效率。
// namespace mystd {
// template<typename T>
// void swap(T& a, T& b) {
// T temp(a);
// a = b;
// b = temp;
// }
// }
// pointer to implementation方法提高swap效率。
class WidgetImpl {
public:
// ...
private:
int a;
int b;
int c;
// ...
std::vector<int> v; // 表示有很多的数据。
};
// class Widget {
// public:
// Widget() : pImpl(nullptr) {}
// Widget(const Widget& right) = default;
// Widget& operator= (const Widget& right) {
// *pImpl = *(right.pImpl);
// }
// private:
// WidgetImpl* pImpl;
// };
// 典型的std::swap因为涉及赋值构造和拷贝构造,所以可能产生异常并且不会单单的去交换指针。
// 解决方法是对std::swap特化(此处使用mystd代替std), 但是由于pImpl为私有,所以无法通过编译。
// namespace mystd {
// template<>
// void swap<Widget>(Widget& a, Widget& b) {
// std::swap(a.pImpl, b.pImpl);
// }
// }
// 解决方法为声明为friend函数,但不如为对象定义公有的swap函数。
class Widget {
public:
Widget() : pImpl(nullptr) {}
Widget(const Widget& right) = default;
Widget& operator= (const Widget& right) {
*pImpl = *(right.pImpl);
return *this;
}
void swap(Widget& right) {
std::swap(this->pImpl, right.pImpl);
}
private:
WidgetImpl* pImpl;
};
// 而后对std::swap进行特化。
// namespace mystd {
// template<typename T>
// void swap(T& a, T& b) {}
// template<>
// void swap<Widget>(Widget& a, Widget& b) {
// a.swap(b);
// }
// }
// 上述情况为classes情况,如果对于classes template呢?
template<typename T>
class TWidgetImpl {
// ...
};
template<typename T>
class TWidget {
public:
void swap(TWidget<T>& right) {
// 此方法中由于一般对内置类型进行操作,故不会发生异常。
}
// ...
};
// 此时尝试特化std::swap时将发生错误,因为C++不支持对function template偏特化。
// namespace mystd {
// template<typename T> void swap(T& a, T& b) {}
// template<typename T>
// void swap<TWidget<T> >(Widget<T>& a, Widget<T>& b) {
// a.swap(b);
// }
// }
// 解决方法为在std中重载一个swap函数,但是不应该在std中添加新的东西,所以应该声明在自身的命名空间之中。
template<typename T>
void swap(TWidget<T>& a, TWidget<T>& b) {
a.swap(b);
}
// 此时调用swap时因该调用自己的还是std的呢?考虑下述写法让编译器调用合适的函数。
// using std::swap;
// swap(a, b);
void run() {
TWidget<std::string> a;
TWidget<std::string> b;
using std::swap;
swap(a, b);
}
}
// 条款26:尽可能的延后变量定义式的出现时间
namespace term26 {
// 定义变量直到使用它时甚至到能够给它赋初始值时。
}
// 条款27:尽量少做转型动作
namespace term27 {
/*
1.const_cast: 将对象的常量性移除
2.dynamic_cast: 安全向下转型
3.reinterpret_case: 低级转型
4.static_cast: 强制转型
*/
// 错误示范:
class Window {
public:
Window() : size(0) {}
virtual void resize() {
size = 100;
}
unsigned int size;
};
class WindowEx : public Window {
public:
virtual void resize() {
// 此时产生了*this的Base Class部分的副本并在其上修改。
static_cast<Window>(*this).resize();
// ...
}
};
// 正确方法:
class WindowExEx : public Window {
public:
virtual void resize() {
Window::resize();
// ...
}
void special() {
std::cout << "This is special function in WindowExEx." << std::endl;
}
};
void run() {
double a = 1.;
int b = static_cast<int>(a);
std::cout << "b = " << b << std::endl;
const std::string str("11111");
const_cast<std::string&>(str).append("22222");
std::cout << str << std::endl;
WindowEx w1;
w1.resize();
std::cout << w1.size << std::endl;
WindowExEx w2;
w2.resize();
std::cout << w2.size << std::endl;
// dynamic_cast可能效率很低,尽量别用。
Window* w3 = new WindowExEx;
dynamic_cast<WindowExEx*>(w3)->special();
delete w3;
// 解决上述问题的好方法是在Base Class中添加virtual函数。
// reinterpret_cast顾名思义为重新解释的转型
int num = 0X00616263;
int* pInt = #
char* pStr = reinterpret_cast<char*>(pInt);
std::cout << "value of pInt is: " << pInt << std::endl;
std::cout << "value of pStr is: " << static_cast<void*>(pStr) << std::endl;
std::cout << std::hex << *pInt << std::endl;
std::cout << pStr << std::endl;
}
}
// 条款28:避免返回handles指向对象内部成分
namespace term28 {
// 这里的handles指指向对象内部私有成员的指针或者引用或者迭代器。
// 首先返回handles显然可能会更改对象的私有成分,这是不允许的。
// 当然我们可以返回const引用,但此时出现另一个问题,即handles的寿命长于对象本身。
class Point {
public:
Point(int x, int y) : x(x), y(y) {
std::cout << "Point(int, int)" <<std::endl;
}
~Point() {
std::cout << "~Point()" << std::endl;
}
Point(const Point& left) : x(left.x), y(left.y) {
std::cout << "Point(const Point&)" << std::endl;
}
void show()const {
std::cout << "x: " << x << " " << "y: " << y << std::endl;
}
private:
int x;
int y;
};
class Line {
public:
Line(const Point& _p1, const Point& _p2) : p1(_p1), p2(_p2) {
std::cout << "Line(const Point&, const Point&)" << std::endl;
}
~Line() {
std::cout << "~Line()" << std::endl;
}
const Point& getPoint1()const {
return p1;
}
private:
Point p1;
Point p2;
};
void run() {
const Point* pPoint1 = &Line(Point(1, 1), Point(9, 9)).getPoint1();
// 此时所有的Point都已经析构,指针悬空,觉得这跟返回局部变量的引用是一回事,但是此处编译器未发出警告。
}
}
// 条款29:为异常安全而努力是值得的
namespace term29 {
class Image {
public:
Image(const std::string& imgName) {}
};
// 考虑下述窗口类:
class Window {
public:
Window() : mtx(), pImg(nullptr), imgChanges(0) {}
// 1.若new产生异常,那么mtx将不会解锁
// 2.若new产生异常,那么imgChanges改变,pImg被删除,图像却未改变
void changeImg1(const std::string& imgName) {
mtx.lock();
if(pImg) {
delete pImg;
}
imgChanges++;
pImg = new Image(imgName);
mtx.unlock();
}
// 解决问题1:
void changeImg2(const std::string& imgName) {
std::lock_guard<std::mutex> lock(mtx);
if(pImg) {
delete pImg;
}
imgChanges++;
pImg = new Image(imgName);
}
private:
std::mutex mtx;
Image* pImg;
unsigned int imgChanges;
};
/*
异常安全函数需要满足下述三个条件之一:
1.基本承诺:如果异常被抛出,程序仍保持在有效状态
2.强烈保证:如果异常被抛出,程序状态不改变
3.不抛掷承诺:绝不抛出异常
*/
// 为达到强烈保证可将Image*换为std::shared_ptr<Image>并调整语序为:
// std::lock_guard<std::mutex> lock(mtx);
// image.reset(new Image(imgName));
// imgChanges++;
// 最一般的策略为copy and swap,但此处不便。
void run() {
Window win;
win.changeImg1("image001.png");
win.changeImg2("image001.png");
}
}
// 条款30:透彻了解inlining的里里外外
namespace term30 {
/*
inline函数背后的整体观念是:对此函数的每一个调用都以函数本体代替之。
inline函数的优点:避免了函数调用的开销;不含函数调用的代码易被编译器最优化。
inline函数的缺点:代码膨胀;inline函数若被修改,所有的相关单元都需要重新编译。
inline只是对编译器的申请,不是强制命令;隐喻申请:定义于类内;显式申请:inline关键字。
inline行为通常发生在编译期,故inline函数通常定义于头文件;function template不一定为inline函数。
virtual函数一般不会inline,因为他在运行期才会确定调用哪个函数;通过函数指针调用的函数一般也不会inline。
*/
}
// 条款31:将文件间的编译依存关系降至最低
namespace term31 {
// 这一部分单独写了一篇文章,还需要慢慢理解。
}
// 条款32:确定你的public继承塑模出is-a关系
namespace term32 {
// public继承意味着适用于base class身上的每一件事情也一定适用于derived class身上,因为每一个derived class对象也都是一个base class对象。
}
// 条款33:比免遮掩继承而来的名称
namespace term33 {
// derived class内的名称将遮掩base class内的名称(同名且只与名称有关,与参数,类型,是否virtual等都无关)。
// 如果被遮掩,那么将违背public继承的is-a关系,可使用using声明式使之重见天日。
// 作者文中的例子可以理解但是实在得不到什么有用的启发。
}
// 条款34:区分接口继承和实现继承
namespace term34 {
// public继承下,derived class总是继承base class的接口。
// pure virtual函数的目的是让derived class只继承接口,而且它可以有自己的实现,调用方法为Base::function()。
// impure virtual函数的目的是让derived class继承接口以及一份缺省实现。
// non-virtual函数的目的是让derived class继承接口以及一份强制性实现。
}
// 条款35:考虑virtual函数以外的其他选择
namespace term35 {
// 假如你在设计一个游戏角色类继承体系,且需要计算角色的生命值,考虑下面的类:
class People {
public:
virtual ~People() = default;
virtual int calHealth() const {
// ...
return 0;
}
};
// 这似乎是不假思索就写出来的而且是正确的,但是考虑一下替代方法,有可能获得更大的灵活性。
// 1.藉由non-virtual interface手法实现Template Method模式。
class People1 {
public:
virtual ~People1() = default;
int calHealth() const {
// 这种的优点是在此处可添加一些准备或者清理工作。
// 准备工作
int value = doCalHealth();
// 清理工作
return value;
}
private:
virtual int doCalHealth() const {
// ...
// derived class可重写此方法。
return 0;
}
};
// 2.藉由function pointer实现Strategy模式。
// 如果人物的健康值计算由public信息就可以搞定,那么就可以将计算函数从继承体系中剥离出来。
// 这种方法的优势是同一类的不同实体可以设置不同的计算函数,且不同时期也可以更换计算函数,更加灵活。
class People2;
int defaultCalHealth(const People2& people) {
// ...
return 0;
}
class People2 {
public:
typedef int(*calHealthFunc)(const People2& people);
virtual ~People2() = default;
explicit People2(calHealthFunc _calHealthFunc = defaultCalHealth) : func(_calHealthFunc) {}
int calHealth() const {
return func(*this);
}
private:
calHealthFunc func;
};
// 疑惑是为什么不直接使用下述写法呢?何必非要将函数指针置于类内呢?
// People2 people;
// defaultCalHealth(people);
// 我想可能的原因是类的其他成员函数需要调用calHealth但是无法提供calHealthFunc参数。
// 3.藉由std::function完成Strategy模式。
// 这种方法本质跟第二种一样,但是std::function提供了更强的兼容性(它可以绑定函数指针,仿函数,成员函数等)。
class People3;
int defaultCalHealth1(const People3& people) {
// ...
return 0;
}
class People3 {
public:
typedef std::function<int (const People3& people)> calHealthFunc;
virtual ~People3() = default;
explicit People3(calHealthFunc _calHealthFunc = defaultCalHealth1) : func(_calHealthFunc) {}
int calHealth() const {
return func(*this);
}
private:
calHealthFunc func;
};
// 4.古典的Strategy模式。
// 古典的Srtategy模式将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。
class People4;
class CalHealthFunc {
public:
virtual int cal(const People4& people) const {
// ...
return 0;
}
};
CalHealthFunc defaultCalHealth2;
class People4 {
public:
virtual ~People4() = default;
People4(CalHealthFunc* _pFunc = &defaultCalHealth2) : pFunc(_pFunc) {}
int calHealth() const {
return pFunc->cal(*this);
}
private:
CalHealthFunc* pFunc;
};
void run() {
People2 people2;
people2.calHealth();
People3 people3;
people3.calHealth();
People4 people4;
people4.calHealth();
}
}
// 条款36:绝不重新定义继承而来的non-virtual函数
namespace term36 {
class Base {
public:
virtual ~Base() = default;
void function() {
std::cout << "base function" << std::endl;
}
};
class Derived : public Base {
public:
void function() {
std::cout << "derived function" << std::endl;
}
};
void run() {
Derived d;
Base* pb = &d;
Derived* pd = &d;
pb->function();
pd->function();
// 同一个对象却调用了不同的函数,这是non-virtual函数不能够动态绑定导致的。
// 而且上述做法违背了public继承的is-a关系。
}
}
// 条款37:绝不重新定义继承而来的缺省参数值
namespace term37 {
class Shape {
public:
enum class ShapeColor: int {
Red = 0,
Blue = 1,
Green = 2,
};
virtual ~Shape() {}
virtual void draw(ShapeColor color = ShapeColor::Red) const {
std::cout << "Shape draw, color = " << static_cast<int>(color) << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw(ShapeColor color = ShapeColor::Blue) const {
std::cout << "Rectangle draw, color = " << static_cast<int>(color) << std::endl;
}
};
class Person {
public:
virtual ~Person() {}
void run(unsigned int distance = 100u) const {
doRun(distance);
}
private:
virtual void doRun(unsigned int distance) const {
std::cout << "Person run, distance = " << distance << std::endl;
}
};
class Student : public Person {
private:
virtual void doRun(unsigned int distance) const {
std::cout << "Student run, distance = " << distance << std::endl;
}
};
void run() {
Shape* rectangle = new Rectangle; // 静态类型Shape*,动态类型Rectangle*。
// 缺省参数值为静态绑定(C++为了效率和编译器易处理而作出的取舍),而virtual函数为动态绑定。
rectangle->draw();
delete rectangle;
// 输出为:Rectangle draw, color = 0,也就是调用了子类函数却使用了基类缺省参数值。
// 解决方法可以为不改写子类的缺省参数值,但是带来了代码重复问题,考虑virtual函数的替代选择(条款35)。
Person* student = new Student;
student->run();
delete student;
}
}
// 条款38:通过复合塑模出has-a或者“根据某物实现出”
namespace term38 {
// 复合和public继承完全不同,复合有两种关系:has-a或者“根据某物实现出”
class Date {};
class Address {};
class Person {
private:
Date birthDate;
Address address;
};
// 这种为has-a关系
template<typename T>
class Set {
public:
std::size_t size() const;
void insert(const T& item);
// ...
private:
std::list<T> rep;
};
// 这种为根据某物实现出关系。
}
// 条款39:明智而审慎的使用private继承
namespace term39 {
// 首先看一下private继承的行为。
class Person {
public:
virtual ~Person() {}
virtual void eat() {}
};
class Student : private Person {};
void eat(const Person& person) {}
void test1() {
Person person;
person.eat();
Student student;
// student.eat(); // 错误:eat函数不可访问。
eat(person);
// eat(student); // 错误:编译器不会自动将Student对象转为Person对象。
eat((const Person&)student); // 但是可以手动转型。
}
// private继承意味着“根据某物实现出”如果你使用private继承,你一定是为了使用Base中已经备妥的某些特性,而不存在任何观念上的东西。
// 而且private继承意味着只有实现被继承,接口部分应该略去(因为基类的接口在子类中为private,也就不再是接口了)。
// private继承和复合都有“根据某物实现出”的关系,那么该如何选择呢?
// 作者建议尽量使用复合除非1:需要访问Base的protected成员;2.需要重写Base的virtual函数。
// 举一个勉强的例子。
class Actives {
public:
virtual void eat() const {}
virtual void run() const {}
};
class People : private Actives {
private:
virtual void eat() const {
std::cout << "People eat" << std::endl;
}
virtual void run() const {
std::cout << "People run" << std::endl;
}
};
// 上述例子是为了让People重写Actives的virtual函数,当然还有其他的实现方法。
class Male {
private:
class MaleActives : public Actives {
public:
virtual void eat() const {
std::cout << "Male eat" << std::endl;
}
virtual void run() const {
std::cout << "Male run" << std::endl;
}
};
MaleActives actives;
};
// 上述例子使用复合方式实现,只是稍微有一些复杂。
// 此外,private继承可以实现empty base空间最优化。
class Empty {};
class Int1 : private Empty {
private:
int value;
};
class Int2{
private:
Empty empty;
int value;
};
void run() {
test1();
std::cout << sizeof(Int1) << std::endl; // 4
std::cout << sizeof(Int2) << std::endl; // 8
}
/*
复习以下三种继承中访问权限的问题:
公有继承 保护继承 私有继承
公有成员 公有 保护 私有
保护成员 保护 保护 私有
私有成员 不可访问 不可访问 不可访问
使用何种继承影响的是外界对成员的访问权限。
子类对基类成员的访问权限与何种继承无关,总是不可访问private成员,总是可以访问protected和public成员。
*/
}
// 条款40:明智而审慎的使用多重继承
namespace term40 {
// 首先因该避免从多个base class中继承引发歧义的同名函数。
// 考虑钻石性继承体系:
class File {
public:
std::string name;
};
class InputFile1 : public File {};
class OutputFile1 : public File {};
class IOFile1 : public InputFile1, public OutputFile1 {};
// 那么IOFile应该含有几个name呢?默认是两个。
class InputFile2 : virtual public File {};
class OutputFile2 : virtual public File {};
class IOFile2 : public InputFile2, public OutputFile2 {};
// 使用虚继承可以保留一份,但是虚继承有着体积变大,速度变慢等众多开销。
// 作者对于虚继承有两点建议:1.非必要不使用虚继承,2.避免在virtual base class中放置任何数据。
// 多重继承的一个应用场景是:从一个类继承接口(public),从另一个类继承实现(private)。
void run() {
std::cout << sizeof(IOFile1) << std::endl;
std::cout << sizeof(IOFile2) << std::endl;
}
/*
虚继承的实现原理:
虚继承的实现是编译器相关的,可以通过虚基表和虚基指针实现。
虚基表中存放着虚基类和本类的偏移地址从而可以拿到虚基类成员,而虚基指针指向虚基表,占用一个指针的存储空间。
当虚继承后的类再被继承时,虚基指针也会被继承。
*/
}
// 条款41:了解隐式接口和编译期多态
namespace term41 {
// 面向对象编程的世界总是以显式接口和运行期多态(考虑哪个virtual函数该被绑定)解决问题。
// 模板及泛型编程的世界更加强调隐式接口和编译期多态(考虑哪个重载函数该被调用)。
}
// 条款42:了解typename的双重意义
namespace term42 {
template<typename T> class Widget1 {};
template<class T> class Widget2 {};
// 在template声明式中,class与typename没有任何区别,但是其他地方typename有着不可替代的作用。
// 在template内部,如果一个名称依赖于某个模板参数,称之为从属名称,如果从属名称在class内呈现嵌套状,称之为从属嵌套名称。
// 对于从属嵌套名称,编译器默认假设它不是类型,除非明确告知它,这就是tepyname的作用。
template<typename T>
void function(const T& t) {
// T::const_iterator iter = t.begin();
typename T::const_iterator iter = t.begin();
}
// 涉及从属嵌套名称时,两个例外不能使用typename,1是不能用于base class list内的从属嵌套名称之前,2是不能用于初始化列表中的base class的修饰符。
}
// 条款43:学习处理模板化基类内的名称
namespace term43 {
class Company1 {
public:
void clearSend(const std::string& msg) {}
void safeSend(const std::string& msg) {}
};
class Company2 {
public:
void safeSend(const std::string& msg) {}
};
template<typename Company>
class MsgSender {
public:
void clearSend(const std::string& msg) {
Company().clearSend(msg);
}
};
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
// using MsgSender<Company>::clearSend;
// 避免与MsgSender函数重名而造成名称遮掩。
void clearSendMsg(const std::string& msg) {
// logging...
// clearSend(msg); // 编译错误,由于基类依赖于模板参数Company,编译器无法知道基类是否含有clearSend函数,方法1,配合使用上面注释掉的using声明。
// this->clearSend(msg); // 方法2。
MsgSender<Company>::clearSend(msg); // 方法3,但是作者不推荐,说是“如果调用的是virtual函数,那么明确资格修饰会关闭virtual绑定行为”。
// logging...
};
};
void run() {
LoggingMsgSender<Company1> sender1;
sender1.clearSendMsg("msg");
LoggingMsgSender<Company2> sender2;
// sender2.clearSendMsg("msg"); // 错误,Company2没有clearSend函数。
}
}
// 条款44:将与参数无关的代码抽离templates
namespace term44 {
// template减少代码重复,但编译结束后,程序中可能因为不同参数具现化出很多函数或者数据,这是比较隐晦的事情。
// 你需要找出这些可能重复的地方,将其抽离templates从而避免二进制膨胀。
template<typename T, std::size_t size>
class SquareMatrix {
public:
void invert() {
// ...
}
};
void test1() {
SquareMatrix<int, 10> m1;
m1.invert();
SquareMatrix<int, 20> m2;
m2.invert();
// 上述代码中会具现化出两份代码,且只有size不同,自然而然想到下述方法:
}
// 首先将size作为参数传入invert函数。
template <typename T>
class SquareMatixBase {
protected:
void invert(std::size_t size) {
// ...
}
};
template<typename T, std::size_t size>
class SquareMatrixDerived : private SquareMatixBase<T> {
private:
using SquareMatixBase<T>::invert; // 避免函数名称遮掩。
public:
void invert() {
this->invert(size);
}
};
void test2() {
SquareMatrixDerived<int, 10> m1;
m1.invert();
SquareMatrixDerived<int, 20> m2;
m2.invert();
// 此时,共享Base中的那一个invert函数。
}
// 上述例子还存在许多问题,暂不讨论,只是为了阐述要将参数无关的代码抽离。
// 上面讨论的只是非类型参数的二进制膨胀问题,当然类型参数也会导致二进制膨胀。
// 比如许多平台int和long有着相同的二进制表述,那么vector<int>和vector<long>就有着可能相同的成员函数,又比如所有指针有着相同的二进制表述等。
void run() {
test1();
test2();
}
// 本章读着挺懂,但是实在是没有现实中的经验来支撑自己的理解。
}
// 条款45:运用成员函数模板接受所有兼容类型
namespace term45 {
class Base {};
class Derived : public Base {};
// 假如我们想自己写一个智能指针,如下:
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T* realPtr) {}
// ...
};
// SmartPtr<Base> pBase = SmartPtr<Derived>(new Derived); // 错误。
// base class与derived class分别具现一个template,产生的具现体并没有继承关系。
// 我们应该为SmartPtr<Base>写一个适合于SmartPtr<Deriveds>的拷贝构造函数。
template<typename T>
class SmartPointer {
public:
explicit SmartPointer(T* _realPtr) : realPtr(_realPtr) {}
template<typename U>
SmartPointer(const SmartPointer<U>& other) : realPtr(other.get()) {}
T* get() const {
return realPtr;
}
private:
T* realPtr;
};
// 我们发现以上的解法提供的泛化超出了我们的需要,即U并不需要是T的派生类,但是,
// 通过上述解法,如果U*不能隐式转化为T*,那么将无法通过编译,事实上达到了我们的目的。
void run() {
// SmartPointer<Derived> pDerived = SmartPointer<Base>(new Base); // 错误。
SmartPointer<Base> pBase = SmartPointer<Derived>(new Derived);
}
// 不只构造函数可以泛化,其他成员函数照样可以泛化,比如赋值运算符等等,详细例子可以查看std::shared_ptr源代码。
// 声明一个泛化拷贝构造函数并不会阻止编译器生成一个常规的拷贝构造函数,如果你想控制拷贝的方方面面,请同时实现它,同样的规则适用于赋值操作符。
}
// 条款46:需要类型转换时请为模板定义非成员函数
namespace term46 {
template<typename T>
class Number {
public:
Number(T _value) : value(_value) {}
const T getValue() const {
return value;
}
private:
T value;
};
// 至于为什么要定义非成员函数请看条款26。
template<typename T>
const Number<T> operator*(const Number<T>& t1, const Number<T>& t2) {
return Number<T>(t1.getValue() * t2.getValue());
}
// Number<int> number = Number<int>(5) * 2; // 无法通过编译。
// 此处的2不能隐式转换为Nnumber(2)的原因是,在function template推导过程中,不会考虑“通过构造函数发生的隐式类型转换”。
// 这里我们要明白一个事情就是class template不会依赖模板参数推导,那么我们就利用这一点,将operator*声明为friend函数。
template<typename T>
class Num {
public:
Num(T _value) : value(_value) {}
const T getValue() const {
return value;
}
// friend const Num<T> operator*(const Num<T>& num1, const Num<T>& num2) {
// return Num<T>(num1.getValue() * num2.getValue());
// }
// 推荐这种省略的写法。
friend const Num operator*(const Num& num1, const Num& num2) {
return Num(num1.getValue() * num2.getValue());
}
private:
T value;
};
// 尝试在类外定义此函数将会导致链接错误,具体原因自己还不是很清楚。
// 类内定义意味着inline,如果代码很长,可以再次调用外部函数。
// template<typename T>
// const Num<T> operator*(const Num<T>& num1, const Num<T>& num2) {
// return Num<T>(num1.getValue() * num2.getValue());
// }
void run() {
// 一旦对象被定义,类便具象化,那么类中那个friend函数也会具现化。
Num<int> num = Num<int>(5) * 2;
num = 3 *4;
std::cout << num.getValue() << std::endl;
}
}
// 条款47:请使用traits classes表现类型信息
namespace term47{
/*
首先复习迭代器的分类:
Input迭代器:一次一步,只读且只能读一次,代表:istream_iterator。
Output迭代器:一次一步,只写且只能写一次,代表:ostream_iterator。
Forward迭代器:读或者写所指物多次。
Bidirectional迭代器:比前者增加了向后移动,例如std::set和std::map的迭代器。
Random access迭代器:比前者增加了“迭代器算术”,例如std::vector和std::string的迭代器。
对于这五种迭代器,标准库分别提供专属的卷标结构加以区分,如下,它们都是明显且有效的is-a关系。
*/
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 {};
// 现在该如何区分迭代器的类型呢?考虑如下做法。
template<typename IterT>
class list {
public:
class iterator {
public:
// 在迭代器内部使用卷标定义迭代器类型。
typedef bidirectional_iterator_tag iterator_type;
};
};
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_type iterator_type;
};
// 若把指针当作迭代器,由于无法在指针内部定义东西,可以偏特化struct traits。
template<typename IterT>
struct iterator_traits<IterT*> {
typedef random_access_iterator_tag iterator_type;
};
// 我们想要在编译期区分迭代器类型,就不能使用if来做,联系函数重载这个东西,正好是编译期且与类型相关。
template<typename IterT>
void doPrintIterType(const IterT& iter, input_iterator_tag) {
std::cout << "input_iterator" << std::endl;
}
template<typename IterT>
void doPrintIterType(const IterT& iter, bidirectional_iterator_tag) {
std::cout << "bidirectional_iterator" << std::endl;
}
template<typename IterT>
void doPrintIterType(const IterT& iter, random_access_iterator_tag) {
std::cout << "random_access_iterator" << std::endl;
}
template<typename IterT>
void printIterType(const IterT& iter) {
doPrintIterType(iter, typename iterator_traits<IterT>::iterator_type {});
}
// 这部分感觉和实现函数模板偏特化的方法之一:标签转发类似。
void run() {
printIterType((char*)nullptr);
printIterType(list<int>::iterator());
}
}
// 条款48:认识template元编程
namespace term48 {
// 模板元编程(template metaprogramming TMP)。
// TMP可以使得某些困难的事情得以实现。
// TMP可以将工作从运行期转移到编译期(更早的发现问题;更小的二进制代码,更快的执行速度)。
// 条款47中的traits实现技术就属于TMP。
template<unsigned int n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> {
enum {value = 1u };
};
// 上述例子就是TMP计算阶乘的简单例子,涉及模板递归具现化。
void run() {
std::cout << Factorial<10>::value <<std::endl;
}
// 个人认为TMP非常有趣,只是接触很少,但是觉得似乎之前在opencv中见过类似东西,有机会一定好好学学。
}
// 条款49:了解new-handler的行为
namespace term49 {
// 当operator new无法满足内存分配需求时,将调用new_handler,可以使用set_new_handler设置自定义函数。
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
void nomem() {
std::cerr << "Unable to satisfy request for memory" <<std::endl;
std::abort(); // 否则将会循环调用此函数。
}
void test1() {
std::set_new_handler(nomem);
int* ptr = new int[10000000000];
}
// 上述是set_new_handler的简单使用,一个设计良好的new_handler函数应该满足以下几点。
// 1.让更多内存可被使用(可以预分配,内存不足时返还系统)。
// 2.安装另一个new_handler(当前new_handler无法完成任务,应提供其他new_handler)。
// 3.卸载new_handler(用nullptr设置new_handler,此时内存不足时立即抛出std::bad_alloc)。
// 4.抛出bad_alloc异常(异常将传播至内存索求处)。
// 5.不返回(abort或者exit)。
// 以对象管理使用set_new_handler后返回的系统new_handler资源。
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler _new_handler) : handler(_new_handler) {}
~NewHandlerHolder() {
std::set_new_handler(handler);
}
NewHandlerHolder(const NewHandlerHolder& other) = delete;
NewHandlerHolder& operator=(const NewHandlerHolder& other) = delete;
private:
std::new_handler handler;
};
template<typename T>
class NewHandlerSurpport {
public:
static std::new_handler set_new_handler(std::new_handler _new_handler);
static void* operator new(std::size_t size);
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSurpport<T>::set_new_handler(std::new_handler _new_handler) {
std::new_handler oldHandler = currentHandler;
currentHandler = _new_handler;
return oldHandler;
}
template<typename T>
void* NewHandlerSurpport<T>::operator new(std::size_t size) {
NewHandlerHolder holder(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
std::new_handler NewHandlerSurpport<T>::currentHandler = nullptr;
// 之后可以方便的为任何类设定new_handler(个人觉得大可不必,但是此实现方式可以学习)。
class Widget : public NewHandlerSurpport<Widget> {};
void run() {
// test1();
Widget::set_new_handler(nomem);
Widget* w = new Widget;
delete w;
}
}
// 条款50:了解new和delete的合理替换时机
namespace term50 {
// 首先思考为什么我们想替换系统默认的new和delete,或者何时我们应该去替换他们呢。
// 1.用来检测运用上的错误。
// 2.为了收集使用上的统计数据。
// 3.为了增加分配和归还的速度。
// 4.为了降低缺省内存管理器带来的空间额外开销。
// 5.为了将相关对象成簇聚集。
// 6.为了弥补缺省内存分配器的非最佳齐位。
// 7.为了获得非传统的行为。
// 但写出一个好的内存管理器是件难事,需要考虑移植性、齐位、线程安全等等,可参考或者使用开放源代码的优秀内存管理器。
}
// 编写new和delete时需要固守常规
namespace term51 {
// operator new内应该含有一个无穷循环,且失败时调用new_handler,并且有能力应对0内存申请。
// 重写了类的operator new后,小心派生类继承它。
// operator delete收到nulllptr时应该不做任何事。
}
// 条款52:写了 placement new也要字写placement delete
namespace term52 {
// 后续涉及此内容时再说。
}
// 条款53:不要轻忽编译器的警告
namespace term53 {
// 从不轻忽。
}
// 条款54:让自己熟悉包括TR1在内的标准程序库
namespace term54 {
// 好的。
}
// 条款55:让自己熟悉Boost
namespace term55 {
// http://boost.org
}
}
int main() {
// effective_cpp::term05::run();
// effective_cpp::term08::run();
// effective_cpp::term09::run();
// effective_cpp::term17::run();
// effective_cpp::term18::run();
// effective_cpp::term21::run();
// effective_cpp::term24::run();
// effective_cpp::term25::run();
// effective_cpp::term27::run();
// effective_cpp::term28::run();
// effective_cpp::term29::run();
// effective_cpp::term35::run();
// effective_cpp::term36::run();
// effective_cpp::term37::run();
// effective_cpp::term39::run();
// effective_cpp::term40::run();
// effective_cpp::term43::run();
// effective_cpp::term44::run();
// effective_cpp::term46::run();
// effective_cpp::term47::run();
// effective_cpp::term48::run();
// effective_cpp::term49::run();
return 0;
}
读Effective C++
于 2021-09-25 23:48:25 首次发布