Có thể một #incoide bị thiếu có thể phá vỡ chương trình khi chạy không?


31

Có trường hợp nào, khi thiếu một #includephần mềm sẽ phá vỡ phần mềm khi chạy, trong khi quá trình xây dựng vẫn đi qua?

Nói cách khác, có thể là

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

complexLogic();
cleverAlgorithms();

Cả hai sẽ xây dựng thành công, nhưng hành xử khác nhau?


1
Có thể với bao gồm của bạn, bạn có thể mang lại mã của mình các cấu trúc được xác định lại khác với các cấu trúc được sử dụng bởi việc triển khai các hàm. Điều này có thể dẫn đến không tương thích nhị phân. Những tình huống như vậy không thể được xử lý bởi trình biên dịch và trình liên kết.
armagedescu

11
Nó chắc chắn là như vậy. Khá dễ dàng để có các macro được định nghĩa trong một tiêu đề thay đổi hoàn toàn ý nghĩa của mã xuất hiện sau tiêu đề đó là #included.
Peter

4
Tôi chắc chắn Code Golf đã thực hiện ít nhất một thử thách dựa trên điều này.
Đánh dấu

6
Tôi muốn chỉ ra một ví dụ cụ thể trong thế giới thực: Thư viện VLD để phát hiện rò rỉ bộ nhớ. Khi một chương trình kết thúc với VLD hoạt động, nó sẽ in ra tất cả các rò rỉ bộ nhớ được phát hiện trên một số kênh đầu ra. Bạn tích hợp nó vào một chương trình bằng cách liên kết với thư viện VLD và đặt một dòng duy nhất #include <vld.h>ở vị trí chiến lược trong mã của bạn. Xóa hoặc thêm tiêu đề VLD không "phá vỡ" chương trình, nhưng nó ảnh hưởng đáng kể đến hành vi thời gian chạy. Tôi đã thấy VLD làm chậm một chương trình đến mức nó trở nên không sử dụng được.
Haliburton

Câu trả lời:


40

Vâng, nó hoàn toàn có thể. Tôi chắc chắn có rất nhiều cách, nhưng giả sử tệp bao gồm chứa định nghĩa biến toàn cục được gọi là hàm tạo. Trong trường hợp đầu tiên, hàm tạo sẽ thực thi và trong trường hợp thứ hai thì không.

Đặt định nghĩa biến toàn cục trong tệp tiêu đề là kiểu kém, nhưng có thể.


1
<iostream>trong thư viện tiêu chuẩn thực hiện chính xác điều này; nếu bất kỳ đơn vị dịch thuật nào bao gồm <iostream>thì std::ios_base::Initđối tượng tĩnh sẽ được xây dựng khi bắt đầu chương trình, khởi tạo các luồng ký tự std::cout, v.v., nếu không thì không.
ecatmur

33

Vâng, điều đó là có thể.

Tất cả mọi thứ liên quan đến #includes xảy ra tại thời gian biên dịch. Nhưng thời gian biên dịch mọi thứ có thể thay đổi hành vi trong thời gian chạy, tất nhiên:

some/code.h:

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

sau đó

#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
}

Với #include, độ phân giải quá tải tìm thấy foo(int)bản in phù hợp hơn và do đó 1thay vì 2. Ngoài ra, vì FOOđược định nghĩa, nó cũng in FOO.

Đó chỉ là hai ví dụ (không liên quan) xuất hiện trong đầu tôi ngay lập tức và tôi chắc chắn còn nhiều điều nữa.


14

Chỉ cần chỉ ra trường hợp tầm thường, chỉ thị tiền biên dịch:

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

Và sau đó

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

Đó là bệnh lý, có lẽ, nhưng tôi đã có một trường hợp liên quan xảy ra:

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

using namespace std;

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

Trông vô thưởng vô phạt. Cố gắng gọi std::max. Tuy nhiên, windows.h định nghĩa tối đa là

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

Nếu đây là std::max, đây sẽ là một lệnh gọi hàm bình thường để đánh giá f () một lần và g () một lần. Nhưng với windows.h trong đó, giờ đây nó đánh giá f () hoặc g () hai lần: một lần trong khi so sánh và một lần để nhận giá trị trả về. Nếu f () hoặc g () không phải là idempotent, điều này có thể gây ra vấn đề. Ví dụ: nếu một trong số chúng là một bộ đếm trả về một số khác nhau mỗi lần ....


+1 để gọi ra chức năng tối đa của Window, một ví dụ trong thế giới thực bao gồm việc thực hiện tà ác và là nguyên nhân dẫn đến tính di động ở mọi nơi.
Scott M

