我最近遇到了一些"未定义的引用"错误,但我不明白为什么该解决方案有效。我有以下主源文件:
主.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();
}
它引用在单独的源文件中声明的类:
日志.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;
}
};
日志:
#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*);
};
上面的代码给了我链接器错误"对 Log::..."对于在 Main.cpp 中调用的类日志的所有成员。搜索四周,我最终发现评论说"静态成员和函数应该初始化",这给了我添加以下内容的想法:
void Init()
{
Log log;
log.SetLevel(Log::LevelInfo);
log.Error("NULL");
log.Warning("NULL");
log.Info("NULL");
}
到我的日志.cpp文件。这惊人地解决了问题并且项目成功构建,但是这些成员没有声明为静态,所以我不明白为什么这有效,或者即使这是正确的解决方案。
我在 linux 中使用 gcc 并使用"g++ Main.cpp Log.cpp -o main"进行编译。源文件位于同一文件夹中。
c++
不是java
也不是c#
。此构造根本不会生成任何代码:
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++ 中,你需要除"定义"之外的东西来编译任何内容。
如果要模拟类似 Java 的编译器行为,请执行以下操作:
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 到你的"Main.cpp"中。
在 c++ 中,编译是由"翻译单元"而不是类完成的。一个单元(比如.cpp
(产生一个目标文件(.o
(。此类目标文件包含机器指令和数据。
编译器现在看不到正在编译的翻译单元之外的任何内容。
因此,与Java不同,当编译main.cpp
编译器时,编译器只能看到 #included 到main.cpp和main.cpp本身的内容。因此,编译器此时看不到 Log 的内容.cpp。
只有在链接时,从翻译单元生成的对象文件才会合并在一起。但此时编译任何东西都为时已晚。
具有内联函数的类(如第一个示例(不定义任何机器指令或数据。
对于类的内联成员,机器指令仅在您使用它们时生成。
由于在编译 Log 期间,您在翻译单元Log.cpp
之外的main.cpp
中使用类成员.cpp因此编译器不会为它们生成任何机器指令。
一个定义规则的问题是一个不同的问题。
您的代码组织不正确。同一类不应有两个不同的class Log { ... };
内容。
main.cpp 需要知道class Log
的内容,因此class Log
的(单个(定义需要在您的头文件中。 这就留下了类成员函数的定义问题。 有三种方法可以定义类成员函数:
- 在类定义中(位于标头中(。
这是您在 Log.cpp 文件中尝试的内容。 如果在 Log.h 中定义类定义中的所有成员,则根本不需要 Log.cpp 文件。
- 在
- 类定义之外,在头文件中使用
inline
关键字。
这看起来像:
// Log.h
class Log
{
// ...
public:
void SetLevel(Level level);
// ...
};
inline void Log::SetLevel(Level level)
{
m_LogLevel = level;
}
- 在
- 类定义之外,在源文件中没有
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,以便编译器在您开始尝试定义其成员之前看到类定义。
您可以混合搭配这些。 尽管没有关于什么是最好的严格规定,但一般准则是小而简单的函数可以放在头文件中,而大而复杂的函数在源文件中可能会做得更好。 一些程序员建议不要在类定义中放置任何函数定义,或者将此选项限制在定义非常短的情况下,并且有助于明确函数的用途,因为那时(公共部分(类定义是类做什么的摘要,而不是关于它如何做的细节。
在某些情况下,在源 *.cpp 文件中定义一个类可能是合适的 - 但这意味着它只能从该文件使用。