838.44K
Category: programmingprogramming

Области видимости переменных

1.

СПбГУТ им. проф. М.А. Бонч-Бруевича
Кафедра программной инженерии и вычислительной техники (ПИ и ВТ)
ПРОГРАММИРОВАНИЕ
Единственный способ изучать новый язык
программирования – писать на нем программы.
Брайэн Керниган
Лекция 9-02: Области видимости переменных
1.
2.
3.
4.
5.
Память компьютера
Область действия переменных
Классы памяти
Объявление и определение переменной
Класс памяти для функций
Санкт–Петербург, 2023г.

2.

Введение
Память – способность объекта обеспечивать хранение
данных.
Все объекты, над которыми выполняются команды, как и
сами команды, хранятся в памяти компьютера.
Память состоит из ячеек, в каждой из которых содержится 1
бит информации, принимающий одно из двух значений: 0 или
1.
Биты обрабатывают группами фиксированного размера.
Для этого группы бит могут записываться и считываться за
одну базовую операцию.
Группа из 8 бит называется байтом.
Байты последовательно располагаются в памяти компьютера.
1 килобайт (Кбайт) = 210 = 1 024 байт
1 мегабайт (Мбайт) = 210 Кбайт = 220 байт = 1 048 576 байт
1 гигабайт (Гбайт) = 210 Мбайт = 230 байт = 1 073 741 824 байт
Для доступа к памяти с целью записи или чтения отдельных
элементов информации используются идентификаторы,
определяющие их расположение в памяти.
Каждому идентификатору в соответствие ставится адрес.
В качестве адресов используются числа из диапазона от 0 до
2k–1 со значением k, достаточным для адресации всей памяти
компьютера.
Все 2k адресов составляют адресное пространство
компьютера.
Способы адресации байтов
Существует прямой и обратный способы адресации байтов.
При обратном способе адресации байты адресуются слева
направо, так что самый старший (левый) байт слова имеет
наименьший адрес.
Прямым способом называется противоположная система
адресации.
Компиляторы языков высокого уровня поддерживают
прямой способ адресации.
Объект в ОП занимает целое слово.
Поэтому для того, чтобы обратиться к нему в памяти, нужно
указать адрес, по которому этот объект хранится.
К началу 2000–х годов стало очевидно, что 32–битное
адресное пространство процессорных архитектур ограничивает
производительность приложений, работающих с большими
объёмами данных.
32–разрядное адресное пространство позволяет процессору
осуществлять непосредственную адресацию лишь 4 ГБайт
данных.
2

3.

1. Память компьютера
Принцип однородности памяти
Команды программ и данные хранятся в одной и той же
памяти. Команды и данные отличаются только по способу
использования. Это утверждение называют принципом
однородности памяти.
Принцип адресности памяти
Команды и данные размещаются в единой памяти,
состоящей из ячеек, имеющих свои номера (адреса). Это
принцип адресности памяти.
Принцип иерархичности памяти
Можно выделить два основных требования, предъявляемых
к памяти компьютера:
объём памяти должен быть как можно больше
время доступа к памяти должно быть как можно меньше
В современных компьютерах используются устройства памяти
нескольких уровней, различающиеся по своим основным
характеристикам:
времени доступа,
сложности,
объёму и
стоимости.
Трудности физической реализации запоминающего
устройства высокого быстродействия и большого объёма
требуют иерархической организации памяти.
Уровни иерархии взаимосвязаны: все данные на одном
уровне могут быть также найдены на более низком уровне.
3

4.

