C++ 声明与定义
🧑💻 User
In this lecture.We’re going to zoom in on the declarations and the definitions in your C plus plus code.And we’re going to think about the meaning of these words in your C plus plus program.A declaration is something or a statement that is going to introduce a name in your C plus plus program.A definition says what that name is or what that name does.And now that sounds a bit confusing right now.So let’s look at an example here.We have a piece of code in which we have a function declaration and we have a function definition below.The function declaration is giving up some information about this function.For example, we know that this function is not going to return anything.We know the name of the function, but we don’t really know what this function is going to do.And this is really what the splitting between declaration and definition is all about.Somebody might not be interested in the full details of your function and they are just going to usethe function declaration.This is going to introduce the name some function in our code.So if we refer to it, the compiler is not going to complain.So we have successfully introduced a name in our program using this declaration.Here again, a declaration is some kind of statement in your C plus plus code that is going to introducea new code if you want the full details about that name.For example, knowing what that name does or how it does what it does, then you will need a definitionfor your thank.If you look down below here we have a definition.This is the code that is going to give up more information about this function here.For example, if we have something in the body of this function, we will be able to see it and we willexactly know how this function does its job.And this is a function definition.Again, the definition is going to give up more information about what the name does.In this case, the name is some function here and how the name does what it does.This is what we really mean when we say we have a declaration of something or we have a definition ofsomething.Let’s look at some things you need to be careful about in terms of definitions and declarations.If a name is never used, for example, a function is called or a variable is used in any way in yourC plus plus program, its definition won’t never be needed because we won’t need those details becausethe name is not really used in your code.So the code will just compile and work fine, even if the definition for that name is not in place.If we look at the second point here, we’re going to say if you compile without a declaration and thename is used, you get a compiler error and we’re going to get some kind of name is not defined in thisscope or something like that.So for your name to be used in any way, you need a declaration in place.And we’re going to see an example of this in a minute when we hit Visual Studio code.But you need to keep this in mind.If we look down here, we’re going to see that if you compile with a declaration without a definitionand the name is not used, the code is going to compile.So you have a declaration, you don’t have a definition, but the name is never used.So the code is going to compile.If we look at this line here, we’re going to say if you compile with a declaration without a definitionand the name is used, you’re going to get a linker error because the definitions are already resolvedat the linker stage.So you’re going to get a linker error.And we saw an example of this in a previous lecture when we split our computation in two steps compilationand linking.Okay, so now that you have these ideas in mind, we’re going to look at declarations and definitions.Strategically, we are going to look at these concepts in the context of freestanding variables.We’re going to look at them in the context of functions and we’re going to look at them in the contextof classes.Let’s start with variables here we have a single variable.It is really simple.Its type is double.The name is weight and it is initialized to zero using braced initialization.Here, the statement here is going to double as both a declaration and a definition because it is introducingthe way to name in our C plus plus program.For example, if we’re trying to print the weight, we’re going to get it printed out.We’re not going to get a compiler error saying we don’t know what weight is because we have introducedthis name using this declaration here.But this is also a definition because it is really saying what this weight is.It is a variable of double type and its value is zero because we initialize this using brace initialization.So this is a definition.And the declaration.At the same time, I have to say that it isn’t really common to separate declarations and definitionsfor variables, and I don’t really know how you would even do that.So we usually do this this way by combining declaration and definitions for variables, but for somevariables, for example, static variables that are inside the classes, it is possible to separatedeclaration and definition.And I am going to show you that in a minute when we hit Visual Studio code.But note that in most cases for freestanding variables, the declaration and definition are combinedin one statement.As you see here, for functions, it is possible to separate the declaration and definition.For example, what we have here is a function declaration.It is introducing this name in our code, but we don’t really see how this function does its job.And if we want to see that, we will need to look at the definition for this function here down here,you see, we have the definition for this function.This is a definition and we have the body of the function.And in the body we can really see what this function is doing.So this is a function definition here for classes.For example, using the example here, this is also going to double as a declaration and a definition.It is a declaration because it is introducing the point name in our code.For example, we can create a point object and that’s going to work.The compiler is not going to be confused.It is a definition because we already have all the information to know what this type does.If you create it, you’re going to have the value stored in your member variables.And we have all the information about a point type in our code.So this is also a definition.Here is another example for classes, but we are using the class keyword here.We are not using struct like we did in the previous slide.This is also going to double as both a definition and a declaration.It is a declaration because it is introducing the person name in our code.We know what it is.So it is a class, but it is also a definition because we already have all the information here.We know its member variables, we know its constructor.So this is a declaration and the definition at the same time.But you know that it is possible to separate the members of a class into declaration and definitionand the definitions might show up at the outside of the class.And we’re going to see how you can do that in a minute.Okay.So this is really all I wanted you to know.The concepts of declarations and the definitions in the context of variables, functions and classes.Now that you have an idea about this, we’re going to head over to Visual Studio code and play withthis a little more.Okay.So here we are in our working folder.The current project is declarations and definitions.We’re going to grab our template files and put them in place.Let’s do that.And we are going to open this in Visual Studio code pretty quick.This is going to give us our main CPP file and we can clean up a little bit.So this is what we have.The first thing we’re going to do is play with free standing variables.We’re going to go on top here and declare a weight variable like we had in the slides and we’re goingto initialize this guy to zero.And one thing you should know is that this is going to double both as a declaration and a definition.It is a declaration because it is introducing the way to name in our program.So, for example, if we go down and try to print out the weight, this is going to be valid C plusplus program.The compiler is not going to wonder about this name here.It was introduced in our C plus plus program.It is also a definition because we already know what is inside this weight variable.We know that it has a value of zero.So this is both a declaration and a definition.If we build and run this program, it is going to work because it is really a valid C plus plus program.We can bring up a terminal and run it.No, we don’t want that.We want a terminal.So let’s run Rooster and we’re going to see that this program is going to print zero because that’swhat we have in here.If we happen to put in an eight, for example, 8.7, why not?We’re going to build again and we’re going to have our value in.If we run Rooster, we’re going to have our value in here and it is going to be printed out.So this is a declaration and a definition.Please note that in most cases variables are going to be set up like this, combining declaration anddefinitions.But it is possible, especially for static variables in classes, to be split into declaration and definition.If you ask me to break this into declaration and definition, I wouldn’t really know how to do that.If you happen to know that, please share.I am interested, but in most cases we’re going to declare and initialize the variables in one go likethis and this.Is going to double as a declaration and a definition.Somebody is going to say, Aha, what if I do something like this and maybe assign a value to the weighthere?Let’s do this to really remove this confusion.For example, you do something like this, they’re going to say maybe this is a declaration and thisis a definition.I hate to break it to you, but this is also a definition like we had before because this is going toinitialize weight anyway.So this is going to initialize weight to a junk value, but it is going to be initialized.So this is going to still be a declaration and the definition and here what you will be doing is justassigning to a variable that’s been already set up, declared and defined.So this is just an assignment here.You should know that.Okay.So if we comment this out, you’re going to see that the compiler is going to accept this.The code is going to compile.Let’s do that.You’re going to see what is good.And if we print whatever is in weight by running our program, as you see here, we’re going to geta junk value.And this happens to be zero in this case.So this is a declaration and the definition for free standing variables.Now, let’s talk about functions.If you set up a function like this, for example, we’re going to say add.We’re going to say double and we’re going to add up two parameters.We’re going to say A and B, and you do something like this and you return, for example, A plus B,this is going to double both as a declaration and a definition because we have the body of this functionhere.We know what this name is.We know it was introduced in our program.So it is a declaration.But we also know what this function does.It is going to add up two parameters and we know how it is going to add up those parameters becausewe can see the statement that is doing exactly that.So this is also a definition, but note that it is possible to break functions into declarations anddefinitions.Let’s comment this out and show you that option here.And I’m going to leave this in as a reference.We’re going to grab the signature of this little guy and we’re going to make it a declaration.Let’s do this.We’re going to put this in.And this is going to be our declaration.You already knew this, but I really wanted this to be super clear.We can now take this declaration here and go down and use it a little bit and set up a definition.We can do that.What we have below here is going to be our definition and we can go inside and do whatever we want todo in this function.And in this case, what we want to do is to add up A and B and return the result.So this is going to be our function here.And if we call it, it is going to work.If we go down here and say double result and say add 10 to 20, we have done this a thousand times,but we’re going to use this example again, we’re going to print the result and we’re going to printthis out.If we build and run this program, it is going to work because we have a declaration that is introducingthis Add name in our program, but we also have a definition that is saying how this function is doingits job.We can see that right here.So if we build this program, we’re going to use GCC like we always do.We’re going to clear the build is good.If we run Rooster, we’re going to see result to be 30.And if we change this, for example, to be 20.5 and above again, just to make sure that this programis working and I am not making this up, we’re going to see 30.5.So this is adding up what we have here.One thing you should know is that if you don’t have a definition in place for functions, you’re goingto get a linker error.Let’s do that.We’re going to comment out this definition and we’re going to try and build again.And you’re going to see that we’re going to get a problem.We’re going to see world finished with errors.And what we have here is an undefined reference to add.So this is a linker error.It is a linker error because we can see they returned one exit status here.But in a previous lecture we also saw that we could split computation in two steps compilation and linkingand we saw that you could see these linker errors if you don’t have definitions in place.So this is one way you can split your functions into declarations and definitions.It is also possible for the definition to live in some other file.So let’s bring up our sidebar here and create another file.We’re going to call it some other file.Why not?And it’s going to be a CPP file.What we’re going to do is move our definition there, so we’re going to copy it.Let’s copy what is inside the comment.So we’re going to copy this and we’re going to put this in some other file here.And the linker is going to be smart enough to search this entire directory looking for the definitionof this function.Now, if we compile, you’re going to see that the definition in main is gone, but we have a definitionin some other file.Now, if we try to compile the program again, it is going to work because we have a definition andyou’re going to see that the world is good because we have this definition here and now if we run theprogram, let’s clear and run Rooster.We’re going to see that it is going to work because we have a definition in place.Another thing we’re going to look at is declarations and the definitions in the context of classes.So let’s create a structure.We’re going to do a struct and we’re going to call it point like we have been doing all along.Inside, we’re going to have two member variables.We’re going to have M, X and m Y to represent the coordinates of our point type here.And I hate to break it to you, but this is also going to double as a declaration and a definition.And in most cases you’re going to see things like this called definitions.They’re going to say it’s a class definition and it’s going to do this and that.So this is going to be mostly called the definition, but note that it’s doubling as a declaration andthe definition it is a declaration because it is introducing the point name in our C plus plus program.It is a definition because we really have all the information we need to know about this point object.It is going to store its member variables in X and Y, so we already know all we need to know aboutthis little guy here.If you also set up a class, it is going to double as a declaration and the definition I am going tograb a class I have lying around on my system and I am going to put it in here because I don’t wantto type all this.We have a class called Person.It has a constructor here and we don’t have a body of this constructor.And so we can put it in.We’re going to say full name and we’re going to pass in names.Param, we’re going to use brace initialization and we’re going to do this on a separate line.I think that’s going to be much cleaner and we’re going to do H and we’re going to put in H parameterhere and we’re going to put in a body for this function here.I think this is all we need for this to be valid C plus plus code.So this is our constructor.We have a function to print information about a person object and we have our member variables here.Notice that we also have a static variable and this function here, so it will need to be initializedin some other CPP file and we can do that using a statement like this, we can say person and we’regoing to say person count because it is a static variable.We are going to specify its type and then we can initialize it to a value.For example.Eight We can do something like this from this.You should note that for this static variables, what we have inside the class here is a declaration.So you should note that.But the entire class here is also a declaration and a definition in one go.So in most cases, things like this are going to be called class definitions.But note that they also double as declarations.But it is possible to take member functions of classes and split them into declarations and definitions.Declarations are going to live inside the classes, but definitions can be put to the outside of theclass.And we’re going to show you an example of that using the constructor here.So we’re going to take out the initialization we have here and we’re going to leave in a declaration.So we’re going to do that.This is going to become a declaration for the constructor and we’re going to put the definition at theoutside so we can go down here, for example.A person.Person.And we’re going to put the body of our constructor down below here.Now, this code is going to work, and if we create a person object, we can go down here.I think we can comment out all we had before because it had served its purpose.It is now confusing us.So let’s do that.We’re going to comment all this out.We’re going to go down and create a person object.We can say person P one and we can give our person a name and an age.I think we can do that.Let’s see.Yes, we can do that.We can pass a first parameter as a name.We’re going to say Jon Snow, and we can give them an age, let’s say 27.Why not?And we can print information about this person.We can say P one Printinfo And if we build and run this program, I think it is going to work.Let’s bring this up a little bit.You see, the build is good.We can clear and run Rooster and we’re going to get name age, age.And if we want, we can also print the static variable here, but we don’t really want to do that.Okay, so now you know that a class itself is going to be a declaration and a definition, but for themember variables of the class, it is possible to split them into declarations and definitions.One thing I want you to see is that we can also move these things out.For example, the class here, it is cluttering our main CPP file.So what we can do is come back to the left here, bring up the sidebar, we can create a header filefor it.We’re going to say person dot age, and then we are going to put in our include guards.We are going to put them in here and we’re going to go down and say end.If I can’t really remember these things and we’re going to say that this is for person age and thenwe’re going to grab the class, we can copy it, let’s do that and we can grab the class here and we’regoing to put that in its separate file.We’re going to do that and we can come back in our main CPP file and now include the person We’re goingto do that.We’re going to say include person dot H, And I think we should also move the definitions for the staticvariable and the person constructor in a separate file.Let’s go back to person and see what problem we have here.What did we have in Main?We had i o stream.So it is complaining that it doesn’t know stdcout and the string.Let’s go on top here and include i o stream.Let’s do that.It should stop complaining here.Let’s wait a minute.So this is all good.If we go in main we see that we have no problem.If we compile our program, it is going to work.We’re going to see that the world is good even if we have our class definition in a separate file andsome definitions in the CPP file, you see that we have the definition for our static variable.In main, we have the definition for the constructor in main, but it is really not cool to have thisdefinitions in main so we can also take them and put them in a separate file.For example, we can move them in our some other file here.We’re going to put them in here.Okay.You see that the moment we do this, we’re going to get these errors because this file doesn’t knowwhat person is.So we’re going to go on top and include the person.And now we shouldn’t have a problem with this.Now the IDE or the editor is happy.We can build and run this and you’re going to see that it is going to work.And this is really all I wanted you to see in this lecture, that you can split things into declarationand definitions.And we really tried our best to explain the difference between declaration and definition and the contextof freestanding variables like we have here, the weight variable functions like the add function hereand the classes using point and person here as an example.And we saw that we could really split things between files.Once you understand the difference between a declaration and a definition, definitions can really livein any files.You have seen that the definition for the constructor here and the definition for the static file couldreally live anywhere.We were able to compile our code with these definitions.In Main, we are able to compile our code with this definitions in this file here.And this is really cool.Okay, before I wrap up this lecture, one thing I want you to see is that if any of these definitionsis not in place, you’re going to get a linker error.And it really proves this.We’re going to comment out the definition for the constructor of our class here, and we’re going totry and build our program.We’re going to build this with GCC.Let’s do that.And you’re going to.See that we get an error and the error is saying undefined reference to person person.So we don’t have a constructor here.Even if we have a declaration and our class here, you can see that it is not enough.The linker is going to complain because it needs to know how to construct person objects so we can comeback and bring this back in.And if we build, this is going to work.So we’re going to run with GCC and the build is going to be good.If we also don’t have this definition for person account, if we build the program, it is going towork and run fine because we already not using this static variable in our program.And let’s build to show you this and we’re going to see that the build is good and the program can run.We can clear and run Rooster.This is going to do whatever it is we want to do.But if this static variable is used anywhere in this class, for example, when we create a person object,we increment the person, count this is a logical thing to do because this is keeping track of how manyperson objects we have.If we’re trying to build this program now, we are going to get an error and that’s going to be a linkererror saying undefined reference to person count.But we have a person count declared in our class here.So some of you might be thinking, what the hell is going on here?But this is a declaration.It is not a definition and the linker really needs a definition to know what this variable is.So we need to go back in whatever CPP file where we have our declaration and we need to bring this backin.And if we build now, we’re going to see that the build is good and everything is working as expected.So I really want you to know that the definitions are important for your program to run.And this is really all I had to share in this lecture, shedding some light on declarations and thedefinitions in the context of free standing variables like the weight variable here functions like theadd functions and classes like point and person.As we have seen in this lecture, we are going to stop here in this one.In the next lecture, we’re going to learn about the one definition rule.Go ahead and finish up here and meet me there.
🤖 Assistant
好的,欢迎来到 C++ 的奇妙世界!我将以C++资深开发者的经验和最佳教学方法,带你深入理解C++中两个核心概念:声明 (Declaration) 和 定义 (Definition)。
C++ 声明与定义:程序构建的基石
摘要: 本讲将详细阐述C++程序中“声明”和“定义”这两个核心概念的含义、区别及其在变量、函数、类等不同语境下的应用。理解它们对于编写清晰、高效且可编译的C++代码至关重要,特别是在多文件项目中。
1. 声明与定义:核心概念深度解析
在C++编程中,声明 (Declaration) 和 定义 (Definition) 是理解程序如何构建的两个基本但又常常令人困惑的术语。
声明 (Declaration): 声明是向 编译器 (Compiler) 介绍一个名字(例如变量名、函数名、类名)。它告诉编译器这个名字的存在,以及这个名字的一些基本信息,比如它的类型、参数列表等。声明的目的主要是为了让编译器知道这个名字是合法的,并且可以在当前 作用域 (Scope) 内被引用。它就像是告诉大家“有这么一个东西存在,它叫XXX,长这样”。
- 关键点: 声明仅仅是“告知”而非“实现”。一个名字可以被声明多次。
定义 (Definition): 定义则是在声明的基础上,为这个名字提供完整的实现或具体内容。它告诉编译器和 链接器 (Linker) 这个名字“是什么”或“做什么”。对于变量,定义是分配内存并可能初始化其值;对于函数,定义是提供函数体内的实际执行代码;对于类,定义是提供类的完整结构(成员变量、成员函数等)。定义是实际的“实体”,它分配资源(内存)并提供行为。
- 关键点: 一个名字只能被定义一次 (One Definition Rule - ODR),否则会导致链接错误。
现在,我们来更详细地看看在不同编程元素中声明和定义的具体表现。
1.1 声明与定义的关系和编译/链接阶段
理解声明和定义之间的关系,以及它们在C++程序构建过程中的作用,是至关重要的。C++程序的构建通常分为两个主要阶段:
编译阶段 (Compilation Stage):
- 任务: 编译器将你的源代码文件(
.cpp
文件)翻译成机器可以理解的 目标文件 (Object File)(例如Windows上的.obj
,Linux上的.o
)。 - 声明的作用: 在这个阶段,编译器主要依赖声明来检查语法和类型。如果你尝试使用一个未声明的名字,编译器会立即报错(“undeclared identifier”)。编译器知道
std::cout
是一个合法的名字,因为它在iostream
头文件中被声明了。
- 任务: 编译器将你的源代码文件(
链接阶段 (Linking Stage):
- 任务: 链接器将一个或多个目标文件以及任何所需的库文件(例如C++标准库)组合起来,生成最终的可执行程序。
- 定义的作用: 链接器的工作是解析所有对外部符号(变量、函数等)的引用,并将它们与它们的实际定义连接起来。如果一个名字被声明了但在程序的任何地方都没有被定义(但被使用了),链接器就会报错(“undefined reference”)。它会找不到那个声明过的“东西”的实际实现。
1.2 为什么区分声明和定义?
区分声明和定义的主要原因是为了:
- 模块化 (Modularity) 和 信息隐藏 (Information Hiding): 允许在头文件 (
.h
或.hpp
) 中放置声明,以便其他文件可以包含并使用这些实体,而无需知道其内部实现的细节。实际的定义则放在对应的.cpp
文件中。这样可以提高代码的可维护性、可重用性,并减少编译时间(因为如果只修改实现,不需要重新编译所有使用它的文件)。 - 分离编译 (Separate Compilation): C++程序通常由多个
.cpp
文件组成。每个.cpp
文件独立编译。声明允许一个文件引用在另一个文件中定义的实体。
1.3 不同场景下的声明与定义
让我们通过具体的例子来理解声明和定义在不同C++元素中的表现。
1.3.1 变量 (Variables)
对于自由变量 (Freestanding Variables)(即不在任何类或函数内部定义的全局变量),声明和定义通常是合二为一的。
声明与定义结合:
cpp1
2
3
4// 这是一个变量的声明和定义,同时进行了初始化
double weight = 0.0;
// 声明:引入了名为 'weight' 的双精度浮点型变量
// 定义:分配了内存空间,并将其值初始化为 0.0当你写
double weight = 0.0;
时,编译器知道weight
是一个double
类型,并且它会为weight
分配一块内存空间,并将0.0
存储到这块内存中。
所以,对于局部变量,通常在声明的同时就完成了定义和初始化。仅声明(外部变量):
虽然不常见,但全局变量可以被仅声明而不定义。这通过extern
关键字实现。extern
关键字告诉编译器:“这个变量在别的地方定义了,你先别给它分配内存,我只是在这里声明它。”- 文件
file1.cpp
:cpp1
2// file1.cpp
double global_variable = 100.0; // 定义 (并初始化) - 文件
main.cpp
:在这里,cpp1
2
3
4
5
6
7
8
9
10// main.cpp
extern double global_variable; // 声明:告诉编译器存在一个名为 global_variable 的双精度变量,
// 但其定义在其他地方
int main() {
std::cout << "Global variable: " << global_variable << std::endl;
return 0;
}extern double global_variable;
是一个纯粹的声明。它告诉main.cpp
编译器global_variable
存在,但它不会在main.cpp
中分配内存。实际的内存分配发生在file1.cpp
中。链接器最终会将main.cpp
中对global_variable
的引用解析到file1.cpp
中的定义。
- 文件
常见误解:
double weight;
是声明还是定义?
对于局部变量:cpp1
2
3
4
5
6
7int main() {
double weight; // 这也是一个定义!它分配了内存,只是没有显式初始化,
// 其值是一个不确定的“垃圾值” (junk value)。
// 这是一个赋值操作,不是定义
weight = 8.7;
std::cout << weight << std::endl;
}当你写
double weight;
在函数内部时,编译器会为weight
分配栈上的内存空间。即使没有显式初始化,这个内存空间也被“定义”为属于weight
变量。后续的weight = 8.7;
是一个赋值操作,而不是定义。如果这个变量是全局的或静态的,它会被自动零初始化。
1.3.2 函数 (Functions)
函数是声明和定义分离最常见的例子,尤其是在大型项目中。
声明与定义结合:
cpp1
2
3
4
5// 这是一个函数的声明和定义结合的形式
double add(double a, double b) { // 声明:函数签名 (signature) 引入 'add' 名称
// 定义:包含函数体的实际代码
return a + b;
}当函数体紧跟着函数签名时,它既是声明(介绍了函数名、返回类型和参数),也是定义(提供了函数的具体实现)。
分离声明 (Function Prototype) 与定义:
这是推荐的做法,尤其是在多文件项目中。函数声明通常被称为 函数原型 (Function Prototype)。函数声明 (在头文件中或使用前):
cpp1
2
3
4// 通常在头文件 (e.g., my_math.h) 中
double add(double a, double b); // 声明:告诉编译器存在一个名为 'add' 的函数,
// 接受两个 double 参数,返回一个 double 值。
// 但不包含函数体。这个声明告诉编译器
add
函数的存在和使用方式。编译器在遇到对add
的调用时,会根据这个声明检查参数类型和数量是否匹配。函数定义 (在
.cpp
文件中):cpp1
2
3
4
5// 通常在对应的源文件 (e.g., my_math.cpp) 中
// 注意:这里需要再次提供完整的函数签名,但关键是它提供了函数体
double add(double a, double b) { // 定义:提供了 'add' 函数的实际实现代码
return a + b;
}这个定义包含了
add
函数的实际逻辑。链接器在程序中找到对add
的所有调用,然后将它们连接到这个定义。
缺失定义时的链接器错误:
如果一个函数被声明并被调用,但其定义缺失,则会产生链接错误。cpp1
2
3
4
5
6
7
8
9
10
11
12// main.cpp
double calculate(double x, double y); // 声明
int main() {
double result = calculate(10.0, 5.0); // 调用了 calculate 函数
std::cout << "Result: " << result << std::endl;
return 0;
}
// calculate 函数的定义在这里缺失!
// 如果没有另一个 .cpp 文件提供 calculate 的定义,链接器会报错编译
main.cpp
会成功,因为calculate
被声明了。但在链接时,链接器会发现calculate
函数被调用了,但它在任何目标文件中都找不到calculate
的实际代码,于是会报“undefined reference tocalculate
”的链接错误。
1.3.3 类 (Classes) 和结构体 (Structs)
类和结构体在 C++ 中是用户定义类型。它们的完整定义通常同时作为声明和定义。
声明与定义结合 (类/结构体本身):
cpp1
2
3
4
5
6// 这是一个 struct 的声明和定义
struct Point { // 声明:引入了 'Point' 这个名字
// 定义:提供了 Point 类型的完整结构(成员变量 m_x, m_y)
double m_x;
double m_y;
};当你定义一个类或结构体时,你不仅引入了它的名字(例如
Point
),还提供了关于它如何布局内存、包含哪些成员变量、成员函数等所有信息。因此,这个完整的结构体或类定义同时也是它的声明。分离成员函数 (Member Functions) 的声明与定义:
虽然类的定义本身通常是完整的,但类中的成员函数 (Member Functions) 可以像普通函数一样,将声明放在类内部,而将定义放在类外部,通常在.cpp
文件中。类声明 (通常在头文件
person.h
中):cpp1
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// person.h
class Person {
public:
// 成员变量声明
std::string m_full_name;
int m_age;
// 静态成员变量声明 (注意:这里只是声明,没有分配内存)
static int s_person_count; // s_person_count 是 static 成员变量
// 构造函数声明 (在类内部)
Person(const std::string& full_name_param, int age_param);
// 成员函数声明 (在类内部)
void print_info() const;
// 静态成员函数声明
static int get_person_count();
};在
person.h
中,Person
类被声明,它的成员变量和成员函数(包括构造函数和静态成员)也都在这里被声明了。编译器知道了Person
类有哪些成员。成员函数定义 (通常在
person.cpp
中):cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// person.cpp
// 静态成员变量的定义 (必须在类外部,且通常在 .cpp 文件中进行,分配内存)
// 注意:这里不需要 'static' 关键字,因为 Person::s_person_count 已经通过其声明是静态的了。
int Person::s_person_count = 0; // 定义并初始化为 0
// 构造函数的定义 (使用作用域解析运算符 ::)
Person::Person(const std::string& full_name_param, int age_param)
: m_full_name(full_name_param), m_age(age_param) {
s_person_count++; // 每次创建 Person 对象时递增计数
}
// 成员函数的定义
void Person::print_info() const {
std::cout << "Name: " << m_full_name << ", Age: " << m_age << std::endl;
}
// 静态成员函数的定义
int Person::get_person_count() {
return s_person_count;
}在
person.cpp
中,我们提供了Person
类成员函数和静态成员变量的实际定义。这里的Person::
是 作用域解析运算符 (Scope Resolution Operator),它指明了这些定义属于Person
类。
静态成员变量的特殊性:
静态成员变量s_person_count
在类内部只是一个声明。它不占用类的实例的内存,而是所有对象共享的一份内存。因此,它必须在类外部的某个.cpp
文件中进行定义并分配内存。如果没有这个定义,并且程序中使用了s_person_count
,就会导致链接错误。
1.4 总结声明与定义的关键规则
- 声明可以多次,定义只能一次。
- 声明告诉编译器一个名字存在。
- 定义为这个名字分配内存并提供实际内容/实现。
- 如果一个名字被使用:
- 无声明: 编译器错误 (undeclared identifier)。
- 有声明无定义: 链接器错误 (undefined reference)。
- 如果一个名字从未使用:
- 有声明无定义: 编译通过 (因为没有被使用,链接器不需要寻找其定义)。
理解这些规则是编写复杂C++程序,尤其是涉及多个源文件和头文件的项目的基础。
2. 代码示例
让我们通过一个完整的项目结构来演示声明和定义的用法。
项目结构:
1 | project_root/ |
some_math.h
(函数声明)
1 | // some_math.h |
some_math.cpp
(函数定义)
1 | // some_math.cpp |
person.h
(类声明)
1 | // person.h |
person.cpp
(类成员定义)
1 | // person.cpp |
main.cpp
(主程序)
1 | // main.cpp |
如何编译这个项目 (以 GCC 为例):
1 | g++ main.cpp some_math.cpp person.cpp -o my_program |
这个命令会将
main.cpp
、some_math.cpp
和 person.cpp
分别编译成目标文件,然后将它们链接起来生成 my_program
可执行文件。如果缺少任何一个 .cpp
文件(例如 person.cpp
),那么当 main.cpp
中使用了 Person
类的定义时,就会出现链接错误。
3. QA 闪卡 (Flash Cards)
Q1: 什么是声明 (Declaration)?
A1: 声明是向编译器介绍一个名字(如变量名、函数名、类名),并提供其基本信息(如类型、参数列表),告知其存在且可在当前作用域内使用。
Q2: 什么是定义 (Definition)?
A2: 定义是为声明过的名字提供完整的实现或具体内容,如为变量分配内存、为函数提供实际代码、为类提供完整结构。
Q3: 一个名字可以被声明多次吗?可以被定义多次吗?
A3: 一个名字可以被声明多次,但只能被定义一次(C++ 的 One Definition Rule)。
Q4: 如果一个函数被声明但没有定义,并且在程序中被调用了,会发生什么错误?
A4: 会发生 链接器错误 (Linker Error),通常是“undefined reference”(未定义引用)。编译器在编译时会通过,但链接器找不到实际的代码实现。
Q5: double value;
在 main
函数内部时,它是声明还是定义?
A5: 它是定义。它为 value
分配了内存空间,即使没有显式初始化,内存也已经被“定义”为属于这个变量。
4. 常见误解和错误
“声明就是头文件,定义就是源文件”的过度简化:
- 误解: 有些初学者会简单地认为头文件里都是声明,源文件里都是定义。
- 纠正: 虽然这是常见且推荐的做法,但并非绝对。例如,模板函数的定义通常也放在头文件中;内联函数 (inline function) 的定义也经常放在头文件中。类的完整定义(包含成员变量和在类内定义的成员函数)本身就是声明和定义的结合,通常也放在头文件中。关键在于理解声明和定义的本质,而不是简单地看文件类型。
混淆局部变量的“声明”和“赋值”:
- 误解: 认为
int x;
是声明,x = 5;
才是定义或初始化。 - 纠正:
int x;
(在函数内部)已经是一个定义了,它为x
分配了内存(尽管值不确定)。x = 5;
是一个赋值操作,不是定义。对于全局变量或静态变量,int x;
也是定义,并且会被自动零初始化。
- 误解: 认为
忘记定义静态成员变量:
- 误解: 认为在类内部声明
static int count;
就足够了。 - 纠正:
static int count;
只是声明,它不为count
分配内存。你必须在类外部的某个.cpp
文件中提供其定义(例如int MyClass::count = 0;
)。如果使用了count
而没有这个定义,就会导致链接错误。
- 误解: 认为在类内部声明
在头文件中定义非内联函数或非模板函数:
- 误解: 为了方便,直接把函数定义(带函数体)写在头文件中。
- 纠正: 除了内联函数和模板函数外,不应该在头文件中定义普通函数。如果这样做,并且这个头文件被多个
.cpp
文件包含,那么每个.cpp
文件都会生成该函数的一个定义,最终在链接时违反 One Definition Rule (ODR),导致“multiple definition”(重复定义)的链接错误。
认为没有
extern
关键字的全局变量就是声明:- 误解:
int globalVar;
放在.cpp
文件顶部就是声明。 - 纠正: 在全局作用域下,
int globalVar;
默认就是定义(并零初始化)。如果你想在另一个.cpp
文件中引用它,才需要使用extern int globalVar;
进行声明。
- 误解:
5. 编程练习
现在,是时候动手实践了!请完成以下代码,使其能够成功编译并运行。你的任务是补全缺失的声明和定义,使其符合C++的声明与定义规则。
项目结构:
1 | exercise_root/ |
math_operations.h
1 | // math_operations.h |
math_operations.cpp
1 | // math_operations.cpp |
rectangle.h
1 | // rectangle.h |
rectangle.cpp
1 | // rectangle.cpp |
main.cpp
1 | // main.cpp |
请你填写 TODO
部分的代码,然后告诉我你的解决方案。期待你的答案!
🧑💻 User
为什么Inline function可以在header中定义?
🤖 Assistant
这是一个非常棒的问题!你观察得很仔细,而且思考得很深入,这正是一个优秀C++程序员应该具备的素质。
我们前面讲到,一个名字(比如函数)只能被 定义 (Definition) 一次,这就是 C++ 的 单一定义规则 (One Definition Rule - ODR)。如果违反了这条规则,在链接阶段就会出现“重复定义”(multiple definition) 的错误。所以,将普通函数的定义放在头文件中,然后这个头文件被多个 .cpp
文件包含时,每个 .cpp
文件都会生成一份该函数的定义,最终导致链接错误。
那么,为什么 inline
(内联) 函数可以在头文件中定义呢?这正是 inline
关键字的一个关键作用!
inline
关键字有两层含义:
首要含义(优化提示):
inline
关键字是对 编译器 (Compiler) 的一个 提示 (Hint),建议编译器在编译时将函数的调用点直接替换为函数的实际代码,而不是生成一个函数调用指令。这就像把函数体“粘贴”到调用它的地方。这样可以消除函数调用的开销(如参数传递、栈帧管理),从而提高程序执行效率。但请注意,这只是一个提示,编译器有权决定是否真正进行内联。现代编译器非常智能,即使没有inline
关键字,它们也可能对小型函数进行内联优化;反之,对于大型或复杂的inline
函数,编译器也可能选择不内联。次要但更重要的含义(ODR 豁免): 这是为什么
inline
函数可以在头文件中定义的核心原因。当一个函数被声明为inline
时,C++ 标准放宽了对它的 单一定义规则 (ODR) 限制。它允许一个inline
函数在程序的多个 翻译单元 (Translation Unit)(即.cpp
文件经过预处理后的结果)中存在多个定义,但前提是所有这些定义都必须是完全相同的。- 链接器如何处理? 当链接器遇到一个
inline
函数的多个相同定义时,它知道这是一个合法的inline
函数,并且能够智能地选择其中一个定义或者以某种方式合并它们,以避免产生“重复定义”的链接错误。它不会像处理普通函数那样,看到多个定义就报错。
- 链接器如何处理? 当链接器遇到一个
为什么这种特性对于头文件很重要?
将 inline
函数的定义放在头文件中,有以下几个主要优点:
实现真正的内联优化: 如果
inline
函数的定义不在头文件中,而是在一个.cpp
文件中,那么当其他.cpp
文件调用它时,编译器在编译调用点时看不到函数的实际代码。在这种情况下,编译器就无法执行内联优化。只有当函数的定义对编译器可见时,它才能进行内联。将定义放在头文件中,可以确保每个包含该头文件的.cpp
文件在编译时都能看到函数体,从而使编译器有机会进行内联替换。简化使用: 将定义放在头文件中,用户只需包含头文件即可使用该
inline
函数,无需在额外的.cpp
文件中定义。与模板函数类似: 实际上,C++ 中的 模板函数 (Template Functions) 也有类似的特性,它们的定义通常也必须放在头文件中。原因也是为了让编译器在实例化模板时能够看到其完整的定义。
inline
关键字和模板的工作机制在打破 ODR 方面有异曲同工之妙。
总结:
inline
关键字的本质是告诉编译器:“这个函数可能很小,请考虑将其代码直接插入到调用点,并且如果这个函数的定义出现在多个地方,请链接器不要抱怨,只要它们完全相同就行。”
所以,当你在头文件中定义一个 inline
函数时,你实际上是告诉编译器和链接器:
- 这个函数可能会被内联。
- 这个函数的定义可以在多个
.cpp
文件中重复出现,只要内容一致,就不要报错。
理解这一点,对于你在 C++ 中正确组织代码,特别是处理头文件和源文件之间的关系,以及理解模板的工作方式,都至关重要。