C++知识点汇总文档

C++知识点汇总文档

  • 持续更新
  • 文档尚未补全
  • 整理这份文档的时候发现, 本人对C++的了解无疑是冰山一角
    C++11属实恐怖, 新"特性"恐怖如斯(没错我就是指右值引用和initializer_list)
  • 另注: 本人才疏学浅, 难免有错漏之处, 还望不吝赐教

0.目录


0.5.网址记录

1.基本概念


指针

  • 如果自己管理指针, 有几点一定要注意
    • 用之前看看是不是nullptr
    • delete之后指针变量一定要设为nullptr (临时变量除外)
    • 避免delete两遍同一个指针
    • 别试图catch由new关键字产生的异常, new都new不出来了, 就应该"任其崩溃"
  • 函数指针
    • 函数指针是指向函数的指针, 并且可以通过函数指针调用其指向的函数
    • 示例:
    class C
    {
    public:
      //静态成员函数
      static void f_static();
      //非静态成员函数
      void f_non_static() {};
      //非静态成员函数的重载
      void f_non_static(int i) {};
    };
    
    void C::f_static()
    {
    
    }
    
    void test()
    {
      //类的静态函数不需要加类名限定
      void (*p_static_fun)()=&C::f_static;//获取静态函数的指针
      //类的非静态成员函数需要加类名限定
      void (C:: * p_fun)() = &C::f_non_static;//获取非静态成员函数的指针
    
      //针对这一规则的个人理解:
      //静态函数不隐式传递this指针, 而非静态成员函数的参数中隐式传递了this指针
      //编译器以类名限定作区分
    
      //编译器自动选择合适的重载版本
      void (C:: * p_fun)(int) = &C::f_non_static;
      //以下语句会报错, 无法推导auto的类型
      auto p_auto = &C::f_non_static;
      //这样写, 让编译器知道是哪一个重载的版本
      auto p_auto = static_cast<void (C:: *)(int)>(&C::f_non_static);
    }
    

函数

  • 函数是C++/C中最基本的概念, 用来描述或者叫做封装一段执行过程
  • 废话不多说, 这里简单讲一个函数调用时的压栈规则问题
    • C++标准没有规定在函数调用时如何将参数压入栈中
    • 大多数编译器环境下默认下都是右序入栈. 看看下面的示例:
    using namespace std;
    
    //创建一个以两个int为参数的匿名函数对象
    auto f = [](int a, int b) {cout << a << b; };
    //下一行语句的输出是: RIGHT LEFT 56
    f(printf("LEFT "), printf("RIGHT "));
    //在VS2019环境下, 先执行了printf("RIGHT "), 得到返回值6
    //再执行printf("LEFT ")
    //得到返回值5, 最后用两个返回值作参数, 调用f(5, 6)
    
    cout << endl;
    
    //下一行语句的输出是: RIGHT LEFT 56
    cout << printf("LEFT ") << printf("RIGHT ");
    //连续的<<操作符和函数调用情况很相似, 也是先从右边开始执行(在我的环境下)
    
    

内存管理

  • C++中的内存划分(内容来自博客):
      • 由用户使用new delete关键字管理的内存区域
      • 栈中用来存放临时变量, 比如函数中的局部变量, 在代码块结束时会自动清除
    • 自由存储区
      • 由malloc等分配的内存块, 他和堆是十分相似的, 不过它是用free来结束自己的生命的(???有空查证)
    • 全局/静态存储区
      • 全局变量和静态变量被分配到同一块内存中
    • 常量存储区
      • 比较特殊的存储区, 他们里面存放的是常量(不太了解, 有空扫下盲)
  • new 关键字
    • 详见条目: 关键字new
  • delete 关键字
    • 详见条目: 关键字delete