Память компьютера
Адрес переменной (на примере 32-разрядной архитектуры)
Память:
Адрес:
0x2c4b1
0x2c4b2
0x2c4b3
0x2c4b4
0x2c4b5
0x2c4b7
a
y
x
0x2c4b6
Память:
Адрес:
0x2c4b1
0x2c4b2
short int x; //или
short x;
x
Память:
10
Адрес:
0x2c4b1
0x2c4b2
0x2c4b3
0x2c4b4
0x2c4b5
0x2c4b6
char y;
int a;
y
a
127
20031
0x2c4b3
0x2c4b4
0x2c4b5
0x2c4b7
Оперативная память
организована как
последовательность ячеек (байт)
Каждая ячейка имеет
собственный адрес
(порядковый номер)
Адрес – целое число, чаще
записываемое в
шестнадцатеричной системе
счисления
Каждая переменная
размещается в
последовательных ячейках
(количество ячеек зависит от
типа переменной)
Адрес переменной – адрес
первой из этих ячеек
Адрес переменной можно
получить с помощью унарной
операции &
Например, &x даст адрес x:
printf(“x=%d, &x=%p”, x, &x);
0x2c4b6
0x2c4b7
2

5.

Память компьютера
32–разрядное адресное пространство позволяет процессору
осуществлять непосредственную адресацию лишь 4 ГБайт
данных.
Этого может оказаться недостаточным для некоторых
приложений, связанных, например, с обработкой видео или
обслуживанием баз данных.
64–разрядное адресное пространство способно хранить в
себе одно из 264 = 18 446 744 073 709 551 616 значений.
Процессор с 64–битной адресацией памяти может напрямую
обращаться к 16 ЭБайт памяти.
Теоретически 64–разрядные процессы используют 64–битные
указатели, так что их максимальное виртуальное адресное
пространство (ВАП) равно 264 = 16 экзабайтам.
Однако в текущих реализациях архитектуры x64 размер
виртуальных адресов ограничен 48 битами.
ОС Windows не делит ВАП равномерно между активными
процессами и системой, а вместо этого определяет область в
адресном пространстве для процессов и других системных
ресурсов памяти, таких как системные записи таблицы
страниц (PTE), файловые кэши, резидентный и нерезидентный
(paged и non–paged) пулы.
Достоинства:
Главное достоинство 64–разрядной архитектуры является
предоставление процессу возможности адресации до 8 Тб ВАП.
Приложение может воспользоваться новыми регистрами
процессора.
Работа с 64–разрядными числами напрямую и поддержка
обработки
128–разрядных чисел по частям.
64–разрядное
адресное
пространство
Сегментация
приложения
на Си
Динамические
данные
Локальные
данные
Код программы
Недостатки:
o Главным недостатком 64–разрядной архитектуры является
то, что одни и те же данные (и код) будут занимать больше
места в памяти и на диске (из–за увеличения размера
указателей, других аналогичных типов и выравнивания).
o Это увеличивает размер необходимой процессу памяти,
снижая эффективность кэш–памяти.
5

6.

Память компьютера
Оперативная память (ОП/ОЗУ) является основным типом
памяти, используемым компьютером для исполнения программ.
Если рассматривать ОП с точки зрения программиста на
Си, то можно выделить следующие аспекты:
Работа с стеком и кучей. В программировании на Си
используются две области памяти: стек и куча. Стек
используется для хранения локальных переменных функций,
а куча – для хранения выделенной динамической памяти.
Работа с указателями. В Си указатели часто используются
для доступа к областям памяти. Указатели могут быть
использованы для передачи больших объемов данных или
для работы с динамически выделяемой памятью.
Управление памятью. В Си программисту необходимо
самостоятельно управлять выделенной динамической
памятью. Нужно контролировать количество выделенной
памяти, освобождать ее после использования и избегать
утечек памяти.
Выделение памяти. В языке Си для выделения
динамической памяти используется оператор "malloc". Он
позволяет выделить блок памяти заданного размера и
вернуть указатель на начало этого блока. После
использования выделенной памяти ее необходимо
освободить при помощи оператора "free".
Работа с массивами. Массивы в Си хранятся в ОП. Для
создания массива в динамической памяти (куче) нужно
выделить память при помощи функции "malloc" и
заполнить ее элементами массива. После использования
массива необходимо освободить выделенную память.
В целом, ОП является критически важным ресурсом в
программировании на Си.
Необходимо тщательно контролировать выделение и
освобождение памяти, чтобы избегать утечек памяти и других
проблем, связанных с управлением памятью.
В Си определено несколько классов памяти для переменных
и функций.
Они изменяют область видимости переменных и функций,
определяют время жизни объекта и расположение в памяти.
Классы памяти языка Си дают возможность определить, с
какими функциями связаны какие переменные, и как долго
переменная сохраняется в программе.
Мы уже упоминали, что локальные переменные известны
только функциям, содержащим их.
В языке Си предполагается также, что о глобальных
переменных знают сразу несколько функций.
NB: В языке Си мы можем записать по указанному адресу в
ОП нужное значение (без использования переменных!!!):
*((int*)0x8000)=1;
/* Представили адрес как указатель и
записали значение по этому адресу. В 4 байта, начиная с
адреса 0x8000, будет записано значение 1
*/
6

