C++小知识

-目录-

构造函数初始化列表

在函数下面直接冒号对成员变量赋值。

为什么要使用这个?

  1. 不是所有成员都可以通过形参赋值的,比如引用类型,const类型。
  2. 为了一些性能原因,对于类里面有声明类的话,编译器会先把里面声明的类构建一下,然后再进入构造函数类型,再调用赋值拷贝函数,然后再析构最初初始化的类。使用初始化列表后,就会在外面的类初始化的适合就直接把传入的参数给它了,就不会再初始化了。省去了3步:最初的构建函数,最初的构建好的类的析构函数,赋值拷贝函数。
int cnt = 0;
class MyClassB
{
public:
    MyClassB(){std::cout << "constructorB called" << cnt++ << std::endl;}
    ~MyClassB(){std::cout << "destructorB called" << cnt++ << std::endl;}
    MyClassB &operator=(MyClassB b)
    {
        std::cout << "operator= called" << cnt++ << std::endl;
        return *this;
    }
};

class MyClassA
{
public:
    MyClassA(MyClassB b)
        //或初始化列表 :bina(b)
    {
        bina=b;
        std::cout << "constructorA called" << cnt++ << std::endl;
    }
    ~MyClassA(){std::cout << "destructorA called" << cnt++ << std::endl;}
    MyClassB bina;
};

int main()
{
    MyClassB b;
    MyClassA a(b);
}

初始化列表输出:

constructorB called0 main.B构建
constructorA called1 main.A构建
destructorB called2  析构形参B
destructorA called3  析构main.A 对应1
destructorB called4  A.B 析构自己
destructorB called5  析构main.B 对应0

=赋值输出:标有*的是多的。

constructorB called0 main.B构建
* constructorB called1 A.B初始化构建
* operator= called2    A.B赋值
* destructorB called3  A.B初始化时的类析构 对应1
constructorA called4 main.A构建
destructorB called5  析构形参B
destructorA called6  main.A析构 对应4
destructorB called7  A.B析构
destructorB called8  main.B析构 对应0

模板

有了重载为什么需要模板?仅仅参数类型不同的情况下,重载还需要一个一个写。而模板传入类型,直接就可以用一个函数补齐所有类型了,这个过程叫做具体化。而且任何类型都可以,哪怕未知的类型。模板不会减少执行代码量,他会生成不同的代码,因此需要控制代码膨胀体积。

模板函数

用法:

template<typename T>//或template<class T>,二者完全相同
void func(T arg){}

模板函数同样可以重载。

template<typename T>
void func(T arg){}

//重新声明
template<typename T>
void func(T arg1, T arg2){}

具体化Specialization和实例化Instantiation

具体化Specialization和实例化Instantiation的区别?

模板先具体化类型确定签名,然后实例化生成代码。

具体化

具体化是指针对某些特定类型参数或条件,定义一份特殊实现的模板代码实现,类似于重载。

显式具体化

template<typename T>
class my_class {
    // 类模板成员定义
};

// 显式具体化
template<>
class my_class<int> { // 语法上,在实现的<>里明确类型, 用不到前面的就不要加
    // 对 int 类型的实现
};

// 显式具体化
template<typename T>
class my_class<T*> {
    // 对 T* 类型的实现
};

// 重载约等于显式具体化+实例化, 不要混用显式具体化和重载!
template<typename T>
void my_func(const T& t) {
    // 函数模板代码实现
}
void my_func(double t) {
    // 重载代码实现
}

隐式具体化

template<typename T>
void my_func(const T& t) {
    // 函数模板代码实现
}

int main() {
    int x = 42;
    my_func(x); // 隐式具体化+实例化,会自动生成 my_func<int>(const int&) 实例化版本
}

实例化

实例化是指将模板加上具体类型转化为具体的代码。

显式实例化

要求编译器在编译时将该模板独立地实例化出来,而不是运行时确定。在显式实例化时因为是指示编译器去生成代码,因此禁止定义。这里只能直接分号结尾。

template void my_func<int>(int); // 语法上,template后面没有<>
//在其他文件里 需要 extern template void my_func<int>(int); 
template void my_func<int>(int){}// 错误

隐式实例化

template<typename T>
void my_func(const T& t) {
    // 函数模板代码实现
}

int main() {
    int x = 42;
    my_func(x); // 隐式具体化+实例化,会自动生成 my_func<int>(const int&) 实例化版本
}

为什么需要显式具体化?

对于某些类型需要特例处理。

显式具体化和函数重载的区别?

函数重载的匹配优先级大于显式具体化。

优先级:函数重载>显式具体化>模板函数。

全特化(Full Specialization)和偏特化(Partial Specialization)

全具体化也叫全特化。

片段具体化也叫偏特化。

全特化(Full Specialization)是指针对模板中所有的参数类型都进行了具体化。

偏特化(Partial Specialization)则是指模板参数列表只有部分被具体化了。一般来说,偏特化版本可以针对某个或多个模板参数进行特化,但至少有一个模板参数需要保留为未指定类型,从而仍然需要依靠实例化时进行推导。

举例如下。

// 基础模板
template<typename TA, typename TB>
class MyClass {

};

// 全特化
template<>
class MyClass<int, int> {

};

// 偏特化
template<typename TA>
class MyClass<TA, int> {

};

其中,模板函数只有全特化,没有部分特化。要实现部分特化应当使用函数重载或者仿函数。

template<typename TA, typename TB>
void func(TA a, TB b) {

}

// 全特化 正确
template<>
void func(int a, int b) {

}

// 偏特化 错误!
template<typename TA>
void func<TA, int>(TA a, int b) {

}

// 偏特化 错误!
template<typename TA, typename TB>
void func<TA, TA>(TA a, TA b) {

}

// 属于重载 因为语法上 func后面没有<>来指定具体类型 否则就属于具体化
template<typename TA>
void func(TA a, TA b) {

}

重载解析

调用函数时,选择哪个函数?

执行顺序:

  1. 函数名正确
  2. 参数数量正确
  3. 匹配函数参数类型
    1. 完全匹配,常规函数优于模板函数
    2. 类型提升转换
    3. 类型标准转换
    4. 类型自定义转换

decltype 和 auto

二者都作为类型推断,都是C++11新增的关键字。decltype是为了解决模板里,不同类型运算后最终类型无法确定的问题。

语法格式:

auto varname = value;  //auto的语法格式
decltype(exp) varname [= value];  //decltype的语法格式

decltype对于输入的不同获取类型是不同的

主要区别:

另外:

在函数体内,decltype可以很好运转,但是在返回时怎么办呢。返回值内的表达式exp是局部变量无法访问到。

于是有了后置返回类型(trailing return type)

模板函数可以这样写:

template<typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
    return t1 + t2;
}

普通函数也可以这样写

auto add(int a, float b) -> double
{
    return a + b;
}

模板类

大部分和模板函数一致,但是要注意模板类实例化时必须显式通过<>指定类型!不像模板函数可加可不加。

template<typename T>
class MyClass{};

template<typename T>
void my_func(T t);

//必须<>指明类型
MyClass<int> mc;

my_func<int>(1);
//或者
my_func(1);

模板参数

模板参数不只是类型,也可以是非类型值和模板。其中非类型的值是有限制的,一般是整数类型:各种整数、指针。

template<typename T>
using VecWrap = std::vector<T>;

// 对std::vector进行了封装 使其模板参数只有一个入参 并且自带了reserve
template<template<typename Inner> typename Outer, int Num> // Inner只是一个占位符 模板参数如果是模板则不能看作一整个typename, 需要写出整个模板参数的模板声明
class MyVector {
public:
    MyVector() {
        myVector.reserve(Num);
    }