左值与右值

  • 左值
    • 左值一般来讲就是变量(能被赋值的), "能出现在等号(赋值号)左边的"都是左值
    • 可以取地址的, 有名字的, 非临时的就是左值
  • 右值
    • "出现在等号右边的临时值"称为右值, 通常情况下C++会产生一个临时变量来存储右值表达式的结果
    • 不能取地址的, 没有名字的, 临时的就是右值
  1. 从本质上理解, 创建和销毁由编译器幕后控制, 程序员只能确保在本行代码有效的, 就是右值(包括立即数);
  2. 而用户创建的, 通过作用域规则可知其生存期的, 就是左值(包括函数返回的局部变量的引用以及const对象).
    摘自博客
  • 左值右值参考示例
    int a{};
    //下式中a是左值, 5+1会产生一个临时变量6, 是右值
    a = 5+1;
    

引用

  • 引用的底层实现是指针, 因此引用在内存中占据一个指针大小的空间
  • 左值引用
    • 左值引用是对左值的引用, 类似于为变量起一个别名
    • 常引用
      • 在左值引用时使用const修饰则会成为一个常引用
      • 常引用在引用左值时, 除不能修改引用对象的值以外, 和普通左值引用相同
      • 常引用在引用右值时(如常量), 会生成一个临时变量存储目标值
    • 左值引用参考示例
      int var = 1;
      int &ref = var;   // 定义一个左值引用变量
      ref = 2;          // 通过左值引用修改引用内存的值
      
      
      //以下语句会报错, 10不是一个合法的左值
      //int &ref0 = 10;
      //用const修饰, 常引用, 引用常量
      const int &ref0 = 10;
      //等价于下面两行代码
      const int temp = 10; 
      const int &ref1 = temp;
      
      
  • 右值引用
    • 右值引用为C++11中引入的新概念
    • 右值引用的存在是为了减少对象的构造和析构操作, 从而提升效率
    • 底层汇编实现与常引用引用右值时的情况相同, 都使用一个临时变量存储目标值, 不同的是右值引用可以修改引用的值
    • 右值引用参考示例
      //右值引用
      int && rref = 10;
      
    • 右值引用实际应用参考
      存在如下类定义
      class C  
      {
        public:
          //获得类C的一个实例
          static C getObject() { return C(); }
          
      };
      
      //这一行会调用类C的复制构造函数, 参数为getObject()函数产生的临时对象
      //其后临时对象会调用析构函数销毁
      C c = C::getObject();
      
      //右值引用避免了一次中间临时变量的构造与析构过程
      //引用直接指向函数返回时构造的对象
      C &&rref = C::getObject();
      
      

    一些技巧

    1. 函数返回的内容在函数体外仍然存在的时候(通常是左值), 可以考虑返回引用以提升效率
    2. 函数返回的内容在函数体外不能继续存在的时候(通常是右值), 则可以在接受返回值时使用右值引用以提升效率(注意, 只有在return一个临时变量/对象的时候用右值引用接收才有意义)
  • 引用折叠
    • 首先, C++中禁止"引用的引用", 即"reference to reference", 也即不允许引用(动词)一个引用(名词)
    • 当多个引用嵌套时, 采用如下规则折叠(塌缩)引用
      • 所有右值引用折叠到右值引用上仍然是一个右值引用(A&& && -> A&&)
      • 所有的其他引用类型之间的折叠都将变成左值引用(A& & 变成 A&; A& && -> A&; A&& & -> A&)
      • 引用嵌套中出现左值引用则塌缩为左值引用
      • 引用嵌套中全为右值引用则塌缩为右值引用

lambda表达式

  • lambda表达式为C++11中引进的新内容
  • lambda表达式用于定义和使用匿名函数对象, 可以部分简化编程工作. 但主要用于提高程序可读性, 使程序更加简洁
  • lambda表达式会创建一个匿名的函数对象(重载了括号表达式的对象)
    //如下代码可以说明这一点
    auto f = []() {};
    //在我的电脑中会产生以下输出:
    //class <lambda_719dedac4c26bf6e08cdff46ce419171>
    std::cout << typeid(f).name();
    
  • lambda表达式的语法形式
    [捕获列表] (参数列表) 修饰信息 -> 返回值类型 {函数体}
    
    [capture list] (params list) mutable exception-> return-type {function-body}
    
    • 捕获列表

      • 不可省略, 捕获列表标志着一个lambda表达式的开始
      • 捕获规则表---啰里吧嗦一大堆, 太烦, 还是表格来的实在
      捕获列表样式 捕获行为
      [ ] 不捕获外部变量
      [var1] 捕获名为var1的外部变量
      [&var1] 引用捕获名为var1的外部变量
      [var1, &var2, ...] 按照指定的方式捕获多个外部变量
      [=] 捕获所有外部变量
      [&] 引用捕获所有外部变量
      [&, var1] 除了var1按值捕获外, 其余所有外部变量按引用捕获
      [=, &var1] 与上一条的行为相反
    • 参数列表

      • xxxx
    • 修饰信息

      • mutable(可选项): 指示lambda表达式是否能够修改捕获到的变量
      • exception(可选项):
    • 返回值类型

      • 可以省略
      • 当省略返回类型时, 编译器根据return语句推导类型
    • 函数体

      • 匿名函数要执行的语句

