Cpp学习10 第 7 章 作用域、生命周期与链接性

铁名_IronName Lv5

7.1 — 复合语句(块)

复合语句 (也称为块语句 )是由 零个或多个语句 组成的一组语句,编译器会将其视为单个语句。用 { } .

当代码块嵌套时,包围它们的代码块通常称为外层代码块 ,被包围的代码块称为内层代码块嵌套代码块 。

if 和 代码块

最佳实践

函数嵌套层数最好控制在三层或以下。如果函数需要更多嵌套层数,请考虑将其重构为子函数。

7.2 — 用户定义的命名空间和作用域解析运算符

C++ 允许我们通过 namespace 关键字定义自己的命名空间。你在自己的程序中创建的命名空间通常被称为用户定义的命名空间 (尽管更准确的说法是程序定义的命名空间 )。

建议 命名空间名称 以大写字母开头.
使用作用域解析运算符 (::) 访问命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

namespace Foo // define a namespace named Foo
{
// This doSomething() belongs to namespace Foo
int doSomething(int x, int y)
{
return x + y;
}
}

namespace Goo // define a namespace named Goo
{
// This doSomething() belongs to namespace Goo
int doSomething(int x, int y)
{
return x - y;
}
}

int main()
{
std::cout << Foo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Foo
std::cout << Goo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Goo
return 0;
}

如果使用了命名空间内的标识符,且未提供作用域解析,编译器首先会尝试在同一命名空间中查找匹配的声明。如果找不到匹配的标识符,编译器将按顺序检查每个包含该标识符的命名空间,最后检查全局命名空间。
使用不带命名空间的解析运算符( ::print() )来显式调用 print() 的全局版本。

命名空间中内容的前置声明

add.h

1
2
3
4
5
6
7
8
9
10
#ifndef ADD_H
#define ADD_H

namespace BasicMath
{
// function add() is part of namespace BasicMath
int add(int x, int y);
}

#endif

add.cpp

1
2
3
4
5
6
7
8
9
10
#include "add.h"

namespace BasicMath
{
// define the function add() inside namespace BasicMath
int add(int x, int y)
{
return x + y;
}
}

main.cpp

1
2
3
4
5
6
7
8
9
10
#include "add.h" // for BasicMath::add()

#include <iostream>

int main()
{
std::cout << BasicMath::add(4, 3) << '\n';

return 0;
}

在多个位置(跨多个文件或同一文件中的多个位置)声明命名空间块是合法的。命名空间内的所有声明都被视为该命名空间的一部分。

警告

不要向 std 命名空间添加自定义功能。

命名空间可以嵌套在其他命名空间内。

Namespace aliases  命名空间别名

由于在嵌套命名空间中键入变量或函数的限定名称可能很麻烦,C++ 允许创建命名空间别名 ,从而可以将一长串命名空间临时缩短为更短的形式

1
2
3
namespace Active = Foo::Goo; // active now refers to Foo::Goo

std::cout << Active::add(1, 2) << '\n'; // This is really Foo::Goo::add()

7.3 — 局部变量

局部变量具有块级作用域 ,这意味着它们的 作用域 从其定义点到定义它们的代码块的末尾。
作用域内的所有变量名必须唯一。
局部变量具有自动存储期 ,它们在定义时创建,并在定义它们的代码块结束时销毁。

最佳实践

尽可能在现有作用域内定义变量。避免创建仅用于限制变量作用域的新代码块。

变量的作用域 scope 决定了变量在源代码中的可访问范围。持续时间 duration 定义了变量创建和销毁的规则。变量的生命周期 lifetime 是指从创建到销毁的实际时间。

7.4 — 全局变量简介

在 C++ 中,变量也可以在函数_外部_声明。这样的变量称为全局变量 。

在全局命名空间中声明的标识符具有全局命名空间作用域 (通常称为全局作用域 ,有时非正式地称为文件作用域 ),这意味着它们从声明点到声明它们的 文件 的末尾都是可见的。

最佳实践

建议在命名空间内部定义全局变量,而不是在全局命名空间中定义。

全局变量在程序启动时( main() 函数执行之前)创建,并在程序结束时销毁。这称为静态持续时间 。具有 静态持续时间 的变量有时也称为静态变量 。

最佳实践