    Outer<int> myVector;
};


int main() {
    MyVector<VecWrap, 10> v;
    //MyVector<std::vector<int>, int> myTemplate2; //Outer模板只有一个参数需要using进行类型定义
    std::cout << v.myVector.capacity() << std::endl; //10
}

模板参数也可以是有名字和无名。

有无默认值 有名字 无名字
无默认值 template<typename T> template<typename>
有默认值 template<typename T = int> template<typename = int>
无默认值的可变数量 template<typename... T> template<typename...>

二阶段名字查找

没有实例化的类是不存在的。

对于依赖模板参数的类需要在实例化后才能得到类里的成员。

template<typename T>
class Base {
public:
    void f() {};
    int a;
};

template<typename T>
class Derived : public Base<T> {
public:
    void b() {
        //f(); //错误, 对于依赖模板参数的名字, 编译时会在模板实例化时(运行时)查找
        //a; //错误
        this->f();
        this->a;
        Base<T>::f();
    }
};

class Derived2 : public Base<int> {
public:
    void b() {
        f(); //正确, 运行到这里《 已经实例化了
    }
};

模板元编程

模板元编程充分利用类型系统。

类型推导

template <typename Fn, typename... Args>
struct result_of_impl {
    using type = typename std::result_of<Fn(Args...)>::type; //std::result_of 判断函数调用的返回值类型
};

template <typename Fn, typename... Args>
using result_of_t = typename result_of_impl<Fn, Args...>::type;

int main() {
    using F = int (*)(double);	//函数指针的类型
    using R = result_of_t<F, double>;
    static_assert(std::is_same_v<R, int>); //编译期间判断
    return 0;
}

控制流

条件语句,除了原始的静态重载还有std::conditional、std::enable_if可以用。

原始

#include <iostream>

template <typename T>
T max(T a, T b, bool use_a = true) {
    return use_a ? a : b;
}

template <typename T>
T max(T a, T b) {
    return max(a, b, a >= b);
}

int main(){
    std::cout << max(1, 2, false) << std::endl; // 手动输入
    return 0;
}

std::conditional 根据表达式的值选择后面的值

#include <iostream>

template<typename T1, typename T2>
struct is_same {
    static const bool value = false;
};

template<typename T>
struct is_same<T, T> {
    static const bool value = true;
};

template<typename T1, typename T2>
using select_type = typename std::conditional<is_same<T1, T2>::value, T1, T2>::type;

int main() {
    std::cout << typeid(select_type<int, int>).name() << std::endl;
    return 0;
}

std::enable_if 根据一个条件表达式来选择是否实例化一个模板

#include <iostream>

template <typename T1, typename T2>
typename std::enable_if<std::is_same<T1, T2>::value, T1>::type //如果条件不成立则不选择这个模板
select_type(T1 a, T2 b) {
    return a;
}

template <typename T1, typename T2>
typename std::enable_if_t<!std::is_same<T1, T2>::value, T2> //std::enable_if_t自带::type, 不需自行添加 
select_type(T1 a, T2 b) {
    return b;
}

int main() {
    int x = 42;
    double y = 3.14;
    auto p = select_type(x, x); // p 的类型为 int
    auto q = select_type(x, y); // q 的类型为 double
    std::cout << p << " " << q << std::endl; // 输出 42 3.14
    return 0;
}

void_t 用于判断是否有某个成员函数,原理是尝试获取返回值类型,若获取失败则不选择该模板

#include <iostream>
#include <type_traits>

template<typename T, typename = void>
struct has_size : std::false_type {};

template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {}; //SFINAE 去实例化其他的

int main()
{
    std::cout << std::boolalpha;
    std::cout << has_size<std::string>::value << std::endl;  // 输出 true,因为 std::string 有 size() 成员函数
    std::cout << has_size<int>::value << std::endl;  // 输出 false,因为 int 没有 size() 成员函数
    return 0;
}

SFINAE

Subsituation failure is not an error。实例化失败不是错误。

目的是模板实例化时,如果我们选择重载的话,会提供多个实例化选择。这时当编译器遇到第一个无法实例化的模板的时候继续查看其他模板能否匹配,而不是直接error结束编译。

相当于实现了模板实例化时的“重载”。

#include <iostream>
#include <type_traits>

template <typename... Ts>
using void_t = void;

//#1 否则就实例化这个
//typename=void 就是一个开关,如果第二选项存在就不使用这个
template <typename T, typename = void>
struct has_typedef_foobar : std::false_type {};

//#2 T::foobar获取成功,那么自然就选择这个进行实例化
template <typename T>
struct has_typedef_foobar<T, void_t<typename T::foobar>> : std::true_type {};

struct foo {
  using foobar = float;
};

int main() {
  std::cout << std::boolalpha;
  std::cout << has_typedef_foobar<int>::value << std::endl;//int::foobar失败 进入#1
  std::cout << has_typedef_foobar<foo>::value << std::endl;//foo::foobar成功 进入#2
}

折叠表达式

C++17。表达式必须用()括起来。折叠表达式避免了模板的递归。

#include <iostream>

template<typename... Args>
auto UniLeft(Args... args) {
    return (... + args); // (((E1 op E2) op E3) op E4)
}

template<typename... Args>
auto UniRight(Args... args) {
    return (args + ...); // (E1 op (E2 op (E3 op E4)))
}

template<typename T, typename... Args>
auto PrintWithSeq(T sep, Args... args) {
    ((std::cout << args << sep), ...); // (std::cout << args[0] << sep) , (std::cout << args[1] << sep)
}

template<typename T, typename... Args>
auto PrintWithSeq2(T sep, Args... args) {
    (..., (std::cout << args << sep)); // (std::cout << args[0] << sep) , (std::cout << args[1] << sep)
}

template<typename... Args>
auto DubLeft(Args... args) {
    (std::cout << ... << args); // (((I op E2) op E3) op E4)
}

template<typename... Args>
auto DubRight(Args... args) {
    return (args = ... = 0); // (E1 op (E2 op (E3 op I)))
}

int main() {
    int sumLeft = UniLeft(1, 2, 3, 4, 5);
    int sumRight = UniRight(1, 2, 3, 4, 5);
    DubLeft(1, 2, 3, 4, 5);                 //12345
    DubRight(1, 2, 3, 4, 5);
    PrintWithSeq(',', 1, 2, 3, 4, 5);       //1,2,3,4,5,
    PrintWithSeq2(',', 1, 2, 3, 4, 5);      //1,2,3,4,5,
    return 0;
}

函数指针分析

如何声明一个函数指针?

//通用格式:
ReturnType (*NickName)(PramType1, PramType2);
//实例
int (*func)(int a, int b);
//或者迷惑人一下
int (*f)();

如上,NickName是调用时使用的,很明显把NickName当作了一个地址。类似的还有int *a;,把*a当作int,那么a就是地址咯。

那么,嵌套一下?

例子1

int (*(*pf())())();

可以这样分析:

  1. pf()是一个普普通通的函数声明(暂且忽略掉返回值),那么他返回的是剩下的一坨,姑且可以拆分成这样:

    T pf();
    
  2. 那么T是什么类型呢,可以看到除去pf()这4个字符后剩下的实际上是有(*)()这样的结构,这是函数指针。所以可以这样声明:

    typedef int (*(*T)())();
    

    此时可以看到T是一个函数指针!,所以最原始的pf函数返回了一个函数指针。

  3. T是函数指针,那么*T就是函数咯,此时可以把(*T)()这几个字符拿掉(他们代表了执行*T这个函数),那么可以这样声明:

    typedef int (*T2)();
    

    此刻就明白了。

最终可以这样简化:

