C++

151 阅读32分钟

虚函数

触发虚函数的三个条件:

  • 继承
  • 声明虚函数
  • 指针或者引用调用

示例代码如下:

#include <iostream>
class Base {
public:
    virtual void fun()
    {
        std::cout << "Base" << std::endl;
    }
};

class Derive : public Base {         //继承
public:
    virtual void fun()               //声明虚函数
    {
        std::cout << "Derive" << std::endl;
    }
};

int main() {
    Derive d;
    Base* p = &d;
    Base& r = d;
    p->fun();      //指针或者引用调用成员函数
    r.fun();
    return 0;
}

构造函数与析构函数

构造函数析构函数
调用创建对象时自动调用析构对象时自动调用
作用初始化成员释放资源
重载支持不支持

构造函数的形式

  • 默认构造函数
  • 拷贝构造函数 (用同类对象去初始化新对象)
  • 转移构造函数(since C++11)

构造函数的调用时机

  • 函数参数值传递 (拷贝构造函数)
  • 函数返回值传递 (拷贝构造函数)
  • 对象构造对象 (拷贝构造函数)
  • 临时对象构造对象 (转移构造函数)
  • 普通变量构造对象 (默认构造函数)

条件变量

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  ready = true;
  cv.notify_all();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_id,i);

  std::cout << "10 threads ready to race...\n";
  go();                       // go!

  for (auto& th : threads) th.join();

  return 0;
}

wait()函数执行流程

  1. unlock mutex

    wait 函数内部会unlock 以及lock

  2. block current thread and wait for notify

    add this thread to waiting-list(or wait to be notified), once cv is notified, resume execution:

  3. unblock current thread.
  4. lock mutex

    here is important, because it will still wait mutex to unlock, ifnotify is under mutex

注意虚假唤醒,需要通过外部变量进行 double check.

  • certain implementations may produce spurious wake-up calls without any of these functions being called

    Ref: www.cplusplus.com/reference/c…

  • 不建议直接使用 wait,推荐使用 while wait 或者 带条件的 wait, 第一防止虚假唤醒,第二防止漏掉(如果notify的时候,线程不处于wait,就 wait 不到)

notify最好不要在 mutex 里面,否则容易引发hurry up and wait的问题,示例代码如下。

Ref: en.cppreference.com/w/cpp/threa…

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying falsely...\n";
    cv.notify_one(); // waiting thread is notified with i == 0.
                     // cv.wait wakes up, checks i, and goes back to waiting

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done)
    {
        std::cout << "Notifying true change...\n";
        lk.unlock();
        cv.notify_one(); // waiting thread is notified with i == 1, cv.wait returns
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join();
    t2.join();
}

join and detach

  • join 阻塞当前线程,直到子线程结束运行
  • detach 分离线程,与当前线程没有关系

如果线程对象析构时未指定 detach 或者 join,将会导致程序crash.

future

async

#include <future>
#include <iostream>

bool is_prime(int x)
{
  for (int i=0; i<x; i++)
  {
    if (x - i == 0)
      return false;
  }
  return true;
}

int main()
{
  std::future<bool> fut = std::async(is_prime, 700020007);
  std::cout << "please wait";
  std::chrono::milliseconds span(100);
  while (fut.wait_for(span) != std::future_status::ready)
    std::cout << ".";
  std::cout << std::endl;

  bool ret = fut.get();
  std::cout << "final result: " << ret << std::endl;
  return 0;
}
  1. 首先创建线程执行 is_prime(700020007),返回一个 std::future 对象。
  2. 主线程使用 std::future::wait_for 等待结果返回,wait_for 可设置最长等待时间。
    • 任务完成,返回 std::future_status::ready
    • 任务尚未完成,返回 std::future_status::timeout
  3. 主线程既可使用 std::future::get 获取结果,且主线程阻塞至任务完成。

promise

#include <iostream>       // std::cout
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

void print_int(std::future<int>& fut) {
    std::cout << "start wait";
    int x = fut.get(); // 获取共享状态的值.
    std::cout << "value: " << x << '\n'; // 打印 value: 10.
}

int main()
{
    std::promise<int> prom; // 生成一个 std::promise<int> 对象.
    std::future<int> fut = prom.get_future(); // 和 future 关联.
    std::thread t(print_int, std::ref(fut)); // 将 future 交给另外一个线程t.
    std::this_thread::sleep_for(std::chrono::seconds(5));
    prom.set_value(10); // 设置共享状态的值, 此处和线程t保持同步.
    t.join();
    return 0;
}
  1. Promise 对象可保存 T 类型的值
  2. get_future 来获取与 promise 对象关联的对象,调用该函数之后,两个对象共享相同的共享状态(shared state)。
  3. Promise 对象某一时刻设置共享状态的值。
  4. Future 对象可以返回共享状态的值(阻塞)

packaged_task

相比与 async, 能够跟更加精细化控制线程的执行。

三者对比

相同点: 获取异步线程中返回数据 不同点:

  • async 层次最高,你只需要给它提供一个函数,它就会返回一个 future 对象。接下来就只需等待结果了。
  • packaged_task 次之,你在创建了 packaged_task 后,还要创建一个 thread,并把 packaged_task 交给它执行。
  • promise 就最低,在创建了 thread 之后,你还要把对应的 promise 作为参数传入。这还没完,别忘了在函数中手动设置 promise 的值。
