avatar

目录
341 - Linkage Cpp

341 - Linkage Cpp

🧑‍💻 User

In this lecture we’re going to learn about linkage and linkage is a property associated with names inyour C plus plus program.We’re going to be exploring this in the context of variable names and function names.And this property is going to control how visible the name is across translation units.Okay, so suppose we have a function here and inside our function we have a variable declared.Inside we have the age variable, the value is six.And the job of this function is to print this out.If you look at this age variable here, it’s really only accessible inside this function.You can’t even access it from the outside of this function here.So we say that this variable, the name for the variable here, this age here, it has no linkage.And what we mean by that is that it really isn’t accessible from anywhere in the global scope of thefile where it is or even in other translation units.So it has no linkage.That’s what we say here.If we look at another example here, we have a variable.For example, it is declared in some file.This is a constant variable and by default, in your C plus plus program, const variables are goingto have internal linkage.What we mean by that is that they can only be accessed or used in the translation unit where they weredeclared and defined.If you try and set up another variable of the same name in another translation unit, you’re not goingto be picking up the name of this variable here.You’re going to be setting up a completely different variable and we’re going to see an example of thisonce we hit Visual Studio code in a minute.But note that constant variables like this have internal linkage by default.And what we mean is that they can only be accessed and used from the translation unit where they weredeclared and defined.So this is another kind of linkage we can have in C plus plus.The first one is null linkage.We can also have internal linkage and it is what we just learned about in this slide here.Another kind of linkage we can have is external linkage.And a good example for this is a global variable which is not const.This is going to have external linkage by default.And what we mean by that is that this variable can be accessed and used in different translation unitsthan the one it was declared and used in.So for example, if this was declared in file one, it is possible to access and use this variable ina separate translation unit.And this is really cool and this is what we mean by external linkage functions.Also default to external linkage in C plus plus you can declare the functions in one translation unitsand use them in a separate translation unit.And we have seen that at multiple occasions.So in C plus plus, we have a lot of options for linkage.We can have no linkage.For example, if a variable is local to a function, we can have internal linkage.And what we mean by that is that this name is not going to be usable or accessible from other translationunits.We can have external linkage.And what we mean by that is that the name can be accessible and used in other translation units, separatefrom the translation units where it was declared and defined.We can also have module linkage, but we don’t have enough tools to really understand this.So in this lecture we’re just going to focus on the three options null linkage, internal and externallinkage.Okay, so let’s recap what we just talked about here.Function.Local variables have no linkage, so they can’t really be accessed even from the outside of the function.If you try to do that, you’re going to get a compiler error cause const global variables have internallinkage by default, so they can only be accessed and used from the translation unit where they weredeclared and Non-const global variables have external linkage.By default they can be used in other translation units.And we’re going to see a few examples of this in a minute.Functions have external linkage and they can be used from the translation units different from the onethey were declared in.Now that you have an idea about what linkage is, again, it is a property associated with names, forexample, variable names, function names, and this property is going to control how visible the nameis across different translation units in your C plus plus program.Now that we know this, we’re going to head over to Visual Studio code and play with this a little more.Okay.So here we are in our working folder.The current project is linkage.We’re going to grab our template files, we’re going to copy them pretty fast and put them in our folderhere.And we are going to open this little guy in Visual Studio code.This is going to give us our main.cpp file.We can clean it up a little bit.And we’re going to close the sidebar here because we don’t need this by now.So the first thing we’re going to look at is no linkage.And the way to set up this example, we’re going to set up a simple function.We’re going to say some function, and inside the function we’re going to set up a variable, call itage and initialize that with a 34, for example.This could work.And what we’re going to do is print this out.We’re going to print the age, but we’re also going to print the address of this age variable.So we’re going to say address of age.We can say that in our C plus plus program.You already know this and we’re going to print the address of age here and we’re going to say Stdendl.And I don’t know why we have this problem here.Why do we have this squiggly lines?We don’t have a pair of parentheses here.That’s the problem.So we can make this work now.And if we’re trying to compile this program, it is going to compile.But before we do that, we’re going to say some function, we’re going to call it, and it is goingto print what we see here.Let’s do that.We’re going to build our program with GCC.And you’re going to see that the world is good.We’re going to bring up a terminal and play with us a little bit.We’re going to run Rooster and we’re going to see our age to be 34.And the address of this variable in memory is what you see right here.But we can separate these guys by putting a space here so that the output is better.Let’s build again.To make sure this is working, we can clear and run Rooster.Now we have the edge here.And the address of this variable is here.It is ending in DBC.But what I want you to see is that the edge variable here is not accessible anywhere outside this functionhere.So, for example, if you try and come in main here and try to print the edge, for example, we’regoing to say edge here.If you try and access this like this, you want to be using this variable here because it isn’t reallyaccessible from the outside.If you’re trying to compile this program, you’re going to see a compiler error that says that edgehere is not declared or defined anywhere.So you’re going to see edge was not declared in this scope, so the compiler doesn’t know what it is.Even if you try and come to the top here and for example, say int H equals 33, for example, whatyou do here is not assigning a value to this edge variable.You are declaring a completely different variable in the global scope of the main function.So the variable we have inside the function and the variable we have on the outside here are different.Let’s try and compile this and see if it works.You’re going to see that the world is good, but if we print the edge, we’re going to see 33, whichis not what we have in our function here.You’re going to see 33.And if we print the address of this, we can do that.We can say edge here.So let’s do that.We’re going to say H, we’re going to put out to the edge and we’re going to put out the address ofthis little guy.We’re going to say address of H and we’re going to do the exact same thing we did in our function here.So if we do this, we will be printing the address of the outside variable here.It won’t be the variable inside our function.So if we do this and say Stdcout.And put a simple separator here so that we can see this fence of the terminal and go down and call somefunction.You can try and guess what we’re going to see.We’re going to see the age printed out.The first one is going to be 33 here and the address is going to be the address of this variable onthe outside.And after our separator here, we’re going to call the function.We’re going to print 34 and we’re going to see a different address.And I am going through this trouble to really try and prove that we can’t access this age variable hereoutside this function.So this guy has no linkage.It can’t be accessed outside this function here.This is what we mean by saying that it has no linkage.Let’s go ahead and run this program so that you can see the output so the world is good.We can clear and run Rooster.We’re going to see first we’re going to see 33.The address is going to be ending in zero 20 and we’re going to see 34 and the address is going to beending in DBC.So they are different variables and the function local variables have no linkage.That’s what we mean here.You can’t access them from the outside of the function.Even in the file where the function was declared and defined and you can’t even access them in a separatefile.For example, if we go here and set up some other file, let’s do that.We’re going to set up some other file dot cpp.If we, for example, set up some function, we’re going to do void.We’re going to call this do something.If we go in and try and print the age and do something like this, for this to work, we need to includeIostream.We’re going to include that.If we do this, we’re going to get a compiler error and the compiler is going to complain that it doesn’tknow what age is because the age we have here can’t really be accessed from anywhere other than withinthis curly braces where this variable is used here and declared.So I hope this is really clear.Let’s try and compile and show you this compiler error we expect from this guy here we’re going to build.Where the JCC and we’re going to see that age was not declared in this scope.The error is coming from some other file dot cpp at line four, and that’s exactly here.So you can’t really do this.Hopefully this drives the point home that function.Local variables have no linkage.Now that we have looked at this, let’s focus on global variables.We’re going to take out this entire thing here because we don’t need this anymore.We’re going to build and make sure the program is compiling correctly.You see, the world is good, so we are good.We’re going to go back in the main CPP file and we’re going to set up a constant variable.We can go down here and actually remove this.Now we have made our point and we don’t want this to confuse us and we’re going to go down and set upa single const variable.We’re going to say const double and it’s going to be called distance.You can call it whatever you want, and we’re going to put in a variable maybe 45.8.Let’s say this is the distance to go somewhere.Okay?So we have this variable here.If we want to use this variable in this translation unit, we can really use it.We can go down and say Stdcout, we’re going to say distance and we’re going to print the distance ofthis variable here.This is going to be important in a minute, and the address of this guy is going to be address of distance.We can close the left sidebar here and make sure this is saying distance.And we’re going to put our new line character using Endl here.And this is going to work.If we build and run this program, we’re going to run the task to build with GCC.The build is going to be good so we can clear and run Rooster.This is going to print our distance.No problem here.The problem is going to come if we want to use this variable in a separate translation unit.So, for example, if we go in our some other file and try to set up something like this, suppose wewant to access this variable from this file here, we can grab the declaration here and the definitionand use that here.For example, we can say const double distance and not put in a value here and hope that inside herewe have this value we have from the main CPP file and this is going to surprise you.Let’s set up a function that is going to print this variable, but it’s going to be printing what wehave in this other translation unit here.So we’re going to say void and we’re going to call this print distance and we’re going to say stdcoutdistance from other file.I think this is going to be descriptive enough, so we’re going to print the distance, but we’re alsogoing to print the address of the variable here.So address of distance is going to be the address of distance, and we’re going to put our new linecharacter.This is going to work.So this is a function declared in some other file or defined.That’s what I should say.So we’re going to come back in main and we’re going to put in a definition for the print distance function.So we’re going to say void print distance.I think it doesn’t take anything.And so we’re going to do this.This is going to do if we go in main and call that function, we’re going to put in a separator.Okay.So let’s try and think about what is going to happen if we run this program.We’re going to print the distance from the variable we have in Main, and we’re going to also call theprint distance function we have in some other file and hope that it’s going to access the distance variablewe have in main because we think this is a declaration for the variable we have in main.So that should bring that in.But what I want you to see is that constant variables have internal linkage by default.So we can’t really access this variable from any other translation unit.If we try to do that like we do here, this is going to be a complete declaration and the definitionof another variable and this is going to initialize this variable to junk.So we’re going to see that junk value printed out here and not what we have in the main function.So the first print out is going to print what we have in main.It’s going to print this value here.It’s going to print its address in memory and the print distance function.It’s going to print some junk value and it’s going to print the address of that junk value.But the addresses are going to be different and the values are also going to be probably different.That’s what we expect here.We’re going to build this with GCC and we’re going to see a problem here.We’re going to see an initialized constant distance.So if you declare a constant, you also have to declare it.So we can’t even do this.And.This is really good.The compiler is helpful here.So in hope to get this to work, we can try and put a pair of braces here to initialize this variableand we’re going to run the test to build with GCC and you’re going to see that the world is good.So if we try to clear and run Rooster, we’re going to see that in Main.We’re going to have our distance here.It’s going to be living at this address.But the value in our some other file is going to be zero because we initialize that to zero and it’sgoing to be a completely different variable because you can’t really access a constant variable or aconstant global variable declared in another file.And we have a constant distance variable here.It has internal linkage.It can only be used from the translation unit where it was declared.This is the message here.So you can’t really do what we are trying to do here.If you do this, this will be a completely separate variable.And we have proven that because we see the addresses to be different here.What we have in Main is living at this address ending in 060.What we have in some other file is living at an address ending in zero, a zero.So they are two completely different variables.We can’t really access this variable from any other translation unit.It has internal linkage.This is what we mean here.Okay.So now that we know about internal linkage, we’re going to learn about external linkage and we’re goingto go down and set up another variable, we are going to say INT and we’re going to call this item count.Why not?And we’re going to put in a value of six.Why not?And because this is a not constant global variable, this thing is going to have external linkage.I really want you to know this.And what this means is that we can access this guy even from other translation units and it’s goingto be the exact same variable.The memory is going to be the same if we print that out.And to really show this, we’re going to go back in some other file and try to access our item count.But to do that, we need to mark this guy as a variable defined in some other translation unit.And to do that, we use the external keyword.We’re going to say extern, we are going to say its type and we’re going to say the name of the variable.We’re going to say item count.And this is going to be a declaration of a variable that was defined in some other translation unit.This is what we mean here.So if, for example, we set up a function to print the item count, we’re going to see that what weare accessing is what we have in the main function here, because this guy has external linkage.It can be accessible and used even from other translation units like some other file here.So let’s go down and do that.We’re going to set up a function.We’re going to say void print item count, and we’re going to put up a parameter list and we’re goingto say stdcout item count from other file, and we’re going to say Stdendl to go to the new line.We should separate these guys a little bit and we’re going to just grab this thing and use this in main.So we’re going to comment out the things we had for the distance, because they should be just confusingus now.And we’re going to put a line to print from the main function.Let’s do that.This is going to print item count that we have in main here.So let’s say that this is from Main and we’re going to put a separator N and then we’re going to setup a declaration for the print item count function.We can go on top here and do that.And if we have this n, we can go down and call the function.So let’s do that.We can say print item count and if we’re build and run this program, we try to think about what we’regoing to get.The first print here is going to print the variable we have in main, which is declared right here.So it’s going to print the item count in Main.It’s going to print the value, which is going to be a six, and it’s going to print the address ofthis variable because this variable is not cost.It has external linkage by default.So it can be accessible even from other translation units like some other file.Here we have it declared.Again, this is not a definition of a variable.It is just a declaration of a variable we have defined in some other translation unit.So what we really have here is a way to access something that we have declared and defined in the mainfunction here.So if we print the item count here, we will be accessing what we have in the main function.So they are going to be the same.The value is going to be the same.And the addresses are going to be the same.That’s what we expect here.Let’s bring this up and we’re going to build the program to make sure we get what we expect.The world is going to be good, so we’re going to clear and run.Rooster You see that the value is the same and the addresses in memory are of the same.So this should really prove that external linkage means that you can access the variable from othertranslation units.And the mechanism we have is to mark the variable as extern.And this is going to mean that it was defined in some other variable, but we can access it and useit even from other translation units like we are doing in this function here.And this is really cool, but this is not just limited to non-const global variables.Even functions have external linkage by default.Okay, so but for function we don’t really need to mark them as external or anything.They have external linkage by default.For example, we had declared and defined the print item function in some other file here, but we areable to access it and use that even from the main CPP file here, which is another translation unit.So functions have external linkage by default and we can go in here and say that, but if you want toreally be explicit, you can go down and say that they are external.You can come and put the external keyword in front.This is not going to hurt anything, but it’s going to be just repeating yourself because they are externalby default.If we try and build the program, you’re going to see that this is going to build and run without aproblem.So the build is good.You can put this external keyword here or you can leave it, but note that functions have external linkageby default.This is really all I wanted you to see in this lecture.Learning about the concept of linkage.It is a property that you associate to your names in your Cplusplus program.That property is going to control how accessible the name is going to be a cross translation unit.And we had a chance to look at three kinds of linkage in this lecture here.We have seen that variables may have a null linkage, for example, local variables to functions.We have seen that some variables have internal linkage, for example, const global variables and wehave seen that some other variables can have external linkage like the item count here.And this should be super clear.We are going to stop here in this lecture and the next one we’re going to zoom in on global externalvariables like we have this guy here.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,各位未来的C++大师们!欢迎来到我们的C++学习之旅。


