《 罗剑锋的C++实战笔记 》

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


生命周期和编程范式

C++的五种范式

  1. 面向过程
  2. 面向对象
  3. 泛型编程
  4. 模板元编程
  5. 函数式

秀出好的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”综合起来

  1. 变量、函数名和名字空间用 snake_case,全局变量加“g_”前缀;
  2. 自定义类名用 CamelCase,成员函数用 snake_case,成员变量加“m_”前缀;
  3. 宏和常量应当全大写,单词之间用下划线连接;
  4. 尽量不要用下划线作为变量的前缀或者后缀(比如 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,表示是标准字符串,直接类型推导

正则表达式
正则

三分天下的容器:恰当选择,事半功倍

在这里插入图片描述

  1. 标准容器可以分为三大类,即顺序容器、有序容器和无序容器;
  2. 所有容器中最优先选择的应该是 array 和 vector,它们的速度最快,开销最低;
  3. list 是链表结构,插入删除的效率高,但查找效率低;
  4. 有序容器是红黑树结构,对 key 自动排序,查找效率高,但有插入成本;
  5. 无序容器是散列表结构,由 hash 值计算存储位置,查找和插入的成本都很低;
  6. 有序容器和无序容器都属于关联容器,元素有 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”

序列化:简单通用的数据交换格式有哪些

序列化,就是把内存里“活的对象”转换成静止的字节序列,便于存储和网络传输;而反序
列化则是反向操作,从静止的字节序列重新构建出内存里可用的对象。

  1. JSON 是纯文本,容易阅读,方便编辑,适用性最广;
  2. MessagePack 是二进制,小巧高效,在开源界接受程度比较高;
  3. ProtoBuffer 是工业级的数据格式,注重安全和性能,多用在大公司的商业产品里。

网络通信:我不想写原生Socket

  1. libcurl 是一个功能完善、稳定可靠的应用层通信库,最常用的就是 HTTP 协议;
  2. cpr 是对 libcurl 的 C++ 封装,接口简单易用;
  3. libcurl 和 cpr 都只能作为客户端来使用,不能编写服务器端应用;
  4. ZMQ 是一个高级的网络通信库,支持多种通信模式,可以把消息队列功能直接嵌入应用
    程序,搭建出高效、灵活、免管理的分布式系统。

脚本语言:搭建高性能的混合系统

  1. C++ 高效、灵活,但开发周期长、成本高,在混合系统里可以辅助其他语言,编写各种
    底层模块提供扩展功能,从而扬长避短;
  2. pybind11 是一个优秀的 C++/Python 绑定库,只需要写很简单的代码,就能够把函
    数、类等 C++ 要素导入 Python;
  3. Lua 是另一种小巧快速的脚本语言,它的兼容项目 LuaJIT 速度更快;
  4. 使用 LuaBridge 可以导出 C++ 的函数、类,但直接用 LuaJIT 的 ffi 库更好;
  5. 使用 LuaBridge 也可以很容易地执行 Lua 脚本、调用 Lua 函数,让 Lua 跑在 C++
    里。

性能分析:找出程序的瓶颈

  1. 最简单的性能分析工具是 top,可以快速查看进程的 CPU、内存使用情况;
  2. pstack 和 strace 能够显示进程在用户空间和内核空间的函数调用情况;
  3. perf 以一定的频率采样分析进程,统计各个函数的 CPU 占用百分比;
  4. gperftools 是“侵入”式的性能分析工具,能够生成文本或者图形化的分析报告,最直
    观的方式是火焰图。
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值