learncpp-7 作用域、生存期、链接
7 作用域、生存期、链接
7.2 用户自定义命名空间和范围解析运算符
- 为了避免命名冲突,在尽可能小的作用域内定义标识符
- 一个命名空间要么在全局作用域内定义,要么在另一个命名空间内定义
- 使用范围解析运算符
::可以告诉编译器去指定命名空间查找指定标识符(如果::前没有命名空间则表示全局命名空间) - 如果使用标识符时没有带范围解析运算符,则编译器首先在使用该标识符的命名空间内查找是否有匹配的声明;如果没有则往外层的命名空间继续查找;最后查找全局命名空间
// 以下代码输出Foo; 如果注释掉Foo里的print()则输出Out;如果再注释掉Out里的print()则输出global
#include "iostream"
void print() {
std::cout << "global";
}
namespace Out {
void print() {
std::cout << "Out";
}
namespace Foo {
void print() {
std::cout << "Foo";
}
void printHelloThere() {
print();
}
}
}
int main() {
Out::Foo::printHelloThere();
return 0;
}
- 对于一个命名空间里的标识符,它们的前向声明也应该放在该命名空间里
// add.h
namespace BasicMath{
int add(int x,int y);// 在BasicMath命名空间里声明add()
}
// add.cpp
namespace BasicMath{
int add(int x,int y){ // 在BasicMath命名空间里定义add()
return x+y;
}
}
// main.cpp
#include "add.h" // 为了引入BasicMath::add()
#include <iostream>
int main(){
std::cout<<BasicMath::add(3,4)<<'\n';
return 0;
}
如果add()前向声明没有放在BasicMath命名空间里,则add()会在全局作用域中声明,则编译器会报错找不到BasicMath::add()的声明;
如果add()的定义没有放在BasicMath命名空间里,编译不会报错,但是链接器会报错BasicMath::add()未定义
- 可以在多个位置(跨多个文件或一个文件的多个位置)声明命名空间块
标准库大量使用了这个特性,因为每个标准库头文件都包含了
std命名空间的声明,否则整个标准库都得在一个头文件中定义
这意味着可以将自己的代码添加到std命名空间中,但这可能会导致未定义的行为,因为std命名空间不允许用户扩展
- 嵌套命名空间有两种形式:
namespace a{namespace b{}}或者namespace a::b{}(自从c++17支持) - 命名空间别名:
namespace c = a::b;
7.4 全局变量
- 全局变量在函数体外声明
- 全局变量可以在文件的任何地方被使用,具有全局作用域(或者叫文件作用域,即标识符的声明处到文件末尾处)
全局变量可以在全局命名空间中声明,也可以在用户自定义命名空间中声明。
只要是全局变量,都具有全局作用域。
#include <iostream>
int g_global; // global variable in global namespace
namespace Foo // Foo is defined in the global namespace
{
int g_foo{}; // g_foo is now inside the Foo namespace, but is still a global variable
}
void doSomething() {
// global variables can be seen and used everywhere in the file
g_global = 3;
std::cout << "g_global: " << g_global << '\n';
Foo::g_foo = 4;
std::cout << "Foo::g_foo: " << Foo::g_foo << '\n';
}
int main() {
doSomething();
// global variables can be seen and used everywhere in the file
g_global = 5;
Foo::g_foo = 5;
std::cout << "g_global: " << g_global << '\n';
std::cout << "Foo::g_foo: " << Foo::g_foo << '\n';
return 0;
}
// 输出结果
// g_global: 3
// Foo::g_foo: 4
// g_global: 5
// Foo::g_foo: 5
- 全局变量在程序启动时创建(在main函数执行之前),在程序结束时销毁。这称为静态生存期(static duration),具有静态生存期的变量称为静态变量。
- 局部变量没有默认初始化,具有静态生存期的变量(全局变量和静态局部变量)默认会进行零初始化。
const和constexpr修饰的全局变量必须显式初始化
- 避免使用非const全局变量
7.8 避免使用非const全局变量
- 非const全局变量的值可以在任何函数中被修改,可能需要浏览所有用到它的地方才能明白它的用处。
- 全局变量会降低程序的模块化和灵活性。(如果一个函数只用到了它的参数而没有用到全局变量的话,这能提高代码的可读性和重用性,即更好的模块化)
- 静态变量(全局变量和静态局部变量)的初始化是程序启动的一部分,早于main函数的执行,分为两个阶段
- 静态初始化:
- 具有constexpr初始化器的全局变量(包括字面量)被初始化为那些值,这被称为常量初始化。
- 没有初始化器的全局变量初始化为零,零初始化被认为是一种静态初始化,因为0是constexpr值。
- 动态初始化:初始化具有非constexpr初始化器的全局变量
- 静态初始化:
- 同一个文件中的全局变量通常按照定义的顺序进行初始化(对于动态初始化阶段有一些例外);不同文件中的静态变量的初始化顺序是不确定的(因此要避免用一个文件中的静态变量来初始化另一个文件中的静态变量)
- 建议封装全局变量,提供访问全局变量的方法,避免直接使用
- 默认情况下,全局的const变量具有内部链接,不需要设置为static
7.13 匿名命名空间和内联命名空间
- 匿名命名空间中声明的所有内容都被视为
父命名空间的一部分
#include <iostream>
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;
}
- 匿名命名空间中的所有标识符都被视为具有
内部链接,这意味着匿名命名空间的内容只能被本文件所访问(对于函数来说,这相当于将匿名命名空间中的所有函数定义为static函数)
#include <iostream>
static 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;
}
使用匿名命名空间
确保某些内容只在本文件中生效,这比将所有声明单独标记为static更加方便
匿名空间也可以使得用户自定义的类型只在本文件中生效,这没有其他方法可以做到
内联命名空间通常用于版本内容。和匿名命名空间一样,内联命名空间中所有声明的内容都被视为父命名空间的一部分,但是不会影响链接
浙公网安备 33010602011771号