7.

2. Область действия переменных
Область действия переменных (объектов)
Внутри
блока
Прототип
функции
Блок. Идентификаторы, описанные внутри блока,
являются
локальными.
Область
действия
идентификатора начинается в точке определения и
заканчивается в конце блока, видимость – в пределах
блока и внутренних блоков, время жизни – до выхода
из блока. После выхода из блока память
освобождается.
Прототип функции. Идентификаторы, указанные в
списке параметров прототипа (декларации) функции,
имеют областью действия только прототип функции.
Функция.
Единственными
идентификаторами,
имеющими такую область действия, являются метки
операторов. В одной функции все метки должны
различаться, но могут совпадать с метками других
функций.
Внутри
функции
(блока)
Структурированный
тип данных
Файл.с
Структурированный тип данных. Элементы структур и
объединений являются видимыми лишь в их пределах. Они
образуются при создании переменной указанного типа и
разрушаются при ее уничтожении.
Файл. Идентификаторы, описанные вне любого блока,
функции (в С++ класса или пространства имен), имеют
глобальную видимость и постоянное время жизни и могут
использоваться с момента их определения.
Область действия объекта – это часть кода программы, в которой его
можно использовать для доступа к связанному с ним участку памяти.
В зависимости от области действия переменная может быть локальной
(внутренней) или глобальной (внешней).
Область действия локальных данных – от точки декларации до конца
функции (блока), в которой произведена их декларация, включая все
вложенные блоки.
Областью действия глобальных данных считается файл, в котором
они определены, от точки описания до его окончания.
7

8.

Область действия переменных
Имеется три основных участка программы,
где можно декларировать переменные
внутри
функции
(блока)
в заголовке
функции при
определении
параметров
Все объекты программы на Си перед их использованием
должны быть декларированы.
Операционные объекты (в частности переменные) при этом,
кроме атрибута «тип», имеют необязательный атрибут
«класс памяти», существенно влияющий на область и время
их действия.
Класс памяти программного объекта определяет время ее
существования (время жизни) и область видимости (действия)
и может принимать одно из значений: auto, extern, static и
register.
Если класс памяти у переменной не указан явным образом,
он определяется компилятором исходя из контекста ее
декларации.
Время жизни может быть постоянным – в течение
выполнения программы, и временным – в течение
выполнения функции (блока) программы.
Класс памяти и область действия объектов по умолчанию
зависят от места их размещения в коде программы
Эти переменные соответственно называются локальными
(внутренними) переменными, формальными параметрами
и глобальными (внешними) переменными.
вне
функции
Область действия объекта – это часть кода программы, в
которой его можно использовать для доступа к связанному с
ним участку памяти.
В зависимости от области действия переменная может быть
локальной (внутренней) или глобальной (внешней).
Область действия локальных данных – от точки
декларации до конца функции (блока), в которой
произведена их декларация, включая все вложенные
блоки.
Областью действия глобальных данных считается файл, в
котором они определены, от точки описания до его
окончания.
8

9.

