C++笔记
目录
- 各类构造函数
- 左值与右值
- 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()指定的数,⻓度最⻓的数据成员的⻓度) 的倍数。
-
- 为什么需要地址对齐?
- 根本原因是存储器的物理结构
原子操作
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