
информатика_2 / Программирование_С
.pdfn ++; |
// main изменяет значение n, нумеруя последовательность |
|
// вызовов функции f1 |
printf ("\nMain. Вход № %d", n); |
|
f1 (); |
// f1 тоже меняет значение n, потому что |
} |
// main и f1имеют одинаковые права на изменение n |
for (i = 1; i <= 3; i++) |
|
{ |
|
a ++; |
// main изменяет значение a |
printf ("\nMain. Глобальная а: %d", a); |
|
f2(); |
// в функции f2 значение а свое собственное |
}
// как передача данного в функцию защищает его от изменения f3 (a);
printf ("\nЗначение глобального а после обращения к f3 %d", a);
}
// f1 печатает номер обращения
void f1 ()
{
n ++; // функция меняет глобальную переменную n printf ("\nФункция f1. Вход %d", n);
}
//f2 печатает значение локальной переменной void f2 ()
{
int a = 90;// а — локальное данное функции f2 printf "\nФункция f2. Локальная а %d", a);
}
//f2 печатает значение локальной переменной
void f3 (int a) // глобальная переменная передается
{ |
// как параметр, защищена от изменения |
a ++; |
// локальное данное функции f3 |
printf("\nФункция f3. Не меняет значение глобального а %d", a);
}
1.2.11.9. Классы памяти. Внешние объекты
Существуют классы памяти, к которым отнесена переменная. Есть три класса памяти: auto, static, register. Они классифицируют переменные по схеме выделения памяти. Приписываются перед объявлением типа переменной,
например, |
|
auto |
int i; |
static int n; |
|
register |
int k; |
61
Класс auto называется автоматическим классом памяти, действует по умолчанию. Регистровый класс памяти (register) раньше использовался для того, чтобы явно указать компилятору, что этот объект должен непосредственно размешаться в регистрах процессора, что позволяло повысить быстродействие программ. Компиляторы современного уровня являются оптимизирующими, и сами определяют механизмы выделения памяти, поэтому объявление register носит рекомендательный характер, и объявленный объект будет, скорее всего, класса auto. Класс static называется статическим классом памяти, используется для того, чтобы вынести на глобальный уровень объявление локального объекта.
Механизм действия классов памяти зависит от того, как локализован объект.
Для локальных объектов.
Объекты auto существуют только внутри того блока, где они определены. Память для объекта выделяется при входе в блок, а при выходе освобождается, то есть объекты перестают существовать. При повторном входе в блок для тех же объектов снова выделяется память, значения переменных не сохраняются.
Объекты static существуют в течение всего времени выполнения программы. Память выделяется один раз при старте программы и при этом обнуляется. При повторном входе в блок, где объявлен статический объект, его значение сохраняется. Вне блока, где объявлен статический объект, его значения недоступны.
Пример:
void f_auto (void)
{// переменная K по умолчанию auto
int K = 1; // локальный объект автоматической памяти printf ("K = %3d ", K);
K++;
return;
}
void main (void)
{
for (int i = 1; i <=3; i ++) f_auto ();
}
Результат выполнения программы обусловлен локализацией объекта К:
К = 1 К = 1 К = 1
В том же примере покажем действие статического объекта: void f_stat (void)
{// переменная K статическая
static int K = 1; // локальный объект статической памяти printf ("K = %3d ", K);
K++;
return;
}
62
void main (void)
{
for (int i = 1; i <= 3; i ++) f_stat ();
}
Результат выполнения программы обусловлен «глобализацией» объекта К с изменением класса памяти:
К = 1 К = 2 К = 3
Вне тела функции f_stat объект недоступен, то есть он внутренний.
Для глобальных объектов.
Объекты auto действуют в данном файле от описания (auto) до конца файла. Известны всем функциям программы. Не действуют в других файлах проекта, где они описаны с атрибутом extern.
Объекты static действуют от описания до конца файла, известны всем функциям программы. Не действуют в других файлах проекта, где они не описаны с атрибутом extern.
Внешние объекты. Этот термин используется для именования объектов, в описании которых присутствует ключевое слово extern. Как правило, программа на С — это совокупность отдельных модулей (файлов), каждый из которых имеет в своем составе некоторый набор функций обработки данных. Все функции в С являются внешними, так как объявлены на одном уровне. В С можно объявить и внешние объекты — переменные любого типа. Ранее мы называли их глобальными, имея в виду, что их определение действует в рамках одного модуля (файла). Они объявляются вне тела всех функций модуля, и доступны всем функциям этого модуля. Когда несколько модулей объединяются в проект, внешние объекты каждого модуля могут быть доступны многим функциям программы, в том числе функциям других модулей, но не всегда эта доступность достигается автоматически. Для того, чтобы сделать объект доступным для функций другого файла или функций, объявленных выше по тексту, используется ключевое слово extern.
Если объект объявлен с ключевым словом extern в начале файла, то он доступен всем функциям этого файла. Если такое объявление в теле функции, то объект локален в теле функции. Чтобы сделать объект доступным в нескольких модулях, его следует определить в одном модуле как глобальный, а в других модулях проекта как внешний с помощью extern.
Во внешнем объявлении прототипов всех функций по умолчанию класс памяти принят extern.
63
1.3. Продвинутые возможности языка C и производные типы данных
1.3.1. Классификация типов данных C
Классифицируя данные C, мы отметили, что есть две группы типов: базовые типы и производные типы.
Синонимами названия «базовый тип» являются названия «стандартные», «предопределенные». Реализация этих типов заложена в стандарте языка, и программист не вправе изменить предопределенное. Производным типом является такой тип, который должен быть описан пользователем перед употреблением. Полезно напомнить, что тип данного определяет его размещение в памяти и набор операций.
Примерная классификация производных типов:
•функции,
•массивы,
•указатели,
•структуры,
•объединения,
•файлы.
1.3.2. Массивы
Определение. Массив — упорядоченное множество данных одного типа, объединенных общим именем.
Тип элементов массива может быть почти любым типом С, не обязательно это базовый тип. Например, можно определить массив координат точек на плоскости, тогда один элемент массива содержит две координаты одной точки. Можно определить массив сведений о студенте, тогда один элемент массива содержит разнообразные сведения о студенте, например, фамилия, имя, возраст, адрес и прочие. В этих случаях для определения типа одного элемента массива используется также производный тип, например, массив или структура.
Обязательные атрибуты массива — это размерность, число элементов и тип элементов.
1.3.2.1. Описание массива
Чтобы ввести в употребление массив, необходимо его описать (конструировать). Можно одновременно выполнить инициализацию элементов массива. Смысл описания и объявления для массивов идентичен.
Описание массива выполняется, как и для обычных переменных, в разделе описаний.
64
Синтаксис:
тип_массива имя_массива [количество_элементов]; Здесь количество_элементов — это константа, заданная явно или именованная
(константное выражение).
Пример: |
|
#define N 10 |
|
int mas [3]; |
// одномерный массив mas содержит 3 целочисленных |
|
// элемента |
int matr [2][10]; |
// одномерный массив matr содержит два одномерных |
|
// массива по N = 10 элемента в каждом |
float w [N]; |
// одномерный массив w содержит N вещественных |
|
// элементов |
char c [5][80]; |
// массив из 5-ти строк, по 80 символов в строке |
Примечание. Элементы массива нумеруются с 0, то есть для int mas [3];
имеются элементы mas [0], mas [1], mas [2].
1.3.2.2. Размещение в памяти элементов массива.
При объявлении массива имя массива сопоставлено всей совокупности значений. Элементы массива размещаются в памяти подряд в соответствии с ростом индекса (номера элемента внутри массива). Размер выделяемой памяти такой, чтобы разместить значения всех элементов. Такие массивы называются статическими, место в памяти выделяется на этапе компиляции, именно поэтому размер массива определен константой.
Чтобы определить общий размер памяти, занимаемой массивом, можно использовать операцию sizeof. Как известно, ее аргументом может быть имя объекта или имя типа, в том числе объекта сконструированного типа.
sizeof (имя_переменной) |
// в байтах для этой переменной |
sizeof (имя_типа) |
// в байтах для любого данного этого типа |
sizeof (имя_массива) |
// в байтах для этой переменной с учетом длины |
1.3.2.3. Операции над массивами
Над массивом как единой структурой никакие операции не определены. Над данными, входящими в массив, набор операций определен их типом. При работе с массивом можно обращаться только к отдельным его элементам. Операция обращения к одному элементу массива называется операцией разыменования. Это бинарная операция [ ], синтаксис которой:
имя_массива [индекс] Первый операнд имя_массива показывает, что происходит обращение к
необычному данному, то есть такому, в составе которого много значений, то есть ко всем элементам массива.
Второй операнд индекс может быть только целочисленным, дает возможность выделить один элемент из группы, указывая его номер внутри массива. Индекс, в
65