Область действия имен
Имена
Локальное
(внутреннее)
Глобальные
(внешние)
описанное
внутри
функции
имя функций или другое
имя, описанное вне
функций
доступно только в
этой функции
доступно в части программы
от точки описания до конца
файла
Область действия имени – часть программы,
в которой это имя может быть использовано
9

10.

Время жизни (существования) имени переменной или константы
Время жизни – время, в течение которого под данные распределена память
Данные
Статические
Существуют все время
выполнения
программы
Автоматические
Динамические
Существуют во время
выполнения
функции, в которой
описаны
Получают место в памяти
с помощью операторов
динамического
распределения памяти
10

11.

Время жизни (существования) имени переменной или константы
Свойство
Автоматический
Статический
Динамический
Объявление
Имеет внутреннее или
Объект без связывания внешнее связывание или
и без static
объявлен с
квалификатором static
Выделен с помощью
malloc
Время существования
Блок, в котором
объявлен объект
Всё время выполнения
программы
От вызова malloc до
вызова free
Инициализация
Отсутствует в случае
отсутствия явной
инициализации
Происходит один раз до
запуска программы
Частично в случае
calloc
Размер
Фиксированный,
неизменяемый
Фиксированный,
неизменяемый
Любой, изменяемый
Типичное размещение
Стек или регистры
процессора
Отдельный сегмент
памяти
Куча
Модель памяти в языке Си — система хранения объектов в языке Си.
Способ хранения объекта в языке Си определяет его время жизни — часть времени выполнения программы, во
время которого объект существует или для него зарезервировано место.
Объект имеет постоянный адрес и сохраняет своё последнее значение.
Запрещается обращаться к объекту, который перестал существовать, при этом, если при работе с объектом
использовался указатель, его значение остаётся неопределённым.
11

12.

Класс памяти переменной определяет область видимости и
время жизни переменной
В языке Си переменные могут иметь один из
следующих классов памяти:
Область
auto – автоматический
действия
static – статический
существования
переменной
extern – внешний
определяется
register – регистровый
областью
внешний статический
видимости
Область
видимости
2.1 Автоматический класс памяти (auto)
задается необязательным ключевым словом auto при
описании переменной перед указанием типа:
auto float f=0;
время существования переменной определяется
областью видимости:
переменная создается в начале блока {…}
переменная удаляется в конце блока {…}
при инициализации переменных допускается употребление
выражений, включающих переменные и вызовы функций:
auto float a=10, b=2*a*sqrt(a);
По умолчанию, локальные переменные имеют класс auto.
Такие переменные располагаются на стеке а их область
видимости ограничена своим блоком.
Очевидно, что глобальные переменные не могут быть
объявлены как auto, потому что располагаются в data–
сегменте.
i
void main()
{
auto int i;
// Создание i в памяти
for (i=0; i<5; ++i) //i=0 – Инициализация i
{
int k=100;
if(i%2)
// Использование i
{
int d = 3;
void main()
k–=d;
{
}
auto int i;
k++;
for (i=0; i<5; ++i)
}
{
} // Удаление i
int k=100;
if(i%2)
{ Инициализация k
Создание k в памяти
int d = 3;
k–=d;
}
k++;
Использование k
}
Удаление k
}
Область видимости k
3. Классы памяти
12

13.

