C: gcc возвращает ошибку «множественное определение», но msvc — нет

Вот у меня есть небольшая программа:

// header.h
#pragma once
int a;
int reta();

// extra.c
#include "header.h"
int reta() {
    return a;
}

// main.c
#include "header.h"
#include <stdio.h>
int main(void) {
    printf("%d, %d", a, reta());
}

Результат при компиляции с помощью Visual Studio (стандарт C: c17, Debug x64):

0, 0

Результат при компиляции с помощью GCC (gcc -L. -o test.exe *.c -std=c17 -mwindows -Wl,-subsystem,console):

C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/13.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:...\Temp\ccGvFOtk.o:main.c:(.bss+0x0): множественное определение `a'; C:...\Temp\ccANU1lv.o:extra.c:(.bss+0x0): впервые определено здесь Collect2.exe: ошибка: ld вернул 1 статус выхода

Мой вопрос: почему существует разница и что мне делать, чтобы исправить ошибку в случае GCC?

Вы компилируете два файла .c, main.c и extra.c, а затем связываете их. Оба определяют a Измените header.h на extern int a; и выполните определение в одном из файлов C.

stark 09.04.2024 16:59

«что мне сделать, чтобы исправить ошибку» — никогда не определяйте переменную в файле H. Туда должны идти только объявления (если только у вас нет особых случаев и вы точно не знаете, что делаете). Переменная должна быть определена в одном источнике. Если вы хотите получить к нему доступ из других источников, укажите его как extern.

Eugene Sh. 09.04.2024 16:59

@stark, ок, но почему msvc компилирует без ошибок?

ItzYurix 09.04.2024 17:03

См. stackoverflow.com/q/3663599/1216776

stark 09.04.2024 17:06

См. обсуждение в Как использовать extern для совместного использования переменных между исходными файлами? , особенно раздел «Не очень хороший способ определения глобальных переменных». GCC 10.1 изменил правила, чтобы они более точно соответствовали стандарту C, решив не использовать вариант, разрешенный Приложением J, в качестве «общего расширения» по умолчанию. Вы видите последствия этого решения.

Jonathan Leffler 10.04.2024 01:21
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
88
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В заголовочном файле у вас есть предварительное определение a. Это означает, что оба ваших файла .c, содержащие этот заголовок, будут иметь определение a. Что делает определение предварительным, так это тот факт, что оно не инициализируется явно.

Некоторые компиляторы, такие как MSVC в данном случае, объединяют несколько предварительных определений в одно определение. Однако версия gcc, которую вы используете, не поддерживает, поэтому вы получаете ошибку множественного определения. Если бы вы инициализировали a в заголовочном файле, это было бы полное определение, и оба компилятора, скорее всего, выдали бы ошибку.

Правильный способ справиться с этим — объявить a в заголовочном файле и определить его ровно в одном исходном файле.

// header.h
#pragma once
extern int a;        // declaration of a
int reta();

// extra.c
#include "header.h"

int a;               // definition of a

int reta() {
    return a;
}

// main.c
#include "header.h"
#include <stdio.h>
int main(void) {
    printf("%d, %d", a, reta());
}

Разве поведение MSVC в данном случае не является стандартным (или находится в области UB)? Насколько я понимаю, предварительное определение становится обычным определением, если в одной единице перевода больше нет определений?

Eugene Sh. 09.04.2024 17:09

Итак, переменные могут существовать тремя способами: объявлением, определением и инициализацией?

ItzYurix 09.04.2024 17:10

@ItzYurix Переменная может быть определена или объявлена. Определение может дополнительно включать инициализатор.

dbush 09.04.2024 17:18

@EugeneSh.: Поведение GCC и MSVC соответствует стандарту C, который не определяет поведение при наличии нескольких внешних определений объекта, будь то регулярные или вытекающие из предварительных определений.

Eric Postpischil 09.04.2024 18:00

Другие вопросы по теме