面向对象

  • class 关键字
    • class 关键字用以声明一个类
  • 访问控制
    • 三种访问控制
      • public
        • 公有. 所有位置均可访问(包括成员函数, 派生类的成员函数, 非成员函数, 本类或派生类的友元)
      • protected
        • 保护. 只有成员函数, 派生类的成员函数, 派生类的友元, 本类的友元可以访问
      • private
        • 私有. 只有成员函数, 本类的友元可以访问
    • 友元静态成员函数
      • 友元机制提供了一种方案, 它让普通的函数也能访问 类的成员函数能够访问的所有内容. 也即让普通的函数也处于"类的内部"
      • 友元的访问权限与成员函数相同. 即: 成员函数中可以访问的内容, 友元也都可以访问
      • 需要注意的是, 这里指的"成员函数"和"友元函数"是指同一类中的成员函数和友元函数
        • 举个例子, 派生类的友元是无法访问基类的private成员的. 因为派生类的成员函数也不能访问基类的private成员
      • 静态成员函数也属于成员函数, 访问权限方面和友元函数或成员函数相同
        • 注: 这里不是指在静态成员函数中也能通过类似 this->menber 的方式访问类的成员变量, 这不是一个概念. this指针只有非静态的成员函数才能使用, 此处不再赘述.
    • 访问控制示例
      class C
      {
        friend void fun0(C& c);//友元函数声明
      public:
        int pub;//公有成员
      protected:
        int pro;//保护成员
      private:
        int pri;//私有成员
      };
      
      void fun0(C& c)
      {
        c.pub;
        c.pro;
        c.pri;//没问题, 可以访问
      }
      
      class C_plus:public C//公有继承, 基类成员可见性不变
      {
        friend void fun2(C_plus &c);//友元函数声明
      
        void fun1()
        {
          this->pub;
          this->pro;
          //this->pri;//报错, 不可访问
        }
      };
      
      void fun2(C_plus& c)
      {
        c.pub;
        c.pro;
        //c.pri;//报错, 不可访问
      }
      
  • 默认的成员函数(如果不手动实现, 编译器会自动实现)
    • 构造函数(constructor)

      • 当一个新的对象产生的时候调用的函数
      • 构造函数都没有返回值, 也不需要指定void
      • 典型实现
        //"C"为类名
        C();//一个无参构造函数
        {
          //do something
        }
        C(int a, int b)//一个接受两个int值的构造函数
        {
          //do something
        }
        
    • 析构函数(destructor)

      • 当一个对象消亡(析构)的时候调用的函数
      • 典型实现
        //"C"为类名
        ~C()
        {
          //do something
          //通常情况下这里会有一些代码用来delete在构造函数中new出的内存指针
        }
        
    • 复制构造函数(copy constructor)(拷贝构造函数)

      • 当使用一个已有的对象创建(初始化)一个新的对象的时候调用的函数
      • 复制构造函数只有一个本类对象的引用作为参数(如果不使用引用会导致无限递归)
      • 复制构造函数的行为涉及到深浅复制的问题
        • 浅复制: 无论成员变量类型为何, 只将成员变量的值复制
        • 深复制: 如果成员变量是一个指针, 则通常不能简单地复制指针的值, 而应该申请新的内存块, 并将得到的指针赋给新的指针类型成员变量
      • 典型实现
        //"C"为类名
        C(const C & another)//必须使用引用作为参数; const可加可不加, 但建议加上
        {
          //do something
        }
        
    • 赋值函数(assignment)(赋值操作符重载)

      • 当使用一个已有的对象对另一个已有的对象进行赋值时调用的函数
      • 典型实现
        //"C"为类名
        C& operator=(const C &another)
        {
          //do something
          return * this;
        }
        
    • 转移构造函数(move constructor)(C++11)

      • 当一个已有的对象的内容转移到一个新产生的对象中时调用的函数(一般来说原对象应该要失效)
      • 转移构造函数在针对指针类型的成员变量通常采取浅复制, 并将被转移的对象的指针成员设为nullptr
      • 典型实现
        //"C"为类名
        C(C && another)/*最好添上noexcept修饰*/
        {
          //do something
        }
        
    • 转移赋值函数(move assignment)(赋值操作符重载)(C++11)

      • 当一个已有的对象的内容转移到另一个已有的对象中时调用的函数(一般来说原对象应该要失效)
      • 典型实现
        //"C"为类名
        C& operator=(C&& ano)/*最好添上noexcept修饰*/
          {
          	//do something
          }
        
    • 取地址操作符重载

      • 用于对象的取地址操作, 一般不需要自行实现
      • 典型实现
        //"C"为类名
        C* operator&()
        {
            return this;//直接将地址返回
        }
        
    • const取地址操作符重载

      • 用于对象的取const地址操作, 一般不需要自行实现
      • 典型实现
        //"C"为类名
        const C* operator&() const
        {
            return this;//直接将地址返回
        }
        
  • 类型转换构造函数
    • 类型转换构造函数用于将一个其他类型的对象转换为一个类对象
    • 当需要一个类对象, 却提供了一个可以转换为类对象的其他类型的对象时, 自动调用对应的类型转换构造函数进行转换
    • 典型实现
      class C
      {
      public:
        int data;//成员变量
        //类型转换构造函数, 将int类型的值转换为类对象
        /*explicit */C(const int &_data)
        {
          this->data = _data;
        }
      }
      
    • explicit关键字
      • 有时候, 存在只有一个参数的构造函数, 或存在一个虽然有不止一个参数但除了第一个参数外其余参数都有默认值的构造函数, 但不希望其成为一个类型转换构造函数, 可以使用explicit关键字进行修饰, 它指示该构造函数不是隐式的, 不会自动隐式调用. (注: 用explicit关键字修饰除上述构造函数之外的函数没有意义)
  • 类型转换函数

    C++中, 类型的名字也是一种运算符, 即强制类型转换运算符

    • 主要用于隐式转换, 当需要一个目标类型对象, 却提供了一个类对象时, 自动调用对应的类型转换函数进行转换
    • 典型实现
      class C
      {
      private:
        int data;
      public:
        //转换函数(转换为int类型), 在需求int类型时传递C类的对象会自动调用
        operator int(){ return data; }
      }
      
  • 在成员函数声明末尾添加const
    • 该操作为成员函数的参数列表中隐含的参数this指针添加const修饰
    • 即: 不允许修改this指针指向的对象; 不允许调用对象的非const成员函数
      //类成员函数
      void fun()const
      {
        //do something
      }
      
      //注:并没有下面这种写法, 只是本质上等价于下面的代码
      
      //"C"为类名
      //注意这里const是修饰类型C的
      //这意味着this指针指向的内容被看作常量
      void fun(const C * this)
      {
        //do something
      }
      
  • 继承
    • 继承表示了一种"is-A"关系. 即, 派生类是一种(特殊的)基类.
    • 派生类(子类)继承了基类(父类)的所有成员
    • 三种继承方式(老实说为了理解这些内容头发都掉了一地)
      • public
        • 公有继承, 基类的成员可见性在派生类中保持不变(保持在基类中声明的可见性)("成员"包括成员变量和成员函数, 又称字段和方法)
      • protected
        • 保护继承, 基类的成员中, 可见性高于protested的, 在派生类中变为protested
      • private
        • 私有继承, 基类的所有成员在派生类中可见性变为private
      • 注1: 私有继承和保护继承会使得类作用域外无法再使用多态, 也即无法将派生类对象的指针赋给基类对象指针
      • 注2: 无论什么继承方式, 基类中的private成员在派生类中都是无法访问的, 因此派生类继承基类成员时是不考虑私有成员的
    • 简言之, 比继承方式可见性高的成员, 在派生类中可见性被削弱为继承方式的可见性(这里假定public可见性最高, private可见性最低)
    • 成员函数的重载, 重写, 隐藏(这几点比较重要, 是C++面向对象中继承的核心难点. 每次搞清楚之后没一会又忘了...记下来比较靠谱)
      • overload(重载)
        • 重载通常发生在同一域(scope)内, 例如一个类有两个同名的成员函数, 但他们的函数参数列表不一样
          class C
          {
          public:
            void print()
            {
              cout<<"5"<<endl;
            }
            void print(int value)
            {
              cout<<value<<endl;
            }
          };
          
      • override(重写/覆盖)
        • 派生类中声明的成员函数与基类中的一个virtual成员函数同名, 并且有着相同的参数列表. 此时发生重写/覆盖, 这个行为是多态的
        • 派生类中重写的成员函数的可见性由派生类指定, 跟基类中的被重写的函数的可见性无关, 和继承方式也无关
      • redefining(重定义/隐藏)
        • 只要派生类中声明了一个与基类中同名的函数, 就发生了重定义. 重定义会隐藏基类中的版本
        • 重写会伴随着重定义的发生, 重定义不一定是多态的
    • 多继承
      • 多继承有些复杂, 说实话也没真正写过相关代码, 很难保证吃透了. 略过
  • 函数对象
    • 在C++中, 如果一个类(或者结构)重载了 () 运算符, 那么其实例可以被称为函数对象
    • 函数对象本质和普通的对象没有任何区别, 唯一不同的是, 重载的 () 运算符使得其可以像函数那样被调用
    • 参考示例
      //这里引自MSVC中模板函数对象类(结构)std::greater的实现
      //注意, 这是一个模板函数对象类
      // STRUCT TEMPLATE greater
      template <class _Ty = void>
      struct greater {
          //_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS是干嘛的暂时看不懂, 与本例无关不管它
          _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type;
          _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type;
          _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type;
      
          //重载()运算符
          constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const {
              return _Left > _Right;
          }
      };
      
      //函数对象使用方法
      //使用匿名对象
      cout << std::greater<int>()(2, 1) << endl;
      //或者构造一个对象来使用
      auto func_obj = std::greater<int>();
      cout << func_obj(1, 2);//函数对象可以像函数那样使用
      
  • 聚合类

    建议直接查看C++参考站(中文机翻味太重, 下面可以选择语言), 这里实在是有点讲不清
    https://zh.cppreference.com/w/cpp/language/aggregate_initialization
    总感觉聚合类是完完全全的语法糖, 但机制又十分复杂, 实在是令人哭笑不得

    • C++ primer 第五版 中的定义(C++11):
      • 所有的成员都是public的
      • 没有定义任何构造函数(定义即实现)
      • 没有类内初始值
      • 没有基类, 没有virtual函数
    • C++17 对聚合类的定义
      • 无用户提供, 继承或 explicit 构造函数(允许显式预置或弃置的构造函数)
      • 没有通过using声明继承基类构造函数
      • 所有non-static成员都是public的
      • 没有virtual函数
      • 没有非public继承的基类
    • 另: 所有的数组都是聚合
    • 一个聚合类的例子
      class C
      {
      public:
        int x;
        int y;
      }
      
    • 聚合类可以通过大括号按照声明的顺序逐个初始化成员变量
      //对于之前的class C, 可以这样初始化, 没有指定的成员变量会被默认构造
      C c0;
      C c1{ 1 };
      C c2{ 1,2 };
      
    • C++17中引入了std::trait is_aggregate<>来测试一个类型是否是聚合的
      //对于上面的class C, 以下代码输出: 1
      cout << std::is_aggregate<C>::value;
      

