使用 C++ 几十年后,就慢慢掌握了 C++ 中“条条大路通罗马”的原理。任何事情都有多种解决方案,反之,造成编译失败的原因也是多种多样。本篇博客野心不小,因为我想试去厘清一些避免 C++ 编译错误的方法。但正本清源,要规避错误,首先要了解错误的源头。
的确,编译器有可能导致编译失败。不过,触发编译器的推手是你——程序员,做了一些不正确的操作。当程序员提供C++程序,编译器无法转换为机器代码时,编译失败。
说得更明白些?让我举一个编译失败的典型例子:
void ConvertStringToPasswordForm(char password[])
{
while (*password != '\0') *password++ = '*';
}
这个函数的驱动程序如下
int main(int argc, char** argv)
{
char* password = "MyTopSecretPasswordPublishedInABlog:-)";
ConvertStringToPasswordForm(password);
std::cout << "Password :: " << password << std::endl;
}
错误提醒信息说得很清楚:
error C2664: ‘void ConvertStringToPasswordForm(char [])’: cannot convert argument 1 from ‘const char *’ to ‘char []’
所以这是一个 const 问题,于是你想当然地把 driver 改成:
int main(int argc, char** argv)
{
const char* password = "MyTopSecretPasswordPublishedInABlog:-)";
ConvertStringToPasswordForm(password);
std::cout << "Password :: " << password << std::endl;
}
但编译器是无法变通的,编译失败的问题仍然没有解决。现在,cpp 编译错误消息为:
error C2664: ‘void ConvertStringToPasswordForm(char [])’: cannot convert argument 1 from ‘const char *’ to ‘char []’
可能有一瞬间,你想到将第一个函数的参数更改为 const char* 以修复编译失败,但最后还是放弃了。作为一名优秀 C++ 程序员,你最终决定关闭编译器:
int main(int argc, char** argv)
{
const char* password = "MyTopSecretPasswordPublishedInABlog:-)";
ConvertStringToPasswordForm(const_cast<char*>(password));
std::cout << "Password :: " << password << std::endl;
}
程序进行编译了吗?当然,但编译结果,显然是失败!
那么,解决这个 cpp 编译问题的正确方法是什么?正确方案如下:
int main(int argc, char** argv)
{
char password[] = "MyTopSecurePasswordPublishedInABlog:-)";
ConvertStringToPasswordForm(password);
std::cout << "Password :: " << password << std::endl;
}
不过,搞定编译器还不够,我们需要理解程序运行的原理(可尝试这组程序:https://coliru.stacked-crooked.com/a/5e246877801d5263).
1.理解语言。在 C 和 C++ 中,数组名会退化为指针,但并不总是像上面例子中呈现的那样。在任何语言中,避免编译失败的最重要方法就是充分理解该语言。
2.语法也在变化。让我用一个例子来解释:
int main(int argc, char *argv[])
{
for (int i = 0; i < 10; ++i) { /*do something */ }
int valueof_i = i;
return 0;
}
我希望这能在旧版的 Dev-C++IDE 上运行,但事实并非如此。所以,我得到以下 C++ 编译错误:
该程序将是有效的,因为在之前,for 循环的索引变量作用域扩展到外部。因此,如果在新 ISO for 作用域导入之前,旧的 C++ 程序进行了迁移,那么 CPP 编译问题就会出现。在 Visual Studio 中,我们可以关闭一致性(不建议新代码使用):
3.IDE 是友军。无论免费还是付费,大部分IDE 的功能都很出色。下面举一个例子,说明如何使用IDE帮助编译:
例如:只要加上大括号{}, Visual Studio 2019 IDE 就会帮助完成编译。
但,还记得你有多少次因为忘记分号而导致编译失败?不过,IDE 能自动修正,进而避免编译失败,这功能简直太友好!此外,优秀的 IDE 还有关键字突出显示、智能提示(IntelliSense)和上下文感应(context-sensitive)等功能。
4.保持好的工作习惯。假设你正在处理一个复杂的程序,使用自下而上或自上而下的方式编码,容易理解和编译的小函数能帮助你将工作简单化。在编写代码时,要随时保持程序编译干净、整洁。
5.格外注意模板化代码编写。模板元编程是图灵完备(Turing complete)的,因此编写元程序很有意思。但编译器编写者是否也希望模板错误是图灵完备的?不过,最新的编译器(支持 C++17 std及更高版本)越来越能精确地定位错误。
例如,下列代码:
template<typename T>
class SimpleTemplateUse
{
private:
const T& v;
public:
SimpleTemplateUse(const T& v) :v(v) {}
};
template<typename T>
int f(T x)
{
f(SimpleTemplateUse<T>(x));
}
在例证这个模板为f(0) 时,旧的 Dev-C++ IDE 显示出现以下错误:
同样的程序,VS 2019 给出了一些合理的解释:
fatal error C1202: recursive type or function dependency context too complex compilation failure error. Much better!
6.在尝试使用第三方库之前,请先了解其 API。这个建议不仅限于避免 C++编译错误,更是适用于所有支持第三方库编译的编程语言。如果不知道在何处使用 HINSTANCE、HANDLE 或 HMODULE,就不能使用 win32 API。所以,在深入研究复杂的库之前,请先详细了解各种信息。
7.如果你的程序打算跨平台运行,请确保平台相关代码在条件编译中得到很好的封装。我最近正在浏览 folly 的源代码,这是一个很完美的案例。跨平台部件封装良好:
#ifndef _WIN32
#define _GNU_SOURCE 1
#include
#endif
以上代码可在 folly\ClockGettimeWrappers.cpp 中找到。因为它们支持 macOS、Linux 和 Windows,所以条件编译很重要。如果程序是跨平台的,最好不要使用任何不同的编译器厂商提供的 C++ 专有扩展。
8.遵循良好的C++编码标准。除了C++的创建者,“C++ 内部是一个更安全的语言环境,但大家都挣扎着要出来”。当你使用像 CPPCoreGuidelines,或是谷歌编码风格等好的编码标准时,可以自动将限制在更小、更安全的语言中,从而减少不必要的编译错误。
9.今天的警告就是明天错误。所以,重视警告,这不仅仅是让软件编译能够经得起未来的重重考验,而且作为一种良好的工作习惯。在开发过程中,我们启用所有警告并认真地修复,这些话都已经说腻了。
10.使用容器技术(如 Docker)修复依赖关系。Docker 的一个重要用例是从基础映像开始复制构建环境。当然,编写一个好的 Dockerfile 需要专业知识,不过这些都是一劳永逸的工作。
11.使用 CMake 或任何此类构建生成器来自动化构建(阅读有关 CMake 生成器的更多内容)。IDE 有利于软件开发,但在不需要人工干预的情况下实现构建的自动化是很重要的。具体请看下一条。
12.设置签入/合并/滚动生成策略。确保任何分支合并都与自动生成同步,如果出现错误,该自动生成将导致合并失败,避免渗入分支的错误不会到处蔓延。
我提醒自己,我写的是一篇博客,而不是书。文章的篇幅已经够长,因此我需要简单归纳一下。
总结
下图很好地总结了文中提到避免编译错误的方法: