文章目录
😍现代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++中文 - API参考文档 (apiref.com)
序言 现代 C++ 教程: 高速上手 C++ 11/14/17/20 - Modern C++ Tutorial: C++ 11/14/17/20 On the Fly (changkun.de)
问答:
C++面试-基础篇(全网最详细) - 知乎 (zhihu.com)
1.0 开始
1.0.1 关键字
绿色表示C++11增加的关键字。蓝色表示C++20增加的关键字。
类型 | 关键字 | 作用 | 备注 |
---|---|---|---|
控制流 | if |
条件语句中的条件判断 | 在C++17后可以在if 语句中定义局部对象。 |
控制流 | else |
条件语句中的否定分支 | |
控制流 | switch |
多分支条件选择语句 | C++17后可以在case 中直接定义局部对象(在C中需要使用{})。 |
控制流 | case |
switch 中的分支标签 |
|
控制流 | default |
switch 中的默认分支 |
在switch表达式匹配不到任何case 时,匹配default 。 |
循环 | while |
||
循环 | do |
do-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_t | 16 位字符类型 | 由于wchar_t在不同系统环境字节数不一样,所以为了统一而出现。 |
数据类型 | char32_t | 32 位字符类型 | |
数据类型 | 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 |
常类型转换 | 只能添加或删除const 或 volatile 属性。 |
类型转换 | dynamic_cast |
动态类型转换 | 用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。 |
类型转换 | reinterpret_cast |
重新解释转换 | 它会忽略类型之间的任何关联关系,通过重新解释内存内容来进行转换。但它却无法丢弃const 或 volatile 属性。(自由度最高,风险性最高) |
异常处理 | 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 | 编译期初始化 | 修饰变量,保证变量在编译期初始化。(只能使用 constexpr 或consteval 函数初始化 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++ 语言的发展历史可以分为以下几个阶段:
- C++ 1.0(1985年):作为 C 语言的扩展,加入了类、继承、多态等面向对象编程特性。
- C++ 2.0(1989年):加入了模板、异常处理、命名空间等新特性,并对标准库进行了重大改进。
- C++ 3.0(1991年):加入了运行时类型识别(RTTI)、抽象类、虚基类等特性,并且对异常机制进行了改进。
- C++ 11(2011年):加入了自动类型推导、lambda 表达式、右值引用、智能指针、线程库等新特性,并且对标准库进行了扩展。
- C++ 14(2014年):修复了一些 C++ 11 的问题,并加入了二进制字面量、泛型 Lambda 表达式、constexpr 函数等新特性。
- C++ 17(2017年):加入了结构化绑定、fold 表达式、if constexpr 等新特性,并对标准库进行了扩展。
- 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_ptr
和std::unique_ptr
等智能指针类,用于管理动态分配的内存。 - 新的标准库组件:如并发编程库、正则表达式库、原子操作等。
C++ 14:
- 二进制字面量(binary literals):引入了以
0b
或0B
开头的二进制表示方法,例如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++98 | auto , 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++03 | bool , mutable , namespace , reinterpret_cast , static_cast , typename |
C++11 | alignas , alignof , char16_t , char32_t , constexpr , decltype , noexcept , nullptr , static_assert , thread_local |
C++14 | 无新增关键字 |
C++17 | if constexpr , inline variables |
C++20 | concept , 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以前,人们说的初始化列表都是构造函数中的冒号初始化。
-
std::initializer_list
:std::initializer_list
是C++11引入的一种特殊容器类型,它用于将一组值作为参数传递给函数或对象的构造函数。这种初始化列表使用花括号{}
来表示,并且是只读的,不能修改其中的值。例如:std::initializer_list<int> numbers = { 1, 2, 3};
-
冒号初始化列表:在类的构造函数中,可以使用冒号初始化列表来初始化成员变量。这种初始化列表出现在构造函数的定义之后,冒号之后,用于对成员变量进行初始化。例如:
class MyClass { public: MyClass(int number) : m_number(number) { } private: int m_number; };
1.1 特点
C++不是C的一个超集(增强版)。
C++11 语言核心的改进中,最为关注的有 rvalue reference (这里),lambda,variadic template。
-
C++ 不允许直接将
void *
隐式转换到其他类型的指针(C可以)。 -
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; }
-
乱序指定初始化、嵌套指定初始化、指定初始化器和正则初始化器的混合以及数组的指定初始化在C编程语言中都受支持,但在C++中不允许。
1.2 名词解释
-
静态类型语言:变量和表达式的类型必须在编译时声明,编译器可以帮助我们提前避免程序在运行期间有可能因为类型发生的一些错误。
-
强类型语言:不会处理与类型定义明显矛盾的运算,而是把它标记为一个问题,并作为错误抛出。
-
数组退化:当数组作为实参进行传递时会自动退化为指针(是一种隐式转换),传入数组的首地址。数组退化成指针后其「类型」和「大小信息」将会丢失。
-
窄化转换:出现在强制转换中,表示对算术值出现了有精度的转换,不能完全表达转换之前所代表的值。
-
谓词(谓语):你之前一定在英语语法课上听过最基本的句式结构是:主谓宾,而谓语就是可以具有“动词”语义的词。其实在C++中也存在这样的谓词,函数就是典型的谓词语义。STL中的谓词类似这样:
bool func(T&a);
或者bool func(T&a,T&b);
常见的谓词:函数、函数指针、函数对象、lambda表达式,库定义的函数对象。如std::find_if()
,std::count_if()
等函数需要传入bool型的判断条件参数。 -
隐式声明:函数或对象仅有定义而没有进行声明;函数的定义在库(静态库或动态库中),但没有引入头文件。
-
匿名对象:不具有名称的对象。产生这种对象的方法通常是这样的:
int(3)
。 -
RVO(返回值优化):编译器可以减少函数返回时生成匿名对象的个数,从某种程度上可以提高程序的运行效率,对需要分配大量内存的类对象其值复制过程十分友好。
-
NRVO(具名返回值优化)2:NRVO优化的是返回指的对象在
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
}
-
CV限定符:是 const 和 volatile 关键字的统称。
-
重载决议:是指编译器在调用函数或运算符时,根据参数类型、数量和顺序来选择最佳匹配的重载函数或运算符的过程。C++ 使用重载决议来确定应该调用哪个重载函数或运算符。如果没有找到完全匹配的函数或运算符,编译器将进行一系列的重载解析规则,以确定最佳匹配。
-
右值:指的是值可以被获取但不具有持久性的表达式。例如,常量、临时变量、字面值都是右值。右值可以作为函数的参数或返回值,但不能被赋值。是一个广义的概念,包括纯右值和将亡值。
以下举例左值和右值的区别。简单来说,右值不可以被赋值,左值可以取地址。
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 是纯右值。 */
-
纯右值:是没有名称、不可寻址的表达式,通常是临时对象或字面量。
-
将亡值:是即将被销毁并且可以转移所有权的表达式,是特殊的右值。
C++11引入的新概念,指的是即将被销毁并且可以转移其所有权的表达式,通常使用
std::move()
进行转移。将亡值通常是右值引用类型的变量、函数返回值或强制类型转换后的表达式。C++11中的将亡值是随着右值引用的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关。所谓的将亡值表达式,就是下列表达式:
- 返回右值引用的函数的调用表达式。
- 转换为右值引用的转换函数的调用表达式。
-
右值引用:一种引用类型,用于绑定临时对象或将所有权转移给另一个对象。右值引用使用"
&&
"符号表示。 -
悬垂引用:是指一个引用在其所引用的对象被销毁后依然存在。当一个对象被销毁时,与之相关的引用仍然存在,但指向的对象已经不存在了。使用悬垂引用是一种严重的错误,因为访问悬垂引用可能会导致未定义的行为。这是因为悬垂引用指向的内存可能已经被其他对象或数据覆盖,或者已经被释放回操作系统。
-
完美转发:是一种C++特性,模板函数可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变(保留原始参数的值和类型)。
-
引用折叠:
-
万能引用:
-
移动语义:将资源从一个对象转移到另一个对象,而不需要进行深拷贝操作。这对于管理动态内存或大型对象的效率非常重要。
-
RTTI :(Run-Time Type Identification)运行时类型识别(RTTI)是一种机制,**用于在程序运行时获取和操作对象的类型信息。**主要有两种形式:
dynamic_cast
和typeid
运算符。注意,使用RTTI可能会导致运行时开销,因此需要谨慎使用。dynamic_cast
用于在运行时将指向基类的指针或引用转换为派生类的指针或引用。如果转换成功,则返回指向派生类的指针或引用;如果转换失败,则返回空指针(对指针进行转换)或引发std::bad_cast
异常(对引用进行转换)。typeid
运算符:typeid
用于在运行时获取对象的类型信息。它返回一个std::type_info
对象,表示对象的实际类型。
-
常量求值语境:指在表达式中某值必须为常量的情况。
// 在常量求值语境时,不同版本的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():... ...
-
立即函数:被关键字
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())
已经限定了常量语境了,为何还不能调用立即函数?但是constexpr
和consteval
的底层机理并不一致,所以并不相通。因此为解决这个问题,C++23 新增
if consteval { }
来代替if(std::is_constant_evaluated())
。 -
默认参数提升:说的是参数提升的行为是被编译器默认的,而不是默认参数的提升。是指在函数调用时,当实参与函数原型中的形参类型不完全匹配时,编译器会向上进行隐式类型转换的过程。
链接中详细提到了默认参数提升的细节:可变长参数列表误区与陷阱——va_arg不可接受的类型_c\c++ va_arg 用法不正确
float
类型的实际参数将提升到double
char
、short
和相应的signed
、unsigned
类型的实际参数提升到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
-
ODR:
-
非类类型:指的是不是类(class)类型的类型,例如基本数据类型(如
int
、float
、char
等)和枚举类型。这些类型在 C++ 中并没有封装成类(class)的形式,因此被称为非类类型。
1.3 约定
使用C++的规则
-
尽量不要使用
usign namespace std;
-
尽量使用{}初始化对象(能够使编译器检测到窄化转换)。
-
优先选择任务而不是线程。
-
优先使用智能指针,而不是原始指针。
-
不使用可能因未定义行为产生歧义的算法(未定义行为:语言标准未做规定的行为。例如:函数实参和同类运算符的计算顺序等)。
意图规则
- 在代码中直接表达思想(通过成员方法名称、返回值类型和STL算法等直接的表达意图)。
- 使用ISO C++标准编写代码(减少使用编译器扩展的C++功能,因为能够减少实现定义行为3)。
1.4 被弃用的特性
被弃用不代表不能使用,而是暗示程序员这些特性在未来的标准中消失,应该避免使用(但是出于兼容性的考虑,大部分特性其实会被永久保留)。
- 不允许将字符串字面值常量赋值给
char *
(如:char *str="hello"
),而是使用const char *str="hello"
。 - C++98 异常说明、
unexpected_handler
、set_unexpected()
等相关特性被弃用,应该使用noexcept
。 auto_ptr
被弃用,应使用unique_ptr
。register
关键字被弃用,可以使用但不再具备任何实际含义。bool
类型的++
操作被弃用。- 为存在析构函数的类自动生成拷贝构造函数和拷贝赋值运算符的特性被弃用。
- C 语言风格的类型转换被弃用(即在变量前使用
(convert_type)
),应该使用static_cast
、reinterpret_cast
、const_cast
来进行类型转换。 - 特别地,在 C++17 标准中弃用了一些可以使用的 C 标准库,例如
<ccomplex>
、<cstdalign>
、<cstdbool>
与<ctgmath>
等。 - 分离编译模式无法对模板进行,标准 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++ 会把 NULL
、0
视为同一种东西,这取决于编译器如何定义 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]:
- 函数体允许声明变量,除了没有初始化、static和thread_local变量。
- 函数允许出现if和switch语句,不能使用goto语句。
- 函数允许所有的循环语句,包括for、while、do-while。
- 函数可以修改生命周期和常量表达式相同的对象(函数外部对象不能修改)。
- 函数的返回值可以声明为void。
- constexpr声明的成员函数不再具有const属性。
另外还有些不允许的:
- 它必须非虚; [c++20前]
- 它的函数体不能是函数 try 块; [c++20前]
- 它不能是协程; [c++20起]
- 对于构造函数与析构函数 [C++20 起],该类必须无虚基类
- 它的返回类型(如果存在)和每个参数都必须是字面类型 (LiteralType)
- 至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函 数为足以用于常量初始化器) (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 引入了
auto
和decltype
这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 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