参考:https://www.learncpp.com/
复合语句(也称为块或块语句)是一组零个或多个语句,编译器将其视为单个语句。
块以符号 {
开头,以 }
符号结尾,要执行的语句放在两者之间。块可以在允许单个语句的任何地方使用。块的末尾不需要分号。
在编写函数时,您已经看到了一个块的示例,因为函数体是一个块
其他块内的块
虽然函数不能嵌套在其他函数中,但块可以嵌套在其他块中:
int add(int x, int y)
{ // block
return x + y;
} // end block
int main()
{ // outer block
// multiple statements
int value {};
{ // inner/nested block
add(3, 4);
} // end inner/nested block
return 0;
} // end outer block
当块嵌套时,封闭块通常称为外部块,封闭块称为内部块或嵌套块。
块最常见的用例之一是与 if 结合使用。最好将嵌套级别保持在 3 或更低。正如过长的函数是重构的良好候选者(分解为较小的函数)一样,过度嵌套的块难以阅读,并且是重构的良好候选者(嵌套最多的块成为单独的函数)
定义自己的命名空间
C++允许我们通过 namespace
关键字定义自己的命名空间。在自己的程序中创建的命名空间通常称为用户定义的命名空间(尽管将它们称为程序定义的命名空间会更准确)。
对于高级阅读者:
首选以大写字母开头的命名空间名称的一些原因:
Foo::x
)时,其中 Foo
可以是命名空间或类类型)。使用范围解析运算符 (: 访问命名空间
告诉编译器在特定命名空间中查找标识符的最佳方法是使用范围解析运算符 (:。范围解析运算符告诉编译器,应在左侧操作数的范围内查找右侧操作数指定的标识符。
下面是使用范围解析运算符告诉编译器我们明确希望使用 foo
位于命名空间中的版本 doSomething()
的示例:
#include
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
return 0;
}
使用不带名称前缀的范围解析运算符
范围解析运算符也可以在标识符前面使用,而无需提供命名空间名称(例如 ::doSomething
)。在这种情况下,标识符(例如 doSomething
)在全局命名空间中查找。
#include
void print() // this print() lives in the global namespace
{
std::cout << " there\n";
}
namespace Foo
{
void print() // this print() lives in the Foo namespace
{
std::cout << "Hello";
}
}
int main()
{
Foo::print(); // call print() in Foo namespace
::print(); // call print() in global namespace (same as just calling print() in this case)
return 0;
}
从命名空间中解析标识符
如果使用命名空间内的标识符,并且未提供范围解析,则编译器将首先尝试在同一命名空间中查找匹配的声明。如果未找到匹配的标识符,编译器将按顺序检查每个包含命名空间以查看是否找到匹配项,最后检查全局命名空间。
#include
void print() // this print() lives in the global namespace
{
std::cout << " there\n";
}
namespace Foo
{
void print() // this print() lives in the Foo namespace
{
std::cout << "Hello";
}
void printHelloThere()
{
print(); // calls print() in Foo namespace
::print(); // calls print() in global namespace
}
}
int main()
{
Foo::printHelloThere();
return 0;
}
请注意,我们还使用不带命名空间 ( ::print()
) 的作用域解析运算符来显式调用 的 print()
全局版本。
允许多个命名空间块
在多个位置(跨多个文件或同一文件中的多个位置)声明命名空间块是合法的。命名空间中的所有声明都被视为命名空间的一部分。
警告:不要向 std 命名空间添加自定义功能。
嵌套命名空间
因为命名空间 Goo
在命名空间 Foo
内,所以我们访问 add
为 Foo::Goo::add
命名空间别名
由于在嵌套命名空间中键入变量或函数的限定名称可能会很痛苦,因此C++允许您创建命名空间别名,这允许我们将一长串命名空间暂时缩短为更短的名称:
#include
namespace Foo::Goo
{
int add(int x, int y)
{
return x + y;
}
}
int main()
{
namespace Active = Foo::Goo; // active now refers to Foo::Goo
std::cout << Active::add(1, 2) << '\n'; // This is really Foo::Goo::add()
return 0;
} // The Active alias ends here
命名空间别名的一个很好的优势是:如果要将其中的功能 Foo::Goo
移动到其他位置,只需更新 Active
别名即可反映新的目标,而不必查找/替换 的每个 Foo::Goo
实例。
何时应使用命名空间
在应用程序中,命名空间可用于将特定于应用程序的代码与以后可能重用的代码(例如数学函数)分开。例如,物理函数和数学函数可以进入一个命名空间(例如 Math::
)。另一个语言和本地化功能(例如 Lang::
)
局部变量具有块范围,这意味着它们从定义点到定义它们的块末尾都在范围内
作用域内的所有变量名称必须是唯一的
标识符具有另一个名为 linkage 的属性。标识符的链接确定该名称的其他声明是否引用同一对象。
Scope , linkage 有些相似。但是,作用域Scope 定义了可以在何处查看和使用单个声明。链接linkage 定义多个声明是否引用同一对象。
通过限制变量的作用域,可以降低程序的复杂性,因为活动变量的数量减少了。
在C++中,变量也可以在函数外部声明。此类变量称为全局变量。
在全局命名空间中声明的标识符具有全局命名空间作用域(通常称为全局作用域,有时非正式地称为文件作用域),这意味着它们从声明点到声明它们的文件末尾都是可见的。
全局变量具有静态持续/static duration
全局变量在程序启动时创建,在程序结束时销毁。这称为静态持续。具有静态持续时间的变量有时称为静态变量。
命名全局变量
按照惯例,一些开发人员在非常量全局变量标识符前面加上“g”或“g_”,以指示它们是全局的。此前缀有多种用途:
它有助于避免与全局命名空间中的其他标识符发生命名冲突。
它有助于防止无意中的名称阴影
它有助于指示前缀变量在函数范围之外持续存在,因此我们对它们所做的任何更改也将持续存在。
全局变量初始化
与默认情况下未初始化的局部变量不同,具有静态持续的变量默认为零初始化。
int g_x; // no explicit initializer (zero-initialized by default)
int g_y {}; // value initialized (resulting in zero-initialization)
int g_z { 1 }; // list initialized with specific value
常量全局变量
就像局部变量一样,全局变量可以是常量。与所有常量一样,必须初始化常量全局变量。
(const
变量用于声明运行时常量,而 constexpr
变量用于声明编译期常量。)
关于(非常量)全局变量的注意事项
新程序员经常倾向于使用大量全局变量,因为它们可以使用,而不必将它们显式传递给需要它们的每个函数。但是,通常应完全避免使用非常量全局变量!
每个块定义自己的范围区域。那么,当我们在嵌套块中有一个变量与外部块中的变量同名时会发生什么?发生这种情况时,嵌套变量会将外部变量“隐藏”在它们都在范围内的区域中。这称为名称隐藏或阴影。
局部变量的阴影
在嵌套块内部时,无法从外部块直接访问阴影变量。
全局变量的阴影
与全局变量同名的局部变量将在局部变量在作用域中的任何位置隐藏全局变量.但是,由于全局变量是全局命名空间的一部分,因此我们可以使用范围运算符 (::)没有前缀告诉编译器,我们的意思是全局变量而不是局部变量。
最佳实践:避免可变阴影。
按照约定,全局变量在全局命名空间中文件顶部的包含下方声明。下面是正在定义的全局变量的示例:
全局变量和函数标识符可以具有 internal linkage
或 external linkage
。具有内部链接的标识符可以在单个翻译单元中查看和使用,但无法从其他翻译单元访问。
具有内部链接的全局变量
具有内部链接的全局变量有时称为内部变量。为了使非常量全局变量成为内部变量,我们使用 static
关键字。
Const 和 constexpr 全局变量默认具有内部链接(因此不需要 static
关键字 - 如果使用,将被忽略)。
具有内部联动==链接的函数
由于链接是标识符(而不是变量)的属性,因此函数标识符具有与变量标识符相同的链接属性。函数默认为外部链接
单一定义规则和内部联系
前向声明和定义中,我们注意到一个定义规则说一个对象或函数不能有多个定义,无论是在文件还是程序中。但是,值得注意的是,在不同文件中定义的内部对象(和函数)被视为独立实体(即使它们的名称和类型相同),因此不会违反一个定义规则。每个内部对象只有一个定义。
static
与未命名的命名空间
在现代C++,使用 static
关键字为标识符提供内部链接正在失宠。未命名的命名空间可以为更广泛的标识符(例如类型标识符)提供内部链接,并且它们更适合为许多标识符提供内部链接。
为什么要费心给标识符内部链接?
当您有明确的理由禁止从其他文件访问时,请为标识符提供内部链接。
函数默认具有外部链接。为了调用在另一个文件中定义的函数,您必须在希望使用该函数的任何其他文件中放置该函数的 a forward declaration 。前向声明告知编译器函数的存在,链接器将函数调用连接到实际的函数定义。
具有外部链接的全局变量有时称为外部变量。为了使全局变量外部(因此可以被其他文件访问),我们可以使用关键字 extern 来执行此操作:
int g_x { 2 }; // non-constant globals are external by default
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 useless, see the note in the next section)
int main()
{
return 0;
}
通过 extern 关键字的变量前向声明
请注意,函数前向声明不需要 extern 关键字 - 编译器能够根据是否提供函数体来判断您是在定义新函数还是进行前向声明。变量前向声明确实需要 extern 关键字来帮助区分未初始化的变量定义和变量前向声明(它们在其他方面看起来相同)
变量的范围、持续时间和链接之间有什么区别?全局变量具有什么样的范围、持续时间和链接?
Scope determines where a variable is accessible. Duration determines when a variable is created and destroyed. Linkage determines whether the variable can be exported to another file or not.
Global variables have global scope (a.k.a. file scope), which means they can be accessed from the point of declaration to the end of the file in which they are declared.
Global variables have static duration, which means they are created when the program is started, and destroyed when it ends.
Global variables can have either internal or external linkage, via the static and extern keywords respectively.
如果你向一位资深程序员询问一条关于良好编程实践的建议,经过一番思考,最有可能的答案是,“避免全局变量!而且有充分的理由:全局变量是该语言中历史上最被滥用的概念之一。
新程序员经常倾向于使用大量全局变量,因为它们易于使用,特别是当涉及对不同函数的许多调用时(通过函数参数传递数据是一种痛苦)。但是,这通常是一个坏主意。许多开发人员认为应该完全避免使用非常量全局变量!
为什么(非常量)全局变量是邪恶的
到目前为止,非常量全局变量危险的最大原因是因为它们的值可以被任何调用的函数更改,并且程序员没有简单的方法知道这种情况会发生。请考虑以下程序:
**全局变量还使您的程序模块化程度降低且灵活性降低。**一个只利用其参数并且没有副作用的功能是完全模块化的。模块化既有助于理解程序的功能,也有助于可重用性。全局变量显著降低了模块化。
静态变量(包括全局变量)的初始化作为程序启动的一部分,在 main 执行函数之前发生。这分两个阶段进行。
第一阶段称为 static initialization 。在静态初始化阶段,具有 constexpr 初始值设定项(包括文本)的全局变量将初始化为这些值。此外,没有初始值设定项的全局变量初始化为零。
第二阶段称为 dynamic initialization 。这个阶段更加复杂和微妙,但它的要点是初始化具有非 constexpr 初始值设定项的全局变量。
更大的问题是,没有定义不同文件之间的初始化顺序。给定两个文件,并且 b.cpp , a.cpp 任何一个都可以先初始化其全局变量。这意味着,如果 中的 a.cpp 变量依赖于 中的 b.cpp 值,则有 50% 的可能性这些变量尚未初始化。
避免使用非常量全局变量。但在某些情况下,明智地使用非常量全局变量实际上可以降低程序的复杂性,在这些罕见的情况下,它们的使用可能比替代方案更好。
日志文件就是一个很好的示例,您可以在其中转储错误或调试信息。将其定义为全局可能是有意义的,因为您可能在一个程序中只有一个日志,并且它可能会在程序中的任何位置使用。
值得一提的是,std::cout 和 std::cin 对象被实现为全局变量(在 std 命名空间内)。
根据经验,全局变量的任何使用都应至少满足以下两个条件: 变量在程序中应该只代表一个东西,并且它的使用应该在整个程序中无处不在
如果您确实发现了非常量全局变量的良好用途,那么一些有用的建议将最大限度地减少您可能遇到的麻烦。此建议不仅适用于非常量全局变量,而且可以帮助处理所有全局变量。
首先,在所有非命名空间的全局变量前面加上“g”或“g_”,或者更好的是,将它们放在一个命名空间中(在第 7.2 课 - 用户定义的命名空间和作用域解析运算符中讨论),以减少命名冲突的机会。
**其次,与其允许直接访问全局变量,不如“封装”变量。**确保变量只能从声明它的文件中访问,例如,通过使变量成为静态或常量,然后提供外部全局“访问函数”来处理变量。这些功能可以确保保持正确的使用(例如,进行输入验证,范围检查等)。此外,如果您决定更改底层实现(例如,从一个数据库移动到另一个数据库),则只需更新访问函数,而不是直接使用全局变量的每段代码。
namespace constants
{
constexpr double gravity { 9.8 }; // has internal linkage, is accessible only within this file
}
double getGravity() // has external linkage, can be accessed be other files
{
// We could add logic here if needed later
// or change the implementation transparently to the callers
return constants::gravity;
}
第三,在编写使用全局变量的独立函数时,不要直接在函数体中使用该变量。改为将其作为参数传入。这样,如果您的函数在某些情况下需要使用不同的值,您可以简单地改变参数。这有助于保持模块化。
在某些应用程序中,可能需要在整个代码中使用某些符号常量(而不仅仅是在一个位置)。这些可以包括不变的物理或数学常数(例如 pi 或阿伏伽德罗数),或特定于应用的“调整”值(例如摩擦或重力系数)。
由于 const 全局具有内部链接,因此每个 .cpp 文件都会获得链接器看不到的全局变量的独立版本。
在 C++17 之前,以下是最简单和最常见的解决方案:
1.Create a header file to hold these constants
创建一个头文件来保存这些常量
2.Inside this header file, define a namespace (discussed in lesson 7.2 – User-defined namespaces and the scope resolution operator)
在此头文件中,定义一个命名空间(在第 7.2 课 – 用户定义的命名空间和作用域解析运算符中讨论)
3.Add all your constants inside the namespace (make sure they’re constexpr)
在命名空间中添加所有常量(确保它们是 constexpr)
4.#include the header file wherever you need it
将头文件 #include 到您需要的任何位置
每次 constants.h #included 到不同的代码文件中时,这些变量中的每一个都会复制到包含代码文件中。因此,如果 constants.h 被包含在 20 个不同的代码文件中,则每个变量都会重复 20 次。
#ifndef CONSTANTS_H
#define CONSTANTS_H
namespace constants
{
// since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
extern const double pi;
extern const double avogadro;
extern const double myGravity;
}
#endif
C++17引入了一个名为. inline variables 在C++,该术语 inline 已经演变为“允许多个定义”。因此,行联变量是允许在多个文件中定义而不违反一个定义规则的变量。默认情况下,内联全局变量具有外部链接。
链接器会将变量的所有内联定义合并到单个变量定义中(从而满足一个定义规则)。
#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
inline constexpr用于在编译时求值的常量表达式,并建议编译器将其内联展开,而extern用于声明具有外部链接性的全局变量或函数,使其在其他文件中可见和可用。它们在功能和使用上有明显的区别,并且用于不同的情况和目的。
如果常量较小且在多个编译单元中直接使用,保留inline constexpr的定义是合适的,并且可以避免额外的链接过程。
如果常量较大或希望在多个编译单元之间共享同一个实例,可以使用extern将常量定义移到源文件中,并在头文件中进行声明。这样可以减少重复的内存占用和链接时间。
在第 4.13 课 – Const 变量和符号常量中,我们介绍了 constexpr 关键字,我们用它来创建编译时(符号)常量。我们还引入了常量表达式,这些表达式可以在编译时而不是运行时计算。
constexpr 函数是一个函数,其返回值可以在编译时计算。为了使函数成为 constexpr 函数,我们只需在返回类型前面使用 constexpr 关键字。
#include
constexpr int greater(int x, int y) // now a constexpr function
{
return (x > y ? x : y);
}
int main()
{
constexpr int x{ 5 };
constexpr int y{ 6 };
// We'll explain why we use variable g here later in the lesson
constexpr int g { greater(x, y) }; // will be evaluated at compile-time
std::cout << g << " is greater!\n";
return 0;
}
要符合编译时计算的条件,函数必须具有 constexpr 返回类型,并且不调用任何非 constexpr 函数。此外,对函数的调用必须具有 constexpr 参数。
#include
constexpr int greater(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int x{ 5 }; // not constexpr
int y{ 6 }; // not constexpr
std::cout << greater(x, y) << " is greater!\n"; // will be evaluated at runtime
return 0;
}
数 x 不是 y constexpr,因此无法在编译时解析该函数。但是,该函数仍将在运行时解析,将预期值作为非 constexpr 返回 int 。
不允许 constexpr 函数调用非 constexpr 函数。如果允许这样做,constexpr 函数将无法在编译时进行评估,这违背了 constexpr 的观点。尝试这样做将导致编译器产生编译错误。
只有在需要常量表达式的情况下使用返回值,则只有在编译时计算符合计算条件的 constexpr 函数才会在编译时计算。否则,不能保证编译时计算。
std::is_constant_evaluated()
#include // for std::is_constant_evaluated
constexpr int someFunction()
{
if (std::is_constant_evaluated()) // if compile-time evaluation
// do something
else // runtime evaluation
// do something else
}
们可以强制在编译时计算的 constexpr 函数在编译时实际计算,方法是确保在需要常量表达式的地方使用返回值。这需要基于每个呼叫完成。
C++20 引入了关键字 consteval,该关键字用于指示函数必须在编译时求值,否则将导致编译错误。此类函数称为即时函数。
#include
consteval int greater(int x, int y) // function is now consteval
{
return (x > y ? x : y);
}
int main()
{
constexpr int g { greater(5, 6) }; // ok: will evaluate at compile-time
std::cout << g << '\n';
std::cout << greater(5, 6) << " is greater!\n"; // ok: will evaluate at compile-time
int x{ 5 }; // not constexpr
std::cout << greater(x, 6) << " is greater!\n"; // error: consteval functions must evaluate at compile-time
return 0;
}
#include
// Uses abbreviated function template (C++20) and `auto` return type to make this function work with any type of value
// See 'related content' box below for more info (you don't need to know how these work to use this function)
consteval auto compileTime(auto value)
{
return value;
}
constexpr int greater(int x, int y) // function is constexpr
{
return (x > y ? x : y);
}
int main()
{
std::cout << greater(5, 6) << '\n'; // may or may not execute at compile-time
std::cout << compileTime(greater(5, 6)) << '\n'; // will execute at compile-time
int x { 5 };
std::cout << greater(x, 6) << '\n'; // we can still call the constexpr version at runtime if we wish
return 0;
}
constexpr 函数通常在头文件中定义,因此它们可以 #included 到任何需要完整定义的.cpp文件中。
在内联命名空间中声明的任何内容都被视为父命名空间的一部分。但是,与未命名的命名空间不同,内联命名空间不会影响链接。
未命名(匿名)命名空间
#include
namespace // unnamed namespace
{
void doSomething() // can only be accessed in this file
{
std::cout << "v1\n";
}
}
int main()
{
doSomething(); // we can call doSomething() without a namespace prefix
return 0;
}
在未命名命名空间中声明的所有内容都被视为父命名空间的一部分。因此,即使函数是在未命名的命名空间中定义的,函数 doSomething() 本身也可以从父命名空间(在本例中为全局命名空间)访问,这就是为什么我们可以在没有任何限定符的情况下调用 doSomething() from main() 的原因。
但未命名命名空间的另一个影响是,未命名命名空间中的所有标识符都被视为具有内部链接,这意味着在定义未命名命名空间的文件之外看不到未命名命名空间的内容。
内联命名空间
内联命名空间是通常用于对内容进行版本控制的命名空间。与未命名命名空间非常相似,在内联命名空间中声明的任何内容都被视为父命名空间的一部分。但是,与未命名的命名空间不同,内联命名空间不会影响链接。
#include
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;
}
如果要推送较新版本:
#include
namespace V1 // declare a normal namespace named V1
{
void doSomething()
{
std::cout << "V1\n";
}
}
inline namespace V2 // declare an inline 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 V2)
return 0;
}