#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int main()
{
    std::packaged_task<int()> task([](){
            std::chrono::milliseconds dura( 2000  );
            std::this_thread::sleep_for( dura  );
            return 0;
            });
    std::future<int> f1 = task.get_future();
    std::thread(std::move(task)).detach();

    std::future<int> f2 = std::async(std::launch::async, [](){
            std::chrono::milliseconds dura( 2000  );
            std::this_thread::sleep_for( dura  );
            return 0;
            });

    std::promise<int> p;
    std::future<int> f3 = p.get_future();
    std::thread([](std::promise<int> p){
            std::chrono::milliseconds dura( 2000  );
            std::this_thread::sleep_for( dura  );
            p.set_value(0);
            },
            std::move(p)).detach();

    std::cout << "Waiting..." << std::flush;
    f1.wait();
    f2.wait();
    f3.wait();
    std::cout << "Done!\nResults are: "
        << f1.get() << " " << f2.get() << " " << f3.get() << "\n";
    return 0;
}

线程池

C++ 线程池的实现

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>

class ThreadPool {
public:
    ThreadPool(size_t);    //构造函数,size_t n 表示连接数

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)   //任务管道函数
        -> std::future<typename std::result_of<F(Args...)>::type>;  //利用尾置限定符  std future用来获取异步任务的结果

    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
    std::vector< std::thread > workers;   //追踪线程
                                          // the task queue
    std::queue< std::function<void()> > tasks;    //任务队列,用于存放没有处理的任务。提供缓冲机制
    std::mutex queue_mutex;   //互斥锁
    std::condition_variable condition;   //条件变量?
    bool stop;
};

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads) : stop(false)
{
    for (size_t i = 0; i<threads; ++i)
        workers.emplace_back(     //以下为构造一个任务,即构造一个线程
            [this]
    {
        for (;;)
        {
            std::function<void()> task;   //线程中的函数对象
            {//大括号作用:临时变量的生存期,即控制lock的时间
                std::unique_lock<std::mutex> lock(this->queue_mutex);
                this->condition.wait(lock,
                    [this] { return this->stop || !this->tasks.empty(); }); //当stop==false&&tasks.empty(),该线程被阻塞 !this->stop&&this->tasks.empty()
                if (this->stop && this->tasks.empty())
                    return;
                task = std::move(this->tasks.front());
                this->tasks.pop();

            }

            task(); //调用函数,运行函数
        }
    }
    );
}

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)  //&& 引用限定符,参数的右值引用,  此处表示参数传入一个函数
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;
    //packaged_task是对任务的一个抽象,我们可以给其传递一个函数来完成其构造。之后将任务投递给任何线程去完成,通过
    //packaged_task.get_future()方法获取的future来获取任务完成后的产出值
    auto task = std::make_shared<std::packaged_task<return_type()> >(  //指向F函数的智能指针
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)  //传递函数进行构造
        );
    //future为期望,get_future获取任务完成后的产出值
    std::future<return_type> res = task->get_future();   //获取future对象,如果task的状态不为ready,会阻塞当前调用者
    {
        std::unique_lock<std::mutex> lock(queue_mutex);  //保持互斥性,避免多个线程同时运行一个任务

                                                         // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task]() { (*task)(); });  //将task投递给线程去完成,vector尾部压入
    }
    condition.notify_one();  //选择一个wait状态的线程进行唤醒,并使他获得对象上的锁来完成任务(即其他线程无法访问对象)
    return res;
}//notify_one不能保证获得锁的线程真正需要锁,并且因此可能产生死锁

 // the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();  //通知所有wait状态的线程竞争对象的控制权,唤醒所有线程执行
    for (std::thread &worker : workers)
        worker.join(); //因为线程都开始竞争了,所以一定会执行完,join可等待线程执行完
}


int main()
{

    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for (int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
            std::cout << "hello " << i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "world " << i << std::endl;
            return i*i;
        })
        );
    }

    for (auto && result : results)    //通过future.get()获取返回值
        std::cout << result.get() << ' ';
    std::cout << std::endl;

    return 0;
}

Ref: github.com/progschj/Th…

std::function 和 std::bind

bind 函数主要用来当作一个函数适配器。

auto f4 = std::bind(fun_1, n,placeholders::_1);

  • f4:std::function
  • fun1:函数名
  • n: 绑定的函数输入数值
  • place_holder: 使用时才输入值, 根据参数列表确定值传递还是引用传递

std 中使用 function 统一了

  • 普通函数
  • 匿名函数(lambada)
  • 含参函数(bind)

如果希望异步接受函数,可以通过 future 类(async,packaged_task,promise 来获取异步的结果)

当调用 bind 函数时,值已经传入,或者认为形参已经生成,即使后期改变传入变量值,bind 函数只会按照传入时的值执行.

Ref: blog.csdn.net/liukang325/…

四种类型转换

  1. static_cast //静态转换,不会进行安全检查
  2. dynamic_cast //动态转换,会执行安全检查
  3. const_cast //将 const 转成非 const
  4. reinpreter_cast //强制成任意类型

Ref: www.cnblogs.com/welfare/art…

右值引用

  1. 可以给临时变量“续命”
    int &&r1 = fun();
    auto r2 = [] {return 5; };
    
  2. 变量转移构造函数,避免深度拷贝
    A(A&& a) {this.data=a.data}
    
  3. 完美转发 std::forward
    void processValue(int& a) { cout << "lvalue" << endl; }
    void processValue(int&& a) { cout << "rvalue" << endl; }
    template <typename T>
    void forwardValue(T&& val)
    {
        //这里切记,&&可以认为是一种接口约定,且仅限于模板中,如果是左值就翻译成(T& val),如果是右值就翻译成(T&& val)
        //processValue(val); //无论左右值,都会按照左值进行转发
        processValue(std::forward<T>(val)); //照参数本来的类型进行转发
    }
    void Testdelcl()
    {
        int i = 0;
        forwardValue(i); //传入左值
        forwardValue(0);//传入右值
    }
    输出:
        lvaue
        rvalue
    

std::reference_wrapper

对应于C语言中的&,C++表达形式std::reference_wrapper<int> c = std::ref(a);

refernce_wrapper 相比于 T&的优点

  1. 支持动态修改绑定对象
  2. ref.get()返回的引用对象等同于 T&
  3. 通过 bind 机制在值传递函数中实现引用传递