Классы памяти
2.2 Статический класс памяти (static)
задается ключевым словом static при описании переменной
перед указанием типа
static int a=0;
область видимости переменной – блок {…}, в котором она
определена
время существования переменной – сеанс работы
программы:
переменная создается при старте программы
переменная удаляется при завершении программы
инициализация переменных осуществляется только при
первом входе в блок:
static int a=0; /* инициализация однократна,
*/
/* между входами в блок
*/
/* значение переменной сохраняется */
параметры функций не могут быть статическими
в зависимости от расположения оператора описания
статические переменные могут быть глобальными и
локальными.
время жизни – постоянное.
инициализируется один раз при первом выполнении
оператора, содержащего определение переменной.
глобальные объекты всегда являются статическими.
переменная, описанная вне
a
функции, является внешней
создается и инициализируется
(глобальной) переменной.
при каждом вызове функции
атрибут static, использованный при описании глобального
объекта, предписывает ограничение его области
применения только в пределах остатка текущего файла, а
значения локальных статических объектов сохраняются до
повторного вызова функции
т.е. в языке Си ключевое слово static имеет разный смысл
для локальных и глобальных объектов.
#include <stdio.h>
void stat(); /* прототип функции */
s создается при
void main()
старте программы
{
int i;
s
for (i=0;i<5;++i) stat();
инициализируется
}
s удаляется при
только при первом
завершении программы
void stat()
{
int a = 0;
static int s = 0;
вызове, сохраняя
значение между
вызовами
/* автоматическая переменная */
/* статическая переменная */
printf(“auto = %d, static = %d\n“, a, s);
++a; ++s;
}
13

14.

Статический класс памяти (static)
Сегментация
приложения
на Си
Динамические
данные
Локальные
данные
Код программы
Статические данные
Глобальные
данные
Статические локальные
данные
Объявлены вне
функций
Данные, объявленные
внутри функций как static
Статические данные распределены в
специальном статическом сегменте памяти
программы
Доступны только функции,
в которой описаны, но существуют
(занимают память) во время
выполнения всей программы
BSS–сегмент (block started by symbol) содержит неинициализированные глобальные переменные, или
статические переменные без явной инициализации.
Этот сегмент начинается непосредственно за data–сегментом.
Обычно загрузчик программ инициализирует bss область при загрузке приложения нулями.
Дело в том, что в data области переменные инициализированы – то есть затирают своими значениями
выделенную область памяти.
Так как переменные в bss области не инициализированы явно, то они теоретически могли бы иметь
значение, которое ранее хранилось в этой области, а это уязвимость, которая предоставляет доступ
до приватных (возможно) данных.
Поэтому загрузчик вынужден обнулять все значения.
За счёт этого и неинициализированные глобальные переменные, и статические переменные по
умолчанию равны нулю.
14

15.

2.3 Регистровый класс памяти (register)
Классы памяти
2.4 Внешний класс памяти (extern)
задается ключевым словом register при описании
переменной перед указанием типа:
register int k=0;
по возможности регистровые переменные размещаются в
регистрах процессора, а не в ОП
регистровые переменные не имеют адреса,
т.е. к ним не применим оператор &
в остальном аналогичны автоматическим переменным
чаще всего регистровый класс памяти используется для
переменных–счетчиков цикла
void main()
{
register int i; /* регистровая переменная */
for (i=0; i<15; ++i) printf(“%d\n”, i);
}
регистровая память (атрибут register) – объекты целого типа
и символы рекомендуется размещать не в ОП, а в регистрах
общего назначения (процессора), а при нехватке регистров – в
стековой памяти (размер объекта не должен превышать
разрядности регистра)
для других типов компилятор может использовать другие
способы размещения или просто проигнорировать данную
рекомендацию
регистровая память позволяет увеличить быстродействие
программы
внешние переменные – переменные, определенные
вне функций
область действия внешних переменных – вся программа,
т.е. внешние переменные глобальны
определение внешней
#include <stdio.h>
переменной должно быть
int Count=0; // определение
единственным на программу
(только в одном файле)
int add(int a, int b)
int global=100;
{
при определении внешней
Count++;
переменной не требуется
указывать специальных
return a+b;
ключевых слов!
}
для использования внешних
int sub(int a, int b)
переменных, определенных
за пределами данного файла,
{
нужно поместить объявление
Count++;
внешней переменной
return a–b;
extern int global;
}
first.c
void main()
{
int x=1, y=10, z=0;
z = add(x,y) + sub(y,x);
printf(“%d (%d)”, z, Count);
}
15

16.