多态

  • 多态, 指一种事物的多种形态
    • 静态多态, 编译时完成
      • 函数重载
        • 通过对比函数调用与不同的函数参数列表推断具体调用的函数版本
      • 泛型
        • 对实现了统一接口(这里的接口指同一套操作方案)的不同对象使用同一套模板, 提高代码重用性(鸭子类型)
    • 动态多态
      • 面向对象中的多态
    • 这一节主要是指面向对象中的多态
  • virtual关键字
    • 使用virtual关键字修饰的成员函数称为"虚函数". 当使用基类指针(或引用)访问一个派生类对象的成员函数时, 默认情况下将调用基类的函数实现, 除非将成员函数声明为"虚"的, 这时将会调用对象真实类型对应的函数实现.

泛型

  • C++通过使用模板机制来支持泛型编程

利用模板, 可以定义类或函数的操作, 并允许用户指定这些操作应使用的具体类型(引自MSDN)
使用模板时的主要限制是类型参数必须支持模板可能应用于类型参数的任何操作

  • 共有两种模板类型
    • 函数模板
    • 类模板
  • 模板参数列表
    • 类型参数
      • class关键字与typename关键字

    在模板参数列表中使用class关键字和typename关键字完全等效

    • 模板参数列表中可以使用"..."省略号运算符, 表示模板接受任意(包括0)个类型参数
    • C++的模板参数还可接受非类型参数, 也称为值参数
      • 值参数有关信息待补全
  • 模板类型参数自动推导
    • 在模板函数中, C++编译器可以根据模板函数调用来推导函数模板参数列表, 但必须满足一定规则:
      1. xxx
  • 典型写法
      template <class Type>
      Type add0(Type var0, Type var1)
      {
        return var0 + var1;
      };
    
      template <typename Type>
      Type add1(Type var0, Type var1)
      {
        return var0 + var1;
      };
    
  • 模板特化
    • 有时通用的模板无法支持所有的类型进行实例化, 因此可以特化模板, 通俗来讲就是写一个特化的模板来为特别的类型实例化
    • 参考
      //模板函数, 用来判断两个相同类型的值是否相同
      template<typename Type>
      bool equals(Type first, Type second)
      {
        return first == second;
      }
      
  • 可变长度模板
    • 待补全
      有待补全

