缺少的#include是否有可能在运行时中断程序?


31

是否有任何情况#include会在构建仍然进行的情况下丢失a 并在运行时破坏软件?

换句话说,是否有可能

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

complexLogic();
cleverAlgorithms();

会成功构建,但行为会有所不同?


1
可能通过包含,您可以引入与函数实现所使用的结构不同的代码重新定义的结构。这可能导致二进制不兼容。编译器和链接器无法处理这种情况。
armagedescu

11
必然是。在标头中定义宏来完全改变标头#included 之后出现的代码的含义是很容易的。
Peter

4
我确信Code Golf在此基础上至少完成了一项挑战。
标记

6
我想指出一个具体的实际示例:用于内存泄漏检测的VLD库。当程序在VLD处于活动状态终止时,它将在某些输出通道上打印出所有检测到的内存泄漏。通过链接到VLD库并#include <vld.h>在代码中的战略位置放置一行,即可将其集成到程序中。删除或添加该VLD标头不会“破坏”程序,但会严重影响运行时行为。我已经看到VLD将程序减慢到无法使用的地步。
哈利伯顿

Answers:


40

是的,这完全有可能。我敢肯定有很多方法,但是假设包含文件包含一个称为构造函数的全局变量定义。在第一种情况下,构造函数将执行,而在第二种情况下,则不会执行。

将全局变量定义放在头文件中的样式不佳,但是有可能。


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

33

是的,那是可能的。

关于#includes的所有事情都在编译时发生。但是,编译时,事情当然可以改变运行时的行为:

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

这只是我立刻想到的两个(不相关的)示例,我敢肯定还有更多示例。


14

只是指出平凡的情况,预编译器指令:

// 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()不是等幂的,则可能导致问题。例如,如果其中一个碰巧是一个计数器,每次都会返回一个不同的数字。


+1用于调出Window的max函数,这是一个现实世界的例子,其中包括实现弊端和对任何地方的可移植性的危害。
Scott M

3
OTOH,如果您摆脱using namespace std;并使用std::max(f(),g());,编译器将抓住问题(带有模糊的消息,但至少指向调用站点)。
Ruslan

@Ruslan哦,是的。如果有机会,那是最好的计划。但是有时候人们正在使用遗留代码……(不……一点也不苦。一点也不苦!)
Cort Ammon

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);
}

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 /共享对象时,这些情况甚至更加困难。因此,有时必须遵循二进制向后兼容的规则。


由于ifndef,不会包含第二个标头。否则它将无法编译(不允许重新定义类)。
伊戈尔·

@IgorR。留心。第二个标头(include1.h)是第一个源代码中唯一包含的标头。这导致二进制不兼容。这正是代码的目的,说明了include如何导致运行时崩溃。
armagedescu

1
@IgorR。这是非常简单的代码,它说明了这种情况。但是在现实生活中,情况可能会更加复杂。尝试修补某些程序而不重新安装整个程序包。在典型情况下,必须严格遵循向后二进制兼容性规则。否则,修补是不可能完成的任务。
armagedescu

我不确定“第一个源代码”是什么,但是如果您的意思是2个翻译单元具有2个类的不同定义,则表示违反ODR,即未定义的行为。
伊戈尔·

1
如C ++标准所述,这是未定义的行为。FWIW当然可以通过这种方式导致UB ...
Igor R.

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);
}

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

0

如果您在此处省略原型,则编译器会假设您已经

int foo(); // Has different meaning in C++

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.