Классы памяти
Внешний класс памяти (extern)
определение внешней переменной – в одном файле
в остальных – объявление переменной
#include <stdio.h>
int Count=0; // определение
int add(int a, int b)
{
first.c
Count++;
return a+b;
}
int sub(int a, int b)
{
Count++;
return a–b;
}
void main()
{
int x=1, y=10, z=0;
z = add(x,y) + sub(y,x);
printf(“%d (%d)”, z, Count);
}
extern int Count; // Объявление
int mul(int a, int b)
{
second.c
Count++;
return a+b;
}
инициализировать внешние переменные
можно только в определении (не в
объявлении):
int global=1024; /* корректно */
extern int global=0; /* некорректно */
инициализировать внешние переменные
можно только константными
выражениями без вызовов функций:
int global=(8*1024)/2; // корректно
float wrong=2*sqrt(global); // некорректно
extern int Count; // Объявление
int div(int a, int b)
third.c
{
Count++;
return a+b;
}
16

17.

2.5 Внешний статический класс памяти
Классы памяти
Квалификаторы const и volatile
Внешние переменные могут быть объявлены как статические
Область видимости внешней статической переменной –
файл, в котором она определена (а не вся программа)
#include <stdio.h>
static int Count=0; /* Внешняя
статическая переменная */
int add(int a, int b)
{
Count++; /* Доступна в том же
файле ниже определения */
return a+b;
first.c
}
void main()
{
int x=1, y=10, z=0;
z = add(x,y);
printf(“%d (%d)”, z, Count);
}
Определение любой переменной может предваряться
квалификаторами const или volatile ("нестабильный",
"изменчивый" – без оптимизации компилятором)
Квалификатор const запрещает любые изменения значения
переменной после ее инициализации
const double PI = 3.141592;
const int N = 1000;
volatile в языке Си – это квалификатор переменной,
говорящий компилятору, что значение переменной
может быть изменено в любой момент и что часть
Недоступна в других
кода, которая производит над этой переменной какиефайлах
то действия (чтение или запись), не должна быть
оптимизирована.
Самая частая ошибка, связанная с volatile – это
просто неиспользование этого квалификатора
extern int Count; /* Ошибка! */
там, где это нужно.
Работая с микроконтроллерами, программист
int mul(int a, int b)
почти всегда сам является автором кода
{
основной программы и кода для обработки
Count++;
прерываний.
second.c
Причем основная программа и прерывания
return a+b;
должны обмениваться данными, а самым
}
распространенным способом для этого
является использование глобальных
переменных (будь то счетчики, привязанные к
периоду переполнения таймера, или буферы
для хранения входных/выходных данных, или
переменные состояния, или еще что-то)…
17

18.

Описание переменных: общая схема
Квалификаторы и модификаторы:
Класс
памяти
Изменяемость
Знаковость
Длина
Тип
auto
const
signed
short
int
static
volatile
unsigned
long
char
long long
float
register
extern
double
Примеры:
static volatile unsigned long long int TimeTicks;
register const unsigned long double MyRealVar;
auto signed short int x_coord;
18

19.

4. Объявление и определение переменной
В глобальном контексте переменная сначала требует
объявления.
Таким образом, компилятор будет знать её имя и тип.
Определение переменной требует выделения под неё памяти
и инициализации.
Посмотрите следующий код. Он абсолютно легален и должен
работать по стандарту:
#include <conio.h>
#include <stdio.h>
int Global; //Объявили переменную
int Global = 20; //Определили переменную
void main()
{
printf("%d", Global);
getch();
}
Теперь, что будет, если одновременно объявить глобальную
переменную и инициализировать её.
Это определение переменной, которое требует её
объявления:
int Global = 20;
В этом случае программа не скомпилируется
Это связано с тем, что отсутствует определение
переменной.
Если определить переменную внутри main, то это будет уже
другой экземпляр переменной, которая будет расположена
на стеке.
Вообще, при работе с одним файлом использование extern
переменных не оправдано.
Рассмотрим ситуацию, когда у нас имеются ещё два файла –
заголовочный File1.h и File1.c.
В заголовочном файле объявим extern переменную Global.
File1.h
File1.c
#ifndef _FILE1_H_
/* в файле исходного кода
#define _FILE1_H_
определим её: */
#include "File1.h"
extern int Global;
int Global = 100;
#endif
После подключения файла File1.h можно использовать эту
переменную в файле main.c, при этом гарантировано, что
существует только один экземпляр этой переменной для всех
файлов проекта:
#include <conio.h>
Если теперь определим функцию,
#include <stdio.h>
которая изменяет эту переменную, то
#include "File1.h"
все функции из всех файлов будут
void main()
видеть эти изменения:
{
// File1.h:
printf("%d\n", Global);
#ifndef _FILE1_H_
getch();
#define _FILE1_H_
}
#include <stdio.h>
extern int Global;
#include "File1.h"
// File1.c:
void changeAndPrint();
int Global = 100;
#endif
void changeAndPrint() {
printf("from File1: Global = %d\n", Global);
Global = 1234;
printf("changed to %d\n", Global);
}
19