在命名全局变量(特别是全局命名空间中定义的变量)时,可以考虑使用“g”或“g_”前缀,以帮助将它们与局部变量和函数参数区分开来。

与默认情况下未初始化的局部变量不同,具有静态持续时间的变量默认情况下会被初始化为零。

7.5 — 变量遮蔽(名称隐藏)

在嵌套代码块内部,无法从外部代码块直接访问被遮蔽的变量。variable shadowing.
与全局变量同名的局部变量会覆盖全局变量。

最佳实践

避免变量遮蔽。

7.6 — 内部链接 Internal linkage

全局变量和函数标识符可以有 internal linkage 或 external linkage 。
具有内部链接internal linkage的标识符可以在单个翻译单元内查看和使用,但无法从其他翻译单元访问。这意味着,如果两个源文件具有名称相同的、具有内部链接的标识符,则这些标识符将被视为独立的(不会因定义重复而导致 ODR 违规)。

具有内部联系的全局变量有时被称为内部变量 。
要将非常量全局变量变为内部变量,我们使用 static 关键字。

1
static int g_x{}; // non-constant globals have external linkage by default, but can be given internal linkage via the static keyword

const 和 constexpr 全局变量默认具有内部链接.

函数标识符也具有链接属性。函数默认使用外部链接(我们将在下一课中介绍),但可以通过 static 关键字设置为内部链接:

1
2
3
4
5
6
// This function is declared as static, and can now be used only within this file
// Attempts to access it from another file via a function forward declaration will fail
[[maybe_unused]] static int add(int x, int y)
{
return x + y;
}

在现代 C++ 中,使用 static 关键字为标识符提供内部链接的做法已逐渐被淘汰。未命名的命名空间可以为更广泛的标识符(例如类型标识符)提供内部链接,因此更适合为多个标识符提供内部链接。

最佳实践

有明确理由禁止从其他文件访问时,请为标识符提供内部链接。
考虑将所有不希望其他文件访问的标识符赋予内部链接(为此使用未命名的命名空间)。

7.7 — 外部链接和变量前向声明

具有外部链接的标识符既可以在定义它的文件中查看和使用,也可以通过前向声明在其他代码文件中查看和使用。从这个意义上讲,具有外部链接的标识符是真正“全局”的,因为它们可以在程序的任何位置使用!

函数默认具有外部链接。

具有外部链接的全局变量有时被称为外部变量 。要将全局变量设为外部变量(从而使其他文件可以访问),我们可以使用 extern 关键字:

1
2
extern const int g_y { 3 }; // const globals can be defined as extern, making them external
extern constexpr int g_z { 3 }; // constexpr globals can be defined as extern, making them external (but this is pretty useless, see the warning in the next section)

要实际使用在其他文件中定义的外部全局变量,您还必须在任何想要使用该变量的其他文件中对其进行 forward declaration 。对于变量,创建前向声明也是通过 extern 关键字完成的(无需初始化值)。

1
2
extern int g_x;       // this extern is a forward declaration of a variable named g_x that is defined somewhere else
extern const int g_y; // this extern is a forward declaration of a const variable named g_y that is defined somewhere else
1
2
3
// global variable definitions
int g_x { 2 }; // non-constant globals have external linkage by default
extern const int g_y { 3 }; // this extern gives g_y external linkage

虽然可以使用 extern 关键字为 constexpr 变量添加外部链接,但它们不能被前向声明为 constexpr。这是因为编译器需要在编译时知道 constexpr 变量的值。如果该值在其他文件中定义,编译器就无法得知该文件中定义的具体值是什么。

不要对非常量全局变量定义使用 extern (它们默认是 extern )。

7.8 — 为什么(非常量)全局变量是邪恶的

最佳实践

尽可能使用局部变量而不是全局变量。

如果你确实需要使用非常量全局变量,以下几点建议可以最大程度地减少你可能遇到的问题。这些建议不仅适用于非常量全局变量,也适用于所有全局变量。(超麻烦…)

  • 首先,将所有非命名空间全局变量加上“g”或“g_”前缀,或者更好的是,将它们放在命名空间中;
  • 其次,与其直接访问全局变量,不如将其“封装”起来。确保变量只能在其声明的文件内部访问,例如将其声明为静态变量或常量变量,然后提供外部全局“访问函数”来操作该变量。
  • 第三,在编写使用全局变量的独立函数时,不要在函数体中直接使用该变量,而应将其作为参数传递。这样,如果函数需要在某些情况下使用不同的值,只需更改参数即可。这有助于保持模块化。

