在运行时缺少 #include 是否可能导致程序崩溃?

31

如果在运行时缺失一个#include会导致软件崩溃,而构建仍然可以继续吗?

换句话说,是否有可能出现这种情况:

#include "some/code.h"
complexLogic();
cleverAlgorithms();

complexLogic();
cleverAlgorithms();

两者都会成功构建,但会表现出不同的行为吗?


1
可能通过您的包含文件,您可以引入重新定义的结构体,这些结构体与函数实现所使用的结构体不同。这可能会导致二进制不兼容性。编译器和链接器无法处理这种情况。 - armagedescu
11
可以的。在头文件中定义宏可以轻松地完全改变包含该头文件后面的代码的含义。 - Peter
4
我确定Code Golf 至少已经做过一个基于这个的挑战。 - Mark
6
我想指出一个具体的现实世界的例子:The VLD library,用于检测内存泄漏。当使用VLD时程序终止时,它将在某个输出通道上打印出所有检测到的内存泄漏。您可以通过链接到VLD库并在代码的战略位置放置单行#include <vld.h>来将其集成到程序中。删除或添加VLD头文件不会“破坏”程序,但它会显著影响运行时行为。我见过VLD使程序变得如此缓慢,以至于无法使用。 - Haliburton
2
https://youtu.be/xVT1y0xWgww?t=1645 - Damon
6个回答

40

是的,这完全是可能的。我相信有很多方法可以实现,但假设包含文件包含了一个调用构造函数的全局变量定义。在第一种情况下会执行构造函数,在第二种情况下则不会。

将全局变量定义放在头文件中是不好的编程风格,但这是可能的。


1
标准库中的<iostream>正是这样做的;如果任何翻译单元包括<iostream>,则在程序启动时将构造std::ios_base::Init静态对象,初始化字符流std::cout等,否则不会。 - ecatmur

34

是的,这是可能的。

有关#include的所有内容都发生在编译时。但编译时的事情当然可以在运行时改变行为:

some/code.h

#define FOO
int foo(int a) { return 1; }

那么

#include <iostream>
int foo(float a) { return 2; }

#include "some/code.h"  // Remove that line

int main() {
  std::cout << foo(1) << std::endl;
  #ifdef FOO
    std::cout << "FOO" std::endl;
  #endif
}
使用 #include,重载决议可以找到更合适的 foo(int),因此打印出了 1 而不是 2。并且,由于定义了 FOO,它还会额外打印 FOO。 这只是我立即想到的两个(无关的)示例,我相信还有很多其他的例子。

15

只是为了指出微不足道的情况,预编译指令:

// main.cpp
#include <iostream>
#include "trouble.h" // comment this out to change behavior

bool doACheck(); // always returns true

int main()
{
    if (doACheck())
        std::cout << "Normal!" << std::endl;
    else
        std::cout << "BAD!" << std::endl;
}

然后

// trouble.h
#define doACheck(...) false

这可能有点病态,但我经历过类似的情况:

#include <algorithm>
#include <windows.h> // comment this out to change behavior

using namespace std;

double doThings()
{
    return max(f(), g());
}

看起来无害。尝试调用std::max。然而,windows.h将 max 定义为

#define max(a, b)  (((a) > (b)) ? (a) : (b))

如果这是std::max,那么这将是一个正常的函数调用,只计算f()和g()一次。但由于有windows.h,现在会对f()和g()进行两次评估:一次用于比较,一次用于获取返回值。如果f()或g()不具有幂等性,这可能会导致问题。例如,如果其中一个是返回每次都不同数字的计数器...


在调用Windows的max函数时加1,这是一个实际的包含实现恶习和普遍不可移植性的祸根。 - Scott M
4
如果你去掉 using namespace std; 并使用 std::max(f(),g());,编译器将会捕获到这个问题(虽然错误信息可能比较晦涩,但至少会指向调用位置)。 - Ruslan
@Ruslan 噢,是的。如果有机会,那是最好的计划。但有时候我们需要处理遗留代码...(不,我一点也不沮丧!) - Cort Ammon

4

二进制不兼容,访问成员变量或更糟的是调用错误类的函数:

#pragma once

//include1.h:
#ifndef classw
#define classw

class class_w
{
    public: int a, b;
};

#endif

一个函数使用它,这样就可以了:

//functions.cpp
#include <include1.h>
void smartFunction(class_w& x){x.b = 2;}

引入另一个类的版本:

#pragma once

//include2.h:
#ifndef classw
#define classw

class class_w
{
public: int a;
};

#endif

在main函数中使用函数,第二个定义更改了类定义。这会导致二进制不兼容并在运行时崩溃。通过删除main.cpp中的第一个include来解决此问题:

//main.cpp

#include <include2.h> //<-- Remove this to fix the crash
#include <include1.h>

void smartFunction(class_w& x);
int main()
{
    class_w w;
    smartFunction(w);
    return 0;
}

没有任何变体会产生编译或链接时错误。

反之,添加一个包含文件可以修复崩溃:

//main.cpp
//#include <include1.h>  //<-- Add this include to fix the crash
#include <include2.h>
...

在修复旧版本程序的漏洞或使用外部库/dll/共享对象时,这些情况会更加困难。这就是为什么有时必须遵循二进制向后兼容性规则的原因。


1
@IgorR. 这是非常简单的代码,它说明了这样一种情况。但在现实生活中,情况可能会更加复杂微妙。尝试在不重新安装整个软件包的情况下修补某些程序。这是必须严格遵循向后二进制兼容性规则的典型情况。否则,修补将是一项不可能完成的任务。 - armagedescu
我不确定“第一个源代码”是什么意思,但如果你的意思是两个翻译单元有两个不同的类定义,那么这就是ODR违规,即未定义行为。 - Igor R.
@IgorR。这是一个运行时崩溃。我在这里创建的示例的确切目的是演示如何使用include在运行时引起崩溃。这是OP的问题,是否可能使用include在不破坏编译的情况下引起崩溃。而没有那个include就不会引起崩溃。 - armagedescu
1
这被C++标准描述为未定义行为。当然,通过这种方式可能会导致UB... - Igor R.
@IgorR。这正是问题和答案。 - armagedescu
显示剩余2条评论

4

可能会缺少模板特化。

// header1.h:

template<class T>
void algorithm(std::vector<T> &ts) {
    // clever algorithm (sorting, for example)
}

class thingy {
    // stuff
};

// header2.h

template<>
void algorithm(std::vector<thingy> &ts) {
    // different clever algorithm
}

// main.cpp

#include <vector>
#include "header1.h"
//#include "header2.h"

int main() {
    std::vector<thingy> thingies;
    algorithm(thingies);
}

3
我想指出这个问题在C语言中也存在。
你可以告诉编译器函数使用某种调用约定。如果你没有告诉它,编译器就必须猜测使用默认的调用约定,而不像在C++中编译器可以拒绝编译。举个例子: main.c
int main(void) {
  foo(1.0f);
  return 1;
}

foo.c

#include <stdio.h>

void foo(float x) {
  printf("%g\n", x);
}

在Linux x86-64上,我的输出是
0

如果你在这里省略原型,编译器会假设你具有...
int foo(); // Has different meaning in C++

未指定参数列表的约定要求将 float 转换为 double 以进行传递。 因此,尽管我给出了 1.0f,编译器会将其转换为 1.0d 以传递给 foo。 根据 System V 应用程序二进制接口 AMD64 架构处理器补充规范,doublexmm0 的 64 个最低有效位中传递。但是,foo 需要一个 float,并从 xmm0 的 32 个最低有效位中读取它,结果为 0。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接