Различные результаты компиляции, не использующие extern в C vs in С++

Когда я объявляю глобальную переменную в двух разных исходных файлах и определяю ее только в одном из исходных файлов, я получаю различные компиляции результатов для C++, чем для C. См. Следующий пример:

main.c

#include <stdio.h>
#include "func.h" // only contains declaration of void print();

int def_var = 10;

int main() {
    printf("%d\n", def_var);
    return 0;
}

func.c

#include <stdio.h>
#include "func.h"

/* extern */int def_var; // extern needed for C++ but not for C?

void print() {
    printf("%d\n", def_var);
}

Я компилирую следующие команды:

gcc/g++ -c main.c -o main.o
gcc/g++ -c func.c -o func.o
gcc/g++ main.o func.o -o main

g++/clan g++ жалуются на multiple definition of def_var (это поведение, которое я ожидал, когда не использовал extern). gcc/clang компилируется просто отлично. (с использованием gcc 7.3.1 и clang 5.0)

Согласно этой ссылке:

Предварительное определение - это объявление, которое может или не может выступать в качестве определения. Если фактическое внешнее определение найдено раньше или позже в той же самой системе перевода, то предварительное определение просто действует как декларация.

Поэтому моя переменная def_var должна быть определена в конце каждой единицы перевода, а затем приводить к нескольким определениям (как это сделано для C++). Почему это не так при компиляции с gcc/clang?

Ответ 1

Это, строго говоря, неверно. Говорит так же в

6.9 Внешние определения - p5

Внешнее определение - это внешнее объявление, которое также является определением функции (отличной от встроенного определения) или объекта. Если идентификатор, объявленный с внешней связью, используется в выражении (кроме как в части операнда оператора sizeof или _Alignof, результат которого является целочисленной константой), где-то во всей программе должно быть ровно одно внешнее определение для идентификатора; в противном случае должно быть не более одного.

У вас есть два определения для идентификатора с внешней связью. Вы нарушаете это требование, поведение не определено. Связь с программой и работа не противоречат этому. Это не требуется для диагностики.

И стоит отметить, что C++ не отличается в этом отношении.

[basic.def.odr]/4

Каждая программа должна содержать ровно одно определение каждой не-встроенной функции или переменной, которая не используется в этой программе вне отбрасываемого оператора; не требуется диагностика. Определение может отображаться явно в программе, оно может быть найдено в стандартной или определяемой пользователем библиотеке или (при необходимости) неявно определено (см. [Class.ctor], [class.dtor] и [class.copy ]). Встроенная функция или переменная должны быть определены в каждой единицы перевода, в которой она используется вне вне отбрасываемого оператора.

Опять же, требование "должно", и оно прямо говорит, что никакой диагностики не требуется. Как вы, возможно, заметили, существует довольно много механизмов, к которым этот пункт может применяться. Таким образом, передние конечные точки для GCC и Clang, вероятно, должны работать усерднее, и, как таковые, могут диагностировать его, несмотря на то, что этого не требуется.

Программа плохо организована в любом случае.


Как отметил М. в комментарии, в стандарте C есть информативный раздел, в котором упоминается само расширение в ответе zwol.

J.5.11 Множество внешних определений

Для идентификатора объекта может быть несколько внешних определений с явным использованием ключевого слова extern; если определения не согласуются или более одного инициализируется, поведение не определено (6.9.2).

Ответ 2

Я считаю, что вы наблюдаете расширение C, известное как " общие символы ", реализованное большинством, но не всеми компиляторами Unix-lineage C, изначально (IIUC) для совместимости с FORTRAN. Расширение обобщает правило "предварительных определений", описанное в рассказе StoryTeller, на несколько единиц перевода. Все определения внешних объектов с тем же именем и без инициализатора,

int foo; // at file scope

сворачиваются в один, даже если они отображаются в более чем одном TU, и если существует внешнее определение с инициализатором для этого имени,

int foo = 1; // different TU, also file scope

то все внешние определения без инициализаторов рассматриваются как внешние объявления. C++ компиляторы не реализуют это расширение, потому что (упрощение) никто не хотел выяснить, что он должен делать при наличии шаблонов. Для GCC и Clang вы можете отключить расширение с помощью -fno-common, но другие компиляторы Unix C могут не иметь возможности отключить его.