现代C++

文章目录

😍现代C++ 语言

​ C++ 是一个用户群体相当大的语言。从 C++98 的出现到 C++11 的正式定稿经历了长达十年多之久的积累。C++14/17 则是作为对 C++11 的重要补充和优化,C++20 则将这门语言领进了现代化的大门,所有这些新标准中扩充的特性,给 C++ 这门语言注入了新的活力。 那些还在坚持使用传统 C++ (本书把 C++98 及其之前的 C++ 特性均称之为传统 C++)而未接触过现代 C++ 的 C++ 程序员在见到诸如 Lambda 表达式这类全新特性时,甚至会流露出『学的不是同一门语言』的惊叹之情。

现代 C++ (本书中均指 C++11/14/17/20) 为传统 C++ 注入的大量特性使得整个 C++ 变得更加像一门现代化的语言。现代 C++ 不仅仅增强了 C++ 语言自身的可用性,auto 关键字语义的修改使得我们更加有信心来操控极度复杂的模板类型。同时还对语言运行期进行了大量的强化,Lambda 表达式的出现让 C++ 具有了『匿名函数』的『闭包』特性,而这一特性几乎在现代的编程语言(诸如 Python/Swift/… )中已经司空见惯,右值引用的出现解决了 C++ 长期以来被人诟病的临时对象效率问题等等。

​ C++17 则是近三年依赖 C++ 社区一致推进的方向,也指出了 现代C++ 编程的一个重要发展方向。尽管它的出现并不如 C++11 的分量之重,但它包含了大量小而美的语言与特性(例如结构化绑定),这些特性的出现再一次修正了我们在 C++ 中的编程范式。

​ 现代 C++ 还为自身的标准库增加了非常多的工具和方法,诸如在语言自身标准的层面上制定了 std::thread,从而支持了并发编程,在不同平台上不再依赖于系统底层的 API,实现了语言层面的跨平台支持;std::regex 提供了完整的正则表达式支持等等。C++98 已经被实践证明了是一种非常成功的『范型』,而现代 C++ 的出现,则进一步推动这种范型,让 C++ 成为系统程序设计和库开发更好的语言。Concept 提供了对模板参数编译期的检查,进一步增强了语言整体的可用性。

​ 总而言之,我们作为 C++ 的拥护与实践者,始终保持接纳新事物的开放心态,才能更快的推进 C++ 的发展,使得这门古老而又新颖的语言更加充满活力。1

C++是“面向过程、面向对象、泛型编程、编译性的静态强类型语言”。

一、前言

阅读本文需要传统C++基础。

PDF书籍:

现代 C++ 教程:高速上手 C++11/14/17/20

网站:

cplusplus.com

工具库 (lerogo.com)

C++ 参考手册 - C++中文 - API参考文档 (apiref.com)

序言 现代 C++ 教程: 高速上手 C++ 11/14/17/20 - Modern C++ Tutorial: C++ 11/14/17/20 On the Fly (changkun.de)

中文的C++ Template的教学指南。与知名书籍C++ Templates不同,该系列教程将C++ Templates作为一门图灵完备的语言来讲授,以求帮助读者对Meta-Programming融会贯通。(正在施工中)

问答:

C++面试-基础篇(全网最详细) - 知乎 (zhihu.com)

1.0 开始

1.0.1 关键字

绿色表示C++11增加的关键字。蓝色表示C++20增加的关键字。

类型关键字作用备注
控制流if条件语句中的条件判断在C++17后可以在if语句中定义局部对象。
控制流else条件语句中的否定分支
控制流switch多分支条件选择语句C++17后可以在case中直接定义局部对象(在C中需要使用{})。
控制流caseswitch 中的分支标签
控制流defaultswitch 中的默认分支在switch表达式匹配不到任何case时,匹配default
循环while
循环dodo-while循环
循环for已知次数的循环C++11支持范围for循环。
循环break提前结束结束当前循环或 switch 语句。
循环continue直接重开结束当前循环的迭代,继续下一轮循环。
循环goto无条件转移语句跳转到程序中指定的标号位置。常做错误处理。
函数return从函数中返回值在现代编译器中存在返回值优化。
数据类型bool布尔类型C++中bool占用一个字节,但 vector<bool> 却存在差异。
数据类型char字符类型也常用于存储1字节的整数。
数据类型short短整数类型
数据类型int整数类型
数据类型long长整数类型
数据类型float单精度浮点数类型
数据类型double双精度浮点数类型
数据类型void空类型通常用于函数无返回值或无参的情况。
数据类型unsigned无符号类型
数据类型signed有符号类型
数据类型wchar_t宽字符类型也称为双字节类型、主要用在国际化程序的实现中。
数据类型 char16_t16 位字符类型由于wchar_t在不同系统环境字节数不一样,所以为了统一而出现。
数据类型char32_t32 位字符类型
数据类型auto自动类型推导在C++11前表示自动存储类型,自C++11开始变更为自动数据类型。
存储类static静态类型修饰成员函数和成员对象表示其独立于类的存在,其他用法与C一致
存储类volatile易变类型表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取。作用:改善编译器的优化能力。
存储类extern声明外部变量或函数除与C一致的用法外,还可以使用extern "C"{},用于在 C++ 中调用 C 语言编写的函数或者在 C++ 中提供给 C 语言使用的接口。
存储类register寄存器类型现期的编译器会自行决定局部变量的寄存器类型,该关键字在C++17后被遗弃、无任何作用,但任然保留以备他用。
存储类thread_local线程期对象指示对象的生命周期为线程的生命周期期。每个线程拥有其自身的对象实例。
类型定义typedef定义类型别名
类成员修饰mutable易变对象mutable是为了突破const成员函数无法修改成员对象的限制而设置。但也被用于lambda表达式中修改值传递的对象。
类成员修饰virtual定义虚函数虚函数是C++的多态机制,使用virtual关键字声明。派生类重写后、允许通过基类指针访问基类与派生类的同名函数。
类成员修饰explicit显式构造函数声明explicit关键字的作用就是防止类的单参数构造函数的隐式自动转换。
类成员修饰friend友元类或函数声明突破类的访问权限限制,以访问任何成员。
类/成员修饰final终态修饰符当应用于类时,表示该类是最终类,不能被其他类继承。
当应用于成员函数时,表示该函数是最终版本,不能被派生类重写。
类成员修饰override显示重写(覆盖)当应用于派生类中的虚函数时,表示该函数是对基类虚函数的重写或覆盖
访问权限public公有成员访问权限
访问权限private私有成员访问权限
访问权限protected保护成员访问权限
构造类型class类的定义
构造类型struct结构体定义
构造类型union联合体定义
枚举类型enum枚举类型定义
命名空间namespace命名空间定义细分全局作用域。
内存管理new动态分配内存分配内存并调用构造函数。
内存管理delete释放动态分配的内存调用析构函数并释放内存。
命名using模板类型别名使用using可以很方便的完成对模板类型的别名定义。
运算符sizeof计算数据类型大小编译期运算符
运算符typeid返回表达式的类型信息静态类型语言,类型在编译时已经确定,但在多态时,无法在编译时确定对象的实际类型(基类指针或引用指向派生类对象)。
这是因为派生类可能在运行时进行创建和销毁,而我们需要在运行时才能获取实际类型信息。
运算符重载关键字operator定义运算符重载为了使自定义的类型可以直接进行算数运算、比较、赋值、移位等操作。
类型转换static_cast静态类型转换在具有相关性的类型之间转换(可以丢弃具备关联类型的CV限定符)。
类型转换const_cast常类型转换只能添加或删除constvolatile属性。
类型转换dynamic_cast动态类型转换用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。
类型转换reinterpret_cast重新解释转换它会忽略类型之间的任何关联关系,通过重新解释内存内容来进行转换。但它却无法丢弃constvolatile属性。(自由度最高,风险性最高)
异常处理throw抛出异常用于在程序中显式地抛出异常。
异常处理try异常处理尝试块用于标记可能抛出异常的代码块。try块出现异常时,代码跳转到catch块中。
异常处理catch异常处理捕获块用于捕获并处理异常。在 catch 块中,可以编写处理异常的代码逻辑。
空指针nullptr空指针常量C++11后,用以解决NULL宏对因无法隐式转换导致的函数重载决议出现有违自觉的问题。
类型信息alignof查询对象对齐要求alignof(int); // 4
用于获取指定类型的对齐系数,返回一个 size_t 类型的值。
类型检查alignas指定对象的对齐方式struct alignas(int) S {};
用于设置指定对齐系数。alignas(N) 表示把绝对对齐系数设置为N字节,N必须是2的正数幂(1、2、4、16)。
编译期static_assert静态断言编译期断言,不生成目标代码,因此不会造成任何运行期性能损失。(将错误排查提前到编译阶段。)
编译期constexpr常量表达式指值不会改变并且在编译期就得到计算结果的表达式(函数)、仅表示支持在编译期求值(是否真的在编译期求值,不确定)。
编译期consteval编译期求值要求表达式、函数必须在编译期求值
编译期constinit编译期初始化修饰变量,保证变量在编译期初始化。(只能使用 constexprconsteval 函数初始化 constinit 变量)
类型推导decltype推导表达式的类型在C++11时,auto已能对对象的类型进行推导,但无法对表达式的类型进行推导,decltype 随之产生。
异常规范noexcept指示函数不抛出异常两种情况下应该使用noexcept:确信函数不会抛出 和(或)不知道如何(无法)处理异常。
模板export模板的外部定义export用于模板的定义和声明分离的关键字。
然而,export关键字在实践中引入了复杂性,并且很少有编译器实现对其进行支持。因此,C++标准委员会决定在C++20标准中将其从语言规范中移除
模板typename在模板中引入类型名typename T::SubType *ptr;
为了解决模板函数中使用模板类中的从属类(SubType 类型被定义在类T中)而产生。(示意T::SubType是T中的类型而不是T的成员)
模板concept模板实参约束以在编译期检查模板实参是否满足指定的约束(解决模板函数无法处理某些特定类型的实例化进行限定)。
其他asm内嵌汇编语句
其他inline内联函数/变量建议编译器在编译时将所修饰的函数代码直接嵌入到主调函数中。C++17后增加内联变量允许我们在头文件中定义变量,而无需担心多重定义错误。
其他true布尔的真值
其他false布尔的假值
其他this指向当前对象的指针当类的成员函数的参数和类的成员同名时,为了区分这两个对象时产生。
其他friend类的友元为其他的类(友元类)或函数(友元函数)突破该类的访问限定符而产生。

1.0.2 运算符

