Menu

Область видимости в C: блочная, функциональная и файловая

Где переменная в C видна и сколько живёт: блочная область видимости, параметры функций, глобальные переменные файловой области, статические локальные переменные, переживающие вызовы, статические функции, затенение имён и чем опасны глобальные переменные.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Два вопроса о каждой переменной

У любой переменной в C есть два независимых свойства, и путаница между ними — источник большинства сюрпризов на этой странице:

  • Область видимостигде имя можно использовать. Вопрос времени компиляции.
  • Время жизникак долго существует хранилище. Вопрос времени выполнения.

Обычно они идут вместе (рядовая локальная переменная видна в своём блоке и живёт ровно столько же), но static их разделяет — поэтому именно это ключевое слово здесь стоит понять.

Блочная область видимости

Переменная, объявленная внутри фигурных скобок, видна от своего объявления до закрывающей скобки — и больше нигде.

Внутренний блок видит outer, потому что вложен в него. Внешний блок не видит inner. Это относится к любой паре фигурных скобок — телу функции, телу if, телу цикла или голому блоку вроде приведённого выше.

Циклы делают это наглядным. Счётчик, объявленный в заголовке for, принадлежит циклу:

for (int i = 0; i < 5; i++) {
    /* i живёт здесь */
}
/* здесь i не существует */

Это ровно то, что нужно: два цикла в одной функции могут использовать i без взаимных помех, а счётчик нельзя случайно прочитать после цикла. Если значение нужно потом — индекс, на котором остановился поиск, — объявите его до цикла.

Параметры функции имеют область видимости тела функции, так что ведут себя как локальные переменные, объявленные в его начале.

C99 разрешает объявление в любом месте блока, а не только в начале. Объявляйте переменные там, где они впервые нужны: переменная с короткой областью видимости — это переменная с меньшим числом способов ошибиться.

Время жизни: автоматическая память

У обычной локальной переменной автоматическая память: она возникает, когда управление входит в её блок, и уничтожается, когда выходит. Её память живёт на стеке.

Выведется 1 три раза. Каждый вызов получает совершенно новую count, инициализированную нулём и отброшенную при возврате. По той же причине у каждого кадра рекурсивной функции своя копия каждой локальной переменной.

Два следствия стоит проговорить прямо. Неинициализированная локальная переменная содержит мусор, а не ноль — те байты, что оказались на стеке. И возврат указателя на локальную переменную — неопределённое поведение, потому что память исчезает в момент возврата из функции:

/* БАГ: buf не существует после возврата */
char *broken(void) {
    char buf[64] = "hello";
    return buf;
}

Локальные static: память между вызовами

Поставьте static на локальную переменную — и её время жизни станет равным всей программе, а область видимости останется прежней.

Теперь счётчик печатает 1, 2, 3. = 0 выполняется один раз, до старта main, а не при каждом вызове.

Три правила для локальных static:

  • Инициализатор должен быть константным выражением, потому что применяется при старте программы, а не во время выполнения.
  • Без инициализатора static обнуляется (в отличие от автоматической локальной переменной, где лежит мусор).
  • Переменная всё равно приватна для функции. Никакой другой код не может её назвать.

Последний пункт и есть вся привлекательность: функция может помнить что-то между вызовами, не выставляя наружу глобальную переменную, которую мог бы изменить кто угодно. Используйте это для счётчиков вызовов, кешированных таблиц, строящихся при первом обращении, и флагов однократной инициализации.

Цена в том, что функция перестаёт быть чистым отображением входа в выход — одни и те же аргументы могут давать разные ответы, — из-за чего её труднее тестировать и небезопасно вызывать одновременно из нескольких потоков без защиты. Применяйте локальные static осознанно, а не ради удобства.

Файловая область видимости: глобальные переменные

Переменная, объявленная вне всех функций, имеет файловую область видимости. Она видна от своего объявления до конца файла, во всех функциях ниже, а её время жизни — вся программа.

Глобальные переменные по умолчанию обнуляются, поэтому totalOperations начинается с 0 даже без = 0.

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

/* в stats.c */
int totalOperations = 0;          /* определение - ровно одно на программу */

/* в main.c */
extern int totalOperations;       /* объявление: "она где-то существует" */

Поместите объявление extern в заголовочный файл, чтобы все пользователи видели одно и то же. Обратите внимание на асимметрию: объявление extern говорит, что переменная существует, и ровно один файл .c обязан её действительно определить.

Чем кусаются глобальные переменные

Глобальные переменные — самый простой способ поделиться данными и самый простой способ породить баги, которые нельзя локализовать. Четыре конкретные проблемы:

Их может изменить что угодно. Когда в totalOperations оказывается неверное значение, виновата может быть любая строка любого файла. С параметром подозреваемые — только видимые вам места вызова.