20.

Объявление и определение переменной
Если теперь определим функцию, которая изменяет эту
переменную, то все функции из всех файлов будут видеть эти
изменения:
// File1.h
#ifndef _FILE1_H_
#define _FILE1_H_
#include <stdio.h>
extern int Global;
void changeAndPrint();
#endif
#include "File1.h"
// File1.c
int Global = 100;
void changeAndPrint() {
printf("from File1: Global = %d\n", Global);
Global = 1234;
printf("changed to %d\n", Global);
}
#include <stdio.h>
// main.c
#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
20

21.

5. Класс памяти для функций
Функции по умолчанию определены как extern, это значит, что
они видны всем, кто подключит данный файл.
То есть, запись
void foo()
extern void foo()
{
{
эквивалентна
...
...
}
}
Для функций также, как и для переменных, различают
объявление и определение.
Объявление обычно прячут в заголовочный файл, также как и
объявление переменных.
Определения находятся в Си–файле.
Другой класс ─ static, делает функцию видимой только
внутри своего модуля.
Рассмотрим пример – у нас будет, как обычно 2 файла.
В первом определим две функции, одну extern, а вторую static
//
File1.h
#ifndef _FILE1_H_
#define _FILE1_H_
#include <stdio.h>
void visible();
static void hidden();
#endif
Заметьте:
мы не сможем
вызвать функцию
hidden вне файла
File1.c, но внутри
файла эта функция
доступна.
#include "File1.h" // File1.c
void visible()
{
printf("Everyone can use me!\n");
hidden();
}
void hidden()
{
printf("No one can use me, except my friends from File1.c\n");
}
#include <stdio.h> // main.c
#include "File1.h"
void main() {
visible();
// hidden(); её теперь не вызвать
getch();
}
21

22.

Выводы
Особенности работы с языком Си
Какой класс памяти применять?
Ответ на вопрос – автоматический.
Этот класс памяти выбран по умолчанию.
Использование внешних переменных очень соблазнительно.
Если описать все переменные как внешние, то не будет забот
при использовании аргументов и указателей для связи между
функциями в прямом и обратном направлениях.
Но тогда возникает проблема с функцией С, изменяющей
переменные в функции А, а мы этого не хотели!
Такая проблема значительно перевешивает кажущуюся
привлекательность широкого использования внешних
переменных.
Одно из золотых правил программирования заключается в
соблюдении принципа "необходимо знать только то, что
нужно".
Организуйте работу каждой функции автономно, насколько
это возможно, и используйте глобальные переменные только
тогда, когда это действительно необходимо!
Операция получения адреса & неприменима к регистровым
переменным.
Любые переменные в блоке, кроме формальных параметров
функции, могут быть определены как статические.
Подведем итог
Классы памяти, которые описываются внутри функции:
автоматический, продолжительность существования –
временно, область действия – локальная;
регистровый, продолжительность существования – временно,
область действия – локальная;
статический, продолжительность существования – постоянно,
область действия – локальная.
Классы памяти, которые определяются вне функции:
внешний, продолжительность существования – постоянно,
область действия глобальная (все файлы);
внешний статический, продолжительность существования –
постоянно, область действия – глобальная (один файл).
22

