主页
搜索
最近更新
数据统计
赞助我们
系统公告
1
/
1
请查看完所有公告
C++ 基础知识
最后更新于 2025-06-16 17:24:27
作者
normalpcer
分类
个人记录
复制 Markdown
查看原文
更新内容
C++ 是许多信息学竞赛选手最熟悉的编程语言。日常训练中,我们可能只用到循环、数组这些基础功能,但这位朝夕相处的"老朋友",其实藏着更多值得探索的奥秘。 也许你曾见过题解中神奇的语法"黑科技",也许你被未定义行为导致的"玄学问题"困扰过,也许你面对突如其来的编译错误百思不得其解... 掌握这些知识,不会让你在赛场多拿几分,但能让你更加了解这个朝夕相处的代码伙伴。它们或许能帮你理解那些精妙的语言特性,或许能让你接触更现代的编程思维,又或许,只是满足你对技术世界的好奇心。 这个专栏并不是“语法大师课”,而是一次共同探索的旅程。我也只是一个 C++ 初学者,尝试分享自己理解中的一些点滴。 我们将从基础出发,逐步走进 C++ 的深处。虽然我的理解有限,无法覆盖那些艰深的内容。无论如何,都希望这些分享能为你打开一扇窗,了解一些可能平时了解不到的小知识,让你对这门语言多一分理解,这便是这篇专栏的最大意义。 <!-- 这篇专栏将会分为几个部分。 第一个部分,我们将会以一个“C++ 入门指南”为脉络,讲述一些较为基础且比较常用的知识。 --> # 前置知识 在正式开始之前,先来了解一些相关的概念。 ## 编程工具 ### 编译器 编译器是一种软件,负责将源代码编译成可执行文件。可执行文件(例如 Windows 系统的 exe 文件)可以被操作系统直接执行。 GCC(GNU Compiler Collection)是算法竞赛中最常用的 C++ 编译器。 代码编辑器负责帮助程序员编写代码。从定义上讲,记事本就可以算作编辑器。编辑器不负责代码的运行。 Sublime Text、Visual Studio Code 等都是常见的代码编辑器。 ### 集成开发环境 集成开发环境(IDE)是一种集成了多种功能的工具,通常包含代码编写、编译运行、调试等功能。现代的代码编辑器,通过插件通常也能实现类似 IDE 的功能。 ### 调试器 调试器可以帮助开发人员调试代码,通常包含断点(在程序运行到某处时停止运行)、逐行调试、查看变量值、检查运行时错误位置等功能。 gdb 是一个常用的调试器,并且**在 NOI Linux 的考试环境下可用**。 ## C++ 标准 C++ 语言一直在发布新的标准,从 2011 年开始,每 3 年都会发布一个新标准。 当前的 C++ 标准有:C++98(C++03)、C++11、C++14、C++17、C++20、C++23,下一个标准是 C++26。 其中 C++03 只是在 C++98 的基础上做了简单的修订,并没有大幅度的更改。 本文讲述的内容,如果没有标注,默认是 C++98 就存在的。 ## 编译器优化(O2 优化) 大多数的现代编译器都提供了优化选项。在代码不存在未定义行为的情况下,编译器优化选项可以保证程序行为正确,并且优化代码的运行速度。 常见的优化选项有 `-O0`(无优化)、`-O1`、`-O2`、`-O3`、`-Ofast` 等,优化效果通常是递增的。 很多情况下,一些简单的优化在开启优化选项 `-O2` 之后,都会被编译器自动完成(例如简单函数调用造成的开销,绝大多数情况下都会被内联)。 开启编译器优化后,可能让存在问题的代码行为变得奇怪,同时会影响调试器的使用。所以在调试时建议禁用优化。 **目前,在 CCF 组织的比赛中,均使用 C++14 标准,开启 O2 优化。** # 基础语法 ## 输入输出 ```cpp include <iostream> using namespace std; int main() { cout << "Hello World!" << endl; return 0; } ``` C++ 标准库中,主要有这几类输入输出方式: - C 风格的输入输出:`scanf`,`printf`,`getchar`,`putchar`,`puts` 等。 - C++ iostream:`cin`,`cout` 等。 - C++23 print:`print`,`println` 等。 在这些标准库提供的工具中,我最常用的是 iostream。它较为简单,并且可以保证类型安全,无需考虑占位符和实际类型的配套问题。而 C++23 的 print 尚未受到广泛支持。 iostream 采用重载的右移(有时又称“流输入”)和左移(“流输出”)运算符进行输入输出。代码如下: ```cpp int x; std::cin >> x; // 输入 std::cout << x; // 输出 ``` `cout` 可以通过输出一些操纵符,来进行一定程度的格式化输出。大多数操纵符在头文件 `iomanip` 中定义。例如,如下代码可以保留 2 位小数输出: ```cpp std::cout << std::fixed << std::setprecision(2) << 3.1415; // 3.14 ``` 执行以上代码以后,接下来所有的浮点数输出都会维持这样的格式。 iostream 最大的问题可能就是它在默认情况下效率很低。**可以通过以下代码来加速**: ```cpp std::ios::sync_with_stdio(false); std::cin.tie(nullptr); ``` 这种方法通常叫做“关闭同步流”,可以大幅度提升 cin 和 cout 的速度。 第一行的原理是,默认情况下 C 风格的输入输出会维护一个缓冲区来加速(即内容不会立即输出到屏幕,而是存到缓冲区中,缓冲区满或者手动刷新再一次性输出)。而 iostream 为了和 C 兼容,会在每一次输入输出时都刷新缓冲区,导致额外开销。调用了 `ios::sync_with_stdio(false)` 之后就不会每次刷新了。 第二行的原理是,默认情况下 cin 会和 cout 互相绑定,在每次使用 cin 输出时都刷新 cout 的缓冲区。这样可以避免交替使用输入输出时的额外刷新开销。 由于 `endl` 会刷新缓冲区,使用 `'\n'` 而不是 `endl` 换行也会加速输出。 经过这样优化的 `cin` 和 `cout`,速度会略快于 `scanf` 和 `printf`。 **关闭同步流的情况下,iostream 和 C 风格输入输出不能混用**。否则可能会导致多次输出的结果乱序。 有些时候,一些题目会要求输入不定量的信息。这种输入之所以可行,是因为从控制台输入信息,本质上是从一个虚拟的文件 `stdin` 输入。文件是有边界的,读到文件结尾就会获得 EOF 信息(End Of File),无法继续读入。 `cin` 可以通过 `cin.good()`(返回 `true`/`false`)来判断是否处于正常状态。同时 `cin.eof()` 可以判断是否到达文件末尾,`cin.fail()` 和 `cin.bad()` 可以判断是否出现其他的问题,例如读入的整数超过类型上限,或者期望读入整数实际读到字母,将会使得 `cin.fail()` 返回 `true`。 如果 `cin.good()` 返回 `false`,此时将会拒绝接下来的读取操作(变量将会保持原值不被修改)。 在条件判断中,`cin` 对象可以隐式转换为布尔值,即 `cin.good()`。可以通过以下代码来持续读入整数直到文件末尾。在控制台中运行时,可以按下快捷键 Ctrl+Z(Windows)或者 Ctrl+D(Linux)并换行,来手动输入一个 EOF。 ```cpp int x; while (cin >> x) { // cin/cout 输入输出之后返回自己 cout << x << ' '; } ``` ## 未定义行为 接下来的代码中,将会涉及到“未定义行为”及相关概念。可以参考 [cppreference](https://en.cppreference.com/w/cpp/language/ub.html) 页面。 C++ 的代码可能出现以下的错误类别: - **非良构**(ill-formed)。程序存在语法错误,或者可以诊断的语义错误。标准要求编译器对这种行为给出诊断信息,通常会导致编译错误。 - 非良构,但是不要求诊断(no diagnostic required)。有些情况下,程序存在语义错误,但是错误可能在链接时才能发现,或者进行诊断需要的代价过大。这类程序被执行,行为是未定义的。 - **实现定义行为**(implementation-defined behavior)。程序在不同的实现中(包括编译器、标准库实现、运行时环境等),可能会有不同的行为。但是符合标准的实现需要在文档中说明每种行为的实际效果。 - 例如,`long` 类型的大小是实现定义的。在常见的实现中,有些是 4 字节,有些是 8 字节。 - **未指定行为**(unspecified behavior)。程序行为在不同的实现中有所不同,并且实现不需要说明这些行为的效果。 - 例如,一些情况下的求值顺序,相同的字符串字面量是否指向不同地址。 - 不应该依赖未指定行为。 - **未定义行为**(undefined behavior,简称 ub)。程序的行为将不受任何限制。 - 例如,数组的越界访问,有符号整数溢出,空指针解引用。 - 实现无需对未定义行为进行诊断(因为有些只能在运行时被发现)。 - **未定义行为可能导致任何问题**,编译器也可以基于“程序不存在未定义行为”的假设进行优化。 以下是几个编译器可能优化的例子: ```cpp bool f(int x) { return x + 1 > x; // 不溢出的情况下,整数 +1 一定大于自身 } // 可能优化成: bool f_(int x) { return true; } ``` ```cpp int g(int *ptr) { int value = *ptr; // 已经进行解引用,一定不是空指针 if (ptr == nullptr) { return 0; } else { return value; } } // 可能优化成: bool g_(int *ptr) { return *ptr; } ``` 以下是一个未定义行为促进编译器优化的例子: ```cpp int array_access(int *arr, int n, int i) { // 可能是封装过的函数 if (0 <= i && i < n) return arr[i]; return 0; } int sum(int *arr, int n) { int result = 0; for (int i = 0; i < n; i += 3) { result += array_access(arr, n, i); } return result; } // 可能会优化成: int sum_(int *arr, int n) { int result = 0; for (int i = 0; i < n; i += 3) { result += arr[i]; // i 不会溢出到负数,边界判断始终没有必要 } return result; } ``` ## 变量 ```cpp #include <iostream> using namespace std; int main() { int a, b; cin >> a >> b; cout << a + b << '\n'; return 0; } ``` C++ 的“变量”(Variable)是通过标识符引用的对象(Object),指向一个内存中的存储单元,持有的值可以改变。变量几乎是一切数值计算的基础。这个最简单的“两数和问题”中,我们就使用了变量进行计算。 ### 变量名 变量名必须是一个合法的“标识符”([Identifier](https://en.cppreference.com/w/cpp/language/identifiers.html))。即有以下的要求: - 首字符必须为英文字母 `A-Za-z` 或下划线 `_`,或其他具有 XID_Start 属性的 Unicode 字符。 - 其余字符必须为英文字母 `A-Za-z` 、数字 `0-9`或下划线 `_`,或其他具有 XID_Continue 属性的 Unicode 字符。 值得注意的是,自 C++11 起,绝大多数语言中的字母(例如中文汉字),甚至表情符号,都是合法的标识符(即上文提到的 Unicode 字符)。 除此以外,用户定义的标识符(变量、函数、类型等)不能与[关键字](https://en.cppreference.com/w/cpp/keyword.html)完全相同。以双下划线开头(如 `__reserved`),或者单下划线紧跟大写字母开头(如 `_Reserved`),行为是未定义的。 ### 变量作用域 变量的作用域主要分为局部作用域和命名空间作用域等。 在函数等花括号(`{}`)包裹的代码块中定义的变量,具有局部作用域。局部作用域的变量,自声明后开始可见,直到代码块结束。 在命名空间中声明的变量,具有命名空间作用域。这类变量自声明后开始,在命名空间内始终可见,或者通过命名空间名来访问(`ns::name`)。全局作用域可以看作一个特殊的命名空间作用域,它自声明后开始,在全局内始终可见。 不同作用域的同名变量存在“遮蔽”原则,内层作用域的同名变量会隐藏外层作用域的变量。 ```cpp include <iostream> int x = 0; // 全局变量 int main() { std::cout << x << '\n'; // 输出 0 int x = 1; // 局部变量将会遮蔽全局的 x std::cout << x << '\n'; // 输出 1 std::cout << ::x << '\n'; // 可以通过 ::x 强制指定全局作用域的 x,输出 0 return 0; } ``` 可以根据以上代码理解这些原则。 ### 存储期(Storage duration) 存储期指定了一个对象的生命周期,即在何时被销毁并回收资源。变量的存储期由定义的方式决定。 C++ 有以下的几种存储期: - 自动存储期。这是局部变量的默认存储期,会在离开自己的作用域后自动销毁。 - 静态存储期。这是命名空间作用域(包括全局作用域)变量的默认存储期,在程序结束后销毁。 - 动态存储期。通过 `new`、`malloc` 等方式在堆空间动态分配对象的属于动态存储期,需要通过配对的解分配函数来销毁。 - 线程存储期。对于多线程程序,这类对象对每一个线程都会有一个独立的值,生命周期与这个线程相同。 有对应的说明符可以指定存储期。 - `auto`:**C++11 起含义改变**。此前表示自动存储期。 - `register`:**在 C++17 起被移除**。此前用于请求编译器把这个值存储在寄存器中,这个请求可以被忽略。 - `static`:静态存储期。 - `thread_local`:线程存储期。 - `extern`:用于声明一个变量(而不是定义),链接到一个外部的来源。 `mutable` 关键字在 cppreference 中被归类为存储说明符,但是实际不会影响存储期,所以不在此讲述。 尽管 `register` 关键字直到 C++17 才被移除,但是即使在更早的标准中,编译器通常也会忽略它。不要试图使用这个关键字优化性能,这不会有任何作用。 自动存储期的对象将会存储在“栈空间”中,栈空间的容量有限(通常为 8MB),所以定义一些较大的数组,或者递归层数过深,都可能会出现“爆栈”的问题。但是大部分 OJ 和比赛环境,以及 CCF 组织的比赛中,允许程序使用无限的栈空间(即与程序总体内存限制相等),这些情况下可以放心使用局部数组和递归(局部数组需要手动初始化)。 全局或命名空间作用域的静态变量,将会在调用主函数之前进行初始化。 可以在局部作用域中通过 `static` 关键字来定义一个静态变量,这个变量将仅会在第一次运行到定义处的时候进行一次初始化,接下来每次使用都会保有一个相同的值。可以结合以下代码理解。 ```cpp include <iostream> void f() { static int count = 0; count++; std::cout << count << ' '; // 静态变量的值不会被清除 } int main() { f(); f(); f(); // 输出 1 2 3 return 0; } ``` ### 变量初始化 定义一个变量的同时会进行初始化,赋予其一个初始值。C++ 的变量初始化规则十分复杂,接下来我们将会进行一些简单的讲解。 本章节中可能会涉及到一些后续章节才出现的知识。如果出现了你不理解的内容,可以暂时忽略。 在章节的结尾,将会有一段简要的总结。你也可以通过这段总结来理解。 #### 零初始化 零初始化([Zero-Initialization](https://en.cppreference.com/w/cpp/language/zero_initialization.html))是将对象逐位赋值为 0 的初始化方式。C++ 中没有专用的零初始化语法,但是其他初始化方式可能包含零初始化。 **所有具有静态存储期的变量**,将会在进行其他初始化之前,先进行一次零初始化。平时常见的结论“全局变量会自动赋值为 0”就是来自这条规则。 #### 默认初始化 默认初始化([Default-Initialization](https://en.cppreference.com/w/cpp/language/default_initialization.html))是在没有指定初始化器时的初始化方式。例如以下场景将会执行默认初始化: ```cpp int x; auto *ptr = new double; ``` 另外,在类的构造函数中,没有在初始化列表中提及的成员,也会执行默认初始化。 ```cpp struct A { int data; A() {} }; A p; // p.data 将会执行默认初始化 ``` 对类型 `T` 进行默认初始化的效果如下: - 如果 `T` 是类类型(Class Type,由 `class`、`struct` 或 `union` 关键字定义的类型),则调用默认构造函数(空参数列表),为对象提供初始值。 - 如果 `T` 是数组类型,对数组的每个元素进行默认初始化。 - 否则,**不额外执行初始化**。 **对象在未执行初始化的情况下,将会持有一个不确定的值**,直到这个值被替换。使用这个不确定的值进行任何求值操作,都是未定义行为。 但是由于静态存储期的对象会预先进行一次零初始化,所以这种写法对它们是安全的。 C++26 起规定,对于一个自动存储期的变量,并且没有被标识 `[[indeterminate]]`,将会有以下行为: - 构成该对象存储的所有字节,填充一个错误值。这个错误值由实现定义,但是与程序状态无关。 - 如果使用错误值进行求值操作,则行为是错误行为(Erroneous Behavior)。错误行为仍然应该被视作不正确的结果,但是标准建议实现对错误行为进行诊断,而非像未定义行为一样假设不会存在并促进优化。 C++26 引入的错误填充值,往往会导致未初始化的对象拥有一个异常值(例如无效指针,或者绝对值很大的整数和浮点数),避免由于“不确定值”有时恰好符合期望,而产生偶发性的错误。 `const` 对象不允许默认初始化。 #### 值初始化 值初始化([Value-Initialization](https://en.cppreference.com/w/cpp/language/value_initialization.html))在使用空初始化器构造对象时执行,以下是几种常见的场景: ```cpp int x{}; auto *ptr = new double(); std::cout << float() /*构造临时对象*/ << '\n'; char arr[100]{}; ``` 下文中用 `T` 代指对象类型。 有以下特例: - 如果 `T` 是聚合类型(见下文“聚合初始化”),那么执行聚合初始化。但是这种情况下聚合初始化的行为与值初始化的效果是一致的。 - 如果 `T` 没有默认构造函数,但是有一个接收 `std::initializer_list` 的构造函数,那么执行列表初始化。 值初始化的效果如下: - 如果 `T` 是类类型,那么: - 如果它的默认构造函数不是用户提供的(即自动生成),先执行零初始化。 - 接下来,执行默认初始化。 - 否则,如果 `T` 是数组类型,值初始化每个元素。 - 否则,对象将会被零初始化。 值初始化在大多数情况下可以保证所有元素被正确初始化(一个反例为上文“默认初始化”章节的 `p.data`)。 #### 聚合初始化 聚合初始化([Aggregate-Initialization](https://en.cppreference.com/w/cpp/language/aggregate_initialization.html))是通过初始化列表来初始化聚合类型的过程。这是一种特殊的列表初始化。 ##### 聚合类型 聚合类型(Aggregate)是以下类型之一: - 数组类型 - 符合以下要求的类类型 - 没有用户声明或继承的构造函数。(C++20 起,此前的要求类似,但是略有不同) - 没有私有(private)或受保护(protected)的非静态数据成员。 - 没有虚基类(virtual),没有私有或受保护的基类。(C++17 起,此前要求没有任何基类) - 没有虚成员函数。 - 没有默认成员初始化器(Default Member Initializers,即在声明成员的同时赋默认值)。(仅 C++11) ##### 指派初始化器 C++20 引入了指派初始化器(Designated Initializers),可以通过成员名称和目标值之间的键值对来进行聚合初始化。 ```cpp struct A { int a; double b; }; A a{.a = 5, .b = 9.0}; // 指派初始化器 ``` ##### 窄化转换 窄化转换是有潜在精度丢失的转换方式。目标类型不能存储源类型的所有值时,视为窄化转换(例如 `double` 到 `int`,`long long` 到 `int`)。 在标准禁止窄化转换的操作中,编译器可能实现为仅视为警告,不拒绝编译。 ##### 初始化流程 聚合初始化可以分为显式初始化(explicit)和隐式初始化(implicit)。 首先,确定需要显式初始化的元素: - 如果初始化列表是指派初始化器,则包含对应的所有成员。 - 否则,按照声明顺序包含最靠前的若干个元素。如果一个成员 `x` 也是聚合体,并且实际传入的值不是聚合体,将会进一步匹配 `x` 的全体成员,再对 `x` 进行聚合初始化,减少一层花括号嵌套。 ```cpp struct A { int x = 0, y = 0, z = 0; }; A arr[2] {0, 1, 2, 3, 4, 5}; // 相当于 {{0, 1, 2}, {3, 4, 5}} ``` 如果 `T` 为联合体(union),包含超过一个显式初始化的元素,程序非良构;若是使用指派初始化器,则只能指定一个成员。 接下来,按照声明顺序初始化这些选中的元素。初始化每个成员时相当于使用复制初始化。 接下来,如果 `T` 不是联合体,每个未显式初始化的成员按照以下方式隐式初始化: - 如果这个元素有默认成员初始化器,从初始化器初始化它。 - 否则,如果元素不是引用,从空的初始化列表对它进行拷贝初始化(多数情况下等价于值初始化)。 - 否则,程序非良构。 特别地,通过字符串字面量初始化一个字符数组,也属于聚合初始化。允许的字符类型有:`char`(或 `signed` 和 `unsigned` 变种)、`wchar_t`、`char16_t`(C++11 起)、`char32_t`(C++11 起)和 `char8_t`(C++20 起)。数组过长的部分将用 0 填充。 聚合初始化的过程中,不允许对参数进行窄化转换。 ```cpp char s[30]{"This is a C-style string."}; ``` #### 列表初始化 通过花括号包裹的初始化列表初始化对象的方式,叫做列表初始化([List-Initialization](https://en.cppreference.com/w/cpp/language/list_initialization.html))。 ```cpp std::pair<int, int> p = {1, 2}; std::vector<int> v{0, 1, 2, 3}; ``` 类似以上方式的初始化,属于列表初始化。 上下两种方式,以语义上是否需要紧跟一次复制为分别,又称为“复制列表初始化”和“直接列表初始化”。例如,向函数参数传递一个初始化列表,或者将初始化列表作为返回值,都属于“复制列表初始化”。经过编译器优化,这种方式通常不会有额外的复制开销, 复制列表初始化,不会调用标记为 `explicit` 的构造函数。 列表初始化有以下的流程: - 如果初始化列表是指派初始化器,执行聚合初始化。 - 如果 `T` 为聚合类型,并且初始化列表提供了一个同类型的对象,则从这个对象初始化。(依据自身类别进行复制初始化/直接初始化) - 如果 `T` 为字符数组,且用花括号括起来一个对应的字符串字面量,则由这个字符串进行聚合初始化。 - 如果 `T` 为聚合类型,执行聚合初始化。 - 如果初始化列表为空,且 `T` 为存在默认构造函数的类类型,执行值初始化。 - 如果 `T` 为 `std::initializer_list` 的特化,逐个成员复制初始化。 - 如果 `T` 为类类型,考虑其构造函数: - 接受单个 `std::initializer_list` 参数的构造函数,优先调用。 - 对于初始化列表中指定的参数执行重载决议,寻找最佳匹配的构造函数。 - 否则(`T` 不是类类型),并且初始化列表中只有一项,并且 `T` 不是引用,或者 `T` 是兼容的引用(同类型或是其基类),则从这个对象初始化,但是不允许窄化转换。 - 否则,如果 `T` 是不兼容的引用类型,将会通过复制列表初始化创建一个 `T` 所引用类型的临时量,然后引用绑定到这个临时对象。如果 `T` 是非 `const` 的左值引用,那么操作失败。 - 否则,如果初始化列表为空,执行值初始化。 初始化列表中,求值顺序是固定的从前到后。相对地,**函数调用的参数求值顺序是不固定的**。 ##### std::initializer_list `std::initializer_list` 可以存储若干个类型相同的对象。列表初始化中,将会优先使用接受 `std::initializer_list` 的构造函数。 例如,通过花括号初始化存在若干个初始元素的 `std::vector`,就是通过 `std::initializer_list`。 ```cpp std::vector<int> vec{0, 1, 2, 3, 4}; ``` #### 复制初始化 复制初始化([Copy-Initialization](https://en.cppreference.com/w/cpp/language/copy_initialization.html))指从另一个对象初始化一个对象,在语义上应该发生复制。 ```cpp int x = y; f(x); // 函数调用时也是复制初始化 ``` - 如果初始化器的类型为 `T`,调用 `T` 的构造函数。 - 初始化器类型与 `T` 无关,则尝试调用: - 初始化器类型的转换函数,转换为 `T` 或派生类。 - `T` 的构造函数,接受初始化器类型。 - 尝试应用标准转换。 复制初始化中,不会使用任何标记为 `explicit` 的构造函数。有些情况下的复制往往可以被编译器优化掉,转换成直接在目标位置构造对象。 C++17 起,标准强制要求进行复制消除,即初始化器为函数返回值这样的纯右值时,一定不会进行额外的复制。此前的编译器往往也会做这样的优化。 #### 直接初始化 直接初始化([Direct-Initialization](https://en.cppreference.com/w/cpp/language/direct_initialization.html))通过指定的参数调用构造函数,初始化对象。 ```cpp std::vector<int> vec(/*n:*/10, /*default:*/2); ``` 直接初始化的效果如下: - 如果 `T` 是数组类型: - C++20 起,数组按照聚合初始化的方式进行初始化,但允许进行窄化转换,并且任何没有初始化器的元素将进行值初始化。 - 如果 `T` 是类类型: - C++17 起,标准规定实现类似复制初始化的“复制省略”机制,如果参数是 `T` 的纯右值,直接使用初始化器本身初始化目标对象。 - 检查 `T` 的构造函数,通过重载决议决定最佳匹配项。 - C++20 起,如果 `T` 是聚合类型,使用类似聚合初始化的方式进行初始化。但是存在以下区别:允许窄化转换,不存在花括号省略机制,没有初始化器的元素将会执行值初始化。 - 否则(`T` 不是类类型),源类型是一个类类型,则会检查其转换函数。 - 否则,如果 `T` 为 `bool` 且源类型为 `std::nullptr_t`,初始化对象为 `false`。 - 尝试应用标准转换。 以下的写法是错误的,因为会和函数声明混淆。这通常可以使用空的花括号代替。 ```cpp std::vector vec(/*参数列表为空*/); ``` #### 总结 核心规则可以大致概括为: - 零初始化:逐位赋值为 0,全局/静态变量自动执行。 - 默认初始化 `int x`: - 类类型,调用默认构造函数。 - 基本类型,**局部变量的值不确定**,全局/静态变量预先零初始化为 0。 - 值初始化 `int x{}`: - 基本类型,初始化为 0。 - 类类型,调用默认构造。 - 通常是最安全的初始化方式。 - 直接初始化 `int x(5)`: - 直接调用匹配的构造函数。 - 复制初始化 `int x = 5`: - 实际行为通常与直接初始化一致。 - 禁用 `explicit` 构造函数。 - 经过编译器优化,通常不会有额外的复制。 - 列表初始化 `int x{5}`: - 优先匹配接受 `std::initializer_list` 的构造函数。 - 禁止窄化转换。 ### cv 标识符(常量性、易变性) 类型可以通过 `const` 和 `volatile` 修饰,获得常量性或者易变性。修饰符不会影响对象的底层表示、对齐要求等。 数组类型与它的元素拥有相同的 cv 标识符。 对象具有的 cv 标识符,也会给予它的成员。被声明为 `mutable` 的成员除外,它不会继承对象的常量性。 #### 常量性 具有常量性的对象不能被修改。直接修改会导致编译错误,而间接修改(例如通过 `const_cast` 获得非常量指针,或者直接修改底层内存)**会导致未定义行为**。 #### 易变性 具有易变性的对象,每次读写都要求立即和内存同步,禁止编译器进行缓存、指令重排等优化。在涉及到多线程交互、信号处理、直接操作内存等情况下需要用到。编译器会假设代码始终单线程执行,从而在一些情况下,可能导致意料之外的优化。 ## 类型 ### 基本类型 C++ 包含以下的[基本类型](https://en.cppreference.com/w/cpp/language/types.html): - 整数类型 - 浮点数类型 - `std::nullptr_t` 空指针类型 - `void` 空类型 #### 整数类型 有以下对于整数类型的长度修饰符。长度修饰符的效果由实现定义,但是需要满足一定要求。 | 长度修饰符 | 要求 | |--|--| | short | 不小于 16 位 | | (无) | 不小于 16 位 | | long | 不小于 32 位 | | long long | 不小于 64 位 | 完整的整数类型包含以下部分: | 组成部分 | 描述 | |--|--| | 长度修饰符 | 指定数字二进制位数 | | 符号标识符 | 指定数字有符号(`signed`)/无符号(`signed`),不填为有符号 | | `int` | 如有其他的单词描述,可以省略 | 这几个部分的顺序可以交换,`signed short int`、`long long unsigned int`、`long int signed` 都是合法的。 除了以上的标准整数类型,还有以下的整数类型: - 布尔类型 `bool`。 - 字符类型。 - `signed char`,`unsigned char`。 - `char`。以上三个类型的长度相同,但是始终是三个不同类型。`char` 是否有符号由实现定义。 - `wchar_t`,`char16_t`(C++11 起),`char32_t`(C++11 起),`char8_t`(C++20 起)。 - 扩展整数类型。 - GCC 扩展的 `__int128` 就是扩展整数类型。 此外,标准保证 `sizeof(char)` 为 1,且 `sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)`。`sizeof` 返回值的单位为字节,字节的位数由实现定义,但是绝大多数情况下都是 8 位。 #### 浮点数类型 - `float`。单精度浮点数,通常为 IEEE-754 binary32 格式。 - `double`。双精度浮点数,通常为 IEEE-754 binary64 格式。 - `long double`。扩展精度浮点数。不一定映射到 IEEE-754 规定类型。它的精度和占用空间都是实现定义的。 - 由实现定义的扩展浮点数类型。
正在渲染内容...
点赞
0
收藏
0