Классы памяти
В си определено несколько классов памяти для переменных и функций. Они изменяют область видимости переменных и функций, определяют время жизни объекта и расположение в памяти.
Классы памяти переменных
По умолчанию, локальные переменные имеют класс auto. Такие переменные располагаются на стеке а их область видимости ограничена своим блоком. Запись
#include <conio.h> #include <stdio.h> void main() { int x = 10; { int x = 20; { int x = 30; printf("%d\n", x); } printf("%d\n", x); } printf("%d\n", x); getch(); }
идентична
#include <conio.h> #include <stdio.h> void main() { int auto x = 10; { int auto x = 20; { int auto x = 30; printf("%d\n", x); } printf("%d\n", x); } printf("%d\n", x); getch(); }
Очевидно, что глобальные переменные не могут быть объявлены как auto, потому что располагаются в data-сегменте.
Следующий класс памяти – register. Когда мы определяем регистровую переменную, то мы просим компилятор, чтобы переменная располагалась в регистре, а не в оперативной памяти. Компилятор может сделать переменную регистровой, если позволяют условия (регистры не заняты, и по мнению компилятора это не приведёт к увеличению издержек). Регистровые переменные определяются с помощью служебного слово register перед типом
register int x = 20; register int y = 30;
Так как регистровая переменная не имеет адреса, то к ней не применима операция взятия адреса, это вызовет ошибку во время компиляции. Аргументы функции также могут быть заданы как register. Внутри функции они будут вести себя также, как и регистровые переменные.
Следующий класс памяти – статический. Переменные, объявленные как static, хранятся в data или в bss сегменте. Отличительной чертой является то, что время их жизни совпадает с временем жизни приложения, как и у глобальных переменных. Но в отличие от глобальных переменных, область видимости ограничена только блоком, в котором они определены.
#include <conio.h> #include <stdio.h> unsigned long long factorial(unsigned char n) { static unsigned char prevArg = 0; static long long prevAns = 1; if (n == prevArg) { printf("return previous answer\n"); return prevAns; } else { unsigned i = 0; printf("count new answer\n"); prevAns = 1; for (i = 1; i <= n; i++) { prevAns *= i; } prevArg = n; return prevAns; } } void main() { printf("!%d == %llu\n", 10, factorial(10)); printf("!%d == %llu\n", 10, factorial(10)); printf("!%d == %llu\n", 11, factorial(11)); printf("!%d == %llu\n", 11, factorial(11)); getch(); }
В этом примере переменные prevArg и prevAns инициализируются единожды, и не уничтожаются после выхода из функции. Переменная prevArg используется для хранения предыдущего аргумента функции, а prevAns для хранения предыдущего результата. Если аргумента функции совпадает с предыдущим, то возвращается ранее вычисленное значение, иначе оно вычисляется по-новому.
Другой показательный пример – функция-генератор, которая при каждом вызове возвращает новое значение.
#include <conio.h> #include <stdio.h> int next() { static int counter = 0; counter++; return counter; } void main() { printf("%d\n", next()); printf("%d\n", next()); printf("%d\n", next()); printf("%d\n", next()); printf("%d\n", next()); _getch(); }
Если бы служебное слово static отсутствовало, то каждый раз при вызове функции локальная переменная counter снова создавалась, инициализировалась и уничтожалась после выхода из функции.
Статическая переменная может иметь только константную инициализацию. Например, она не может быть инициализирована вызовом функции.
... static double x = foo(3); //Ошибка ...
Переменная, объявленная как static, должна иметь только один экземпляр в данной области видимости и вне этой области видимости не видна. Глобальная переменная, объявленная как static, видна только в своём файле.
Напротив, переменная, объявленная как extern может быть использована в других файлах при условии, что она была определена.
Объявление и определение переменной.
В глобальном контексте переменная сначала требует объявления. Таким образом, компилятор будет знать её имя и тип. Определение переменной требует выделения под неё памяти и инициализации. Посмотрите следующий код. Он абсолютно легален и должен работать по стандарту
#include <conio.h> #include <stdio.h> int Global; //Объявили переменную int Global = 20; //Определили переменную void main() { printf("%d", Global); getch(); }
Теперь, что будет, если одновременно объявить переменную и инициализировать её. Это определение переменной, которое требует её объявления
int Global = 20;
Следующая программа не скомпилируется
#include <conio.h> #include <stdio.h> extern int Global; void main() { Global = 30; printf("%d", Global); getch(); }
Это связано с тем, что отсутствует определение переменной. Если определить переменную внутри main, то это будет уже другой экземпляр переменной, которая будет расположена на стеке. Вообще, при работе с одним файлом использование extern переменных не оправдано. Рассмотрим ситуацию, когда у нас имеются ещё два файла – заголовочный File1.h и File1.c. В заголовочном файле объявим extern переменную Global
#ifndef _FILE1_H_ #define _FILE1_H_ extern int Global; #endif
в файле исходного кода определим её
#include "File1.h" int Global = 100;
После подключения файла File1.h можно использовать эту переменную в файле main.c, при этом гарантировано, что существует только один экземпляр этой переменной для всех файлов проекта
#include <conio.h> #include <stdio.h> #include "File1.h" void main() { printf("%d\n", Global); getch(); }
Если теперь определим функцию, которая изменяет эту переменную, то все функции из всех файлов будут видеть эти изменения.
File1.h:
#ifndef _FILE1_H_ #define _FILE1_H_ #include <stdio.h> extern int Global; void changeAndPrint(); #endif
File1.c
#include "File1.h" int Global = 100; void changeAndPrint() { printf("from File1: Global = %d\n", Global); Global = 1234; printf("changed to %d\n", Global); }
main.c
#include <conio.h> #include <stdio.h> #include "File1.h" void main() { Global = 567; printf("From main: Global = %d\n", Global); changeAndPrint(); printf("From main: Global = %d\n", Global); getch(); }
Вывод
From main: Global = 567
from File1: Global = 567
changed to 1234
From main: Global = 1234
Класс памяти для функций
Функции по умолчанию определены как extern, это значит, что они видны всем, кто подключит данный файл. То есть, запись
void foo() { ... }
эквивалентна
extern void foo() { ... }
Другой класс - static, делает функцию видимой только внутри своего модуля.
Рассмотрим пример – у нас будет, как обычно 2 файла, File1.h и File1.c. В первом определим две функции, одну extern, а вторую static
#ifndef _FILE1_H_ #define _FILE1_H_ #include <stdio.h> void visible(); static void hidden(); #endif
Fil1.c
#include "File1.h" void visible() { printf("Everyone can use me!\n"); hidden(); } void hidden() { printf("No one can use me, except my friends from File1.c\n"); }
Заметьте: мы не сможем вызвать функцию hidden вне файла File1.c, но внутри файла эта функция доступна.
#include <conio.h> #include <stdio.h> #include "File1.h" void main() { visible(); //hidden(); её теперь не вызвать getch(); }
Для функций также, как и для переменных, различают объявление и определение. Объявление обычно прячут в заголовочный файл, также как и объявление переменных. Определения находятся в си файле.