C++ 名字的“可见度”:深入理解链接属性(Linkage)

摘要: 本节课将带你深入理解C++中“名字”(变量名、函数名等)的链接属性(Linkage),它决定了一个名字在不同翻译单元(Translation Unit)之间的可见性,以及它是否引用同一个实体。我们将重点探讨无链接(No Linkage)、内部链接(Internal Linkage)和外部链接(External Linkage)这三种类型。


1. 什么是链接属性?

在C++中,链接属性(Linkage)是与你的程序中名字(Name),例如变量名、函数名等,关联的一个重要特性。它控制着这些名字在不同的翻译单元(Translation Unit)之间如何被看见和使用。理解链接属性对于编写大型、模块化的C++程序至关重要。

首先,我们得清楚什么是“翻译单元”。简单来说,一个翻译单元(Translation Unit)通常指一个独立的 .cpp 源文件及其所包含(通过#include)的所有头文件,在经过预处理器处理之后,生成的一个可以直接编译的单一文件。你的整个程序可能由多个这样的翻译单元组成,它们最终会被链接器(Linker)组合起来形成可执行文件。

链接属性就是用来告诉编译器和链接器:这个名字,是只在这个翻译单元里有效,还是可以在其他翻译单元里也能访问到,又或者根本就不能从这个名字以外的地方访问。

C++中的链接属性主要有以下几种类型:

  • 无链接(No Linkage): 名字只在它被定义的作用域(Scope)内可见和使用,无法从其作用域外部访问,更不能在其他翻译单元中访问。
  • 内部链接(Internal Linkage): 名字可以在它被定义的那个翻译单元的全局作用域(Global Scope)内被访问和使用,但无法从其他翻译单元中访问。每个翻译单元如果定义了同名的内部链接实体,它们都是独立的,互不影响。
  • 外部链接(External Linkage): 名字可以在它被定义的翻译单元中被访问,也可以在其他翻译单元中被访问和使用。所有引用同一个外部链接名字的地方,都指向内存中的同一个实体。
  • 模块链接(Module Linkage): 这是C++20引入的新概念,用于模块(Module),目前我们暂时不深入探讨,先专注于前三种。

接下来,我们将通过具体的代码示例来深入理解这三种最常见的链接属性。

2. 无链接(No Linkage)

当一个名字没有链接属性时,它仅仅在它被声明的局部作用域(Local Scope)内可见。这意味着你不能在定义它的函数外部访问它,更不能在其他翻译单元中访问它。

示例:函数内的局部变量

函数内部声明的局部变量,如age,它们只在函数内部的块作用域(Block Scope)内有效。一旦函数执行完毕,这些变量通常就会被销毁,并且在函数外部根本无法通过这个名字来访问它们。

main.cpp

cpp
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream> // 引入输入输出流库

// 假设我们有一个函数
void someFunction() {
int age = 34; // 这是一个函数局部变量,它具有“无链接”属性
std::cout << "在 someFunction 内部: " << std::endl;
std::cout << "age 的值: " << age << std::endl;
std::cout << "age 的地址: " << &age << std::endl; // 打印变量的内存地址
}

int main() {
// ------------------------------------------
// 演示无链接 (No Linkage)
// ------------------------------------------
std::cout << "--- 演示无链接 ---" << std::endl;

// 1. 调用 someFunction,内部的 age 变量会被创建并使用
someFunction();

std::cout << "\n--- 尝试访问函数外部的局部变量 ---" << std::endl;
// 2. 尝试在 main 函数中访问 someFunction 内部的 age 变量
// 这会导致编译错误,因为 age 只在 someFunction 内部可见。
// std::cout << "在 main 内部尝试访问 someFunction 的 age: " << age << std::endl;
// ^^^ 如果取消注释这一行,会报错:'age' was not declared in this scope

// 3. 证明即使在同一个文件,不同作用域的同名变量也是独立的
int age = 33; // 这是 main 函数自己的局部变量,与 someFunction 里的 age 毫无关系
std::cout << "在 main 内部: " << std::endl;
std::cout << "main 自己的 age 的值: " << age << std::endl;
std::cout << "main 自己的 age 的地址: " << &age << std::endl; // 打印 main 自己的 age 的地址

// 再次调用 someFunction,看看它内部的 age 仍然是独立的
std::cout << "\n--- 再次调用 someFunction ---" << std::endl;
someFunction(); // 再次调用,可以看到 someFunction 内部的 age 仍然是 34,且地址可能不同(如果栈分配)

std::cout << "\n--- 演示无链接 (跨文件) ---" << std::endl;
// 4. 尝试在另一个翻译单元中访问。
// 假设我们有另一个文件 some_other_file.cpp
// 里面有一个函数尝试访问 someFunction 里的 age。
// 这同样会失败。
// 在这里,我们需要声明 someOtherFunction 的原型,因为它在另一个文件中定义
void doSomething(); // 声明在 some_other_file.cpp 中定义的函数
doSomething(); // 调用在 some_other_file.cpp 中定义的函数

return 0;
}

some_other_file.cpp

cpp
1
2
3
4
5
6
7
8
#include <iostream>

// 尝试在另一个翻译单元中访问 main.cpp 中 someFunction 里的 age 变量
void doSomething() {
std::cout << "在 some_other_file.cpp 内部的 doSomething 函数: " << std::endl;
// int age_from_main_function = age; // 这将导致编译错误:'age' was not declared in this scope
std::cout << "无法直接访问 main.cpp 中 someFunction 里的局部变量 age。" << std::endl;
}

解释:
当你在main函数中尝试访问someFunction内部的age时,编译器会报错'age' was not declared in this scope(age在此作用域中未声明)。这清楚地表明age只在someFunction作用域内可见。即使你在main函数中声明了一个同名的age变量,它们也是完全独立的实体,位于不同的内存地址,互不干扰。这说明了局部变量的“无链接”特性。

3. 内部链接(Internal Linkage)

具有内部链接属性的名字,只能在定义它的那个翻译单元内部被访问和使用。即使你在另一个翻译单元中声明了一个同名的实体,它们也是完全独立的,互不影响。这就像每个文件都有自己私有的同名副本。

示例:const 全局变量

在C++中,const修饰的全局变量(Global Variable)默认具有内部链接属性。

main.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

// 这是一个 const 全局变量,它默认具有“内部链接”属性
const double distance = 45.8; // 距离某个地方

// 声明在 some_other_file.cpp 中定义的函数原型
void printDistance();

int main() {
std::cout << "\n--- 演示内部链接 ---" << std::endl;
std::cout << "在 main.cpp 内部: " << std::endl;
std::cout << "distance 的值: " << distance << std::endl;
std::cout << "distance 的地址: " << &distance << std::endl; // 打印在 main.cpp 中 distance 的地址

std::cout << "\n--- 尝试在另一个翻译单元中访问 distance ---" << std::endl;
printDistance(); // 调用在 some_other_file.cpp 中定义的函数

return 0;
}

some_other_file.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

// 尝试在另一个翻译单元中“访问” main.cpp 中的 distance。
// 但由于 const 全局变量默认是内部链接,这里实际上是定义了一个全新的、独立的 distance 变量。
// 注意:如果这里不初始化,会报错 "uninitialized const 'distance'"
// 因为它不是声明一个外部变量,而是一个新的定义。
const double distance = 0.0; // 在这个文件里定义了一个独立的 const distance 变量,并初始化为 0.0

void printDistance() {
std::cout << "在 some_other_file.cpp 内部的 printDistance 函数: " << std::endl;
std::cout << "distance (从其他文件) 的值: " << distance << std::endl;
std::cout << "distance (从其他文件) 的地址: " << &distance << std::endl; // 打印在 some_other_file.cpp 中 distance 的地址
}

解释:
当你编译并运行这个程序时,你会发现main.cpp中打印的distance值是45.8,地址是某个值;而some_other_file.cpp中打印的distance值是0.0,地址是另一个完全不同的值。这证明了即使它们有相同的名字,由于const全局变量默认的内部链接属性,每个翻译单元都拥有自己独立的distance副本。它们之间没有任何关联。

思考: 为什么const全局变量默认是内部链接?
这是C++语言设计的一个考量。如果const全局变量默认是外部链接,那么当你在多个源文件中#include同一个定义了const全局变量的头文件时,会导致多个翻译单元都定义了同一个外部链接变量,从而违反了C++的单一定义规则(One Definition Rule, ODR),链接器会报错。将const全局变量默认设为内部链接,可以避免这个问题,每个翻译单元都会获得一个独立的副本,这通常是期望的行为。

4. 外部链接(External Linkage)

具有外部链接属性的名字,可以在它被定义的翻译单元中被访问和使用,更重要的是,它也可以在其他翻译单元中被访问和使用。所有引用同一个外部链接名字的地方,都指向内存中的同一个实体。这是实现多文件协作和模块化编程的基础。

示例1:非 const 全局变量

const修饰的全局变量默认具有外部链接属性。要在一个翻译单元中使用另一个翻译单元中定义的外部链接变量,你需要使用extern关键字进行声明(Declaration),而不是再次进行定义(Definition)。

main.cpp

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

// 这是一个非 const 全局变量,它默认具有“外部链接”属性
int item_count = 6; // 物品数量

// 声明在 some_other_file.cpp 中定义的函数原型
void printItemCount();

int main() {
std::cout << "\n--- 演示外部链接 (非 const 全局变量) ---" << std::endl;
std::cout << "在 main.cpp 内部: " << std::endl;
std::cout << "item_count 的值: " << item_count << std::endl;
std::cout << "item_count 的地址: " << &item_count << std::endl; // 打印在 main.cpp 中 item_count 的地址

std::cout << "\n--- 尝试在另一个翻译单元中访问 item_count ---" << std::endl;
printItemCount(); // 调用在 some_other_file.cpp 中定义的函数

// 可以在 main 中修改 item_count 的值,然后看看 printItemCount 是否会受到影响
item_count = 10;
std::cout << "\n--- 在 main.cpp 中修改 item_count 后再次调用 ---" << std::endl;
printItemCount(); // 再次调用,item_count 的值应该会反映出修改

return 0;
}

some_other_file.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

// 使用 extern 关键字声明 item_count
// 这告诉编译器:item_count 在其他翻译单元中已经定义了,这里只是声明要使用它。
// 这不是定义一个新的变量,而是引用 main.cpp 中已经存在的那个 item_count。
extern int item_count;

void printItemCount() {
std::cout << "在 some_other_file.cpp 内部的 printItemCount 函数: " << std::endl;
std::cout << "item_count (从其他文件) 的值: " << item_count << std::endl;
std::cout << "item_count (从其他文件) 的地址: " << &item_count << std::endl; // 打印在 some_other_file.cpp 中 item_count 的地址
}

解释:
你会看到main.cppsome_other_file.cpp中打印的item_count的值和地址都是相同的。这证明了它们访问的是内存中的同一个变量。当main.cpp修改了item_count的值后,some_other_file.cpp中的printItemCount函数也能立即看到这个改变,这正是外部链接的强大之处。

示例2:函数

函数(Function)默认也具有外部链接属性。这就是为什么你可以在一个 .cpp 文件中定义函数,然后在另一个 .cpp 文件中调用它,只要提供了函数的声明(原型)即可。

main.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

// 声明在 some_other_file.cpp 中定义的函数原型
// void doSomething(); // 这个我们前面演示无链接时用过,这里可以继续保留
void printItemCount(); // 声明 printItemCount 函数,它在 some_other_file.cpp 中定义

int main() {
std::cout << "\n--- 演示外部链接 (函数) ---" << std::endl;
// 我们已经多次调用了 printItemCount 函数,它是在 some_other_file.cpp 中定义的。
// 这本身就证明了函数的默认链接属性是外部链接。
std::cout << "printItemCount 函数是在 some_other_file.cpp 中定义的,但可以在 main.cpp 中调用。" << std::endl;
printItemCount(); // 再次调用,以示函数可以在不同翻译单元间使用

// 你也可以显式地给函数添加 extern 关键字,但这通常是多余的,因为函数默认就是外部链接。
// extern void printItemCount(); // 这样写是完全合法的,但通常省略。

return 0;
}

some_other_file.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

extern int item_count; // 再次声明 item_count 为外部链接

// printItemCount 函数的定义
// 默认情况下,函数是外部链接的
void printItemCount() { // 这里的 void 也可以加上 extern,但通常省略
std::cout << "在 some_other_file.cpp 内部的 printItemCount 函数 (再次被调用): " << std::endl;
std::cout << "item_count 的值: " << item_count << std::endl;
std::cout << "item_count 的地址: " << &item_count << std::endl;
}

解释:
我们一直以来都在跨文件调用函数,而没有遇到任何问题,这正是因为C++中的函数默认就具有外部链接属性。这意味着你可以在一个文件(翻译单元)中定义一个函数,然后通过在另一个文件(翻译单元)中提供其函数原型(Function Prototype)来调用它。

5. QA 闪卡 (QA Flash Cards)

问题 (Q): 什么是 C++ 中的“链接属性”?
答案 (A): 链接属性是与 C++ 程序中的名字(如变量名、函数名)相关联的一个特性,它决定了这些名字在不同翻译单元之间如何被看见和使用。

问题 (Q): 局部变量(如函数内部定义的变量)具有哪种链接属性?它的可见范围是什么?
答案 (A): 局部变量具有无链接(No Linkage)属性。它们只在被定义的作用域内可见和使用,无法从其作用域外部或其它翻译单元中访问。

问题 (Q): const 修饰的全局变量默认具有哪种链接属性?这意味着什么?
答案 (A): const 修饰的全局变量默认具有内部链接(Internal Linkage)属性。这意味着它只能在定义它的那个翻译单元内部被访问和使用。即使在其他翻译单元中定义同名变量,它们也是独立的实体,互不影响。

问题 (Q):const 修饰的全局变量和函数默认具有哪种链接属性?这意味着什么?
答案 (A):const 修饰的全局变量和函数默认具有外部链接(External Linkage)属性。这意味着它们不仅可以在定义它们的翻译单元中访问,也可以在其他翻译单元中被访问和使用。所有引用同一个外部链接名字的地方,都指向内存中的同一个实体。

问题 (Q): 如果我想在文件 B.cpp 中使用文件 A.cpp 中定义的非 const 全局变量 myVar,我应该怎么做?
答案 (A):B.cpp 中,你需要使用 extern 关键字对 myVar 进行声明,例如:extern int myVar;。这告诉编译器 myVar 在其他地方已经定义了。

6. 常见误解与错误

  1. 误解1:函数内的局部变量可以在文件其他地方访问。

    • 错误: 认为someFunction内部的age可以在main函数中直接使用。
    • 原因: 局部变量是“无链接”的,它们只存在于其函数或块的作用域内。一旦作用域结束,变量就会销毁。
    • 正确理解: 局部变量与外部世界是隔离的,只能在定义它们的 {} 内部访问。
  2. 误解2:const 全局变量可以在不同文件之间共享。

    • 错误: 认为在 main.cpp 定义 const int MY_CONST = 10; 后,可以在 other.cpp 中通过 extern const int MY_CONST; 来访问同一个 MY_CONST
    • 原因: const 全局变量默认是“内部链接”。在 other.cpp 中写 const int MY_CONST; 会定义一个完全独立的新的 MY_CONST 变量(需要初始化),而不是引用 main.cpp 中的那个。
    • 正确理解: 要想在不同文件之间共享一个 const 值,通常有两种方法:
      • 将其定义为 extern const int MY_CONST = 10; (定义一次,外部链接)。
      • 将其放入头文件,并标记为 inline const int MY_CONST = 10; (C++17),或者只在头文件声明 extern const int MY_CONST; 然后在某个 .cpp 文件中定义 const int MY_CONST = 10;
  3. 误解3:extern 关键字是用来定义的。

    • 错误:some_other_file.cpp 中写 extern int item_count = 10; 认为这是在声明并引用 main.cpp 中的 item_count
    • 原因: extern 关键字表示“此变量在别处定义”,但如果同时给它赋了初始值,它就变成了定义,而不是简单的声明。这意味着你会在 some_other_file.cpp 中创建一个新的 item_count 变量,与 main.cpp 中的 item_count 冲突(单一定义规则 violation),导致链接错误。
    • 正确理解: extern 后面跟着的通常只是声明,不带初始化。例如:extern int item_count;定义(Definition)发生在没有extern,或者带有初始化的那一次。
  4. 误解4:没有理解“翻译单元”的概念。

    • 错误: 认为只要在一个 .cpp 文件里定义了,它就在整个项目里都可见。
    • 原因: C++ 程序是分模块编译的,每个 .cpp 文件及其包含的头文件构成一个独立的“翻译单元”。链接属性正是用来控制这些翻译单元之间的名字可见性。
    • 正确理解: 区分声明(告诉编译器一个名字存在)和定义(分配内存并初始化)。链接属性控制的是如何跨越翻译单元共享或隔离这些声明和定义。

6. 编程练习:理解链接属性

现在,是时候动手实践了!请完成下面的代码,使其能够正确演示不同链接属性的行为。

任务:

  1. 创建一个 main.cpp 文件。
  2. 创建一个 helper.cpp 文件。
  3. 创建一个 helper.h 头文件。

helper.h 文件内容:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once // 确保头文件只被包含一次
#include <iostream>

// 1. 声明一个函数,它将在 helper.cpp 中定义
// 这个函数将尝试访问 helper.cpp 中具有内部链接的变量。
void printInternalVariable();

// 2. 声明一个函数,它将在 helper.cpp 中定义
// 这个函数将尝试访问 main.cpp 中具有外部链接的变量。
void printExternalVariable();

// 3. 在 helper.h 中声明一个 extern int 变量,表示它将在某个 .cpp 文件中定义
// TODO: 在这里添加 extern int variable_from_main 的声明
// extern int variable_from_main; // 提示:你需要在这里声明它

main.cpp 文件内容:

cpp
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include "helper.h" // 包含 helper.h 头文件

// 1. 定义一个函数局部变量(无链接)并在 main 中尝试访问它
void myFunctionWithLocalVariable() {
int local_data = 100; // 局部变量,无链接
std::cout << "myFunctionWithLocalVariable 内部: local_data = " << local_data << ", 地址: " << &local_data << std::endl;
}

// 2. 定义一个 const 全局变量 (内部链接)
const int internal_global_const = 50;

// 3. 定义一个非 const 全局变量 (外部链接)
// TODO: 在这里定义一个名为 external_global_variable 的 int 变量,并初始化为 200
// int external_global_variable = 200; // 提示:你需要在这里定义它
int external_global_variable = 200;


int main() {
std::cout << "--- 链接属性演示 ---" << std::endl;

// 演示无链接
std::cout << "\n--- 无链接 (局部变量) ---" << std::endl;
myFunctionWithLocalVariable();
// std::cout << "在 main 中尝试访问 local_data: " << local_data << std::endl; // 这行如果取消注释会报错,验证无链接

// 演示内部链接
std::cout << "\n--- 内部链接 (const 全局变量) ---" << std::endl;
std::cout << "main.cpp 内部: internal_global_const = " << internal_global_const
<< ", 地址: " << &internal_global_const << std::endl;
printInternalVariable(); // 调用 helper.cpp 中的函数,它将尝试访问它的内部链接变量

// 演示外部链接
std::cout << "\n--- 外部链接 (非 const 全局变量和函数) ---" << std::endl;
std::cout << "main.cpp 内部: external_global_variable = " << external_global_variable
<< ", 地址: " << &external_global_variable << std::endl;
printExternalVariable(); // 调用 helper.cpp 中的函数,它将访问 external_global_variable

// 尝试在 main 中修改 external_global_variable 的值,并再次在 helper.cpp 中打印
external_global_variable = 300;
std::cout << "\n--- 修改 external_global_variable 后 ---" << std::endl;
std::cout << "main.cpp 内部: external_global_variable = " << external_global_variable
<< ", 地址: " << &external_global_variable << std::endl;
printExternalVariable(); // 再次调用,观察值是否同步改变

return 0;
}

helper.cpp 文件内容:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "helper.h" // 包含 helper.h 头文件
#include <iostream>

// TODO: 在这里重新定义一个名为 internal_global_const 的 const int 变量,并初始化为 77。
// 注意:这应该是一个新的、独立的变量,与 main.cpp 中的同名变量互不影响。
// const int internal_global_const = 77; // 提示:你需要在这里定义它
const int internal_global_const = 77;


// TODO: 声明 main.cpp 中定义的 external_global_variable 为外部链接。
// 注意:这里只是声明,不能初始化。
// extern int external_global_variable; // 提示:你需要在这里声明它
extern int external_global_variable;


void printInternalVariable() {
std::cout << "helper.cpp 内部: internal_global_const = " << internal_global_const
<< ", 地址: " << &internal_global_const << std::endl;
}

void printExternalVariable() {
std::cout << "helper.cpp 内部: external_global_variable = " << external_global_variable
<< ", 地址: " << &external_global_variable << std::endl;
}

请完成上述代码中标注 TODO: 的部分。完成后,编译并运行你的程序,观察输出,特别是变量的值和地址,以验证你的理解。


请在完成上述代码后,将你的解决方案粘贴到回复中。我将为你评分并提供详细的答案和解释。


🧑‍💻 User

误解2:const 全局变量可以在不同文件之间共享。

错误: 认为在 main.cpp 定义 const int MY_CONST = 10; 后,可以在 other.cpp 中通过 extern const int MY_CONST; 来访问同一个 MY_CONST。
原因: const 全局变量默认是“内部链接”。在 other.cpp 中写 const int MY_CONST; 会定义一个完全独立的新的 MY_CONST 变量(需要初始化),而不是引用 main.cpp 中的那个。
正确理解: 要想在不同文件之间共享一个 const 值,通常有两种方法:
将其定义为 extern const int MY_CONST = 10; (定义一次,外部链接)。
将其放入头文件,并标记为 inline const int MY_CONST = 10; (C++17),或者只在头文件声明 extern const int MY_CONST; 然后在某个 .cpp 文件中定义 const int MY_CONST = 10;。

请问best practice是什么? 要不要定义成全局变量?


🤖 Assistant

这是一个非常棒的问题!你不仅关注了技术细节,还深入思考了“最佳实践”和设计原则,这正是成为优秀C++开发者所需要的思维方式。

我们来逐一解答你的问题:

关于共享 const 值(常量)的最佳实践

正如你所理解的,C++中 const 全局变量默认是内部链接(Internal Linkage)。这意味着每个翻译单元(Translation Unit)都会有自己的独立副本,如果直接将它放在头文件中,并且该头文件被多个 .cpp 文件包含,这不会导致链接错误,但会导致每个 .cpp 文件都有一个独立的副本,这可能不是你想要的“共享”同一个实体。

为了在多个翻译单元之间真正“共享”同一个 const 值(即它们都引用内存中的同一个实体),C++ 提供了几种最佳实践方法,它们避免了单一定义规则(One Definition Rule, ODR)的冲突:

  1. 使用 extern 关键字进行声明(传统的、C++17以前的方法)

    • 概念: 在一个 .cpp 文件中进行定义(Definition),然后在头文件中使用 extern 关键字进行声明(Declaration)。
    • 示例:
      • my_constants.h
        cpp
        1
        2
        #pragma once
        extern const int SHARED_MAX_VALUE; // 声明:告诉编译器 SHARED_MAX_VALUE 存在,但在别处定义
      • my_constants.cpp
        cpp
        1
        2
        #include "my_constants.h"
        const int SHARED_MAX_VALUE = 100; // 定义:这里分配内存并初始化
      • main.cpp
        cpp
        1
        2
        3
        4
        5
        6
        7
        8
        #include <iostream>
        #include "my_constants.h" // 包含头文件,即可使用 SHARED_MAX_VALUE

        int main() {
        std::cout << "Max value: " << SHARED_MAX_VALUE << std::endl;
        // std::cout << "Address: " << &SHARED_MAX_VALUE << std::endl; // 可以打印地址验证是同一个
        return 0;
        }
    • 优点: 兼容性好,适用于所有C++标准。
    • 缺点: 需要在 .h.cpp 两个地方操作,多一个文件。
  2. 使用 inline 关键字(C++17 及更高版本推荐)

    • 概念: C++17 引入了 inline 变量。当一个变量被标记为 inline 时,它可以在多个翻译单元中被定义,但链接器会确保它们都指向内存中的同一个实体,从而解决了 ODR 问题。对于 const 常量,这是非常优雅和推荐的做法。
    • 示例:
      • my_constants.h
        cpp
        1
        2
        3
        4
        5
        6
        7
        8
        #pragma once
        #include <iostream> // 如果需要,也可以包含其他头文件

        inline const int SHARED_MAX_VALUE = 100; // 在头文件中直接定义并初始化
        // 编译器和链接器会确保这是唯一的实体

        // 对于更复杂的常量对象,也可以这样:
        // inline const std::string APP_NAME = "My Awesome App";
      • main.cpp (或任何其他 .cpp 文件)
        cpp
        1
        2
        3
        4
        5
        6
        7
        8
        #include <iostream>
        #include "my_constants.h"

        int main() {
        std::cout << "Max value: " << SHARED_MAX_VALUE << std::endl;
        // std::cout << "Address: " << &SHARED_MAX_VALUE << std::endl; // 打印地址验证是同一个
        return 0;
        }
    • 优点: 简单、直观,只需在头文件中定义一次即可,无需额外的 .cpp 文件。通常是对于简单 const 变量的首选方法
    • 缺点: 需要 C++17 或更高版本。
  3. 使用 constexpr 关键字(更佳实践,如果值是编译时常量)

    • 概念: constexpr 变量在编译时就确定了值,并且通常会被内联(inlined)到使用它的地方,不占用运行时内存。即使它在多个翻译单元中被定义(例如通过头文件),也不会有 ODR 问题,因为编译器会确保它们都是相同的编译时常量。
    • 示例:
      • my_constants.h
        cpp
        1
        2
        3
        4
        #pragma once

        constexpr int MAX_ITERATIONS = 1000; // 编译时常量
        constexpr double PI = 3.14159265358979323846; // 编译时常量
      • main.cpp (或任何其他 .cpp 文件)
        cpp
        1
        2
        3
        4
        5
        6
        7
        8
        #include <iostream>
        #include "my_constants.h"

        int main() {
        std::cout << "Max iterations: " << MAX_ITERATIONS << std::endl;
        std::cout << "Pi: " << PI << std::endl;
        return 0;
        }
    • 优点: 性能最高,不占用运行时存储,真正实现编译时优化。对于数值型或简单对象常量是最佳实践
    • 缺点: 只能用于编译时(Compile-Time)已知的值。
  4. 使用 enum class (枚举类)

    • 概念: 对于一组相关的整数常量,enum class 是一个非常好的选择。枚举器的值也是编译时常量,不会有 ODR 问题。
    • 示例:
      • game_states.h
        cpp
        1
        2
        3
        4
        5
        6
        7
        8
        #pragma once

        enum class GameState {
        MainMenu,
        Playing,
        Paused,
        GameOver
        };
      • main.cpp
        cpp
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        #include <iostream>
        #include "game_states.h"

        int main() {
        GameState current_state = GameState::Playing;
        if (current_state == GameState::Playing) {
        std::cout << "Game is playing!" << std::endl;
        }
        return 0;
        }
    • 优点: 编译时常量,类型安全,易于组织相关常量。
    • 缺点: 仅适用于整数类型(或可转换为整数的类型),主要用于表示一组离散状态。

要不要定义成全局变量?

这是一个更深层次的设计问题,涉及到软件架构和代码质量。

一般原则:尽量避免使用裸露的(non-const)全局变量。

  • 全局变量的缺点(特别是可变的全局变量):
    • 难以管理状态: 任何函数都可能修改它,使得代码的行为难以预测和调试。
    • 增加了耦合: 程序的各个部分都依赖于全局变量,降低了模块的独立性。
    • 测试困难: 编写单元测试时,需要小心设置和清理全局状态。
    • 命名冲突: 在大型项目中,全局变量容易导致名字冲突。
    • 线程安全问题: 在多线程环境中,可变全局变量是常见的竞态条件(Race Condition)来源。

那么对于 const 全局变量呢?

对于 const 全局变量,上述的一些缺点(如状态难以管理、线程安全问题)得到了极大的缓解,因为它们的值是不可变的。然而,以下几点仍然需要考虑:

  • 命名污染: const 全局变量仍然会污染全局命名空间,尤其是在没有良好封装的情况下。
  • 并非所有“常量”都适合“全局”: 很多所谓的“常量”实际上只在一个特定模块、类或函数内部有意义。
  • 依赖性: 尽管值不变,但如果一个模块广泛依赖于全局常量,它仍然与该常量紧密耦合。

最佳实践(对于常量):

  1. 优先使用 constexprenum class 如果常量的值在编译时已知,并且是基本类型或简单结构,这通常是最好的选择。它们通常不会占用运行时存储,也避免了链接问题。

  2. 如果常量与特定类相关,定义为 static conststatic constexpr 成员:

    • 示例:
      cpp
      1
      2
      3
      4
      5
      class MyClass {
      public:
      static const int DEFAULT_SIZE = 10; // C++11/14 需要在 .cpp 中定义: const int MyClass::DEFAULT_SIZE;
      static constexpr double RATIO = 0.5; // C++11/14 可以在这里初始化
      };
    • 优点: 将常量与它所属的逻辑实体(类)绑定,提高了封装性,避免了全局命名空间污染。
  3. 如果常量是运行时确定但不可变(如通过配置读取),考虑使用单例模式或依赖注入:

    • 这超出了我们目前链接属性的范畴,但要知道,并非所有“不变”的值都适合作为全局常量。有时候,它们是程序启动时从配置文件读取的配置值,一旦读取后不再改变。这种情况下,使用配置类或通过依赖注入(Dependency Injection)传递它们是更好的选择。
  4. 只有当常量真正是“全局”且“通用”时,才考虑使用 inline constextern const 全局变量:

    • 例如,数学常数 PI,物理常数 LIGHT_SPEED,或者程序中真正普遍使用的配置阈值。即使在这种情况下,inline const (C++17) 或 constexpr 通常也优于传统的 extern const 全局变量,因为它们更现代、更简洁。

总结:

  • 对于共享 const 值:

    • constexpr 是第一选择,如果常量是编译时已知且适用。
    • inline const (C++17) 是第二选择,如果需要运行时存储但值是固定的,并且希望在头文件中定义。
    • extern const (经典做法) 是第三选择,如果你不能使用 C++17 的 inline 变量,或者常量是复杂的对象且不适合放在头文件中。
    • enum class 适用于组织一组相关的整数常量。
  • 对于是否定义为全局变量:

    • 一般不推荐定义可变的(non-const)全局变量。
    • 对于 const 常量: 优先考虑constexprinline const(在头文件中)、类静态成员常量。只有当常量确实具有全局普遍的意义时,才考虑使用它们作为全局变量,即便如此,也要优先使用现代的 constexprinline const 方式。

希望这个详细的解释能帮助你更好地理解和选择最佳实践!


评论