虚函数
触发虚函数的三个条件:
- 继承
- 声明虚函数
- 指针或者引用调用
示例代码如下:
#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()函数执行流程
- unlock mutex
wait 函数内部会
unlock以及lock - block current thread and wait for
notifyadd this thread to waiting-list(or wait to be notified), once cv is notified, resume execution:
- unblock current thread.
- lock mutex
here is important, because it will still wait mutex to unlock, if
notifyis under mutex
注意虚假唤醒,需要通过外部变量进行 double check.
- certain implementations may produce spurious wake-up calls without any of these functions being called
- 不建议直接使用 wait,推荐使用 while wait 或者 带条件的 wait, 第一防止虚假唤醒,第二防止漏掉(如果notify的时候,线程不处于wait,就 wait 不到)
notify最好不要在 mutex 里面,否则容易引发hurry up and wait的问题,示例代码如下。
#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;
}
- 首先创建线程执行 is_prime(700020007),返回一个 std::future 对象。
- 主线程使用 std::future::wait_for 等待结果返回,wait_for 可设置最长等待时间。
- 任务完成,返回 std::future_status::ready
- 任务尚未完成,返回 std::future_status::timeout
- 主线程既可使用 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;
}
- Promise 对象可保存 T 类型的值
- get_future 来获取与 promise 对象关联的对象,调用该函数之后,两个对象共享相同的共享状态(shared state)。
- Promise 对象某一时刻设置共享状态的值。
- 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;
}
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 函数只会按照传入时的值执行.
四种类型转换
- static_cast //静态转换,不会进行安全检查
- dynamic_cast //动态转换,会执行安全检查
- const_cast //将 const 转成非 const
- reinpreter_cast //强制成任意类型
右值引用
- 可以给临时变量“续命”
int &&r1 = fun(); auto r2 = [] {return 5; }; - 变量转移构造函数,避免深度拷贝
A(A&& a) {this.data=a.data} - 完美转发 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&的优点
- 支持动态修改绑定对象
- ref.get()返回的引用对象等同于 T&
- 通过 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"},
};
递归展开
需要有两个函数
- 展开函数
- 终止函数
#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;
}
少用#define
类型安全,无需括号
- const 取代 define 变量
- inline 取代 define 函数
构造函数执行顺序
- 基类构造函数
- 成员构造函数
- 自身构造函数
- 当没有指定构造函数时,隐式的构造函数如下所示:
D():Base(),Member(){};当需要指定初始值时,就需要显示调用构造函数了。- 推荐使用初始化表,避免某些编辑器二次赋值
- 执行顺序与初始化表无关,与定义顺序有关
对象的两个指针
- this指针: 调用成员函数时,由编辑器传入。(成员函数也是一种普通函数)
- 虚表指针: 同一类型的对象各自拥有一个指针,指向同一个表
new 与 delete流程
new
- 申请空间
- 指针转换
- 构造函数
delete
- 析构函数
- 释放空间
overwrite 与 overload
- overwrite 只适用于虚函数 //这里的 write 指的是重写虚函数表
- overload 适用于函数重载
modern C++ 两大关键改进
- 变参模板
- std::move 操作
pragma once
这只能防止在一个 cpp 文件中重复包含一个.h 文件,而不是所有的 cpp 只包含一次,因为每个 cpp 都是独立编译的。因此,不要在头文件中定义变量。
volatile
背景:编译器优化过程中,会检查变量是否修改过,如果没有修改,可能直接从 cpu 缓存等缓存中读取,而不是内存。
方法:通过设置 volatile,避免编译器的优化,强制从内存中获取变量,而不是 CPU 的缓存。 遇到以下两种应用场景:
- 当汇编与 c 代码混合编程时,编译器无法知道汇编代码中是否修改过变量,可能直接从 cpu 缓存中读取,导致错误。
- 断点调试并且通过外部命令行进行变量设置,如果不设置成 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 机制
运行时类型检查
- typeid 函数: 如果有虚函数,执行动态检查,否则执行静态检查。
- dynamic_cast: 一般用于向下的类型转换,运行时会动态检查以及调整。 //占用运行时间,且有其他隐患
虚继承
为了解决菱形继承中数据双份的问题,比如
class Animal
{
int weight=0;
}
class CrawlAnimal : public Animal
{
}
class MammalAnimal : public Animal
{
}
//horse 既是爬行动物又是哺乳动物,所以可能会继承这两个类。但是weight就会有两份
虚继承相比于普通的继承,子类会多出一个指针,这个指针(virtual base table pointer)指向一个表,这个表记录了父类成员的偏移值。当这中类再次被继承的时候(这里指的普通继承),会重写 virtual base table 的值以及合并虚基类对象,并且保留只有一个对象。
实验代码如下
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
模板实例化的过程(由模板变成普通函数或者类)
- 自动推导参数/指定参数
只有函数模板支持自动推导,而且未必能成功(C++17支持类模板自动推导) 参数可以是类型参数,也可以是非类型参数
- 替换参数,将具体参数替换不同的模板,每个模板生成一个的函数接口
这里仅仅是函数接口推导,不编译 之所以有多个模板,因为函数的重载,类模板不存在此情况,只会有一个模板 特例化模板与通用模板属于同一个模板
- 接口推导,丢弃推导失败的与保留推导成功的接口。
SFINAE,”从一组重载函数中删除模板实例化无效的函数”。 至少要有一个候选项,否则就失败。
- 编译函数,选择最优的接口编译。
如果最优接口中存在特化,直接编译特化。 有可能出现 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
注意事项:
- 特例化用在模板之后
- 类后面写全部参数
- 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
- 首先利用特例化模板,生成一个有效或者无效的类型,并将此类型传递至下一次推导
- 利用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
- 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
- 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)
- 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
- 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
}
- 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;
}
字符集
-
ASCII 编码
计算机设计之初 -
ANSI 编码(本地化)
2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。 -
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使用后者。
Base64 编码
目的: 是把二进制码转成可打印的 str,便于在网络(http 请求,邮件等)上传输。
原理:每 64bit 转成对应的一个符号。
左右值
简单来说,常规自定义变量为左值,右边值就是没有名字的临时值。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&&
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... Argsis called a template parameter packArgs... argsis called a function parameter packargs...参数
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 却是基类的指针,调用的是基类的析构函数,不会执行子类的析构函数。 所以将父类析构函数声明为虚函数,这样会进行虚函数查表自动调用到子类的析构函数,同时子类析构会自动调用父类析构,父类的析构函数也会执行。
引用
常见于两种情况:
- Base& a= Derive();实现多态
- 引用传递与引用返回来避免拷贝,以及直接修改值。
//原理上就是指针
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 之后,引入{}来统一初始化.
- 避免歧义
Widget w1(10); // 调用Widget的带参构造函数
Widget w2(); //声明函数
Widget w2{}; // 无歧义
Widget w2; // 无歧义
- 类内成员的默认初始值
class Widget {
...
private:
int x{ 0 }; // x的默认初始值为0
int y = 0; // 同上
int z( 0 ); // 报错
}
- 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构造函数
哈希函数
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(折叠表达式)
可以使用这个替换模板的循环递归。
- 只支持一个运算符的不断递归
- 没有递归调用,只是括号内的迭代展开,最终形成一个很长的括号内容。
- 左右不同顺序,导致括号顺序不同。
...代表折叠内容,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
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.