typedef int (*T2)();
typedef T2 (*T)();
typedef T pf();

也可以使用后置类型简化:

auto pf() -> auto (*)() -> int (*)();
//进行一个实现
auto pf() -> auto (*)() -> int (*)()
{
    return []() -> int (*)()
    {
        return []() -> int
        {
            return 0;
        };
    };
}

例子2

int (*(*(*(*fn())(int))())())();
typedef int (*(*(*(*T)(int))())())();
typedef int (*(*(*T2)())())();
typedef int (*(*T3)())();
typedef int (*T4)();
T fn();
//或者
auto fn() -> auto (*)(int) -> auto (*)() ->auto (*)() -> int (*)();

仿函数

一种避免使用函数指针的方式是使用仿函数。如下面传入了一个less函数来构建大顶堆。

仿函数实际上是重载了operator()。这样可以灵活的改变函数内部的参数。

 #include <bits/stdc++.h>
using namespace std;

//仿函数
struct MyLess{
    bool operator()(const int& a, const int& b) const{
        return a < b;
    }
};

int main(){
    vector<int> v {8,9,0,7,4,5,6,3,2,1};
    priority_queue<int, vector<int>,MyLess> pq(v.begin(), v.end());
    pq.push(-1);
    while (!pq.empty()){
        cout << pq.top() << endl;
        pq.pop();
    }
}

存储持续性和链接性

链接性决定了编译器在编译时到可不可以到其他文件里找,静态性决定当前作用域的变量是否共享内存区,即不同实例使用同一份内存。可以看到全局变量和函数一旦加了static就不能在其他文件使用了。

名称 在当前文件定义声明格式 在其他文件中使用时的引用声明格式 链接性 静态性 作用域
全局变量 int a; extern int a; 外部 是 全局
全局变量 static int a; 不可使用 内部 是 当前文件
函数 void foo(); void foo(); 外部 是 全局
函数 static void foo(); 不可使用 内部 是 当前文件
局部变量 int a; 不可使用 无 否 当前代码块
局部变量 static int a; 不可使用 无 是 当前代码块

需要注意全局变量和静态全局变量的区别。

  1. 生命周期:静态全局变量在整个程序的运行期间都存在,而非静态的全局变量的生命周期仅限于它们在程序中的可见范围内。
  2. 作用域:静态全局变量只能在定义它们的文件内访问;而非静态的全局变量可以在其他文件中通过 extern 关键字来访问。
  3. 初始化方式:非静态的全局变量可以在定义时直接初始化;而静态全局变量只能在定义时初始化,且只能初始化一次。
  4. 存储方式:静态全局变量在程序的指定数据段(五大内存区域之一)中分配存储空间;而非静态的全局变量在程序的栈中或堆中分配存储空间。

实例:

test1.cpp

int globalA = 0;
static int staticGlobalA = 0;

void Fn() {}
static void  StaticFn() {}

test2.cpp

extern int globalA;
//extern int staticGlobalA;  can not use

void Fn(); 
//或 extern void Fn()

//extern void StaticFn();   can not use

以上都是不用命名空间时整的花活,在使用命名空间后,严格遵循“声明在header,定义在cpp”的规则,使用时引入命名空间就好了,相当于直接把整个东西都拿过来,不管是否是内部还是外部链接性,这时只有静态非静态之分。

