Классы памяти

Теги: Классы памяти, auto, register, extern, static, объявление переменной, определение переменной, константный инициализатор.



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

Классы памяти переменных

По умолчанию, локальные переменные имеют класс 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();
}

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

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Перечисляемый тип