当前位置: 首页 > news >正文

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()指定的数,⻓度最⻓的数据成员的⻓度) 的倍数。

  • 为什么需要地址对齐?
    • 根本原因是存储器的物理结构

原子操作

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


http://www.mrgr.cn/news/75260.html

相关文章:

  • Redis - 哨兵(Sentinel)
  • Spring 解析xml中的 BeanDefination 大概过程
  • 小程序服务商常见问题
  • golang分布式缓存项目 Day2 单机并发缓存
  • Git - Think in Git
  • 【element-tiptap】Tiptap编辑器介绍
  • 多模态大模型(2)--BLIP
  • 【电子设计】按键LED控制与FreeRTOS
  • NGUI————按钮练习题
  • Towards Reasoning in Large Language Models: A Survey
  • Spring加载流程,Springboot自动装配原理
  • android开发
  • exo - 使用日常设备运行AI集群
  • 2024年09月CCF-GESP编程能力等级认证Python编程一级真题解析
  • 微信小程序-prettier 格式化
  • Diffusion Policy——斯坦福机器人UMI所用的扩散策略:从原理到其编码实现(含Diff-Control、ControlNet详解)
  • leetcode hot100【LeetCode 105.从前序与中序遍历序列构造二叉树】java实现
  • Web性能优化:从基础到高级
  • 二叉树的遍历(手动)
  • 一文了解Android的核心系统服务
  • 不宽的宽字符
  • 面试中如何回答“怎样实现 RPC 框架”的问题?
  • 高效的 JSON 处理库 json.cpp
  • ubuntu里面的gcc编译方法
  • 三维测量与建模笔记 - 特征提取与匹配 - 4.2 梯度算子、Canny边缘检测、霍夫变换直线检测
  • 使用SimpleDateFormat的踩坑指南