变长参数函数

C语言中通过 va_list args 等去实现, 但C++中有三种选择,其中初始化列表是运行时,作为一个对象传入,其他都是编译期间自动推导的

initializer_list

通过大括号,生成一个 initializer_list 对象,将此作为一个大对象传入。

//原始写法
std::vector<int> vec;
vec.push_back(1);
vec.push_back(3);
vec.push_back(3);
vec.push_back(2);
//现代写法
std::vector<int> aa({1,2,3,4});
std::vector<int> aa={1,2,3,4};

//现代写法的其他场景
std::map<string, string> const nameToBirthday = {
       {"lisi", "18841011"},
       {"zhangsan", "18850123"},
       {"wangwu", "18870908"},
       {"zhaoliu", "18810316"},
   };

Ref: www.cnblogs.com/lysuns/p/42…

递归展开

需要有两个函数

  • 展开函数
  • 终止函数
#include <iostream>
using namespace std;
//递归终止函数
void print()
{
   cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
   cout << "parameter " << head << endl;
   print(rest...);
}

int main(void)
{
   print(1,2,3,4);
   return 0;
}

逗号表达式

编译期间{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )

template <class T>
void printarg(T t)
{
   cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
   int arr[] = {(printarg(args), 0)...};
}

expand(1,2,3,4);

模板元编程

将数值作为模板的参数,在编译期间就完成相关的计算, 例如求固定值的幂

template<int NUM>
class T
{
public:
    //enum { v = num * T<num - 1>::v };  also ok
    static const int value = NUM*T<NUM - 1>::value;
};

template<>
class T<1>
{
public:
    static const int value = 1;
};

int main() {
    std::cout << T<5>::value;
    return 0;
}

## VS #

int fun_a() { return 0; }
int fun_1() { return 5; }
#define print1_1(x) {std::cout<<##x##;}
#define print1_2(x) {std::cout<<x;}
#define print2_1(x) {std::cout<<fun_##x##();}
#define print2_2(x) {std::cout<<fun_x();}
#define print3_1(x) {std::cout<<x<<#x;}

int main(int i, char** args)
{
    print1_1(1);
    print1_2(1);

    print2_1(a);                    //normal, expand to     std::cout<<fun_a();
    //print2_2(a);                    //error expand to      std::cout<<fun_x();

    int buffer_number = 5;
    print3_1(buffer_number);        // normal, expand to std::cout<<buffer_number<<"buffer_number";
}
  • ##var##var 实际上一个东西,有点类似 cmake 中${var},不过很多情况下可以省略##,因为没有歧异产生。如print1 的两个函数。但是在print2中就不能省,因为前后有其他有效字符(a-z,_,0-9)等,编译器会认为它们时一个整体,就不会替换,例如print2_2,这时候需要加##来进行强行替换,如print2_1
  • #var 实际上时加了一个"",能够打印出变量的内部名字。如print3_1

C与C++的可变参数

#include <iostream>

//base
#define fun_1(str) {std::cout<<str<<std::endl;}

//example of "#"
#define fun_2(function) \
{   \
    if (!function)\
    {\
        std::cout << "[Error] in" << #function << std::endl;\
    }\
}

//example of "##"
#define fun_append(str1, str2, str3)   "HASH:"##str1##str2##str3

//example of "VA_ARGS"
#define LOG(...)     fprintf(stdout, __VA_ARGS__)

//mix example of virable agruments fuction and macro
template<typename T>  //must be ahead of template<typename T1, typename ...Rest>
void print(T t)
{
    std::cout << t << std::endl;
}

template<typename T1, typename ...Rest>
void print(T1 t1, Rest ...rest)
{
    std::cout << t1 << std::endl;
    print(rest...);  //rest... is a special virable,"rest" is nothing and cannot be used.
}

#define LOG2(...) print(__VA_ARGS__)


int checkNegtive(int val)
{
    return val < 0;
}

int main()
{
    fun_1("A");
    fun_2(checkNegtive(10));
    std::cout << fun_append("John", "23", "Male") << std::endl;
    LOG("ww_%d\n", 39);
    LOG2("love", 4, "you");
    return 0;
}

Ref: msdn.microsoft.com/zh-cn/libra…

少用#define

类型安全,无需括号

  • const 取代 define 变量
  • inline 取代 define 函数

构造函数执行顺序

  1. 基类构造函数
  2. 成员构造函数
  3. 自身构造函数
  • 当没有指定构造函数时,隐式的构造函数如下所示:D():Base(),Member(){};当需要指定初始值时,就需要显示调用构造函数了。
  • 推荐使用初始化表,避免某些编辑器二次赋值
  • 执行顺序与初始化表无关,与定义顺序有关

对象的两个指针

  1. this指针: 调用成员函数时,由编辑器传入。(成员函数也是一种普通函数)
  2. 虚表指针: 同一类型的对象各自拥有一个指针,指向同一个表

new 与 delete流程

new

  1. 申请空间
  2. 指针转换
  3. 构造函数

delete

  1. 析构函数
  2. 释放空间

overwrite 与 overload

  • overwrite 只适用于虚函数 //这里的 write 指的是重写虚函数表
  • overload 适用于函数重载

modern C++ 两大关键改进

  1. 变参模板
  2. std::move 操作

pragma once

这只能防止在一个 cpp 文件中重复包含一个.h 文件,而不是所有的 cpp 只包含一次,因为每个 cpp 都是独立编译的。因此,不要在头文件中定义变量。

volatile

背景:编译器优化过程中,会检查变量是否修改过,如果没有修改,可能直接从 cpu 缓存等缓存中读取,而不是内存。

