C++如何找到函数声明

How do C++ find the function declaration

本文关键字:函数 声明 何找 C++      更新时间:2023-10-16

我需要帮助将一个概念从Java翻译成C++。

在 Java 中,当您创建一个类并为其提供方法(函数)时,您必须在该类中定义函数,以便任何可能需要的类实例都可以正确调用它。例如,在类Employee中,您将声明并定义方法 salaryRaise(int amount) .每当Employee对象想要使用它时,它都会调用Employee.salaryRaise(i)并且Java确切地知道在哪里可以找到它 - 在Employee类中。

在C++中,函数在.h文件中声明,然后在其他地方定义。编译器如何知道在哪里可以找到此方法?

这实际上是链接器的工作,而不是编译器的工作。编译器将调用引用尚未定义的符号的函数。然后,链接器将获取不同的翻译单元,并按名称解析符号(即,将附加信息编码为命名空间、参数类型等的损坏名称......

在标头 (.h) 文件中声明函数。然后,该头文件在您需要使用该函数的任何其他文件中#include。这让编译器满意,因为它知道函数的签名以及如何调用它。

然后在源 (.cpp) 文件中定义该函数。源文件包含它自己的头文件,因此当您编译它时,您最终会得到一个包含该函数的完整编译代码的对象文件。

然后,链接器将调用函数的代码的任何部分(即包含标头的文件)与该函数的实际代码链接。

使用示例进行编辑:

#ifndef FOO_H
#define FOO_H
class Foo
{
public:
   int fooFunction(double a);
};
#endif

福.cpp:

#include "Foo.h"
int Foo::fooFunction(double a)
{
   // Function definition
   return 1;
}

编译Foo.cpp会生成包含fooFunction完整定义Foo.obj。目前为止,一切都好。

酒吧:

#ifndef BAR_H
#define BAR_H
#include "Foo.h"
class Bar
{
public:
   void barFunction();
};
#endif

酒吧.cpp:

#include "Bar.h"
void Bar::barFunction()
{
   Foo foo;
   int returnValue = foo.fooFunction(2.0);
}

Bar.cpp包括Bar.h,而又包括Foo.h。因此,当Bar.cpp进行预处理时,Foo::fooFunction的声明将插入到文件的顶部。因此,当编译语句int returnValue = foo.fooFunction(2.0);时,编译器知道如何发出机器指令来调用fooFunction,因为它知道返回值的类型(int)并且知道参数的类型(doublefoo对象的隐式this指针)。由于未提供函数定义,因此不会内联该函数(内联意味着将函数的整个代码复制到调用它的点)。相反,指向函数内存地址的指针用于调用它。因为使用了指针,编译器不关心定义 - 它只需要知道"我需要在内存位置 Y 使用参数 A 和 B 调用函数 X,并且我需要有一个 int 大小的内存部分准备好存储返回值,我将假设该地址的代码知道如何执行该函数".但是,编译器无法提前知道该函数的地址是什么(因为函数定义位于单独的.cpp文件中,并且将成为单独的编译作业(AKA 翻译单元)的一部分)。

这就是链接器的用武之地。一旦所有翻译单元都编译完(可以按任意顺序),链接器返回到编译的代码进行Bar.cpp,并通过在Bar.cpp中调用fooFunction时填写现在编译的定义的地址将两者链接在一起,从而使编译的代码完全可执行。

在C++中,函数在 .h 文件中声明,然后在其他地方定义。编译器如何知道在哪里可以找到此方法?

因为当您提供方法的定义时,您会告诉它在哪里可以找到它。

考虑一个假设的头文件:

class Gizmo
{
public:
  bool Foo();
};

现在我们将在另一个 CPP 文件中定义上面的Foo方法:

bool Gizmo::Foo()
{
  return true;
}

以下是您与上述定义的释义对话:

好的,编译器。 我正在定义一个返回布尔值的函数。 这 函数是类 Gizmo 的一部分,该函数名为 Foo。 该函数不带任何参数。 该函数的定义是: 返回 true。

作用域解析令牌::分隔封闭类的名称和函数的名称。

如果你省略了Gizmo::一点,而是写:

bool Foo()
{
  return true;
}

您仍然会定义一个名为 Foo 的函数,该函数不带任何参数并返回布尔值,但现在不是将其定义为Gizmo的一部分,而是将其定义为自己。 所谓的"自由函数"或"全局函数"。 这可以很好地编译,但是如果某处的其他代码尝试使用Gizmo::Foo(),则会出现"未解析的外部"链接器错误。

当编译器将源代码转换为二进制时,它通常会生成包含(除其他外)类的成员函数的中间文件(也称为对象文件)。

在链接时,中间文件被转换为机器代码。 如果当时可以使用的所有成员函数进行解析,则一切正常,否则会出现链接错误。

这样,就可以将成员函数的定义放在项目中的任何位置 - 只要链接器找到它需要的内容,它就可以生成二进制文件。 但是,不建议这样做,因为这种方法会使人类更难阅读/理解类目的/机制

编译器通常具有编译阶段和链接阶段。编译阶段将源文件编译为目标文件。链接阶段将收集目标文件并创建可执行映像。在链接阶段,在编译阶段未解析的符号将被解析。这对于Java和C++没有太大区别。例如,它是 Java 类如何调用不同 Java 类上的方法。

在C++中,您可以通过类名后跟::然后是方法声明来确定主体:

class Employee
{
   void salaryRaise(int amount); // Now, compiler knows this method belongs to 
                                 // the class Employee
};

然后,您可以定义正文:

void Employee::salaryRaise(int amount) // Now, compiler knows everything about
{    ^^^^^^^^^^                        // definition of the method
}

要生成目标文件(和可执行二进制文件),您必须将.cpp文件传递给编译器(实际上是链接器)。因此,编译器可以看到所有内容。

在你的头文件中,你原型/转发声明这样的类。

class Foo
{
private:
    int m_Fooint;
    int m_Fooint2;
public:
    Foo(int a, int b);
    int getFooTotal();
};

然后,您将像这样定义成员函数

Foo::Foo(int a, int b) // prototypes names don't have to be the same
{
    Foo::m_Fooint = a;
    Foo::m_Fooint2 = b;
}
int Foo::getFooTotal()
{
    return Foo::m_Fooint + Foo::m_Fooint2;
}

除了构造函数和析构函数之外,还需要数据类型。即使它是无效的,不返回任何东西。

所以你可能有这样的东西

float Foo::getSomeFloat();

double Foo::getSomeDouble();

或者您甚至可以返回一个对象

OtherClass Foo::getOtherClassObject();