虚函数和纯虚函数

  1. 只要某类重写了父类的方法A(virtual),那么子类作为父类类型指针/引用传入时,调用方法A,就可以直接使用子类已经重写的方法,而不是父类的方法。若父类的方法不定义为virtual,则还是使用父类的方法。如果还是按照对象调用则virtual相当于没有,对象是谁就用谁的。

    为什么只有指针才可以?使用对象调用方法属于静态联编,不会考虑对象实际是什么,只用到了对象的类型。

    class Base {
    public:
        virtual void foo() {
            cout << "Base::foo()" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        void foo() {
            cout << "Derived::foo()" << endl;
        }
    };
    
    int main() {
        Derived *d = new Derived();
        d->foo();
        dynamic_cast<Base *>(d)->foo(); //向上类型转换,没有丢失数据
    
        Derived dd;
        dd.foo();
        Base bb = dd; //进行了赋值拷贝,只将Derived里的Base部分传过来
        bb.foo();
    }
    
    /*
    Derived::foo()
    Derived::foo()
    Derived::foo()
    Base::foo()
    */
    

    在Java里,一个类实现一个Interface,然后就可以作为Interface传入,就可以调用这个类自己实现的方法,C++这里实现机制完全和Java一致了,只不过C++没有interface关键字,强行用class去模拟interface。

    在Go里,struct实现了interface后,就可以传入该struct,然后使用该struct实现的interface的方法。其实就是实现了真正意义上的“接口”。

  2. override与final

    要想明确子类实现的是父类的方法,加override关键字在子类实现方法签名里,避免写错。

    要想父类拒绝子类override方法,可以加final关键字在子类实现方法签名里,若父类想要拒绝继承,可以在类声明后标记为finalclass Base final{};。

    实现父类方法时,子类方法要满足:函数名、入参、返回值完全一致,但是当返回类的this指针时,返回值可以不一样。

  3. 虚函数的实现原理

    虚函数使用虚函数表实现,它是编译期的静态数组,类有一个隐藏成员vptr指向它。vptr 是一个指向虚函数表的指针,它是每个包含虚函数的类的非静态成员变量之一,在类的内存布局中被放在对象的起始地址处。每个类实现时,都会创建一个虚表,虚表记录了各个虚函数的入口地址,若自己的类有实现,就指向自己的函数,否则指向基类的虚函数。若派生类继续虚下去,则更新地址,不会记录路径上的虚函数。使用第一个虚函数就是使用数组的第一个元素。

  4. 纯虚函数和抽象类

    在虚函数后=0即为纯虚函数。含有纯虚函数的类成为抽象类,无法实现。子类继承抽象类时应当实现里面的纯虚方法。此处概念和Java的接口-抽象类几乎完全一致。

  5. 在继承中为什么一般虚析构函数,而不是虚构造函数?

    从类的生命周期谈起,子类需要继承父类的相关数据,所以从父类开始构造,父类构造完子类构造,父类构造函数不能为虚函数,否则父类数据没有。而在析构时,因为是以父类指针调用的子类,从父类指针调用父类的析构方法,如果父类析构函数不设为虚的,那么子类的析构函数将无法调用,可能会引起内存泄漏。

存储区

成员函数const和mutable

class C
{
    void change() const
    {
        a = 1;//错误
        b = 2;//正确
    }
    
    int a;
    mutable int b;
};

const放在成员函数后面可以让函数不改变类的变量,若又想改变,需要在成员变量前加mutable。

成员函数const保证不更改对象。const对象调用的方法只能是const方法,以此保证const。

当const和非const成员函数共存时,const对象只会调用const成员函数,非const只会调用非const成员函数。

友元函数

解决了什么问题?

非类成员函数可以访问类私有变量

友元函数定义在类内,但不是类的成员函数。

使用场景举例

二元运算符重载,常规C operator+(int a),使用时必须按照顺序c + 1或c.operator+(1),而不能1 + c。

此时可以使用类外运算符重载,但是类外访问不到类内变量怎么办?使用友元函数就可以。

此外,像一元运算符<<,必须使用<<

遇到问题

class C
{
public:
    int operator+(int a) const
    {
        return num + a;
    }

private:
    int num;
};

int main()
{
    C c;
    cout << c + 1;//可以
    //cout << 1 + c;//不可以
}

解决方案 友元函数

class C
{
public:
    int operator+(int a) const
    {
        return num + a;
    }
    
    friend int operator+(int a, C c);

private:
    int num;
};

int operator+(int a, C c)
{
    return a + c.num;
}

/*更优雅的实现
int operator+(int a, C c)
{
    return c + a;
}
*/

int main()
{
    C c;
    cout << c + 1;//可以
    cout << 1 + c;//可以了
}

多继承

c++ 支持多继承,使用了一些操作让基类们不会冲突。

  1. 基类函数/变量冲突?

    使用类作用域解析::

  2. 菱形继承创建多个类如何解决?

    使用虚基类,可以保证只创建一个基类。否则以下将创建两个。

    class A{};
    class B : virtual A{};
    class C : virtual A{};
    class D : B, C {};
    
  3. 菱形继承,若D对B,C传不同数据,而BC用这些数据对A的初始化不同怎么办【自动传递信息】?

    在虚基类的情况下,这样是不允许的,只能使用默认构造,或者直接在D中显式调用A构造函数。

    而在普通基类的情况下,由于各自创建了一个基类,当然可以这样做,而在D中显式调用A构造函数是不可以的。

内存对齐

首先找到block = min(max(sizeof(member)), pack_pram),然后从头填充,每一次都分配block大小,下一个如果不能塞进上一个,则另起一个新的block,如果仍有结构体则拆开。

#pragma pack(8)
struct A {
    char a;
    int b;
    double c;
};//16

struct B {
    char a;
    double c;
    int b;
};//24

struct C {
    int a;
    char b;
    double c;
};//16

struct D {
    char b;
};//1

struct E {
    char b;
    int a;
};//4+4=8

#pragma pack(1)
struct E2{
    char b;
    char c;
    char a;
};//3

#pragma pack(4)
struct F {
    char a;
    E2 e;
};//4 //将E2拆开,重新计算pack

类型转换

C语言暴力版 (type) var 相当于staic_cast

int a;
float b = (int)a;

dynamic_cast

用于继承体系下,不同层次类间的类型转化,带有安全检查。

在进行反向转换时,即将 Base* 类型的指针转换为 Derived* 类型的指针时,需要注意以下两点:

dynamic_cast如果转换失败,则返回 nullptr。

class Base {
public:
    virtual void Eat() {} // 需要存在虚函数才能向上转换
};

class Derived : public Base {
};

int main() {
    Derived d;
    Base *b = dynamic_cast<Base *>(&d);
    Base *b1 = &d; // 自动转化

    Base *bb = new Derived();
    Derived *dd = dynamic_cast<Derived *>(bb);
}

static_cast

类型间转换,当然也可以用作dynamic_cast

int a;
float b = static_cast<float>(a);

reinterpret_cast

重新解释指针,指针间转换,可以用作dynamic_cast,参数必须是指针,否则出错。

int* a;
float* b = reinterpret_cast<float*>(a);

const_cast

如果不用const_cast,通过a改变b内存的值根本不可能。 ·

int main() {
    int b=1;
    const int *a = &b;
    int* pb = const_cast<int*>(a);
    *pb=2;
     //int *c = &(*a);
    //Cannot initialize a variable of type 'int *' with an rvalue of type 'const int *'
    cout << *a << endl; // 2
}

例子:类之间的转换

#include <iostream>

using namespace std;

class A {
public:
    virtual void PrintA() {
        cout << "A" << endl;
    }
};

class B : public A {
public:
    void PrintB() {
        cout << "B" << endl;
    }

    void PrintA() {
        cout << "Derived A" << endl;
    }
};

int main() {
    A a, *pa;
    B b, *pb;
    //子类使用父类方法 天经地义
    //直接使用
    b.PrintA();
    //dynamic cast
    dynamic_cast<A *>(&b)->PrintA();
    //static cast
    static_cast<A *>(&b)->PrintA();

    //父类使用子类方法
    //直接使用,当然不行
    //a.PrintB();
    //隐式类型转换 不可以
    //pa = &b;pa->PrintB();

    //dynamic_cast 可以, 但是需要父类有virtual方法,否则编译不通过,
    // dynamic_cast 进行类型转换,vptr的值会指向新的类的虚函数表,从而保证了动态绑定的正确性。因此可以调用,a中没有B的数据,若B的方法使用数据则UB/
    dynamic_cast<B *>(&a)->PrintB();
    //dynamic_cast<B *>(&a)->PrintA(); 出错,转换后发现应当调用B override的PrintA,但是a中没有B的数据
    //强制类型转换 不可以
    //((A*)b)->PrintA();
    //static_cast 可以
    static_cast<B *>(&a)->PrintB();
}

生命周期函数

C++中6个类的生命周期函数如下:

  1. 构造函数 — 用于初始化对象的状态
  2. 复制构造函数 — 用于从一个已有对象创建新对象
  3. 移动构造函数 — 用于从一个可被移动的对象创建新对象
  4. 复制赋值运算符 — 用于将一个对象的状态复制到另一个对象中
  5. 移动赋值运算符 — 用于将一个可被移动的对象的状态移动到另一个对象中
  6. 析构函数 — 用于清理对象在内存中分配的资源
class C {
public:
    C() {}
    ~C() {}
    
    C(const C &) {}
    C(C &&) {}

    C &operator=(const C &) { return *this; }
    C &operator=(C &&) { return *this; }
};
名称 形式 默认提供 惯用法
默认构造 C() 用户没有提供其他构造函数时,编译器提供 =default
拷贝构造 C(const C&) 用户没有提供时,编译器提供;当用户提供移动构造或移动赋值运算符时,会=delete =delete
拷贝赋值运算符 operator=(const C&) 用户没有提供时,编译器提供;当用户提供移动构造或移动赋值运算符时,会=delete 和拷贝构造一起存在或删除
移动构造 C(C&&) 用户不声明除了默认构造函数外的任一生命函数(4个),编译器会提供 一般不=default或=delete
移动赋值运算符 operator=(C&&) 用户不声明除了默认构造函数外的任一生命函数(4个),编译器会提供 一般不=default或=delete
析构函数 ~C() 用户没有提供时,编译器提供 基类=default

三法则

任何想要手动管理资源或内存的类都应声明全部以下五个成员函数

五法则

任何想要移动语义的类都应声明全部以下五个成员函数

显式声明 noexcept

为了保证强异常安全,有一些生命周期成员函数不允许抛异常,需要显式声明noexcept,例如move相关成员函数。

默认noexcept:隐式声明的函数是noexcept。

如vector在内存重分配时期望使用noexcept的移动构造。

#include <iostream>
#include <vector>

class C {
public:
    C() = default;

    ~C() = default;

    C(const C &) {
        std::cout << "copy constructor" << std::endl;
    }

    C(C &&) noexcept /*显式声明noexcept才会被容器默认调用*/ {
        std::cout << "move constructor" << std::endl;
    }

};

int main() {
    std::vector<C> v;
    v.reserve(3);                   // 为了避免内存重新分配造成额外move
    C c1, c2;
    v.emplace_back(c1);             // copy constructor
    v.emplace_back(std::move(c2));  // move constructor
    v.emplace_back(C());            // move constructor
    v.emplace_back(C());            // move constructor
    /*
     * 触发扩容,重新分配内存,将原来的元素
     * 1. 若是有noexcept的move constructor,就会调用move constructor
     * 2. 若是没有noexcept的move constructor,就会调用copy constructor
     */
    return 0;
}

vector.emplace_back()是在最后位置构造参数,不涉及再次拷贝,是在传入时根据传入的左值右值类型来调用copy constructor或者move constructor。在vector内部调用时会调用noexcept的move constructor,否则就调用copy constructor。

自动生命周期顺序

  1. 栈式析构,后创建的先析构。对于全局和静态的 与 局部的,各自单独计算栈。局部的在}时析构,而全局和静态的在}后析构
  2. 全局/静态变量在进入main之前构造
  3. 函数(局部)静态对象在第一次执行到声明语句时创建
  4. 函数(局部)对象在定义时构造,在定义所在的右大括号析构
  5. 临时对象在当前执行语句完成后析构,在赋值给引用变量会延长至和引用一致。