方法:通过设置 volatile,避免编译器的优化,强制从内存中获取变量,而不是 CPU 的缓存。 遇到以下两种应用场景:

  1. 当汇编与 c 代码混合编程时,编译器无法知道汇编代码中是否修改过变量,可能直接从 cpu 缓存中读取,导致错误。
  2. 断点调试并且通过外部命令行进行变量设置,如果不设置成 volatile,就可能导致变量外部设置失败。

enum class

推荐使用enum class 代替enum ,否则以下操作就会通过编译,进而带来安全隐患。

enum  Color { black, white, red };
enum  Size {big, small };
Color cl=Color::black;
Size sz=Size::big;
if (sz == cl) //bad code
if (1==sz)  //bad code

RTTI 机制

运行时类型检查

  1. typeid 函数: 如果有虚函数,执行动态检查,否则执行静态检查。
  2. dynamic_cast: 一般用于向下的类型转换,运行时会动态检查以及调整。 //占用运行时间,且有其他隐患

Ref: www.jianshu.com/p/3b4a80adf…

虚继承

为了解决菱形继承中数据双份的问题,比如

class Animal
{
int weight=0;
}
class CrawlAnimal : public Animal
{
}
class MammalAnimal : public Animal
{
}
//horse 既是爬行动物又是哺乳动物,所以可能会继承这两个类。但是weight就会有两份

虚继承相比于普通的继承,子类会多出一个指针,这个指针(virtual base table pointer)指向一个表,这个表记录了父类成员的偏移值。当这中类再次被继承的时候(这里指的普通继承),会重写 virtual base table 的值以及合并虚基类对象,并且保留只有一个对象。

Ref: blog.csdn.net/xiejingfa/a…

实验代码如下

struct A {
	int data_a;
	virtual void print() { std::cout << "A" << std::endl; }
};
struct B : public virtual A {
	virtual void Bprint() { std::cout << "B_only" << std::endl; }
};
struct C : public virtual A {
	virtual void print() { std::cout << "C"<<std::endl; }

};

struct D : public B, public C {
	//virtual void print() { std::cout << "B_only" << std::endl; }
	//virtual void print() { std::cout << "D" << std::endl; }
};

int main() {
	std::cout << sizeof(A);
	std::cout << sizeof(B);
	std::cout << sizeof(C);
	std::cout << sizeof(D);
	D d;
	d.print();
	d.A::print();
	}
/*vs 输出结果
8
16
12
20
C
A
*/
内存分布
A: VPTR_a | data_a
B: VBPTR_a | VPTR_b | VPTR_a | data_a  
C: VBPTR_a | VPTR_a | data_a //因为 C 中没有新加虚函数,所以不需要第二个虚表指针。
D: VBPTR_a | VPTR_b| VBPTR_a | VPTR_a | data_a //两个 VBPTR 分别继承自 B C 两个对象

继承中的内存分布

  • 简单继承

    Base data
    Added data
    
  • 虚函数

    VPTR //创建一个新表并覆盖重写,VPTR 指向新表
    Base data
    Added data
    
  • 虚继承

    VBPTR //VBPTR 指向表,此表记录了 BaseData 的偏移值
    Added data
    Base data 1. 头部新增 base class table pointer 2. 旧数据移至最后
    
  • 虚函数+虚继承

    VPTR_new // 新虚函数表指针(指向了新增的虚函数)
    VBPTR //VBPTR 指向表,此表记录了 BaseData 的偏移值
    Added data
    VPTR_old // 旧虚函数表指针
    Base data
    
  • 多重继承过程中,虚继承的部分执行合并操作。
  • 继承过程中,无论是虚表指针,还是虚类表指针都需要重写,所指向内容也需要新建。

thread_local

每次线程拥有一个独立的 static 对象。而不是全局的 static 对象。

反射机制

java 可以在运行时动态获取对象的方法以及变量。

注意,指向派生类对象的基类指针只能调用基类中声明过的方法以及成员,不可以运行时增加,所以 c++没有反射机制。Ref: www.zhihu.com/question/24…

mutable

一般用于设置此变量可修改,即使在 const 函数中。

还有一个应用就是 lambda 表达式中,修改值捕获的量,但是这不多见。

模板 template

模板实例化的过程(由模板变成普通函数或者类)

  1. 自动推导参数/指定参数

    只有函数模板支持自动推导,而且未必能成功(C++17支持类模板自动推导) 参数可以是类型参数,也可以是非类型参数

  2. 替换参数,将具体参数替换不同的模板,每个模板生成一个的函数接口

    这里仅仅是函数接口推导,不编译 之所以有多个模板,因为函数的重载,类模板不存在此情况,只会有一个模板 特例化模板与通用模板属于同一个模板

  3. 接口推导,丢弃推导失败的与保留推导成功的接口。

    SFINAE,”从一组重载函数中删除模板实例化无效的函数”。 至少要有一个候选项,否则就失败。

  4. 编译函数,选择最优的接口编译。

    如果最优接口中存在特化,直接编译特化。 有可能出现 ambiguity

模板偏特化

Demo

template<class T1, class T2, int I>
class A {};            // primary template

template<class T, int I>
class A<T, T*, I> {};  // #1: partial specialization where T2 is a pointer to T1

template<class T, class T2, int I>
class A<T*, T2, I> {}; // #2: partial specialization where T1 is a pointer

template<class T>
class A<int, T*, 5> {}; // #3: partial specialization where T1 is int, I is 5,

template<>
class A<int, int, 5> {}; // #4: fully specialization

注意事项:

  1. 特例化用在模板之后
  2. 类后面写全部参数
  3. template 后面声明未特例化的参数(包括类型参数与非类型参数)。

SFINAE

SFINAE 的的全称是 Substitution Failure Is Not An Error。In the process of template argument deduction, a C++ compiler attempts to instantiate signatures of a number of candidate overloaded functions to make sure that exactly one overloaded function is available as a perfect match for a given function call. www.jianshu.com/p/45a2410d4…

