欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > C++笔记

C++笔记

2025/5/18 18:41:05 来源:https://blog.csdn.net/weixin_46347213/article/details/143713577  浏览:    关键词: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& &&param),这里进行了一个折叠引用,会被折叠为f(int& param),param是对左值的引用。
    调用f(1)时,T会被推导为int,那么其实就是f(int &&param),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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词