#include <iostream>
#include <vector>

class A {
public:
    A() {
        std::cout << "A()" << std::endl;
    }

    ~A() {
        std::cout << "~A()" << std::endl;
    }
};

class B {
...
class D{
...定义类似于A

C c;

int main() {
    A *pa = new A();
    B b;
    static D d;
    delete pa;
    return 0;
}
/*
 * C()
 * A()
 * B()
 * D()
 * ~A()
 * ~B()
 * ~D() D晚于局部变量B析构 
 * ~C()
 */

其他

模板里的typename

在模板函数里可以帮助编译器区分static成员和类型。

template <class T>
Class MyClass
{
  typename T::SubType * ptr;
  ...
};

c++ - Officially, what is typename for? - Stack Overflow

typeid 比较类型

需包含

属于RTTI(run-time type information)的一部分。

tuple pair tie

tuple

元组,融合任意类型和任意个数个元素。一旦形成,不可再改变长度。

#include <tuple>

using namespace std;

using Ty1 = int;
using Ty2 = float;

int main() {
    Ty1 e1;
    Ty2 e2;
    tuple<Ty1,Ty2> t1(e1,e2);       //直接构造
    auto t2 = make_tuple(e1,e2);    //通过make_tuple构造
    get<0>(t1);                     //通过get获取元素
    size_t v1 = tuple_size<tuple<Ty1,Ty2>>::value;        //获取元素个数
    size_t v2 = tuple_size_v<tuple<Ty1,Ty2>>;             //c++14 获取元素个数
    tuple_element<0, tuple<int,float>>::type t;           //获取类型
    auto cat = tuple_cat(t1,t2);    //合并元组, 编译期运作
}

内部实现是利用模板可变参数递归解开的特点实例化一个继承类的链条。每个类仅有一个成员。

pair

相当于只存两个元素的tuple

tie

用于解包多返回值,在C++17中被结构化绑定取代。

std::multimap<int, int> m;
std::multimap<int, int>::iterator lower, upper;
std::tie(lower, upper) = m.equal_range(5);
auto [lower2, upper2] = m.equal_range(5); // c++17 结构化绑定

设置entry_point

C程序允许设定一个entry_point作为启动函数,而不是main。

linux + gcc 下的一个简单的C程序设置入口函数。

#include <cstdio>
using namespace std;
int my_main(){
    printf("my_main");
    exit(0);//此处必须有此代码,否则会segment fault,因为之后要进行堆空间分配
}

int main(){
	printf("main");
}
gcc -e my_main test.c

embedded pointer

嵌入式指针,指在构建链表的时候可以使用union,将数据和指针共用内存。节省了一个指针的空间。概念图如下。

---------------------
|next  | data       |
---------------------
---------------------
|next  | data       |
---------------------
---------------------
|      data         |
---------------------

Trivial类型和Standard Layout

原来被叫做POD(Plain Old Data),意指可以完全和C兼容的类型。POD类型被广泛应用于C++语言的底层编程以及与其他语言进行交互时的数据传输。例如,在使用二进制流进行数据传输时,可以将POD类型直接作为字节流传输,无需进行任何转换操作。

C++11之后,POD这个概念被更加严格地划分到两个概念,即Trivial和Standard Layout。Trivial是指满足一些条件(比如有默认构造函数等)的类,而Standard Layout则是指满足一些布局要求的类。

Trivial

如果一个类满足以下条件,则该类被认为是“Trivial”:

  1. 它有一个非删除、非私有的默认构造函数,或者是一个使用 =default 标记的默认构造函数。
  2. 它有一个非删除、非私有的复制构造函数,或者是一个使用 =default 标记的默认复制构造函数。
  3. 它有一个非删除、非私有的移动构造函数,或者是一个使用 =default 标记的默认移动构造函数。
  4. 它有一个非删除、非私有的复制赋值运算符函数,或者是一个使用 =default 标记的默认复制赋值运算符函数。
  5. 它有一个非删除、非私有的移动赋值运算符函数,或者是一个使用 =default 标记的默认移动赋值运算符函数。
  6. 它有一个非删除的、公开且没有参数的析构函数,或者是一个使用 =default 标记的默认析构函数。

如果一个类同时满足以上所有条件,则可以将其认为是“平凡的”。对于平凡的类,编译器可以采用一些优化手段,例如直接调用 memcpy() 和 memmove() 来进行复制和移动操作。

Standard Layout

如果一个类是标准布局(Standard Layout),那么这个类可以保证在不同编译器、不同操作系统下,它的内存布局是相同的。同时,标准布局类还能够更好地支持C和C++之间的互操作。

一个类被认为是标准布局,需要满足以下要求:

  1. 所有非静态数据成员拥有相同的访问权限(public、protected、private)。
  2. 所有非静态数据成员在类中被声明的顺序就是它们在内存中出现的顺序。
  3. 类中没有任何虚函数或虚基类。
  4. 没有任何成员的类型与类本身相同(递归定义)。
  5. 所有基类都出现在派生类的最开始位置,且派生类中只包含非静态数据成员和零个或一个虚函数。

返回值优化

考虑返回对象,避免拷贝构造,提升性能。在返回值确定时,编译器会对返回值进行优化,避免额外的拷贝。

#include <iostream>

class C {
public:
    C() { std::cout << "C::C()" << std::endl; }

    ~C() { std::cout << "C::~C()" << std::endl; }

    C(const C &) { std::cout << "C::C(const C&)" << std::endl; }

    C(C &) { std::cout << "C::C(C&)" << std::endl; }

    C(C &&) noexcept { std::cout << "C::C(C&&)" << std::endl; }

    C &operator=(const C &) {
        std::cout << "C::operator=(const C&)" << std::endl;
        return *this;
    }

    C &operator=(C &&) {
        std::cout << "C::operator=(C&&)" << std::endl;
        return *this;
    }


};

// 无命名的,直接在指定位置构造
C nrvo() {
    return C();
}

// 有名字的,看情况优化,以下可预测的是同无命名的情况相同。正常会调用移动构造(优先)和拷贝构造。
C rvo() {
    C c;
    return c;
}

int main() {
//    C c = nrvo(); /*C::C() C::~C()*/
    C c2 = rvo();
    /*
     *  C::C()
        C::C(C&&)
        C::~C()
        C::~C()
     */
    return 0;
}

auto

局限性

auto自动推断无法知道何时要加引用。

在右边为const时,auto会推断出const。

在右边为&或&&时,auto不会主动推断任何引用,会默认为值类型,会进行额外拷贝,因此需要额外注意引用符号的添加。

相当于模板匹配

const auto&& a =expr;
//==
template<typename T>
void func(const T&&);

破解局限性

decltype(auto) a = anyRef;

模板自动推导

从C++17开始可以自动推导模板参数。

std::array a = {1, 2, 3}; // c++17开始
std::array<int,3> a = {1, 2, 3}; // c++11必须这样

std::begin end

对于裸数组和容器,会返回它们的迭代器。

对于容器,和容器.begin end相同。

通用初始化

C++98只能对数组这样做int arr[]={1,2,3};。

C++11起,允许在创建任意对象时以逗号分隔的值序列的形式进行初始化{1,2,3}。

// 调用构造函数
int a{1};
int a{}; // POD类型 memreset为0 a=0
std::string s{}; // 默认构造
std::string s{"Hello"}; // const char* 构造
struct M{}; // POD类型 memreset为0

// 构造后类型隐式转换
int a = {};
int a = {1};

注意,在类型隐式转换时需要注意explicit的声明:

class Point {
public:
    explicit Point(double x, double y) : x(x), y(y) {}