move语义


异常处理


初始化

  • C++有几种不同的形式用于进行初始化

2.重要关键字或操作符


noexcept

  • noexcept关键字指示编译器, 某一个函数不会抛出异常, 这有利于编译器针对noexcept函数进行优化
  • C++的异常处理相关操作在运行时执行, 也就是说编译器需要生成额外的代码用于异常处理
  • noexcept函数抛出异常时程序会终止
//老式的写法
void f0() throw()
{

}
//新式的写法
void f1() noexcept
{

}
  • noexcept关键字的典型运用场所

    • move构造函数
      • 这会让编译器在可能的情况下使用move构造函数取代copy构造函数以提升效率.
        比如, 当vector容器的容量不足时, 会重新分配内存并转移, 这时使用move构造函数来转移原来的对象可能比使用copy构造函数来复制一个新的对象更优
    • move赋值重载
      • 同上
    • 析构函数
      • 新版本的编译器默认为析构函数添加noexcept关键字
    • 叶子函数(leaf function)

      叶子函数是指在函数内部不分配栈空间, 也不调用其它函数, 也不存储非易失性寄存器, 也不处理异常。

  • 内容参考自博客


decltype

  • decltype是一个关键字(运算符), 用于捕获对象的类型
  • 对于左值与右值有着不同的表现
    • 若decltype的目标类型是(被推断为)左值, 则结果为引用类型
    • 若decltype的目标类型是(被推断为)右值, 则结果不为引用类型
  • 由C++11引入