3
OTOH, nếu bạn thoát khỏi using namespace std;và sử dụng std::max(f(),g());, trình biên dịch sẽ nắm bắt được vấn đề (với một thông báo tối nghĩa, nhưng ít nhất là chỉ vào trang web cuộc gọi).
Ruslan

@Ruslan Ồ, vâng. Nếu có cơ hội, đó là kế hoạch tốt nhất. Nhưng đôi khi một người đang làm việc với mã kế thừa ... (không ... không đắng. Không đắng chút nào!)
Cort Ammon

4

Có thể thiếu một chuyên môn mẫu.

// 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

Không tương thích nhị phân, truy cập một thành viên hoặc thậm chí tệ hơn, gọi một chức năng của lớp sai:

#pragma once

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

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

#endif

Một hàm sử dụng nó, và nó là ok:

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

Đưa vào một phiên bản khác của lớp:

#pragma once

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

class class_w
{
public: int a;
};

#endif

Sử dụng các hàm trong main, định nghĩa thứ hai thay đổi định nghĩa lớp. Nó dẫn đến sự không tương thích nhị phân và đơn giản là sự cố khi chạy. Và khắc phục sự cố bằng cách xóa phần đầu tiên bao gồm trong main.cpp:

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

Không có biến thể nào tạo ra lỗi thời gian biên dịch hoặc liên kết.

Tình huống ngược lại, thêm một bao gồm sửa lỗi sự cố:

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

Những tình huống này thậm chí còn khó khăn hơn nhiều khi sửa lỗi trong một phiên bản cũ của chương trình, hoặc sử dụng một thư viện / dll / đối tượng chia sẻ bên ngoài. Đó là lý do tại sao đôi khi phải tuân theo các quy tắc tương thích ngược nhị phân.


Tiêu đề thứ hai sẽ không được bao gồm do ifndef. Nếu không, nó sẽ không biên dịch (xác định lại lớp không được phép).
Igor R.

@IgorR. Hãy chú ý. Tiêu đề thứ hai (gồm1.h) là tiêu đề duy nhất được bao gồm trong mã nguồn đầu tiên. Điều này dẫn đến sự không tương thích nhị phân. Đó chính xác là mục đích của mã, để làm sáng tỏ cách bao gồm có thể dẫn đến sự cố khi chạy.
armagedescu

1
@IgorR. Đây là mã rất đơn giản, minh họa một tình huống như vậy. Nhưng trong tình huống thực tế cuộc sống có thể phức tạp hơn nhiều. Hãy thử vá một số chương trình mà không cần cài đặt lại toàn bộ gói. Đây là tình huống điển hình trong đó phải tuân thủ nghiêm ngặt các quy tắc tương thích nhị phân lạc hậu. Nếu không thì vá là một nhiệm vụ bất khả thi.
armagedescu

Tôi không chắc "mã nguồn đầu tiên" là gì, nhưng nếu bạn muốn nói rằng 2 đơn vị dịch thuật có 2 định nghĩa khác nhau về một lớp thì đó là vi phạm ODR, tức là hành vi không xác định.
Igor R.

1
Đó là hành vi không xác định , như được mô tả bởi Tiêu chuẩn C ++. FWIW, tất nhiên, có thể gây ra một UB theo cách này ...
Igor R.

3

Tôi muốn chỉ ra rằng vấn đề cũng tồn tại ở C.

Bạn có thể nói cho trình biên dịch một hàm sử dụng một số quy ước gọi. Nếu bạn không, trình biên dịch sẽ phải đoán rằng nó sử dụng cái mặc định, không giống như trong C ++ nơi trình biên dịch có thể từ chối biên dịch nó.

Ví dụ,

main.c

int main(void) {
  foo(1.0f);
  return 1;
}

foo.c

#include <stdio.h>

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

Trên Linux trên x86-64, đầu ra của tôi là

0

Nếu bạn bỏ qua nguyên mẫu ở đây, trình biên dịch giả định rằng bạn có

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

Và quy ước cho các danh sách đối số không xác định yêu cầu floatphải được chuyển đổi doubleđể được thông qua. Vì vậy, mặc dù tôi đã đưa ra 1.0f, trình biên dịch chuyển đổi nó 1.0dđể chuyển nó sang foo. Và theo Bổ sung Bộ xử lý Kiến trúc AMD64 của Giao diện nhị phân ứng dụng System V, phần doubleđược truyền trong 64 bit có ý nghĩa nhỏ nhất xmm0. Nhưng foomong đợi một số float và nó đọc nó từ 32 bit có ý nghĩa nhỏ nhất xmm0và được 0.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.