关于在 C++ 中从不同的源文件引用 class 的问题

Question about referencing a class from a different source file in C++

我最近遇到了一些 "undefined reference" 错误,我设法解决了这些错误,但我不明白该解决方案为何有效。我有以下主要源文件:

Main.cpp:

 #include <iostream>
#include "Log.h"

    int main()
    {
        std::cout << "Hello World!" << std::endl;

        Log log;
        log.SetLevel(Log::LevelWarning);
        log.Error("Hello!");
        log.Warning("Hello!");
        log.Info("Hello!");

        std::cin.get();
    }

它引用了在单独的源文件中声明的 class:

Log.cpp:

#include <iostream>

class Log
{
public:
    enum Level
    {
       LevelError, LevelWarning, LevelInfo 
    };

private:
    Level m_LogLevel = LevelInfo;

public:
    void SetLevel (Level level)
    {
        m_LogLevel = level;
    }

    void Error (const char* message)
    {
        if (m_LogLevel >= LevelError)
            std::cout << "[ERROR]: " << message << std::endl;
    }

    void Warning (const char* message)
    {
        if (m_LogLevel >= LevelWarning)
            std::cout << "[WARNING]: " << message << std::endl;
    }

    void Info (const char* message)
    {
        if (m_LogLevel >= LevelInfo)
            std::cout << "[INFO]: " << message << std::endl;
    }
};

Log.h:

#pragma once

class Log
{
public:
    enum Level { LevelError, LevelWarning, LevelInfo };

private:
    Level m_LogLevel;

public:
    void SetLevel (Level);
    void Error (const char*);
    void Warning (const char*);
    void Info (const char*);
};

上面的代码为 Main.cpp 中调用的 class 日志的所有成员提供了链接器错误 "undefined reference to Log::..."。四处搜索,我最终发现评论说的是 "static members and functions should be initialized",这让我想到添加以下内容:

void Init()
{
    Log log;
    log.SetLevel(Log::LevelInfo);
    log.Error("NULL");
    log.Warning("NULL");
    log.Info("NULL");
}

到我的 Log.cpp 文件。这令人惊讶地解决了问题并且项目构建成功,但是这些成员没有声明为静态的,所以我不明白为什么会这样,或者即使这是正确的解决方案。

我在 linux 中使用 gcc 并使用 "g++ Main.cpp Log.cpp -o main" 进行编译。源文件在同一文件夹中。

c++ 不是 javac#。此构造根本不会生成任何代码:

class X
{
public:
     void foo()
     {
         std::cout << "Hello, world"<< std::endl;
     }
};

是的,在 java 编译后你会得到 X.class 你可以使用。然而,在 C++ 中,这不会产生任何结果。

证明:

#include <stdio.h>

class X
{
    void foo()
    {
        printf("X");
    }
};

$ gcc -S main.cpp
$ cat main.s
    .file   "main.cpp"
    .ident  "GCC: (GNU) 4.9.3"
    .section        .note.GNU-stack,"",@progbits

在 c++ 中你需要 "definitions" 以外的东西来编译任何东西。

如果你想模仿 java-like 编译器行为,请执行以下操作:

class X
{
public:
    void foo();
};

void X::foo()
{
    std::cout << "Hello, world"<< std::endl;
}

这将生成包含 void X::foo().

的目标文件

证明:

$ gcc -c test.cpp
$ nm --demangle test.o
0000000000000000 T X::foo()

另一种选择当然是像您一样使用内联方法,但在这种情况下,您需要将整个 "Log.cpp" #include 整个 "Log.cpp" 到您的 "Main.cpp".

在 c++ 中,编译由 "translation units" 而不是 classes 完成。一个单元(比如.cpp)产生一个目标文件(.o)。这样的目标文件包含机器指令和数据。

编译器现在看不到正在编译的翻译单元之外的任何内容。

因此,与 Java 不同,当 main.cpp 被编译时,编译器只会看到 #included 到 main.cpp 和 main.cpp 本身的内容。因此编译器此时看不到 Log.cpp 的内容。

只有在 link 时,翻译单元生成的目标文件才会合并在一起。但是这个时候编译什么都来不及了

具有内联函数的class(如第一个示例)未定义任何机器指令或数据。

对于 class 的内联成员,只有在您使用它们时才会生成机器指令。

由于您在编译 Log.cpp 期间在翻译单元 Log.cpp 之外的 main.cpp 中使用了 class 成员,因此编译器不会为它们生成任何机器指令。

One Definition Rule 的问题是不同的。

您的代码组织不正确。同一个 class.

不应有两个不同的 class Log { ... }; 内容

Main.cpp 需要知道 class Log 的内容,因此 class Log 的(单个)定义需要在您的 header 文件中。这就留下了 class 成员函数定义的问题。定义class成员函数的三种方式:

  1. 在 class 定义中(在 header 中)。

这就是您在 Log.cpp 文件中尝试的内容。如果您在 Log.h 中定义 class 定义中的所有成员,那么您根本不需要 Log.cpp 文件。

  1. 在 class 定义之外,使用 inline 关键字,在 header 文件中。

这看起来像:

// Log.h
class Log
{
    // ...
public:
    void SetLevel(Level level);
    // ...
};

inline void Log::SetLevel(Level level)
{
    m_LogLevel = level;
}
  1. 在 class 定义之外,在源文件中没有 inline 关键字。

这看起来像:

// Log.h
class Log
{
    // ...
public:
    void SetLevel(Level level);
    // ...
};

// Log.cpp
#include "Log.h"

void Log::SetLevel(Level level)
{
    m_LogLevel = level;
}

注意 Log.cpp 包含 Log.h,以便编译器在您开始尝试定义其成员之前看到 class 定义。

您可以混合搭配这些。虽然对于什么是最好的没有严格的规定,但一般准则是小而简单的函数可以放在 header 文件中,而大而复杂的函数可能在源文件中做得更好。一些程序员建议根本不要在 class 定义中放置任何函数定义,或者将此选项限制在定义非常短且有助于明确函数目的的情况下,从那时起 (public 的一部分) class 定义是对 class 所做的事情的总结,而不是关于它如何做的细节。

在某些情况下,在源 *.cpp 文件中定义 class 可能是合适的 - 但这意味着它只能在该文件中使用。