typeid

  • typeid是一个操作符, 用于返回一个对象的类型信息对象
  • 使用typeid后将产生一个type_info类型的对象(C++中唯一创建该对象的方法)
  • 当typeid操作符的操作数是不带有虚函数的类类型时, typeid操作符会指出操作数的声明类型, 而不是底层对象的类型
    • 个人的理解是: 如果操作数的声明类类型中没有任何虚函数, 那么调用该操作数的所有成员函数都将使用其声明类型版本的实现, 那么返回其底层具体类型将没有任何意义
  • 当没有函数需要设为虚的时, 为达到目的可以将析构函数设为虚的. 事实上, 存在派生类的基类的析构函数必须设为虚的, 否则可能因为不调用派生类的析构函数而不能完全释放资源
//示例1
int a{};
//下面这一行语句的输出是"int"
cout << typeid(a).name();

存在如下类定义

class C0
{ };

class C0_plus:public C0	
{ };

class C1
{
protected:
	//虚函数
	virtual ~C1() { }
};

class C1_plus :public C1
{
	virtual ~C1_plus() { }
};
//示例2

C0* p0 = new C0();
//下面的语句输出"class C0"
cout << typeid(*p0).name() << endl;

C0* p1 = new C0_plus();
//下面的语句输出"class C0"
cout << typeid(*p1).name() << endl;