7.9 — 内联函数和变量

 内联扩展是一个将函数调用替换为被调用函数定义中的代码的过程。
除了消除函数调用的成本之外,内联扩展还可以让编译器更有效地优化生成的代码

然而,在现代 C++ 中,不再使用 inline 关键字来请求内联展开函数。
在现代 C++ 中,inline 一词的含义已经演变为“允许多次定义”。因此,内联函数是指允许在多个翻译单元中定义的函数(而不违反 ODR)。
链接器会将标识符的所有内联函数定义合并为一个定义(从而仍然满足一个定义规则的要求)。
内联函数通常定义在头文件中,

1
2
3
4
5
6
#ifndef PI_H
#define PI_H

inline double pi() { return 3.14159; }

#endif

这对于仅包含头文件的库尤其有用,这类库由一个或多个头文件组成,实现了某些功能(不包含任何 .cpp 文件)。

为什么不把所有函数都写成内联函数,并在头文件中定义呢?

主要原因是这样做会显著增加编译时间。
当包含内联函数的头文件被 #include 到源文件中时,该函数定义将被编译为该翻译单元的一部分。一个被 #include 到 6 个翻译单元中的内联函数,其定义将被编译 6 次(在链接器进行去重之前)。相反,一个在源文件中定义的函数,无论其前向声明被包含在多少个翻译单元中,其定义都只会被编译一次。
其次,如果源文件中定义的函数发生更改,则只需重新编译该源文件。但如果头文件中的内联函数发生更改,则所有包含该头文件的代码文件(无论是直接包含还是通过其他头文件包含)都需要重新编译。在大型项目中,这可能会导致连锁重新编译,并造成严重影响。

C++17 内联变量

C++17 引入了内联变量 ,即允许在多个文件中定义的变量。内联变量的工作方式与内联函数类似,并且具有相同的要求(编译器必须能够在变量被使用的每个地方看到完全相同的完整定义)。

7.10 — 在多个文件之间共享全局常量(使用内联变量)

早期方法

在 C++17 之前,以下是最简单、最常见的解决方案:

  1. 创建一个头文件来保存这些常量。
  2. 在这个头文件中,定义一个命名空间(在第 7.2 课中讨论——用户定义的命名空间和作用域解析运算符 )。
  3. 将所有常量添加到命名空间内(确保它们是 constexpr )。
  4. 在需要的地方包含头文件

全局常数作为外部变量

将这些常量转换为外部变量,这样我们就可以使用一个所有文件共享的单一变量(只需初始化一次)。在这种方法中,我们将常量定义在一个 .cpp 文件中(以确保定义仅存在于一处),并在头文件中进行声明(其他文件会包含该头文件)。

C++17 中的全局常量作为内联变量

constants.h:

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef CONSTANTS_H
#define CONSTANTS_H

// define your own namespace to hold constants
namespace constants
{
inline constexpr double pi { 3.14159 }; // note: now inline constexpr
inline constexpr double avogadro { 6.0221413e23 };
inline constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
// ... other related constants
}
#endif

main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "constants.h"

#include <iostream>

int main()
{
std::cout << "Enter a radius: ";
double radius{};
std::cin >> radius;

std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';

return 0;
}

将 constants.h 包含到任意数量的代码文件中,这些变量只会实例化一次,并在所有代码文件中共享。

最佳实践

如果需要全局常量并且您的编译器支持 C++17,建议在头文件中定义内联 constexpr 全局变量。

对于 constexpr 字符串,请使用 std::string_view 。

7.11 — 静态局部变量

 static 在不同的上下文中具有不同的含义。

Static local variables  静态局部变量

在局部变量上使用 static 关键字会将其持续时间从自动创建改为静态创建。这意味着该变量现在会在程序开始时创建,并在程序结束时销毁(就像全局变量一样)。因此,即使超出作用域,静态变量仍会保留其值!

ID 生成

使用静态持续时间局部变量生成唯一 ID 号非常容易。
静态变量兼具全局变量的部分优点(程序结束前不会被销毁),同时又将其作用域限制在代码块内。这使得它们更容易理解,使用起来也更安全。