大白话,就是针对多个模板同时推倒,不需要每个模板都正确,有失败的无所谓,只要有一个活着就可以。应用最为广泛的场景是 C++中的std::enable_if

  1. 首先利用特例化模板,生成一个有效或者无效的类型,并将此类型传递至下一次推导
  2. 利用SFINAE,选出了支持的函数。

enable_if 内部实现原理

template<bool x, typename T1=void>
class MyEnable
{
public:
    MyEnable() {
        std::cout << "default";
    }
};

//specialization template
template<typename T>
class MyEnable<true,T>
{
public:
    typedef T type;
    MyEnable() {
        std::cout << "second";
    }
};

enable_if 的使用 demo


//自己实现的常用的example,设置print2仅支持两种类型vector<int>,vector<string>。
template <class T>
typename std::enable_if<std::is_same<T, std::vector<int>::value>::value, void>::type
print2(T& i)
{
    for (const auto& p: i)
        std::cout << p;
}
template <class T>
typename std::enable_if<std::is_same<T, std::vector<std::string>::value>, void>::type
print2(T& i)
{
    for (const auto& p: i)
        std::cout << p;
}
//case 1: T = int  两者都推导失败,编译错误
//case 2: T = vector<int> 前者成功,后者失败
//case 3:T = vector<string> 前者失败,后者成功

type traits

  1. is_same 判断两个类型是否相等。
//常规用法
bool isInt = std::is_same<int, int>::value; //为true

//但其检查很严格,如下
std::cout << "int, const int: " << std::is_same<int, const int>::value << std::endl;//false
std::cout << "int, int&: " << std::is_same<int, int&>::value << std::endl;//false
std::cout << "int, const int&: " << std::is_same<int, const int&>::value << std::endl;//false
  1. decay
//能够进行类型退化,再进行类型检查
typedef std::decay<int>::type A;           // int
typedef std::decay<int&>::type B;          // int
typedef std::decay<int&&>::type C;         // int
typedef std::decay<const int&>::type D;    // int
typedef std::decay<int[2]>::type E;        // int*
typedef std::decay<int(int)>::type F;      // int(*)(int)

Ref: blog.csdn.net/czyt1988/ar…

  1. remove_cv

remove either const and/or volatile,其中 decay 中包括了这个操作。

typedef const volatile char cvchar;
std::remove_cv<cvchar>::type a;       // char a
std::remove_cv<char* const>::type b;  // char* b
std::remove_cv<const char*>::type c;  // const char* c (no changes)

类似的操作还有

remove_pointer
remove_const
  1. tuple
int main()
{
    auto t = std::make_tuple(1, "Foo", 3.14);
    // 基于下标的访问
    std::cout << "(" << std::get<0>(t) << ", " << std::get<1>(t)
        << ", " << std::get<2>(t) << ")\n";
    // 基于类型的访问
    std::cout << "(" << std::get<int>(t) << ", " << std::get<const char*>(t)
        << ", " << std::get<double>(t) << ")\n";
    // 注意: std::tie 和结构化绑定亦可用于分解 tuple
}
  1. enable_if

主要作用就是当某个 condition 成立时,enable_if 可以提供某种类型, 具体见上面。

#include <iostream>
#include <type_traits>

#define Print(trait) std::cout<<#trait<<"\t"<<std::##trait<T>::value<<std::endl

template <typename T>
struct Show
{
	Show()
	{
		Print(is_void);
		Print(is_null_pointer);
		Print(is_integral);
		Print(is_floating_point);
		Print(is_array);
		Print(is_enum);
		Print(is_union);
		Print(is_class);
		Print(is_function);
		Print(is_pointer);
		Print(is_lvalue_reference);
		Print(is_rvalue_reference);
		Print(is_member_object_pointer);
		Print(is_member_function_pointer);
		Print(is_fundamental);
		Print(is_arithmetic);
		Print(is_scalar);
		Print(is_object);
		Print(is_compound);
		Print(is_reference);
		Print(is_member_pointer);
		Print(is_const);
		Print(is_volatile);
		Print(is_trivial);
		Print(is_trivially_copyable);
		Print(is_standard_layout);
	}
};

int main() {
	Show<int>();
	return 0;
}

字符集

  1. ASCII 编码
    计算机设计之初

  2. ANSI 编码(本地化)
    2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。

  3. UNICODE(国际化)
    国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。

  • 区分字符集与编码(utf-8,16,32),字符集建立数字与字符之间的映射关系,编码为了省内存
  • visual studio 中Multi-Byte Character Set(ANSI)Unicode Character Set,前者每个字符占字节数不固定,后者每个字符占有两个字节,当设置 unicode 时,内部定义 unicode 宏,对于_T("xxx"),内部会在字符串前面加 L"..",但是前者就不会进行任何操作。std::string 默认前者,std::wstring 使用后者。

Ref: blog.csdn.net/luoweifu/ar…

Base64 编码

目的: 是把二进制码转成可打印的 str,便于在网络(http 请求,邮件等)上传输。
原理:每 64bit 转成对应的一个符号。

Ref: www.ruanyifeng.com/blog/2008/0…

左右值

简单来说,常规自定义变量为左值,右边值就是没有名字的临时值。c++类可以根据对象的左右值属性调用不同的函数,如下所示。 stackoverflow.com/a/28066865/…

&符号在 c++的多种含义

  • 用在变量声明处,申明引用
  • 加载变量前面,取地址
  • 表达式之间,按位与
  • 用在函数参数列表中,引用传递

T&&

  • 左值引用可以绑定任何值,但是右值引用只能绑定右值。
  • && 与右值引用并不是完全划等号。
  • universal reference(&&) 两个条件:T&&以及 deduced type T(且不能加任何修饰))
  • auto&& 也是一个 URef
  • URef handle everything.然后根据实际情况变成 LRef 或者 RRef。