类别运算符结合性
后缀() [] -> . ++ --从左到右
一元+ - ! ~ ++ -- (type) * & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND&从左到右
位异或 XOR^从左到右
位或 OR``
逻辑与 AND&&从左到右
逻辑或 OR`
条件?:从右到左
赋值= += -= *= /= %= >>= <<= &= ^= `=`
逗号,从左到右

1.0.3 发展史

C++ 的历史 (lerogo.com)

C++ 语言的发展历史可以分为以下几个阶段:

  1. C++ 1.0(1985年):作为 C 语言的扩展,加入了类、继承、多态等面向对象编程特性。
  2. C++ 2.0(1989年):加入了模板、异常处理、命名空间等新特性,并对标准库进行了重大改进。
  3. C++ 3.0(1991年):加入了运行时类型识别(RTTI)、抽象类、虚基类等特性,并且对异常机制进行了改进。
  4. C++ 11(2011年):加入了自动类型推导、lambda 表达式、右值引用、智能指针、线程库等新特性,并且对标准库进行了扩展。
  5. C++ 14(2014年):修复了一些 C++ 11 的问题,并加入了二进制字面量、泛型 Lambda 表达式、constexpr 函数等新特性。
  6. C++ 17(2017年):加入了结构化绑定、fold 表达式、if constexpr 等新特性,并对标准库进行了扩展。
  7. C++ 20(2020年):加入了概念(Concepts)、协程(Coroutines)、三方比较运算符(Three-way comparison operator)等新特性,并且对标准库进行了扩展。

C++各个标准的语法变更历史的概述:

C++ 89/C++98

  • 这是最初的 C++ 标准,引入了类、模板、异常处理等基本特性。

C++ 03

  • 主要是对 C++98 的一些技术错误和缺陷进行修正,并没有引入太多新的语法特性。

C++ 11

  • 引入了自动类型推导(auto 关键字)和区间循环(range-based for loop)。
  • Lambda 表达式:允许在代码中定义匿名函数。
  • 右值引用(rvalue reference)和移动语义:引入了 && 运算符,支持将资源的所有权从一个对象转移到另一个对象,提高了性能和效率。
  • 智能指针(smart pointers):引入了 std::shared_ptrstd::unique_ptr 等智能指针类,用于管理动态分配的内存。
  • 新的标准库组件:如并发编程库、正则表达式库、原子操作等。

C++ 14

  • 二进制字面量(binary literals):引入了以 0b0B 开头的二进制表示方法,例如 0b101010
  • 泛型 Lambda 表达式:Lambda 表达式可以使用模板参数。
  • constexpr 函数的扩展:constexpr 函数可以包含更多的语法,并且可以在运行时使用。
  • 基础字符串字面量(raw string literals):引入了原始字符串字面量,以 R"()" 的形式表示,可用于包含大段的原始文本。

C++ 17

  • 结构化绑定(structured bindings):允许直接解构元组或其他数据结构,将其成员绑定到变量中。
  • 折叠表达式(fold expressions):可用于简化模板元编程中的复杂表达式。
  • if constexpr:引入了编译时条件判断,用于在编译期间选择不同的代码分支。
  • 行内变量声明(inline variables):允许在头文件中定义并初始化变量(一种简洁且高效的方式来定义和共享全局变量)。
  • 并行算法:引入了一系列支持并行执行的标准算法。

C++ 20

  • 概念(Concepts):引入了对泛型编程中概念的支持,用于约束模板参数的类型。
  • 协程(Coroutines):引入了协程支持,允许在函数中暂停和恢复执行。
  • 三方比较运算符(Three-way comparison operator):引入了 <=> 运算符,用于支持三方比较操作。
  • 初始化上下文相关的内联变量(constinit):引入了 constinit 关键字,用于指示编译时非常量求值问题。

C++各版本的关键字变更历史

标准版本关键字变更
C++89/C++98auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, int, long, register, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while
C++03bool, mutable, namespace, reinterpret_cast, static_cast, typename
C++11alignas, alignof, char16_t, char32_t, constexpr, decltype, noexcept, nullptr, static_assert, thread_local
C++14无新增关键字
C++17if constexpr, inline variables
C++20concept, consteval, constinit, co_await, co_return, co_yield, requires
C++23 (预计)import, module, export (从 C++20 移至 C++23), synchronized

请注意,以上是对各个 C++ 标准的关键字变更进行的概述,并不是详尽无遗的列表。每个标准还包含了其他的改进和修复,以提高语言的性能和可用性。此外,某些关键字的用法也可能发生变化,例如 C++11 引入的 auto 关键字的用法与 C++17 引入的 auto 关键字略有不同。

1.0.4 易混点

不同的方法或函数之间的关系
  • 隐藏(Hiding): 隐藏指的是在派生类中定义了与基类中同名的成员函数(方法),从而使得基类中的同名成员函数无法被派生类直接访问到。当派生类通过对象调用该同名成员函数时,实际上只会调用派生类中的成员函数,而不会调用基类中的同名函数。这种情况下,基类的同名成员函数被隐藏了。

      class Base {
      public:
        void display() {
          cout << "Base class display() function" << endl;
        }
      };
      
      class Derived : public Base {
      public:
        void display() {
          cout << "Derived class display() function" << endl;
        }
      };
      
      int main() {
        Derived d;
        d.display();  // 调用派生类的display()函数
        d.Base::display();  // 通过作用域解析运算符调用基类的display()函数
        return 0;
      }
      /*
      Derived class display() function
      Base class display() function
      */
    
  • 重写(Override): 重写是指派生类重新定义了继承自基类的虚函数。重写要求派生类的函数名称、参数列表和返回类型完全相同,并且使用关键字 override 明确表示重写基类的虚函数。重写的目的是为了实现多态性,即通过基类指针或引用调用派生类的函数时,能够根据对象的实际类型来调用相应的函数。

      class Shape {
      public:
        virtual void draw() {
          cout << "Drawing a shape" << endl;
        }
      };
      
      class Circle : public Shape {
      public:
        void draw() override {
          cout << "Drawing a circle" << endl;
        }
      };
    
  • 重载(Overload): 重载发生在同一个类(准确来说是同一作用域)中,指的是在一个类中定义了多个同名但参数列表不同的成员函数。重载函数具有相同的名称但不同的参数列表,可以根据传入的参数类型或个数的不同来调用不同的重载函数。重载函数之间的区别主要依靠参数列表,返回类型通常不是区分重载函数的条件。

      class Math {
      public:
        int add(int a, int b) {
          return a + b;
        }
      
        float add(float a, float b) {
          return a + b;
        }
      };
    
虚函数和纯虚函数
  • 虚函数(Virtual Functions)是在基类中声明的函数,可以在派生类中被重写(覆盖),不是必须要求重写。通过在函数声明前加上关键字 virtual 来定义虚函数。当通过基类的指针或引用调用虚函数时,将根据实际对象的类型来决定调用哪个版本的函数。

      class Base {
      public:
          virtual void foo() {
              cout << "Base::foo()" << endl;
          }
      };
      
      class Derived : public Base {
      public:
          void foo() {
              cout << "Derived::foo()" << endl;
          }
      };
    
  • 纯虚函数(Pure Virtual Functions)是在基类中声明的没有实际实现的虚函数。通过在函数声明后加上= 0来定义纯虚函数。纯虚函数在基类中没有具体的实现,但是派生类必须实现它们,否则派生类也会成为抽象类

    含有纯虚函数的类被称为抽象类,是不能被实例化的,只能作为基类来派生新的类使用。

      class Shape {
      public:
          virtual void draw() = 0;  // 纯虚函数
      };
      
      class Circle : public Shape {
      public:
          void draw() {
              cout << "Drawing a circle." << endl;
          }
      };
    
初始化列表

​ 这应该叫名称混淆点,因为这完全是两个不一样的东西,一个是语法,一个是类。不过在C++11以前,人们说的初始化列表都是构造函数中的冒号初始化。

  1. std::initializer_liststd::initializer_list 是C++11引入的一种特殊容器类型,它用于将一组值作为参数传递给函数或对象的构造函数。这种初始化列表使用花括号 {} 来表示,并且是只读的,不能修改其中的值。例如:

    std::initializer_list<int> numbers = {1, 2, 3};
    
  2. 冒号初始化列表:在类的构造函数中,可以使用冒号初始化列表来初始化成员变量。这种初始化列表出现在构造函数的定义之后,冒号之后,用于对成员变量进行初始化。例如:

    class MyClass {
    public:
        MyClass(int number) : m_number(number) {}
    private:
        int m_number;
    };
    

1.1 特点

C++不是C的一个超集(增强版)。

C++11 语言核心的改进中,最为关注的有 rvalue reference (这里),lambda,variadic template。

  1. C++ 不允许直接将 void * 隐式转换到其他类型的指针(C可以)。

  2. C++ 不允许隐式声明(而C允许,但仅仅出现警告,而这将产生难以发现的bug)。

    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
        int *i = malloc(sizeof(int)); 
        /* 使用编译命令(gcc -Wall)(于c标准无关,使用gcc -std2x -Wall,效果一致),提示警告(但可能无法运行)
         * warning: 函数' malloc '的隐式声明[-wimplicit-function-declaration]
         * warning: 内置函数' malloc'的不兼容隐式声明( 函数被隐式声明为:int malloc(unsigned int) )
         */
        /* 使用编译命令(g++ -Wall)提示错误并希望你加上头文件:#include <cstdlib>
         * error: ' malloc '没有在此范围内声明
         * 加上头文件后编译任然报错:error: 从'void* '到' int* '的转换无效[-fpermissive]
         */
        if (i == NULL) {
            printf("malloc failed\n");
            return 1;
        }
        return 0;
    }
    
  3. 乱序指定初始化、嵌套指定初始化、指定初始化器和正则初始化器的混合以及数组的指定初始化在C编程语言中都受支持,但在C++中不允许。

1.2 名词解释

  1. 静态类型语言:变量和表达式的类型必须在编译时声明,编译器可以帮助我们提前避免程序在运行期间有可能因为类型发生的一些错误。

  2. 强类型语言:不会处理与类型定义明显矛盾的运算,而是把它标记为一个问题,并作为错误抛出。

  3. 数组退化:当数组作为实参进行传递时会自动退化为指针(是一种隐式转换),传入数组的首地址。数组退化成指针后其「类型」「大小信息」将会丢失。

  4. 窄化转换:出现在强制转换中,表示对算术值出现了有精度的转换,不能完全表达转换之前所代表的值。

  5. 谓词(谓语):你之前一定在英语语法课上听过最基本的句式结构是:主谓宾,而谓语就是可以具有“动词”语义的词。其实在C++中也存在这样的谓词,函数就是典型的谓词语义。STL中的谓词类似这样:bool func(T&a);或者bool func(T&a,T&b);常见的谓词:函数、函数指针、函数对象、lambda表达式,库定义的函数对象。如std::find_if()std::count_if()等函数需要传入bool型的判断条件参数。

  6. 隐式声明:函数或对象仅有定义而没有进行声明;函数的定义在库(静态库或动态库中),但没有引入头文件。

  7. 匿名对象:不具有名称的对象。产生这种对象的方法通常是这样的:int(3)

  8. RVO(返回值优化):编译器可以减少函数返回时生成匿名对象的个数,从某种程度上可以提高程序的运行效率,对需要分配大量内存的类对象其值复制过程十分友好。

  9. NRVO(具名返回值优化)2NRVO优化的是返回指的对象在return语句前就已经构造完成(指在该函数体到return语句前完成构造的局部对象)。与RVO一样都是对函数内创建的返回值对象的优化、都是从函数返回值创建对象的优化、都是C++11之后才具有的编译器优化。

    可以使用如下编译选项禁用**(N)RVO**:

   -fno-elide-constructors
   
   int fun()  { return int(1);}
   auto fun1() {
       struct Obj
       {
           int a;
           Obj(int a): a(a){}
       };
       Obj a(1);
       return a;
   };
   
   void main(){
       int a = fun(); // RVO
       auto b = fun1();// NRVO
   }
  1. CV限定符:是 constvolatile 关键字的统称。

  2. 重载决议:是指编译器在调用函数或运算符时,根据参数类型、数量和顺序来选择最佳匹配的重载函数或运算符的过程。C++ 使用重载决议来确定应该调用哪个重载函数或运算符。如果没有找到完全匹配的函数或运算符,编译器将进行一系列的重载解析规则,以确定最佳匹配。

  3. 右值:指的是值可以被获取但不具有持久性的表达式。例如,常量、临时变量、字面值都是右值。右值可以作为函数的参数或返回值,但不能被赋值。是一个广义的概念,包括纯右值和将亡值。

    以下举例左值和右值的区别。简单来说,右值不可以被赋值,左值可以取地址。

    char i[4] = {1, 2, 3, 4};
    int main()
    {
        ++i[0] = 1; // 左值
        i[0]++ = 0; // 纯右值 error: lvalue required as left operand of assignment
        auto a = *reinterpret_cast<int *>(i) = 65536; // 左值
        &a = i + 3; // 纯右值 error: lvalue required as left operand of assignment
        
        i[2] + i[3] = 0; // 将亡值
    }
    /*
    ++i是左值,i++是纯右值
    解引用表达式 *p是左值,取地址表达式 &a 是纯右值。
    */
    
  4. 纯右值:是没有名称、不可寻址的表达式,通常是临时对象或字面量。

  5. 将亡值:是即将被销毁并且可以转移所有权的表达式,是特殊的右值。

    C++11引入的新概念,指的是即将被销毁并且可以转移其所有权的表达式,通常使用 std::move() 进行转移。将亡值通常是右值引用类型的变量、函数返回值或强制类型转换后的表达式。

    C++11中的将亡值是随着右值引用的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关。所谓的将亡值表达式,就是下列表达式:

    • 返回右值引用的函数的调用表达式。
    • 转换为右值引用的转换函数的调用表达式。
  6. 右值引用:一种引用类型,用于绑定临时对象或将所有权转移给另一个对象。右值引用使用"&&"符号表示

  7. 悬垂引用:是指一个引用在其所引用的对象被销毁后依然存在。当一个对象被销毁时,与之相关的引用仍然存在,但指向的对象已经不存在了。使用悬垂引用是一种严重的错误,因为访问悬垂引用可能会导致未定义的行为。这是因为悬垂引用指向的内存可能已经被其他对象或数据覆盖,或者已经被释放回操作系统。

  8. 完美转发:是一种C++特性,模板函数可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变(保留原始参数的值和类型)。

  9. 引用折叠

  10. 万能引用

  11. 移动语义:将资源从一个对象转移到另一个对象,而不需要进行深拷贝操作。这对于管理动态内存或大型对象的效率非常重要。

  12. RTTI :(Run-Time Type Identification)运行时类型识别(RTTI)是一种机制,**用于在程序运行时获取和操作对象的类型信息。**主要有两种形式:dynamic_casttypeid运算符。注意,使用RTTI可能会导致运行时开销,因此需要谨慎使用。

    • dynamic_cast用于在运行时将指向基类的指针或引用转换为派生类的指针或引用。如果转换成功,则返回指向派生类的指针或引用;如果转换失败,则返回空指针(对指针进行转换)或引发std::bad_cast异常(对引用进行转换)。
    • typeid运算符:typeid用于在运行时获取对象的类型信息。它返回一个std::type_info对象,表示对象的实际类型。
  13. 常量求值语境:指在表达式中某值必须为常量的情况。

    // 在常量求值语境时,不同版本的getVal函数
    constexpr int getVal()
    {
        if consteval {  // C++23 常量求值语境时执行的分支
            return 10;	// if consteval {}; consteval前后不能有圆括号,后面必须接花括号
        } 
        else{
            return 20;
        }
    }
    // C++14
    #include <utility>
    constexpr int getVal()
    {
        if (std::__is_constant_evaluated())  // C++14
            return 10;
        else
            return 20;
    }
    // C++20
    #include <utility>
    constexpr int getVal()
    {
        if (std::is_constant_evaluated())  // C++20
            return 10;
        else
            return 20;
    }
    
    // 以下均是常量求值语境
    static_assert(getVal() == 10);
    char a[getVal()];
    ...
        case getVal():...
    ...
    
  14. 立即函数:被关键字consteval修饰的函数,又称consteval函数。它必须在编译时被求值,并且不能包含任何运行时的代码。换句话说,consteval函数必须在编译时产生结果。

    // 立即函数
    #include <utility>
    consteval int f(int i) 
    { 
        return i; 
    }
    
    constexpr int getVal(int a) // 本节 常量求值语境 中C++20版本的微改
    {
        if (std::is_constant_evaluated())
            return f(a) + 1; // error: call to consteval function 'f' is not a constant expression
        else
            return 20;
    }
    

    ​ 在语义角度看,if constexpr (std::is_constant_evaluated())已经限定了常量语境了,为何还不能调用立即函数?但是constexprconsteval的底层机理并不一致,所以并不相通。

    因此为解决这个问题,C++23 新增 if consteval { } 来代替 if(std::is_constant_evaluated())

  15. 默认参数提升:说的是参数提升的行为是被编译器默认的,而不是默认参数的提升。是指在函数调用时,当实参与函数原型中的形参类型不完全匹配时,编译器会向上进行隐式类型转换的过程。

    链接中详细提到了默认参数提升的细节:可变长参数列表误区与陷阱——va_arg不可接受的类型_c\c++ va_arg 用法不正确

    • float 类型的实际参数将提升到 double
    • charshort 和相应的signedunsigned 类型的实际参数提升到int
    • 如果 int 不能存储原值,则提升到unsigned int
    void foo(int a, double b = 3.14) {
        std::cout << "a = " << a << ", b = " << b << std::endl;
    }
    {
        char c = 'A';
        short s = 123;
        float f = 2.718f;
    
        foo(c);     // c被提升为int类型,使用默认参数3.14
        foo(s, f);  // s被提升为int类型,f被提升double
    }
    
    a = 65, b = 3.14
    a = 123, b = 2.718
    
  16. ODR

  17. 非类类型:指的是不是类(class)类型的类型,例如基本数据类型(如 intfloatchar 等)和枚举类型。这些类型在 C++ 中并没有封装成类(class)的形式,因此被称为非类类型。

1.3 约定

使用C++的规则

  1. 尽量不要使用 usign namespace std;

  2. 尽量使用{}初始化对象(能够使编译器检测到窄化转换)。

  3. 优先选择任务而不是线程。

  4. 优先使用智能指针,而不是原始指针。

  5. 不使用可能因未定义行为产生歧义的算法(未定义行为:语言标准未做规定的行为。例如:函数实参和同类运算符的计算顺序等)。

意图规则

  1. 在代码中直接表达思想(通过成员方法名称、返回值类型和STL算法等直接的表达意图)。
  2. 使用ISO C++标准编写代码(减少使用编译器扩展的C++功能,因为能够减少实现定义行为3)。

1.4 被弃用的特性

​ 被弃用不代表不能使用,而是暗示程序员这些特性在未来的标准中消失,应该避免使用(但是出于兼容性的考虑,大部分特性其实会被永久保留)。

  1. 不允许将字符串字面值常量赋值给char *(如:char *str="hello"),而是使用const char *str="hello"
  2. C++98 异常说明、 unexpected_handlerset_unexpected() 等相关特性被弃用,应该使用 noexcept
  3. auto_ptr 被弃用,应使用 unique_ptr
  4. register 关键字被弃用,可以使用但不再具备任何实际含义。
  5. bool 类型的 ++ 操作被弃用。
  6. 存在析构函数的类自动生成拷贝构造函数和拷贝赋值运算符的特性被弃用。
  7. C 语言风格的类型转换被弃用(即在变量前使用 (convert_type)),应该使用 static_castreinterpret_castconst_cast 来进行类型转换。
  8. 特别地,在 C++17 标准中弃用了一些可以使用的 C 标准库,例如 <ccomplex><cstdalign><cstdbool><ctgmath> 等。
  9. 分离编译模式无法对模板进行,标准 C++ 为此制定了“模板分离编译模式(Separation Model)”及 export 关键字。然而由于 template 语义本身的特殊性使得 export 在表现的时候性能很次,因此,该标准受到了几乎所有知名编译器供应商的强烈抵制。在较早的 C++ 草案中,export 关键字被用来表示模板的分离式定义,即模板的声明和定义可以分开写。然而,由于实现上的挑战和语义上的复杂性,C++ 标准委员会决定在 C++20 中移除对 export 关键字的支持。 实际上,这几乎没有影响,因为大多数编译器(MSVC、GCC)从不支持它。

1.5 语言可用性的强化

语言可用性是指发生在编译器前的语言行为。代指用户的体验:实用性(能用)、可用性(好用)、赞许性(推荐用)。

可用性分为:易学性、高效性、可记忆性、容错性、满意度。

  • 易学性:新手在第一次使用C++,是否可以轻松完成基本任务?
  • 高效性:当程序员学会使用你的产品后,他的使用效率有多高?
  • 可记忆性:当程序员在一段时间内不使用C++后,再一次使用C++时,他们的熟练度如何?
  • 容错性::程序员犯了多少错误,这些错误有多严重,以及他们从错误中恢复的难易程度?
  • 满意度:程序员使用C++的愉快程度如何?

1.5.1 常量

1.5.1.1使用 nullptr替代 NULL(C++11)

​ 在某种意义上来说,传统 C++ 会把 NULL0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0

C++ 不允许直接将 void * 隐式转换到其他类型。因此下列代码不能在C++中完成编译。

#define NULL ((void*)0)

char *ch = NULL;

​ 没有了 void * 隐式转换的 C++ 只好将 NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:

#define NULL (0)

void foo(char*){};
void foo(int){};

int main()
{
    foo(NULL); // 因此,这个语句将会去调用 foo(int)
}

​ 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。

​ 为了解决这个问题(1. 无法直接将 void * 隐式转换到其他类型;2. 重载特性发生混乱),C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

1.5.1.2 使用constexpr修饰常量表达式(C++11)

常量表达式指编译期间能够确定结果的表达式、表达式(函数)的相同输入总会产生相同的输出,没有任何副作用(不会修改其他外部变量)。

编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。

constexpr是一个加强版的const,它不仅要求常量表达式是常量,并且要求是一个编译阶段就能够确定其值的常量。

​ C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。 C++ 标准中数组的长度必须是一个常量表达式,而对于一个 const 常数作为数组长度的行为,(即便这种行为在大部分编译器中都支持,但是)是一个非法的行为。

int len = 10;
const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;
// char arr_4[len_2];                // 非法
char arr_4[len_2_constexpr];         // 合法

注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下会变得合法,若需重现编译报错的现象需要使用老版本的编译器。

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 被修饰的表达式(函数)在编译期就应该是一个常量表达式。

此外,constexpr 修饰的函数可以使用递归:

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:

不过在定义常量表示函数的时候,我们会遇到更多的约束规则[C++14]:

  1. 函数体允许声明变量,除了没有初始化、static和thread_local变量。
  2. 函数允许出现if和switch语句,不能使用goto语句。
  3. 函数允许所有的循环语句,包括for、while、do-while。
  4. 函数可以修改生命周期和常量表达式相同的对象(函数外部对象不能修改)。
  5. 函数的返回值可以声明为void。
  6. constexpr声明的成员函数不再具有const属性

另外还有些不允许的:

  1. 它必须非虚; [c++20前]
  2. 它的函数体不能是函数 try 块; [c++20前]
  3. 它不能是协程; [c++20起]
  4. 对于构造函数与析构函数 [C++20 起],该类必须无虚基类
  5. 它的返回类型(如果存在)和每个参数都必须是字面类型 (LiteralType)
  6. 至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函 数为足以用于常量初始化器) (C++14 起)。不要求诊断是否违反这点。
constexpr int fibonacci(const int n) {
    if(n == 1) return 1;
    if(n == 2) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

​ 为此,我们可以写出下面这类简化的版本来使得函数从 C++11 开始即可用:

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}

​ 在C++17标准中,constexpr声明静态成员变量时,也被赋予了该变量的内联属性。

class X 
{
    public:
    static constexpr int num{ 5 };
};

以上代码从C++17开始等价于:

class X 
{
    public:
    // X::num既是声明又是定义(无需(也不能)在外部再定义constexpr int X::num=5)
    inline static constexpr int num{ 5 }; 
};

1.5.2 变量及其初始化

1.5.2.1 if/switch变量声明强化(C++17)

C++17 之后可以在if/switch语句中声明局部变量,使得我们可以在 if(或 switch)中完成很多操作。

语法结构:

if (<变量声明>; <条件语句>)
{
    // 语句块
}

例子:

// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
    itr != vec.end()) {
    *itr = 4;
}
1.5.2.2 初始化列表(C++11)

​ 在C++11以前(传统 C++ ),不同的对象有着不同的初始化方法,例如普通数组、POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同方法都针对各自对象,不能通用。

为解决这个问题(需要使用不同方式去初始化对象,而没有统一的办法),C++11 首先把初始化列表的概念绑定到类型上,称其为 std::initializer_list(除了用在对象的构造之外,还能用于函数的形参),允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁。

#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo {
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
        for (std::initializer_list<int>::iterator it = list.begin();
             it != list.end(); ++it)
            vec.push_back(*it);
    }
};
int main() {
    // after C++11
    MagicFoo magicFoo = {1, 2, 3, 4, 5};

    std::cout << "magicFoo: ";
    for (std::vector<int>::iterator it = magicFoo.vec.begin(); 
        it != magicFoo.vec.end(); ++it) 
        std::cout << *it << std::endl;
}

C++11 还提供了统一的语法来初始化任意的对象(即:使用{}进行初始化)

#include <iostream>
#include <vector>

class Foo {
public:
    int value_a;
    int value_b;
    Foo(int a, int b) : value_a(a), value_b(b) {}
};

int main() {
    // before C++11
    Foo foo(1, 2);
	// C++11
    Foo foo2{11, 22}; // 可以用其初始化任意对象
    return 0;
}
1.5.2.3 结构化绑定(C++17)

​ **结构化绑定提供了类似其他语言中提供的多返回值的功能。**在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。

C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:

#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> f() {
    return std::make_tuple(1, 2.3, "456");
}

int main() {
    auto [x, y, z] = f();
    std::cout << x << ", " << y << ", " << z << std::endl;
    return 0;
}

1.5.3 类型推导(C++11)

​ 在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。

C++11 引入了 autodecltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。

1.5.3.1 auto

auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变更也就非常自然了。

关键字auto的变化历史

  • 传统C++和C:表示对象(变量)的存储类型。
  • C++11:更改了auto关键字的意义,能自动推导对象类型和返回值类型(需要结合-> decltype(表达式)构成尾返回类型)。
  • C++14:auto可以对函数的返回值类型直接进行推导(不在需要-> decltype(表达式)的尾返回类型),并且结合decltype(auto) 用于对转发函数或封装的返回类型进行推导。
  • C++20:在函数的形参中使用,用于模版的简化写法。

注意:auto还不能推导数组类型。

auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。

#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo {
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
        // 从 C++11 起, 使用 auto 关键字进行类型推导
        for (auto it = list.begin(); it != list.end(); ++it) {
            vec.push_back(*it);
        }
    }
};
int main() {
    MagicFoo magicFoo = {1, 2, 3, 4, 5};// C++11 初始化列表
    std::cout << "magicFoo: ";
    // C++11
    for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
        std::cout << *it << ", ";
    }
    // before C++11
    for (std::vector<int>::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
        std::cout << *it << ", ";
    }
    std::cout << std::endl;
    return 0;
}

从 C++ 20 起,auto 甚至能用于函数传参,考虑下面的例子:

int add(auto x, auto y) {
    return x+y;
}

auto i = 5; // 被推导为 int
auto j = 6; // 被推导为 int
std::cout << add(i, j) << std::endl;
1.5.3.2 decltype(C++11)

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的(decltype能对表达式进行类型推导)。它的用法和 typeof 很相似:


auto x = 1;
auto y = 2;
decltype(x+y) z; //  decltype用于推断类型的用法

// 判断上面的变量 `x, y, z` 是否是同一类型
if (std::is_same<decltype(x), int>::value)
    std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
    std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)
    std::cout << "type z == type x" << std::endl;
1.5.3.3 尾返回类型推导(C++11)

在传统 C++ 中我们使用模板函数时,对于函数的返回值可能会这样写:

template<typename R, typename T, typename U>
R add(T x, U y) {
    return x+y;
}

注意:typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename 消除歧义

​ 但是这样的代码不仅丑陋,还必须要求我们在使用这个模版函数时必须指明函数的返回值类型,但实际上我们在大部分时候仅凭函数名并不清楚这个函数到底会做什么样的操作,因此大部分时候我们并不清楚返回值的类型到底是什么。

在 C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:

decltype(x+y) add(T x, U y)

但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,xy 尚未被定义。

C++11 引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
    return x + y;
}

令人欣慰的是 从C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:

template<typename T, typename U>
auto add3(T x, U y){
    return x + y;
}
1.5.3.4 decltype(auto)(C++14)

要理解它你需要知道 C++ 中参数转发的概念。

简单来说,decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:

std::string  lookup1();
std::string& lookup2();

在 C++11 中,封装实现是如下形式:

std::string look_up_a_string_1() {
    return lookup1();
}
std::string& look_up_a_string_2() {
    return lookup2();
}

而有了 decltype(auto),我们可以让编译器完成这一件烦人的参数转发:

decltype(auto) look_up_a_string_1() {
    return lookup1();
}
decltype(auto) look_up_a_string_2() {
    return lookup2();
}

1.5.4 控制流

1.5.4.1 if constexpr(C++17)

注意,不能在函数体外部使用if constexpr。因此,不能使用它来替换条件预处理器指令(但在函数中可以替换#ifdef)。

  1. if constexpr的条件必须是编译期能确定结果的常量表达式。
  2. 条件结果一旦确定,编译器将只编译符合条件的代码块。

我们知道了 C++11 引入了 constexpr 关键字,它将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件。

#include <iostream>

template<typename T>
auto print_type_info(const T& t) {
    if constexpr (std::is_integral<T>::value) {
        return t + 1;
    } else {
        return t + 0.001;
    }
}
int main() {
    std::cout << print_type_info(5) << std::endl;
    std::cout << print_type_info(3.14) << std::endl;
}

在编译时,实际代码就会表现为如下(编译器将只编译符合条件的代码块):

int print_type_info(const int& t) {
    return t + 1;
}
double print_type_info(const double& t) {
    return t + 0.001;
}
int main() {
    std::cout << print_type_info(5) << std::endl;
    std::cout << print_type_info(3.14) << std::endl;
}
1.5.4.2 区间 for 迭代(C++11)

C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 2, 3, 4};
    if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;
    for (auto element : vec)
        std::cout << element << std::endl; // 只读
    for (auto &element : vec) {
        element += 1;                      // 读写
    }
    for (const auto &element : vec)
        std::cout << element << std::endl; // 只读
}

1.5.5 模版

​ C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。==模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。==因此模板也被很多人视作 C++ 独有的黑魔法之一。

1.5.5.1 外部模板(C++11)4

​ “外部模板”是C++11中一个关于模板性能上的改进。实际上,“外部”(extern)这个概念早在C的时候已经就有了。通常情况下,我们在一个文件中a.c中定义了一个变量int i,而在另外一个文件b.c中想使用它,这个时候我们就会在没有定义变量ib.c文件中做一个外部变量的声明。比如:

extern int i;

这样做的好处是,在分别编译了a.cb.c之后,其生成的目标文件a.ob.o中只有i这个符号的一份定义。

​ 而对于函数模板来说,现在我们遇到的几乎是一模一样的问题。不同的是,发生问题的不是变量(数据),而是函数(代码)。这样的困境是由于模板的实例化带来的。

外部模板作用:对编译器的编译时间的优化,减少冗余的代码,减少开销。

比如,我们在一个test.h的文件中声明了如下一个模板函数:

template <typedef T> void fun(T){}

在第一个testl.cpp文件中,我们定义了以下代码:

#include "test.h"
void test1(){ fun(3); }

而在另一个test2.cpp文件中,我们定义了以下代码:

#include "test.h"
void test1(){ fun(4); }

由于两个源代码使用的模板函数的参数类型一致,所以在编译testl.cpp的时候,编译器实例化出了函数fun< int>(int),而当编译test2.cpp的时候,编译器又再一次实例化出了函数fun< int>(int)。那么可以想象,在testl.o目标文件和test2.o目标文件中,会有两份一模一样的函数fun< int>(int)代码。代码重复和数据重复不同。数据重复,编译器往往无法分辨是否是要共享的数据;而代码重复,为了节省空间,保留其中之一就可以了(只要代码完全相同)。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码fun< int>(int)删除掉,只保留了单个副本。这样一来,就解决了模板实例化时产生的代码冗余问题。

​ **对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。**很明显,这样的工作太过多余,而在广泛使用模板的项目中,由于编译器会产生大量余代码,会极大地增加编译器的编译时间和链接时间解决这个问题(重复实例化导致编译器产生的额外时间开销)的方法基本跟变量共享的思路是一样的,就是使用“外部的”模板。

​ 在实际上,C++11中“模板的显式实例化定义、外部模板声明和使用”好比“全局变量的定义、外部变量使用”方式的再次应用。不过相比于外部变量声明,不使用外部模板声明并不会导致任何问题外部模板定义更应该算作一种针对编译器的编译时间及空间的优化手段。很多时候,由于程序员低估了模板实例化展开的开销,因此大量的模板使用会在代码中产生大量的余。这种余,有的时候已经使得编译器和链接器力不从心。但这并不意味着我们需要为四五十行的代码写很多显式模板声明及外部模板声明。只有在项目比较大的情况下才建议进行外部模版的优化(编译器的编译时间及空间的优化手段)

​ 传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化(体现在编译器额外的代码块的生成,链接器移除重复实例化代码的时间)。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知(传统C++)编译器不要触发模板的实例化。

​ 为此,C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化,比如(实例),我们在一个test.h的文件中声明了如下一个模板函数:

template <typedef T> void fun(T){}

首先,在test1.cpp做显式地实例化:

#include "test.h"
template void fun<int>(int); // 强行实例化(显式实例化)
void test1(){ fun(3); }

接下来,在test2.cpp中做外部模板的声明:

#include "test.h"
extern template void fun<int>(int); // 不在该当前编译文件中实例化模板
void test1(){ fun(3); }

语法:

template class std::vector<bool>;          // 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板
1.5.5.2 合法的连续尖括号 “>”(C++11)

在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:

std::vector<std::vector<int>> matrix;

​ 这在传统 C++ 编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。甚至于像下面这种写法都能够通过编译:

template<bool T>
class MagicType {
    bool magic = T;
};

// in main function:
std::vector<MagicType<(1>2)>> magic; // 合法, 但不建议写出这样的代码
1.5.5.3 类型别名模板(C++11)

​ 在了解类型别名模板之前,需要理解『模板』和『类型』之间的不同。仔细体会这句话:模板是用来产生类型的。在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型例如:

template<typename T, typename U>
class MagicType {
public:
    T dark;
    U magic;
};

template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic; // before C++11 不合法

C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:

通常我们使用 typedef 定义别名的语法是:typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。

typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;

int main() {
    TrueDarkMagic<bool> you;
}

别名模板的作用主要是简化书写和加强可读性。

#include <map>

template<typename T>
using str_map_t = std::map<std::string, T>;	// 别名模板

template<typename T>
class A
{
    template<typename T>
	using str_map_t = std::map<std::string, T>;	// 成员别名模板
public:
    str_map_t<int> map1;
};

int main()
{
    str_map_t<int> map1;
    map1.insert({'one', 1});
    map1.insert({'two', 2});
    A<float> obja;
    obja.map1.insert({'one', 1});
}
1.5.5.4 函数模板的默认类型参数(C++11)

​ 在C++中,我们可以为模板参数提供默认值,从而使得在使用模板时,如果不显式地传递模板参数,那么就会使用默认值。默认值的语法如下:

template <typename T = int>

​ 函数模板在C++98中与类模板一起被引入,不过类模板支持默认模板参数,而函数模板不支持默认模板参数,不过在c++11中已经解决该限制,如下所示:

void DefParm(int m = 3) {};						 // C++98 编译通过, C++11 编译通过
template<typename T = int> class DefClass {}; 	 // C++98 编译通过, C++11 编译通过
template<typename T = int> void DefTempParm() {} // C++98 编译失败, C++11 编译通过

​ ==在为多个默认模板参数声明指默认值的时候,必须遵照“从右往左”的规则进行指定(函数模板除外)。==其实这和这和函数参数默认值的指定规则一致(我说的是“从右往左”的规则),如下所示:

template<typename T1, typename T2 = int> class DefClass1;
template<typename T1 = int, typename T2> class DefClass2; // 无法通过编译

template<typename T, int i = 0> class DefClass3;
template<int i = 0, typename T> class DefClass4; // 无法通过编译

template<typename T1 = int, typename T2> void DefFun1(T1 a, T2 b);
template<int i = 0, typename T> void DefFun2(T a);
1.5.5.5 变长参数模板(C++11)

​ 在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定

  • 形参包
template<typename... Ts> class Magic;

模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:

class Magic<int,
            std::vector<int>,
            std::map<std::string,
            std::vector<int>>> darkMagic;

既然是任意形式,所以个数为 0 的模板参数也是可以的:class Magic<> nothing;。如果不希望产生的模板参数个数为 0,可以手动的定义至少一个模板参数:

template<typename Require, typename... Args> class Magic;

​ 变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf 函数,虽然也能达成不定个数的形参的调用,但其并非类别安全。而 C++11 除了能定义类别安全的变长参数函数外,还可以使类似 printf 的函数能自然地处理非自带类别的对象。
​ 除了在模板参数中能使用 ... 表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数,这也就为我们简单编写变长参数函数提供了便捷的手段,例如:

template<typename... Args> void printf(const std::string &str, Args... args);
  • 函数模板:引用传参、指针传参、值传递5
// 函数形参的值接收
void f() {} // 特例化版本
template<class Ts,class... Us> void f(Ts value, Us... pargs) // 接收值
{
	f(pargs...);		// 值传递
}

template<class... Ts> void g(Ts... args)
{
    f(args...);  // 传递数据值
	f(&args...); // 传递数据指针
    f(const_cast<const Args*>(&args)...);  // 传递常量指针
    f(*const_cast<const Args*>(&args)...); // 传递常量实参原型 (因模板函数形参的不同而不同)
    f(h(args...) + args...); // h() 的返回值与 args 所有元素加和的结果,作为新的参数
    
    f(++args..., n);         // 展开成 f(++E1, ++E2, ++E3, n);
    f(n, ++args...);         // 展开成 f(n, ++E1, ++E2, ++E3);
}

// 函数模板形参的引用接收
void fun_ref() {}
template<class Ts, class... Us> void fun_ref(Ts& value, Us&... pargs) // 引用接收
{
	fun_ref(pargs...);		// 递归调用
}

因模板函数形参的不同而不同的意思是:

  • 调用:f(*const_cast<const Args*>(&args)...); // 以常量实参原型调用

    • 如果 void f( Ts value, Us ... pargs),则我们在函数体内修改 value = 10,将不会引发报错。
      (因为行参传递过程,是另外开辟的一片空间,其f()的参数列表未规定其const属性)
    • 如果 void f(const Ts value, const Us ... pargs),则我们在函数体内修改 value = 10,会引发报错。
      (因为函数形参列表限定参数为常量,不可修改)
    • 如果 void f(Ts& value, Us& ... pargs),则我们在函数体内修改 value = 10,会引发报错。
      (因为是以引用传递,而原实参是const的,因此在函数内引用的参数也是cosnt的不允许修改)
    • 其他:例如常量左值引用const Type&、非常量右值引用Type&&、常量右值引用const Type&& 这里不再列举
  • 调用:f(const_cast<const Args*>(&args)...); // 以常量实参指针调用

    注:f() 接收到的参数是指针

    • 如果 void f(Ts value, Us ... pargs),则 value += 1; 成功, *value = 10;失败
      (因为,const_cast<const Args*>() 将 type 强转为 const type *,因此指针值不可变。而并没有限制指针的指向,因此value += 1成功 )
    • 如果 void f(Ts const value, Us const ... pargs),则 value += 1; 失败, *value = 10;失败
      (因为,原实参是 const type *, f() 函数中限定了参数类型为 type * const,最终参数被叠加为 const type*const。 既不可改变指向,又不可改变指向的值)
    • 其他:同理这里可以传入引用或多级指针测试。

具体内容可以参考原文连接5

  • 模板函数的形参指针限定
template<typename T>
void f(const T p){};
void f(T const p){};
  • 如果我们在模板类型前加const
    • f( const T p ) 注意这里const 修饰的是指针 p 本身 。
    • 因此将 char* 参数 与模板参数 T 解析后的函数为 f( const (char*) p ) ,这里 const 仍然修饰的是 p 本身,而不是 p 的所指之物。
    • 因此,最终 p 的类型是 char* const p。限定指向。
  • 如果我们在模板类型后加const
    • f( T const p ) ==》char* ==》f( char* const p )
    • 很明显,这里的 p 仍然是被限定了指向。

综上,使用 T 方式传参(以指针的形式),如果不想原数据被改变,可以使用 const_cast<const type*>(&arg) 进行强制类型转换,或者使用引用的方式。

  • 包展开

推荐使用逗号表达式+初始化列表的方式(下方4.2版本)进行包的展开

首先,我们可以使用 sizeof... 来计算参数的个数,:

template<typename... Ts>
void magic(Ts... args) {
    std::cout << sizeof...(args) << std::endl;
}

我们可以传递任意个参数给 magic 函数:

magic(); // 输出0
magic(1); // 输出1
magic(1, ""); // 输出2

其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:

1. 递归模板函数

递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的:

#include <iostream>
template<typename T0>
void printf1(T0 value) {
    std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
    std::cout << value << std::endl;
    printf1(args...);
}
int main() {
    printf1(1, 2, "123", 1.1);
    return 0;
}

2. 变参模板展开

你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完成 printf 的编写:

// 例1 (使用递归:终止条件和执行条件为下方判断语句)
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
    std::cout << t0 << std::endl;
    if constexpr (sizeof...(t) > 0) printf2(t...);
}

// 例2 (使用折叠表达式)
template<typename ...Args>
void print(Args && ...args)
{
    (std::cout << ... << args) << "\n";
}

事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用 std::bind 及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。

3. 初始化列表展开

递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。这里介绍一种使用初始化列表展开的黑魔法:

// 例1 (使用了初始化列表、逗号表达式和lamabda表达式)
template <typename T, typename... Ts>
auto printf3(T value, Ts... args)
{
    std::cout << value << std::endl;
    (void)std::initializer_list<T>{([&args]
                                    { std::cout << args << std::endl; }(),
                                    value)...};
}

// 例2 (使用了初始化列表、逗号表达式和lamabda表达式)
void show_list(T&& value) {
    cout << value << endl;
}
template <typename F,typename ...Args>
void show_listl(F&& f,Args&& ...args){
    initializer_list<int>({(f(args),0)...});
}

int main(int argc,char **argv[])
{
	show_listl([](auto t){cout << t << endl;},1,123,"qqq");
	return 0;
}

在例1中,额外使用了 C++11 中提供的初始化列表以及 Lambda 表达式的特性。通过初始化列表,(lambda 表达式, value)... 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。为了避免编译器警告,我们可以将 std::initializer_list 显式的转为 void

4. 逗号表达式展开

// 版本1 (使用递归函数和逗号表达式)
template <typename T>
void show_list(T&& value) {
    cout << value << endl;
}
template <typename ...Args>
void show_listl(Args&& ...args){
    int arr[] = {(show_list(args),0)...};
}

// 版本2 (使用初始化列表和逗号表达式)
template <class ...Args>
void FormatPrint(Args... args)
{
   (void)std::initializer_list<int>{ (std::cout << "[" << args << "]", 0)... };
   std::cout << std::endl;
}

版本1的展开式为 {(show_list(arg0),0)…},{(show_list(arg0),0),(show_list(args1),0)…},{(show_list(arg0),0),(show_list(args1),0),(show_list(args2),0)…}…,{(show_list(arg0),0),(show_list(args1),0),(show_list(args2),0),…,(show_list(argsn),0)}.最终会创建一个元素全部为0的数组。

版本2也用到了C++11的特性初始化列表以及很传统的逗号表达式,我们知道逗号表达式的优先级最低,(a, b) 这个表达式的值就是 b,那么上述代码中(std::cout << "[" << args << "]", 0)这个表达式的值就是0,初始化列表保证其中的内容从左往右执行,args参数包会被逐步展开,表达式前的(void)是为了防止变量未使用的警告,运行过后我们就得到了一个N个元素为0的初始化列表。


1.5.5.6 折叠表达式(C++ 17)

​ C++ 17 中将变长参数这种特性进一步带给了表达式,使用折叠表达式可以简化对C++11中引入的参数包的处理,从而在某些情况下避免使用递归。

只要条件允许,我们都应该使用折叠表达式去处理一个参数包而不是使用递归:

  1. 代码更少。
  2. 运行更快(在没有优化的情况下),因为你只需要一条表达式而不是多次函数调用。
  3. 编译更快,因为模板实例化更少。

缺点就是代码通常会比较难读,需要额外的注释去解释原理。

  • 支持的操作符

折叠表达式支持 32 个操作符:

+, -, *,/, %, ^, &, |, =, <,>, <<, >>,+=, -=, *=, /=, %=, ^=, &=, |=, <<=,>>=,==, !=, <=, >=, &&, ||, ,, .*, ->*

  • 语法形式
  1. 一元右折叠( pack op ... )
      一元右折叠(E op ...)展开之后变为 E1 op (... op (EN-1 op EN))

  2. 一元左折叠( ... op pack )
      一元左折叠(... op E)展开之后变为 ((E1 op E2) op ...) op EN

  3. 二元右折叠( pack op ... op init )
      二元右折叠(E op ... op I)展开之后变为 E1 op (... op (EN−1 op (EN op I)))

  4. 二元左折叠( init op ... op pack )

    ​ 二元左折叠(I op ... op E)展开之后变为 (((I op E1) op E2) op ...) op EN

左折叠表达式和右折叠表达式之间的差异比预期的更为重要。即使使用运算符+,可能也会有不同的效果:

// 左折叠
// 折叠表达式的首选语法
template<typename... T>
auto foldSumL(T... args)
{
    return (... + args); // ((arg1 + arg2) + arg3) ...
}
std::cout << foldSumL(std::string("hello"), "world", "!") << '\n'; // OK
std::cout << foldSumL("hello", "world", std::string("!")) << '\n'; // ERROR
// 右折叠
template<typename... T>
auto foldSumR(T... args)
{
    return (args + ...); // (arg1 + (arg2 + arg3)) ...
}

std::cout << foldSumR(std::string("hello"), "world", "!") << '\n'; // ERROR
std::cout << foldSumR("hello", "world", std::string("!")) << '\n'; // OK

几乎所有的情况下,都是从左到右求值,通常,应该首选带有参数包的左折叠语法(除非这不起作用)。

对每个元素调用一次函数6

/* 1.正序对每个元素调用一次函数 */
(f(ts), ...);
// expands to: f(ts[0]), f(ts[1]), f(ts[2]), ...

/* 2.逆序对每个元素调用一次函数 */
int dummy;
(dummy = ... = (f(ts), 0));
// expands to: dummy = ((f(ts[0]), 0) = (f(ts[1]), 0)) = ...

​ 为了逆序调用函数,我们需要一个对参数从右到左求值的运算符。其中一个具有这样特性的运算符就是=a = b = c,首先对c求值,然后是b,最后是a。所以我们使用逗号运算符将函数调用后的结果转换成某个int值,然后折叠成对一个无用变量的赋值。最终我们将得到一个超大的赋值表达式,其中每个操作数都首先调用函数然后再返回0,这些操作数将逆序求值。

​ 这个技巧实际上水很深。如果你只是写dummy = 0 = 0,虽然这基本上和我们得到的结果差不多,但是它编译不过:=具有右结合性,所以上述表达式等价于dummy = (0 = 0),然而你不能对0赋值。不过,在我们的这个技巧中我们使用的是左折叠,等价于(dummy = 0) = 0,也就是对dummy赋值两次。最后我们得到的是一个从右到左求值的具有左结合性的表达式!

更多的折叠表达式的用法可以翻阅这个文章:【翻译】折叠表达式实用技巧 - 知乎 (zhihu.com)

1.5.7 非类型模板参数推导(C++17)

非类型模板参数的类型是有一定限制的,只能是整型常量包括枚举,而浮点数、类对象以及字符串是不允许作为非类型模板参数的;非类型的模板参数必须在编译期就能确认结果,即非类型模板参数的实参只能是一个常量

前面我们主要提及的是模板参数的一种形式:类型模板参数。

template <typename T, typename U>
auto add(T t, U u) {
    return t+u;
}

其中模板的参数 TU 为具体的类型。但还有一种常见模板参数形式可以让不同字面量成为模板参数,即非类型模板参数:

template <typename T, int BufSize>
class buffer_t {
public:
    T& alloc();
    void free(T& item);
private:
    T data[BufSize];
}

buffer_t<int, 100> buf; // 100 作为模板参数

​ 在这种模板参数形式下,我们可以将 100 作为模板的参数进行传递。在 C++11 引入了类型推导这一特性后,我们会很自然的问,既然此处的模板参数以具体的字面量进行传递,能否让编译器辅助我们进行类型推导,通过使用占位符 auto 从而不再需要明确指明类型?幸运的是,C++17 引入了这一特性,我们的确可以 auto 关键字,让编译器辅助完成具体类型的推导,例如:

template <auto value> void foo() {
    std::cout << value << std::endl;
    return;
}

int main() {
    foo<10>();  // value 被推导为 int 类型
}

1.5.6 面向对象

二、类和对象

分类

[c++] 什么是平凡类型,标准布局类型,POD类型,聚合体_c++平凡-CSDN博客

精髓文章:c++ 聚合/POD/平凡/标准布局 介绍 - shadow_lr - 博客园 (cnblogs.com)

在 C++ 中,有一些类型具有特殊的性质,包括平凡类型、标准布局类型、POD 类型和聚合体。

  1. 平凡类型(Trivial Type) 平凡类型是指满足以下条件的类型:

  2. 标准布局类型(Standard Layout Type) 标准布局类型是指满足以下条件的类型:

  3. POD 类型(Plain Old Data Type) POD 是一种 C++98 中引入的概念,是指满足以下条件的类型:

  4. 聚合体(Aggregate) 聚合体是指一个类满足以下条件:

    • 所有非静态数据成员都是公共的。

    可以看出,聚合体是一种比 POD 类型和平凡类型更为宽泛的概念,即聚合体可以具有公共的数据成员,但不一定需要满足其他条件。

2.1 构造函数

所有的构造函数的分析情况均只考虑C++11及以后

​ 在 C++ 中,构造函数是一种特殊的成员函数,用于创建和初始化对象。构造函数的名称与类名相同,没有返回值类型,可以有参数。

​ 当创建一个对象时,编译器会自动调用该类的构造函数来初始化对象。如果类没有定义构造函数,则编译器会生成一个默认的构造函数,在创建对象时调用该构造函数。如果类定义了一个或多个构造函数,则在创建对象时必须选择一个符合要求的构造函数进行调用。

​ 构造函数可以执行任意操作,例如分配内存、初始化数据成员等。构造函数可以被重载,即可以定义多个不同参数列表的构造函数来满足不同的需求。

需要注意的是,在 C++ 中,构造函数不能被直接调用,它只能在对象创建时自动调用。

C++ 20标准下,有哪几种构造函数类型? - 知乎 (zhihu.com)

编译器会自动生成的构造函数

  1. 无参构造
  2. 拷贝构造
  3. 赋值构造

构造函数的分类

构造函数初始化器

​ 在构造函数参数列表与构造函数体之间使用冒号开始,逗号分隔的方式初始化数据成员。这种方式就是构造函数初始化器。

class A
{
public:
    // 使用初始化器初始化数据成员
    A(int a, double b) : a(a), b(b) {}

private:
    int a;
    double b;
};

成员初始化的顺序

​ 数据成员的初始化顺序是按照它们在类定义中出现的先后顺序来决定的。而不是在构造函数初始化器中的顺序。

构造函数的关键字限定

noexcept constexpr explicit

constexpr是C++11引入的关键字,用于指示表达式或函数可以在编译时求值为常量。当一个函数被声明为constexpr时,它被限制为只能执行可在编译时计算出结果的操作。

如果一个构造函数被声明为constexpr,那么它必须满足以下要求:

  1. 构造函数的函数体必须为空,或者只包含返回语句。
  2. 在构造函数的初始化列表中,只能使用其他constexpr函数或常量表达式初始化成员变量。
  3. 构造函数必须能够在编译时计算出结果。

当一个构造函数满足以上要求时,它被视为constexpr构造函数。这意味着该构造函数可以在编译期间被调用,并且其结果可以用于常量表达式的求值。

如果生成的移动构造函数满足constexpr构造函数的要求,那么它也将被视为constexpr构造函数。这意味着该移动构造函数可以在编译时进行常量表达式的求值,例如在模板元编程(template metaprogramming)中使用。

使用constexpr构造函数可以在编译时进行更多优化和静态分析,提高程序的性能和效率。但需要注意,constexpr构造函数的使用场景和限制条件是有一定的限制的,需要根据具体情况进行判断和使用。

2.1.1 无参构造

​ 它不具有任何参数,也称默认构造函数,如果类中没有任何构造函数,那么编译器会自动生成无参构造函数。类中所有的对象成员都可以调用编译器默认生成的构造函数,但是不会初始化语言的基本类型,例如intdouble

默认构造函数这个名词,有两种含义:

  • 在对象被创建时,默认调用的构造函数:默认构造函数指不带参数或者所有参数都有缺省值的构造函数
  • 编译器生成的无参构造函数(大多数时候是这个意思)。
编译器的无参构造

以下情况编译器均不会生成默认构造函数

  1. 类显式定义了任何构造函数时,编译器都不会自动生成默认构造函数。

    ​ 这意味着如果你在类中定义了一个构造函数,但没有定义默认构造函数,那么在创建对象时如果没有提供参数,编译器将无法生成默认构造函数,导致编译错误。但如下例外,这并不能说明没有默认构造使用 Foo a; 的语法会出错:

    class Foo 
    {
    public:
        Foo(int a = 10, double b = 0.0) :val(a), f(b) {} // 具有默认值的一般构造(多参数时类似)
        int val;
        double f;
    };
    

    对该类调用 Foo a; 将调用到 Foo(int, double) 的一般构造函数,因为它的参数都具有默认值。但也意味着将无法再定义无参构造。

  2. 默认构造函数是类的公有成员,**当无参构造被标记为保护成员、私有成员或明确弃用无参构造时 classname() = delete; **均不会创建默认构造函数。

    ​ 删除的构造函数是通过使用 = delete 关键字来标记的,它们被明确禁用,不能被调用。私有的构造函数只能在类内部访问,而受保护的构造函数只能在类及其派生类内部访问。

  3. 当类继承了没有默认构造函数的基类时,编译器不会自动生成默认构造函数。

    这是因为在构造派生类对象时,必须先构造基类对象,而如果基类没有默认构造函数,编译器无法生成默认构造函数。

编译器生成的默认构造

​ 这篇文章很精髓:C++ Default Constructor什么时候才会被编译器生成出来呢?

​ 隐式声明的默认构造函数(编译器自动生成的默认构造函数)有两种:一种是trivial constructor,什么都不做;另一种是nontrival constructor,编译器合成的是后者。[C++ - nontrivial default constructor - 博客园 (cnblogs.com)

  • 无用构造 trivial constructor:编译器为语法正确而生成空函数体的构造。函数体为空,非类成员的值并不确定。

      class Foo 
      {
      public:
          Foo(){} // 编译器生成的默认构造 的形式
          int val; 
      }; 
    
  • 有用构造 nontrival constructor:编译器为确保对象正确初始化合成的无参构造。

    在以下4种情况编译器会**自动合成**默认构造:(拷贝构造也有类似规则)

    • 带有默认构造(这个构造函数可以是程序员自己写的无参构造,也可以是具有默认值的有参构造,以此类推)的成员对象:

      示例链接

      class Foo 
      {
      public:
       Foo() { cout << "Foo" << endl; } // 自己写的无参构造
       int val; 
      }; 
      class Bar
      { 
      public: 
       Foo a; // 成员对象
       char *str; 
       int i; 
      }; 
      

      示例链接:https://compiler-explorer.com/z/bfM3xa5x3

      class Foo 
      {
      public:
       Foo(int a = 10) :val(a) { cout << "Foo" << endl; } // 具有默认值的一般构造(多参数时类似)
       int val; 
      }; 
      class Bar
      { 
      public: 
       Foo a; // 成员对象
       char *str; 
       int i; 
      }; 
      
    • 继承 自 带有默认构造的类:https://compiler-explorer.com/z/n1r8rGdx4

      class Foo 
      {
      public:
          Foo()
          {
              cout << "Foo" << endl;
              val = 56;
          }
          int val; 
      }; 
      class Bar:public Foo // 继承自 带有默认构造的函数
      { 
      public: 
          char *str; 
          int i; 
      }; 
      
    • 基类或该类中含有虚函数

    • 这个类虚继承于其它类

2.1.2 一般构造

​ 一般构造函数(通常称为普通构造函数或非默认构造函数)是一个带有参数的构造函数,用于创建对象时进行特定的初始化操作。与默认构造函数不同,一般构造函数需要提供参数来进行初始化。它可以被重载,允许使用不同的参数组合来创建对象。

class Foo 
{
public:
    Foo(int a = 10) :val(a), f(0.0f) { cout << "Foo" << endl; } // 函数默认值
    Foo(int a, double b) : val(a), f(b) {};	// 函数重载
    int val;
    double f;
};

在C++11后可以使用初始化列表类型作为构造函数参数:

#include <iostream>
#include <vector>
#include <initializer_list>

class MyClass {
public:
    MyClass(std::initializer_list<int> nums) : numbers(nums) { } // 使用了初始化列表类

    void printNumbers() const {
        for (auto num : numbers) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }

private:
    std::vector<int> numbers;
};

int main() {
    MyClass obj({1, 2, 3, 4, 5});
    obj.printNumbers();
    return 0;
}

2.1.3 拷贝构造

​ 拷贝构造函数(复制构造函数)是一种特殊的构造函数,用于创建一个新对象并将其初始化为现有对象的副本。拷贝构造函数接受一个同类型的对象作为参数,并使用该对象的值来初始化新对象。

拷贝构造函数具有以下特点:

  1. 它的参数是一个同类型的对象的引用(通常使用const引用)
  2. 它用于创建一个新对象,并将其初始化为参数对象的副本。
  3. 可以执行深拷贝或浅拷贝,具体取决于类的实现
  4. 如果没有显式定义拷贝构造函数,编译器会自动生成默认的拷贝构造函数,前提是你没有定义移动构造和析构函数
  5. 其首个形参为 T&const T&volatile T&const volatile T& (CV限定符的4种组合),而且要么没有其他形参,要么剩余形参均有默认值。
class Person 
{
private:
    std::string name;
    int age;

public:
    Person() = default;
    Person(const std::string& n, int a) : name(n), age(a) {} // 一般构造

    // 拷贝构造函数
    Person(const Person& other) : name(other.name), age(other.age) {
         puts("拷贝构造"); 
    }
    ~Person(){
		puts("析构");
    }
};

何时调用
什么时候会调用拷贝构造函数?
什么场景调用拷贝构造关闭优化C++标准存在移动构造备注(均是存在拷贝构造)
使用具名对象初始化另一个对象时无关无关无关任何时候必定调用
具名对象值传递方式传递给函数无关无关无关任何时候必定调用
函数中返回 静态对象 或者 全局对象无关无关无关任何时候必定调用
使用匿名对象初始化另一个对象[C++11, C++17)三个条件缺一不可
匿名对象值传递的方式传递给函数[C++11, C++17)三个条件缺一不可
函数中返回 匿名对象[C++11, C++17)三个条件缺一不可
函数中返回 具名对象[C++11, 至今]关闭优化、且不存在移动构造时

​ 在后面四种情况时,存在移动构造时将会调用移动构造(当然前提是:另外两个条件缺一不可,需要满足关闭优化和对应标准);在正确情况下(默认是开启优化),它们即不会调用拷贝构造,也不会调用移动构造,code

全部情况的示例链接

必定情况与C++标准无关、与是否开启优化无关、与是否存在移动构造无关示例链接

  1. 使用具名对象初始化另一个对象时:

       Person person1("Alice", 25); // 一般构造
       
       Person person2 = person1;	// 拷贝构造
       Person person3{person1};	// 拷贝构造
    
  2. 具名对象值传递方式传递给函数:

       void func1(Person obj) {}
       
       func1(person1); // 具名对象传值
    
  3. 在函数中返回静态对象或者全局对象

       Person func4() {
           static auto a = Person{"a", 25};
           return a;
       }
    

一定条件下调用拷贝构造(不存在移动构造、禁用拷贝优化)

  1. 使用匿名对象初始化另一个对象:

       Person person4 = Person{};
    

    ​ 当使用匿名对象初始化其他对象时,编译器可以直接在目标位置构造该对象,而不是先在匿名对象中构造后再进行拷贝。

  2. 匿名对象值传递的方式传递给函数:

       void func1(Person obj) {}
       
       func1(Person{}); // 匿名对象传值
    
  3. 函数中返回 具名对象 或 匿名对象(并且这个对象属于局部作用域的,非静态的):

    ​ 严谨的来说,在函数中返回对象需要考虑返回值优化(RVO/NRVO)和使用的编译器C++标准(强制复制消除),但规律是C++标准越高,返回的对象拷贝操作将越少(甚至禁用返回值优化后,拷贝操作也不存在)。在该例中,使用gcc13.2版本的C++11,不禁用返回值优化,均不发生拷贝操作,即不会调用拷贝构造。

       Person func2() {
           Person m{};
           return m;
       }
       
       Person func3() {
           return Person{"b", 25};
       }
    

    但是在不属于返回值优化的场景,拷贝操作不可避免(这属于必定调用拷贝构造的情况,上面已经介绍):

       Person func4() {
           static Person a{};
           return a;
       }
    

    以下是禁用优化的场景的调用方式:

       puts("***********0");
       puts("-----匿名对象初始化对象-----");
       Person person4 = Person{};
       
       puts("\n\n***********1"); 
       puts("-----匿名对象值传递-----"); func1(Person{});
       
       puts("\n\n***********2"); 
       puts("-----返回具名对象-----"); func2();
       puts("\n-----返回匿名对象-----"); func3();
       
       puts("\n\n***********4"); return 0;
    

    在C++11标准下的输出(禁用返回值优化):

    ***********0
    -----匿名对象初始化对象-----
    拷贝构造	析构	
    
    ***********1
    -----匿名对象值传递-----
    拷贝构造	析构	析构	
    
    ***********2
    -----返回具名对象-----
    拷贝构造	析构	析构	
    -----返回匿名对象-----
    拷贝构造	析构	析构	
    
    ***********4
    析构	析构	析构
    

    在C++17/C++20标准下的输出(禁用返回值优化):

    ***********0
    -----匿名对象初始化对象-----
    
    
    ***********1
    -----匿名对象值传递-----
    析构	
    
    ***********2
    -----返回具名对象-----
    拷贝构造	析构	析构	
    -----返回匿名对象-----
    析构	
    
    ***********4
    析构	析构	析构
    

    ​ 可以发现,随着C++标准的提高,即使-fno-elide-constructors被开启,对于对于匿名对象赋值、传值和作返回值的优化被逐步加强,间接说明,提高C++标准可以提高程序效率。初始化语句 auto a = Person{}; 在编译器默认的编译选项下,不会进行任何拷贝操作,它和 Person a{}; 并无区别,在C++98中也一样,code

    ​ 这其实和C++17标准中复制消除变成强制要求有关。

    ​ 其次这和移动构造也有很大关系,如果我们将移动构造显式禁用Person(Person &&) = delete;, 无论 -fno-elide-constructors 是否被打开,这段程序都将出错,说明这这个具名返回值优化应用了移动构造:示例

    Person func2() {
        auto a = Person{"a", 25};
        return a;
    }
    
    Line 4: error: use of deleted function 'Person::Person(Person&&)'
    编译报错出现:使用被删除的移动构造。
    

    ​ 至于匿名返回值优化为什么没有调用移动构造是因为:纯右值表达式作为构造对象的参数,不会再调用移动构造,也不会去检测,而是原位构造

    Person func3() {
        return Person{"b", 25}; // Person{"b", 25} 是 匿名对象,临时对象,纯右表达式。
    }
    

编译器的拷贝构造
编译器生成的拷贝构造

默认生成的拷贝构造函数会逐成员地进行浅拷贝,也就是简单地将一个对象的成员值复制给另一个对象。对于指针类型的成员变量,进行简单的赋值,使得两个对象共享同一个内存块。这可能导致多个对象指向同一块内存,从而出现潜在的问题。

  • 编译器在什么时候会主动创建拷贝构造?什么时候又不会?
编译器会主动合成拷贝构造编译器不会定义拷贝构造
类中没有显式定义拷贝构造类中显式定义了拷贝构造
类中显式定义了 移动构造 或 析构函数

以下对表格做详细解释:

有些时候,虽然看上去我们没有定义移动构造,如:

class Person 
{
private:
    int age;

public:
    Person()= default;
    
    // Person(Person &) = default;
    Person(Person &&) = delete;
};

int main()
{
	Person person1{};
    Person person2 = person1; // 使用拷贝构造函数创建对象
}

将移动构造显式抑制,但程序16行报错:

use of deleted function 'constexpr Person::Person(const Person&)'
使用了被删除的拷贝构造函数

所以 显式的删除移动构造,编译器也不会生成拷贝构造函数,并且显式的删除移动构造这一行为也可以称为“定义了移动构造”

  • 编译器创建的拷贝构造是会完成什么操作?

浅拷贝、深拷贝 Bitwise Copy和Memberwise Copy_memberwise的拷贝-CSDN博客

​ 在这之前,我们需要清楚什么是按位拷贝和按成员拷贝:按位拷贝和按成员拷贝是 C++ 中两种不同的拷贝方式,它们的主要区别在于拷贝的粒度不同。

按位拷贝(bitwise copy):将一个对象的内存全部复制到另一个对象所在的内存地址,包括对象的所有成员变量、函数指针等。

按成员拷贝(member-wise copy):是将一个对象的每个成员变量分别复制到另一个对象的对应成员变量中。这个过程可以通过拷贝构造函数、赋值运算符等方式来实现。

​ 需要注意的是,按位拷贝只适用于 POD 类型(Plain Old Data Type,即标量类型和传统的 C 结构体),而对于含有虚函数、引用、const 成员变量或者非标量类型的类,则不能采用按位拷贝的方式进行对象的复制C++中类的默认的拷贝构造函数是按位拷贝,以下4种情况会进行按成员拷贝:

  1. 就是含有的成员对象本身提供了拷贝构造函数(不管是默认的还是自己提供的),当拷贝这个对象的时候调用的是对象的类提供的拷贝构造函数。
  2. 继承的基类有拷贝构造函数,这个时候编译器会插入基类的拷贝构造函数,而不是编译器自己来提供。
  3. 基类或该类中含有虚函数
  4. 这个类虚继承于其它类

​ 然而,对于具有虚函数表的类,无论是自定义的还是默认的拷贝构造,编译器都会插入对虚拟机制的处理代码这就保证对象切片和拷贝正确的发生(可能会出乎你的意料,但符合C++的语法语义)。

深拷贝和浅拷贝
深拷贝和浅拷贝

​ 拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并将其初始化为与现有对象相同的值或状态。当一个类包含指针成员时,拷贝构造函数需要进行深拷贝(deep copy),以确保新对象和原对象拥有独立的内存空间。相反,如果只是简单地复制指针地址,则是浅拷贝(shallow copy),这可能导致多个对象共享同一块内存,从而出现潜在的问题。

  • 浅拷贝:简单的赋值操作
  • 深拷贝:在堆区中重新申请空间,再进行拷贝操作

以下为举例,完整示例链接:https://compiler-explorer.com

// 浅拷贝示例
class ShallowCopy {
public:
    ShallowCopy(int value) : data(new int{value}) { }
    ShallowCopy(const ShallowCopy& other) : data(other.data) { } // 拷贝构造函数
    // ~ShallowCopy() { delete data; }
    ~ShallowCopy() {  } // 不回收以观察对象

    int* data;
};

​ 浅拷贝只是将指针成员的值进行了复制,使两个对象指向了同一块堆空间,在析构时,将会发生 double free 而参数段错误。当然对于非指针成员,或者该指针存在的意义本就是指向固定的地址(成员对象是常量指针),对于类似的情况它们没有深拷贝的说法,也不需要显式的定义拷贝构造(编译器会自动生成浅拷贝的拷贝构造)。

// 深拷贝示例
class DeepCopy {
public:
    DeepCopy(int value) : data(new int{value}) { }
    DeepCopy(const DeepCopy& other) : data(new int{*other.data}){ } // 拷贝构造函数
    ~DeepCopy() { delete data; }
    
    int* data;
};

​ 深拷贝重新申请了堆空间,并复制了成员数据,这确保了每个对象都拥有独立的内存空间,当其中一个对象修改数据时,另一个对象不会受到影响。

引用和常引用
拷贝构造中的常引用和引用
  • 为什么拷贝构造函数不能使用传值传参吗,而是引用(左值引用)?

​ 因为值传递本身就会调用拷贝构造函数形参对象,这样就形成了死递归。

在这里插入图片描述

  • 为什么参数有时候是常量左值引用,有时候是普通的左值引用?

    ​ 至于为什么不是右值引用,应该很清楚了,因为参数是右值引用就属于移动构造了。使用普通的左值引用(T&)来接受传入的对象,则会限制拷贝构造函数的使用场景。具体来说,如果传入的对象是一个右值(如临时对象),则无法使用普通的左值引用来接受它们。因此,使用常量左值引用可以使拷贝构造函数更加通用。

    对左值引用和右值引用的概念在语法的引用章节中,并且,常量左值引用常常用于拷贝语义

    其次在没有定义移动构造时,参数是 常量左值引用 的拷贝构造将会被移动语义所调用code

      class Person // -fno-elide-constructors
      {
      public:
          Person() = default;
          Person(const Person& other){ printf("拷贝构造\t"); } // 拷贝构造函数
          // Person(Person&& old){ printf("移动构造\t"); }
          ~Person(){ printf("析构\t"); }
      };
      
      int main() {
          Person a;
          Person b = std::move(a);
          return 0;
      }
    
    	  拷贝构造	析构	析构
    

2.1.4 移动构造

​ C++的移动构造函数(Move Constructor)是一种特殊的构造函数,用于源对象资源的控制权全部移交给目标对象,在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。而现在在某些情况下,我们没有必要复制对象——只需要移动它们。移动构造是C++11标准中提供的一种新的构造方法,移动构造可以减少不必要的复制,带来性能上的提升

移动构造函数不会移动类的静态成员。静态成员是属于类本身而不是对象实例的,它们在内存中只有一份拷贝,并且在程序运行期间一直存在。移动构造函数主要用于移动对象实例的非静态成员,这些成员在对象之间进行资源所有权的转移。

移动语义的出现,减少了复制语义的出现,避免了一些非必要拷贝的产生,例如:一个局部对象的资源需要在另一作用域中使用。

c++移动构造函数 - 斗战胜佛美猴王 - 博客园 (cnblogs.com)

特点:

  • 源对象资源的控制权全部移交给目标对象。
  • 首个形参是 T&&const T&&volatile T&&const volatile T&&,且无其他形参,或剩余形参均有默认值。
class Foo{
public:
    Foo() = default;
    ~Foo() = default;
    // 四种移动构造函数
    Foo(Foo&&) noexcept {std::cout << "Foo(Foo&&)\n";}
    Foo(const Foo&&) noexcept {std::cout << "Foo(const Foo&&)\n";}
    Foo(volatile Foo&&) noexcept {std::cout << "Foo(volatile Foo&&)\n";}
    Foo(const volatile Foo&&) noexcept {std::cout << "Foo(const volatile Foo&&)\n";}

    Foo& operator=(Foo&&) noexcept {std::cout <<"Foo& operator=(Foo&&)\n"; return *this;}
    Foo(const Foo&) {std::cout << "Foo(const Foo&)\n";}
    Foo& operator=(const Foo&) { std::cout << "Foo& operator=(const Foo&)\n"; return *this;}
};

    Foo f1;
    Foo dst1{std::move(f1)}; // 调用Foo(Foo&&)
    const Foo f2;
    Foo dst2{std::move(f2)}; // 调用Foo(const Foo&&)
    volatile Foo f3;
    Foo dst3{std::move(f3)}; // 调用Foo(volatile Foo&&)
    const volatile Foo f4;
    Foo dst4{std::move(f4)}; // 调用Foo(const volatile Foo&&)

移动构造的使用场景解释一下移动构造的使用场景,并举例说明 -高性能服务器开发 (0voice.com)

​ 对于内部有资源的类,比如智能指针(不允许复制)、在栈上的内存(高效)、文件(复制代价大)、网络端口、寄存器等,合适的移动构造函数更加高效,甚至是必须的

注意:

何时调用
什么时候会调用移动构造?

​ 移动构造出现在C++11标准中,但在C++17后复制消除变成了强制:纯右值表达式作为构造对象的参数,不会再调用移动构造,也不会去检测,而是原位构造。

C++17后纯右值不可能再调用移动构造( 无论 -fno-elide-constructors 选项是否被打开)。没有移动构造或者复制构造不影响使用同类型纯右值初始化对象,如 X x{X{}} ,即使移动/复制构造函数都被 delete,也无所谓,code

因此,什么时候被调用与C++标准和优化有关。

什么场景调用移动构造关闭优化C++标准备注(均是存在移动构造)
显式地将表达式转换为右值无关无关必定调用
函数中返回具名对象无关关闭优化调用
函数中返回匿名对象[C++11, C++17)关闭优化和对应版本调用
匿名对象值传递[C++11, C++17)关闭优化和对应版本调用

必定调用移动构造:前提是函数是值传递,引用传递必定不会调用移动构造。

  1. 显式地将表达式转换为右值:static_cast<T &&>();std::move()

    示例链接

    template<typename T> void forwardFunction(T&& arg) {
        otherFunction(std::forward<T>(arg)); } // 完美转发
    void otherFunction(Person obj) {} // 调用移动构造的函数必定是值传递
    int main() {
        Person a = Person{};
        puts("1-------"); otherFunction(a);				// 拷贝构造
        puts("2-------"); otherFunction(std::move(a));	// 移动构造
        puts("3-------"); otherFunction(static_cast<Person &&>(a)); // 移动构造
        puts("4-------"); auto b = static_cast<Person &&>(a); // 移动构造
        puts("5-------"); forwardFunction(Person{}); // Person{}是右值,调用移动构造,调用移动构造并不是std::forward的“功劳”,而是Person{}本就是右值,forward是真正意义的转发。
        puts("6-------"); return 0;
    }
    

一定条件下调用移动构造(关闭优化):完整例子

在拷贝构造的介绍中,以下三种情况会调用拷贝构造,不过,当时并未定义移动构造。

  1. 在函数中返回具名对象:C++标准的区间 [C++11, 至今],关闭优化。

    Person func2() {
        Person a{};
        return a;
    }
    

    在函数中返回具名对象,会调用移动构造,这个条件是C++11后(移动构造出现的标准),关闭优化。

  2. 在函数中返回匿名对象:C++标准的区间 [C++11, C++17)(包含C++11,但不包含C++17),并且关闭优化。

    Person func3() {
        return Person{}; // Person{}是纯右表达式
    }
    

    C++17后纯右值不可能再调用移动构造( 无论 -fno-elide-constructors 选项是否被打开)。

  3. 匿名对象的值传递:C++标准的区间 [C++11, C++17)(包含C++11,但不包含C++17),并且关闭优化。

    void func1(Person obj) {}
    
    func1(Person{}); // 使用匿名对象传递参数
    

    C++17后纯右值不可能再调用移动构造( 无论 -fno-elide-constructors 选项是否被打开)。

编译器的移动构造
编译器生成的移动构造
  • 什么时候生成默认的移动构造,什么情况下不会

​ 若类类型(structclassunion没有自定义的移动构造函数、移动赋值运算符、拷贝构造函数,拷贝赋值运算符或者析构函数时,编译器会自动生成一个非explicitinline public隐式移动构造函数T::T(T&&),即使用户自定义了上述函数,也可以用default 来显式地告诉编译器要生成隐式移动构造函数。

如果一个类拥有满足以下条件之一的情况,那么该类将不会生成默认的移动构造函数移动构造函数 (lerogo.com)

  1. 类拥有无法移动的非静态数据成员

    无法移动(拥有被弃置、不可访问或有歧义的移动构造函数)

  2. 类拥有无法移动的直接或虚基类

    当一个类拥有无法移动的直接或虚基类时,意味着该类继承了一个或多个无法进行移动操作的基类。

    直接基类是指在派生类中通过继承方式直接声明的基类。虚基类是通过虚继承方式从多个路径继承的基类。

    ​ 如果直接或虚基类的移动构造函数被定义为弃置、不可访问或有歧义,那么派生类的移动构造函数也会被定义为弃置,无法使用该构造函数进行对象的移动操作。

    ​ 这种情况通常发生在基类的移动构造函数被删除或不可访问(例如私有或受保护),或者基类的移动构造函数存在二义性(有多个重载版本但无法确定使用哪个)。

  3. 类 拥有带被弃置或不可访问的析构函数的直接或虚基类;

  4. 类T是一个联合体(union),并且拥有带非平凡移动构造函数的变体成员(如联合体中存在 std::string 类型)。

换句话说,如果一个类的移动构造函数无法被正常使用,编译器会自动将其定义为弃置,禁止使用该构造函数进行对象的移动操作。这样做是为了确保程序的正确性和安全性,避免出现潜在的错误。

  • 默认的移动构造完成什么样的功能

    该隐式移动构造函数的定义方式如下:

    ​ 对于 union 类型,它会使用类似于 std::memmove 函数直接进行内存拷贝。

    ​ 对于非 union 类型,该构造函数用于以亡值(xvalue)的实参(右值引用)执行直接初始化,在初始化过程中按照初始化顺序逐个移动对象的各个基类和非静态成员。移动操作将资源所有权从源对象转移到目标对象,通常是通过移动构造函数来完成的。注意,移动构造函数不应修改源对象的状态。

    对于平凡类型(包括平凡类类型),一切的移动构造,移动赋值之类的操作,等价于复制int b = std::move(a); 等价于 int b = a;

    ​ 如果满足constexpr构造函数的要求,生成的移动构造函数也将是constexpr的,这意味着可以在编译时进行常量表达式的求值。

    C++编译器合成的默认移动函数对于基本类型所谓移动只是把其值拷贝,对于类类型的成员调用其类实现的移动构造函数,实现真正的资源移动。以下代码可以验证“默认的移动函数对于基本类型所谓移动只是把其值拷贝,对于类类型的成员调用其类实现的移动构造函数”:

      struct A
      {
      	int *p;
      	string str;
          A(): p(new int),str("hello") { }
      };
      
      int main()
      {
      	A a;
      	A b(std::move(a));
      	cout << a.p << " " << a.str << '\n';
      	cout << b.p << " " << b.str << '\n';
      	return 0;
      }
    
      0x10f02b0 
      0x10f02b0 hello
    
noexcept
为什么移动构造需要 noexcept ?

为什么移动构造要使用noexcept-CSDN博客

​ 为使强异常保证可行,用户定义的移动构造函数不应抛出异常。例如,std::vector 在需要重新放置元素时,基于 std::move_if_noexcept 在移动和复制之间选择。

​ 若一同提供了复制和移动构造函数而没有其他可行的构造函数,则当实参是相同类型的右值(如 std::move 的结果的亡值,或如无名临时量的纯右值)时,重载决议选择移动构造函数,而当实参是左值(具名对象或返回左值引用的函数/运算符)时,重载决议选择复制构造函数。若只提供复制构造函数,则所有实参类别都选择它(只要它接收到 const 的引用,因为右值能绑定到 const 引用),这使得在移动不可用时,以复制为移动的后备。

​ 当接收右值引用为其形参时,构造函数被称作‘移动构造函数’。它没有义务移动任何内容,不要求类拥有要被移动的资源,而且在受允许(但可能没意义)的以 const 右值引用(const T&&)为形参的情况中,‘移动构造函数’可能无法移动资源

部分标准库会要求移动构造函数不抛出异常,案例如下:

​ 由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此通知给标准库。否则,它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做出一些额外的工作。

​ 一种通知标准库的方法是在我们的构造函数中指明 noexcept。 这是我们承诺一个函数不抛出异常的一种方法。并且,我们必须在类的头文件的声明中以及它的定义中(如果定义在类外的话),都指定noexcept

​ 移动一个对象通常会改变它的值。如果重新分配过程使用了移动构造函数,且在移动了部分而不是全部元素后抛出了一个异常,就会产生问题。旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。在此情况下,vector将不能满足自身保持不变的要求。

​ 另一方面,如果vector使用了拷贝构造函数且发生了异常,它可以很容易地满足要求,因为在新内存中构造元素时,旧元素保持不变,如果此时发生了异常,vector可以释放新分配的内存并返回,而其旧内存中原有的元素仍然存在。

​ 为了避免这种潜在问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望vector在重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库,我们的移动构造函数可以安全使用。这一点就是通过将移动构造函数标记为 noexcept 来做到的。

移动构造和移动赋值运算符
移动构造和移动赋值运算符

https://compiler-explorer.com/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1DIApACYAQuYukl9ZATwDKjdAGFUtAK4sGIAGykrgAyeAyYAHI%2BAEaYxCAAzBqkAA6oCoRODB7evgGp6ZkCoeFRLLHxSbaY9o4CQgRMxAQ5Pn6BdpgOWQ1NBCWRMXGJyQqNza15HeP9YYPlw0kAlLaoXsTI7BzmCWHI3lgA1CYJbshj%2BKgn2CYaAII7eweYx6dejrSEAJ7Xtw/3%2ByYCgUhwAIl9mCw8Mg7sRiEwvscAOxWe4pYh4ABuTAI7D%2BhwJhzCBAAVId0DimCdUXdCYcMgAvTAAfQI9LwTOpfz%2BKS80U%2ByBA%2BMJAHoRcdsEiTHcpRYrNgAKwmAAcDzuypM2H8JgAnDdlZJNTqVRppabFSrZaDNdqdUqLKb7nTwZDobD4V8IC7WG64QjzNqzP5DqgCAg4ktDgxUJhVJsUmyTCjhXSCRTGq9QSGw3EAHTpqkJGmpwmMl4nLOh8PEXNlrlOkvZ6v5ymZqNeWi0BPEeu0xtVvNltumosp1O8ggKCDmMyaqUykxyzVK1XSjVa3X6w3YY3K013c0rq023X201mMxLXt0pPW%2B5jsUS%2Bey%2BUrtXrjUWFevk2LtzLy1F2tDc7UXR0%2BwJb0oRhP0fiDEMUjiHESArL0IR9GCPQDAMmwjKMYzjTAE2RYtGzwKhDggMM8BBMAwArY4gwHYhIyTUjG0JLB6FxEwFSsBUswLa8OM41sGOYltGmEkT2SZNsJLrUcGxkiSCzbBgOy7AgeyUiCRIUjlywSLMR3Y1NbzHOkJynGc52lF8ANXdVNU/b9HNNCx/wtZVjxAs8ZyvXSOOITACHWBhDhJaiFGk5E73%2BPSoN9D0IDLVlDmiSMQHJSkIHCAB3IlBF4ixol40EVlkzAIEykiLPvJFrV0v5iUOFgmDCCArwasykswhFDipFEuFNRrYr690BrKlFZzGoLCWiNsLhAEAWFQTFqqYQKaW5RqOBWWhOAVXg/A4LRSFQTh/0sax6TWDZyzMBIeFIAhNH2lYAGsQAVBVcx1JEEiDfxJB1O1lQvJF9E4SQTvei7OF4BQQGSN6zv20g4FgGBEBAWMuneEhyEoJpgAUZRDBqIQEFQfLTpetAWBSOgcSyCnwloanadO87GeZ%2Bh4mALggdIPm6DiCJWC2XgxYFgB5d4ubp%2BH8ZgsnEaCVQugafBTt4fhBBEMR2CkGRBEUFR1HR0hdC4fRDGMaxrH0PBomRyAVlQBMsmRjgAFoLgrUwbssMwNEOP2qGjP2ajwLA/bQBgxmILwHBIBReHWuIMSwd2utIFPBDwNgABVUE8POVgUe7Nj0C4wnZqmaeV7heG0zB2GSfL4RSTgeAOo64ety6OGwLXkEJ4hDlUZV/D9kHDmAZBkEOYXczMSjrqsSxSEOXBCBIRjnqWNv3qWFZwyYLB4nzw6OFh0gWB%2B5IeczjXkdRs/MZxiAkFluJiYQH/vEQERguBIg0HbGgtBcTEGRjVeG0QwhNC%2BH3XgSDmDEC%2BHLaI2guhowZqgFgbBBBywYLQVB1ssDRC8MANwYhaC%2BxelgdqRhxBULwCFboG1fbnVVu8LYL1iQ1Hhp8aI8IsEeCwGg16GIn6txWFQAwZMABqeBMD5Tlohem%2BtZBG3EKbA28glBqHhrbe2RgUBOx3mIyuF1vYCF9gHAg6Ag7WIsGHTOG04Rx0wHYzo3RnAQFcJMPwdsQhzDKBUPQaQMh1GyJ4NoMTCjxIGFE4YdsAnxN6BMRJeRMk1HwT0GYaShjxEyTMUJdcSmRLKRIKuNcTYDw4MdUgr8EYcGnrPeekhDigOAKvJEuYNC5i4JRfeRAp47C4CfV6Z8vogEkEMnUs8FRIiRFwZUXAw57gVNDe%2BvAn4KhfvDEeSMUZzPRufUg31JCSGGUGQGCRjkaEkOAqGd8EhD3Omcy5Whrl3zMN8t%2BHBT5XJWN4jIzhJBAA%3D%3D

委托构造

转换构造

虚函数

虚函数表

虚基类

虚析构

普通成员函数

不可以同时用const和static修饰成员函数。

因为位域不必然始于一个字节的开始,故不能取位域的地址。指向位域的指针和非 const 引用是不可行的。从位域初始化 const 引用时,将创建一个临时量(其类型是位域的类型),并以该位域的值复制初始化,而引用绑定到该临时量。

位域的类型只能是整型或枚举类型。

位域不能是静态数据成员

默认生成的成员函数

C++ 编译器默认生成的八个成员函数 - 知乎 (zhihu.com)

三、语法

变参函数

cstdarg库

#include <cstdarg>

其中的宏函数如下

void va_start(std::va_list ap, parm_n);            // 允许访问可变函数参数
T va_arg(std::va_list ap, T);                      // 访问下一个可变参数函数
void va_copy(std::va_list dest, std::va_list src); // 生成可变函数参数的副本
void va_end(std::va_list ap);                      // 结束对可变函数参数的遍历

​ C++11增加的 va_copy() 是为了解决多次读参数的问题,因为va_arg()每次都会使参数指针后移,从而无法重新读取前面的参数,而va_copy()则是复制一份参数指针,从而使得能够重复读。

使用 cstdarg 库(即 C 中的 stdarg.h )定义变参函数时,该函数必须具备至少一个具名形参如:

double average(int num, ...);

​ 它接受一个格式字符串,并且后面跟随任意指定的参数,根据实际需要而确定入参的个数。

​ 函数参数是存储在栈中的,为了正确调用这些参数,必须知道它们的确切类型和个数。但是,由于无法确定这些信息,直接使用内存地址来访问参数是不明智的,可能会导致严重的安全漏洞。因此,C++ 不推荐使用这种方法。

​ 其次就算我们明确知道了可变参数的类型,即:正确的使用了va_arg() 函数,依旧无法按我们的要求获得实际传递的值,甚至会直接导致程序出错而结束。引起该现象的原因是:默认参数提升导致函数无法接收到类型的实际参数。

避免此现象,在传递和取值时都建议使用(默认参数提升会将对象提升到 intdouble):

va_arg(args, int);
va_arg(args, double);

复现该现象可以尝试下面的例子:

可变长参数列表误区与陷阱——va_arg不可接受的类型_c\c++ va_arg 用法不正确-CSDN博客

#include <cstdarg>

void my_printf(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt); /*  用最后一个具有参数的类型的参数去初始化ap  */
    for (; *fmt; ++fmt)
    {
        if (*fmt != '%') /*  如果不是控制字符  */
        {
            putchar(*fmt); /*  直接输出  */
            continue;
        }

        ++fmt; /*  如果是控制字符,查看下一字符  */
        switch (*fmt)
        {
        case '%':
            putchar('%');
            break;
        case 'd':
        {
            int i = va_arg(ap, int);
            printf("%d", i);
        }
        break;
        case 'c': /*  按照字符输出  */
        {
            /* * 但是,下一个参数是char吗 */
            /*   可以这样取出吗?  */
            char c = va_arg(ap, char);
            printf("%c", c);
        }
        break;
        }
    }
    va_end(ap);
}

int main()
{
    my_printf("%d%c\n",2,3);
}

使用该库的步骤

  1. 定义一个 va_list 类型的指针对象,以指向该可变参数的首地址,如:va_list args

    va_list 的定义根据编译器实现的不同可能如下:

    typedef char* va_list;
    
    // GCC的实现
    struct va_list
    {
        fp_offset; 	// fp_offset 记录下一个参数相对于帧指针的偏移量。
        gp_offset;  // gp_offset 记录下一个参数相对于全局指针(Global Pointer)的偏移量
        overflow_arg_area;  // 当参数个数超过寄存器容量时,用于存储额外的参数。这个区域可能是栈上的一块内存。
        reg_save_area;		// 用于保存超过寄存器容量的参数的寄存器备份。
    }
    
  2. 对象 args 进行初始化(使用va_start(args, 首个可变参数前紧接的具名参数)va_copy(va_list dest, va_list src);

  3. 获取参数,调用 va_arg(args, 类型)args是第一步定义的指针对象,va_arg第二个参数是变参中的一个类型。

    需要注意的是,va_arg 在调用之后将会根据类型将args偏移到下一个变参列表中对象的地址,因此使用该宏时需要注意:

    • va_arg 被调用时,其类型参数是否有误;
    • va_arg 的调用次数是否大于可变参数个数;
  4. 清理 va_list 实例,方法是调用 va_end(),应该养成获取完参数表之后销毁对象的习惯(每个编译器实现的不同,va_end()可能进行不同的方式销毁实例)。

#include <cstdio>
#include <cstdarg>

double average(int num, ...)
{
    va_list args;   // 定义一个可变参数列表指针
    double sum = 0;
    va_start(args, num);    // 初始化可变参数列表指针
    for (int i = 0; i < num; i++) {
        sum += va_arg(args, double);    // 获取可变参数值
    }

    va_end(args);   // 结束可变参数列表

    return sum / num;
}
int main()
{
    printf("Average of 2, 3, 4, 5 = %lf\n", average(4, 2.0, 3.0, 4.0, 5.0));
    return 0;
}

const char *fmt...:这种声明方式通常用于格式化字符串函数,例如printfsprintf等。其中,const char *fmt是固定参数,表示格式化字符串,后面的省略号 ... 表示可变参数列表。在函数内部,通过解析格式化字符串来获取后续的可变参数值。

#include <iostream>
#include <cstdarg>

void simple_printf(const char *fmt...)
{
    va_list args;
    va_start(args, fmt);

    while (*fmt != '\0')
    {
        if (*fmt == 'd')
        {
            int i = va_arg(args, int);
            std::cout << i << '\n';
        }
        else if (*fmt == 'c')
        {
            // 注意自动转换到整数类型
            int c = va_arg(args, int);
            std::cout << static_cast<char>(c) << '\n';
        }
        else if (*fmt == 'f')
        {
            double d = va_arg(args, double);
            std::cout << d << '\n';
        }
        ++fmt;
    }

    va_end(args);
}

int main()
{
    simple_printf("dcff", 3, 'a', 1.999, 42.5);
}

initializer_list

​ C++11在标准库中提供了 std::initializer_list 类,用于处理参数数量可变但类型相同的情况。使用 std::initializer_list 最常用的方式是通过大括号包围的值列表对其进行初始化,与 std::list 容器的区别就是对 std::initializer_list 初始化完成后不能添加、删除和修改元素,其他用法基本和 std::lits 相同。

​ 花括号里面的数组,C++可以将其识别成一个 std::initializer_list,并且其中的内容不可被修改;C++11中 的 std::vector,是通过新增的构造函数的方式使用 std::initializer_list 进行初始化。

auto a = {0, 1}; // 通过大括号包围的值列表对initializer_list进行初始化
cout<< typeid(a).name() << "\n";

// 输出 St16initializer_listIiE

因此可以利用该方法实现类型一致的变参函数

#include <iostream>
#include <initializer_list>

// 变参函数,接收多个整数参数
void printNumbers(std::initializer_list<int> numbers) {
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    // 调用变参函数并传入多个参数
    printNumbers({1, 2, 3, 4, 5});
    return 0;
}

变长参数模板

​ 虽然 initializer_list版本的变参函数 解决了 cstdarg库 诸多缺陷(需要注意传递的参数类型及其个数,还有va_arg()函数的默认参数提升引起的各种bug),但是却增加一个很大的问题:使用 initializer_list 做函数形参只能传递类型一致的可变参数。为了解决这个问题,可以使用C++11增加的可变模板参数。[跳转到可变参数模板](#1.5.5.5 变长参数模板(C++11))

使用该方法依旧存在缺点:可变模板参数的包展开较复杂。

此处不能使用 initializer_list 去进行包展开的原因在段落开头就已阐述。

#include <iostream>

// 版本1. 使用递归:终止条件和执行条件为该判断语句(可以将constexpr if语句换为 printNumbers 的特化版本)
template<typename T, typename... Args>
void printNumbers(T value, Args... args) {
    std::cout << value << " ";
    if constexpr (sizeof...(Args) > 0) printNumbers(args...); // C++17
    else std::cout << '\n';
}
// 版本2. 使用折叠表达式展开参数包,并打印每个参数
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << std::endl; // C++17
}

int main() {
    // 调用变参函数并传入多个参数
    printNumbers(1, 2, 3, 4, 5);
    printNumbers(1.3, 2, 3, "asd", true, 'd');

    print(1, 2, 3, 4, 5);
    print(1.3, 2, 3, "asd", true, 'd');

    return 0;
}

临时对象

深入探讨C++中临时对象的常见产生情况及其解决的方案-CSDN博客

引用

​ 在 C++ 中,引用类型是一种特殊的数据类型,它用于建立一个已经存在变量的别名。引用在定义时必须初始化,并且只能绑定到一个已经存在的对象上。引用不能被重新指向其他对象。

  • 引用只能在定义时被初始化一次,之后不可变。

  • 不存在 void 的引用,也不存在引用的引用。

  • 引用本身不占用内存,因为它只是一个别名或者句柄,底层指向的是已经存在的内存空间。

    ​ 由于引用本质上是指向已存在对象的别名,所以它不需要分配额外的存储空间。编译器在处理引用时,通常会将其编译成对原对象的直接操作,而不会产生额外的存储开销。

    ​ 然而,需要注意的是,对于某些特殊情况下的引用类型,例如包含引用类型的非静态数据成员,编译器可能需要为了实现所需的语义而分配额外的存储空间。这是因为引用类型的非静态数据成员通常会增加类的大小,以存储引用所需的内存地址。这种情况下,编译器会根据需要进行存储分配,以满足引用的语义要求。

    总之,引用本身不占用额外的存储空间,但在某些特殊情况下,编译器可能会为了实现引用的语义而分配额外的存储空间。

  • 引用不是对象,故不存在引用的数组不存在指向引用的指针不存在引用的引用

    不存在引用的数组意思是:数组的内容不可能是引用,而是绑定到引用的实体的拷贝。

    #include <iostream>
    int main() {
           int x = 10;
           int& ref = x; // 定义一个引用 ref,绑定到变量 x
           int arr[3] = {1, 2, 3}; // 定义一个整型数组,内容为整数值
           // 尝试将引用 ref 赋值给数组的元素
           arr[0] = ref;
           std::cout << &ref <<'\n' << &arr[0] << std::endl;
           return 0;
    }
    
  • 引用的类型的权限只能比引用实体相同或更小,而不能将权限扩大(不能将常量绑定到可读可写的引用上,反过来却可以)。

  • 引用类型必须和引用实体具有相同的底层类型(而不能说:引用类型必须和引用实体是同种类型),因为引用实际上是目标变量的一个别名,与目标变量共享同样的内存地址。**但需要注意的是,即使引用类型和引用实体类型不完全匹配,也有可能通过隐式类型转换来创建一个临时变量,并将该临时变量的地址绑定到引用上。**这个临时变量的生命周期将与引用的生命周期相同,即在引用作用域结束时销毁。

    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
    

    ​ 对于 double d = 12.34; ,我们用 int& rd = d; ,因为类型转换会将 12.34 转换成整形的临时变量给 int& d;临时变量具有常属性(这是语言级别的限制),所以权限放大了,编译会报错,所以需要加 const 进行修饰。这种行为也将该临时变量的生命周期延长至 rd 的作用域内。

    ​ 临时变量具有常属性,也就是说它们不能被修改。这是因为临时变量通常是为了计算某个表达式而创建的,不具有持久性和可修改性。如果允许对临时变量进行修改,就会破坏程序的语义和正确性。

  • 在使用引用时,确保所引用的对象仍然有效,避免出现悬垂引用。避免在函数中返回局部变量的引用,以免超出引用的作用域。谨慎使用指向动态分配对象的引用,确保在释放对象之前使用引用。下面这个简单的例子描述了在使用引用时避免悬垂引用的方法:

    #include <iostream>
    int global_variable = 10;
    int& getVariable(bool f) {
        static int static_variable = 100;
        if (f)
            return static_variable; // 返回一个对静态变量的左值引用
        else
            return global_variable; // 返回一个对全局变量的左值引用
    }
    
    int main() {
        int& ref = getVariable(false); // 获取全局变量的左值引用
        ref = 20; // 修改全局变量的值
        std::cout << "static_variable: " << ref << std::endl;
        std::cout << "global_variable: " << (getVariable(true) += 60) << std::endl;
        return 0;
    }
    

引用的 const 修饰符

const int &a = 10;
int const &b = 10;

以上 const 的修饰和位置无关,无区别的修饰。

char a[2] = {0};
const char *pa = a;
int arg = 30;

// 左值引用
char* const &str1 = a;		// a 是右值
const char* &str2 = pa;		// pa是左值
const char* const &str3 = "abc";  // "abc" 是右值
auto &&str4 = a;	// 对数组的引用 左值
auto &&str5 = "abc";// 对数组的引用 左值
auto &&ch = a[1];	// 对字符的引用 左值
// 右值引用
char * &&sstr1 = a;				// 对指针a的引用 右值
const char* &&sstr2 = "abc";	// 对"abc"的常量指针的引用 右值
auto const &&astr1 = std::move(a[1]);	// 使用move将左值转换为右值
auto &&astr2 = const_cast<char *>(pa);	// 强转的临时对象是右值

/*
str1	char *const &
str2	const char *&
str3	const char *const &
str4	char (&)[2]
str5	const char (&)[4]
ch		char &

sstr1	char *&&
sstr2	const char *&&
astr1	const char &&
astr2	char *&&
*/
  1. char* const &str1 = a;const 修饰 &,表示 str1常量左值引用,可以绑定到右值。

    str1 是一个常量引用,指向一个字符数组。由于它是一个常量引用,所以不能修改指针的值,即不能让它指向其他的字符数组。但可以通过指针修改所指向的字符。

  2. const char* &str2 = pa;const 修饰 char* ,表示 str2 只能被绑定const char*常量字符指针,这是一个对常量字符指针的引用。

    str2 是一个非常量引用,指向一个常量字符指针。由于它是一个非常量引用,所以可以修改指针的值,即让它指向其他的常量字符指针。同时也可以通过指针修改所指向的字符。

  3. const char* const &str3 = "abc";:这是一个对常量字符指针的常量引用。

    str3 是一个常量引用,指向一个常量字符指针。由于它是一个常量引用,所以不能修改指针的值,即不能让它指向其他的常量字符指针。同时也不能通过指针修改所指向的字符。

  4. auto &&str4 = a;:这是对**数组的引用:**这篇文章介绍了数组引用的细节:了解C++的类型推导 - 知乎 (zhihu.com)

    ​ C语言遗传下来一个特性,“数组和指针类型的形参是一回事”。因为你经常可以使一个数组退化成一个指针。因此,按值传递给函数模板的数组类型会被推导成指针:

    // 声明:
    template<typename T>
    void f(T param);
    
    // 调用:
    const char name[] = "Hello World!";
    f(name); // const char*
    

    但是如果将数组按照引用的方式传形参,那么事情就不一样了。T会被推导成数组的引用!

    // 声明:
    template<typename T>
    void f(T& param);
    
    // 调用:
    const char name[] = "Hello World!";
    f(name); // const char (&)[13]
    

总结:

  • char* const &str1 是对字符数组的常量引用,不能修改指针的值,但可以通过指针修改所指向的字符。
  • const char* &str2 是对常量字符指针的引用,可以修改指针的值,并且可以通过指针修改所指向的字符。
  • const char* const &str3 是对常量字符指针常量引用,不能修改指针的值,也不能修改所指向的字符。

测试程序:

#include <iostream>
 
template <typename T>
constexpr auto type_name() noexcept {
	std::string_view name = "Error: unsupported compiler", prefix, suffix;
#ifdef __clang__
	name = __PRETTY_FUNCTION__;
	prefix = "auto type_name() [T = ";
	suffix = "]";
#elif defined(__GNUC__)
	name = __PRETTY_FUNCTION__;
	prefix = "constexpr auto type_name() [with T = ";
	suffix = "]";
#elif defined(_MSC_VER)
	name = __FUNCSIG__;
	prefix = "auto __cdecl type_name<";
	suffix = ">(void) noexcept";
#endif
	name.remove_prefix(prefix.size());
	name.remove_suffix(suffix.size());
	return name;
}

int main()
{
    using std::cout;
    using std::endl;

    char a[2] = {0};
    const char *pa = a;
    int arg = 30;

    char* const &str1 = a;		
    const char* &str2 = pa;		
    const char* const &str3 = "abc";  
    auto &&str4 = a;
    auto &&str5 = "abc";
    auto &&ch = a[1];
    
    char * &&sstr1 = a;
    const char* &&sstr2 = "abc";

    auto const &&astr1 = std::move(a[1]);
    auto &&astr2 = const_cast<char *>(pa);

    cout<<"str1\t"<< type_name<decltype(str1)>() << endl;
    cout<<"str2\t"<< type_name<decltype(str2)>() << endl;
    cout<<"str3\t"<< type_name<decltype(str3)>() << endl;
    cout<<"str4\t"<< type_name<decltype(str4)>() << endl;
    cout<<"str5\t"<< type_name<decltype(str5)>() << endl;
    cout<<"ch\t"<< type_name<decltype(ch)>() << endl;
    cout<<"sstr1\t"<< type_name<decltype(sstr1)>() << endl;
    cout<<"sstr2\t"<< type_name<decltype(sstr2)>() << endl;
    cout<<"astr1\t"<< type_name<decltype(astr1)>() << endl;
    cout<<"astr2\t"<< type_name<decltype(astr2)>() << endl;

    return 0;
}

左/右值引用

​ 在 C++ 中,左值(Lvalue)和右值(Rvalue)是两种不同的表达式类型,它们主要用于区分表达式的使用方式和生命周期。

表达式属性
泛左值
右值
左值
将亡值
纯右值

详细内容可以查看大佬文章:C++ / 左值、右值、将亡值 | 谈谈我对它们的深入理解 - 知乎 (zhihu.com)

文中提到的结论和广大网友的结论相冲突,以下是普遍认为的结论:

​ 左值是指具有内存地址的表达式。左值可以被取地址、修改或者作为引用传递,因为它们代表着一个具体的内存位置,可以被程序所访问。例如,变量、数组元素、对象成员、函数返回的引用等都是左值。
​ 右值是指没有内存地址的表达式。右值一般是被移动、复制或者作为值传递,因为它们代表的是一个临时的值,不具有持久性。例如,字面量、临时变量、表达式的计算结果等都是右值。

​ 如果你了解过单片机的链接脚本,其中的 data段bss段 都是用来存储全局变量和静态变量,他们唯一的区别就是是否具有初始值,而为什么要将这两种情况分开呢?

static int a = 10;
static int b;

​ 以上就是使用C/C++的定义在所有函数外的静态变量,他们的区别就是有没有初始值。在单片机中,这些变量(不管是全局还是静态)都为可变变量,所以存放在 RAM 区,其中 .bss 段存储无初始值全局变量,所以只需划分出相应大小的内存块,而不用真实导入数据。.data 段就比较复杂,因为它有初始值,所以我们不仅要在 RAM 区划出内存块,还要在 ROM 区保存一份它的初始值。当程序加载到内存中时,.data段的内容会被直接拷贝到相应的内存地址。

​ 因此,在单片机这个角度来看,文章的结论并不和大众的结论相冲突——左值是指具有内存地址的表达式,而ROM并不属于内存,但字面量10 确实存在地址,即在ROM区域。以下是引用自文章对左值和右值的总结:

​ 最后总结一下,不能取地址就是右值的说法有些不准确,或者说我不太认同这种说法,我认为只要数据位于的区域你没有权限访问,这些数据就是右值,数据位于你有权限访问的区域,存储的数据是左值。并且这个权限不是语言限制的,而是系统限制的访问权限,语言位于系统之上,我们可以突破语言的限制,但是底层系统的限制我们无法突破,也不能突破。

std::cout << &("abc") << std::endl;

​ 根据以上结论,对于字符串字面值"abc" ,它是存在地址的,并且我们也可以获取它;在单片机中它属于.rodata段,并且这部分内容可以和.text段结合。也就是说,他们经常被放置在一起,即正文段,这部分数据是不允许修改的,并且大部分单片机在运行程序时并不会将 .text 程序搬移到 RAM 区域再执行,但其实FLASH中的数据部分(.data)确实是需要搬运到RAM去的,在用户的main函数之前会有Reset_Handler 由该函数负责将FLASH中的数据拷贝到RAM中(知乎上也有针对flash的程序是否必须搬移到 RAM 中的回答:嵌入式系统中,FLASH中的程序代码必须搬到RAM中运行吗? - 知乎)。根据文章作者的结论(数据位于的区域你没有权限访问,这些数据就是右值)字符串字面值就属于右值。

const char* &&str = "abc";		// 右值引用
const char* &str1 = "abc";		// error:cannot bind non-const lvalue reference of type 'const char*&' to an rvalue of type 'const char*'
const char* const &str2 = "abc";// 常量左值引用

const char* const &str1 = "abc";表示的是 str2 被绑定到一个类型为const char* 的常量指针(这个类型的指针表示的是不能通过该指针修改其内容),const &str2 表示不能通过这个引用修改被绑定的对象。

​ 字符串字面量在 C++ 中被视为右值。在大多数情况下,字符串字面量会被编译器优化为指向静态存储区的指针,因此它们不会占用额外的内存。

​ 然而,需要注意的是,根据 C++11 的规定,在某些上下文中,字符串字面量可以被推断为左值。这是因为在这些上下文中,字符串字面量被视为一个指向字符数组的指针,而指针本身是左值。

​ 总结起来,字符串字面量通常是右值,但在特定上下文中可以被推断为左值。这取决于如何使用它们以及所处的语境。例如,在函数调用中作为参数传递时,字符串字面量被推断为右值,而在某些赋值操作中,它们可能被推断为左值。例子如下:

// 字符串字面量作为函数参数
void foo(const char* str);

foo("Hello, world!"); // 字符串字面量被推断为右值

// 字符串字面量作为数组初始化器
char arr[] = "Hello, world!"; // 字符串字面量被推断为左值

// 字符串字面量赋值给指针变量
const char* ptr = "Hello, world!"; // 字符串字面量被推断为右值

// 字符串字面量作为条件表达式
if ("Hello") { // 字符串字面量被推断为左值
  // ...
}

以上是对左值和右值的讨论,可以再强调一下左值引用和右值引用的定义:

  • 左值是指具有内存地址的表达式。左值可以被取地址、修改或者作为引用传递,因为它们代表着一个具体的内存位置,可以被程序所访问。

    • 非常量左值引用只能绑定到非常量左值上;

    • 常量左值引用可以绑定到所有的值类型,如:非常量左值、常量左值、非常量右值、常量右值等。

  • 右值是指没有内存地址的表达式。右值一般是被移动、复制或者作为值传递,因为它们代表的是一个临时的值,不具有持久性。并且编译器不允许访问临时变量,修改它没有意义,也不合逻辑

    • 右值引用:其实也是绑定到右值的引用,通过 && 来获得右值引用。C++11增加

    • 非常量右值引用只能绑定到非常量右值上,常量右值引用可以绑定到非常量右值、常量右值上。

    • 右值引用可用于为临时对象延长生存期(注意,左值引用亦能延长临时对象生存期,但不能通过左值引用修改它们)。

img

以下是对右值引用的讨论

在该文中C++ 右值引用解释 — C++ Rvalue References Explained (thbecker.net)提出了右值引用至少解决的两个问题,并介绍了右值引用为何而产生:

  1. 移动语义
  2. 完美转发

非类右值的引用可以被修改

​ 右值不应与对象的恒定性相混淆。右值并不意味着对象是不可变的。这存在一些混淆,因为非类右值是不可修改的。用户类型并非如此。类右值可用于通过其成员函数修改对象。(这句话引用自:左值和右值 — Lvalues and Rvalues (accu.org)

​ 对于非类类型的右值,它们是不可修改的,因为它们是字面量(literal)。字面量是一种预定义的常量,它们的值在编译时就已经确定,不能被改变。例如,10 是一个整数字面量,它的值永远是 10,不能被修改。

​ 相比之下,用户定义的类类型可以通过移动构造函数和移动赋值运算符来定义自己的移动语义,使其可修改。当使用右值引用传递对象时,可以调用这些移动语义,从而允许修改对象的内部状态。可见下面的例子:

#include <iostream>
#include <cstring>
#include <typeinfo>
class MyString {
public:
    MyString() : data(nullptr), size(0) {}
    MyString(const char* str) : size(std::strlen(str)) {
        data = new char[size + 1];
        std::strcpy(data, str);
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept : data(nullptr), size(0) {
        swap(*this, other);
    }

    // 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        swap(*this, other);
        return *this;
    }

    // 修改成员函数
    void append(const char* str) {
        std::size_t len = std::strlen(str);
        char* newData = new char[size + len + 1];
        std::strcpy(newData, data);
        std::strcat(newData, str);
        delete[] data; // 释放原有的内存
        data = newData;
        size += len;
    }

    friend std::ostream& operator<<(std::ostream& os, const MyString& str) {
        os << str.data;
        return os;
    }

// private:
    char* data;
    std::size_t size;

    // 辅助函数:交换两个对象的成员
    friend void swap(MyString& first, MyString& second) noexcept {
        using std::swap;
        swap(first.data, second.data);
        swap(first.size, second.size);
    }
};

template <typename T>
constexpr auto type_name() noexcept {
	std::string_view name = "Error: unsupported compiler", prefix, suffix;
#ifdef __clang__
	name = __PRETTY_FUNCTION__;
	prefix = "auto type_name() [T = ";
	suffix = "]";
#elif defined(__GNUC__)
	name = __PRETTY_FUNCTION__;
	prefix = "constexpr auto type_name() [with T = ";
	suffix = "]";
#elif defined(_MSC_VER)
	name = __FUNCSIG__;
	prefix = "auto __cdecl type_name<";
	suffix = ">(void) noexcept";
#endif
	name.remove_prefix(prefix.size());
	name.remove_suffix(suffix.size());
	return name;
}

int main() {
    MyString s1("hello");
    MyString&& s2 = std::move(s1); // 将 s1 强制转换为右值引用
    std::cout << type_name<decltype(s2)>() << '\n';  // 查看s2的类型
    s2.append(", world!"); // 调用修改成员函数
    std::cout << s2 << std::endl; // 输出 "hello, world!"
    s2.data[0] = 'g';
    std::cout << s2 << std::endl; // 输出 "gello, world!"

    return 0;
}

&& 并不一定表示右值引用

Widget&& var1 = someWidget;      // 这里,“&&”表示右值引用
 
auto&& var2 = var1;              // 这里,“&&”并不表示右值引用
 
template<typename T>
void f(std::vector<T>&& param);  // 这里,“&&”表示右值引用
 
template<typename T>
void f(T&& param);               // 这里,“&&”并不表示右值引用

​ If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.
如果一个变量或者参数被声明为T&&,其中T是被推导的类型,那这个变量或者参数就是一个万能引用universal reference)。具体是什么左值引用还是右值引用,得结合 void f(T&& param); 的形参和实参来看。

该头文件提供了3个模板类,用于获取编译期对象的类型信息:

#include <type_traits>

std::cout << std::is_rvalue_reference<int&&>::value << '\n';
std::cout << std::is_lvalue_reference<int&>::value << '\n';
std::cout << std::is_reference<int&&>::value << '\n';

转发引用(万能引用)

​ C++11除了带来了右值引用以外,还引入了一种称为“万能引用”的语法。万能引用是一种可以同时接受左值或右值的引用,它的形式是 T &&auto &&,其中 T 是一个模板参数。万能引用不是C++的一个新特性,而是利用了模板类型推导引用折叠的规则来实现的功能。

​ 万能引用的出现是为了函数的参数转发而来,从文章 转发引用——2014-10-06 摘抄的几句话,文中说到更应该称他为“转发引用”,在上面这篇文章里,他们指出“万能引用”这个名称并不合适,因为“我们”不希望给人以一种“它应该被‘普遍‘使用“的印象:

​ The last point gets to the heart of the matter: The current T&& design was specifically designed to support
argument forwarding uses.最后一点直指问题的核心:当前的T&&设计是专门为支持而设计的参数转发使用。

​ We (the committee) should guide the community to use the term “forwarding reference” as a clearer term. The simplest way to do that is to mention the name in the standard, even in a non-normative note.我们(委员会)应指导社会使用“转发引用”一词,使其更清晰。要做到这一点,最简单的方法是在标准中提及该名称,即使是在非规范的注释中。

​ “Universal reference” is a reasonable name with an obvious meaning—one that happens to be wrong in at least the authors’ opinion. A “universal reference” must (obviously to many of us in retrospect) mean:

  • a reference that can be used everywhere; or
  • a reference that can be used for everything; or
  • something similar.

​ And this is not the case, and is not the appropriate use of this construct. The concern is that some people will interpret that something having such a name is meant to be used “universally”. And that’s a bad thing to encourage by a name that will imply that to many people, even if we constantly put up disclaimers when we use it.

​ So the rub is that it is a name that just rolls off the tongue and is misleading because “universal references” aren’t universal in the sense of how pervasively they should be used. Furthermore, they aren’t even really references per se, but rather a set of rules for using references in a particular way in a particular context with some language support for that use, and that use is forwarding.

​ “万能引用”是一个意义明显的合理名称——至少在作者看来,这个名称是错误的。“万能引用”必须意味着(回想起来,显然对我们许多人来说):

  • 一个可以在任何地方使用的引用;或

  • 一个可以用于所有内容的引用;或

  • 类似的东西。

​ 但事实并非如此,这不是这个构念的恰当用法。令人担心的是,有些人会认为具有这样的名称的东西意味着要“普遍”使用。用一个会暗示很多人的名字来鼓励这是一件不好的事情,即使我们在使用它的时候不断地声明。

​ 所以问题是,这是一个很容易脱口而出的名字,并且具有误导性,因为“万能引用”并不是一个通用的名称,因为它们应该如何被广泛使用。此外,它们甚至不是真正的引用本身,而是一组规则,用于在特定上下文中以特定方式使用引用,并且有一些语言支持这种使用,这种使用就是转发。

​ 此外它还有一个功能:使一个函数可以接收左值和右值的参数,而不需要使用重载分别处理左值和右值,重载的方式如下:

void fun(int &&a) { // a为右值引用
  // do sth
}

void fun(int &a) { // a为左值引用
  // do sth
}

但这种情况应该尽量使用常量左值引用去替换万能引用,转发除外。

万能引用的定义只能是形如如下的两种场景:

template<typename T>
void f(T&& param);
auto&& var2 = var1;

函数的形参是 T&& 也不一定右值引用,以下例子均不是万能引用:

template<typename T>
void f1(std::vector<T>&& param); // param是一个右值引用

template<typename T>
void f2(const T&& param);		 // param也是一个右值引用

剥夺:const修饰词会剥夺一个引用成为万能引用的资格,会被打回原型成为右值引用

template<class T, class Allocator=allocator<T>>
class vector{
public:
  void push_back(T&& x);
};

辨认:函数 push_back 并没有类型的推断,类型推断发生在 vector 类实例化时。例如 vector 存储的是 int 时,T 即为 int,函数 push_back 会被实例化为int push_back(int&& x)x 成为实实在在的右值引用。将其修改一下 x 就成为了万能引用:

template<class T, class Allocator=allocator<T>>
class vector{
public:
    template<typename A>
	void push_back(A&& x);
};

除此之外,形如如下,引用中加入变长模版参数的参数 args 是万能引用:

template<class T, class Allocator=allocator<T>>
class vector{
public:
  template<class...Args>
  void emplace_back(Args&&... args);
};

万能引用的作用

​ C++ 中的万能引用(也称为完美转发引用)是一个非常强大的特性。它允许编写通用代码,能够处理任意类型和值类别的参数,并将它们完美地转发给其他函数或对象。可以接受任何类型的参数,并保留原始参数的值类别。

template<class T>
void bar(T&& arg);

这意味着,如果传递给 emplace_back 函数的参数是左值,那么 arg 也是左值;如果传递的参数是右值,那么 arg 就是右值。

对于函数模板,万能引用通常与 std::forward 一起使用,以将参数完美转发给其他函数或对象。例如:

template<typename T>
void bar(T&& arg)
{
    other_function(std::forward<T>(arg));
}

​ 在这个例子中,arg 是万能引用,它被传递给 std::forward,并使用 std::forwardarg 完美转发给 other_function。这将确保参数的值类别被正确地保留,并且可以避免不必要的拷贝或移动操作,提高程序的效率。

引用折叠

首先,C++不存在“引用的引用”,这样的代码在C++中是非法的:

int x;
auto& & rx = x;

但是,假设我们向前面的万能引用函数 f 传入一个左值引用:

void f(auto &&param);

int w = 10;
f(w);  //	auto的推导型别为 int&

那么实例化的结果如下:

void f(int& &&param);

之所以这样的代码能通过,是因为在特殊的情况下(比如模板实例化),C++应用了引用折叠的语法(传统C++是不支持这种用法的)。

C++中有两种引用(左值引用和右值引用),因此引用折叠就有四种组合。引用折叠的规则:

如果两个引用中至少其中一个引用是左值引用,那么折叠结果就是左值引用;否则(即两个都为右值引用)折叠结果就是右值引用

所以函数 f 实例化的结果是:

void f(int& param);
模板类型T实际类型最终类型
T &intint &
T &int &int &
T &int &&int &
T &&intint &&
T &&int &int &
T &&int &&int &&

完美转发

C++入门(四):完美转发_c++完美转发-CSDN博客

  • 定义

    函数模版可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

    特点:

    1. 当接收函数是引用接收时,能保证被转发参数的左、右值属性不变。
    2. 当接收函数是值接收时,能根据左右值属性调用拷贝构造函数和移动构造函数。

    接收函数是指 std::forward() 被作为参数的函数:

    void forwardFunction(T&& arg) {
        otherFunction(std::forward<T>(arg)); // 使用 std::forward 进行完美转发
    }
    

    函数 otherFunction() 就是接收函数。

    值接收是指:接收函数的形参是值传递的形式。

    void otherFunction(Person obj) {} // 值传递(值接收)
    

    引用接收:

    void otherFunction(Person&& obj) {}
    void otherFunction(Person &obj) {}
    

    并且值接收和引用接收不能同时存在

  • 为什么需要完美转发?

​ C++ 中引入完美转发的主要目的是为了解决在模板编程中函数重载和类型推导的问题

​ 在 C++ 中,函数模板可以使用不同类型的参数进行多次声明,这称为函数模板的重载。然而,当我们传递一个参数到函数模板时,编译器必须确定传递的参数类型,以便能够选择正确的模板实例化。但是,如果我们将传递的参数再次传递给另一个函数,则很难确定该参数的类型。

​ 此时,完美转发就可以派上用场了。通过使用完美转发,我们可以将传递给函数模板的参数(包括其值类别、常量性和引用性等)转发给另一个函数,而不会改变参数的类型。这使得我们可以避免出现类型错误或无法匹配的情况,提高了程序的可靠性和灵活性

​ 此外,完美转发还可以减少不必要的对象拷贝和移动操作,提高程序的性能。在实现移动语义和右值引用等新特性时,完美转发也扮演了重要角色。

  • 如何做到完美转发?

​ 无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。所以无论给函数传递右值还是左值,在函数内部这个参数在使用时都会被自动当成左值,为了找回丢失的右值属性(如果传递了右值),要么手动强转 static_cast<>(),要么 std::move() ,更好的做法是使用 std::forward<>() 来自动帮你推断。

​ 这个模版函数内部也是通过静态强转来实现的,不过不需要人为确定类型,它传递的是编译器自动推导的类型:

/**
*  @brief  Forward an lvalue.
*  @return The parameter cast to the specified type.
*
*  This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
	forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
*  @brief  Forward an rvalue.
*  @return The parameter cast to the specified type.
*
*  This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
	forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
  static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
        " substituting _Tp is an lvalue reference type");
  return static_cast<_Tp&&>(__t);
}

使用它的方法也很简单:

#include <iostream>
#include <type_traits>
using namespace std;

#if 0
//重载被调用函数,查看完美转发的效果
void _otherdef(int & t) {
    cout << "lvalue\n";
}
void _otherdef(const int & t) {
    cout << "rvalue\n";
}
#else
template <typename T>
void _otherdef(T && t)
{
    if(std::is_rvalue_reference<decltype(t)>::value) // 使用模版
        cout << "rvalue\n";
    else
        cout << "lvalue\n";
}
#endif

//实现完美转发的函数模板
template <typename T>
void func(T&& t) {
    cout << &t <<'\n';
    _otherdef(std::forward<T>(t));
}

int main()
{
    func(5);
    int  x = 1;
    func(x);
    return 0;
}
完美转发可以根据表达式属性调用拷贝构造或移动构造

​ 当使用完美转发时,接收参数的函数如果不是引用接收,如果传递的参数是右值(临时对象或即将销毁的对象),则会调用移动构造函数来转移资源如果传递的参数是左值(具有名称的对象),则会调用拷贝构造函数来进行复制。这意味着完美转发可以根据传递的参数值类别选择调用移动构造函数或拷贝构造函数。示例链接

#include <cstdio>
#include <utility>
class Person
{
public:
    Person()= default;
    Person(const Person& other)  { puts("拷贝构造"); }
    Person(Person &&old) { puts("移动构造"); }
    ~Person() { puts("析构"); }
};

template<typename T>
void forwardFunction(T&& arg) {
    otherFunction(std::forward<T>(arg)); // 完美转发
void otherFunction(Person obj) {} // 以值接收参数

int main() {
    Person a = Person{};
    puts("1-------"); forwardFunction(Person{}); // 右值,调用移动构造
    puts("2-------"); forwardFunction(a);         // 左值,调用拷贝构造
    puts("3-------"); otherFunction(std::move(a)); // 右值,调用移动构造
    return 0;
}
/* gcc -std=C++20 *.c
1-------
移动构造
析构
析构
2-------
拷贝构造
析构
3-------
移动构造
析构
析构
*/

移动语义

/**
*  @brief  Convert a value to an rvalue.
*  @param  __t  A thing of arbitrary type.
*  @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&& 
    move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

using

命名空间

lambda

四、模版

五、标准库

string

c++联合体union中为什么不能用string类型_C++|野牛程序员 (yncoders.com)

六、C++标准

强制拷贝省略(C++17)

具体来说,C++17 中的 copy elision 规则包括以下几个方面:

  1. Named Return Value Optimization (NRVO,命名返回值优化):当使用命名的局部对象作为函数返回值时,编译器可以直接在函数的调用者中构造该对象,而不是在函数内部构造后再进行拷贝。这样可以避免不必要的拷贝操作。
  2. RVO (Return Value Optimization,返回值优化):当使用匿名对象作为函数返回值时,编译器可以直接在函数的调用者中构造该对象,而不是在函数内部构造后再进行拷贝。这也可以避免不必要的拷贝操作。
  3. Copy Elision in Initialization (初始化中的拷贝省略):当使用匿名对象初始化其他对象时,编译器可以直接在目标位置构造该对象,而不是先在匿名对象中构造后再进行拷贝。

这些优化规则可以显著提高程序的性能和效率,避免了不必要的拷贝操作。然而,需要注意的是,在某些情况下,编译器仍然可能执行拷贝构造,具体取决于编译器的实现和优化级别。

总之,C++17 对匿名对象的优化使得编译器可以在某些情况下省略拷贝构造,直接在目标位置构造对象,从而提高效率并减少不必要的拷贝操作。

-fno-elide-constructors 是一个编译选项,用于告诉编译器禁用拷贝省略优化。当使用该选项编译程序时,编译器将无视拷贝省略规则,强制执行拷贝构造或移动构造操作。

这个编译选项可以用于调试目的,或者在特定情况下需要确保对象的拷贝构造或移动构造函数被调用时使用。注意,禁用拷贝省略可能会导致性能下降,因为会引入不必要的拷贝操作。

需要注意的是,-fno-elide-constructors 是 GCC 和 Clang 编译器的选项,对于其他编译器可能有不同的选项名称或类似的功能。

以下程序在C++17及之后的版本中是可以正常编译的,C++17之前会编译错误:

#include<iostream>
 
class MyClass
{
public:
	MyClass() = default;
	MyClass(const MyClass&) = delete;
	MyClass(MyClass&&) = delete;
    static auto foo() { return  MyClass{}; }
};
int main()
{
	MyClass obj = MyClass::foo();
	return 0;
}

设计模式

​ 设计模式是一套被反复使用的代码设计经验的总结,是经过提炼的出色的设计方法,是程序员在长期的开发实践中总结出的一套提高开发效率的编程方法,是在特点问题发生时的可重用解决方案。设计模式代表了一些解决常见问题的通用做法,体现人们尝试解决某些问题时的智慧,所以它是一种强大的管理复杂度的工具。

​ 使用设计模式的主要目的是在设计大型项目时,保证所设计的模块之间的代码灵活性可复用性,但毫无疑问,这两个特性都以增加复杂性作为代价。

  • 模块之间的灵活性
    • 修改现有部分不会影响其他部分的内容(影响面尽可能窄或尽量将修改的代码集中在一起,不希望大面积修改)。
    • 增加新内容的时候尽量少甚至不需要改动系统现有的内容。
  • 可复用性:可以重复使用,可以到处使用(可以被很多地方调用)。

何时建议(鼓励)使用设计模式?

  1. 在大型项目和框架类项目中。
  2. 软件需求经常变化的场所。

前言

​ 与设计模式类似的有微服务架构设计模式:为了解决单一可执行程序过于复杂,过于庞大的问题,将这个单独的程序按照功能进行拆分,成为很多小的程序,彼此之间通过一些架构的方式配合实现业务需求。

​ 但是无论是微服务还是一个程序来实现业务都需要把每一个程序写好,其次并不是所有的业务都能通过微服务来解决。

抽象思维

先解释一下“耦合”与“解耦合”的概念。

耦合:两个模块相互依赖,修改其一,另一个也需要修改,这两者之间存在的互相影响的关系就称为两个模块之间存在耦合关系。

解耦合:通过修改程序,切断两个模块之间的依赖关系,使得对任意一个模块的修改都不会影响到另一个模块,这种行为被称为解耦合或称两个模块之间已经解耦合了。

设计模式,从某种角度来说的本质就是寻求模块之间的解耦(耦合度越低,就越容易专注地增加新需求和维护)。

所谓抽象思维,是指能从这个事物中提取出或者说提炼出一些本质、共性的内容,把这些共性的内容组合到一起(封装)

解决问题复杂性的方法:

  • 分解法:化繁为简,分而治之(把一个复杂的事物分解成若干比较简单的事物)。
  • 抽象法:从每个简单的事物中,抽象出本质的内容并封装起来。

毫无疑问,设计模式是很依赖抽象思维的,而==利用抽象思维的目的是:减少代码的重复性,方便代码的扩展。而这也是设计的原则和核心优点。==同样的,设计模式也有缺点:增加程序书写的复杂性、增加学习和理解的负担、会在一定程度上降低程序的运行效率。

那么如何检验某种抽象是否合理?

​ 抽象思维的能力因人而异,并且在一个项目中把哪些内容抽象出来封装成一个类是一件很主观的事情,没有统一的标准,因此我们需要有一个标准用来检验这个抽象是否合适。

  1. 当项目需求发生变更或增加,不更改现有代码,可以通过新增代码应对需求。
  2. 单一职责原则:一个类只干好一个事情(不要把毫不相干的代码都写到一个类中)。

软件开发思想

  1. 前期:做细致的需求分析以及架构设计这一行为的地位特别重要(提前为程序员提供开发指导、减少后期调整和改动的时间成本)。
  2. 设计阶段:发挥抽象思想,先划分模块,再抽象出类,最后确定类的接口。
  3. 在大型项目中时刻注意代码的可维护性和可扩展性(尽可能的降低模块之间的耦合度『解耦』)。

设计模式的分类

类型关注对象的释义
行为型模式行为和交互描述一组对象如何协作来完成一个整体任务。
创建型模式如何创建把对象的创建和使用相分离(解耦以提高维护)。
结构型模式结构关系如何组合对象以获得更加灵活的结构(获得灵活的程序结构以简化设计)。

杂项

基本用法

  • 范围for遍历二维数组
using int_arry = int[4];  //等价int_arry和int[4]等价

int ia[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
for (int_arry &row : ia)
{
    for (int i : row)
    {
        cout << ' ' << i << ends;
    }
}
  • 使用标准算法库查找容器中的元素

使用std::find配合运算符重载查找指定数据

std::find(this->typeInfoList.begin(), this->typeInfoList.end(), type);

// 需要为类重载==运算符
struct byTypeInfo
{
    int fd;                           // 文件描述符
    unsigned char type;               // 数据类型
    byTypeInfo() : fd(-1), type(0xff){};

    bool operator==(const unsigned char &x) const // 重载运算符 ==
    {
        return (this->type == x);
    }
};

使用std::find_if配合匿名函数或仿函数查找指定对象中的数据。

auto it = std::find_if(this->typeInfoList.begin(), this->typeInfoList.end(), 
                       [this, &type](const byTypeInfo &info)
                           { return info.type == type; });
  • std::underlying_type获取枚举类型的基类型
std::underlying_type<E>::type>
    
  • std::is_same<T, U> 用于判断 TU 这两个类型是否相等
if (std::is_same<decltype(x), float>::value)
    std::cout << "type x == float" << std::endl;
  • std::enable_if
/* 源码 */
template<bool B, class T>
struct enable_if {}; 
  
template<class T>
struct enable_if<true, T> { using type = T; };

// 用法
enable_if<bool, class>::type obj;
// 当enable_if的第一个模板参数为真时,obj的类型为class;当第一个模板参数为假时obj不会定义。

// 示例
/* 重载输出流运算符 */
template <typename T> 
std::ostream &operator<<(typename std::enable_if<std::is_enum<T>::value, std::ostream>::type &stream,
                         const T &e) noexcept
{
    return stream << static_cast<typename std::underlying_type<T>::type>(e);
}
  • 打印变量的真正类型7
template <typename T>
constexpr auto type_name() noexcept {
	std::string_view name = "Error: unsupported compiler", prefix, suffix;
#ifdef __clang__
	name = __PRETTY_FUNCTION__;
	prefix = "auto type_name() [T = ";
	suffix = "]";
#elif defined(__GNUC__)
	name = __PRETTY_FUNCTION__;
	prefix = "constexpr auto type_name() [with T = ";
	suffix = "]";
#elif defined(_MSC_VER)
	name = __FUNCSIG__;
	prefix = "auto __cdecl type_name<";
	suffix = ">(void) noexcept";
#endif
	name.remove_prefix(prefix.size());
	name.remove_suffix(suffix.size());
	return name;
}

// 用法
std::cout <<type_name<decltype(变量名)>()  << std::endl;

工具与管理

安装xmake:Installation - xmake

开箱即用的 C++多功能日志库 - 知乎 (zhihu.com)

使用spdlog:spdlog使用详细解析_Knowledgebase的博客-CSDN博客

spdlog开源链接:GitHub - gabime/spdlog: Fast C++ logging library.

cmake_minimum_required(VERSION 3.10)
project(szjcmk)

#cmakedefine AUTOMATIC_CAPACITY 

set(IS_MAKE_LIB ON) # 是否编译动态库

set(TARGET usrApp)  #生成可执行文件的名称

set(SRC_DIR src)    #源代码文件目录
set(LDIR lib)       #动态库依赖目录

# 设置编译器路径
set(COMPILER_PATH /home/daylong/project/10S-SPSJJLMK/SF/CPU/os/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf/bin)

# 设置编译器
set(CMAKE_COMPILER arm-linux-gnueabihf)

# 设置windows共享路径
set(WIN_PATH /home/daylong/windows/szjcImage)

# 设置项目编译选项
# add_compile_options(-Werror) # 警告当成错误
add_compile_options(-Wall -Wextra ) # 开启大部分警告
# add_compile_options(-pedantic)      # 开启严格的C++编译选项 
add_compile_options(-Warray-bounds) # 开启数组越界检查
add_compile_options(-Waddress)      # 开启地址越界检查
# add_compile_options(-Wconversion-extra) # 开启转换警告
# add_compile_options(-Wfloat-equal)  # 开启浮点数相等警告
add_compile_options(-Wuninitialized)    # 开启未初始化变量检查
add_compile_options(-Wunused-parameter) # 开启未使用参数检查
add_compile_options(-Wnonnull)          # 开启空指针检查

add_compile_options(-Wno-psabi)         # 忽略ABI更改
add_compile_options(-Wno-error=deprecated-declarations -Wno-deprecated-declarations)# 忽略已弃用的声明
add_compile_options(-Wno-type-limits)       # 忽略类型限制警告
# add_compile_options(-Wno-unused-variable)   # 忽略未使用的变量
add_compile_options(-Wno-format-overflow)   # 忽略格式溢出警告

add_compile_options(-D_FILE_OFFSET_BITS=64) # 在32位系统中使用64位文件大小

# 设置全局C++编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wold-style-cast") # 开启C++的旧式类型转换警告
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") 

# 设置默认构建类型为 Release
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g")# 设置Debug版本的编译选项
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -g")    # 设置Debug版本的编译选项

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Og")# 设置Release版本的编译选项
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3")    # 设置Release版本的编译选项

# set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")  # RelWithDebInfo 构建类型
# set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")  # MinSizeRel 构建类型


message(STATUS "Build type:                                       ${CMAKE_BUILD_TYPE}")
message(STATUS "C flags, Debug configuration:                     ${CMAKE_C_FLAGS_DEBUG}")
message(STATUS "C flags, Release configuration:                   ${CMAKE_C_FLAGS_RELEASE}")
# message(STATUS "C flags, Release configuration with Debug info:   ${CMAKE_C_FLAGS_RELWITHDEBINFO}")
# message(STATUS "C flags, minimal Release configuration:           ${CMAKE_C_FLAGS_MINSIZEREL}")
message(STATUS "C++ flags, Debug configuration:                   ${CMAKE_CXX_FLAGS_DEBUG}")
message(STATUS "C++ flags, Release configuration:                 ${CMAKE_CXX_FLAGS_RELEASE}")
# message(STATUS "C++ flags, Release configuration with Debug info: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
# message(STATUS "C++ flags, minimal Release configuration:         ${CMAKE_CXX_FLAGS_MINSIZEREL}")

# 设置交叉编译器路径
if(COMPILER_PATH)
    set(CMAKE_C_COMPILER ${COMPILER_PATH}/${CMAKE_COMPILER}-gcc)
    set(CMAKE_CXX_COMPILER ${COMPILER_PATH}/${CMAKE_COMPILER}-g++)
    set(CMAKE_STRIP ${COMPILER_PATH}/${CMAKE_COMPILER}-strip)
else()
    set(CMAKE_C_COMPILER ${CMAKE_COMPILER}-gcc)
    set(CMAKE_CXX_COMPILER ${CMAKE_COMPILER}-g++)
    set(CMAKE_STRIP ${CMAKE_COMPILER}-strip)
endif()

# 设置源代码文件
aux_source_directory(${SRC_DIR} SRC_FILES)

# 设置头文件路径
include_directories(
    inc
    lib/inc
    lib/inc/spdlog
    )

# 设置库文件路径
link_directories(${LDIR})

# 添加可执行文件
add_executable(${TARGET} ${SRC_FILES})

if(IS_MAKE_LIB)
    add_subdirectory(
        ${CMAKE_SOURCE_DIR}/user_so # 添加子目录
        ${CMAKE_BINARY_DIR}/user_so # 添加子目录编译后的目标文件目录
        )

    add_dependencies(${TARGET} fpga) # 添加依赖关系
endif()

# 添加链接库
target_link_libraries(${TARGET}
        fpga  # 添加libfpga.so链接库
        pthread  # 添加libpthread.so链接库
        )

# 设置输出目录
if(CMAKE_BUILD_TYPE MATCHES "Debug")
    set(EXECUTABLE_OUTPUT_PATH ./debug)
else()
    set(EXECUTABLE_OUTPUT_PATH ./release)
endif()
# set(EXECUTABLE_OUTPUT_PATH .)

# 添加链接前的自定义命令
if(CMAKE_BUILD_TYPE MATCHES "Debug")
    add_custom_command(TARGET ${TARGET}  # 指定目标
        PRE_BUILD  # 指定在编译之前执行
        COMMENT $<$<CONFIG:Debug> : "正在执行 ${EXECUTABLE_OUTPUT_PATH}/${TARGET} (Debug模式)..."
    )
else()
    add_custom_command(TARGET ${TARGET} PRE_BUILD
        COMMENT $<$<CONFIG:Release> : "正在执行 ${EXECUTABLE_OUTPUT_PATH}/${TARGET} (Release模式)..."
    )
endif()

# 编译完成后执行Shell命令
if(CMAKE_BUILD_TYPE MATCHES "Debug")
    add_custom_command(TARGET ${TARGET} POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different ${EXECUTABLE_OUTPUT_PATH}/${TARGET} ${WIN_PATH}
        COMMAND echo "[100%] COPY ${EXECUTABLE_OUTPUT_PATH}/${TARGET} to ${WIN_PATH}")
else()
    add_custom_command(TARGET ${TARGET} POST_BUILD
        COMMAND ${CMAKE_STRIP} ${EXECUTABLE_OUTPUT_PATH}/${TARGET}
        COMMAND echo "[100%] Strip ${EXECUTABLE_OUTPUT_PATH}/${TARGET}"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different ${EXECUTABLE_OUTPUT_PATH}/${TARGET} ${WIN_PATH}
        COMMAND echo "[100%] COPY ${EXECUTABLE_OUTPUT_PATH}/${TARGET} to ${WIN_PATH}")
endif()

# ${CMAKE_COMMAND} -E copy_if_different 是一个 CMake 提供的命令行工具,用于复制文件。这个命令会在编译过程中执行。

# ${CMAKE_COMMAND} 是一个 CMake 变量,它包含了 CMake 的执行路径。

# -E copy_if_different 是一个选项,表示执行复制操作,并且只会在源文件和目标文件不同时才进行复制。
# 这意味着如果源文件和目标文件相同,则不会执行复制操作,以避免重复复制。
cmake_minimum_required(VERSION 3.10)

set(TARGET fpga)

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src FPGA_SRCS)

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv7") # -march=armv7

#生成动态库
add_library(${TARGET} SHARED ${FPGA_SRCS})

#设置输出目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY  ${CMAKE_CURRENT_SOURCE_DIR}/lib)

# 编译完成后执行Shell命令
if(CMAKE_BUILD_TYPE MATCHES "Debug")
    add_custom_command(TARGET ${TARGET} POST_BUILD
        # 复制动态库及头文件到指定目录
        COMMAND ${CMAKE_COMMAND} -E copy_if_different lib${TARGET}.so ${CMAKE_SOURCE_DIR}/lib
        # COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/src/fpga.h ${CMAKE_SOURCE_DIR}/lib/inc

        # 复制动态库到共享目录
        COMMAND ${CMAKE_COMMAND} -E copy_if_different lib${TARGET}.so ${WIN_PATH}
        COMMAND echo "[----] COPY lib${TARGET}.so to ${WIN_PATH}")
else()
    add_custom_command(TARGET ${TARGET} POST_BUILD
    
        COMMAND ${CMAKE_STRIP} lib${TARGET}.so
        COMMAND echo "[----] Strip lib${TARGET}.so"
        # 复制动态库及头文件到指定目录
        COMMAND ${CMAKE_COMMAND} -E copy_if_different lib${TARGET}.so ${CMAKE_SOURCE_DIR}/lib
        # COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/src/fpga.h ${CMAKE_SOURCE_DIR}/lib/inc        

        # 复制动态库到共享目录
        COMMAND ${CMAKE_COMMAND} -E copy_if_different lib${TARGET}.so ${WIN_PATH}
        COMMAND echo "[----] COPY lib${TARGET}.so to ${WIN_PATH}")
endif()

远程连接VSCODE时,找不到头文件

在这里插入图片描述

链接

C++ STL学习之【优先级队列】_stl优先队列_北 海的博客-CSDN博客

c++ fstream

std::future<bool> Network::futureObj = Network::writeSignal.get_future(); // 绑定

第 2 章 语言可用性的强化 现代 C++ 教程: 高速上手 C++ 11/14/17/20 - Modern C++ Tutorial: C++ 11/14/17/20 On the Fly (changkun.de)

注译

参考文献

文章目录


  1. 序言 现代 C++ 教程: 高速上手 C++ 11/14/17/20 - Modern C++ Tutorial: C++ 11/14/17/20 On the Fly (changkun.de) ↩︎

  2. 什么是完整的RVO以及NRVO过程? - 知乎 (zhihu.com) ↩︎

  3. 程序的行为因编译器的实现而出现差异。 ↩︎

  4. c++模板_c++ 模板-CSDN博客 ↩︎

  5. 【精选】C++模板编程之变长参数模板_使用c语言实现 c++变长参数模板-CSDN博客 ↩︎ ↩︎

  6. 【翻译】折叠表达式实用技巧 - 知乎 (zhihu.com) ↩︎

  7. 取代typeid打印变量类型:是否可能在标准变量中打印变量的类型C++? ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值