342 - C++链接属性翻转
🧑💻 User
In this lecture, we’re going to see how we can flip linkage properties associated with names in ourC plus plus program.For example, if something had internal linkage, we can change it to have external linkage or the reverse.Here we have a simple program.We have our distance variable.It is a constant variable.So by default it’s going to have internal linkage, but we can change it to have external linkage byputting the external keyword in front.And when we want to use it in a separate translation unit, we are also going to prefix it with theexternal keyword.I realize this is a little bit confusing, but it is possible to do this.When we set up our constant variable like this, what we have here is going to be a declaration forthe variable we had in the main CPP file.And if we try to print it out, for example, here we are printing the distance.This is going to be accessing the variable.We have in the main function.It’s not going to be a separate variable.So this is how you can do this.This is an example of how you can take something that had internal linkage and change it to have externallinkage and you can do this.Another thing we’re going to see is how we can take something that had external linkage like a functionhere.For example, the print distance function here has external linkage by default or the other.Some function here we want to change one of these guys to have internal linkage.And the way to do that you prefix that thing with the static keyword.Okay, so we can do something like this.For example, for the function, some function here, we can prefix it with the static keyword and thisis going to mark it as an internal function to this translation unit here.If you try to call it from another translation unit, you’re going to get a compiler error because nowit will be limited to this translation unit where it is defined.Another way to do this is to wrap the function or the name that you want to have internal linkage inan anonymous namespace like we do here.If we do this, this function here will only be accessible from the translation unit where it was declared.So we have two techniques here.We can take something that had internal linkage and mark it to have external linkage.And to do that we just prefix that with the external keyword as we have seen before.Or we can take something that has external linkage and change it to have internal linkage by prefacingthat with the static keyword or wrapping that in an anonymous namespace like we see here.Now that you know this, we’re going to head over to Visual Studio code and play with this a littlemore.Okay.Here we are in our working folder.The current project is flipping linkage.We’re going to grab the template files like we always do.We’re going to put them in place and we are going to open this in Visual Studio code by dragging anddropping here.This is going to give us our main CPP file and we can start setting up things.The first thing we want to do is to turn something from internal linkage to external linkage as we seehere.And a good example of this is a const variable.We’re going to do const and that’s going to be a double variable.The name is going to be distance and we’re going to put in a distance here.You can put in whatever you want, but the main message here is that this is a constant global variable.So it’s going to have internal linkage by default.But we want this guy to be used in other translation units.For example, if we set up another file, we’re going to do that.We’re going to call this some other file dot CPP.We’re going to go in and we want to have access to this constant here and use it in our program.The way to do this is really simple.You prefix whatever you want to have external linkage with the external keyword here.And once you want to use it, you go in whatever translation unit where you want to use it and you markit as external.You say external, you say the type of the variable.So it is conceivable and we’re going to say the name of the variable.It happens to be distance.And this is going to be a declaration for the variable we had in main.What we have here is both a declaration and a definition, but what we have in some other file is goingto be a declaration.It’s not a definition.The definition for this variable is in the main function.And notice that we were able to change the default behavior for linkage for global constant variables.Okay, now that we have this, we’re going to set up a function which is going to access this guy andwe’re going to let the function have external linkage because we want to call this from our main CPPfile.We are going to include Iostream here because we want to print some things.That’s why we.Have the squiggly lines here.You’re going to see they’re going to go away and we’re going to hop over in the main function and setup a declaration for the print distance function here so we can grab its signature here or its header,put that in the main function, and then we’re going to do whatever we did before to print what we havein the main function here and what we have in some other file dot cpp here I am going to hop over inthe main function and put in the code to do that because I don’t want to type all this.We’re going to print the distance in main, we’re going to see its distance and we’re going to printthe same thing from our other translation unit and we’re going to see that they actually are the samethings, because now our constant here has external linkage.And what we have in some other file is the same thing we had in our main CPP file here.And we are effectively changing from internal linkage to external linkage for this variable here.And this is really cool.So let’s build the program and show you that this is actually what we expect.Let’s see if the world is good, the world is good here.We can clear, but let’s bring up a terminal.First.We’re going to clear and run Rooster.You’re going to see that what we have in Main is 45.7.The address is what we see here.It ends in zero 50.And what we have in some other file is exactly the same thing, because now this constant has externallinkage and this is how you can change the linkage for your constant global variables to be externallinkage.If this happens to be making sense for your project.The other thing we’re going to see is how we can change from external linkage to internal linkage.And we know that functions have external linkage by default.So for example, if we have a function in our other translation unit, we’re going to make it not returnanything and it’s going to be called some other function.We can do this and it’s just going to say, Hello there, let’s do that.Okay, so this function has external linkage by default because that’s how C plus plus compilers wehave, we have external linkage here, but we wanted to change this to go from external linkage to internallinkage.So we want this function only usable inside this translation unit here.But before we do that, let’s make sure that this function is usable from the other translation unitHere.For example, we can put in its declaration.We’re going to say, what is it called?The name is some other function.So we’re going to grab the signature for this little guy here.We’re going to hop over in Main and we’re going to put in the declaration for this.This is a declaration, let’s say that to be super clear and we can call it in main, for example,we can go down and put a separator.This is going to do and we’re going to say some other function.That’s what it’s called.I think we have its name in our clipboard so we can take out the return type here and turn this intoa function call.This is going to work just fine.If we build and run the program, we’re going to see that the world is going to be good because thisguy has external linkage.We want to change the linkage of this guy.One way to do this is to prefix it with a static keyword, and at the moment you do this, the C plusplus compiler is going to look at this and note that this function can only be used or called from thistranslation unit here.Now, if we try to build our program, we’re going to see that we’re going to get a compiler error andthe compiler error is going to say undefined reference to some other function.We have this function and this translation unit here, but when the linker is going to look for it becauseof this call here, the compiler is going to say this function is only for use in this translation unit.You can’t call it from the main CPP file here, which is another translation unit.And we have effectively changed the linkage of this function from external to internal using this statickeyword here.This is what I want you to know, but this is not the only way to do this.Another way to do this is to wrap this function in an anonymous namespace.So if we go down here and say namespace and not give this namespace a name and we’re going to grab ourfunction here and move that inside our namespace here, we’re going to take out the static keyword.We’re going to see that we have the same effect here we’re going to build.And we are going to get an undefined reference to our function here because it is internal to this function.We have effectively changed from external linkage to internal linkage here.So we have seen two ways we can change the linkage.We can change from internal linkage to external linkage using the technique here, prefixing thingswith the external keyword and declaring them using the external keyword.This is going to tell the compiler that what we have in some other translation unit is just a declaration.We can also change from external linkage to internal linkage by prefixing what we want to set up withthe static keyword.Or we can wrap our name in an anonymous namespace and this is going to give it internal linkage andyou want to be able to use that thing from other translation units.For example, if we go into the main function and call some other function, this is not going to workand we have effectively changed the default behavior for linkage.This is really all I wanted you to see in this lecture, that you can change the linkage if you want,but don’t really overdo this because it’s going to make your code not easy to read and understand forother developers.Another thing is that the static keyword is really not welcome in modern C plus plus.If you happen to want to change from external linkage to internal linkage, wrapping your things insideunnamed or anonymous namespaces is the way to go in modern C plus plus.We are going to stop here in this lecture.In the next one, we’re going to learn about inline variables and functions.Go ahead and finish up here and meet me there.
🤖 Assistant
好的,同学你好!很高兴能成为你的C++启蒙老师。别担心,C++虽然知识点多,但只要我们一步一个脚印,用正确的方法学习,你会发现它的魅力所在。
今天我们要学习的是C++中一个非常重要但有时又有点微妙的概念——链接属性 (linkage),以及如何“翻转”它。
1. 标题与概要
标题: C++ 链接属性揭秘:轻松翻转内部与外部链接
概要: 本节课我们将学习什么是C++中的链接属性,特别是内部链接 (internal linkage) 和外部链接 (external linkage)。我们将重点探讨如何使用 extern
关键字将默认具有内部链接的 const
(常量) 全局变量改为外部链接,以及如何使用 static
(静态) 关键字或匿名命名空间 (anonymous namespace) 将默认具有外部链接的函数改为内部链接。这将帮助我们更好地组织代码和控制符号 (symbol) 在不同编译单元 (translation unit) 间的可见性。
2. 详细解释
在我们开始之前,首先要理解几个核心概念:
- 符号 (Symbol): 在编程中,符号通常指变量名、函数名等标识符。
- 编译单元 (Translation Unit): 简单来说,一个
.cpp
文件(以及它包含的所有头文件,经过预处理器处理后)通常被视为一个编译单元。编译器会分别编译每个编译单元,生成目标文件 (object file)。 - 链接 (Linkage): 链接是编译器和链接器用来确定一个标识符(如变量或函数名)在不同编译单元中是否指向同一个实体的规则。它决定了一个名称的“可见范围”。
C++ 主要有两种链接属性:
内部链接 (Internal Linkage):
- 具有内部链接的符号只在它自己所在的编译单元内可见。即使其他编译单元声明了同名的符号,它们也互不相关,是各自独立的存在。
- 默认情况:
const
(常量) 和constexpr
(常量表达式) 全局变量,以及static
(静态) 全局变量和static
函数(非类成员)默认具有内部链接。匿名命名空间中的所有符号也具有内部链接。
外部链接 (External Linkage):
- 具有外部链接的符号可以在多个编译单元之间共享。也就是说,在一个编译单元中定义的外部链接符号,可以在其他编译单元中声明并使用,它们都指向同一个实体(同一块内存地址或同一个函数实现)。
- 默认情况: 普通的全局变量(非
const
、非static
)和普通函数(非static
、非匿名命名空间内)默认具有外部链接。
现在,我们来看看如何“翻转”这些默认的链接属性。
(1) 将内部链接翻转为外部链接
正如我们所说,const
全局变量默认具有内部链接。这意味着如果你在一个 main.cpp
文件中定义了一个 const
全局变量,例如:
1 | // main.cpp |
那么在另一个 other.cpp
文件中,你无法直接访问到 main.cpp
中的这个 PI
。如果你在 other.cpp
中也写 const double PI = 3.14159;
,那么 other.cpp
会拥有一个自己的、独立的 PI
变量,它和 main.cpp
中的 PI
不是同一个。
如何改变?使用 extern
(外部) 关键字。
extern
关键字可以用来告诉编译器,某个变量或函数的定义在别处(可能在当前编译单元,也可能在其他编译单元)。
步骤一:在定义处使用
extern
当你想让一个const
全局变量具有外部链接时,你需要在它的定义处使用extern
关键字。cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// main.cpp
// 定义一个 const 全局变量,并使用 extern 显式声明其为外部链接
extern const double DISTANCE = 45.7; // DISTANCE 现在具有外部链接
// 声明一个在 other.cpp 中定义的函数
void print_distance_from_other();
int main() {
std::cout << "In main.cpp: distance is " << DISTANCE
<< ", address is " << &DISTANCE << std::endl;
print_distance_from_other();
return 0;
}这里的
extern const double DISTANCE = 45.7;
是一个定义 (definition),因为它分配了存储空间并提供了初始值。通过extern
,我们覆盖了const
变量默认的内部链接行为,使其变为外部链接。步骤二:在其他编译单元使用
extern
进行声明
在其他需要使用这个DISTANCE
变量的编译单元中,你需要使用extern
来声明 (declaration) 它。声明告诉编译器这个变量存在,并且具有外部链接,但定义在别处。cpp1
2
3
4
5
6
7
8
9
10
11
12// other.cpp
// 声明 main.cpp 中定义的 DISTANCE 变量
// 这个声明告诉编译器 DISTANCE 是一个在别处定义的 const double 类型变量
// 并且它具有外部链接
extern const double DISTANCE;
void print_distance_from_other() {
std::cout << "In other.cpp: distance is " << DISTANCE
<< ", address is " << &DISTANCE << std::endl;
}这里的
extern const double DISTANCE;
是一个声明,它不分配内存,只是告诉编译器DISTANCE
的类型和名称,并指明它是在其他地方定义的。
关键点:
通过这种方式,main.cpp
和 other.cpp
中的 DISTANCE
指的是同一个变量。如果你打印它们的地址,会发现地址是相同的。这就实现了从内部链接到外部链接的翻转。
(2) 将外部链接翻转为内部链接
默认情况下,普通函数(非成员函数)具有外部链接。这意味着如果你在一个 helper.cpp
中定义了一个函数,你可以在 main.cpp
中声明并调用它。
1 | // helper.cpp |
1 | // main.cpp |
有时候,我们可能希望一个函数只在它自己定义的那个 .cpp
文件内部使用,不希望被其他编译单元意外地调用,或者为了避免与其他编译单元中可能存在的同名函数发生命名冲突。这时,我们就需要将它的外部链接改为内部链接。
有两种主要方法:
方法一:使用
static
(静态) 关键字
在函数定义前加上static
关键字,可以将其链接属性改为内部链接。cpp1
2
3
4
5
6
7
8
9
10// helper.cpp
static void this_is_an_internal_function() { // 使用 static,变为内部链接
std::cout << "This function can only be called within helper.cpp" << std::endl;
}
void call_internal_from_helper() {
this_is_an_internal_function(); // 在 helper.cpp 内部可以调用
}现在,如果你尝试在
main.cpp
中声明并调用this_is_an_internal_function()
:cpp1
2
3
4// main.cpp
// void this_is_an_internal_function(); // 尝试声明
// ...
// this_is_an_internal_function(); // 尝试调用链接器在链接阶段会报错,因为它在
main.cpp
的编译单元找不到this_is_an_internal_function
的定义(因为它在helper.cpp
中是内部链接的,对外部不可见)。方法二:使用匿名命名空间 (Anonymous Namespace)
匿名命名空间(也叫未命名命名空间)是一种更现代、更推荐的做法,用来限制符号的链接为内部链接。所有在匿名命名空间中声明的符号(变量、函数、类等)都自动具有内部链接。cpp1
2
3
4
5
6
7
8
9
10
11
12// helper.cpp
namespace { // 这是一个匿名命名空间
void another_internal_function() { // 自动具有内部链接
std::cout << "This function (in anonymous namespace) is also internal to helper.cpp" << std::endl;
}
} // 匿名命名空间结束
void call_another_internal_from_helper() {
another_internal_function(); // 在 helper.cpp 内部可以调用
}效果与使用
static
关键字类似。another_internal_function
也无法从main.cpp
或其他编译单元中直接访问。
为什么匿名命名空间更好?static
关键字在C++中有多种含义(例如,静态局部变量、静态成员变量、静态成员函数),这有时会引起混淆。匿名命名空间的目的非常明确:提供内部链接。因此,在现代C++中,当需要将非成员函数或全局变量限制在当前编译单元时,推荐使用匿名命名空间。
注意:
- 不要过度使用链接翻转技术。默认的链接行为通常是合理的。仅在确实需要控制符号可见性或解决命名冲突时才使用。
static
关键字对于类成员(变量或函数)有不同的含义,与链接属性无关,而是与类的实例或类型本身相关联。我们这里讨论的是全局作用域或命名空间作用域下的static
。
3. 代码示例
我们将整合上面的例子,创建两个文件:main.cpp
和 utils.cpp
。
utils.cpp
文件:
1 |
|
main.cpp
文件:
1 |
|
如何编译和运行 (以 g++ 为例):
1 | g++ main.cpp utils.cpp -o program |
预期输出:
1 | main.cpp: Accessing SHARED_CONSTANT_VALUE: 9.81 (Address: 0x........) |
你会注意到
SHARED_CONSTANT_VALUE
在 main.cpp
和 utils.cpp
中的地址是相同的,证明它们是同一个变量。而 internal_via_static
和 internal_via_anonymous_namespace
无法在 main.cpp
中直接调用。
4. QA 闪卡 (QA Flash Cards)
问:
const
全局变量默认具有什么链接类型?
答: 内部链接 (Internal linkage)。问: 如何将一个
const
全局变量的链接类型从内部改为外部?
答: 在其定义处使用extern
关键字 (e.g.,extern const int VAL = 10;
),并在其他需要使用它的编译单元中也使用extern
进行声明 (e.g.,extern const int VAL;
)。问: 普通的非成员函数默认具有什么链接类型?
答: 外部链接 (External linkage)。问: 有哪两种主要方法可以将一个默认具有外部链接的函数改为内部链接?
答: 1. 在函数定义前加上static
关键字。 2. 将函数定义在一个匿名命名空间中。问: 在现代C++中,如果想将一个函数改为内部链接,更推荐使用
static
还是匿名命名空间?为什么?
答: 推荐使用匿名命名空间。因为static
关键字在C++中有多种含义,可能引起混淆,而匿名命名空间的意图更清晰,就是为了提供内部链接。问: 什么是编译单元 (Translation Unit)?
答: 通常指一个.cpp
源文件及其包含的所有头文件,在经过预处理器处理后形成的一个单元,编译器会独立编译它。
5. 常见误解或易犯错误
extern
的滥用与误解:- 错误: 在多个
.cpp
文件中都写extern const int MY_CONST = 100;
。这会导致多重定义 (multiple definition) 链接错误。extern
变量的定义(带初始化的)只能在一个编译单元中出现。其他编译单元应该只用extern const int MY_CONST;
进行声明。 - 混淆声明与定义:
extern int x;
是声明。int x;
在全局作用域是定义(如果之前没有声明过,则为暂定性定义,若链接时无其他定义,则为0初始化)。extern int x = 10;
是定义。
- 错误: 在多个
static
关键字的多义性:- 误解: 认为函数内的
static
局部变量也与链接属性有关。函数内的static
变量表示它具有静态存储期(生命周期贯穿整个程序),并且只初始化一次,它的作用域仍然是局部的,与链接属性无关。我们这里讨论的是全局或命名空间作用域的static
对链接属性的影响。
- 误解: 认为函数内的
忘记在其他文件中使用
extern
声明:- 如果在
file1.cpp
中定义了extern const int VAL = 5;
,但在file2.cpp
中直接写const int VAL = 10;
(没有extern
),那么file2.cpp
会认为这是一个新的、独立的、具有内部链接的VAL
,而不是引用file1.cpp
中的那一个。如果file2.cpp
中尝试使用VAL
但没有自己的定义也没有extern
声明,则会报未定义符号错误。
- 如果在
无法区分编译器错误和链接器错误:
- 链接属性问题通常导致链接器错误 (linker error),例如 “undefined reference to
symbol_name
“ 或 “multiple definition ofsymbol_name
“。而语法错误、类型不匹配等通常是编译器错误 (compiler error)。
- 链接属性问题通常导致链接器错误 (linker error),例如 “undefined reference to
不必要的链接翻转:
- 新手可能因为刚学了就想用,但实际上很多时候默认的链接属性就是最合适的。过度翻转链接属性会使代码可读性变差,更难维护。
对匿名命名空间的误解:
- 认为匿名命名空间只是“隐藏”了名字。实际上,它确实为其中的符号提供了内部链接,使得这些符号在不同编译单元中即使同名也是独立的。
6. 编码练习
现在,请你来动手实践一下。这里有两个文件,exercise_main.cpp
和 exercise_helper.cpp
。你的任务是补全代码,使得程序能够按预期工作。
exercise_helper.cpp
:
1 |
|
exercise_main.cpp
:
1 |
|
你需要将 _______
替换为正确的关键字,并补全函数体和调用。完成后,尝试编译这两个文件并运行程序。
等你完成后,把你的 exercise_helper.cpp
和 exercise_main.cpp
的代码发给我,我会帮你批改并给出解答和进一步的建议。加油!