Как поделиться переменными между файлами в C++: гайд по extern

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваш код не собирается: тайна extern в C++

Вы когда-нибудь писали аккуратную программу на C++, разбивали её на несколько файлов, компилировали — и получали стену ошибок вроде multiple definition of 'myVariable' или undefined reference to 'myVariable'? Знакомо? Если да — вы не одиноки. Вопрос "How do I use extern to share variables between source files?" входит в топ самых популярных на Stack Overflow с десятками тысяч просмотров.

Проблема проста: когда проект разрастается, держать весь код в одном main.cpp невозможно. Но как сделать так, чтобы переменная, объявленная в config.cpp, была видна в game.cpp и render.cpp? Интуиция подсказывает: "Просто объяви её глобально!" — но компилятор думает иначе. На помощь приходит маленькое, но могучее ключевое слово extern. Давайте разберёмся, как оно работает на практике, чтобы вы больше никогда не путались в линковке.

Что такое extern и зачем он нужен?

В C++ каждое имя (переменная, функция, класс) должно быть объявлено (declared) перед использованием и определено (defined) ровно один раз. Определение — это то, что выделяет память. Объявление — это просто обещание компилятору: "Не волнуйся, определение есть где-то в другом файле, я покажу тебе тип и имя".

Ключевое слово extern — это именно такое обещание. Оно говорит компилятору: "Эта переменная определена в другом месте, не выделяй под неё память здесь — просто дай мне к ней доступ".

Без extern каждая попытка объявить глобальную переменную в двух .cpp-файлах приведёт к ошибке линковщика: два определения — два участка памяти — конфликт.

Правило трёх файлов: как организовать код

Стандартный паттерн для разделения переменных между файлами в C++ выглядит так:

  1. Файл определения (*.cpp) — содержит одно определение переменной.
  2. Заголовочный файл (*.h) — содержит extern-объявление переменной.
  3. Любой другой .cpp файл — подключает заголовок и использует переменную.

Пример 1: Делимся целым числом

Представьте, что мы пишем игру и хотим хранить глобальный счёт в отдельном файле. Вот как это сделать правильно.

Файл: globals.cpp — здесь живёт настоящая переменная:

// globals.cpp
#include "globals.h"

int gameScore = 0;  // Определение: память выделяется здесь

Файл: globals.h — объявление для всех остальных:

// globals.h
#ifndef GLOBALS_H
#define GLOBALS_H

extern int gameScore;  // Объявление: "ищи определение в другом .cpp"

#endif // GLOBALS_H

Файл: main.cpp — используем переменную:

// main.cpp
#include <iostream>
#include "globals.h"

void updateScore(int points) {
    gameScore += points;  // Работает! extern дал доступ
}

int main() {
    gameScore = 100;
    updateScore(50);
    std::cout << "Score: " << gameScore << std::endl; // Выведет 150
    return 0;
}

Файл: display.cpp — ещё один файл, которому нужен доступ:

// display.cpp
#include <iostream>
#include "globals.h"

void showScore() {
    std::cout << "Current score: " << gameScore << std::endl;
}

Обратите внимание: gameScore определён ровно один раз — в globals.cpp. Во всех остальных местах мы используем extern int gameScore (через заголовок).

Частая ошибка №1: определение в заголовочном файле

Новички часто пишут так в .h файле:

// BAD: bad_globals.h
int gameScore = 0;  // Это определение!

Если этот заголовок подключить в main.cpp и display.cpp, линковщик увидит два определения gameScore — и выдаст ошибку multiple definition. Запомните: в заголовках — только extern объявления, никаких определений!

extern с массивами и структурами

Тот же принцип работает для массивов, структур и классов. Главное — не забыть указать полный тип.

Пример 2: Массив и структура

Файл: config.h

// config.h
#ifndef CONFIG_H
#define CONFIG_H

struct GameConfig {
    int screenWidth;
    int screenHeight;
    bool fullscreen;
};

extern GameConfig config;        // объявление структуры
extern const char* levelNames[]; // объявление массива (размер можно не указывать)

#endif // CONFIG_H

Файл: config.cpp

// config.cpp
#include "config.h"

GameConfig config = {1920, 1080, true};          // определение
const char* levelNames[] = {"Forest", "Cave", "Castle"}; // определение

Файл: game.cpp

// game.cpp
#include "config.h"
#include <iostream>

void loadLevel(int index) {
    std::cout << "Loading: " << levelNames[index] << std::endl;
    if (config.fullscreen) {
        // переключить полноэкранный режим
    }
}

extern "C": мост между C++ и C

Если вы работаете с библиотеками на чистом C или пишете код, который должны вызывать из C, используйте extern "C". Это отключает name mangling (искажение имён), характерное для C++.

// c_interface.h
#ifdef __cplusplus
extern "C" {
#endif

extern int globalCounter;  // переменная, доступная из C и C++

void resetCounter();       // функция, доступная из C

#ifdef __cplusplus
}
#endif

Теперь эту переменную и функцию можно использовать как в C, так и в C++ коде.

Когда НЕ стоит использовать extern

Глобальные переменные — это зло? Не всегда, но злоупотреблять ими не стоит. Вот несколько ситуаций, когда extern лучше заменить на более безопасные механизмы:

  • Конфигурация приложения — используйте синглтон или передавайте параметры через конструкторы.
  • Счётчики и флаги — рассмотрите std::atomic для потокобезопасности

Блоги

Рекомендаций по книгам