    Point(double x) : x(x), y(0) {}

    double x;
    double y;
};

int main() {
    Point p11 = {5, 6}; // 不可以, 因为声明了explicit, iniliazer_list不会被隐式转换为Point
    Point p12 = {5};    // 可以, 因为没有声明explicit, iniliazer_list会被隐式转换为Point

    Point p21{5, 6};    // 可以, 直接调用构造函数不受explicit影响
    Point p21{5};
    std::vector<Point> points1{{1, 2}, {3, 4}}; //同p11
    std::vector<Point> points2{{1}, {3}};

    return 0;
}

std::initializer_list是一个容器用于支持这种语法传递。

#include <iostream>
#include <vector>

class Example {
public:
    // 使用initializer_list将数组参数初始化转移到数组
    Example(int a, int b, std::initializer_list<int> c) : m_a(a), m_b(b), m_c(c) {}

private:
    int m_a;
    int m_b;
    std::vector<int> m_c;
};

int main() {
    Example ex(1, 2, {3, 4, 5});
    return 0;
}

字面量后缀

与1.0f的后缀f相似,用户可以自定义字面量的后缀。

与系统提供的不同,自己定义的需要加下划线。用户自定义的后缀和系统后缀一样,都可以用于静态初始化。

类型也是有限制的:

char const *
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t

例子:

int operator ""_cm(unsigned long long a) {
    return a;
}

int operator ""_m(unsigned long long a) {
    return a * 100;
}

int main() {
    auto a = 1_cm + 1_m;
    std::cout << a;
    return 0;
}

std::bitset

bitset 是一种容器类,用于存储二进制位信息。它可以内置数组的形式存储和访问一系列二进制位。

bitset可以用字符串初始化,并且自定义谁是0;还可以转换成数值和String。

注意:

#include <bitset>
#include <cassert>
#include <cstddef>
#include <iostream>
 
int main()
{
    typedef std::size_t length_t, position_t; // the hints
 
    // constructors:
    constexpr std::bitset<4> b1;
    constexpr std::bitset<4> b2{0xA}; // == 0B1010
    std::bitset<4> b3{"0011"}; // can also be constexpr since C++23
    std::bitset<8> b4{"ABBA", length_t(4), /*0:*/'A', /*1:*/'B'}; // == 0B0000'0110
 

 
    // bitset supports bitwise operations:
    b3 |= 0b0100; assert(b3 == 0b0111);
    b3 &= 0b0011; assert(b3 == 0b0011);
    b3 ^= std::bitset<4>{0b1100}; assert(b3 == 0b1111);
 
    // operations on the whole set:
    b3.reset(); assert(b3 == 0);
    b3.set(); assert(b3 == 0b1111);
    assert(b3.all() && b3.any() && !b3.none());
    b3.flip(); assert(b3 == 0);
 
    // operations on individual bits:
    b3.set(position_t(1), true); assert(b3 == 0b0010);
    b3.set(position_t(1), false); assert(b3 == 0);
    b3.flip(position_t(2)); assert(b3 == 0b0100);
    b3.reset(position_t(2)); assert(b3 == 0);
 
    // subscript operator[] is supported:
    b3[2] = true; assert(true == b3[2]);
 
    // other operations:
    assert(b3.count() == 1);
    assert(b3.size() == 4);
    assert(b3.to_ullong() == 0b0100ULL);
    assert(b3.to_string() == "0100");
}

static_assert

对const量进行假设,在编译期进行检查。

int a = rand();
static_assert(a == 4, "a is not 4"); // 错误Static_assert expression is not an integral constant expression
static_assert(sizeof(int) == 4, "int is not 4 bytes");

enum class

和整数没有转换,具有独立作用域。

enum class EnumName : underlying_type { enumerator_list };

enum class Color : uint8_t {
    RED = 0,
    GREEN = 1,
    BLUE = 2
};


int main() {
    Color c1 = Color::RED;
    //Color c2 = 0; //错误
    Color c3 = static_cast<Color>(0); //可以
}

std::chrono

提供时钟,时间点和持续时间。

时钟有系统时钟和单调时钟。

std::chrono::system_clock表示的是系统级别的时钟,它的时钟周期由系统决定。

std::chrono::steady_clock表示的是一个单调时钟(monotonic clock),它的时钟周期不会受到系统时间调整(例如闰秒、时区变更等)的影响,且保证不会出现时间倒流的情况。因此,std::chrono::steady_clock常用于计算时间间隔、计时等需要较高精度的任务。

注意,std::chrono::steady_clock不是绝对时间,其原点是未指定的,不能用于表示日期和时间。而 std::chrono::system_clock则可以被用于表示日期和时间,但其不保证单调性。

#include <chrono>
#include <iostream>

int main() {
    // 获取当前系统时钟上的时间点
    auto now = std::chrono::system_clock::now();

    // 将时间点转换为 time_t 类型
    auto now_time_t = std::chrono::system_clock::to_time_t(now);

    // 输出当前时间
    std::cout << "Current time: " << std::ctime(&now_time_t);

    // 计算程序运行时间
    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        // do something
    }
    auto end = std::chrono::steady_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "Elapsed time: " << elapsed.count() << " microseconds." << std::endl;

    return 0;
}

std::random

std随机数生成可以分为两个部分:引擎和分布器。引擎负责生成随机位序列,而分布器负责将这些随机位转换成各种不同的分布形式,从而实现不同类型随机数的生成。

#include <random>

int main() {
    std::mt19937_64 rng(std::random_device{}());        // 随机数引擎
    std::ranlux48 rng2(std::random_device{}());         // 随机数引擎

    std::uniform_int_distribution<int> dist(0, 100);    // 随机数分布器-均匀分布
    std::normal_distribution<double> dist2(0, 1);       // 随机数分布器-正态分布
    
    int a = dist(rng); // 生成随机数
    return 0;
}

std::any

C++17起存在。

std::any可以存储任意类型的值。std::any的使用需要注意类型安全,如果在调用 std::any_cast时传入了错误的类型,会抛出一个 std::bad_any_cast异常。

std::any a = 1; // 声明对象
std::cout << std::any_cast<int>(a) << std::endl;
if(a.type() == typeid(int)){
    std::cout << "int" << std::endl;
}

std::optional

C++17起存在。它是一种可选值类型,表示一个值可能存在,也可能不存在的情况。

std::optional与std::any相比,语义更加明确,而且使用值的时候不必再进行类型判断。

optional和对象存放在一起,都放在栈上或堆上。因此在大数据情况下会发送拷贝。

std::optional<int> divide(int a, int b) {
    if (b == 0) {
        return std::nullopt; // 没有值
    }
    return a / b;
}

int main() {
    std::optional<int> result = divide(10, 2);
    if (result.has_value()) {

    }
    return 0;
}

std::varient

类似于union,std::variant可以存储多个类型的值,但只有其中一种类型的值是有效的,即只有一种类型的值处于“激活”状态。可以通过 std::variant 提供的 API 显式地切换各种类型之间的值。

std::variant<int, double> v = 2.0;  // 定义一个 variant 对象, 若初始化时不赋值则初始化第0个的类型

if (v.index() == 0) {               // 判断当前存储的值类型,并进行对应操作
    int i = std::get<int>(v);
    std::cout << "stored int value: " << i << std::endl;
} else if (v.index() == 1) {
    double d = std::get<double>(v);
    std::cout << "stored double value: " << d << std::endl;
}

if (auto ptr = std::get_if<int>(&v)) {
    std::cout << "stored int value: " << *ptr << std::endl;
}
    
/***********类似实现, 适用于POD***********/
struct FloatIntChar {
    enum {
        Float,
        Int,
        Char
    } type;
    union {
        float f;
        int i;
        char c;
    };
};
FloatIntChar fic;
fic.type = FloatIntChar::Float;
fic.f = 1.0f;
项目 any optional variant
默认空构造 是 是 否,初始化第0个对象类型
存储内容 任意类型的值 指定类型的值或未初始化状态 多种可能类型的值
值检查 has_value has_value has_value
值获取 std::any_cast<> * 或 -> 运算符 std::get,get_if
使用场景 不确定值类型;需要动态存储和访问不同类型的值 可能为空的类型;仅在需要时才存储和访问值 需要处理多种数据类型的值;类型已知但可能变化

std::error_code error_condition

标准库提供了错误码支持,给不能使用异常的情况提供支持。

error_code是平台相关的错误,error_condition是平台独立的错误(通常由用户提供),它们内置了很多错误码,可以直接使用。错误库的解释如下:

std::errc              -> std::posix_error_code
std::error_code        -> std::platform_dependent_error
std::error_condition   -> std::platform_independent_error
std::error_category    -> std::error_domain/utility/mapper
std::system_category   -> std::platform_dependent_error_domain
std::generic_category  -> std::platform_independent_error_domain
std::future_errc       -> std::future_error_code
std::future_category   -> std::future_error_domain
std::io_errc           -> std::iostream_error_code
std::iostream_category -> std::iostream_error_domain

简单使用例子:

