1、参考引用
- C++高级编程(第4版,C++17标准)马克·葛瑞格尔
2、建议先看《21天学通C++》 这本书入门,笔记链接如下
- 21天学通C++读书笔记(文章链接汇总)
1. 引用
- 在 C++ 中,引用是另一个变量的别名,对引用的所有修改都会改变被引用的变量的值
- 可将引用当作隐式指针,这个指针没有取变量地址和解除引用的麻烦
- 也可将引用当作原始变量的另一个名称
- 可创建单独的引用变量,在类中使用引用数据成员,将引用作为函数和方法的参数,也可让函数或方法返回引用
1.1 引用变量
- 引用变量在创建时必须初始化
int x = 3; int &xRef = x; // xRef 是 x 的另一个别名 xRef = 10; // 对 xRef 赋值会改变 x 的值int &emptyRef; // 错误,必须初始化
- 不能创建对未命名值 (例如一个整数字面量) 的引用,除非这个引用是一个 const 值
int &unnamedRef1 = 5; // 无法编译 const int &unnamedRef2 = 5; // 正常编译
- 不能具有临时对象的非 const 引用,但可具有 const 引用
std::string getString() {return "Hello world!"; }std::string &string1 = getString(); // 无法编译 const std::string &string2 = getString(); // 正常编译
1.1.1 修改引用
- 引用总是引用初始化的那个变量,引用一旦创建,就无法修改
int x = 3, y = 4; int &xRef = x; xRef = y; // 将 y 的值 4 赋给 x,引用不会更新为指向 y
1.1.2 指向指针的引用和指向引用的指针
- 可创建任何类型的引用,包括指针类型
int* intP; int* &ptrRef = intP; // ptrRef 是一个指向 intP 的引用,intP 是一个指向 int 值的指针 ptrRef = new int; *ptrRef = 5;
- 对引用取地址的结果与对被引用变量取地址的结果相同
- xPtr 是一个指向 int 值的指针,xRef 是一个指向 int 值的引用
int x = 3; int &xRef = x; int *xPtr = &xRef; // 取 x 引用(xRef)的地址,使 xRef 指向 x *xPtr = 100; // x 的值也变为 100
注意:无法声明引用的引用或者指向引用的指针,例如,不允许使用 int&& 或 int&*
1.2 引用数据成员
- 类的数据成员可以是引用
- 如果不指向其他变量,引用就无法存在
- 因此必须在构造函数初始化器中初始化引用数据成员,而不是在构造函数体内
class MyClass { public:MyClass(int &ref) : mRef(ref) {} private:int &mRef; };
1.3 引用参数
-
默认的参数传递机制是按值传递:函数接收参数的副本,修改这些副本时,原始的参数保持不变
-
引用允许指定另一种向函数传递参数的语义:按引用传递。当使用引用参数时,函数将引用作为参数。如果引用被修改,最初的参数变量也会被修改。例如,下面给出了一个简单的交换函数,交换两个 int 变量的值
- 当使用 x 和 y 作为参数调用函数 swap() 时,first 参数被初始化为 x 的引用,second 参数被初始化为 y 的引用。当使用 swap() 修改 first 和 second 时,x 和 y 实际上也被修改
void swap(int &first, int &second) {int temp = first;first = second;second = temp; }int x = 5, y = 6; swap(x, y);
-
就像无法用常量初始化普通引用变量一样,不能将常量作为参数传递给 “按非 const 引用传递” 的函数
swap(3, 4); // 编译错误
-
将指针转换为引用
- 某个函数或方法需要以一个引用作为参数,而你拥有一个指向被传递值的指针,此时可对指针解除引用,将指针 “转换” 为引用。这一行为会给出指针所指的值,随后编译器用这个值初始化引用参数。例如,可以这样调用 swap()
int x = 5, y = 6; int *xp = &x, *yp = &y; swap(*xp, *yp);
1.4 将引用作为返回值
- 还可让函数或方法返回一个引用,这样做的主要原因是为了提高效率。返回对象的引用而不是返回整个对象可避免不必要的复制。当然,只有涉及的对象在函数终止之后仍然存在的情况下才能使用这一技巧
- 若变量的作用域局限于函数或方法 (例如堆栈中自动分配的变量,在函数结束时会被销毁),绝不能返回这个变量的引用
1.5 右值引用
- 右值就是非左值,例如常量值、临时对象或值。通常而言,右值位于赋值运算符的右侧
void handleMessage(std::string &&message) {cout << "handleMessage with rvalue reference: " << message << endl; }handleMessage("Hello World"); std::string a = "Hello "; std::string b = "World"; handleMessage(a + b);
1.6 使用引用还是指针
- 在 C++ 中,可认为引用是多余的:几乎所有使用引用可以完成的任务都可以用指针来完成
- 但是,引用能使程序更容易理解,且不存在无效引用,也不需要显式解除引用
- 也有一些情况要求使用指针,一个例子是更改指向的位置,因为无法改变引用所指的变量
- 动态分配内存时,应该将结果存储在指针而不是引用中
- 需要使用指针的另一种情况是可选参数,即指针参数可以定义为带默认值 nullptr 的可选参数,而引用参数不能这样定义,还有一种情况是要在容器中存储多态类型
- 有一种方法可以判断使用指针还是引用作为参数和返回类型:考虑谁拥有内存
- 如果接收变量的代码负责释放相关对象的内存,那么必须使用指向对象的指针,最好是智能指针,这是传递拥有权的推荐方式
- 如果接收变量的代码不需要释放内存,那么应该使用引用
要优先使用引用。也就是说,只有在无法使用引用的情况下,才使用指针
2. 关键字的疑问
2.1 const 关键字
2.1.1 const 变量和参数
-
可使用 const 来 “保护” 变量不被修改。这个关键字的一个重要用法是替换 #define 来定义常量
const double PI = 3.1415926535;
-
还可使用 const 指定函数或方法的参数保持不变
void func(const int param) {// ... }
-
const 指针
- 为阻止修改所指的值(常量指针)
int const *ip; ip = new int[10]; ip[4] = 5; // 编译错误,无法修改 ip 所指的值
- 为阻止修改指针的指向(指针常量)
int* const ip = nullptr; ip = new int[10]; // 编译错误,无法修改 ip 的指向
- 为阻止修改所指的值和阻止修改指针的指向
- 第一个 const 位于 int 的右边,因此将 const 应用到 ip 所指的 int,从而指定无法修改 ip(指针)所指向的值
- 第二个 const 位于 * 的右边,因此将 const 应用于指向 int 变量的指针,也就是 ip 变量。因此,无法修改 ip(指针)本身指向
int const * const ip = nullptr;
-
const 引用
- 引用默认为 const,因为无法改变引用所指的对象,不必显式地将引用标记为 const
- 无法创建指向引用的引用,所以引用通常只有一层间接取值
int z; const int &zRef = z; zRef = 4; // 编译错误,由于将 const 应用到 int,因此无法对 zRef 赋值 z = 5; // 但仍然可以修改 z 的值,直接改变 z 而不是通过引用
-
const 引用经常用作参数。如果为了提高效率,想按引用传递某个值,但不想修改这个值,可将其标记为 const 引用
void doSomething(const BigClass &arg) { // 通常默认为 const 引用// ... }
2.1.2 const 方法
- 可将类方法标记为 const,以禁止方法修改类的任何非可变数据成员
2.1.3 constexpr 关键字
- C++ 在某些情况下需要常量表达式。例如当定义数组时,数组的大小就必须是一个常量表达式
const int getArraySize() {return 32; }int main() {int myArray[getArraySize()]; // 无效return 0; }
constexpr int getArraySize() { // 常量表达式return 32; }int main() {int myArray[getArraySize()]; // okreturn 0; }
2.2 static 关键字
2.2.1 静态数据成员和方法
- 可声明类的静态数据成员和方法。静态数据成员与非静态数据成员不同,它们不是对象的一部分。相反,这个数据成员只有一个副本,这个副本存在于类的任何对象之外
2.2.2 静态链接
-
C++ 的每个源文件都是单独编译的,编译得到的目标文件会彼此链接。C++ 源文件中的每个名称,包括函数和全局变量,都有一个内部或外部的链接
- 外部链接意味着这个名称在其他源文件中也有效,内部链接(也称为静态链接)意味着在其他源文件中无效
- 默认情况下,函数和全局变量都拥有外部链接。然而,可在声明的前面使用关键字 static 指定内部(或静态)链接。例如,假定有两个源文件 FirstFile.cpp 和 AnotherFile.cpp。这两个源文件都可成功编译,程序链接也没有问题:因为 f() 函数具有外部链接,main() 函数可从另一个文件调用这个函数
- 现在假定在 AnotherFile.cpp 中将 static 应用到 f() 函数原型,不需要在 f() 函数定义前重复使用 static 关键字。现在每个源文件都可成功编译,但链接时将失败,因为 f() 函数具有内部(静态)链接,FirstFile.cpp 无法调用
// FirstFile.cpp void f(); // 提供了原型但没给出定义int main() {f(); // f() 函数默认具有外部链接,可从 AnotherFile.cpp 调用该函数return 0; }
// AnotherFile.cpp #include <iostream> void f(); // 提供了原型和定义 // static void f(); // 此时 FirstFile.cpp 无法调用,因为 f() 为静态链接void f() {std::cout << "f\n"; }
-
将 static 用于内部链接的另一种方式是使用匿名名称空间,可将变量或函数封装到一个没有名字的名称空间,而不是使用 static,如下所示
- 在同一源文件中,可在声明匿名名称空间之后的任何位置访问名称空间中的项,但不能在其他源文件中访问,这一语义与 static 关键字相同
- 要获取内部链接,建议使用匿名名称空间,而不要使用 static 关键字
#include <iostream>namespace {void f();void f() {std::cout << "f\n";} }
-
extern 关键字
- extern 关键字是 static 的反义词,将它后面的名称指定为外部链接。某些情况下可使用这种方法。例如,const 和 typedef 在默认情况下是内部链接,可使用 extern 使其变为外部链接
2.2.3 函数中的静态变量
- C++ 中 static 关键字的最终目的是创建离开和进入作用域时都可保留值的局部变量。函数中的静态变量就像只能在函数内部访问的全局变量。静态变量最常见的用法是 “记住” 某个函数是否执行了特定的初始化操作
void performTask() {static bool initialized = false;if (!initialized) {cout << "initialized" << endl;initialized = true;}// 执行期望的任务 }
- 应该避免使用单独的静态变量,为了维持状态可以改用对象
3. 类型和类型转换
3.1 类型别名
- 类型别名为现有的类型声明提供了新名称,使用新类型名称创建的变量与使用原始类型声明创建的变量完全兼容
using IntPtr = int*;
- 类型别名最常见的用法是:当实际类型的声明过于笨拙时,提供易于管理的名称,这一情形通常出现在模板中
using StringVector = std::vector<std::string>; // 类型别名可包括作用域限定符
3.2 函数指针的类型别名
- 通常不考虑函数在内存中的位置,但每个函数实际上都位于某个特定地址。在 C++ 中,可像使用数据那样使用函数。换言之,可使用函数的地址,就像使用变量那样
- 函数指针的类型取决于兼容函数的参数类型的返回类型。处理函数指针的一种方式是使用类型别名。类型别名允许将一个类型名指定给具有指定特征的一系列函数。例如,下面的代码行定义了 MatchFunction 类型,该类型表示一个指针,这个指针指向具有两个 int 参数并返回布尔值的任何函数
using MatchFunction = bool(*)(int, int);void findMatches(int values1[], int values2[], size_t numValues, MatchFunction matcher) {for (size_t i = 0; i < numValues; i++) {if (matcher(values1[i], values2[i])) {cout << "Match found at position " << i << " (" << values1[i] << ", " << values2[i] << ")" << endl;}} }
- 由于 intEqual() 函数与 MatchFunction 类型匹配,可将其作为 findMatches() 的最后一个参数进行传递
bool intEqual(int num1, int num2) {return num1 == num2; }int arr1[] = {2, 5, 6, 9, 10, 1, 1}; int arr2[] = {4, 4, 2, 9, 0, 3, 4}; size_t arrSize = std::size(arr1); // C++17 前用法:sizeof(arr1) / sizeof(arr1[0]); cout << "Calling findMatches() using intEqual():" << endl; findMatches(arr1, arr2, arrSize, &intEqual);
// 结果输出 Calling findMatches() using intEqual(): Match found at position 3 (9, 9)
- 如果不使用这些旧式的函数指针,还可以使用 std::function
- 尽管在 C++ 中函数指针并不常见 (被 virtual 关键字替代),但在某些情况下还是需要获取函数指针,或许在动态链接库中获取函数指针是最常见的示例
3.3 方法和数据成员的指针的类型别名
- 方法和数据成员的指针通常不会出现在程序中。然而要记住,不能在没有对象的情况下解除对非静态方法或数据成员的指针的引用
3.4 typedef
- 类型别名和 typedef 并非完全等效。与 typedef 相比,类型别名与模板一起使用时功能更强大
- 始终优先使用类型别名而非 typedef
using IntPtr = int*; typedef int* IntPtr; // 可读性较差using FunctionType = int(*)(char, double); typedef int(*FunctionType)(char, double); // 非常复杂
3.5 类型转换
3.5.1 const_cast()
- const_cast() 可用于给变量添加常量特性,或去掉变量的常量特性
- 从理论上讲,并不需要 const 类型转换:如果某个变量是 const,那么应该一直是 const。然而实际中,有时某个函数需要采用 const 变量,但必须将这个变量传递给采用非 const 变量作为参数的函数
extern void ThirdPartyLibraryMethod(char* str);void f(const char* str) {ThirdPartyLibraryMethod(const_cast<char*>(str)); }
3.5.2 static_cast()
- 可使用 static_cast() 显式地执行 C++ 语言直接支持的转换
int i = 3; int j = 4; // 只需要把两个操作数之一设置为 double,就可确保执行浮点数除法 double result = static_cast<double>(i) / j;
- static_cast() 的另一种用法是在继承层次结构中执行向下转换
- 这种类型转换可以用于指针和引用,而不适用于对象本身
- static_cast() 类型转换不执行运行期间的类型检测
class Base { public:virtual ~Base() = default; };class Derived : public Base { public:virtual ~Derived() = default; };int main() {Base* b;Derived* d = new Derived();b = d;d = static_cast<Derived*>(b); // 需要向下转换Base base;Derived derived;Base& br = derived;Derived& dr = static_cast<Derived&>(br);return 0; }
- static_cast() 无法将某种类型的指针转换为不相关的其他类型的指针
- 如果没有可用的转换构造函数,static_cast() 无法将某种类型的对象直接转换为另一种类型的对象
- static_cast() 无法将 const 类型转换为非 const 类型,无法将指针转换为 int
3.5.3 reinterpret_cast()
- reinterpret_cast() 的功能比 static_cast() 更强大,同时安全性更差。可以用它执行一些在技术上不被 C++ 类型规则允许,但在某些情况下程序员又需要的类型转换
- 例如,可将某种引用类型转换为其他引用类型,即使这两个引用并不相关
- 同样,可将某种指针类型转换为其他指针类型,即使这两个指针并不存在继承层次上的关系。这种用法经常用于将指针转换为 void*,这可隐式完成,不需要进行显式转换。但将 void* 转换为正确类型的指针需要 reinterpret_cast()。void* 指针指向内存的某个位置。void* 指针没有相关的类型信息
class X {}; class Y {};int main() {X x;Y y;X* xp = &x;Y* yp = &y;xp = reinterpret_cast<X*>(yp);void* p = xp;xp = reinterpret_cast<X*>(P);X& xr = x;Y& yr = reinterpret_cast<Y&>(x);return 0; }
使用 reinterpret_cast() 时要特别小心,因为在执行转换时不会执行任何类型检测
3.5.4 dynamic_cast()
- dynamic_cast() 为继承层次结构内的类型转换提供运行时检测。可用它转换指针或引用。dyamic_cast() 在运行时检测底层对象的类型信息,如果类型转换没有意义,dymamic_cast() 将:
- 返回一个空指针(用于指针)
- 抛出 std::bad_cast 异常(用于引用)
class Base { public:virtual ~Base() = default; };class Derived : public Base { public:virtual ~Derived() = default; };
Base* b; Derived* d = new Derived(); b = d; d = dynamic_cast<Derived*>(b);
运行时类型信息存储在对象的虚表中。因此,为使用 dynamic_cast(),类至少要有一个虚方法。如果类不具有虚表,尝试使用 dynamic_cast() 将导致编译错误
4. 作用域解析
- 程序中的所有名称,包括变量、函数和类名,都具有某种作用域。可使用名称空间、函数定义、花括号界定的块和类定义创建作用域
#include <iostream> #include <memory>class Demo { public:static int get() {return 5;} };int get() {return 10; }namespace NS {int get() {return 20;} }int main() {auto pd = std::make_unique<Demo>();Demo d;std::cout << pd->get() << " ";std::cout << d.get() << " ";std::cout << NS::get() << " ";std::cout << Demo::get() << " ";std::cout << ::get() << " ";std::cout << get() << " "; }
// 输出结果 5 5 20 5 10 10
5. 特性
5.1 [[noreturn]] 特性
5.2 [[deprecated]] 特性
5.3 [[fallthrough]] 特性
5.4 [[nodiscard]] 特性
5.5 [[maybe_unused]] 特性
5.6 供应商专用特性
6. 用户定义的字面量
-
C++ 有许多可在代码中使用的标准字面量
- ‘a’:字符
- “character array”:以 \0 结尾的字符数组(C风格的字符串)
- 3.14f:浮点数
- 0xabc:十六进制值
-
C++11 允许定义自己的字面量
- 用户定义的字面量应该以下划线开头,下划线之后的第一个字符必须小写
- 例如:_i、_s、_km 和 _miles
-
C++ 定义了如下标准的用户定义字面量
注意,这些标准的用户定义字面量并非以下画线开头
- “s” 用于创建 std::string
auto myString = "Hello World"s; // 需要 using namespace std::string_literals;
- “sv” 用于创建 std::string_view
auto myStringView = "Hello World"sv; // 需要 using namespace std::string_view_literals;
- “h” “min” “s” “ms” “us” “ns” 用于创建 std::chrono::duration 时间段
auto myDuration = 42min; // 需要 using namespace std::chrono_literals;
- “i” “il” “if” 分别用于创建复数 complex<double>、complex<long double> 和 complex<float>
auto myComplexNumber = 1.3i; // 需要 using namespace std;
- “s” 用于创建 std::string
7. 头文件
- 头文件是为子系统或代码段提供抽象接口的一种机制
- 使用头文件需要注意的一点是:要避免循环引用或多次包含同一个头文件
- 例如,假设 A.h 包含 Logger.h,里面定义了一个 Logger 类;B.h 也包括 Logger.h。如果有一个源文件 App.cpp 包含 A.h 和 B.h,最终将得到 Logger 类的重复定义,因为 A.h 和 B.h 都包含 Logger.h 头文件
- 可使用文件保护机制来避免重复定义
#ifndef LOGGER_H #define LOGGER_Hclass Logger {// ... };#endif
- 也可使用 #pragma once 指令替代前面的文件保护机制
#pragma onceclass Logger {// ... };
- 前置声明是另一个避免产生头文件问题的工具。如果需要使用某个类,但是无法包含它的头文件(例如,这个类严重依赖当前编写的类),就可告诉编译器存在这么一个类,但是无法使用 #include 机制提供正式的定义
- 在代码中无法真正地使用这个类,因为编译器对此一无所知,只知道在链接之后存在这个已命名的类
- 但是仍可在代码中使用这个类的指针或引用
- 也可声明函数,使其按值返回这种前置声明类,或将这种前置声明类作为按值传递的函数参数。当然,定义函数的代码以及调用函数的任何代码都需要添加正确的头文件,在头文件中要正确定义前置声明类
- 例如,假设 Logger 类使用另一个类 Preferences。Preferences 类又使用 Logger 类,由于产生了循环依赖,因此无法使用文件保护机制来解决,此时需要使用前置声明
#pragma once #include <string_view>class Preferences; // 前置声明class Logger { public:static void setPreferences(const Preferences& prefs);static void logError(std::string_view error); };