目录
- 各类构造函数
- 左值与右值
- std::remove_reference
- 完美转发
- 虚函数
- 纯虚函数
- volatile
- 智能指针
- extern关键字
- const&static
- 大小端
- 地址对齐
- 原子操作
- 多线程
各类构造函数
-
构造函数
用来初始化对象的,若没有显示定义构造函数,有时候编译器会自动帮忙创建默认构造函数,编译器创建的默认构造函数是无参的且是空实现的,关于编译器什么情况下会自动帮忙创建默认构造函数略,一个良好的编程习惯是自己定义构造函数。
注意,构造函数并不是用来创建对象的,是用来给创建的对象进行初始化操作的。 -
析构函数
在对象销毁前调用,用来释放该对象的一些指针所指向的堆空间的(因为对象中的非指针变量在栈中,其实栈中的变量是不需要特意用析构函数来释放的)。若没有显示定义构造函数,有时候编译器会自动帮忙创建默认构造函数,编译器创建的默认构造函数是无参的且是空实现的,关于编译器什么情况下会自动帮忙创建默认构造函数略,一个良好的编程习惯是自己定义构造函数。
如果一个类作为基类,那么其析构函数要声明为虚函数,不然在 "用父类指针指向子类对象,然后delete子类对象"时 子类对象析构函数得不到调用。 -
拷贝构造函数
在①进行对象赋值操作的时候调用,用来初始化对象 ② 如下面代码所示。同样的编译器在某些情况下会自动生成默认拷贝构造函数,需要注意的是,默认拷贝构造函数是浅拷贝。#include <thread> #include <iostream> using namespace std;class Student{ public:Student(){cout<<"构造函数"<<endl;}Student(const Student &a){ //用引用传递而不用值传递是防止”无限递归“ 用const是为了防止a被修改cout<<"拷贝构造函数"<<endl;}~Student(){cout<<"析构函数"<<endl;} };int main(int argc,char* argv[]){Student stu1=Student();Student stu2=stu1;}/* 执行结果:构造函数拷贝构造函数析构函数析构函数 */
#include <thread> #include <iostream> using namespace std;class Student{ public:Student(){cout<<"构造函数"<<endl;}Student(const Student &a){cout<<"拷贝构造函数"<<endl;}~Student(){cout<<"析构函数"<<endl;} };void test(Student stu){}int main(int argc,char* argv[]){Student stu1=Student();test(stu1);} /* 执行结果:构造函数拷贝构造函数析构函数析构函数 */
-
移动构造函数
见 左值与右值 章节 -
拷贝赋值函数 与 移动赋值函数
#include <thread> #include <iostream> #include <vector> using namespace std;class Student{ public:Student() {cout<<"构造函数"<<endl;};virtual ~Student() {cout<<"析构函数"<<endl;};Student(const Student&){cout<<"拷贝构造函数"<<endl;}Student& operator=(const Student&){cout<<"拷贝赋值函数"<<endl;return *this;}Student(const Student&&){cout<<"移动构造函数"<<endl;}Student& operator=(const Student&&){cout<<"移动赋值函数"<<endl;return *this;}};int main(int argc,char* argv[]){Student stu1;Student stu2;stu2=stu1;//调用拷贝赋值函数stu2=std::move(stu1);//调用移动赋值函数}
左值与右值
参考链接
-
左值:可以取地址;右值:不能取地址。一定要以这个标准判断一个值是左值还是右值,比如字符串字面值其实是左值,因为其可以取地址。
-
左值引用:对左值的引用;右值引用:对右值的引用
-
左值引用可以引用左值,也可以引用右值(加const);右值引用只能引用右值
-
&一定是左值引用,&&即可能是右值引用也可能是万能引用(universal references,表示根据不同情况自动决定是左值引用还是右值引用),注意只有在类型需要推导的时候&&才表示万能引用
关于上图的解释:
调用f(a)时,T会被推导为int&,那么其实就是f(int& &¶m),这里进行了一个折叠引用,会被折叠为f(int& param),param是对左值的引用。
调用f(1)时,T会被推导为int,那么其实就是f(int &¶m),param是对右值的引用。 -
折叠引用
- T && &&折叠为T&&
- T & && 折叠为T&
- T && & 折叠为T&
- T & & 折叠为T&
-
右值引用+移动构造函数:实现节省堆内存空间
对于stu1对象,假如我确定之后不会再使用它了,并且我想把其值赋值给一个新的对象stu2,那么其实我可以使用移动构造函数,让stu2接管stu1在堆中的空间,而不是让stu2又重新在堆中开辟一个空间存age。class Student{ public:int* age;int sex;Student(){sex=1;age=new int(18);cout<<"构造函数"<<endl;}Student(const Student &stu){ this->sex=stu.sex;this->age=(int*)malloc(sizeof(int));//深拷贝*(this->age)=*(stu.age);cout<<"拷贝构造函数"<<endl;}Student(Student &&stu){ this->sex=stu.sex;this->age=(int*)malloc(sizeof(int));this->age=stu.age;//接管stu.agestu.age=nullptr;cout<<"移动构造函数"<<endl;}~Student(){cout<<"析构函数"<<endl;} };int main(int argc,char* argv[]){Student stu1=Student();Student stu2(std::move(stu1));}
std::remove_reference
- 其实就是一个类型提取器,
std::remove_reference<int&&>::type a; 等价于int a;
完美转发
-
什么是完美转发,如下面代码所示
- 我们希望在调用函数时传入的形参是左值,那么在函数内部仍然保持左值;若在函数调用时传入的形参是右值,那么在函数内部仍然保持右值
- 完美转发是通过 万能引用+std::forward函数共同完成的
#include <thread> #include <iostream> #include <vector> using namespace std;void print(int& t) {cout << "int&" << endl; }void print(int&& t) {cout << "int&&" << endl; }template <class T> void testforward(T&& a) { //若testforward的形参是右值,则forward的返回值是右值//若testforward的形参是左值,则forward的返回值是左值print(std::forward<T>(a)); }int main(int argc,char* argv[]){int x=2;testforward(2); //形参传入右值testforward(x); //形参传入左值}
-
关于完美转发的实现原理,以上面的代码进行讲解,先贴出std::forward的源码(如下图)
-
当testforward(2)时,T=int,那么即
constexpr int&& forward(int& __t) noexcept { return static_cast<int&&>(__t); }constexpr int&& forward(int&& __t) noexcept {static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"" substituting _Tp is an lvalue reference type");return static_cast<int&&>(__t); }
当执行forward< T>(a)时,因为a是左值,那么重载到第一个函数也就是forward(int& __t)执行,
返回static_cast<int&&>(a),是一个右值(因为返回的a是右值引用,那么a只能是右值) -
当testforward(x)时,T=int&,那么即(这里省略了折叠引用的过程)
constexpr int& forward(int& __t) noexcept { return static_cast<int&>(__t); }constexpr int& forward(int&& __t) noexcept {static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"" substituting _Tp is an lvalue reference type");return static_cast<int&>(__t); }
当执行forward< T>(a)时,因为a是左值,那么重载到第一个函数也就是forward(int& __t)执行(注意,两种情况其实都是执行第一个forward,因为a是左值)
返回static_cast<int&>(a),是一个左值(因为返回的a是左值引用,那么a只能是左值)
-
虚函数
- 准确来说是类的虚函数,因为virtual关键字修饰的函数只能是类的成员函数(注意不能与static一起使用)
- 虚函数的作用是用来实现多态,基类指针可以指向子类,若想通过父类指针调用子类函数,不用virtual关键字是无法实现的,如下方代码的运行结果是Father,只有给Father的test函数加上virtual关键字运行结果才是Son
#include <iostream>using namespace std;class Father { public:void test(){cout<<"Father"<<endl;} };class Son : public Father { public:void test(){cout<<"Son"<<endl;} };int main() {Father* p = new Son;//若Son* p = new Son;那么运行结果是Son(运行哪个(非虚)函数是在编译使其确定的!)p->test();return 0; }
- 虚函数表
参考链接- 每个类,只要含有虚函数,new出来的对象就包含一个虚函数指针(8字节),指向这个类的虚函数表(这个虚函数表一个类用一张)
- 子类继承父类,会形成一个新的虚函数表,但是虚函数的实际地址还是用的父类的,如果子类重写了某个虚函数,那么子类的虚函数表中存放的就是重写的虚函数的地址
纯虚函数
- 用virtual void 函数名()=0;在基类中声明一个纯虚函数,那么继承该基类的子类就必须实现该函数
volatile
- volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象
- volatile一般在多线程开发中才会用到(单线程好像不需要用到volatile关键字?不确定…)
智能指针
参考链接
extern关键字
-
和extern "C"用在函数前面,表示用C而不是C++的规则去编译该函数。
由于C++支持函数重载而C不支持,所以同一个函数用C和C++编译得到的函数名字不同。
假如有一个用C开发并编译的库文件,这个库文件中 现在要写一个C++代码来调用这个C的库文件中的某个函数,我们在c++文件中声明函数时一定要带extern “C”,如果不带的话,用g++编译的时候,会报函数undefined的错误,因为g++编译c++文件中的函数时候将该函数编译成了不同的名字。extern "C" void fun(){}
-
用在变量前,表示该变量在其他文件中定义,如下方代码通过g++ file1.cpp file2.cpp可正确编译,输出结果是2
//file1.cpp int x=2; //file2.cpp int main() {extern int x;std::cout<<x;return 0; }
const&static
-
static
- 修饰全局作用域 变量或函数 ,将其作用域限制在本文件内
- 修饰函数内的局部变量,在第一次访问的时候初始化并直至程序结束其生命周期才结束。
- 修饰类的成员变量或成员函数,将该变量或函数让类持有而不是类的对象持有
static修饰的成员变量不能在类内声明的时候初始化,必须在类外初始化(这点好像是由于历史原因导致的语法,感觉有点奇怪)
-
const
- 修饰变量,表明变量不可以被修改。const修饰的变量必须在声明的时候就初始化(在声明时的赋值操作我们通常称为是初始化)
- 修饰类的成员方法,表明此方法不会更改类对象的任何数据
- const int* x;和int * const x;
const int* x; x可以修改,x指向的内容不能修改
int * const x;x不可以修改,x指向的内容可以修改
-
底层const和顶层const
参考链接- 若是因为const直接修饰这个变量导致其不能修改,则称为top-level const
- 若是因为const间接修饰(比如指针或引用)这个变量导致其不能修改,则称为low-level const
- 对于关于const变量赋值的问题 参考链接
大小端
- 大端:低地址存高字节,高地址存低字节;小端:低地址存低字节,高地址存高字节
- 大小端是由CPU决定的,准确来讲是由指令集决定的(待定)
- 寄存器是不区分大小端的,因为寄存器其实是没用地址的概念的,若非要把寄存器强加地址概念
- 现代CPU一般都是小端序,网络字节序是大端序(即接受到的第一个字节认为是大端)
- 大端小端谁更好???
地址对齐
- 地址对齐规则
-
对于标准数据类型其对齐规则:该数据的首地址必须是m的整数倍,m的取值如下:
1.如果变量的尺寸小于4字节,那么该变量的m值等于变量的长度。
2.如果变量的尺寸大于等于4字节,则一律按4字节对齐。
3.如果变量的m值被人为调整过,则以调整后的m值为准。 -
对于类或结构数据类型其对齐规则
1.中的各个成员,第⼀个成员位于偏移为 0 的位置,以后的每个数据成员的偏移必须是min(#pragma pack()指定的数,数据成员本身长度)的倍数
2.在所有的数据成员完成各⾃对⻬之后,结构体或联合体本身也要进⾏对⻬,整体⻓度是 min(#pragma pack()指定的数,⻓度最⻓的数据成员的⻓度) 的倍数。
-
- 为什么需要地址对齐?
- 根本原因是存储器的物理结构
原子操作
-
什么是原子操作
首先原子操作在多线程下讨论才有意义,且原子操作要从指令级别去理解。
对于一组指令操作,要么全部执行完,要么一条都不执行,且其他线程看不到中间状态,我们称这组操作叫原子操作 -
c++的互斥锁std::mutex
-
互斥锁
参考链接
参考链接
参考链接
参考链接
参考链接
参考链接下面代码是一个使用mutex的例子,这个例子通过mutex实现了对count变量的原子自增操作。当线程调用mtx.lock()函数时,若mtx未被上锁则线程获得该锁并继续执行,若mtx已被上锁则线程被挂起到阻塞队列;当拥有该锁的线程调用mtx.unlock();函数时,会从阻塞队列中唤醒一个线程。
std::mutex属于不可重入锁(若一个线程连续两次调用lock函数的话,在第二次就会死锁了),而std::recursive_mutex属于可重入锁(若一个线程连续调用lock函数不会导致死锁,但要注意,某个线程调了多少次lock就要调多少次unlock)
#include <mutex> #include <thread>int count = 0; std::mutex mtx;void safe_increment() {mtx.lock();++count;mtx.unlock(); }int main() {std::thread t1(safe_increment);std::thread t2(safe_increment);t1.join();t2.join();return 0; }//g++ test.cpp -lpthread
mtx.lock()
底层原理:lock cmpxchg指令其实锁具体而言就是存在内存中的一个变量罢了,mtx类中具体的锁是_M_mutex这个成员变量(假设这个变量在内存中初始化是initial_value也就是未上锁状态,_M_mutex的地址是lock_addr),在执行lock函数上锁的时候(假设我们上锁操作就是将_M_mutex置为set_value),mtx.lock()会被编译成指令:cmpxchg(当然肯定不止被编译成这一条指令,但最关键的是这个cmpxchg指令):
cmpxchg lock_addr,set_value
这条指令的作用是比较eax寄存器中的值(eax是该指令的一个隐藏操作数,执行cmpxchg指令前会执行一条指令先将eax中的值设置为initial_value) 与lock_addr地址处的值是否相等,若相等则将lock_addr地址处写入set_value并将写入成功标志写入状态寄存器中的某个标志位,若不相等则将写入失败标志写入状态寄存器中的某个标志位
需要注意的是,因为单条指令执行过程本身就是不可被中断的,那么其实在单核情况下,上面的
cmpxchg lock_addr,set_value
这条指令完全可以确保原子性了,因为单核情况下多线程是不能真正并行的,是通过时间切片并发的,多个线程之间并不会存在同时执行cmpxchg 指令的情况但在多核情况下,不同线程可能同时运行在多个核中,所以可能同时存在两个线程同时执行cmpxchg指令的情况或错开几个时钟周期执行(每个core都有自己的时钟,cmpxchg执行需要一个指令周期,一个指令周期分为若干个时钟周期),又由于一个core上的线程执行cmpxchg的整个指令周期中不会一直占用总线 ,可能core1读取了lock_addr地址处的数据后把总线让出来了(此时core1还未执行完cmpxchg指令),然后core2又去占用总线读取lock_addr地址处的数据,然后core1将读取到的数据和eax寄存器中的值比较发现相等,于是将set_value写入lock_addr,但core2读取到的是core1写之前的数据,所以这样其实多core之间就无法保证这种既读又写的指令的原子性
所以在多核编译下,cmpxchg都会带上一个lock前缀也就是
lock cmpxchg lock_addr,set_value
这个lock的意思是锁总线,在锁住总线的情况下,那么在某个core执行cmpxchg期间,总线会一直被其占用,从而导致其他core无法执行cmpxchg指令。所以在多核的情况下,std::mutex其实是通过lock cmpxchg
指令来实现原子操作的。mtx.unlock()
这个函数就是将mutex对象的_M_mutex的变量写入initial_value(也就是未上锁状态),对应的指令其实就是一个写指令,一个指令只执行写操作不需要加lock前缀即可保证原子性(因为写的时候core是独占总线的,其他core也拿不到总线使用权)。补充
在x86下,若一条指有大于1次的内存操作(比如上面的cmpxchg),那么必须要用lock前缀保证其原子性(有些指令省略lock前缀,但执行的时候还是会锁总线,比如xchg指令);若一条指令只有一次内存操作,那该条指令是可以保证原子性的。
-
-
C++的原子变量std::atomic_flag和std::atomic< T >
-
std::atomic_flag
C++提供的最基础的原子变量类型,有两个函数可用:test_and_set()和clear(),具体见下面代码#include <atomic> #include <iostream>int main() {std::atomic_flag flag=ATOMIC_FLAG_INIT; //一定要用ATOMIC_FLAG_INIT初始化(ATOMIC_FLAG_INIT其实就是0)std::cout<<flag._M_i<<std::endl;//输出0flag.test_and_set();//这个函数是将flag置为1,并返回flag的旧值std::cout<<flag._M_i<<std::endl;//输出1flag.clear();//这个函数是将flag置为0std::cout<<flag._M_i<<std::endl;//输出1return 0; }
需要注意的是test_and_set()函数和clear函数可以保证是原子性的,分析见下方。
test_and_set函数
该编译后得到了三条指令如下:
最关键的是xchg指令(这个指令不需要带lock前缀,是默认会锁总线的,因此在多核情况下也可以保证xchg指令的原子性),将寄存器dl(edx的低8位,值是1)和 内存地址rax处(rax是flag._M_i的地址)的值进行交换。也就是将flag._M_i的值写入1了,并通过edx寄存器拿到了flag._M_i的旧值返回(test_and_set函数的返回值就是这样拿到的)
clear函数
其编译后得到如下指令(红框中),最关键的是mov %dl,(%rax)指令,将0写入flag._M_i,因为mov是单次内存操作,所以不加lock前缀即可保证原子性。需要注意是后面还跟了个mfence指令,这个是用来保证可见性的。
-
-
std::atomic< T >
TODO
-
C++的自旋锁
刚刚的mutex是互斥锁,当线程加锁不成功时,线程会阻塞挂起,而自旋锁的思想是:若线程加锁失败了,还让线程一直尝试加锁(让线程一直跑,不阻塞挂起)c++没用直接提供自旋锁的接口,需要自己实现,下面是两种实现自旋锁的方法,
第一种是通过std::atomic_flag利用TAS思想实现
第二种是通过std::atomic< bool >利用CAS思想实现用std::atomic_flag实现自旋锁
#include <atomic> #include <thread> #include <iostream>int count = 0;class SpinLock { public:SpinLock() : flag(ATOMIC_FLAG_INIT) {}void lock() {while (flag.test_and_set()); }void unlock() {flag.clear();}private:std::atomic_flag flag; };SpinLock spinLock;void safe_increment() {spinLock.lock();++count;spinLock.unlock(); }int main() {std::thread t1(safe_increment);std::thread t2(safe_increment);t1.join();t2.join();std::cout<<count;return 0; }
用std::atomic< bool > 实现自旋锁
#include <atomic> #include <thread> #include <iostream>int count = 0;class SpinLock { public:SpinLock() : flag(false) {}//初始值为false 表示未上锁void lock() {bool expect = false;//若flag是期望值false(即未上锁),则将其置为true(上锁)while (!flag.compare_exchange_weak(expect, true)){expect = false;//这里一定要将expect复原,执行失败时expect结果是未定的}}void unlock() {flag.store(false);}private:std::atomic<bool> flag; };SpinLock spinLock;void safe_increment() {spinLock.lock();++count;spinLock.unlock(); }int main() {std::thread t1(safe_increment);std::thread t2(safe_increment);t1.join();t2.join();std::cout<<count;return 0; }
TODO…
================================================================
多线程
-
示例
#include <thread> #include <iostream> using namespace std;void ThreadMain(){cout<<"子线程id:"<<this_thread::get_id()<<endl; }int main(int argc,char* argv[]){cout<<"主线程id"<<this_thread::get_id()<<endl;thread th(ThreadMain);th.detach();//th.join();//主线程等待子线程结束 }//g++ demo1.cpp -lpthread
-
detach()
detach()的作用是将子线程和主线程的关联分离,也就是说detach()后子线程在后台独立继续运行,主线程无法再取得子线程的控制权,即使主线程结束,子线程未执行也不会结束。
detach后的线程我们称为 daemon thread