提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
生命周期和编程范式
C++的五种范式
- 面向过程
- 面向对象
- 泛型编程
- 模板元编程
- 函数式
秀出好的code style
留白的艺术
好程序里的空白行至少要占到总行数的 20% 以上
if (!value.contain()) { // if后{前有空格
LOGIT(WARING, "value is incomplete.\n") // 逗号后有空格
}
// 新增空行分隔段落
char suffix[16] = "xxx";
int data_len = 100; // 等号两边有空格
if (!value.empty() && value.contains("tom")) { // &&两边有空格
const char* name = value.c_str();
for (int i = 0; i < MAX_LEN; i++){ // =;<处有空格
//do something
}
}
起个好名字
普遍共识的变量名,比如用于循环的i/j/k、用于计数的 count、表示指针的 p/ptr、表示缓冲区的 buf/buffer、表示变化量的delta、表示总和的 sum……
我认为的命名法,是由“匈牙利命名法”,“驼峰式命名法”,“snake_case”综合起来
- 变量、函数名和名字空间用 snake_case,全局变量加“g_”前缀;
- 自定义类名用 CamelCase,成员函数用 snake_case,成员变量加“m_”前缀;
- 宏和常量应当全大写,单词之间用下划线连接;
- 尽量不要用下划线作为变量的前缀或者后缀(比如 local、name),很难识别。
#define MAX_PATH_LEN 256
int g_sys_flag = 0;
namespace linux_sys {
void get_rlimit_core();
}
class FilePath {
public:
void set_file(const string& path);
private:
int m_path;
int m_level;
};
注释
1 // Copyright (c) 2020 by Chrono
2 //
3 // file : xxx.cpp
4 // since : 2020-xx-xx
5 // desc : ...
宏定义和条件编译
预处理
#ifndef __HEAD_H__
#define __HEAD_H__
//头文件
#endif
#if __linux__
#define HAS_LINUX 1
#endif
包含头文件
#include 不止包含头文件 包含任意头文件
以利用“#include”的特点玩些“小花样”,编写一些代码片段,存进“*.inc”文件里,然后有选择地加载,用得好的话,可以实现“源码级别的抽象
1 static uint32_t calc_table[] = { // 非常大的一个数组,有几十行
2 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
3 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
4 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
5 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
6 ...
7 };
这个时候,你就可以把它单独摘出来,另存为一个“*.inc”文件,然后再
用“#include”替换原来的大批数字。这样就节省了大量的空间,让代码更加整洁
static uint32_t calc_table = {
#include "calc_table.inc"
}
条件编译(#if/#else/#endif)
#if __cplusplus >= 201402
cout << "C++14 or later" << endl;
#elif __cplusplus >= 201103
cout << "C++11 or before" <<< endl;
#else
#endif
条件编译还有一个特殊的用法,那就是,使用“#if 1”“#if 0”来显式启用或者禁用大段代码,要比“/* … */”的注释方式安全得多,也清楚得多
属性和静态断言
typedef、using、template、struct/class 这些关键字定义的类型,而不是运行阶段的变量。所以,这时的编程思维方式与平常大不相同。我们熟悉的是 CPU、内存、Socket,但要去理解编译器的运行机制、知道怎么把源码翻译成机器码,这可能就有点“强人所难”了
让编译器递归计算斐波那契数列,这已经算是一个比较容易理解的编译阶段数值计算用法
template<int N>
struct fib{
static const int value = fib<N-1>::value + fib<N-2>::value;
};
template<>
struct fib<0>{
static const int value = 1;
};
template<>
struct fib<1>{
static const int value = 1;
};
cout << fib<5>::value << endl;
属性
deprecated:与 C++14 相同,但可以用在 C++11 里。
unused:用于变量、类型、函数等,表示虽然暂时不用,但最好保留着,因为将来可能会用。
constructor:函数会在 main() 函数之前执行,效果有点像是全局对象的构造函数。
destructor:函数会在 main() 函数结束之后执行,有点像是全局对象的析构函数。
always_inline:要求编译器强制内联函数,作用比 inline 关键字更强。
hot:标记“热点”函数,要求编译器更积极地优化。
断言
assert(p != nullptr);
assert 虽然是一个宏,但在预处理阶段不生效,而是在运行阶段才起作用
当程序(也就是 CPU)运行到 assert 语句时,就会计算表达式的值,如果是 false,就会输出错误消息,然后调用 abort() 终止程序的执行
怎样才能写出一个“好”的类?
一个类总是会有六大基本函数:三个构造、两个赋值、一个析构
using uint_t unsigned int ;
typedef unsigned int uint_t;
auto
C++ 标准不允许使用 auto 推导类型(但我个人觉得其实没有必要,也许以后会放开吧)。所以,在类里你还是要老老实实地去“手动推导类型”
在 C++14 里,auto 还新增了一个应用场合,就是能够推导函数返回值,这样在写复杂函数的时候,比如返回一个 pair、容器或者迭代器,就会很省事。
auto get_a_set() {
std::set<int> s = {1, 2, 3};
return s;
}
decltype
在定义类的时候,因为 auto 被禁用了,所以这也是 decltype 可以“显身手”的地方。它可以搭配别名任意定义类型,再应用到成员变量、成员函数上,变通地实现 auto 的功能。
class DemoClass {
public:
using set_type = std::set<int> ;
private:
set_type m_set;
using iter_type = decltype(m_set.begin());
iter_type m_pos;
}
const/volatile/mutable:常量/变量究竟是怎么回事
1.const
它是一个类型修饰符,可以给任何对象附加上“只读”属性,保证安全;
它可以修饰引用和指针,“const &”可以引用任何类型,是函数入口参数的最佳类型;
它还可以修饰成员函数,表示函数是“只读”的,const 对象只能调用 const 成员函数。
2.volatile
它表示变量可能会被“不被察觉”地修改,禁止编译器优化,影响性能,应当少用。
3.mutable
它用来修饰成员变量,允许 const 成员函数修改,mutable 变量的变化不影响对象的常量性,但要小心不要误用损坏对象。
你今后再写类的时候,就要认真想一想,哪些操作改变了内部状态,哪些操作没改变内部状态,对于只读的函数,就要加上 const 修饰。写错了也不用怕,编译器会帮你检查出来。
总之就是一句话:尽可能多用 const,让代码更安全。
智能指针到底“智能”在哪里
认识unique_ptr
unique_ptr<int> u_ptr( new int(5) );
assert(u_ptr != nullptr);
//不可
u_ptr++;
u_ptr += 2;
//可变参数模板
template<typename T, class ... Args> //Args为多个参数的类型
std::unique_ptr<T> //返回智能指针
make_unique_ptr(Args&& ... args){
return std::unique_ptr<T>(
new T (std::forward<Args>(args)...)); //完美转发
}
unique_ptr 应用了 C++ 的“转移”(move)语义,同时禁止了拷贝赋值,所以,在向另一个 unique_ptr 赋值的时候,要特别留意,必须用 std::move() 函数显式地声明所有权转移
unique_ptr<int> u_ptr( new int(5));
auto u_ptr1 = std::move(u_ptr);
怎么才能用好异常 exception
C++ 已经为处理异常设计了一个配套的异常类型体系,定义在标准库的 < stdexcept >
头文件里
void func(int num)
try
{
cout << "num :" << num ;
}catch(const exception &e){
std::cout << e.what() << std::endl;
}
noexcept 专门用来修饰函数,告诉编译器:这个函数不会抛出异常。编译器看到noexcept,就得到了一个“保证”,就可以对函数做优化,不去加那些栈展开的额外代码,消除异常处理的成本。
void func_maybe_noexcept() noexcept
{
cout << "" << endl;
}
lambda:函数式编程带来了什么?
在 lambda 表达式赋值的时候,我总是使用 auto 来推导类型。这是因
为,在 C++ 里,每个 lambda 表达式都会有一个独特的类型,而这个类型只有编译器才知道,我们是无法直接写出来的,所以必须用 auto
auto f2 = []()
{
auto f3 = [](int x)
{
return x*x;
}
f3(4);
}
不过,因为 lambda 表达式毕竟不是普通的变量,所以 C++ 也鼓励程序员尽量“匿名”使用 lambda 表达式。也就是说,它不必显式赋值给一个有名字的变量,直接声明就能用,免去你费力起名的烦恼。
vector<int> v = {1,8,5,3,2};
std::cout << *find_if(v.begin(),v.end(),[](const int x)
{
return x >= 5;
}
) << endl;
变量捕获
“[=]”表示按值捕获所有外部变量,表达式内部是值的拷贝,并且不能修改;
“[&]”是按引用捕获所有外部变量,内部以引用的方式使用,可以修改;
int x =3;
auto f1 = [&](){
x += 3;
}
auto f2 = [=](){
//不能对x进行操作
}
auto f3 = [=, &x](){
x += 20; // x是引用,可以修改
}
你在使用捕获功能的时候要小心,对于“就地”使用的小 lambda 表达式,
可以用“[&]”来减少代码量,保持整洁;而对于非本地调用、生命周期较长的 lambda 表达式应慎用“[&]”捕获引用,而且,最好是在“[]”里显式写出变量列表,避免捕获不必要的变量。
class DemoLanbuda {
public:
auto print(){
return [this](){
cout << x << endl;
}
}
private:
int x = 3;
}
一枝独秀的字符串:C++也能处理文本
在处理字符串的时候,我们还会经常遇到与数字互相转换的事情,以前只能用 C 函数
atoi()、atol(),它们的参数是 C 字符串而不是 string,用起来就比较麻烦,于是,C++11
就增加了几个新的转换函数:stoi()、stol()、stoll() 等把字符串转换成整数;
stof()、stod() 等把字符串转换成浮点数;to_string() 把整数、浮点数转换成字符串。
C++11 为方便使用字符串,新增了一个字面量的后缀“s”,明确地表示它是 string 字符
串类型,而不是 C 字符串,这就可以利用 auto 来自动类型推导,而且在其他用到字符串
的地方,也可以省去声明临时字符串变量的麻烦,效率也会更高:
auto str = "std string"s; // 后缀s,表示是标准字符串,直接类型推导
正则表达式
正则
三分天下的容器:恰当选择,事半功倍
- 标准容器可以分为三大类,即顺序容器、有序容器和无序容器;
- 所有容器中最优先选择的应该是 array 和 vector,它们的速度最快,开销最低;
- list 是链表结构,插入删除的效率高,但查找效率低;
- 有序容器是红黑树结构,对 key 自动排序,查找效率高,但有插入成本;
- 无序容器是散列表结构,由 hash 值计算存储位置,查找和插入的成本都很低;
- 有序容器和无序容器都属于关联容器,元素有 key 的概念,操作元素实际上是在操作
key,所以要定义对 key 的比较函数或者散列函数。
不要在手写for循环了
迭代器和指针类似,也可以前进和后退,但你不能假设它一定支持“++”“–”操作符,
最好也要用函数来操作,常用的有这么几个:
distance(),计算两个迭代器之间的距离;
advance(),前进或者后退 N 步;
next()/prev(),计算迭代器前后的某个位置。
1 array<int, 5> arr = {0,1,2,3,4}; // array静态数组容器
2
3 auto b = begin(arr); // 全局函数获取迭代器,首端
4 auto e = end(arr); // 全局函数获取迭代器,末端
5
6 assert(distance(b, e) == 5); // 迭代器的距离
7
8 auto p = next(b); // 获取“下一个”位置
9 assert(distance(b, p) == 1); // 迭代器的距离
10 assert(distance(p, b) == -1); // 反向计算迭代器的距离
11
12 advance(p, 2); // 迭代器前进两个位置,指向元素'3' 13 assert(*p == 3); 14 assert(p == prev(e, 2)); // 是末端迭代器的前两个位置
vector<int> v = {1,2,6,4,6};
for(auto& i : v) {
std::cout << i << ",";
}
auto print = [](const auto& x){
std::cout << x << ",";
};
std::for_each(v.begin(),v.end(),print);
std::for_each(cbegin(v),cend(v),[](const auto& x){
std::cout << x << ",";
});
十面埋伏的并发
仅调用一次
这个功能用起来很简单,你要先声明一个 once_flag 类型的变量,最好是静态、全局的
(线程可见),作为初始化的标志:
然后调用专门的 call_once() 函数,以函数式编程的方式,传递这个标志和初始化函数。这
样 C++ 就会保证,即使多个线程重入 call_once(),也只能有一个线程会成功运行初始
化。
static std::once_flag flag;
int main(){
auto f = [](){
std::call_once(flag,[](){
std:: cout << "only once" << std::endl;
});
};
//f();
std::thread f_func (f);
std::thread f_func1 (f);
f_func.join();
f_func1.join();
}
注意 是call_once传入的函数 只执行一次 而不是f
线程局部存储
由关键字 thread_local 实现,有 thread_local 标记的变量在每个线程里都会有一个独立的副本,是“线程独占”的,所以就不会有竞争读写的问题,
thread_local int n = 0;
auto func = [&](int x){
n += x;
std::cout << n << std::endl;
};
std::thread tFunc(func,10);
std::thread tFunc_1(func,20);
tFunc.join();
tFunc_1.join();
结果: 10 20
以试着把变量的声明改成 static,再运行一下。这时,因为两个线程共享变量,所以 n
就被连加了两次,最后的结果就是 30。
原子变量
这在多线程编程里早就有解决方案了,就是互斥量(Mutex)。但它的成本太高,所以,
对于小数据,应该采用“原子化”这个更好的方案。
1 using atomic_bool = std::atomic<bool>; // 原子化的bool
2 using atomic_int = std::atomic<int>; // 原子化的int
3 using atomic_long = std::atomic<long>; // 原子化的long
原子变量禁用了拷贝构造函数,所以在初始化的时候不能用“=”的赋值形式,只能用圆括号或者花括号
强烈不建议你自己尝试去写无锁数据结构
调用函数 async(),它的含义是“异步运行”一个任务,隐含的动作是启动一
个线程去执行,但不绝对保证立即启动(也可以在第一个参数传递 std::launch::async,要
求立即启动线程)。
大多数 thread 能做的事情也可以用 async() 来实现,但不会看到明显的线程:
1 auto task = [](auto x) // 在线程里运行的lambda表达式
2 {
3 this_thread::sleep_for( x * 1ms); // 线程睡眠
4 cout << "sleep for " << x << endl;
5 return x;
6 };
7
8 auto f = std::async(task, 10); // 启动一个异步任务
9 f.wait(); // 等待任务完成
10
11 assert(f.valid()); // 确实已经完成了任务
12 cout << f.get() << endl; // 获取任务的执行结果
其实,这还是函数式编程的思路,在更高的抽象级别上去看待问题,异步并发多个任务,让
底层去自动管理线程,要比我们自己手动控制更好(比如内部使用线程池或者其他机制)。
async() 会返回一个 future 变量,可以认为是代表了执行结果的“期货”,如果任务有返
回值,就可以用成员函数 get() 获取。
不过要特别注意,get() 只能调一次,再次获取结果会发生错误,抛出异常
std::future_error。(至于为什么这么设计我也不太清楚,没找到官方的解释)
另外,这里还有一个很隐蔽的“坑”,如果你不显式获取 async() 的返回值(即 future 对
象),它就会同步阻塞直至任务完成(由于临时对象的析构函数),于是“async”就变成
了“sync”
序列化:简单通用的数据交换格式有哪些
序列化,就是把内存里“活的对象”转换成静止的字节序列,便于存储和网络传输;而反序
列化则是反向操作,从静止的字节序列重新构建出内存里可用的对象。
- JSON 是纯文本,容易阅读,方便编辑,适用性最广;
- MessagePack 是二进制,小巧高效,在开源界接受程度比较高;
- ProtoBuffer 是工业级的数据格式,注重安全和性能,多用在大公司的商业产品里。
网络通信:我不想写原生Socket
- libcurl 是一个功能完善、稳定可靠的应用层通信库,最常用的就是 HTTP 协议;
- cpr 是对 libcurl 的 C++ 封装,接口简单易用;
- libcurl 和 cpr 都只能作为客户端来使用,不能编写服务器端应用;
- ZMQ 是一个高级的网络通信库,支持多种通信模式,可以把消息队列功能直接嵌入应用
程序,搭建出高效、灵活、免管理的分布式系统。
脚本语言:搭建高性能的混合系统
- C++ 高效、灵活,但开发周期长、成本高,在混合系统里可以辅助其他语言,编写各种
底层模块提供扩展功能,从而扬长避短; - pybind11 是一个优秀的 C++/Python 绑定库,只需要写很简单的代码,就能够把函
数、类等 C++ 要素导入 Python; - Lua 是另一种小巧快速的脚本语言,它的兼容项目 LuaJIT 速度更快;
- 使用 LuaBridge 可以导出 C++ 的函数、类,但直接用 LuaJIT 的 ffi 库更好;
- 使用 LuaBridge 也可以很容易地执行 Lua 脚本、调用 Lua 函数,让 Lua 跑在 C++
里。
性能分析:找出程序的瓶颈
- 最简单的性能分析工具是 top,可以快速查看进程的 CPU、内存使用情况;
- pstack 和 strace 能够显示进程在用户空间和内核空间的函数调用情况;
- perf 以一定的频率采样分析进程,统计各个函数的 CPU 占用百分比;
- gperftools 是“侵入”式的性能分析工具,能够生成文本或者图形化的分析报告,最直
观的方式是火焰图。