not all T&& in template are URefs;

template<class T>
class vector
{
public:
void push_back(const T& x); //LRef
void push_back(T&& x);  //RRef, 因为这里的T不是自动推导,而是外部指定而来的。
template<class... Args>
void emplace_back(Args&&... args); // URef
}

右值引用后,参数仍然为左值

void doWork(Widget&& param)
{
 //param 的类型是一个右值引用,但是他是一个左值,因为可以取名字。
 //通常这种结构,需要std::move(param).
}

T&& 前面加了任何东西,都不是 URef,而是 RRef

template<typename T>
void f(T&& param);  //URef

template<typename T>
void f(const T&& param); //RRef

template<typename T>
void f(std::vector<T>&& param); //RRef

推导冲突合并

template<typename T>
void f(T&& param);

Widget w;
f(w); //推导出 T is Widget&
//函数的接口变为 void f<Widget&>(Widget& && param);
//合并后的接口为 void f<Widget&>(Widget& param);
f(std::move(w)); //推导出 T is Widget
//函数的接口变为 void f<Widget>(Widget&&)

//以下是引用合并规则
T& & = T&
T&& & = T&
T& && = T&
T&& && = T&&

Ref: channel9.msdn.com/Shows/Going…

remove_cv 的用法

#include <iostream>
#include <type_traits>

int main() {
    typedef std::remove_cv<const int>::type type1;
    typedef std::remove_cv<volatile int>::type type2;
    typedef std::remove_cv<const volatile int>::type type3;
    typedef std::remove_cv<const volatile int*>::type type4;
    typedef std::remove_cv<int * const volatile>::type type5;

    std::cout << "test1 " << (std::is_same<int, type1>::value
        ? "passed" : "failed") << '\n';
    std::cout << "test2 " << (std::is_same<int, type2>::value
        ? "passed" : "failed") << '\n';
    std::cout << "test3 " << (std::is_same<int, type3>::value
        ? "passed" : "failed") << '\n';
    std::cout << "test4 " << (std::is_same<const volatile int*, type4>::value
        ? "passed" : "failed") << '\n';
    std::cout << "test5 " << (std::is_same<int*, type5>::value
        ? "passed" : "failed") << '\n';
}

注意 type4 与 type5 的区别,const volatile int* 这些修饰符并不是修饰的指针本身,而是指针所指的对象,所以 type4 并没有变化,而 type5 修饰的是指针本生,所以变成了int*.

可变参数模板

解释:针对输入参数不确定的情况下,可以考虑使用可变参数模板
原理:其内部会根据函数模板递归推导出很多函数。
用法:掌握好三个名字

  • typename... Args is called a template parameter pack
  • Args... args is called a function parameter pack
  • args... 参数
template<typename T>
decltype(auto) fun(T t)
{
	return t;
}

template<typename T,typename ...Args>
decltype(auto) fun(T t,Args... args)
{
	return t+fun(args...);
}

int main()
{
	std::cout<<fun(10, 20, 30);
	return 0;
}
  • 函数参数第一个都需要普通的参数传递,这样在迭代的过程中,随着深度的增加,参数越来越少。
  • c++17 中可以使用迭代(fold expression)代替递归。

析构函数

  • 调用子类的析构函数,会自动调用父类的析构函数
  • 为什么需要将父类的析构函数声明为虚函数?
    因为 new 的是子类对象,调用的是子类的构造函数,但是 delete 却是基类的指针,调用的是基类的析构函数,不会执行子类的析构函数。 所以将父类析构函数声明为虚函数,这样会进行虚函数查表自动调用到子类的析构函数,同时子类析构会自动调用父类析构,父类的析构函数也会执行。

引用

常见于两种情况:

  1. Base& a= Derive();实现多态
  2. 引用传递与引用返回来避免拷贝,以及直接修改值。
//原理上就是指针
void fun(A& a)
{
	a.data = 3;
}

A a
fun(a);

//等价版本
void p_fun(A* p_a)
{
	p_a->data = 3;
}

A a;
p_fun(&a);

/////////////////////////////////////////////////
A& get()
{
	A temp;
	return temp;
}

A a;
a = get();
//等价版本
A* get()
{
	A temp;
	return &temp;
}

A a;
a = *(get()); //可以看出如果不用引用去接受返回值,一样要进行拷贝,并没有达到目的。

lambda 表达式

lambda 表达式本质上是函数,不过这个函数没有名字(匿名函数)。他的参数传递有两个阶段,第一是捕获,第二是常规传递。常规传递可忽略。这里着重说明一下捕获操作。 捕获分为值捕获以及引用捕获。

  • [=]捕获操作里面会执行拷贝,捕获的时候,会缓存这个变量
  • [&]没有拷贝。
  • [this] 就在成员函数里面,捕获 this 指针,就可以访问所有的成员变量。
//test code
class A
{
public:
	int data;
	A(int d) { data = d; }
	A(const A& a)
	{
		data = a.data;
		std::cout <<data<< "copy....\n"<<std::endl;
	}

};

int main()
{

		A aa{ 123 };
		std::cout << "original data = " << aa.data << "\n";
		auto lambuda1 = [=]() {std::cout << "lambuda1 data = " << aa.data  << "\n"; };
		auto lambuda2 = [&]() {std::cout << "lambuda2data = " << aa.data << "\n"; };
		aa.data = 100;
		std::cout << "changed data = " << aa.data << "\n";
		lambuda1();
		lambuda2();

	return 0;
}

循环引用

使用类的前向声明,可以解决循环引用的问题。

return 返回