//---------------------

C1* p3 = new C1();
//下面的语句输出"class C1"
cout << typeid(*p3).name() << endl;

C1* p4 = new C1_plus();
//下面的语句输出"class C1_plus"
cout << typeid(*p4).name() << endl;

auto

  • auto关键字早在C++98时就已经存在. 当时用来指示一个变量具有自动的生命周期, 可是一个局部变量总是默认拥有自动生命周期, 因此auto很少被使用, 并且是多余的
  • C++11标准删除了auto关键字的传统用法. 现在, auto关键字用于自动类型推断

题外话
显而易见, auto执行的类型推断是在编译时完成的,不会对运行效率有任何影响.
同时, C++编译器在编译时本身也需要判断声明类型和实际类型是否匹配, 因此应该也不会影响编译速度 (就算影响了也不差这点, 扯远了)

  • C++14后, lambda表达式中的参数捕获也能使用auto
  • auto类型推导可以用于模板, 用以省略模板参数类型的衍生类型在模板参数列表中的声明
    本例摘自博客
    //传统写法, 需要专门为makeObject()的返回值类型在模板的参数列表中添加一项
    template <typename Product, typename Creator>
    void processProduct(const Creator& creator) {  
      Product* val = creator.makeObject();
      // do somthing with val
    }
    
    //使用auto的写法, 得到一定简化
    template <typename Creator>
    void processProduct(const Creator& creator) {
      auto val = creator.makeObject();
      // do somthing with val
    } 
    

const


class


typename


explicit


new

  • 用来在内存(堆)中开辟一块空间, 用户获得一个指向内存中目标位置的指针
    • 用法参考示例
      //申请一块指向单个int对象的内存, 并将值初始化为"5"
      int *ptr_0 = new int(5);
      
      //申请一块指向5个连续int类型对象的内存
      //使用C++11统一的花括号初始化, 直观地对内存进行初始化
      int * ptr_1 = new int[5]{1,2,3,4,5};
      
  • new关键字不仅可以用来在内存中开辟一段全新的空间, 还能用于对已经申请的空间, 调用构造函数重新初始化
    int var{ 5 };
    //调用int的构造函数重新对内存初始化
    int* p = new (&var) int(9);
    
    //输出:
    //var: 9
    //*p : 9
    //address &var: 00000067478FF984
    //address p   : 00000067478FF984
    cout
      	<< "var: " << var << endl
      	<< "*p : " << *p << endl
      	<< "address &var: " << &var << endl
      	<< "address p   : " << p << endl;
    

delete

  • 删除使用new申请的内存
    • 用法参考示例
      //删除之前申请的内存
      delete ptr_0;
      //删除数组的写法, 注意
      delete [] ptr_1;
      