    std::error_code ec = std::make_error_code(std::errc::no_such_file_or_directory);
    std::cout << "Error message: " << ec.message() << std::endl;
    std::cout << "Error category: " << ec.category().name() << std::endl;
    std::cout << "Error code: " << ec.value() << std::endl;
    std::error_condition ec2 = std::make_error_condition(std::errc::no_such_file_or_directory);
    std::cout << "Error message: " << ec2.message() << std::endl;
    std::cout << "Error category: " << ec2.category().name() << std::endl;
    std::cout << "Error code: " << ec2.value() << std::endl;

std::error_code可以和varient配合,解决了optional返回空,但是不知道什么错误的场景。

std::error_code和std::error_condition都继承自std::error_category,可以自定义error类型实现向std::error_code和std::error_condition的转换。

#include <system_error>
#include <iostream>
#include <variant>
#include <any>

enum class MyErrorType : int {
    success,
    error1
};

class my_error_category : public std::error_category {
public:

    // Return a short descriptive name for the category, 继承自基类std::error_category
    const char *name() const noexcept override {
        return "my_error_category";
    }

    // Return what each enum means in text, 继承自基类std::error_category
    std::string message(int i) const override {
        switch (i) {
            case static_cast<int>(MyErrorType::success):
                return "success";
            case static_cast<int>(MyErrorType::error1):
                return "error1";
        }
        return "Unknown error";
    }

    // 转换成std::error_condition, 继承自基类std::error_category
    std::error_condition default_error_condition(int i) const noexcept override {
        switch (i) {
            case static_cast<int>(MyErrorType::success):
                return {};
            case static_cast<int>(MyErrorType::error1):
                return make_error_condition(std::errc::invalid_argument);
        }
        return std::error_condition(i, *this);
    }

    std::error_code make_error_code(MyErrorType e) const noexcept {
        return std::error_code(static_cast<int>(e), *this);
    }
};

int main() {
    std::error_condition e = my_error_category().default_error_condition(1);
    std::cout << e.message() << std::endl;              //Invalid argument
    std::cout << e.category().name() << std::endl;      //generic
    std::cout << e.value() << std::endl;                //22

    std::error_code ec = my_error_category().make_error_code(MyErrorType::error1);
    std::cout << ec.message() << std::endl;             //error1
    std::cout << ec.category().name() << std::endl;     //my_error_category
    std::cout << ec.value() << std::endl;               //1
}

视图

视图(View)通常指一些不拥有自己的元素、而是引用其他容器中已有元素的对象。它们能够提供对数据的不同方式的访问或切片。

容器一般会管理内容的生命周期,而视图不会,不拥有内容也不能修改内容,因此需要自行保证内容是可以合法访问的。

std::string_view C++17

无需进行string的统一构造或移动。

std::string gen(std::string_view str) {
    std::string str2 = "hi ";
    return str2 + str.data();
}

int main() {
    std::cout << gen("Jack") << std::endl;
    std::string str = "Bob";
    std::cout << gen(str) << std::endl;

    return 0;
}

std::function

函数包装器,让不同实现的函数统一类型。

std::function<int(int, int)> add = [](int a, int b) { return a + b; };
add(1,2)

std::function/lambda表达式用于取代std::bind。

int sum(int a, int b) {
  return a + b;
}

auto new_sum = std::bind(sum, 1, std::placeholders::_1);

Currying(柯里化)

Currying(柯里化)是一种函数式编程技术,它可以将接受多个参数的函数转换成接受单一参数(最初函数的第一个参数)的函数序列。

int sum(int a, int b, int c) {
  return a + b + c;
}
int a = sum(1,2,3);

auto curry_sum = [](int a) {
  return [a](int b) {
    return [a, b](int c) {
      return sum(a, b, c);
    };
  };
};
int b = curry_sum(1)(2)(3);

if constexpr

C++17。constexpr表示编译期常量,简化了对SFINAE的使用。

#include <iostream>
#include <type_traits>

template <typename T>
void Print(const T& data) {
    if constexpr (std::is_integral_v<T>) { //在编译期就可以判断了! 不需要额外去做模板去匹配
        std::cout << "Integer: " << data << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "Float: " << data << std::endl;
    } else {
        static_assert(std::is_same_v<T, void>, "Unsupported type!");
    }
}

int main() {
    int i = 42;
    float f = 3.14;
    Print(i); // 输出 "Integer: 42"
    Print(f); // 输出 "Float: 3.14"
    // Print("hello"); // 编译错误,不支持字符串类型
    return 0;
}

多线程

C++标准库没有提供进程相关的支持,提供的都是线程相关的。

多线程库

std::thread

#include <iostream>
#include <thread>

void my_func() {
    std::cout << "Hello from thread" << std::endl;
}

void my_func_args(int a) {
    std::cout << "Hello from thread" << a << std::endl;
}

int main() {
    std::thread t(my_func);             //函数指针
    std::thread t2([]() {
        std::cout << "Hello from thread2" << std::endl;
    });                                 //lambda表达式
    std::thread t3(my_func_args, 3);    //带参数的函数


    // 必须进行join或者detach 否则会抛出异常
    t.join();
    t2.join();
    t3.join();
    return 0;
}

join()和detach的作用和区别

join()和detach()都是std::thread类的成员函数,用于在创建新线程后控制线程的运行状态和资源回收。

join待新线程执行结束,并阻塞当前线程,直到新线程执行结束才可以继续执行当前线程。

detach将新线程和当前线程分离,新线程在后台执行,当前线程(一般是主线程)不等待新线程执行结束就会继续执行下去。主线程退出后,新线程直接结束了。

joinable()是std::thread类的成员函数,用于判断当前线程是否可被join或detach。一个线程不可以被两次join或detach。

std::future库

类似于Javascript的promise,用于方便的执行异步任务,相对于thread创建线程更加简洁。内部实现多是线程池。

std::future的实现原理是两个关键组件:Promise和Future。

std::promise 类就是Promise的具体实现,而std::future则是Future的具体实现。

当通过 std::async 启动一个新的异步任务时,它会创建一个 std::promise 对象,并返回一个与之关联的 std::future 对象。异步任务在执行过程中可以使用 std::promise 对象来设置异步操作的结果或异常信息,并由 std::future 对象来获取异步操作的结果或异常信息。

常用的std::future方法包括以下几个:

void task(std::promise<std::string> promise) {
    std::string result = "Task completed.";
    promise.set_value(result);
}

int main() {
    std::promise<std::string> promise; // 获取生产者
    std::future<std::string> future = promise.get_future(); // 获取消费者对象

    // 启动异步任务
    std::async(std::launch::async, task, std::move(promise));

    // 输出异步操作结果
    std::cout << future.get() << std::endl;

    return 0;
}

互斥操作

基础锁:std::mutex

std::mutex是一个裸的互斥量,用于实现互斥锁机制。有以下成员函数:

不推荐直接使用裸的互斥锁,因为加锁解锁都需要手动进行,容易出错。

std::mutex g_mutex;
int g_counter = 0;

void increment_counter() {
    g_mutex.lock(); // 获取互斥锁
    ++g_counter;    // 修改共享资源
    g_mutex.unlock(); // 释放互斥锁
}

读写锁:std::unique_lock / std::shared_lock

std::unique_lock/std::shared_lock与std::mutex配合使用,实现读/写锁机制。还可以与其他基础锁配合。

std::unique_lock具有独占性,无法独占变量就阻塞。

std::shared_lock不具有独占性,只是占用锁,防止std::unique_lock获取锁,以阻塞他。

std::shared_mutex g_mutex;
int g_counter = 0;

void increment_counter() {
    std::unique_lock<std::shared_mutex> lock(g_mutex); // 获取共享锁
    ++g_counter; // 修改共享资源
    lock.unlock(); // 释放共享锁
}

void read_counter() {
    std::shared_lock<std::shared_mutex> lock(g_mutex); // 获取共享锁
    std::cout << "Counter value: " << g_counter << std::endl; // 读取共享资源
}

封装的互斥锁:std::lock_guard

是一种RAII锁保护机制,它可以自动对互斥量进行加锁和解锁,避免了使用裸互斥锁的复杂。

在离开第一个作用域(})时自动解锁。