class Test
{
public:
	int data{0};
	Test() = default;
	Test(const Test& t) :data(t.data)
	{
		std::cout << "copy constructor"<<std::endl;
	}
	void operator=(const Test& t)
	{
		data = t.data;
		std::cout << "assign constructor" << std::endl;
	}
	Test& getDataByReference()
	{
		return *this;
	}
	Test getDataByValue()
	{
		return *this;
	}
};
int main()
{
	Test t;
	std::cout << "------------test1-----------------\n";
	Test t1_1 = t.getDataByReference();     //with copy (copy constructor)
	Test t1_2;
	t1_2 = t.getDataByReference();         //with copy (assign constructor)
	Test& t1_3 = t.getDataByReference();   //without copy
	t.getDataByReference();                //without copy too.
	//test 2
	std::cout << "------------test2-----------------\n";
	Test t2_1 = t.getDataByValue();        //with copy (copy constructor)
	Test t2_2;
	t2_2 = t.getDataByValue();             //with copy twice(copy constructor, then assign constructor)
	Test& t2_3 = t.getDataByValue();       //with copy (copy constructor)
	t.getDataByValue();                    //with copy (copy constructor)
	Test&& t2_4 = t.getDataByValue();      //with copy (copy constructor)
	return 0;
}

返回类型为引用

  • 接收类型与返回类型同时为引用时,才是真正的引用(不会发生拷贝)。t1_2
  • 直接将返回值当左值使用也行。t1_3

返回类型为值

  • 都会发生至少一次的内存拷贝。
  • t2_3, t2_4 只有一次,第二次是引用,引用了临时创建的变量(右值)。
  • t2_1 只有一次,因为编译器的优化,如果没有优化就是两次相同的拷贝构造函数,现在就变成了直接拷贝到新建的对象了,不需要再有临时对象去中转了。
  • t2_2 发生两次 copy,首先生成了一个临时变量,然后在由这个临时变量去调用赋值函数。这里的两次 copy 是无法优化的。因为是两个不同的函数,return 返回调用拷贝构造,赋值调用=重载函数。

智能指针的简单实现

  • 两个指针指着两个堆对象(data 与 counter)
  • 析构函数,只有计数为 0 的时候,才执行 delete

class Data
{
public:
	int m_data{ 10 };
	~Data()
	{
		std::cout << "Deconstruct.\n";
	}
};

template <class DataType>
class SharePointer
{
	size_t* m_counter = nullptr;
	DataType* m_data = nullptr;
public:
	SharePointer(DataType* data)
	{
		m_counter = new size_t();
		*m_counter = 1;
		m_data = data;
	}
	SharePointer(const SharePointer& p)
	{
		m_counter = p.m_counter;
		m_data = p.m_data;
		(*m_counter)++;
	}
	~SharePointer() {
		(*m_counter) -= 1;
		if (*m_counter == 0)
		{
			delete m_data;
			delete m_counter;
		}
	}
};

int main()
{
	SharePointer<Data> p1(new Data());
	SharePointer<Data> p2 = p1;
	SharePointer<Data> p3 = p2;
}

交叉引用本质的原因,智能指针析构时,引用计数不为 0,不能 delete 对象。

工程项目小技巧

#include <assert.h>
#include <limits>
#include <iostream>
using namespace std;

//the following 3 ways to exit program during the differnt step.

//1. exit compiling via MACRO when some env are not correct.
#if !defined(WIN32)
#error "it only supported in windows OS".
#endif

//2. exit compiling during template deducing when some var are not correct.
template<typename T, int size>
struct A
{
	A()
	{
		static_assert(std::is_arithmetic<T>::value, "only supports arithmetic types");
		static_assert(!(size > 300), "size is too large");
		static_assert(!(sizeof(T) >4), "type is too large");
	}
};
void static_asserdemo()
{
	A<int, 10> a;
	//A<int, 1000> b;
	//A<void*, 10> c;
}

//3. exit program during running if some var is empty.
//it only works for Debug, because there are "#define NDEBUG" in other modes.
void* checkPointer()
{
	std::cout << "run"<<__func__<<std::endl;
	return nullptr;
}
void assertdemo(const char* path = nullptr)
{
	assert(checkPointer()); //this function won't run in other modes except debug.
}


int main()
{

	return 0;
}

统一初始化

C++ 11 之后,引入{}来统一初始化.

  1. 避免歧义
Widget w1(10);  // 调用Widget的带参构造函数
Widget w2();    //声明函数
Widget w2{};   // 无歧义
Widget w2;     // 无歧义

  1. 类内成员的默认初始值
class Widget {
  ...
private:
  int x{ 0 };   // x的默认初始值为0
  int y = 0;    // 同上
  int z( 0 );   // 报错
}
  1. std::initializer_list 在构造函数中,只要形参不带有 std::initializer_list,圆括号和大括号行为一致.
class Widget {
public:
  Widget(int i, bool b);
  Widget(int i, double d);
  ...
};

Widget w1(10, true);  // 调用第一个构造函数

Widget w2{10, true};  // 调用第一个构造函数

Widget w3(10, 5.0);   // 调用第二个构造函数

Widget w4{10, 5.0};   // 调用第二个构造函数

但是如果有(不能当成普通的容器处理),就会强制使用 std::initializer_list(即使参数正常匹配)

class Widget {
public:
  Widget(int i, bool b);
  Widget(int i, double d);
  Widget(std::initializer_list<long double> il);
  ...
};
Widget w1(10, true);   // 使用圆括号,调用第一个构造函数

Widget w2{10, true};   // 使用大括号,强制调用第三个构造函数,10和true被转换为long double

Widget w3(10, 5.0);    // 使用圆括号,调用第二个构造函数

Widget w4{10, 5.0};    // 使用大括号,强制调用第三个构造函数,10和5.0被转换为long double

但是有个特殊情况,即使用了{},也会调用非 std::initializer_list 的函数。

class Widget {
public:
  Widget();
  Widget(std::initializer_list<int> il);
  ...
};

Widget w1;     // 调用默认构造函数
Widget w2{};   // 调用默认构造函数
Widget w4({});   // 用了一个空的list来调用带std::initializer_list构造函数