23.

NB: Размерность в Си
Если говорить о размерах, то лучше сразу вывести размеры
различных типов:
#include <stdio.h>
#include <stdint.h>
// Windows 10, 64-разрядный процессор, компилятор GCC
int main(int argc, char** argv)
{
// вот размеры разных типов
// Полные названия типов
printf("sizeof int8_t: %llu\n", sizeof(int8_t));
// char
printf("sizeof uint8_t: %llu\n", sizeof(uint8_t);
// unsigned char
printf("sizeof int16_t: %llu\n", sizeof(int16_t);
// short int
printf("sizeof uint16_t: %llu\n", sizeof(uint16_t); // unsigned short int
printf("sizeof int32_t: %llu\n", sizeof(int32_t);
// int
printf("sizeof uint32_t: %llu\n", sizeof(uint32_t); // unsigned int
printf("sizeof int64_t: %llu\n", sizeof(int64_t);
// long long int
printf("sizeof uint64_t: %llu\n", sizeof(uint64_t); // unsigned long long int
}
// Но это не всё. Сразу отмечу ещё один момент, который меняет ВСЁ.
printf("sizeof uint8_t*: %llu\n", sizeof(uint8_t*); // unsigned char*
printf("sizeof uint16_t*: %llu\n", sizeof(uint16_t*); // unsigned short int*
printf("sizeof uint32_t*: %llu\n", sizeof(uint32_t*); // unsigned int*
printf("sizeof uint64_t*: %llu\n", sizeof(uint64_t*); // unsigned long long int*
printf("sizeof void*: %llu\n", sizeof(void*);
// void*
/* В этом блоке размеры РАВНЫ.
Это потому что размер указателя диктуется РАЗРЯДНОСТЬЮ ПРОЦЕССОРА,
где 64-битные процессоры соответствуют 8-байтным указателям,
в следствие чего меняется и максимальный размер оперативной памяти. */
// И раз зашла речь о типах данных, то в библиотеке stdint.h
// содержится ещё несколько интересных типов
printf("sizeof uintptr_t: %llu\n", sizeof(uintptr_t));
printf("sizeof intptr_t: %llu\n", sizeof(uintptr_t));
return 0;
Размер указателя одинаковый в пределах программы на
одном компьютере.
Указатель - это число равное, или меньшее размером, чем
разрядность процессора
То есть на 64-битном процессоре это 8-байтовые указатели,
или меньшие, 4-байтовые и 2-байтовые, на 32-битном - 4байтовые, или меньшие, 2-байтовые. uintptr_t в свою очередь
по размеру совпадает с максимальным допустимым
размером указателя на процессоре.
NB: на микроконтроллерах AVR указатели могут быть 24битными(3 байта) и 16-битными(2 байта)
// Вывод:
// sizeof int8_t: 1
// sizeof uint8_t: 1
// sizeof int16_t: 2
// sizeof uint16_t: 2
// sizeof int32_t: 4
// sizeof uint32_t: 4
// sizeof int64_t: 8
// sizeof uint64_t: 8
// sizeof uint8_t*: 8 /* Размеры указателей верны для 64-битного
процессора. */
// sizeof uint16_t*: 8 // На других архитектурах возможны другие размеры.
// sizeof uint32_t*: 8
// sizeof uint64_t*: 8
// sizeof void*: 8
// sizeof uintptr_t: 8 // Этот два типа представляет собой указатель, как
число.
// sizeof intptr_t: 8 /* Да, В него можно преобразовать указатель. В этом
даже фишка */
Управление памятью одновременно сложно и просто. Одни могут и не понять его и за годы, другие поймут с ходу. Всё зависит от
представлениях об оперативной памяти. Надеюсь, материалы лекций смогли не понимающим дать это самое понимание, а
понимающим укрепить свои познания… :))
23

24.

24
English     Русский Rules