1. 常用语法
1.1. cosnt
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针和指针常量;
- 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量。
示例:
// 类
class A
{
private:
const int a; // 常对象成员,只能在初始化列表赋值
public:
// 构造函数
A() { };
A(int x) : a(x) { }; // 初始化列表
// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数、更新常成员变量
const A *p = &a; // 常指针
const A &q = a; // 常引用
// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量
char* const p3 = greeting; // 常指针,指向字符数组变量
const char* const p4 = greeting; // 常指针,指向字符数组常量
}
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常指针
void function4(const int& Var); // 引用参数在函数内为常量
// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
1.2. static
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在开发项目时,为了防止与其他文件中的函数重名,可以将函数定位为 static。
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
1.3. inline
编译器在开启优化时,会自动将行数较少的函数按内联编译,会自动将标记了inline的长函数取消内联编译。所以,一般情况下,不需要特定标记某个函数为inline。
1.4. 断言
- 动态断言,assert()在运行的时候检测函数的执行结果,如果不符合预期,则报出提示。
- 静态断言,static_assert在编译器检测常量表达式的结果,如果不符合预期,则报编译错误。
1.5. sizeof
sizeof() 是一个判断数据类型或者表达式长度的运算符,在编译期间计算出变量或类型所占有的空间。成员的对齐方式影响空间的占用大小。
void Find(int arr[10])
{
int size = sizeof(arr);
}
1.6. #pragma pack(n)
设定结构体、联合以及类成员变量以 n 字节方式对齐。VS C++中,为了提高访问效果,32位程序默认以4BYTE对齐,64位程序默认以8BYTE对齐。
#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop) // 恢复对齐状态
1.7. 位域
位域是指信息在存储时,并不需要占用一个完整的位元组, 而只需占几个或一个二进制位。
struct
{
unsigned int widthValidated : 1;
unsigned int heightValidated : 1;
} status2;
1.8. volatile
C++中的volatile,除了访问硬件寄存器以外,其他情况下不需要用volatile。volatile不具备原子操作特性,不保证线程同步。原子操作需要使用std::atomic,保证同步则需要相应的同步对象。
1.9. extern “C”
C++支持重载,所以C++函数编译出的函数名会加上参数修饰。如果想让C++编译出的函数被其他C代码使用,则需要extern "C"来修饰函数,来让编译用C代码风格来编译链接函数。
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
1.10. struct & class
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
最本质的一个区别就是默认的访问控制
- 默认的继承访问权限。struct 是 public 的,class 是 private 的。
- struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
1.11. union
共用体表示几个变量共用一个内存位置,在不同的时间保存不同的数据类型和不同长度的变量。
typedef union
{
u8_t all;
struct
{
u8_t protectionInfo: 3; // bit [2:0] Protection information
u8_t protectionInfoLoc: 1; // bit [03] Protection information location
u8_t rsvd7_4: 4; // bit [7:4] Reserved
};
} data_protection_setting_t;
1.12. explicit
explicit 修饰的构造函数可用来防止隐式转换。
class Test1
{
public:
Test1(int n) // 普通构造函数
{
num=n;
}
private:
int num;
};
class Test2
{
public:
explicit Test2(int n) // explicit(显式)构造函数
{
num=n;
}
private:
int num;
};
int main()
{
Test1 t1=12; // 隐式调用其构造函数,成功
Test2 t2=12; // 编译错误,不能隐式调用其构造函数
Test2 t2(12); // 显式调用成功
return 0;
}
1.13. friend
修饰类或函数,使得其可以访问另外的类的私有成员。
一般情况下,不建议使用,会破坏类的封装性,引入新的风险。
1.14. using
using可以引用命名空间,定义别名,子引用基类成员。
不要在头文件中用using引用命名空间,源文件中可以适量使用。或者使用如下:
using std::cin;
using std::cout;
using std::endl;
定义别名:
typedef std::string (Foo::* fooMemFnPtr) (const std::string&);
using fooMemFnPtr = std::string (Foo::*) (const std::string&);
// C++11之前,只能使用typdef定义具体的模板实现的别名,使用非常鸡肋。
template <typename T>
using Vec = MyVector<T, MyAlloc<T>>;
1.15. reference
左值引用,常规引用,一般表示对象的身份。
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。右值引用可实现转移语义(Move Sementics)和完美转发(Perfect Forwarding),它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
引用折叠
- X& &、X& &&、X&& & 可折叠成 X&
- X&& && 可折叠成 X&&
完美转发就是因为引用 折叠的规则来实现的。
1.16. Macro
一般情况下不建议使用宏,尤其是用来代替字面量。但是很多情况下,宏可以大量简化编写代码量。
- 字符串操作(#),将宏参数转换成一个新的字符串。
#define exp1(s) printf("test s is : %s\n", #s);
- 符号连接操作符(##),将宏参数和前面的字符连接为一个新的字符串。
#define expA(s) printf("前缀加上后的字符串为:%s\n",gc_##s) // gc_s必须存在
- 使用do {}while(1),避免代码曲解
#define fun() do {f1();f2();}while(0);
- 不定参宏
#define LOG(format, ...) do {fprintf(logfile, format, __VA_ARGS__); printf(format, __VA_ARGS__); fflush(logfile);} while(0)
1.17. initializer
C++是初始使用初始化器的,C++17在C++11的基础上进一步强化了初始化的功能。
int arr[2] = {1, 2};
struct MY_STRUCT
{
int a;
int b;
int c;
}
MY_STRUCT my = {}; // 全部初始化为默认参数0
MY_STRUC my1 = {1, 2, 3};
MY_STRUCT my2 = {.b = 3, .c = 5}; // a默认为0
static std::map<string, string> const nameToBirthday = {
{"lisi", "18841011"},
{"zhangsan", "18850123"},
{"wangwu", "18870908"},
{"zhaoliu", "18810316"},
};
1.18. new
动态申请内存,默认在堆上申请内存,也可以指定在栈上申请内存。new默认是不抛出异常的,如果想抛出异常,使用operator new。一般情况下不建议使用,new异常表示代码有Bug,要报出错误,更利于查找问题。
int p = new [10](); // ()表示全部初始化0
char mem[100] = {}; // 申请栈内存
STRUCT* P = new (mem)STRUCT;
1.19. delete
delete/delete[]的区别,针对POD数据类型指针,因为不存在构造函析构函数,所以两都没有差别。针对有析构函数的指针时,delete只调用一次析构函数,delete[]会循环调用多次析构函数。
1.20. smart pointer
- unique_ptr,顾名思义,保持保证一的指针。大胆使用,如果编译不过,表明当前情况需要使用shared_ptr。
- shared_ptr,不能使用unique_ptr的地方,都使用shared_ptr。
- weak_ptr,在循环依赖时使用。
注意:
- 使用std::make_unique和std::make_shared来构造相应的对象。
- 使用std::shared_ptr和std::make_shared后,如果报内存泄露。
- 检查是不是数组,如果是数组,则要指定数组释放器。
- 如果不是数组,依然是内存泄露,那么就可能是循环依赖了,就需要使用std::weak_ptr来解除循环依赖。
1.21. cast
为什么要使用C++的类型转换,一则更安全,如dynamic_cast会检测基类指针转换为子类指针是否成功。另外,C+类型转换更显式,更容易引起程序员的注意。
static_cast用于非多态类型转换,dynamic_cast主要用于多态类型转换,const_cast用于const相关的转换,reinterpret_cast用于指针相关的转换。
1.22. 指针与引用
指针,指向一片指定类型的内存,指针可以直接理解为地址,*(解引用),即将地址对应的内存转换为指定类型的对象。
引用,是变量的别名,引用在语法上可以理解为T * const ,即一个常量指针。在汇编层面上,引用就是指针。
总之,能用引用的地方就用引用,因为其相比单纯的指针,更安全。
void GetMemory1(int* p)
{
p = new int[10]();
}
void GetMemory2(int** p)
{
*p = new int[10]();
}
void GetMemory3(int*& p)
{
p = new int[10]();
}
1.23. 数组
数组是固定大小的一种复合类型
- 因为数组是固定大小,所以在编译期间就决定了基大小
- 数组的内存是连续(无论是一维数组还是多维数组)
- 数组之间不允许拷贝和赋值
- 数组名是常指针
- 一般情况下,能够使用vectory或array代替的尽量代替。
int nArr[4] = {}; // 默认所有值为0
int nArr2[4] = {1, 2, 3}; // 第4值默认为0
char szArr[] = "abcd"; // 编译器在编译阶段推断数组维数为5
const int MAX_SIZE = 1024*1024*1024;
const int ARRAY_CNT = 3;
char (*pArrVal)[MAX_PAGE_SIZE]= new char[ARRAY_CNT][MAX_PAGE_SIZE]();
std::unique_ptr<char[][MAX_PAGE_SIZE]> temp(pArrVal);
1.24. 参数传递
在 C++ 里面,一个函数在使用参数时,如果使用 pass-by-value 方式,那么编译系统会在调用该函数的地方, 把实参复制一份传给函数的形参 。例如:int FunA(string strTest);
为什么要将实参拷贝给形参呢?因为C++默认的参数传递方法是通过栈来传递的,调用函数时,参数压栈,退出函数时,参数出栈。那么压栈的时候,就必须在栈上构造一个相应的参数用来接受实参的赋值。那么FunA调用时,必须在参数栈上构建一个string的参数来接受实参的内容。如果这个strTest参数非常大,那么在参数栈上构造的参数就会浪费很大的空间,并且影响代码的执行效率。所以一般情况下,针对一个复杂的类型,都建议使用const &来修饰,避免不必要的参数拷贝。
在 C++ 里面,一个函数在使用参数时,如果使用 pass-by-reference 方式,那么编译系统会在调用该函数的地方, 直接将实参的内存地址(指针)传给形参 。引用传递可以理解为常量指针传递。
指针作为形参时,可以理解为就是值传递,就是一个指针类型的值(地址在传递)。
void GetMemory1(int* pTemp)
{
pTemp = new int[10]();
}
int* p = NULL; // 即p指向0地址
// 在参数栈上构造一个int* pTemp,然后pTemp = p(即pTemp指向0);
GetMemory1(p); // 调用之后,pTemp指向new int[10]();此时p还是指向0
1.25. Modern C++
Modern C++泛指C++11及之后的新标准C++。Modern C++相比C++98标准,有了非常大的改动,代码的表达能力更强,安全性更好。
1.25.1. 右值引用
一个变量有名字,那么它是左值,否则它是右值。右值可以理解为在当前语句结束后就立即消失的无名变量。普通类型的常量都是右值,但是字符串常量因为生存周期是全局的,所以字符串常量是左值。右值引用,即绑定到右值的引用,通过&&来获取右值的引用。
int&& nRRef = 1;
const string& strLRef = “LValue Reference”;
// nRRef 虽然是右值引用,但它是具名的,所以 nRRef 是左值
1.25.2. 移动语义
移动语义,即将变量的主权移动给其他变量,移动之后,原变量无效。移动语义可以针对左值,也可以针对右值,不过一般针对右值,因为右值本来就会消失。
class CMyString
{
public:
CMyString()
{
m_data = NULL;
m_len = 0;
}
CMyString(const char* p)
{
m_len = strlen (p);
Init(p);
}
CMyString(const CMyString&& str)
{
m_len = str.m_len;
m_data = str.m_data;
str.m_data = NULL;
std::cout << "Copy Constructor is called! source: " << m_data << std::endl;
}
CMyString& operator=(const CMyString&& str)
{
if (this != &str)
{
m_len = str.m_len;
m_data = str.m_data;
str.m_data = NULL;
}
std::cout << "Copy Assignment is called! source: " << m_data << std::endl;
return *this;
}
virtual ~CMyString()
{
if (m_data)
delete[] m_data;
}
private:
void Init(const char *s)
{
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
private:
char* m_data;
size_t m_len;
};
CMyString GetMyString()
{
CMyString str = "abc";
return str; // A
}
int _tmain(int argc, _TCHAR* argv[])
{
CMyString myStr;
myStr = GetMyString(); // B:1个右值赋给1个左值,移动赋值函数,因为右值赋值给左值
CMyString strex(std::move(myStr); // 此处则调用移动构造函数,myStr移动之后无效
return 0;
}
1.25.3. auto
C++11废弃了之前的auto的自动变量的语义,使其作为一个类型推导语义,动态类型。
auto不宜过度使用,如下面代码中的j和m,m的声明并没有简化代码,相反阅读代码时需要自行推导一下,才能得知其类型。但是像迭代器指针,就能大幅带来代码简洁,收益会更大。Python是一个动态类型语言,各种变量都需要推荐其类型,如果不熟悉代码,要花很长时间才能弄明白变量的类型,这样不仅不利于人阅读代码,也不利于代码静态分析。这也是Python3.6之后大幅引入类型提示用来标识变量的类型。
int j = 0;
auto m = j; // m 是 int 类型
auto n = 0; // 0 默认是 int 类型
map<int,list<string>>::iterator i = m.begin();
auto i = m.begin();
std::unique_ptr<int> p1 = std::make_unique<int>(4);
auto p2 = std::make_unique<int>(4);
1.25.4. Lambda
Lambda 表达式就是匿名函数。Lambda 表达式表示一个可调用的代码单元。与其他函
数一样,Lambda 具有一个返回类型、一个参数列表和一个参数体匿名函数Lambda的好处即用即消失,作用域非常小,这样更安全,而且也不用为取名操心。Lambda如果过度使用,会导致调用函数变得复杂,会带来负作用。这也是为什么。Python中的Lambda,只能使使用一行代码,也即严格限制了Lambda的使用。
int nFlag = 10;
int nArr[] = {5, 3, 2, 11, 4, 22};
auto first = find_if(nArr, nArr+6, [nFlag](int nValue){return nValue > nFlag;});
1.25.5. std::function
回调函数是一种非常有利的设计思路,但是在早期,回调函数只能使用全局函数或静态成员函数。那么像类的成员函数,或者匿名函数甚至仿函数呢?此时std::function提出来统一这所有函数的回调。
#include <functional>
class Foo
{
public:
void Sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
};
void Sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
struct SUM
{
void operator()(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
};
int TestBind()
{
// 全局函数或静态成员函数
std::function<void (int, int)> fun1 = Sum;
std::function<void (int, int)> fun11 = std::bind(&Sum, std::placeholders::_1, std::placeholders::_2);
fun1(1, 2);
fun11(1, 2);
// 类成员函数
Foo foo;
std::function<void (int, int)> fun2 = std::bind(&Foo::Sum, &foo, std::placeholders::_1, std::placeholders::_2);
fun2(3, 4);
// 仿函数
std::function<void (int, int)> fun3 = SUM();
fun3(5, 6);
// 匿名函数(Lambda表达式)
std::function<void (int, int)> fun4 = [](int a, int b) {std::cout << a+b << '\n'; };
fun4(7, 8);
// 占位符可以用作添加默认参数
auto fun5 = std::bind(&Foo::Sum, &foo, 95, std::placeholders::_1);
std::function<void (int)> fun6 = std::bind(&Foo::Sum, &foo, std::placeholders::_1, 5);
fun5(5);
fun6(95);
return 0;
}
1.25.6. 修饰符
- override,标记子类的虚函数是重写基类的虚函数,避免子类虚函数名字写错了没有达到重写的目的。
- delete,标记此函数废弃,不用再像C++98中那样要用private来修饰。
- final,禁止子类重写父类的函数或禁止从此类派生。
1.25.7. class enum
在C++11之前,enum无法指定作用域范围,导致容易和其他常量名冲突。
enum class Color { black, white, red }; // black, white, red
if (clr == Color::red) {.....}
1.25.8 基于范围的for
for语句要尽量简单,其功能越少,其出错的风险越小。基于范围的for就只做一个循环,避免for语句中的申明和判断导致的风险。
int a[] = {0, 1, 2, 3, 4, 5};
for (int n : a) // 初始化器可以是数组
std::cout << n << ' ';
for(const auto& [key, value]: map){
// ...
1.25.9. std::lock_guard
基于构造函数析构函数(RAII)来完成加锁和自动解锁,避免有分支路径未解锁导致死锁。
void testFunc()
{
//lock_guard 互斥锁 作用域内上锁
std::lock_guard<std::mutex> lockGuard(mutex);
//函数体
counter++;
} //函数结束时,作用域结束,自动释放
1.26. Object-oriented programming
1.26.1. 封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
1.26.2. 继承
继承,主要是为了复用代码,并且要保证是继承的关系。是包括关系,还是继承关系,要弄明白。包括关系用成员变量,继承关系才用继承。除了多重继承接口类(抽象类)以外,其他情况不要使用多重继承。
基类(父类)——> 派生类(子类)
1.26.3. 多态
- 多态,即多种状态,在面向对象语言中,接口的多种不同的实现方式即为多态。
- C++ 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的,原理是用虚函数表来达到的。
- 多态是以封装和继承为基础的。
2. Effective C++
- 视 C++ 为一个语言联邦(C、Object-Oriented C++、Template C++、STL)
- 宁可以编译器替换预处理器(尽量以 const、enum、inline 替换 #define)
- 尽可能使用 const
- 确定对象被使用前已先被初始化(构造时赋值(copy 构造函数)比 default 构造后赋值(copy assignment)效率高)
- 了解 C++ 默默编写并调用哪些函数(编译器暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符、析构函数)
- 若不想使用编译器自动生成的函数,就应该明确拒绝(将不想使用的成员函数声明为 private,并且不予实现)
- 为多态基类声明 virtual 析构函数(如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数)
- 别让异常逃离析构函数(析构函数应该吞下不传播异常,或者结束程序,而不是吐出异常;如果要处理异常应该在非析构的普通函数处理)
- 绝不在构造和析构过程中调用 virtual 函数(因为这类调用从不下降至 derived class)
- 令 operator= 返回一个 reference to *this (用于连锁赋值)
- 在 operator= 中处理 “自我赋值”
- 赋值对象时应确保复制 “对象内的所有成员变量” 及 “所有 base class 成分”(调用基类复制构造函数)
- 以对象管理资源(资源在构造函数获得,在析构函数释放,建议使用智能指针,资源取得时机便是初始化时机(Resource Acquisition Is Initialization,RAII))
- 在资源管理类中小心 copying 行为(普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权(类似 auto_ptr))
- 在资源管理类中提供对原始资源(raw resources)的访问(对原始资源的访问可能经过显式转换或隐式转换,一般而言显示转换比较安全,隐式转换对客户比较方便)
- 成对使用 new 和 delete 时要采取相同形式(new 中使用 [] 则 delete [],new 中不使用 [] 则 delete)
- 以独立语句将 newed 对象存储于(置入)智能指针(如果不这样做,可能会因为编译器优化,导致难以察觉的资源泄漏)
- 让接口容易被正确使用,不易被误用(促进正常使用的办法:接口的一致性、内置类型的行为兼容;阻止误用的办法:建立新类型,限制类型上的操作,约束对象值、消除客户的资源管理责任)
- 设计 class 犹如设计 type,需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等等。
- 宁以 pass-by-reference-to-const 替换 pass-by-value (前者通常更高效、避免切割问题(slicing problem),但不适用于内置类型、STL迭代器、函数对象)
- 必须返回对象时,别妄想返回其 reference(绝不返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。)
- 将成员变量声明为 private(为了封装、一致性、对其读写精确控制等)
- 宁以 non-member、non-friend 替换 member 函数(可增加封装性、包裹弹性(packaging flexibility)、机能扩充性)
- 若所有参数(包括被this指针所指的那个隐喻参数)皆须要类型转换,请为此采用 non-member 函数
- 考虑写一个不抛异常的 swap 函数
- 尽可能延后变量定义式的出现时间(可增加程序清晰度并改善程序效率)
- 尽量少做转型动作(旧式:(T)expression、T(expression);新式:const_cast(expression)、dynamic_cast(expression)、reinterpret_cast(expression)、static_cast(expression)、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)
- 避免使用 handles(包括 引用、指针、迭代器)指向对象内部(以增加封装性、使 const 成员函数的行为更像 const、降低 “虚吊号码牌”(dangling handles,如悬空指针等)的可能性)
- 为 “异常安全” 而努力是值得的(异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏,分为三种可能的保证:基本型、强列型、不抛异常型)
- 透彻了解 inlining 的里里外外(inlining 在大多数 C++ 程序中是编译期的行为;inline 函数是否真正 inline,取决于编译器;大部分编译器拒绝太过复杂(如带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空;inline 造成的代码膨胀可能带来效率损失;inline 函数无法随着程序库的升级而升级)
- 将文件间的编译依存关系降至最低(如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects;如果能过够,尽量以 class 声明式替换 class 定义式;为声明式和定义式提供不同的头文件)
- 确定你的 public 继承塑模出 is-a(是一种)关系(适用于 base classes 身上的每一件事情一定适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象)
- 避免遮掩继承而来的名字(可使用 using 声明式或转交函数(forwarding functions)来让被遮掩的名字再见天日)
- 区分接口继承和实现继承(在 public 继承之下,derived classes 总是继承 base class 的接口;pure virtual 函数只具体指定接口继承;非纯 impure virtual 函数具体指定接口继承及缺省实现继承;non-virtual 函数具体指定接口继承以及强制性实现继承)
- 考虑 virtual 函数以外的其他选择(如 Template Method 设计模式的 non-virtual interface(NVI)手法,将 virtual 函数替换为 “函数指针成员变量”,以 tr1::function 成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)
- 绝不重新定义继承而来的 non-virtual 函数
- 绝不重新定义继承而来的缺省参数值,因为缺省参数值是静态绑定(statically bound),而 virtual 函数却是动态绑定(dynamically bound)
- 通过复合塑模 has-a(有一个)或 “根据某物实现出”(在应用域(application domain),复合意味 has-a(有一个);在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出))
- 明智而审慎地使用 private 继承(private 继承意味着 is-implemented-in-terms-of(根据某物实现出),尽可能使用复合,当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的时候 virtual 函数,或需要 empty base 最优化时,才使用 private 继承)
- 明智而审慎地使用多重继承(多继承比单一继承复杂,可能导致新的歧义性,以及对 virtual 继承的需要,但确有正当用途,如 “public 继承某个 interface class” 和 “private 继承某个协助实现的 class”;virtual 继承可解决多继承下菱形继承的二义性问题,但会增加大小、速度、初始化及赋值的复杂度等等成本)
- 了解隐式接口和编译期多态(class 和 templates 都支持接口(interfaces)和多态(polymorphism);class 的接口是以签名为中心的显式的(explicit),多态则是通过 virtual 函数发生于运行期;template 的接口是奠基于有效表达式的隐式的(implicit),多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期)
- 了解 typename 的双重意义(声明 template 类型参数是,前缀关键字 class 和 typename 的意义完全相同;请使用关键字 typename 标识嵌套从属类型名称,但不得在基类列(base class lists)或成员初值列(member initialization list)内以它作为 basee class 修饰符)
- 学习处理模板化基类内的名称(可在 derived class templates 内通过 this-> 指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成)
- 将与参数无关的代码抽离 templates(因类型模板参数(non-type template parameters)而造成代码膨胀往往可以通过函数参数或 class 成员变量替换 template 参数来消除;因类型参数(type parameters)而造成的代码膨胀往往可以通过让带有完全相同二进制表述(binary representations)的实现类型(instantiation types)共享实现码)
- 运用成员函数模板接受所有兼容类型(请使用成员函数模板(member function templates)生成 “可接受所有兼容类型” 的函数;声明 member templates 用于 “泛化 copy 构造” 或 “泛化 assignment 操作” 时还需要声明正常的 copy 构造函数和 copy assignment 操作符)
- 需要类型转换时请为模板定义非成员函数(当我们编写一个 class template,而它所提供之 “与此 template 相关的” 函数支持 “所有参数之隐式类型转换” 时,请将那些函数定义为 “class template 内部的 friend 函数”)
- 请使用 traits classes 表现类型信息(traits classes 通过 templates 和 “templates 特化” 使得 “类型相关信息” 在编译期可用,通过重载技术(overloading)实现在编译期对类型执行 if…else 测试)
- 认识 template 元编程(模板元编程(TMP,template metaprogramming)可将工作由运行期移往编译期,因此得以实现早期错误侦测和更高的执行效率;TMP 可被用来生成 “给予政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码)
- 了解 new-handler 的行为(set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new 是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常)
3. STL
3.1. container
容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 | 其他 |
array | 数组 | 随机读改 O(1) | 无序 | 可重复 | 支持快速随机访问 |
vector | 数组 | 随机读改、尾部插入、尾部删除 O(1) 头部插入、头部删除 O(n) | 无序 | 可重复 | 支持快速随机访问 |
list | 双向链表 | 插入、删除 O(1) 随机读改 O(n) | 无序 | 可重复 | 支持快速增删 |
deque | 双端队列 | 头尾插入、头尾删除 O(1) | 无序 | 可重复 | 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 |
stack | deque / list | 顶部插入、顶部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
queue | deque / list | 尾部插入、头部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
priority_queue | vector + max-heap | 插入、删除 O(log2n) | 有序 | 可重复 | vector容器+heap处理规则 |
set | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multiset | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
map | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multimap | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
hash_set | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
hash_multiset | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 | |
hash_map | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | |
hash_multimap | 哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 | |
any | 模板 | 无序 | 可重复 |
3.2. Algorithm
sort、search、copy、erase、fill、transform、find等。
3.3. Iterator
迭代器,容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。
3.4. functors
仿函数,也即像函数。即通过重载类或结构体的operator() 操作符来达到函数的作用。仿函数相比普通的函数,可以通过构造函数来扩展仿函数的功能。
#include <vector>
#include <algorithm>
#include <iostream>
// The function object multiplies an element by a Factor
template <class Type>
class MultValue
{
private:
Type Factor; // The value to multiply by
public:
// Constructor initializes the value to multiply by
MultValue ( const Type& _Val ) : Factor ( _Val )
{
}
// The function call for the element to be multiplied
void operator ( ) ( Type& elem ) const
{
elem *= Factor;
}
};
int main( )
{
std::vector <int> v1;
std::vector <int>::iterator Iter1;
// Constructing vector v1
int i;
for ( i = -4 ; i <= 2 ; i++ )
{
v1.push_back( i );
}
// Using for_each to multiply each element by a Factor
std::for_each ( v1.begin ( ) , v1.end ( ) , MultValue<int> ( -2 ) );
}
greater()和less()也是直接构建一个无名对象,然后调用重载的括号运算符。
3.5. adapters
适配器,一种用来修饰容器、仿函数、迭代器接口的东西,是一种适配器模式的应用。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。
3.6. allocators
负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。有时,如果想实现超大内存的vector,怎么办呢?真实的内存不够用,必须使用硬盘来存储,此时就可以重写分配器,当内存超过一定范围时,将内存直接分配到硬盘上云。
4. Data Structure
4.1. stack
typedef struct {
ElemType *elem;
int top;
int size;
int increment;
} SqSrack;
4.2. Queue
SqQueue.rear = (SqQueue.rear + 1) % SqQueue.maxSize
if (rear == front) // empty
if ((rear+1) % maxsize == front) // full
4.3. Hash Table
主要是通过Hash函数将Key转换为索引,建立一个数组存储数据。这样就可以通过Key->Index->value。
Hash函数常用:
- 直接定址法
- 除留余数法
- 数字分析法
- 折叠法
- 平方取中法
难免有不同的Key通过Hash函数之后生成相同的Index,此时就冲突,一般处理:
- 链地址法:key 相同的用单链表链接来表示。先通过Key生成的Index去查找存储的Key-Value,如果此时存储的Key与实际查找的Key不一致,则认为是冲突。
- 开放定址法
- 线性探测法:key 相同 -> 放到 key 的下一个位置,Hi = (H(key) + i) % m
- 二次探测法:key 相同 -> 放到 Di = 1^2, -1^2, …, ±(k)^2,(k<=m/2)
- 随机探测法:H = (H(key) + 伪随机数) % m
4.4. tree
4.4.1. 二叉树
即有2个分叉,遍历方式
- 先序遍历
- 中序遍历
- 后续遍历
- 层次遍历
- 二叉查找树(二叉排序树):左 < 根 < 右,这样排列有利于查找。
- 平衡二叉树(AVL树):| 左子树树高 - 右子树树高 | <= 1,这样树高更平衡,更有利于查找。
- 红黑树,不要求严格的平衡,并且增加了红色黑色状态,这样在构造左右顺序时更容易,红黑树在添加和删除时相比AVL树更容易,但是查找因为不要求严格的平衡,所以会多查找一步。综合性能更平均。在实际的APP应用中,如果搜索次数远远多于插入和删除,则应该选择AVL;如果搜索次数差不多,则应该选择RB。
- B树和B+树
从上图可以看出:
B树是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance)
B+树有一个最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了。
B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
5. Algorithm
5.1. 排序算法
排序算法 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 数据对象稳定性 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(1) | 数组不稳定、链表稳定 |
插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
快速排序 | O(n*log2n) | O(n2) | O(log2n) | 不稳定 |
堆排序 | O(n*log2n) | O(n*log2n) | O(1) | 不稳定 |
归并排序 | O(n*log2n) | O(n*log2n) | O(n) | 稳定 |
希尔排序 | O(n*log2n) | O(n2) | O(1) | 不稳定 |
计数排序 | O(n+m) | O(n+m) | O(n+m) | 稳定 |
桶排序 | O(n) | O(n) | O(m) | 稳定 |
基数排序 | O(k*n) | O(n2) |
| 稳定 |
5.2. 查找算法
查找算法 | 平均时间复杂度 | 空间复杂度 | 查找条件 |
顺序查找 | O(n) | O(1) | 无序或有序 |
二分查找(折半查找) | O(log2n) | O(1) | 有序 |
插值查找 | O(log2(log2n)) | O(1) | 有序 |
斐波那契查找 | O(log2n) | O(1) | 有序 |
哈希查找 | O(1) | O(n) | 无序或有序 |
二叉查找树(二叉搜索树查找) | O(log2n) | ||
红黑树 | O(log2n) | ||
2-3树 | O(log2n - log3n) | ||
B树/B+树 | O(log2n) |
5.3. 树图搜索算法
图搜索算法 | 数据结构 | 遍历时间复杂度 | 空间复杂度 |
---|---|---|---|
BFS广度优先搜索 | 邻接矩阵 邻接链表 | O(|v|2) O(|v|+|E|) | O(|v|2) O(|v|+|E|) |
DFS深度优先搜索 | 邻接矩阵 邻接链表 | O(|v|2) O(|v|+|E|) | O(|v|2) O(|v|+|E|) |
5.4. Other
算法 | 思想 | 应用 |
---|---|---|
分治法 | 把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并 | 循环赛日程安排问题、排序算法(快速排序、归并排序) |
动态规划 | 通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,适用于有重叠子问题和最优子结构性质的问题 | 背包问题、斐波那契数列 |
贪心法 | 一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法 | 旅行推销员问题(最短路径问题)、最小生成树、哈夫曼编码 |
6. Design Pattern
6.1. 工厂模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。定义一个抽象工厂类,每一个产品,按照抽象工厂的基本要求新建一个工厂来生产新的产品。创建型模式。
6.2. 单例模式
保证一个类只有一个实例,并提供一个全局访问点。创建型模式。
6.3. 适配器模式
适配器模式,将一个类的接口,转换成客户期望的另一个接口。适配器模式让原本由于接口不兼容而不能一起工作的类可以一起工作。结构型模式。
6.4. 装饰者模式
动态地给一个对象添加一些职责。就扩展功能而言,装饰者模式比继承更加灵活。结构型模式。
6.5. 观察者模式
定义了对象之间一对多的依赖关系,当一个对象发生改变时,它的所有依赖者都会收到通知并自动更新。对象行为型模式。
7. Design Principle
71. 开闭原则(Open Closed Principle)
定义:软件实体应该对扩展开放,对修改关闭。
由来:一些软件生命周期很长,必然面临维护升级等变化。而新添加的代码很容易对旧有的代码造成影响,甚至给旧有的代码带来Bug。
解决:当软件代码需要进行变动时,尽量以添加新的代码来完成,而不去修改原有的代码。也即通过扩展来完成所需要的功能的添加。
7.2. 里氏替换原则(Liskov Substitution Principle)
定义:继承必须确保父类所拥有的性质在子类中仍然成立。
由来:通过子类来完成父类的任务,可能会产生问题。
解决:子类可以实现父类的抽象方法,但是不去Override父类的非抽象方法。这也算是某种意义上的开闭原则吧,尽量不要去影响旧有的代码,通过扩展(取新名字,而不是Override)来完成新功能。
7.3. 依赖倒置原则(Dependence Inversion Principle)
定义:高层模块不依赖于底层模块,两者都应该依赖于抽象,抽象不依赖于细节,细节依赖于抽象。
由来:表示层、业务逻辑层以及数据访问层之间如果分得不太清楚,各模块之间交叉调用,就会带来很强的耦合性,往往会牵一发而动全身,改动一个地方,很多地方都会受到影响,增加出错的风险。
解决:主要是通过面对接口编程,将实现细节与业务逻辑分开,它们都是通过抽象的接口来完成交互的。业务逻辑只和抽象的接口打交到,而不必关注具体的实现过程。同样实现过程也不必关注业务,它只需要关注接口即抽象即可。
7.4. 接口隔离原则(Interface Segregation Principle)
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应建立在最小接口上。
由来:一个接口里完成了很多工作,但是当个功能只需要调用接口里的一小部分功能的时候,如果调用这个接口,就会做一些不必要的工作,甚至可能产生问题。
解决:一个接口尽量完成比较单一的任务,这样两个类交互时,产生的影响才会在控制范围内。
7.5. 合成/聚合复用原则(Composite/Aggregate Reuse Principle)
定义:能够使用合成/聚合的,不要使用继承。合成是指局部与整体的关系;聚合则是包含的关系。
由来:继承关系是在编译时就确定了,如果想在运行时改变父类与子类的关系就不行了;另外父类改变了一定会影响到子类。继承的关系限制了更灵活地复用代码。
解决:通过使用合成/聚合来替代继承关系,达到更灵活地修改代码。
迪米特法则(Law Of Demeter)
定义:也称最少知道原则(Least Knowledge Principle),只和你最直接的类(成员变量、方法参数、方法返回值中的类)沟通,尽可能少地与其他实体发生交互。
由来:想降低类之间的耦合关系,相互之间尽量减少依赖关系。
解决:与越少的类相互越好,尽量做到低耦合高内聚。尽量做到模块化。
P.S. 经常被提到的原则还有单一职责原则(Single Responsibility Principle),一个类只应该有一个引起它变化的原因,也即一个类只负责一件事。像流水线作业一样,一位员工只负责自己的那一部分,完了就交给其他员工。其实不仅仅是类,一个函数同样应该如此。
P.S. 设计模式的这几大原则,是我们设计模式的一个根本。每一个设计模式都是为了更好的完成这些设计模式的原则而总结设计出来的。设计模式的原则是基本功,而23个设计模式则是对基本功的应用。当23个设计模式能够融会贯通的时候,就可以不用固定于那23个设计模式了,而是根据这些设计模式的原则来自由设计代码。