Static local constants

静态局部变量可以声明为 const(或 constexpr)。当函数需要使用常量值,但创建或初始化该对象开销较大时(例如,需要从数据库读取值),静态局部变量就非常适用。如果使用普通的局部变量,每次函数执行时都会创建并初始化该变量。而使用 const/constexpr 静态局部变量,只需创建并初始化一次开销较大的对象,即可在每次调用函数时重复使用该对象。

最佳实践

通常情况下,使用 const 静态局部变量是可以的。
通常应避免使用非常量静态局部变量。如果必须使用,请确保该变量永远不需要重置,并且不会用于改变程序流程。

当应用于全局变量时,static 关键字将全局变量定义为具有内部链接,这意味着该变量不能导出到其他文件。
当应用于局部变量时,static 关键字将局部变量定义为具有静态持续时间,这意味着该变量只会创建一次,并且直到程序结束才会销毁。

7.12 — 范围、持续时间和关联性小结

需要再查吧….
传送门: https://www.learncpp.com/cpp-tutorial/scope-duration-and-linkage-summary/

7.13 — 使用声明和使用指令

Using declarations and using directives

为什么你应该避免使用“using namespace std;“

命名冲突发生的可能性显著增加。
更新后的库可能引入一个不仅名称相同,而且实际上更适合某些函数调用的新函数。在这种情况下,编译器可能会优先选择新函数,导致程序的行为在不知不觉中发生意想不到的改变。

如果在代码块内使用 using 声明或 using 指令,则名称仅适用于该代码块(遵循正常的代码块作用域规则)。这很好,因为它将命名冲突的发生概率降低到该代码块内部。

最佳实践

优先使用显式命名空间限定符,而不是 using 语句。

尽量避免使用 using 指令(除了 using namespace std::literals 来访问 s 和 sv 字面量后缀)。using 声明可以在 .cpp 文件中的 #include 指令之后使用。不要在头文件中使用 using 语句(尤其是在头文件的全局命名空间中)。

7.14 — 未命名命名空间和内联命名空间

未命名(匿名)命名空间

未命名命名空间 (也称为匿名命名空间 )是指没有定义名称的命名空间.
未命名命名空间中声明的所有内容都被视为父命名空间的一部分。因此,即使函数 doSomething() 定义在未命名命名空间中,该函数本身也可以从父命名空间(在本例中为全局命名空间)访问。这可能会让未命名命名空间看起来毫无用处。但未命名命名空间的另一个作用是,其中所有标识符都被视为具有内部链接,这意味着未命名命名空间的内容无法在定义该命名空间的文件之外查看。

最佳实践

如果您希望某些内容仅在编译单元‌内可用,请优先使用未命名的命名空间。
避免在头文件中使用未命名的命名空间。

编译单元==翻译单元,translation unit)

Inline namespaces  内联命名空间

内联命名空间通常用于对内容进行版本控制。与未命名空间类似,在内联命名空间中声明的任何内容都被视为父命名空间的一部分。但是,与未命名空间不同的是,内联命名空间不会影响链接。

要定义内联命名空间,我们使用 inline 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

inline namespace V1 // declare an inline namespace named V1
{
void doSomething()
{
std::cout << "V1\n";
}
}

namespace V2 // declare a normal namespace named V2
{
void doSomething()
{
std::cout << "V2\n";
}
}

int main()
{
V1::doSomething(); // calls the V1 version of doSomething()
V2::doSomething(); // calls the V2 version of doSomething()

doSomething(); // calls the inline version of doSomething() (which is V1)

return 0;
}

在上面的例子中,调用 doSomething() 的调用者将得到 doSomething() 的 V1 版本(内联版本)。想要使用新版本的调用者可以显式调用 V2::doSomething() 。这样既保留了现有程序的功能,又允许新程序利用更新/更好的版本。inline namespace 中的是默认的。

在这种情况(inline namespace Foo + namespace)下,最好将匿名命名空间嵌套在内联命名空间中。这样做效果相同(匿名命名空间内的所有函数默认 内部链接),但仍然可以提供一个可供使用的显式命名空间名称

  • 标题: Cpp学习10 第 7 章 作用域、生命周期与链接性
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-08 15:32:53
  • 更新于 : 2026-08-08 19:05:19
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习10/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论