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

Теги: Классы памяти, 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
Перечисляемый тип