Функции становятся нетестируемыми. Функцию, читающую глобальную переменную, нельзя вызвать изолированно: сначала нужно подготовить глобальное состояние, а потом не забыть его сбросить.

Имя занимает всю программу. Глобальная переменная с именем count или buffer рано или поздно столкнётся с чьей-то ещё.

Их инициализаторы обязаны быть константами времени компиляции. В C глобальную переменную можно инициализировать только константным выражением: int limit = readConfig(); не скомпилируется, и int b = a * 2; на файловом уровне тоже. Всё вычисляемое приходится присваивать во время выполнения какой-нибудь функцией настройки, и любой код, который трогает глобальную переменную до этого вызова, молча видит 0.

Альтернативы почти всегда есть: передавать значения параметрами, возвращать результаты и собирать связанное состояние в структуру, которую передают явно. Настоящие исключения бывают — общая конфигурация программы, дескриптор логирования, — но и им лучше быть static на файловом уровне с функциями доступа, о чём следующий раздел.

static на файловом уровне: приватность

На глобальной переменной или функции static означает совсем не то, что на локальной: внутреннее связывание. Имя становится приватным для своего файла .c, и до него нельзя дотянуться ниоткуда ещё.

/* counter.c */
#include "counter.h"

static int count = 0;           /* приватно для этого файла - никто больше не тронет */

static void validate(void) {    /* приватный помощник, не часть интерфейса */
    if (count < 0) count = 0;
}

void increment(void) {          /* публичная: объявлена в counter.h */
    count++;
    validate();
}

int getCount(void) {            /* публичная */
    return count;
}

Другой файл может вызвать increment и getCount, но не видит count и не может вызвать validate — компоновщик не разрешит эти имена. Такова система модулей в C, какая уж есть: заголовок, объявляющий публичные функции, и static на всём остальном.

Два практических плюса помимо аккуратности. В двух файлах может быть по своей static void validate(void) без конфликта, чего не вышло бы, будь функции публичными. И компилятор знает, что у static-функции нет вызывающих вне файла, поэтому может агрессивнее встраивать её или предупредить, что она не используется.

Итак, у ключевого слова два разных смысла, определяемых тем, где оно стоит:

static на ЛОКАЛЬНОЙ переменной  ->  время жизни становится постоянным (область та же)
static на ФАЙЛОВОМ уровне       ->  связывание становится внутренним  (время жизни то же)

Затенение имён

Объявление во внутренней области видимости имени, уже существующего во внешней, затеняет внешнее имя: внутри внутренней области имя обозначает новую переменную, а до внешней не дотянуться.

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

gcc -Wshadow сообщает о каждом таком случае. В -Wall он не входит, поэтому включайте его явно:

gcc -Wall -Wextra -Wshadow program.c -o program

Привычка, полностью снимающая проблему, — называть переменные по роли, а не по типу: totalScore и itemScore не смогут затенить друг друга, а две переменные с именем n рано или поздно смогут.

Краткая справка

объявлена внутри блока          блочная область, автоматическое время жизни, мусор без инициализации
параметр функции                блочная область тела функции, копия аргумента
static внутри функции           блочная область, время жизни ПРОГРАММЫ, обнуляется, хранится между вызовами
объявлена вне всех функций      файловая область, время жизни программы, обнуляется, видна другим файлам
static вне всех функций         файловая область, время жизни программы, ПРИВАТНА для этого файла .c
объявление extern               называет переменную, определённую в другом файле

Часто задаваемые вопросы

Что такое область видимости в C?

Это участок кода, где имя видно. Переменная, объявленная внутри блока (пары фигурных скобок), видна только в этом блоке — это блочная область видимости. Объявленная вне всех функций имеет файловую область видимости и видна от своего объявления до конца файла.

Что делает static с локальной переменной в C?

Меняет её время жизни, не меняя область видимости. Локальная переменная с static создаётся и инициализируется один раз и сохраняет значение между вызовами, но по-прежнему видна только внутри своей функции. Это способ дать функции память, не заводя глобальной переменной.

Чем static отличается от глобальной переменной в C?

Обычная глобальная переменная видна каждому файлу программы, и другие файлы добираются до неё через extern. Переменная или функция с static на файловом уровне приватна для этого одного файла .c — другие файлы не смогут с ней скомпоноваться. static на файловом уровне — про сокрытие; static на локальной переменной — про время жизни.

Почему глобальные переменные в C — это плохо?

Их может изменить любая функция, поэтому причина бага может быть где угодно в программе; функции, которые их читают, нельзя тестировать изолированно; а имя занимает всю программу, провоцируя конфликты. Передавайте значения параметрами и возвращайте результаты, а там, где общее состояние действительно нужно, делайте его static на файловом уровне, чтобы трогать его мог только один файл.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