using

  • 传统上using用于导入命名空间中的全部或者部分内容
    比如:
    using namespace std;//导入std命名空间中的所有内容
    
    using std::cout;//只导入命名空间std中的cout
    
  • 较新的C++中(C++11), using可以用于取代typedef
    虽然功能相同但(似乎)更加直观
    //这一条语句为std::string取了一个新的别名"stdstring"
    using stdstring = std::string;
    //等价于如下传统写法
    typedef std::string stdstring;
    
    也可以用在类声明中
    class C
    {
    public:
      using uss = std::string;
      typedef std::string tss;
    };
    
  • 为派生类引入基类的成员
    • 这个和第一条比较类似
  • 别名模板
    //别名模板
    template<class Type>
    using ptr = Type*;
    
    ptr<int> x;//ptr<int> 是 int* 类型的别名
    
    //别名模板
    template<typename type>
    using BinaryVector = std::pair<type, type>;
    
    BinaryVector<double> bvd;
    

mutable

  • 用于labmda表达式, 指示lambda表达式中可以修改捕获到的变量

exception


3.STL/C++标准库


initializer_list

C++ primer 6.2.6 标准库 initializer_list 类

  • initializer_list是C++11中引入的内容

std::vector

  • vector是C++中最通用,也最常用的容器

如果不确定要使用什么容器, 可以先考虑考虑vector
(不知道哪听来的)

  • vector容器将其内的元素连续地存放在一整个内存段中, 本质上是一个数组
  • 容器扩展机制
    • 再说
  • vector由C++模板实现, 可以存放任意类型的元素(注意, 一个vector实例只能存放统一的某种类型的元素)
  • 成员函数
    • void push_back(const _Ty& _Val)
      • STL中很多其它容器类模板(比如链表)也有该成员函数, 作用是将一个新的元素置入容器末尾
    • void resize(const size_type _Newsize)
      • 用于重新指定容器的大小(这里是指元素的个数)
      • _Newsize大于当前元素个数时, 新的元素使用默认构造函数进行构造(比如int类型的变量使用int()进行构造)
  • 迭代器
    • 迭代器用于遍历, 访问, 修改vector容器中的元素.
    • 本质上来讲有点类似于一种经过封装的指针对象. 使用*运算符重载来获取元素
    • vector的迭代器重载了++, --, +, -等类似数学运算的运算符来移动(定位)迭代器
    • vector有多种迭代器
      • vector<...>::iterator
        • 普通迭代器, 可以访问或修改元素
      • vector<...>::reverse_iterator
        • 反向迭代器, 可以访问或修改元素
        • 不同于普通迭代器的是, 当迭代器往后移动时(执行++, 或+某个数字), 在容器中实际上朝容器头部移动
      • vector<...>::const_iterator
        • const迭代器
      • vector<...>::const_reverse_iterator
        • const反向迭代器
  • 效率问题---使用vector最重要的部分
    • 注: 以下内容使用C++的库进行了时间测量, 仅供参考未必正确
    • 尽量不要使用assign()函数进行大面积单值初始化, 函数非常慢.
      • 使用resize()扩大size并自己写循环赋值, 效率可以提高60%
    • 能不用push_back()或者emplace_back()就不用.
      • 如果预先已经知道了需要多少空间, 直接用resize()扩大size并自己写循环赋值, 效率会高7~8倍
      • 即使预先使用reserve扩大了capacity, 使用这两个函数的效率仍然非常非常不乐观
    • vector尽量不要使用迭代器进行访问或者赋值, 迭代器的定位运算十分耗时(诸如++,--, +5, -5等等)
      • 使用下标索引访问vector比起用迭代器访问效率高出45%左右
      • 使用数字下标遍历整个vector比用迭代器遍历(使用迭代器的前置++运算符)整个vector效率高出80%左右
    • 迭代器自增自减时, 用前置运算符替代后置运算符
      • iter++比++iter慢一倍左右

4.源码剖析

5.一些术语

  • RTTI
    • Run-Time Type Identification: 运行时类型检测
  • RAII机制
    • Resource Acquisition Is Initialization: 资源获取就是初始化
    • 是C++语言的一种管理资源、避免泄漏的惯用法
    • 简单的说, RAII 的做法是使用一个对象, 在其构造时获取资源, 在对象生命期控制对资源的访问使之始终保持有效, 最后在对象析构的时候释放资源
posted @ 2020-01-29 02:06  九家的酱瓶子  阅读(152)  评论(0编辑  收藏  举报