重温 C/C++ 笔记

工作里用到 C/C++ 越来越多,花了些时间加强。

本文主要是学习极客时间的 C++ 课程笔记。

弄懂这些位于“犄角旮旯”里的特性(无贬义),需要花费我们很多的脑力,但在我们一般的开发过程中,通常很少会触及这些点,或者说是会尽力避免,它们通常只是对编译器有意义,所以在这些“细枝末节”上下功夫就不是很值了,说白了,就是性价比太低。

我个人认为,在掌握了专栏里 C++11/14 知识的基础上,如果再面对一个 C++ 新的语言特性,你不能够在五分钟(或者再略长一点)的时间里理解它的含义和作用,就说明它里面的“坑”很深。

你应当采用“迂回战术”,暂时放弃,不要细究,把精力集中在现有知识的消化和理解上,练好“基本功”,等你以后真正不得不用它的时候,通过实践再来学习会更好。

一些细节点

  • 使用条件编译可以提早优化代码,产生最适合系统、编译环境的代码

  • “deprecated”属性只会导致编译警告,函数和类仍然可

  • 属性标签是由编译器负责解释的,自定义标签编译器无法识别。

  • const 常量也是变量,可以修改,但修改通常会被优化掉,无法直接体现

  • const 成员函数可以修改 mutable 成员变量,不是完全不可以修改变量

  • mutable 与 volatile 不冲突,但与 const 无法共存

  • “const int ”和“int const”的意思?

  • 前者是指向常量的指针,后者指向的是变量,但指针是常量

  • shared_ptr 的行为最接近原始指针,但不能滥用

  • shared_ptr 有少量的成本,而且有无法克服的循环引用风险,需要搭配 weak_ptr 才能获得最佳效果。

  • lambda 表达式不是函数是变量,但可以像函数一样被调用

  • 字符串的拷贝、修改代价比较高,应当尽量用 const string& 的方式来引用字符串

  • 原始指针可以拷贝,符合值语义,可以放进容器,但需要用户自己管理指针的生命周期

  • 只排序部分数据,最佳的算法是 partial_sort

  • 二进制格式与复杂数据结构无关,MessagePack 可以序列化任意数据类型,无论多复杂的结构。

  • top 只能看 CPU 和内存,其他指标要用别的工具(比如网络 I/O 看不了)。

  • 设计原则只是一些基本思想,所以没有具体的操作方式。

  • DRY 和 KISS 不涉及面向对象,更多地偏向代码编写规范

笔记

  • 类内部为什么不能用 auto
  • 无捕获的 lambda 才能转成函数指针?
  • g++ 参数都什么意思?
  • -E 是只输出预处理后的结果

02 编码阶段

编码风格:http://openresty.org/cn/c-coding-style-guide.html
在这里插入图片描述

03 预编译

在这里插入图片描述

C++ 预编译期间,通过各种宏和条件判断修改代码逻辑。
在这里插入图片描述

  • #if 必须得以 #endif 配对出现

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

内置的宏:

g++ -E -dM - < /dev/null    //查看内置的宏

在这里插入图片描述

基于它们,你就可以更精细地根据具体的语言、编译器、系统特性来改变源码,有,就用新特性;没有,就采用变通实现

  • __cplusplus: 目前使用的 c++ 标准,201402, 201103

04 编译阶段:属性和静态断言

属性:可以给变量、函数、类添加一个标签(类似注解),方便编译器识别处理。

用两对方括号的形式,方括号中间就是属性标签:[[noreturn]], [[depreacated]]

attribute :

  1. attribute 详解及应用
  2. https://gcc.gnu.org/onlinedocs/gcc/Attribute-Syntax.html

attribute 参数:

  1. constructor 在构造器之前执行
  2. destructor 在析构后执行
  3. cleanup

在这里插入图片描述

static_assert 运行在编译阶段,只能看到编译时的常数和类型,看不到运行时的变量、指针、内存数据等,