общем случае — это выражение целого типа, определяющее номер элемента внутри массива (счет с нуля), например:
mas [0]; |
// обращение к первому элементу массива (номер его =0) |
|
|||||||
matr [1][3]; |
// обращение к элементу матрицы, стоящему на пересечении |
||||||||
|
|
|
// 1-ой строки и 3-го столбца |
|
|
|
|
||
char c[i][2]; |
// обращение ко 2-му символу i - той строки текста |
|
|||||||
Замечание. Контроль выхода индекса за границы массива не существует. |
|
||||||||
Покажем размещение элементов массива в памяти на рис. 1.6. |
|
||||||||
int mas [4]; |
|
|
|
|
|
|
|
||
|
|
|
Имя mas сопоставлено всей совокупности данных |
|
|
||||
|
|
|
|
|
|||||
|
|
|
|
|
|
|
|
|
|
|
… |
|
mas[0] |
mas[1] |
|
mas[2] |
mas[3] |
|
… |
|
|
|
|
|
|
|
Индекс=3 |
|
|
|
|
|
Индекс=0 |
Индекс=1 |
|
Индекс=2 |
|
|
Рис. 1.6. Размещение в памяти элементов массива
Для каждого элемента массива выделено по 2 байта, как для данного типа int. В целом для всего массива выделено 2 * 4 = 8 байт. Значения элементов массива неизвестны.
1.3.2.4. Начальная инициализация элементов массива
При описании массива можно выполнить начальную инициализацию значений его элементов, для этого нужно задать список инициализирующих значений. В списке инициализации перечисляются через запятую значения элементов массива. Например, в году всегда 12 месяцев, значение числа дней в каждом месяце известно, значит, такая структура может быть задана массивом:
int month [12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
Эквивалентом инициализации является простое присваивание вида: month [0] = 31; // Январь
.. и т.д.
month [11] = 31; // Декабрь
Механизм инициализации прост, но имеет некоторые особенности.
1. Если значение размера массива опущено, то число элементов массива равно числу значений в списке инициализации:
int month [] = {31, 28, 31}; // количество элементов равно 3
Чтобы программно определить число элементов в таком массиве, используется операция sizeof:
int len; // число элементов
…
len = sizeof (month ) / sizeof (int);
66
2.Если число элементов в списке инициализации меньше, чем объявлено в описании, то число элементов массива будет, сколько объявлено, а остальные значения будут равны 0:
int month [12] = {31, 28, 31, 30}; // количество элементов равно 12,
//их значения 31, 28, 31,30,0,0,0…
3.Если число элементов в списке инициализации больше, чем объявлено в описании, то это синтаксическая ошибка:
int month [2] = {31, 28, 31, 30}; // ошибка.
1.3.2.5. Массивы переменной длины.
Одним из недостатков статических массивов, обусловленным требованиями реализации, является то, что длина такого массива в тексте программы определена константным выражением, следовательно, жестко задана в программе. Часто бывает необходимо, чтобы число элементов массива изменялось бы при работе программы. В полной мере такие массивы реализуются с использованием динамической памяти, механизмы работы с которой изложены далее. Существуют приемы, которые используются, чтобы работать с массивом, длина которого может изменяться:
•#define определенные константы,
•массив условно переменной длины,
•динамические массивы.
Рассмотрим два первых приема. Механизм #define определенных констант заключается в изменении текста программы перед ее компиляцией, значит, препроцессор может редактировать текст программы, внеся в него любое количество изменений. Длина массива записывается в директиве #define, например:
#define N 20
Имя N теперь именованная константа, и в тексте программы для управления алгоритмами обработки массива следует использовать ее имя. Числовое значение константы 20 записывается в тексте один раз в самом начале, и перед очередным запуском программы может быть изменено один раз, остальные изменения выполнит препроцессор. После этого программа нуждается в повторной компиляции и сборке.
Пример. Выполним ввод данных массива и вывод на печать.
#define N 10 |
// статический массив. Длина массива равна 10 |
|
// числовое значение константы записано тексте один раз |
||
void main (void) |
|
|
{ |
|
|
int |
a[N]; |
|
int |
i; |
|
printf("\nВведите %d значений\n", N); for (i = 0; i < N; i++)
scanf("%d", &a[i]); for (i = 0; i < N; i++)
67
printf("%5d", a[i]); printf ("\n");
}
Смысл второго приема заключается в том, что длина массива, даже если она изменяется, может быть оценена заранее. Если знать, какова возможная наибольшая длина массива, то именно это значение и следует выбрать для описания массива. Для того чтобы знать реальную длину массива, вводится специальная переменная, которая принимает значение при выполнении программы, а затем использует его для управления алгоритмами обработки массива. Оба приема можно совместить.
1.3.2.6. Алгоритмы работы с одномерными массивами
Алгоритмы работы с одномерными массивами основаны на использовании циклических алгоритмов, в которых выполняется последовательное обращение к элементам массива. Управляющей переменной в таких алгоритмах должен быть индекс массива.
Приведем фрагменты алгоритмов решения некоторых задач для массивов. Будем ориентироваться на абстрактное решение задачи для произвольного массива произвольной длины. Такую возможность дает только использование функций обработки массивов. В функцию следует передать массив (передается указатель) и длину массива. Если функция не изменяет длину массива, она передается по значению, если изменяет, то по адресу. Объявление фактических массивов, то есть тех, с которыми фактически будет работать функция, и присваивание значений их элементам происходит в вызывающей программе.
Прямой поиск.
Поиск — это одна из наиболее часто решаемых задач, заключается в том, что в массиве требуется найти одно или несколько значений, удовлетворяющих какому-то, заранее заданному, условию. Механизм поиска предельно прост. Нужно сканировать последовательно (прямо) все элементы массива, проверяя каждый на соответствие заданному условию. Иногда это называется «прямой перебор».
Пусть условие будет «значение элемента массива четное». С каждым найденным элементом можно выполнить какое-то действие, например, вывести на экран.
int Found_chot (int a[], int n)
{
for (int i = 0; i < n; i++) if (a[i] % 2 = =0)
printf ("Элемент найден, его номер = %d, значение % d = \n", i, a[i]);
return 1;
}
Использование флага.
68
Приведенный алгоритм имеет существенный недостаток. Если в массиве не найдется ни одного значения, удовлетворяющего условию, то функция вывода printf не будет выполнена ни разу, и экран останется чистым. Алгоритм будет лучше, если при неудачном поиске на экран вывести сообщение об этом. Чтобы при поиске запомнить хотя бы один факт удачного сравнения, вводится специальная переменная, называемая «флаг», которая принимает логическое значение, определенное смыслом алгоритма, например, 1, если поиск удачен, и 0, если поиск неудачен.
int Found_chot (int a[], int n)
{
int flag;
flag = 0; // полагаем, что поиск будет неудачен, // флаг сброшен
for (int i = 0; i < n; i++) if (a[i] % 2 = = 0)
{
flag = 1; // поиск удачен, флаг поднят printf ("Элемент найден, его номер \
= %d, значение % d = \n", i, a[i]);
}
// проверка состояния флага if (flag = = 0)
printf ("Четных элементов нет\n"); return flag;
}
Обратите внимание, что функция возвращает значение флага, которое может быть использовано в вызывающей функции, например:
if (flag (mas,10))
//одно решение
else
//другое решение
Поиск первого (последнего) вхождения элемента в массив.
Суть алгоритма в том, что выполняется прямой поиск, который необходимо остановить на первом вхождении элемента, удовлетворяющего условию. Для поиска последнего элемента цикл перебора следует организовать с конца массива. Следует предусмотреть, что искомого элемента может не оказаться. Найдем элемент, который по модулю больше некоторого наперед заданного значения М. Функция вернет номер (индекс) найденного значения или 0, если такого нет.
int Found_first (int a[], int n, int M)
{
int i = 0;
//из цикла перебора есть два способа выхода:
//1) когда abs (a[i]) > M, цикл прерывается на текущем значении i,
69
// 2) когда достигнут конец массива, i >= n while (abs (a[i]) <= M && i < n)
i++; if (i > n)
return 0; else
return i;
}
В вызывающей функции следует анализировать возвращаемое значение, например:
int Ind = Found_first (mas, len, 25); if ( Ind != 0)
printf ("Номер первого элемента > 25 = %d, значение %d\n", Ind, mas[Ind]); else
printf ("Элементов, > 25, нет в массиве\n");
…
Алгоритмы с изменением длины массива.
К таким алгоритмам относятся удаление элемента из массива и вставка элемента в массив. Обычно они не решаются как самостоятельные задачи, а входят в алгоритм поиска. Например, удалить из массива все элементы, удовлетворяющие какому-то условию, или добавить элемент в упорядоченный массив с сохранением упорядоченности. В любом случае в массиве первоначально должно быть определено место, в котором выполняется операция (точка вставки или удаления). Это индекс элемента, который должен быть удален, или индекс элемента, за которым должен появиться новый элемент. Обе операции приводят к изменению длины массива.
Рассмотрим эти задачи как самостоятельные в предположении, что точка вставки (удаления) известна. Обозначим ее индексом k.
При удалении элемента происходит сдвиг части массива влево на одну позицию, начиная с k-го элемента: a[k] = a[k+1], a[k+1] = a[k+2] и так далее до конца массива, a[n–2] = a[n–1]. Каждый элемент записывается на место предыдущего, стирая его. В итоге длина массива уменьшается на 1.
Приведем фрагмент программы, реализующей этот алгоритм:
int j; |
|
int k = 4; |
// номер удаляемого элемента = 4 |
… |
|
for (j = k; j < n – 1; j++) |
|
a [j] = a [j+1]; |
// цикл сдвига закончился |
n – –; |
// длина массива стала меньше |
… |
|
При добавлении элемента в массив сначала нужно освободить место для записи нового значения. Для этого нужно выполнить сдвиг части массива вправо на одну позицию, начиная с последнего элемента: a[n] = a[n–1], a[n–1] = a[n–2] и так далее до точки вставки a[k+1] = a[k]. Последний элемент записывается на
70