Ref: zhuanlan.zhihu.com/p/268894227

哈希函数

C++标准库中提供了基本的哈希函数,能够将 key 转成 hash_code(int),这里 key 的类型包括常见的 int,float,void,string 等等。但是如果是自定义的类,就需要自己去些哈希函数,转成一个 int。这里的 hash_code 只是一个值,并非地址索引。一般来说,key 不会相同,hash_code 可以相同(冲突),但这回导致索引时间增长。

分配器

  • 可以重写 allocate 与 deallocate 方法来自定义分配细节,来控制实际分配的内存。
  • 不可以改变容器的扩充策略(尽管有些时候也是过度请求)。
  • 比如,分配不同 device 的内存(非 cpu memory),只能自定义分配器。
  • 比如,当 vector 的 size 是 4,且 capacity 也是 4,这时候继续 push,那 vector 可能要求分配 8,而这时候我们可以多分配些,避免多次申请。
  • 比如,当 list 不断与释放内存的时候,后台可以做成一个内存池。
#include <iostream>
#include <vector>
using namespace std;

template <typename T>
class mmap_allocator : public std::allocator<T>
{
public:
    typedef size_t size_type;
    typedef T* pointer;
    typedef const T* const_pointer;

    template<typename _Tp1>
    struct rebind
    {
        typedef mmap_allocator<_Tp1> other;
    };

    pointer allocate(size_type n, const void* hint = 0)
    {
        fprintf(stderr, "Alloc %d bytes.\n", n * sizeof(T));
        return std::allocator<T>::allocate(n);
    }

    void deallocate(pointer p, size_type n)
    {
        fprintf(stderr, "Dealloc %d bytes (%p).\n", n * sizeof(T), p);
        return std::allocator<T>::deallocate(p, n);
    }

    mmap_allocator() throw() : std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
    mmap_allocator(const mmap_allocator& a) throw() : std::allocator<T>(a) { }
    template <class U>
    mmap_allocator(const mmap_allocator<U>& a) throw() : std::allocator<T>(a) { }
    ~mmap_allocator() throw() { }
};


int main()
{
    std::vector<int, mmap_allocator<int>> aa;
}

fold expression(折叠表达式)

可以使用这个替换模板的循环递归。

  1. 只支持一个运算符的不断递归
  2. 没有递归调用,只是括号内的迭代展开,最终形成一个很长的括号内容。
  3. 左右不同顺序,导致括号顺序不同。
  4. ...代表折叠内容,args代表已展开内容。
template<typename ...Args>
int sum1(Args&&... args)
{
	return (args + ...);
}


template<typename ...Args>
int sum2(Args&&... args)
{
	return (... + args);
}


template<typename ...Args>
void print(Args&&... args)
{
	(std::cout << ... <<args);
}

int main()
{
	sum1(1, 2, 3); //(1+(2+3))
	sum2(1, 2, 3); //((1+2)+3)
	print(1, 2, "str"); //(((std::cout<<1)<<2)<<"str")
}

类型推导

T 可能被推导为T&& T&/T* T

Ref: www.bilibili.com/video/BV1H7…

template<typename T>
void f(T& param);

int x = 22;
const int cx = x;
const int& rx = x;
f(x);          //T=int, param type = int&
f(cx);         //T=const int, param type = const int&
f(rx);         //T=const int, param type = const int&
template<typename T>
void f(const T& param);

int x = 22;
const int cx = x;
const int& rx = x;
f(x);          //T=int, param type = int&
f(cx);         //T=int, param type = const int&
f(rx);         //T=int, param type = const int&
template<typename T>
void f(T* param);

int x = 22;
const int* pcx = &x;

f(&x);          //T=int, param type = int*
f(pcx);         //T=const int, param type = const int*
int x = 22;
const int cx = x;
const int& rx = x;
auto& v1 =x;  //auto=int, param type= int&
auto& v2 = cx; //auto=const int, param type=const int&
auto& v3 = rx; //auto=const int, param type=const int&
const auto& v4 = x; //auto=int, param type= const int&
const auto& v5 = cx; //auto=int, param type= const int&
const auto& v6 = rx; //auto=int, param type= const int&
template<typename T>
void f(T&& param);
f(expr);

int x = 22;
const int cx = x;
const int &rx = x;

f(x);  //T=int&, param type = int &
f(cx); //T=const int&, param type =const int &
f(rx); //T=const int&, param type =const int &
f(22); //T=int, param type = int &&

template<typename T>
void f(T param);
f(expr);

int x = 22;
const int cx = x;
const int &rx = x;

f(x);  //T=int, param type = int
f(cx); //T=const int, param type =const int
f(rx); //T=const int, param type =const int

int x = 22;
const int cx = x;
const int &rx = x;

auto v1 =x;  //auto=int ,param type = int
auto v2 =cx; //auto=int ,param type = int
auto v3 =rx; //auto=int ,param type = int
auto v4 =rx; //auto=int, param type = int
auto & v5 = rx; //param type = const int&
auto&& v6 = rx; //param type = const int&
  • auto 在推导的时候,忽略 refence 以及 const. expr's reference/const qualifiers always dropped in deducing T
  • auto 不会推导出引用,引用需要用户手动加上去。

自定义对象支持auto遍历

结构体需要支持 begin 以及 end 函数。

struct cstring {
  const char* ptr = 0;
  const char* begin() const { return ptr?ptr:""; }// return empty string if we are null
  null_sentinal_t end() const { return {}; }
};

cstring str{"abc"};
for (char c : str) {
    std::cout << c;
}

Ref: stackoverflow.com/questions/8…

gcc in windows

  • MinGW is the gcc compiler in windows.
  • Cygwin provide the emulation layer of Uninx api in Windows.
  • MSYS2 is a fork version of Cygwin, there are a little difference.