std::mutex g_mutex;

void foo(int id) {
    std::lock_guard<std::mutex> lock(g_mutex);
    std::cout << "Thread " << id << " start" << std::endl;
    // 执行一些需要互斥访问的操作
    std::cout << "Thread " << id << " end" << std::endl;
}

std::atomic

能够通过硬件支持或操作系统提供的原子指令来实现对共享变量的原子操作,从而避免了使用锁的开销和复杂性。

使用atomic显著快于使用加锁操作实现原子操作。

std::atomic支持的操作包括:

std::atomic<int> g_count(0);

void foo(int id) {
    for (int i = 0; i < 10000; ++i) {
        ++g_count;
        g_count.fetch_add(1, std::memory_order_relaxed);
    }
}

其中std::memory_order_relaxed代表内存序。内存序问题见下节。atomic支持的内存序如下。

内存序 描述
memory_order_relaxed 没有同步或顺序制约,仅对此操作要求原子性
memory_order_acquire 当前线程中读或写不能被重排到此加载前
memory_order_release 当前线程中的读或写不能被重排到此存储后
memory_order_acq_rel 结合 acquire 和 release 两种内存序的效果,在读取和修改操作中都具有同步效果。
memory_order_seq_cst 是最强的内存序。所有操作都按照序列顺序执行,且对其他线程的修改和访问都具有同步效果。

std::condition_variable

用于实现异步通知。

std::condition_variable支持的操作包括:

std::mutex g_mutex;
std::condition_variable g_cv;
bool g_ready = false; //此处g_ready是为了防止虚假唤醒

void worker_thread() {
    std::unique_lock<std::mutex> lock(g_mutex);
    // 小心虚假唤醒 应当使用while
    while (!g_ready) {
        g_cv.wait(lock); // 等待条件变量被唤醒
        // g_cv.wait(lock, g_ready); //等价
    }
    std::cout << "Worker thread is running!" << std::endl;
}

int main() {
    std::thread t(worker_thread);

    // 做一些计算... 比如文件读写 之后g_ready会为true

    {
        std::lock_guard<std::mutex> lock(g_mutex);
        g_ready = true;
        g_cv.notify_one(); // 唤醒一个等待线程
    }

    t.join();
    return 0;
}

thread_local关键字

C++11。用于声明线程局部变量。与全局变量和局部变量不同,线程局部变量是每个线程独立拥有的变量,不同线程之间互不干扰。线程局部变量的生命周期与声明它的线程的生命周期一样长。每个线程都会拥有自己的变量实例,并且在线程结束时被自动销毁。

机器执行时的内存读写顺序

乱序执行问题

当CPU 遇到需要等待的指令时,它不会停下来等待这条指令执行完毕,而是会这时执行它后面还没有执行的指令。当这条指令被执行完毕后,CPU 再回过头去执行刚才因为等待这条指令而跳过的指令。

编译器和CPU都会进行乱序操作。

Memory Reordering Caught in the Act (preshing.com)

智能指针

以下指针都是模板类,使用时传入类型和对应类型的指针,例如std::shared_ptr<int> sp1(new int(42));。

unique_ptr

它是一个用于管理动态分配对象的类模板。它的特点在于,它是独占式的,也就是说,同一时间只能有一个unique_ptr指向一个对象。当该unique_ptr被销毁时,它所拥有的对象也会被自动销毁。

shared_ptr

它也用于管理动态分配对象。与unique_ptr不同的是,多个shared_ptr可以指向同一个对象,且对象的引用计数会随之增加。只有当所有shared_ptr被销毁时,对象的引用计数才会降为0,才会被销毁。

weak_ptr

它可以与shared_ptr协同工作。当一个对象被一个shared_ptr管理时,有时我们需要一个非拥有者的观察者,使用weak_ptr可以实现这个功能。weak_ptr不会改变引用计数,也不会影响所观察的对象的生命周期。

auto_ptr(已弃用)

auto_ptr 类的主要特点是“所有权的转移”,也就是说当一个 auto_ptr 对象拥有资源时,其它的 auto_ptr 对象不能拥有相同的资源。这意味着,当一个 auto_ptr 对象被赋值给另一个 auto_ptr 对象时,原始对象将失去对资源的控制权。这种特性可能会导致一些意外的行为,因此,C++11标准推荐使用 unique_ptr 来代替 auto_ptr。

#include <iostream>
#include <memory>
 
int main() {
    std::auto_ptr<int> ap1(new int(42)); // 创建一个 int 对象,并用 auto_ptr 管理它
    std::auto_ptr<int> ap2;
    
    ap2 = ap1; // 将 ap1 赋给 ap2,ap1 将失去对资源的所有权
    std::cout << "ap2 指向的整数值为:" << *ap2 << std::endl;
    
    // ap1 已经失去了对资源的所有权,因此不需要手动释放内存
    return 0;
}

make_shared和make_unique(推荐使用)

make_shared和make_unique都是C++11提供的工厂函数,用于创建智能指针(shared_ptr和unique_ptr)。

好处如下:

  1. 与直接使用new分配内存不同,工厂函数使用单个内存块来管理对象和控制块,将内存块和管理区放在一起,减少了堆内存分配的次数。
  2. 避免构造时出现异常,导致智能指针对象没有获得裸指针,从而导致内存泄漏。

示例代码:

std::shared_ptr<int> p = std::make_shared<int>(42);
std::unique_ptr<int> p = std::make_unique<int>(42);

特性:

© 2019 - 2023 · YuYoung's Blog