(__visibility__("default"): 某个符号是否导出

试想这样的情景,程序调用某函数A,A函数存在于两个动态链接库liba.so,libb.so中,并且程序执行需要链接这两个库,此时程序调用的A函数到底是来自于a还是b呢?
这取决于链接时的顺序,比如先链接liba.so,这时候通过liba.so的导出符号表就可以找到函数A的定义,并加入到符号表中,链接libb.so的时候,符号表中已经存在函数A,就不会再更新符号表,所以调用的始终是liba.so中的A函数。
为了避免这种混乱,所以使用__attribute__((visibility(“default”)))attribute((visibility(“hideen”))) 设置这个属性。

05 面向对象编程

https://time.geekbang.org/column/article/235301

  • C 语言中,也可以使用 struct 实现抽象和封装。
  • 建议设计时,少用继承和虚函数,多使用组合。

多重继承、纯虚接口类、虚析构函数,动态转型、对象切片、函数重载等很多危险的陷阱

在这里插入图片描述

  • C++ 用 : 表示继承,final 放在类名后面

C++ 四大函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值函数

为了减少创建对象成本,C++ 11 引入了右值 (Rvalue) 和转移(move)

  1. 转移构造函数
  2. 转移赋值函数

在这里插入图片描述

对于比较重要的构造、析构函数,可以使用 = default,让编译器生成默认实现。
= delete 表示明确禁用某个函数(非构造、析构也可以用),让外界无法调用

C++ 有隐式构造和隐式转型的规则。

隐式类型转换:类型 A 被自动转成表达式需要的类型 B。

彻底理解c++的隐式类型转换

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

两种类型别名:

using name = type
typedef type name

第一种比较直观。
如果类里有外部类型,名称比较长,可以定义个别名。

06 自动类型推导

  • auto:初始化时类型推导,总是“值”类型,没办法是引用类型
  • decltype, 表达式的方式计算类型,可以用于任何场合

“自动类型推导”实际上和“attribute”一样,是编译阶段的特殊指令,指示编译器去计算类型。所以,它在泛型编程和模板元编程里还有更多的用处。

auto 的“自动推导”能力只能用在“初始化”的场合。

赋值初始化或者花括号初始化(初始化列表、Initializer list),变量右边必须要有一个表达式(简单、复杂都可以)

很容易理解,只是声明,没有赋值,无法推导类型。

另外,auto 也不能用于类成员变量:
在这里插入图片描述

但可以使用 using + decltype 的方式,简化类变量的类型声明:
在这里插入图片描述

除了简化代码,auto 还避免了对类型的“硬编码”。方便后续修改。

自动类型推导”实际上和“attribute”一样(第 4 讲),是编译阶段的特殊指令,指示编译器去计算类型。所以,它在泛型编程和模板元编程里还有更多的用处

auto 用于有表达式的时候,decltype 用于没有表达式,但先确定类型的情况。

在这里插入图片描述

上图里的名称写错了,*p 是取值符。

一个数取地址,得到的类型就是一个指针:

在这里插入图片描述
在这里插入图片描述

使用 auto 可以简化遍历的方式,使用 rang-for-loop

07 常量变量:const/volatile/mutable

在这里插入图片描述

const 其实是只读变量,编译保证了不会被修改。

  • const
  • 可以修改引用和指针,const& 是函数参数的最佳选择
  • 修饰成员函数的话,这个函数只能访问常量变量;const 函数,实际上是传入一个 const this
  • const 常量在预处理阶段不存在,在运行阶段才出现。
  • 编译器看到 const 常量会做一些优化,比如把这个变量直接换成对应的值
  • 只读指针对象,调用它的方法,只能调用 const 的
  • const_cast,去常量化,变成普通变量
  • volatile
  • 表示变量会随时会被修改,禁止编译器优化,应该少用
  • mutable
  • volatile 可以用来修饰任何变量,而 mutable 却只能修饰类里面的成员变量,表示变量即使是在 const 对象里,也是可以修改的
  • mutable 像是 C++ 给 const 对象打的一个“补丁”,让它部分可变

在这里插入图片描述

既然对象是 const,那么它所有的相关操作也必然是 const。
在这里插入图片描述
在这里插入图片描述

const_cast,去常量化,变成普通变量

在这里插入图片描述

08 智能指针

#include <memory>

智能指针,使用代理模式,利用 RAII 技术代替裸指针(重载了 * 和 -> 操作符,所以使用和裸指针一样),能够在指针对象析构时自动释放内存。用于替代手动创建裸指针和手动释放内存。

什么是 RAII 技术:

  • Resource Acquisition Is Initialization
  • 利用对象生命周期控制对资源的使用

函数内部的一些成员是放置在栈空间上的,当函数返回时,这些栈上的局部变量就会立即释放空间,于是Bjarne Stroustrup就想到确保能运行资源释放代码的地方就是在这个程序段(栈)中放置的对象的析构函数了。
RAII就利用了栈里面的变量的这一特点。
RAII 的一般做法是这样的:
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

指针与引用的区别?

  • 指针是内存地址,引用是变量别名,指针可以是空,而引用不能为空(引用必须初始化,否则编译失败)
  • 引用是通过指针常量实现的

指针完全映射了计算机硬件,操作效率高,是 C++ 效率高的根源。
同时也导致了很多问题:访问无效地址,指针越界,内存没有即使释放等。
Java/Go 没有这方面问题。

  • unique_ptr
  • 独占使用的指针,不能直接赋值给其他指针,需要通过右值转移,转移后之前指针变成空指针
  • make_unique()
  • shared_ptr
  • 共享使用的指针
  • 内部使用了引用计数,use_count 可以查看有几个引用,引用为 0 时,才会 delete 内存
  • make_shared()
  • 还可以定制内存删除函数
  • 缺点:
  • 引用计数的存储和管理都是成本
  • 在运行阶段,引用计数的变动很复杂,很难知道它真正释放资源的时机
  • 对象的析构函数,不要有非常复杂、严重阻塞的操作。一旦 shared_ptr 在某个不确定时间点析构释放资源,就会阻塞整个进程或者线程
  • 可能遇到循环引用导致计数始终不为 0,无法 delete 内存
  • weak_ptr
  • 专门为打破循环引用设计,只观察引用,不计数
  • 可以调用 lock() 获取 shared_ptr

在这里插入图片描述
在这里插入图片描述

自定义智能指针工厂方法:

template<class T, class... Args>    //可变参数模板
std::unique_ptr<T>          //返回智能指针
my_make_unique(Args&&... args)      //可变参数模板的入口参数
{
    return std::unique_ptr<T>(  //构造智能指针
            new T(std::forward<Args>(args)...)  //转发参数
            );
}

09 exception

#include <stdexcept>

C++ 处理异常的方式:

  1. 判断函数返回值
  2. 判断全局错误码,errno
  • 业务逻辑和错误处理混在一起
  • 很容易被忽略,出现异常还继续执行,导致出现意料之外的情况
  1. 抛出、处理异常
  • 错误处理集中在 catch 代码块
  • 异常不能被忽略,必须处理,否则向上传播,直到被处理或者崩溃。不会带病工作
  • 使用范围更广,比如没有返回值的函数,出现异常

在这里插入图片描述
在这里插入图片描述

使用 noexcept 修饰不会抛出异常的函数,方便编译器做优化:
在这里插入图片描述

noexcept 的真正意思是:“我对外承诺不抛出异常,我也不想处理异常,如果真的有异常发生,请让我死得干脆点,直接崩溃(crash、core dump)。”
一般认为,重要的构造函数(普通构造、拷贝构造、赋值构造、转移构造)、析构函数,尽量声明为 noexcept,优化性能。

10 节 函数式编程

函数的目的:封装执行的细节,简化程序的复杂度。

面向对象编程:程序由一个个对象组成,彼此组合、通信完成任务;
函数式编程:程序由一个个函数组成,彼此组合、调用完成任务。

C++ 函数的特点:

  1. 没有类型,只能通过函数指针间接操作
  2. 函数都是全局的,没有生命周期的概念(static、namespace 只是限制了范围,避免名字重复)
  3. 函数里不能嵌套函数

C++ 中的 lambda:

  1. 是一个变量,可以赋值、嵌套
  2. 全新编程思维 — 函数式编程:把计算机程序的函数,等价于数学上的求解函数。
  3. 结合 auto 声明 lambda 类型的函数变量
  4. 捕获时注意外部变量的生命周期,小心失效

在这里插入图片描述

void test_lambda_2() {
    //4.嵌套 lambda 定义、调用
    auto open_website = [](string website)
    {
        string local_ip = "192.168.77.33";

        //按值捕获,指定捕获变量名
        auto dns = [local_ip](string website)
        {
            cout << "Transform website to ip: " << website << " to " << local_ip << endl;
        };
        auto tcp = [local_ip](string server)
        {
            cout << "Establish connect from " << local_ip << " to " << server << endl;
        };

        cout << "Your website is opening... " << website << endl;
        dns(website);
        tcp("202,184,293,23");
    };

    open_website("www.baidubaike.com");
}

和 JS 的闭包差不多
lambda 保存了定义时捕获的外部变量,就可以跳离定义点,把这段代码“打包”传递到其他地方去执行

在 C++ 里,每个 lambda 表达式都会有一个独特的类型,而这个类型只有编译器才知道,我们是无法直接写出来的,所以必须用 auto。

捕获外界变量:

  1. =:按值捕获所有外部变量,不可修改
  2. &:按引用捕获,可以修改,比较危险,可能出现被修改地址已经不可使用的情况!
  3. 也可以明确指定变量名和捕获方式

参数使用 auto 声明,可以让 lambda 表达式处理不同类型的参数:

//5.泛型的 lambda
void test_lambda_3() {
//参数使用 auto 声明,泛型化
    auto f = [](const auto& i) {
        cout << i+i << endl;
    };

    f(6);
    f(0.24f);
    string str = "hao";
    f(str);
}

用“map+lambda”的方式来替换难以维护的 if/else/switch,可读性要比大量的分支语句好得多。

11节 STL-字符串

template <class _CharT,             // for <stdexcept>
          class _Traits = char_traits<_CharT>,
          class _Allocator = allocator<_CharT> >
    class _LIBCPP_TEMPLATE_VIS basic_string;
typedef basic_string<char, char_traits<char>, allocator<char> > string;
typedef basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t> > wstring;
#ifndef _LIBCPP_HAS_NO_UNICODE_CHARS
typedef basic_string<char16_t> u16string;
typedef basic_string<char32_t> u32string;
#endif  // _LIBCPP_HAS_NO_UNICODE_CHARS
  • Unicode,统一的编码处理人类语言,使用 32 位(4个字节)容纳文字
  • C 里的 char 是单个字符,因此增加了 wchar_t
  • 后来又有了 char16_t ,适配 UTF-16
  • char32_t,适配 UTF-32

wstring 等新字符串基本上没人用,大多数程序员为了不“自找麻烦”,还是选择最基本的 string。
Unicode 还有一个 UTF-8 编码方式,与单字节的 char 完全兼容,用 string 也足以适应大多数的应用场合

  • 建议你只用 string,而且在涉及 Unicode、编码转换的时候,尽量不要用 C++,目前它还不太擅长做这种工作,可能还是改用其他语言来处理更好

  • 字符串后缀s是C++14的特性,C++11没有,需要升级gcc到5.4,然后启用C++14标准。
    在这里插入图片描述

  • *end = 0,这一句很重要,执行前,end 指向的是第一个换行符的位置;执行后,把换行符改成了 0

  • 这时操作的是 line 指向的内存数据,因此 line 也被截断了


void test_other_string_api() {
    //单纯保存的是字符序列,使用 vector<char>
    vector<char> char_array;

    //字面量后缀, s 后缀,表示是一个 string 类型
    auto str = "shixinzhang hahaha"s;

    auto not_raw_string = "zhangshi        \n                  xin hhha";
    //想直接输出各种转义符,使用 raw string
    auto raw_string = R"(zhangshi        \n                  xin hhha)";
    cout << not_raw_string << endl;
    cout << raw_string << endl;


    //字符串转数字  stoi, stof, stod
    auto test_integer = "444.4";
    cout << stoi(test_integer) << endl;
    cout << stof(test_integer) << endl;
    //to_string 数字转字符串
    cout << to_string(1024) << endl;

}

void test_string_func() {

    string name = "shixinzhang";
    cout << "\nlength: " << name.length() << "size:" << name.size() << endl;
    //c_str() 和 data() 都返回一个 const char* 指针,但 c_str() 会在末尾加一个 \0
    cout << "c_str strlen: " << strlen((name.c_str())) << endl;
    cout << "data strlen: " << strlen((name.data())) << endl;

    cout << "substr:" << name.substr(0, 5) << endl;
    //既可以查找一个子字符串,也可以查找某个字符(如果有多个,返回的是第一个)
    cout << "find:" << name.find("shi") << " , " << name.find('i') << endl;

    //也支持查找第一个和最后一个字符
    cout << "find_first_of:" << name.find_first_of('n') << endl;
    cout << "find_first_not_of:" << name.find_first_not_of('z') << endl;
    cout << "find_last_of:" << name.find_last_of('i') << endl;
    cout << "find_last_not_of:" << name.find_last_not_of('n') << endl;

}

void test_string_reg() {
    auto make_regex = [](const string& str){
        return std::regex(str);
    };

    auto make_match = [](){
        return std::smatch();
    };

    //1.创建字符串, s 表明类型
    auto str = "name:shixin-2016"s;
    //2.创建正则表达式
    auto reg = make_regex(R"(^(\w+):(\w+)-(\w+)$)");
    //3.创建存储结果的 match
    auto match = make_match();

    //4.调用匹配
    regex_match(str, match, reg);

    cout << "regex result:" << match.size() << "\n" << endl;
    //匹配结果可以像容器那样被访问,第0个是整个匹配的内容,后面的匹配的多个子串
    for (const auto&x: match) {
        cout << x << " , ";
    }
    cout << endl;
    cout << "Parsed name: " << match[2] << endl;
    cout << "Parsed age: " << match[3] << endl;
}

void StringTest::test() {
    test_string_func();
    test_other_string_api();
    test_string_reg();
}

字符串相关 stl 方法

  1. strchr,查找字符串里某个字符的位置,返回对应位置的指针
  2. strlen, 获取某个字符串指针的长度

12节 STL-容器

#include <vector>
#include <list>
#include <set>
#include <map>
#include <deque>
#include <forward_list>

容器里存储的是元素的拷贝、副本,而不是引用,尽量为元素实现转移构造和转移赋值函数,在加入容器的时候使用 std::move() 来“转移”,减少元素复制的成本(测试一下内存占用)

  • 数组
  • array 固定长度
  • vector 动态数组,扩容时×2
  • deque 双端队列,也可以扩容,可以在 2 端高效的添加、删除(vector 只能往后面插入)
  • 链表
  • list:双端链表
  • forward_list:单向链表

array 和 vector 直接对应 C 的内置数组,内存布局与 C 完全兼容,所以是开销最低、速度最快的容器。

有序容器

  • set
  • map
  • multiset/multimap,multi 表示 key 可以重复
class Student {
public:
    Student(int age) : age(age) {}

    //const 函数,不修改成员函数
    int operator+(int v) const {
        return this->age + v;
    }

    //return-type operator操作符(参数)函数题
    //Overloaded 'operator<' must be a binary operator (has 3 parameters)
    friend bool operator<(const Student& a, const Student& b){
        //这里的顺序,就是有序容器里的顺序(大的在前)
        return a.age > b.age; // 自定义比较运算
    }

    friend ostream& operator << (ostream & os, const Student& s)
    {
        os << "student: " << s.age;
        return os;
    }
public:
    int age;
};


void test_order_collection() {
    //set, map, multiset, multimap

    auto comparator = [](const Student&a, const Student&b) {
        return a.age > b.age;
    };

    //1.自定义类,需要重载比较 < 操作符,才能放入 set
    std::set<Student> students ;
    //1.2 或者自定义比较器
//    std::set<Student, decltype(comparator)> students ;

    //emplace 可以直接构造元素,免去了构造后再拷贝、转移的成本
    students.emplace(12); //无需 new Student(12)
    //每次插入都会排序,如果数量很大时,需要考虑是否合理
    students.emplace(16);
    students.emplace(13);

    //2.要输出内容,需要重载 << 操作符
    for(const Student&x: students) {
        std::cout << x << " , addr:" <<&x << " , size: " << sizeof(x) << std::endl;
    }
}

无序容器

  • unordered_set/unordered_map
  • 无序容器用 hash 表实现,而不是红黑树
  • 需要重载= 和 hash 函数(敲一下)

Overloaded ‘operator<’ must be a binary operator (has 3 parameters) 为什么重载 < 需要加 friend

https://www.zhihu.com/question/44865154

2 点:

  1. friend 友元函数可以访问私有方法
  2. 规定:重载一个二元的全局运算符,需要声明这个函数为友元
  • < 比较运算符是个“二元运算符”

在这里插入图片描述

13节 STL-算法

https://en.cppreference.com/w/cpp/header/iterator

https://en.cppreference.com/w/cpp/header/algorithm

stl 算法操作的是迭代器,这是泛型编程,分离数据和操作

通过容器的 begin() end() 可以获取指向两个端的迭代器(cbegin() 返回的是常量迭代器);也有通用函数:std::begin(容器对象)。



void test_iterator() {

    typedef std::array<int, 6> my_array;
    my_array a = {0,1,2,3,4, 4};

    cout << "print array:" << endl;
    for (auto iterator = a.begin(); iterator != a.end(); iterator++) {
        //迭代器是个指针
        cout << *iterator << " , ";
    }
    cout << endl;

    //全局函数获取迭代器
    auto begin = std::begin(a);
    auto end = std::end(a);
    //逆序迭代
    auto reverse_begin = std::rbegin(a);
    
    //返回的是常量迭代器
    auto const_begin = std::cbegin(a);

    //distance, 计算 2 个迭代器之间的距离
    cout << "Distance of two iterator: " << std::distance(begin, end) << endl;
    //获取下一步,不修改参数
    begin = std::next(begin);
    cout << "Distance of two iterator: " << std::distance(begin, end) << endl;
    //往前走几步,修改参数
    std::advance(begin, 2);
    cout << "Distance of two iterator: " << std::distance(begin, end) << endl;
}

void test_algorithm() {
    typedef std::array<int, 6> my_array;
    my_array a = {4,2,1,3,5, 4};
    cout << a[1] << endl;

    auto begin = std::begin(a);
    auto end = std::end(a);

    //1.统计满足条件的个数
    //结合 lambda 表达式写的很 easy
    auto n =std::count_if(begin, end, [](auto x){
        return x > 1;
    });

    cout << "Number of item which larger than 1 :" << n << endl;

    //2.二分查找,返回的是是否存在
    auto binary_search_result = std::binary_search(begin, end, 44);
    cout << "binary_search_result: " << binary_search_result << endl;

    //3.快速排序
    std::sort(begin, end);

    //4.另一种遍历方式,可以自定义每个 item 的输出内容
    cout << "test_for_each" << endl;
    //std::for_each 把要做的事情分成了两部分:一个遍历容器元素,另一个操纵容器元素
    int index = 0;
    std::for_each(begin, end, [&index](const auto&x){
        if (x == 4) {
            //可以通过修改捕获的参数的地址,来向外传递数据
            index++;
        }
        cout << x << " ; ";
    });
    cout << "4 count: " << index << endl;
}


算法

  1. 用 for_each 代替手写的 for 循环(可以传入 lambda 表达式,逻辑更清晰)
  2. 很多排序算法(topN等),记在脑图里

https://time.geekbang.org/column/article/243357
要求排序后仍然保持元素的相对顺序,应该用 stable_sort,它是稳定的;
选出前几名(TopN),应该用 partial_sort;
选出前几名,但不要求再排出名次(BestN),应该用 nth_element;
中位数(Median)、百分位数(Percentile),还是用 nth_element;
按照某种规则把元素划分成两组,用 partition;第一名和最后一名,用 minmax_element。
最好在顺序容器 array/vector 上调用!

查找:

  • lower_bound (https://en.cppreference.com/w/cpp/algorithm/lower_bound),返回第一个大于等于参数的位置
  • upper_bound,返回第一个大于的位置

它俩的返回值构成一个区间,这个区间往前就是所有比被查找值小的元素,往后就是所有比被查找值大的元素:
begin < x <= lower_bound < upper_bound < end

还有很多有用的方法,类似 memcpy 的 copy/move 算法(搭配插入迭代器)、检查元素的 all_of/any_of 算法,用好了都可以替代很多手写 for 循环。

14节 并发

一个最基本但也最容易被忽视的常识:“读而不写”就不会有数据竞争
多用 const 关键字,尽可能让操作都是只读的,为多线程打造一个坚实的基础。

  • 仅调用一次:call_flag,call_once(),可以实现类似单例的操作,避免发生并发初始化的问题
  • 线程局部变量:thread_local,用于非共享数据
  • 互斥量:Mutex,避免同时写;成本太高
  • 原子变量:atomic_bool ,数据修改涉及较少的,可以使用原子变量
  • 只能有基本类型的(std::atomic<int>) 和一个 flag std::atomic_flag
  • 原子变量禁用了拷贝构造,所以初始化时不能用 = 赋值的方式
  • 把原子变量当作线程安全的全局计数器或者标志位
  • async(),异步执行一个任务,返回一个 future,通过 get() 获取结果

lock_guard, condition_variable, promise 等需要自己去查

c++20 引入了协程,用户态的线程,创建、切换成本低,开销更低,性能更好

#include <iostream>
#include <string>
#include <thread>

#include <unistd.h>
#include <future>

using namespace std;

//仅调用一次
//最好是静态、全局的(线程可见)
static std::once_flag flag;

//线程局部变量
thread_local int n = 0;

void test_once_flag() {
    auto thread_print = []() {
        //通过 std::call_once 调用,参数 flag 是否初始化作为是否执行
        std::call_once(flag, []() {
            cout << "thread_print:"<< endl;
        });
    };

    //启动一个线程,执行参数函数
    thread t(thread_print);
    thread t2(thread_print);

    t.join();
    t2.join();
}

void test_thread_local() {
    //按值捕获外部参数,内部要修改
    auto f = [&](int p) {
        n += p;
        cout << this_thread::get_id() << " thread run: " << n << endl;
    };

    //启动一个线程,执行参数函数
    thread t(f, 10);
    thread t2(f, 20);
    cout << "test_thread_local >>> " << endl;

    //等待执行完
    t.join();
    t2.join();
}

//原子标记位
static atomic_flag flag_a(false);

void test_atomic() {
    //atomic 禁用了拷贝构造函数,所以不能 = 赋值,而需要圆/花括号赋值
    std::atomic<int> a{0};
//    std::atomic<int> b(1);

    auto f = [&](int sleep_millis) {
        //修改标记位,通过标记位进行同步
        auto set = flag_a.test_and_set();
        if (set) {
            cout << "flag has been set" << endl;
        } else {
            cout << "flg set by " << this_thread::get_id() << endl;
        }
        //时间字面量?
        this_thread::sleep_for(sleep_millis * 100ms);

        //修改
        a.store(1);
        //获取
        cout << "load a: " << a.load() << endl;
    };

    thread thread_a(f, 5);
    thread thread_b(f, 7);

    thread_a.join();
    thread_b.join();
}

void test_async() {
    auto task = [](const auto& x) {
        this_thread::sleep_for(x * 1ms);
        cout << this_thread::get_id() << " sleep for " << x << endl;
        return x * 2;
    };

    //不获取返回值的话,就变成了同步?!
    std::async(task, 100);

    //启动一个异步任务,不保证立刻执行
    //返回一个 future
    auto f = std::async(task, 100);

    //等待执行结束
    f.wait();

    //获取结果, get 只能调用一次?
    cout << "valid ? " << f.valid() << endl;
    cout << "get ? " << f.get() << endl;
    cout << "get 2 ? " << f.get() << endl;
}

void ConcurrentTest::run() {
    test_once_flag();
    test_thread_local();
    test_atomic();
    test_async();
}

15 节 序列化

https://time.geekbang.org/column/article/245880

  • c++如何添加第三方依赖库?
  • g++ -I 包含查找路径?

json, messagePack, pb 都敲一下

为什么需要序列化,不能直接 memcpy 吗?
直接memcpy,同一种语言不同机器,或者不同语言可能存在兼容问题(变量内存存储布局、编码可能不同),而Json是一种标准,且不同语言间统一

json不存在大小端,字节序的问题吧,反正就是一个字符串。messagepack这种二进制格式的东西,才要考虑大小端。

16 节 网络通信

使用 libcurl 进行网络请求的四个步骤:初始化句柄、设置参数、发送请求、清理句柄

(无捕获的 lambda 可以转成函数指针)

  • cpr:对 libcurl 的封装
  • ZMQ:无阻塞传递海量数据;消息队列,可以用于高并发场景

看的时候可以先从代码风格看起,再熟悉C++关键字的用法,再到整体架构、接口设计。不能心急,不要想着几天或者一个月就能看完。而且也没必要完全看懂,只要能从中学到一两个点就可以说是值得了。

17 节 和脚本语言混合使用

写 Lua 扩展模块的时候,内部可以用 C++,但对外的接口必须转换成纯 C 函数(extern “C”)。

C++ 高效、灵活,但开发周期长、成本高,在混合系统里可以辅助其他语言,编写各种底层模块提供扩展功能,从而扬长避短;

Python 很“大众”,但比较复杂、性能不是特别高;而 Lua 比较“小众”,很小巧,有 LuaJIT 让它运行速度极快。你可以结合自己的实际情况来选择,比如语言的熟悉程度、项目的功能 / 性能需求、开发的难易度,等等。

从零开始学C++,我的建议是不要去抠那些内存管理、指针、构造/析构等细枝末节,先把C++当做java、Python来用,有了一些实际经验体会后再了解底层机制。
C++偏向在底层写高性能组件,Python实现业务逻辑,而服务器应用一般业务比较多,这样就难以发挥C++

18 节 性能分析

在运行阶段能做、应该做的事情主要有三件:调试(Debug)、测试(Test)和性能分析(Performance Profiling)

GDB:让高速的 CPU 慢下来,让我们可以理清程序的状态

什么是性能分析呢?

你可以把它跟 Code Review 对比一下。Code Review 是一种静态的程序分析方法,在编码阶段通过观察源码来优化程序、找出隐藏的 Bug。而性能分析是一种动态的程序分析方法,在运行阶段采集程序的各种信息,再整合、研究,找出软件运行的“瓶颈”,为进一步优化性能提供依据,指明方向。

性能分析的关键是测量,而测量就需要使用工具

  • Linux 系统自己就内置了很多用于性能分析的工具,比如 top、sar、vmstat、netstat,等等。但是,Linux 的性能分析工具太多、太杂,有点“乱花渐欲迷人眼”的感觉,想要学会并用在实际项目里,不狠下一番功夫是不行的。
  • top 还可以按 M 和 P 查看指定内容?!
  • pstack:查看进程的调用栈信息(静态数据)【试一下】
  • strace:系统正在进行的系统调用(念哥分析 PerfDog 就是通过这个?)

把 pstack 和 strace 结合起来,你大概就可以知道,进程在用户空间和内核空间都干了些什么。当进程的 CPU 利用率过高或者过低的时候,我们有很大概率能直接发现瓶颈所在。

在这里插入图片描述
在这里插入图片描述

perf 可以说是 pstack 和 strace 的“高级版”,它按照固定的频率去“采样”,相当于连续执行多次的 pstack,然后再统计函数的调用次数,算出百分比。只要采样的频率足够大,把这些“瞬时截面”组合在一起,就可以得到进程运行时的可信数据,比较全面地描述出 CPU 使用情况。

在这里插入图片描述

gperftools 是“侵入”式的性能分析工具,能够生成文本或者图形化的分析报告,最直观的方式是火焰图。

ASAN,检测内存问题

19 20 设计模式

在这里插入图片描述

在这里插入图片描述

经典的《设计模式》一书里面介绍了 23 个模式,并依据设计目的把它们分成了三大类:创建型模式、结构型模式和行为模式。
这三类模式分别对应了开发面向对象系统的三个关键问题:如何创建对象、如何组合对象,以及如何处理对象之间的动态通信和职责分配。解决了这三大问题,软件系统的“架子”也就基本上搭出来了。

创建,结构,行为

模式里的结构和实现方式直接表现为代码,可能是最容易学习的部分,但我认为,其实这些反而是最不重要的。
你更应该去关注它的参与者、设计意图、面对的问题、应用的场合、后续的效果等代码之外的部分,它们通常比实现代码更重要。

因为代码是“死”的,只能限定由某几种语言实现,而模式发现问题、分析问题、解决问题的思路是“活”的,适用性更广泛,这种思考“What、Where、When、Why、How”并逐步得出结论的过程,才是设计模式专家经验的真正价值。

21 节 实战项目

在这里插入图片描述

1.头文件
include guard(其实就是判断 #ifndef xx, #define xx, #endif)
通过判断内置属性,提供兼容处理的方法的宏,比如 deprecated 和 static_assert

2.自旋锁的头文件
类型别名,禁止拷贝构造和赋值函数,通过自旋重试、原子变量的 TAS 来判断获得锁

自定义的 LockGuard,用于在析构函数里 unlock

使用原子变量(atomic)可以实现自旋锁,比互斥量的成本要低,更高效

C++要求静态成员变量必须在cpp文件里定义实现,头文件里只是声明。
而在静态成员函数里声明静态变量,再以函数返回值的形式来使用,就不需要在cpp里重复写一遍,只要在头文件里写就行了。

22 节 实战项目 2

1.类名使用 CamelCase,函数和变量用 snake_case,成员变量加“m_”前缀

在编译阶段使用静态断言,保证整数、浮点数的精度

使用 final 终结类继承体系,不允许别人产生子类

使用 default 显示定义拷贝构造、拷贝赋值、转移构造、转移赋值等重要函数

using 定义类型别名

使用 noexcept 标记不抛出异常,优化函数

使用 const 来修饰常函数;

2.【SalesData】 跟着敲一下

代码里显式声明了转移构造和转移赋值函数,这样,在放入容器的时候就避免了拷贝,能提高运行效率。

3.MessagePack

小巧轻便,而且用起来也很容易,只要在类定义里添加一个宏,就可以实现序列化:public: MSGPACK_DEFINE(m_id, m_sold, m_revenue); // 实现MessagePack序列化功能

4.Summary 类

关联了好几个类的核心类

类型别名对于它来说就特别重要,不仅可以简化代码,也方便后续的维护

锁不影响类的状态,所以要用 mutable 修饰??

lockGuard 使用很简单,申明一个全局 lock 变量,然后在访问数据的函数里,创建临时的 lockGuard(参数是这个 lock)。获取到 lock 时才会继续执行,然后在走出代码块后,就会析构。简单、安全

在使用 lambda 表达式的时候,要特别注意捕获变量的生命周期,如果是在线程里异步执行,应当尽量用智能指针的【值】捕获,虽然有点麻烦,但比较安全

5.搭建 http 服务

介绍及Windows:https://time.geekbang.org/column/article/100124
Mac/Linux:https://time.geekbang.org/column/article/146833
OpenResty 使用介绍:https://www.runoob.com/w3cnote/openresty-intro.html


问题记录

C++ undefined reference:

1.这个符号的确不存在

  • 检查依赖的 so 有没有添加到 target_link_libraries 里

2.符号存在,但命名规则不对(C 与 C++ 不同)

  • C++ 调用 C 方法,需要在 #include 前,用 extern “C” {} 包围

Android NDK 报错:undefined reference to ‘main‘(invalid character)解决办法

今天遇到一个奇怪的问题,报错如下:

* What went wrong:
Execution failed for task ':profmancompat:externalNativeBuildRelease'.
> Build command failed.
  Error while executing process /Users/simon/Library/Android/sdk/cmake/3.10.2.4988404/bin/ninja with arguments {-C /Users/simon/AndroidStudioProjects/profmancompat/profmancompat/.cxx/cmake/release/armeabi-v7a profmancompat-lib}
  ninja: Entering directory `/Users/simon/AndroidStudioProjects/profmancompat/profmancompat/.cxx/cmake/release/armeabi-v7a'
  [1/2] Building CXX object CMakeFiles/profmancompat-lib.dir/profmancompat.cpp.o
  [2/2] Linking CXX executable profmancompat-lib
  FAILED: profmancompat-lib 
  : && /Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++ --target=armv7-none-linux-androideabi16 --gcc-toolchain=/Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64 --sysroot=/Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot  -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -march=armv7-a -mthumb -Wformat -Werror=format-security   -std=c++11 -g -Oz -DNDEBUG  -Wl,--exclude-libs,libgcc_real.a -Wl,--exclude-libs,libatomic.a -static-libstdc++ -Wl,--build-id -Wl,--fatal-warnings -Wl,--exclude-libs,libunwind.a -Wl,--no-undefined -Qunused-arguments -Wl,--gc-sections CMakeFiles/profmancompat-lib.dir/profmancompat.cpp.o  -o profmancompat-lib -L/Users/simon/AndroidStudioProjects/profmancompat/profmancompat/src/main/cpp/../jniLibs/armeabi-v7a -lprofman-slib -lprofman-29 -llog -latomic -lm && :
  /Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9.x/../../../../arm-linux-androideabi/bin/ld: error: CMakeFiles/profmancompat-lib.dir/profmancompat.cpp.o:1:3: invalid character
  /Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9.x/../../../../arm-linux-androideabi/bin/ld: error: CMakeFiles/profmancompat-lib.dir/profmancompat.cpp.o:1:3: syntax error, unexpected $end
  /Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9.x/../../../../arm-linux-androideabi/bin/ld: error: CMakeFiles/profmancompat-lib.dir/profmancompat.cpp.o: not an object or archive
  /Users/simon/Library/Android/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/arm-linux-androideabi/16/crtbegin_dynamic.o:crtbegin.c:function _start_main: error: undefined reference to 'main'
  clang++: error: linker command failed with exit code 1 (use -v to see invocation)
  ninja: build stopped: subcommand failed.

重点是这几句:

  1. profmancompat.cpp.o:1:3: invalid character
  2. profmancompat.cpp.o:1:3: syntax error, unexpected $end
  3. crtbegin_dynamic.o:crtbegin.c:function _start_main: error: undefined reference to ‘main’

搜了下关键字,回答基本上是 main 定义的问题,和我实际情况不符。

在尝试把代码里无关的字符删除后,还是不行,那报错信息里的字符究竟是哪儿来的呢?

后来盯着 CMakeList,看到这些编译、link 优化项,心想也没有可能是这些的配置导致的:

add_compile_options(-Oz -flto -ffunction-sections -fdata-sections -fexceptions -frtti)
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto -Wl,--exclude-libs,ALL -Wl,--gc-sections -Wl,--no-fatal-warnings")

从 https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html 里查到,-flto 大概的作用就是:在编译时,会在生成的目标文件里,插入一个特殊的格式信息(GIMPLE 格式)。然后在链接时,读取多个目标文件里的 GIMPLE 信息,合并成一个。这样 gcc 就能做一些内联优化,从而减少最终生成物体积。

在这里插入图片描述

那有没有可能是 -flto 优化过程中生成的信息导致了这个问题呢?

试着删除这个信息后,居然真的编译通过了!

这个问题耗费了些时间,虽然具体原因还不清楚,但希望遇到同样问题的同学,可以多一个尝试选择。

猜你喜欢

转载自blog.csdn.net/u011240877/article/details/109266047
今日推荐