Вы находитесь на странице: 1из 31

7.

Производные однородные типы данных


Оглавление
7. Производные однородные типы данных.........................................................................1
7.1. Указатели.........................................................................................................................1
7.1.1. Описание указателей....................................................................................................1
7.1.2. Указатели и модели памяти.........................................................................................3
7.1.3. Операция взятия адреса...............................................................................................5
7.1.4. Операция косвенной адресации..................................................................................5
7.1.5. Инициализация указателей..........................................................................................7
7.1.6. Адресная арифметика.................................................................................................10
7.1.7. Ссылки.........................................................................................................................13
7.2. Массивы.........................................................................................................................15
7.2.1. Описание массивов.....................................................................................................15
7.2.2. Инициализация массивов...........................................................................................17
7.2.3. Доступ к элементам массива.....................................................................................20
7.2.4. Массивы символьных строк......................................................................................24
7.2.5. Приeмы обработки массива.......................................................................................25
7.3. Перечисления...............................................................................................................28
7.3.1. Описание перечисляемого типа................................................................................29
7.3.2. Пример использования перечислений......................................................................30

7. Производные однородные типы данных


Производные типы данных характеризуются тем, что это более сложные
объекты программы, нежели те, которые были рассмотрены до сих пор.
Производные типы данных не отражают архитектурные особенности компьютера
один к одному, как это имеет место для основных типов char, int, float и double, но
их машинное представление выражается через эти основные типы. Производные
типы данных могут быть однородными и смешанными. Для однородного
производного данного, например массива, все элементы, составляющие данное,
имеют один и тот же тип, а для смешанного производного данного, например
структуры, элементы, составляющие данное, могут иметь различные типы. В этом
разделе будут рассмотрены производные однородные типы данных.

7.1. Указатели
Указатель представляет собой переменную, значением которой является адрес
памяти. Значение указателя "показывает" (отсюда и термин – указатель), по какому
адресу памяти размещено некоторое данное, а из описания указателя известен тип
этого данного.
7.1.1. Описание указателей
Указатели описываются операторами вида:

<тип> *<объект1>,*<объект2>,...,*<объектN>;
где
<тип> – это любой, допустимый в Си тип данных; * звездочка говорит о том,
что следующий за ней <объект> является объектом, указывающим на данное
1
<тип>; <объект> в простейшем случае это <идентификатор> и тогда он
представляет собой скалярную переменную, являющуюся указателем.
Более сложным объектом может быть <идентификатор> [<кол-во>]...[<кол-
во>], и тогда идентификатор представляет собой имя n-мерного массива
указателей, содержащего <кол-во> элементов по n-му измерению. Если в качестве
объекта в оператор описания подставить конструкцию вида *<объект>, то тогда
объект является указателем на указатель данного <тип>. Если вправо от
идентификатора объекта записаны парные пустые круглые скобки, то такой
идентификатор является указателем на функцию. При описании указателей можно
использовать круглые скобки для задания нужной интерпретации. Правила
интерпретации сложных описаний будут рассмотрены чуть ниже, пока же
исследуем простые примеры описаний,
int *p1; p1 – указатель на данные целого типа.
float *p2; p2 – указатель на данные с плавающей точкой.
char *p3; p3 – указатель на символьные данные.
int *p4[3]; p4 – массив указателей на данные целого типа, каждый из
элементов которого p4[0], р4[1], p4[2] является
указателем на целое.
void *p5; p5 – указатель на данные, тип которых заранее не
определен.
char **p6 p6 – указатель на указатель данных типа сhar.

Более сложные примеры описания указателей:

int (*fn)(); fn – указатель на функцию, возвращающую целое


значение.
char (*pt)[5]; pt – указатель на пятиэлементный массив
символьных данных.
int (*uk)[5][6]; uk – указатель на двухмерный массив данных
целого типа размером 5 на 6.
int *(*vc[10])(); vc – массив из десяти указателей на функции,
возвращающие указатели на целые значения.
При интерпретации сложных описаний квадратные скобки, задающие массив,
и круглые пустые скобки, задающие функцию (и те и другие стоят справа от
идентификатора) обладают приоритетом перед звездочкой (слева от
идентификатора). Квадратные и круглые скобки имеют один и тот же приоритет и
связываются слева направо. Тип данного, на который ссылается указатель,
рассматривается на последнем шаге, когда объект уже полностью
проинтерпретирован. Непустые круглые скобки используются для того, чтобы
установить необходимый (и, естественно, допустимый в языке) порядок
интерпретации. Для интерпретации сложных описаний данных вообще, в том
числе и указателей, известно простое правило, называемое "изнутри наружу" и
состоящее из четырех шагов.

2
1. Начать с идентификатора и посмотреть вправо, есть ли квадратные или
круглые скобки.
2. Если они есть, то проинтерпретировать эту часть описания и затем
посмотреть налево в поиске звездочки.
3. Если на любой стадии справа встретится закрывающая круглая скобка, то
вначале необходимо применить все эти правила внутри круглых скобок, а
затем продолжить интерпретацию.
4. Интерпретировать описатель типа.
Применим это правило для интерпретации последнего из вышеприведенных
примеров:

int * ( * vc[10] ) ();


6 5 3 1 2 4

Цифрами показан порядок просмотра и интерпретации описания в


соответствии с правилом: объявляется переменная vc (1) как массив из десяти (2)
указателей (3) на функции (4), возвращающие указатели (5) на целые значения (6).
И, наконец, последнее в этом разделе замечание по поводу описания указателей.
Если мы хотим описать указатель, используя который нельзя изменить в
программе значение данного, на которое он указывает, то можем использовать
модификатор const. Например:

const char *p7;

Указателю p7 могут быть присвоены различные значения, но данные, на


которые он указывает, не могут быть изменены.

7.1.2. Указатели и модели памяти


Размер памяти, выделяемой компилятором под указатель, зависит от модели
памяти, для которой будет компилироваться программа. Память для программы на
Турбо Си требуется в следующих четырех целях: для размещения программного
кода, для размещения статических данных, для размещения динамических данных
по запросу из программы (память под них выделяется из области, называемой
куча) и для организации стека, в котором создаются локальные переменные и
заносятся параметры при вызовах функций.
В зависимости от объема кода программы и данных, программу нужно
скомпилировать для одной из шести моделей памяти, предлагаемых Турбо Си.
1. Tiny (крошечная). Под код программы, статические данные, кучу и стек
отводится в сумме 64К памяти. В такой модели памяти для указателя требуется два
байта (близкий указатель).
2. Small (маленькая). Под код программы отводится 64К, под стек, кучу и
статические данные отводится в сумме 64К. В такой модели памяти программа
работает также с близкими указателями.

3
3. Medium (средняя). Под код программы отводится 1Мгб памяти. Это
обозначает, что все обращения к функциям и возвраты из функций,
осуществляемые через указатель, должны использовать указатель длиной в четыре
байта, чтобы в нем мог разместиться далекий адрес памяти (далекий указатель).
Под стек, кучу и статические данные выделяется 64К памяти и, следовательно, для
адресации данных используются близкие указатели.
4. Compact (компактная). Под код программы отводится 64К. Под данные
отводится всего 1Мгб, но объем статических данных ограничивается размером
64К. Под стек выделяется 64К. Адресация внутри программы осуществляется
близкими указателями, адресация – данных далекими указателями.
5. Large (большая). Под код программы отводится 1Мгб, под статические
данные – 64К, куча может занимать до 1Мгб памяти. Как программа, так и данные
адресуются далекими указателями.
6. Huge (огромная). Аналогична большой модели, но объем статических
данных может превышать 64К. Для любой адресации необходимы далекие
указатели.
В зависимости от соотношения объемов код–данные, выбирается модель
памяти для компиляции вашей программы. Нередки случаи, когда при
неправильном выборе модели памяти программа работает некорректно, хотя на
первый взгляд – все в порядке. В некоторых случаях возникает необходимость
задать размер указателя, отличный от размера, устанавливаемого при компиляции
в той или иной модели памяти. Это можно сделать при помощи модификаторов
описания указателей near, far и huge:
 модификатор near устанавливает 16-тибитный, т.е. близкий указатель. При
использовании near-указателей данные программы ограничены размером
сегмента 64К;
 модификатор far устанавливает 32-ухбитный, т.е. далекий указатель. При
использовании far-указателей можно ссылаться на данные в пределах 1
мегабайта адресного пространства;
 модификатор huge также устанавливает 32-ухбитный указатель
аналогичный far-указателю, однако между ними есть различия. Во-первых,
операции отношения ==, !=, <, >, <= и >= работают правильно с huge-
указателями, а с far-указателями они работают неправильно. Во-вторых,
все арифметические операции над указателями типа huge воздействуют как
на адрес сегмента, так и на смещение, а при использовании указателей
типа far воздействуют только на смещение.
Из-за того, что арифметические операции при работе с far-указателями
воздействуют только на смещение, то хотя far-указатель и может адресовать
данные в пределах одного мегабайта, данные, к которым идет обращение, должны
начинаться и заканчиваться в пределах сегмента. Например, все элементы массива,
расположенного в нескольких сегментах, доступны при помощи huge-указателя, а
при помощи far-указателя можно получить доступ только к элементам,
расположенным в пределах данного сегмента, остальные будут недоступны для far-
4
указателя, т.к. арифметические операции изменяют смещение, но оставляют
неизменным адрес сегмента. Попутно заметим, что для near–указателя ограничения
еще более жесткие: все данные должны располагаться в пределах одного сегмента
(для huge u far эти пределы – 1 Мб).
Рассмотрим способы описания указателей с модификаторами near, far и huge.
Когда одно из этих ключевых слов встречается в описании, то оно модифицирует
объект, расположенный справа от ключевого слова, например:

char far *ptr; ptr описан как far-указатель,


int huge *ptr; ptr описан как huge-указатель,
float near *ptr; ptr описан как near-указатель,
int *(far *ptr); ptr описан как far-указатель на указатель к
данным целого типа.

7.1.3. Операция взятия адреса


Операция взятия адреса является одной из важнейших, связанных с
указателями. Она обозначается знаком &, является унарной операцией и ее
операндом может быть только переменная или элемент массива, причем тип этой
переменной может быть любым допустимым типом данных Си. Попытка
применить операцию взятия адреса к какому-либо другому объекту языка вызывает
предупреждающее сообщение компилятора. Операция взятия адреса записывается
в выражениях в виде:

&<переменная>

Результатом операции взятия адреса является адрес ячейки памяти, которая


была выделена компилятором под соответствующую переменную. Например, если
предположить, что для некоторой переменной var выделена область памяти,
начиная с адреса 3456, тогда &var будет иметь значение 3456. Для операции взятия
адреса следует учитывать, что &var является константой, так как адрес переменной
var не может измениться в ходе выполнения программы. Эта константа является
константой типа указатель, ибо представляет собой адрес памяти, а длина
константы типа указатель зависит от модели памяти, выбранной при компиляции
программы. Операцию взятия адреса & не следует путать с бинарной операцией &
(поразрядное логическое И). Как и все унарные операции, операция взятия адреса
имеет высокий приоритет, занимая вторую строку таблицы приоритетов операций.
7.1.4. Операция косвенной адресации
Другой важной операцией, непосредственно имеющей отношение к
указателям, является операция косвенной адресации. Операция косвенной
адресации также является унарной, обозначается знаком * и ее операндом может
быть только адресное выражение, т.е. выражение, значением которого является
адрес памяти. Операция косвенной адресации записывается в виде:

5
*<адресное_выражение>

В результате выполнения операции косвенной адресации происходит


обращение к памяти по адресу, определяемому значением адресного выражения, и
из этого адреса извлекается данное либо по этому адресу посылается данное в
зависимости от контекста использования операции. Например, если мы рассмотрим
фрагмент программы:
int q,b,*pq;
q=5; pq=&q;
b=*pq;

то в результате его выполнения переменная b примет значение 5. В последнем


операторе b=*pq; по адресу, записанному в указателе pq, извлекается значение
переменной q, т,е. числo 5, и присваивается переменной b. В данном случае мы
извлекли значение не прямым обращением к переменной путем использования ее
имени – q, а косвенно, путем использования ее адреса, имеющегося в указателе
рq. Выражение *pq записано справа от знака присваивания и является
правоопределенным (r-value, right value) вариантом этого выражения. Как в
указатель pq попал адрес переменной q – понятно, в результате выполнения
предыдущего оператора рq=&q;. Таким образом, в этом контексте операция
косвенной адресации трактуется как "обратиться к данному по адресу", и ее
значением является данное, размещенное по этому адресу. Но как же определяется,
сколько байтов данного нужно извлечь из адреса? Очень просто. К моменту
использования указателя его тип всегда должен быть определен, в данном случае
он имеет тип int и, следовательно, нужно извлечь два байта данного. Рассмотрим
другой, несколько видоизмененный пример:
int q,b,*pb;
q=5; pb=&b;
*pb=q;

в котором операция косвенной адресации использована слева от операции


присваивания. Это случай так называемого левоопределенного выражения (l-value,
left value) вида: *<адресное_выражение> . Теперь левосторонняя операция
косвенной адресации с учетом операции присваивания трактуется как "записать
данное по адресу", и в результате выполнения этого фрагмента программы
переменная b примет значение 5, как и в предыдущем случае. Разница заключается
в том, что мы присвоили ей значение путем косвенной адресации через
переменную–указатель, содержащую ее адрес. Таким образом, мы установили, что
операция косвенной адресации может быть использована как для извлечения
значения объекта через его адрес, так и для присвоения значения через адрес
объекта. Естественно, что перед использованием операции косвенной адресации в

6
программе должны быть известны адрес объекта, тип объекта и его значение.
Только в этом случае операция косвенной адресации будет выполнена корректно.
Тип объекта не может быть типом void – пустым типом, но значение объекта
может быть "пустым", т.е. еще не известным, если ему присваивается значение с
использованием левосторонней операции косвенной адресации. Мы уже приводили
пример описания указателя на данные, тип которых заранее не определен: void
*p5; Такой указатель совместим с указателем на любой тип данных, т.е. ему
можно присвоить адрес объекта любого типа, но перед использованием указателя,
объявленного как void, он обязательно должен быть специфицирован типом
объекта, на который он указывает, например:

void *p5;
double r,q;
q=1.85e-12;
p5=&q;
r=*((double *)p5);

Здесь использована операция преобразования типа (double *), с тем чтобы


указатель на неопределенный тип данных р5 был преобразован к указателю на тип
данных double, потому что операцией косвенной адресации мы желаем получить
доступ к данному именно этого типа. Было бы ошибкой записать последний
оператор примера как r=*((double)p5);, ибо в этом случае указатель р5
приводился бы к числу с плавающей точкой типа double, а не к указателю на тип
double. Применение указателей, операций взятия адреса и косвенной адресации в
программах на Си весьма обширно. Очень часто их применяют при передаче
параметров в функции, когда необходимо передать не значения, а их адреса, при
работе с массивами и т.д. Все это и многое другое будет рассмотрено далее.
7.1.5. Инициализация указателей
Инициализации указателей корректными значениями адресов памяти
необходимо уделять повышенное внимание. Дело в том, что указатели можно
инициализировать точно так же, как и обычные переменные, например:

int *р1=1242, *р2=1246; .

Такая инициализация вызовет предупреждающее сообщение компилятора о


том, что нарушается мобильность программы, программа будет скомпилирована,
отредактирована и выполнена. Такое возможно, если мы точно знаем, что в этих
адресах имеются интересующие нас данные целого типа и мы получим доступ к
ним через указатели. Для другой какой-либо операционной системы или другого
распределения памяти в этих адресах может оказаться нечто иное, отличное от
того, что мы предполагаем. Поступая таким образом, мы, в лучшем случае, можем
прийти к некорректно работающей программе. Поэтому рассмотрим три надежных
способа инициализации указателей.

7
Первый способ: описать переменные программы. Поскольку компилятор
отводит память под переменную в момент ее описания, то присваивание указателю
адреса переменной гарантирует, что нужная память отведена и там будут
находиться значения переменных, например:
float r, b;
float *ukr=&r, *ukb=&b;

В таком описании указателей с инициализацией ничего странного нет, ибо,


как уже известно, &r и &b являются константами типа указатель. Эти же указатели
можно инициализировать и в ходе выполнения программы адресами уже
описанных переменных, например так:

float r, b;
float *ukr, *ukb;
. . . . . . . . .
ukr=&r; ukb=&b:

Второй способ: присвоить указателю значение другого указателя, к этому


моменту уже правильно инициализированного, например:
char *uk_simv_rus, *uk_simv_lat;
char simvsmoll, simvlarge;
. . . . . . . . .
uk_simv_lat=&simvsmoll;
uk_simv_rus=uk_simv_lat;
Обратим внимание на то, что в этом случае разные указатели содержат адреса
одной и той же переменной. Значит, к переменной simvsmoll можно обратиться
как через указатель uk_simv_rus так и через указатель uk_simv_lat, что
зачастую предоставляет программисту дополнительные удобства.
Третий способ: использовать одну из встроенных функции распределения
памяти, например, malloc. Обращение к функции malloc имеет вид:

malloc(<размер>);

Функция malloc выделяет (резервирует) из области куча память длиной


<размер> байтов, где <размер> задается выражением типа unsigned. Функция
возвращает адрес выделенной памяти в виде константы-указателя типа void. Если в
памяти нет под кучу свободного места требуемого размера, то в качестве
результата будет выдан адрес 0. Инициализацию указателя можно осуществить
следующим образом:

int *x=(int *)malloc(sizeof(int)); .

Для такого способа инициализации полезно задать себе ряд вопросов, задуматься
над ними и получить аргументированные ответы. Рассуждать будем так: это
8
оператор описания, ибо он начинается с ключевого слова, задающего <тип> . Мы
знаем, что при описании переменных допустима их инициализация только
константными выражениями. Исходя из такой посылки, сформулируем наши
вопросы и дадим на них ответы. Вопрос первый: является ли выражение справа от
знака присваивания константным? Да, является, так как функция возвращает адрес
выделенной памяти и можно считать, что обращение к функции есть именованная
константа-указатель, значение которой устанавливается в момент применения
функции. Вопрос второй: почему в константном выражении использована
операция преобразования типа (int *)? Потому что возвращается константа-
указатель на неопределенный тип void, а мы инициализируем указатель на данные
типа int. Следовательно, нам нужно преобразовать константу-указатель на
неопределенный тип к константе-указателю на целый тип. И, наконец, последний,
третий вопрос: почему <размер> задан операцией sizeof(int)? Ответ: для того,
чтобы обеспечить мобильность программы. В самом деле, если мы запишем этот
оператор в виде int*x=(int*)malloc(2);, то при использовании компьютера
другой архитектуры, где, например, данные типа int представляются
четырехбайтными словам памяти, такая программа может оказаться
неработоспособной, т.к. для четырехбайтного целого резервируется два байта
памяти. С использованием функции malloc, инициализацию описанного
указателя можно осуществлять в нужный момент и в ходе выполнения программы:

int *x;
. . . .
x=(int *)malloc(sizeof(int);
if (x != 0)
{
/* действия программы, если память выделена */
*х=-5244;
/* в выделенную память послали -5244 */
};
else {
/* действия программы, если память не выделена */
};

В языке С++ для выделения памяти используется операция new:


new <тип>
new <тип>[<количество элементов>]
Например:
int *x;
x = new int; //выделить память под хранение данного типа int
x = new int[10];//выделить память под хранение 10-ти данных типа int
Если операция new возвращает 0 (NULL), то это означает, что память не
выделилась.
9
Во всех случаях, когда используется функция запроса памяти, рекомендуется
проверять, выделена ли требуемая память на самом деле, как это сделано в
предыдущем примере. Вообще, частое применение в программе функций
выделения памяти может привести к тому, что в конце концов памяти в куче может
не хватить. Поэтому если динамически выделяемая память не нужна, то ее следует
освободить. Освобождение памяти осуществляется функцией free, обращение к
которой имеет вид:
free(<указатель>);

где <указатель> должен содержать в себе адрес памяти, которая должна быть
освобождена. Чтобы уничтожить в памяти значение -5244, динамически созданное
в области кучи, как показано в предыдущем примере, нужно записать оператор
free(x); .
В языке С++ для освобождения динамически выделенной памяти
используется операция delete:
delete <указатель>;
delete [ ] <указатель>;
Например:
int * p;
int i = 10;
p= new int[i]; //выделить память под массив данных
if(p != NULL) printf("Память выделена");
else printf("Свободной памяти нет");
delete[] p; //освободить память массива данных по адресу р

int *x;
x = new int;
delete x; // освободить память по адресу x
7.1.6. Адресная арифметика
Под адресной арифметикой понимаются действия над указателями, связанные
с использованием адресов памяти. Рассмотрим операции, которые можно
применять к указателям, попутно заметив, что некоторые из них уже
рассматривались, однако здесь мы повторимся, для систематизации изложения
материала.
1. Присваивание. Указателю можно присвоить значение адреса. Любое
число, присвоенное указателю, трактуется как адрес памяти:

int *u,*adr;
int N;
u=&N; // указателю присвоен адрес переменной N */
adr=0х00FD; // указателю присвоен 16-теричный адрес, (не
10
// рекомендуется так делать )

2. Взятие адреса. Так как указатель является переменной, то для получения


адреса памяти, где расположен указатель, можно использовать операцию взятия
адреса & :

.int *a,b;
a=&b; // указателю a присвоен адрес указателя b

3. Косвенная адресация. Для того, чтобы получить значение, хранящееся по


адресу, на который ссылается указатель, или послать данное по адресу,
используется операция косвенной адресации * :

int *uk;
int n;
int m=5;
uk=&m; // uk присвоен адрес переменной m
n=*uk; // переменная n примет значение 5
*uk=-13; // переменная m примет значение -13

4. Преобразование типа. Указатель на объект одного типа может быть


преобразован в указатель на другой тип. При этом следует учитывать, что объект,
адресуемый преобразованным указателем, будет интерпретироваться по-другому.
Операция преобразования типа указателя применяется в виде
(<тип> *)<указатель> :

int i, *ptr;
i=0x8e41;
ptr=&i;
printf("%d\n", *ptr); //печатается значение int:-29119
printf("%d\n", *((char *)ptr));
// ptr преобразован к типу сhar, извлекается 1 байт и его двоичный код
// печатается в виде десятичного числа65
Преобразование типа указателя чаще всего применяется для приведения
указателя на неопределенный тип данных void к типу объекта, доступ к которому
будет осуществляться через этот указатель.

5. Определение размера. Для определения размера указателя можно


использовать операцию размер в виде sizeof(<указатель>). Размер памяти,
отводимой компилятором под указатель, зависит от модели памяти. Для близких
указателей операция sizeof дает значение 2, для дальних 4.

6. Сравнение. Сравнение двух указателей любой из операций отношения


имеет смысл только в том случае, если оба указателя адресуют общий для них
объект, например, строку или массив.

11
7. Индексация. Указатель может индексироваться применением к нему
операции индексации, обозначаемой в Си квадратными скобками []. Индексация
указателя имеет вид <указатель>[<индекс>], где <индекс> записывается
целочисленным выражением. Операция индексирования является унарной и
применяется справа от операнда, а не слева, как для других унарных операций.
Возвращаемым значением операции индексации является данное, находящееся по
адресу, смещенному в бóльшую или меньшую сторону относительно адреса,
содержащегося в указателе в момент применения операции. Этот адрес
определяется так: (адрес в указателе) + (значение <индекс>) * sizeof(<тип>), где
<тип> – это тип указателя. Из этого адреса извлекается или в этот адрес
посылается, в зависимости от контекста применения операции, данное, тип
которого интерпретируется в соответствии с типом указателя. Рассмотрим
следующий пример:
int *uk1;
int b,k;
uk1=&b; //в uk1 адрес переменной b */
k=3;
b=uk1[k]; // переменной b присваивается значение int, взятое из
// адреса на 6 большего, чем адрес переменной b; в uk1
// адрес не изменился
uk1[k]=-14; // в адрес на 6 больший, чем адрес переменной b,
// посылается -14
Операция индексации не изменяет значение указателя, к которому она
применялась.

8. Увеличение/уменьшение. Если к указателю применяется операция


увеличения ++ или уменьшения --, то значение указателя увеличивается или
уменьшается на размер объекта, который он адресует:

long b; // b – переменная типа int длиной 4 байта


long *ptr; // ptr – указатель на объект int длиной 4 байта
ptr=&b; // в ptr адрес переменной b
ptr++; // в ptr адрес увеличился на 4
ptr--; //ptr адрес уменьшился на 4
9. Сложение. Одним из операндов операции сложения может быть указатель,
а другим операндом обязательно должно быть выражение целого типа. Операция
сложения вырабатывает адрес, который определяется следующим образом: (адрес в
указателе) + (значение int_выражения)*sizeof(<тип>), где <тип> это тип данных,
на которые ссылается указатель.
double d;
int n;
double *uk;
uk=&d; //в uk адрес переменной d */
12
n=3;
uk=uk+n; // в результате выполнения операции сложения, а затем
// операции присваивания,
// в uk новый адрес на 24 больше, чем предыдущий
uk=n+uk; // в uk адрес увеличился еще на 24
10. Вычитание. Левым операндом операции вычитания должен быть
указатель, а правым должно быть выражение целого типа. Операция вычитания
вырабатывает адрес, который определяется так: (адрес в указателе) - (значение
int_выражения)*sizeof(<тип>).
К указателям можно применять только описанные операции и операции,
которые выражаются через них, например, разрешается к указателю применить
операцию uk += n;, так как ее можно выразить через uk = uk+n; . Следующие
операции недопустимы с указателями:

- сложение двух указателей;


- вычитание двух указателей на различные объекты;
- сложение указателей с числом с плавающей точкой;
- вычитание из указателей числа с плавающей точкой;
- умножение указателей;
- деление указателей;
- поразрядные операции и операции сдвига;
7.1.7. Ссылки
В языке С++ введен тип данных, которого нет в С, – ссылка. Ссылка
позволяет определить еще одно имя для переменной, синоним (eng. alias).
Формат объявления ссылки выглядит следующим образом:

<тип> &<идентификатор1> = <идентификатор2>;

Ссылка при объявлении всегда должна быть проинициализирована, после


этого её нельзя будет изменить. Например:
int c, a, b = 5;
int &ref_a = a; // ref_a является ссылкой на a
ref_a = b; // a = b, т.е. a = 5
ref_a++; // a = 6, ref_a = 6;
c = ref_a; // c = 6;
int *p = &ref_a; // p инициализируется адресом a
int &ir = 1024; // ошибка: не адрес, только ссылка на постоянный объект
// может быть проинициализирована таким значением как 1024
const int &ir = 1024; // верно: ссылка на константный объект
const int &ar = a + b; // верно: ссылка на константный объект
int const &ar = a; // const (константная ссылка) здесь
//избыточно, т.к. ссылка и так не может меняться
double d[10];
double &rd9 = d[9]; // имя(псевдоним) элемента d[9]

13
Ссылку можно рассматривать как постоянный указатель, который всегда
разадресован и для которого не надо выполнять операцию разадресации (*).
Подчеркнем, что ссылка не создает копии объекта, а является именем объекта.
Т.е. выражение &ref_a == &a всегда истинно.
Из этого правила имеется два исключения, когда при объявлении ссылки
создается копия во временной памяти.
1. При объявлении ссылки на константу. При этом создается внутренняя
переменная, ей присваивается константа, а затем ссылка инициализируется этой
переменной.
Например, есть объявление:

char &ref = ‘\0’;


компилятор создаст код:

char temp = ‘\0’;


char &ref = temp;
Это делается по соображениям безопасности. Компилятор оптимизирует
размещение одинаковых констант, размещая их одной области памяти. Поэтому
выполнение кода:

char &ref = ‘\0’;


ref = ‘\t’;
char n = ‘\0’;
могло бы привести к тому, что переменная n получила бы значение ‘\t’. Более
того, ‘\0’ в программе была бы заменена на ‘\t’.

2. При инициализации ссылки переменной другого типа. Например,


объявление:
unsigned int ui = 77;
int &r_ui = ui;
интерпретируется следующим образом:

int t = (int)ui;
int &r_ui = t;
Когда при объявлении ссылки создается копия объекта во временной памяти,
компилятор выдает соответствующее предупреждение. В случае выдачи
сообщения об ошибке положение можно исправить следующим образом:

const int &r_ui = ui; //ссылку на константный объект можно


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

14
объектам пользовательских типов (классов). Передача по значению объектов,
возможно больших, в качестве операндов вызовет лишние затраты на их
копирование. Кроме того, передача операндов с использованием указателей,
приводит к необходимости взятия адресов операндов в выражениях.
Домашнее задание. Определите данные:
int a, b, c;
int *pa = &a, *pb = &b, *pc = &c;
int x = 75;
int &refx = x;
printf ("a %p a %d \nb %p b %d \nc %p c %d \n\n",
pa,pa,pb,pb,pc,pc);
printf ("a %p a %d \nb %p b %d \nc %p c %d \n\n",
&a,&a,&b,&b,&c,&c);

printf("x %p refx %p\n", &x, &refx);


printf("x %d refx %d\n", x, refx);
a 0012FF50 a 1245008
b 0012FF4C b 1245004
c 0012FF48 c 1245000 (второй printf тоже самое)

x 0012FF44 refx 0012FF44


x 75 refx 75

7.2. Массивы
Как и в большинстве других языков программирования, в Си под массивом
понимается совокупность однородных данных, рассматриваемых как нечто единое.
Математическими аналогами программистского понятия массив является,
например, матрица, которая в программе задается в виде двухмерного массива, или
вектор, задаваемый одномерным массивом. Массив – это такой объект программы,
который характеризуется именем, размерностью, количеством элементов по
каждому измерению и типом значений его элементов.
7.2.1. Описание массивов
В программе на Си описание массивов осуществляется оператором, имеющим
следующую общую форму записи:

<тип> <имя>[<размер1>][<размер2>]...[<размерN>];

где <тип> определяет тип элементов массива. Все элементы массива однородны и
могут принимать значения одного из любого допустимого в Си типа данных, за
исключением функций и файлов. Чаще всего в качестве <тип> используются
ключевые слова, такие, как int, float, char и другие ключевые слова и
словосочетания, задающие основные и производные типы данных, например,
указатели, структуры и т.д.; <имя> – это <идентификатор>, задающийся по
усмотрению программиста и являющийся именем массива. Имя массива
используется для доступа к элементам массива; <размер> определяет количество

15
элементов массива по каждому измерению и записывается целой беззнаковой
константой. Количество конструкций [<размер>] определяет размерность массива.
При описании одномерного массива указывается только <размер1>, двухмерного
массива <размер1> и <размер2> и т.п. для многомерных массивов. Многомерные
массивы рассматриваются в Си как одномерные, элементами которых могут быть
любые объекты, в том числе и массивы.
Примеры описания массивов:

int v[120]; v – одномерный массив из 120-ти целых чисел;


float mt[7][9]; mt – двумерный массив из 63-х (7*9) чисел с
плавающей точкой;
char str[25]; str – массив из 25-ти символов;
char *u[10]; u – массив указателей на объекты типа char;
int *pr[2][3]; pr – двухмерный массив из 6-ти указателей на
целые данные;
int (*r)[5][6]; r – указатель на данные целого типа,
сгруппированные в массивы 5*6; обратите внимание, что
r в данном случае не является массивом.
Доступ к элементу массива обеспечивается использованием имени массива,
вслед за которым в квадратных скобках записываются координаты элемента
(индексы) по соответствующим измерениям. Индексы однозначно определяют
положение элемента в массиве. Нижняя граница индекса по каждому измерению
массива имеет значение 0, а верхняя имеет значение <размер>-1. В качестве
индекса выступают выражения, значения которых приводятся к целому типу.
Рассмотрим фрагмент программы:
int vect[10];
int k;
for(k=0; k<=9; k++) vect[k]=k*k;
В этом фрагменте описан одномерный массив с именем vect, содержащий 10
элементов типа int, первый элемент массива имеет индекс 0, второй – 1, третий –
2 и т.д., последний элемент имеет индекс 9. Доступ к элементам массива
осуществляется путем использования вышеописанной языковой конструкции
vect[k], которая называется индексированной переменной, или переменной с
индексом. Здесь уместно обратить внимание на то, что сами по себе массивы
представляют векторные данные, но индексированная переменная есть скаляр. В
результате выполнения этого фрагмента элементу vect[0] будет присвоено
значение 0, элементу vect[1] значение 1, vect[2] значение 4 и т.д., до
последнего элемента массива vect[9], которому будет присвоено значение 81.
Рассмотрим, каким образом определяются координаты элементов в многомерных
массивах. Многомерные массивы Си трактуются как одномерные массивы,
элементами которых, в свою очередь, являются массивы. С этой точки зрения
препарируем следующее описание массива: int m[2][3][2]; .

16
Рассуждаем так: константа 2 в первых квадратных скобках определяет
базовый одномерный массив m из двух элементов m[0] и m[1]. Вот они:

Константа 3 в следующих квадратных скобках говорит о том, что каждый из


этих элементов состоит, в свою очередь, из трех элементов:

Элементы, которые находятся в m[0], это элементы m[o][0], m[0][1], m[0][2], а


элементы, которые содержатся в m[1], это элементы m[1][0], m[1][1] и m[1][2].
Константа 2 в последних квадратных скобках говорит о том, что каждый из
предыдущих элементов состоит, в свою очередь, из двух элементов типа int:

Таким образом, элементы трехмерного массива отображаются в линейной


памяти одномерным массивом, доступ к элементам которого осуществляется путем
использования индексированной переменной с тремя индексами. В данном случае
элементы массива расположены в памяти в следующей последовательности: m[0]
[0][0], m[0][0][1], m[0][1][0], m[0][1][1], m[0][2][0], m[0][2][1], m[1][0][0], m[1][0]
[1], m[1][1][0], m[1][1][1], m[1][2][0], m[1][2][1]. Чтобы обратиться к элементу n-
мерного массива, нужно использовать индексированную переменную с n
индексами, например, оператор m[1][2][1]=15; присваивает значение 15
последнему элементу рассмотренного массива, а оператор m[0][0][1]=m[1][2][1];
переприсваивает это значение второму элементу. Для случая двухмерных массивов
полная аналогия с матрицами устанавливается следующим образом: при описании
массивов первая константа в квадратных скобках задает количество строк, а вторая
– количество столбцов матрицы; для индексированной переменной первый индекс
определяет номер столбца, а второй – номер строки, на пересечении которых
находится элемент матрицы.

7.2.2. Инициализация массивов


Нередко возникает необходимость в том, чтобы перед началом выполнения
программы элементы массива уже имели определенные значения. В этом случае,
как и для переменных, имеется возможность при описании массива произвести
инициализацию его элементов начальными значениями, например, так:
int num[8]={1,3,5,7,9,11,13,9}; .

17
Здесь описан массив данных целого типа с именем num. Список
устанавливаемых значений заключается в фигурные скобки. Во время компиляции
программы элементы массива приобретут следующие значения:
num[0]=1; num[1]=3; num[2]=5; num[3]=7;
num[4]=9; num[5]=11; num[6]=13; num[7]=9;
При описании массивов с инициализацией элементов не обязательно
указывать размеры массива. Компилятор сам определит количество элементов и
выделит для них память соответствующего размера, например:
int num[]={1,3,5,7,9,11,13,9};
Приведем еще несколько примеров инициализации массивов:
float vector[4]={1.2,34.57,81.9,100.77}; – здесь все ясно;

int digit[5]={1,2,3}; – недостающие при инициализации значения


принимаются равными нулю (для массивов
char равными нуль-символу );
char chr[7]={'A','Б','В','Г','Д'}; - первые пять элементов
массива приняли значение соответствующих
элементов списка инициализации, а последние два
элемента – значение нуль-символа;
float const mt[4]={25,26,17,18}; – инициализация массива
констант типа float.
Рассмотрим, как инициализируются многомерные массивы.
Инициализация матрицы:
int mat[3][3]={
{1,2,3},
{4,5,6},
{7,8,9}
};
Инициализация матрицы происходит по строкам в порядке запоминания
элементов в памяти и в данном случае элемент матрицы, например mat[2][1]
получит значение 8. При инициализации данного массива, значения элементов
каждой строки матрицы заключены в фигурные скобки, что отражает сущность
выражения многомерных массивов через одномерные. Если в этом примере
опустить внутренние скобки, то результат будет тем же. Другое дело, если бы в
задаваемых нами внутренних фигурных скобках не хватало значений для
заполнения соответствующих элементов строк матрицы. В этом случае, если
каждая строка значений заключена в отдельные фигурные скобки, элементам
строки матрицы, для которых не хватило значений, присваивается 0. Если же
внутренние фигурные скобки отсутствуют, то элементам массива последовательно,
в порядке их расположения в памяти, будут присваиваться значения, выбираемые
из списка инициализации. Элементы массива, для которых в списке инициализации
не хватило значений, получат нулевое значение. Если в списке значений больше,
18
чем элементов массива, то такая запись считается ошибочной. Все вышеуказанное
справедливо для массива любой размерности. При инициализации многомерных
массивов, необходимо учитывать, что чем правее индекс, тем "быстрее" он
изменяется, – это следует из рассмотренного способа распределения памяти для
массивов.
Инициализация двухмерного массива символов:
char chr[3][5]={
{'A','Б','В','Г','Д'},
{'а','б','в','г','д'},
{'A','B','C','D','E'}
};
Три равносильных способа инициализации трехмерного массива:
int p[3][2][2]={
{ {1,2},{3,4} },
{ {5,6},{7,8} },
{ {9,10},{11,12} }
};
int p[3][2][2]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int p[3][2][2]={1,2,3,4,5,6,7,8,9,10,11,12};

//если мы "забудем" в первой строчке 2 числа:

int p[3][2][3] = {
{ 1, 2, 3, 4},
{ 7, 8, 9, 10, 11, 12},
{13, 14, 15, 16, 17, 18}
};
//то эквивалентное объявление "в одну строчку" будет следующим:

int p[3][2][3] =
{1,2,3,4,0,0,7,8,9,10,11,12,13,14,15,16,17,18};

Динамическое создание массивов. Динамическое создание (выделение


памяти под хранение) одномерных массивов рассматривалось в разделе
7.1.5. Инициализация указателей. Сейчас рассмотрим как создать двумерный
массив в динамической памяти. Двумерный динамический массив может быть
создан как массив указателей на массивы строк, при этом память, как под массив
указателей, так и под массивы строк должна быть выделена динамически.

Пример для языка С:


int N = 3; //число строк
19
int M = 4; //число столбцов
int **A = (int **)malloc(N*sizeof(int *));
for(int i = 0; i < N; i++)
{
A[i] = (int *)malloc(M*sizeof(int));
}

Пример для языка С++:


int n = 3; //число строк
int m = 4; //число столбцов
int **a; //указатель на указатель на данные типа int (будет указывать на массив)
//Динамическое выделение памяти под матрицу:
a = new int* [n]; //1.создаём массив указателей на указатели на данные типа int
//2. теперь выделяем память (построчно) собственно под матрицу
for(i = 0; i < n; i++)
a[i] = new double[m];
// Освобождение динамической памяти:
//1. вначале освободим память, отведённую собственно под матрицу, т.е. под данные
for(i = 0; i < n; i++)
delete [] a[i];
delete [] a; //2. теперь освобождаем память, занятую массивом указателей на строки
Как видим, вначале выделяется память под массив указателей, затем под
данные. Освобождение памяти делается в обратном порядке: вначале освобождают
память, занятую данными матрицы, а затем — массивом указателей.
Когда память выделена, действия с элементами динамической матрицы
выполняются точно так же, как и с обычной матрицей, т.е. везде, где это
необходимо, используем обычную форму обращения к элементу матрицы a[i][j].
Как эти действия можно оформить в виде функций см. в разделе
9.4.6. Массивы в качестве аргументов.
7.2.3. Доступ к элементам массива
Рассмотрим способы, на основе которых программист может реализовать
разнообразные языковые конструкции, обеспечивающие доступ к элементам
массива.
1. Использование индексированной переменной. Это наиболее естественный,
простой и понятный способ доступа к элементам массива любой размерности. О
доступе к элементам массива путем использования индексированной переменной
уже говорилось в разделе 7.2.1. и поэтому повторяться не будем, а рассмотрим
пример:
int i,j; /* индексные переменные */
int N, m[3][6]={ { 1, 2, 3, 4, 5, 6 },
{ 7, 8, 9, 10, 11, 12 },
{ 13, 14, 15, 16, 17, 18 }
};
i=2; j=4;
N=m[i][j]; /* N получит значение элемента, стоящего на */

20
/* пересечении 2-ой строки и 4-го столбца */
N=m[i-2][2*j-3]; /* N получит значение элемента нулевой строки и */
/*5-го столбца */
N=m[0][0];
m[0][0]=m[2][5];
m[2][5]=N;
/* m[0][0] и m[2][5] - первый и последний элементы матрицы */
/* обменялись значениями */
/* печать матрицы в один столбец: */
for(i=0; i<=2; i++)
for(j=0; j<=5; j++)
printf("%d\n", m[i][j]);
/* печать матрицы по строкам: */
for(i=0; i<=2; i++)
printf("%3d%3d%3d%3d%3d%3d\n", m[i][0],m[i][1],m[i]
[2],m[i][3],m[i][4],m[i][5]);
2. Использование имени одномерного массива. Этот способ доступа к
элементам массива основан на том, что в Си имя одномерного массива трактуется
как константа-указатель на данные, тип которых определен типом элементов
массива, а значением этой константы является адрес нулевого элемента.
Следовательно, применив к имени массива операции адресной арифметики, можно
получить доступ к любому элементу массива. Поэтому, например, для
одномеpного массива, описанного как int mas[15];, выражения Си mas и
&mas[0], mas+1 и &mas[1], mas+2 и &mas[2] являются равносильными.

Чтобы получить значение первого элемента массива, можно записать *mas,


используя операцию косвенной адресации. Правила адресной арифметики и тот
факт, что имя массива является константой типа указатель, дают возможность
получить значение любого элемента одномерного массива, прибавляя
соответствующее число к имени массива:
*(mas+0) – значение элемента mas[0],
*(mas+1) – значение элемента mas[1],
*(mas+2) – значение элемента mas[2],
......................................
*(mas+10) – значение элемента mas[10],
*(mas+11) – значение элемента mas[11] и т.д.
Таким способом мы получаем доступ к элементам массива путем
использования адреса элемента. Здесь необходимо обратить внимание на роль
скобок, например, *(mas+1) – это второй элемент массива mas, а *mas+1 – это
прибавление единицы к первому элементу массива mas. Использованный в
исходной программе способ обращения к элементу массива на самом деле
заменяется компилятором на обращение через указатель – имя массива. Отсюда
становится понятным, почему в Си нумерация индексов массива начинается с нуля.
3. Доступ к элементам многомерных массивов. Как уже известно,
многомерные массивы трактуются как одномерные, элементами которых могут
быть любые объекты, в том числе и массивы. Когда в разделе 7.2.1. мы
21
рассматривали распределение памяти под трехмерный массив int m[2][3][2],
то употребляли имя массива с одним индексом, например, m[0] и m[1], а также с
двумя индексами, например, m[1][0], и с тремя индексами, например m[0][2]
[1]. Кроме того, мы установили, что доступ к элементу n-мерного массива может
быть осуществлен путем использования индексированной переменной с n
индексами. Таким образом, индексированная переменная m[0][2][1]
представляет собой значение соответствующего элемента массива m. Что же тогда
представляют собой имя массива – m или, допустим, конструкции вида m[1] и
m[1][0]? Си дает на это однозначный ответ – это константы-указатели. А раз так,
то теперь нам должно быть понятно следующее: то, что ранее мы назвали
индексированной переменной, есть не что иное, как применение операции
индексации к указателю. Приводимое далее описание трехмерного массива с
инициализацией элементов и комментарии к нему дают представление о
возможностях доступа к элементам через константы-указатели:

int m[2][3][2] = /* Имя массива m – указатель на начало массива. */


/* Индексация имени массива m дает m[0] – указатель на эти массивы ---> */
{
/* Индексация указателя m[0] дает: */
/* m[0][0] – указатель на int ---> */{ 10, 11 },
/* m[0][1] – указатель на int ---> */{ 12, 13 },
/* m[0][2] – указатель на int ---> */{ 14, 15 }
},
/* Индексация имени массива m дает m[1] – указатель на эти массивы ---> */
{
/* Индексация указателя m[1] дает: */
/* m[1][0] – указатель на int --> */{ 16, 17 },
/* m[1][1] – указатель на int --> */{ 18, 19 },
/* m[1][2] – указатель на int --> */{ 20, 21 }
};
Нам уже известно, что применение операции индексации к указателю на
данное дает значение данного. Следовательно, например, m[1][0][1] – то, что
мы назвали индексированной переменной, обеспечивает доступ к элементу массива
и дает значение 17, так как m[1][0] является указателем на данное int. Из всего
вышесказанного ясно, что доступ к элементам многомерных массивов можно
обеспечить не только через индексированную переменную, но и через любую
константу-указатель в массиве.
Например, если объявлен массив:
int m[2][3]={
{1,2,3},
{4,5,6}
};
то:
*(m[0]+0) – значение элемента m[0][0], равное 1,
*(m[0]+1) – значение элемента m[0][1], равное 2,
22
*(m[0]+2) – значение элемента m[0][2], равное 3,
*(m[1]+0) – значение элемента m[1][0], равное 4,
*(m[1]+1) – значение элемента m[1][1], равное 5,
*(m[1]+2) – значение элемента m[1][2], равное 6,
Рассмотрим пример программы, распечатывающей элементы массива
различными способами:

#include <stdio.h>
main()
{ int m[3][6]={ { 1, 2, 3, 4, 5, 6},
{ 7, 8, 9,10,11,12},
{13,14,15,16,17,18}
};
int i,j;
/* Доступ к элементам через указатели m[0], m[1], m[2] */
for(i=0; i<=2; i++)
for(j=0; j<=5; j++) printf("%d \n",*(m[i]+j));
/* Доступ к элементам массива через указатель m[0] */
for(i=0; i<=17; i++) printf("%d \n",*(m[0]+i));
/* Доступ к элементам через указатель m */
for(i=0; i<=17; i++) printf("%d \n",*(*m+i));
}
4. Доступ через переменную указатель. К элементам массива можно
обратиться при помощи вспомогательного указателя. Для этого необходимо
описать указатель с типом, соответствующим типу массива, и присвоить ему
значение имени массива, которое является константой типа указатель. Теперь с
указателем можно манипулировать как с переменной, ссылающейся на элементы
массива, над ней можно выполнять любые действия, допустимые в адресной
арифметике. При этом необходимо учитывать принятый порядок расположения
элементов массива в памяти компьютера:
int arr[2][3]={1,2,3,4,5,6};
int *uk;
int i;
uk=arr; // uk указывает на начало массива arr
uk++; // теперь uk указывает на arr[0][1]
i=*uk; // i получило значение 2
uk=uk+2; // uk указывает на arr[1][2]
i=*uk; // i получило значение 4
uk=arr[0]; // uk снова указывает на начало массива
uk=uk+1;
i=*uk; // i приняло значение 2
i=uk[1]; // i приняло значение 3
i=uk[3]; // i приняло значение 5
i=uk[0]; // i приняло значение 2

23
При доступе к элементам массива при помощи указателя надо учитывать , что
указатель "передвигается" по данным в той последовательности, в какой они
расположены в памяти. Если, увеличивая указатель, мы выйдем за пределы данных
массива, то он будет указывать на неопределенные данные. Для того, чтобы
обращаться к массиву при помощи указателя точно так же, как и при помощи
имени массива, необходимо соответствующим образом описать указатель.
Например, объявлен массив:
int arr[2][3]={1,2,3,4,5,6}; ,
который можно рассматривать как одномерный массив, элементами которого
являются одномерные массивы, содержащие по 3 элемента, т.е. arr[0] и arr[1]
являются указателями на соответствующие трехэлементные массивы. Теперь, если
описать указатель на трехэлементные массивы:
int (*ukt)[3];
то, используя ukt, можно обращаться к элементам массива arr по индексам:
ukt=arr; // ukt[0] – указывает на 1 строку матрицы
// ukt[1] – указывает на 2 строку матрицы
i=ukt[0][1]; // i приняло значение 2, ибо arr[0][1] равносильно
// ukt[0][1],
// что равносильно *(ukt[0]+1), которое дает значение 2
i=ukt[0][0]; // i приняло значение равное 1
ukt=ukt+1; // для arr присваивание было бы недопустимо
// ukt[0] сейчас указывает на 1 строку
i=ukt[0][0]; // i приняло значение, равное 4
Таким образом, любое действие, которое достигается с помощью индексации
массива, достигается и с помощью указателей на него. Тесная взаимосвязь
указателей, массивов и адресной арифметики представляет собой одно из
достоинств языка Си.
Замечание. Т.к. операция индексации массива является бинарной операцией,
в которой участвуют адрес массива и индекс, указывающий на смещение от начала
массива (первый элемент имеет смещение 0), то возможны такие индексации
массива:
int a[] = { 1, 2, 3, 4, 5, 6 };
std::cout << (1 + 3)[a] - a[0] + (a + 1)[2] << std::endl;
//Ответ: 8
//(1 + 3)[a] == a[1+3] (идентичные выражения)
// а – адрес, (a + 1)[2] a + 1- адрес, [2] - индексация

В случае двумерных массивов пока не записаны все индексы – извлекаются


адреса элементов, а не значения.
int a[2][2] = { 1, 2, 3, 4 };
24
// a - адрес
// a[1] - адрес
// a[1][1] – значение (4)

7.2.4. Массивы символьных строк


Как уже отмечалось ранее, символьная строка в Си есть не что иное, как
массив символов, последним символом которого является нуль-символ – признак
конца строки. Поэтому если нам нужно описать массив, содержащий строку,
например, "строка символов", то это можно сделать обычным способом:

char mas1[] = {'с','т','р','о','к','а',' ','с','и','м', 'в','о','л','о','в','\0'};


Последним элементом массива записан нуль-символ. Если его опустить, то
получится обыкновенный массив символов, а не строка. Это же объявление можно
сделать более коротким способом:
char mas1[]="строка символов";
или
char *mas2="строка символов";
В конце строк будет находится нуль-символ. Имя массива mas1 и указатель
mas2 ссылаются на первый элемент строки. Доступ к элементам строки такой же ,
как и доступ к элементам массива. Здесь следует обратить внимание на то, что
mas1 является константой (адресом первого элемента строки), а mas2 –
переменной, содержащей адрес первого элемента. Это предопределяет различные
способы доступа к элементам массива, что уже рассматривалось ранее. Размер
массива можно задавать и явно:
char mas1[16]="строка символов";
Для нуль-символа также надо зарезервировать место в массиве.
Если указать размер массива, превышающий длину строки, то
неиспользованные элементы массива заполняются нуль-символами ('\0'). При
обработке нескольких строк удобно организовать для их хранения массив
символьных строк, например:

char m[4][11]={ "строка",


"string",
"characters",
"символы"
};
В этом случае двухмерный массив m содержит четыре символьных строки по
11 символов в каждой, включая нуль-символ. Элемент m[0][0] имеет значение
символа 'c', элемент m[0][1] – значение символа 'т', элемент m[0][5] –
значение символа 'a', элементы m[0][6], m[0][7], m[0][8], m[0][9] и
m[0][10] имеют значение нуль-символа – '\0' и т.д. Как уже неоднократно
25
утверждалось, двухмерный массив можно трактовать как одномерный массив,
элементами которого являются другие одномерные массивы. Таким образом, m[0]
имеет смысл указателя на массив из 11 элементов, т.е. m[0] указывает на первую
строку, m[1] – на вторую и т.д. Следовательно, обращаться ко всей строке массива
надо при помощи соответствующих указателей, поэтому нередко имеет смысл
объявить не массив строк, а массив указателей на символьные строки. Используя
указатели, массив символьных строк, рассмотренный в предыдущем примере,
можно организовать следующим образом:

char *mas1[4]={ "строка",


"string",
"characters",
"символы"
};
В этом описании mas1 является массивом из 4-х указателей на символьные
строки. Каждый указатель инициализируется адресом соответствующей строки,
хотя в списке инициализации указаны сами строки. Адрес строки – это адрес ее
первого символа. Таким образом, указатель mas1[0] ссылается на "строка", а
указатель mas1[3] – на "символы". В отличие от массива символьных строк,
здесь строки не дополняются нуль-символами для достижения одинаковой длины,
а занимают столько места в памяти, сколько в них символов плюс нуль-символ.

7.2.5. Приeмы обработки массива


Рассмотрим приемы обработки массивов на примерах программ сортировки
числовых и текстовых (строковых) данных методом пузырька. Это один из
простейших методов сортировки, название которого происходит из того факта, что
бóльшие (меньшие) по значению элементы массива как бы всплывают вверх по
массиву, подобно пузырькам газа в воде. Суть алгоритма заключается в
следующем: начиная с первого, сравнивается каждая пара соседних элементов
массива. Если предыдущий элемент больше последующего и массив
упорядочивается по возрастанию значений, то они меняются местами. Таким
образом, при первом попарном просмотре элементов массива и возможных при
этом перестановках, самый большой элемент попадает на последнее место в
массиве.
При следующем просмотре массива этот элемент не участвует в сравнении,
так как он уже находится на своем месте. При каждом последующем просмотре
больший из оставшихся элементов "всплывает", занимая свое место в массиве. На
последнем просмотре остается сравнить только первые два элемента в массиве и,
если необходимо, поменять их местами.

Пример 1. Упорядочивание числового массива по возрастанию значений


элементов:
#include <stdio.h>
#define razmer 12
26
main() {
int s,p,i;
int massiv[razmer]={11,10,9,8,7,6,5,4,3,2,1,0};
for (p=razmer; p>=2; p--)
fоr (i=0; i<p-1; i++)
if ( massiv[i] > massiv[i+1]) //Доступ к элементам
{ s=massiv[i+1]; // через переменную
massiv[i+1]=massiv[i]; // с индексом.
massiv[i]=s;
};
for(i=0; i<razmer; i++) // Печать отсортированного массива
printf("%d ",*(massiv+i)); //Доступ через имя массива.
}
Результат работы программы:

0 1 2 3 4 5 6 7 8 9 10 11

Комментарий к программе. Оператор #define razmer 12 является


директивой препроцессора и означает, что везде в программе текст razmer будет
заменен на 12. Это делается для повышения наглядности программы и удобства ее
модификации. В программе объявлены три переменные: i – для индексации
массива, p – определяет количество просмотров массива, s – промежуточная
переменная для обмена значениями элементов массива. Объявлен и инициирован
массив massiv размерностью razmer. Первый цикл for с параметром цикла р
определяет число проходов, необходимых для сортировки массива. Второй цикл
for с параметром цикла i организует сравнение и перестановку элементов
массива. Обратите внимание, что при каждом новом проходе, который стартуется
первым циклом с переменной р, уменьшенной на единицу (р--), будет
просматриваться на 1 элемент меньше, чем в предыдущем проходе. Последний
цикл содержит в своем теле оператор printf, выводящий элементы массива на
печать.

Условие цикла i<razmer предписывает вывести на печать элементы


массива, имеющие индексы от 0 до 11, т.е. весь массив. Воспользовавшись
правилами адресной арифметики и тем, что имя массива является константой типа
указатель, мы организовали вывод массива на печать не при помощи индексации, а
при помощи изменения указателя на элементы массива и операции косвенной
адресации.

В примере использованы два различных способа доступа к элементам


массива. В целом справедливо утверждение, что любое действие, которое
достигается с помощью индексации массива, достигается и с помощью указателей
на него.

Пример 2. Упорядочивание строк по алфавиту:


27
#include <stdio.h>
#define razmer 11
main()
{
int p,i;
char *s;

/* Инициализация массива указателей на строки */


char *str[razmer]={
"Чехов А. Ионыч",
"Толстой Л. Детство",
"Достоевский Ф. Братья Карамазовы",
"Толстой Л. Война и мир",
"Пушкин А. Метель",
"Куприн А. Яма",
"Тургенев И. Вешние воды",
"Островский А. Гроза",
"Бунин И. Темные аллеи",
"Гоголь Н. Ревизор",
"Набоков В. Лолита"
};

/* Упорядочивание указателей */
for (p=razmer; p>=2; p--)
for (i=0; i<p-1; i++)
if (*str[i] > *str[i+1])
{
s=str[i+1];
str[i+1]=str[i];
str[i]=s;
};
/* Вывод на печать строк, упорядоченных по алфавиту */
for (i=0; i<=razmer; i++)
printf("%s \n ",str[i]);
}

Результат работы программы:


Бунин И. Темные аллеи
Гоголь Н. Ревизор
Достоевский Ф. Братья Карамазовы
Куприн А. Яма
Набоков В. Лолита
Островский А. Гроза
Пушкин А. Метель
Толстой Л. Детство
Толстой Л. Война и мир
Тургенев И. Вешние воды
Чехов А. Ионыч.

При сортировке учитывается, что коды заглавных букв русского алфавита


расположены в возрастающей последовательности в соответствии с алфавитом, т.е.
справедливо, что 'A'<'Б', 'Б'<'В' и т.д. Алгоритм аналогичен предыдущему,
но обратите внимание, что перемещаются не сами строки, а лишь указатели на эти
28
строки. Такой прием повышает эффективность программы, так как нет
необходимости перезаписывать строки, достаточно лишь обменять значения
указателей. Для того, чтобы иметь возможность обменять значения указателей,
описана промежуточная переменная s типа указатель. Выражение *str[i]
предписывает извлечь символ, на который ссылается указатель, т.е. первый символ
i-ой строки. Заметим, что если бы массив был описан так:
char str[razmer][32]={
"Чехов А. Ионыч",
"Толстой Л. Детство",
"Достоевский Ф. Братья Карамазовы",
"Толстой Л. Война и мир",
"Пушкин А. Метель",
"Куприн А. Яма",
"Тургенев И. Вешние воды",
"Островский А. Гроза",
"Бунин И. Темные аллеи",
"Гоголь Н. Ревизор",
"Набоков В. Лолита"
};
то необходимо было бы объявить промежуточный массив на 32 символа, чтобы
осуществить действительную перестановку строк, производя при этом
посимвольную перезапись каждой строки.

7.3. Перечисления
Перечисление – это конструируемый тип данных, во время описания которого
задается имя типа данных и значения, которые могут принимать переменные этого
типа.
7.3.1. Описание перечисляемого типа
Описание перечисляемого типа данных осуществляется оператором вида:

enum <имя_типа> {<список_значений>}<список_переменных>;

где <имя_типа> задается идентификатором Си по усмотрению программиста;


<список_значений> задается в виде: <элемент1>, <элемент2>,...,<элементN>, где
любой из элементов есть либо <имя>, либо <имя> = <константное_выражение>.
В качестве <имя> используется идентификатор Си; <список_переменных> – это
имена переменных типа перечисление, отделенные друг от друга запятой.
Семантически описание перечисления задает имя типа, определяет
именованные константы и имена переменных, которые могут принимать значения
именованных констант. Значением каждой именованной константы из списка
значений является целое число. Переменные типа перечисление могут принимать
значения одной из именованных констант списка значений.

Константные выражения, если они используются, должны быть целыми и


могут быть отрицательными. Если константные выражения отсутствуют, то по

29
умолчанию первому имени списка значений соответствует значение 0, второму 1 и
т.д.
Использование в программе именованной константы равносильно
использованию ее значения. Явная инициализация именованной константы
константным выражением переопределяет последовательность значений, заданных
по умолчанию. Имя, следующее за переопределенным именем, принимает
значение, увеличенное на 1, если только его значение не задано явно другой
величиной. Список значений может содержать повторяющиеся значения имен, но
сами имена должны быть различны.

Список переменных в описании перечисляемого типа может быть опущен. В


этом случае описание задает лишь тип перечисление, имеющий заданное имя, и
список допустимых значений. Переменные в этом случае должны быть описаны
при помощи уже определенного перечисляемого типа. Этот способ описания
пользуется именем типа перечисления, иногда называемым тегом (ярлычком)
перечисления, которое ссылается на ранее описанный тип и использует его список
перечисления, например:
enum day {saturday,sunday=0,monday,
tuesday,wednesday,thursday,
friday} workday;
enum day nextday;
enum day today=monday;
В первом случае определяется тип перечисления day, объявляется
переменная перечисления workday. Именованные константы получают
следующие значения: saturday – 0 (по умолчанию), sanday – 0 (явно),
остальные имена получают значения от 1 до 5. Во втором случае объявлена
переменная nextday типа enum day. Так как тип enum day был ранее объявлен,
то достаточно лишь на него сослаться. В третьем случае объявлена переменная
today типа enum day, и она инициализирована значением monday из списка
значений. Можно объявлять и массивы перечисляемого типа, например:
enum day week[6];
Перечисляемый тип можно рассматривать как синоним целого типа.
Переменной перечисляемого типа может быть присвоено значение любого типа,
проверка типа не производится. Над переменными перечисляемого типа можно
производить любые операции, допустимые для переменных целого типа.
Использование перечисляемого типа данных в ряде случаев дает возможность
писать программу в терминах постановки задачи, а не в типах int.
7.3.2. Пример использования перечислений
Предположим, нам необходимо написать фрагмент программы, который
будет определять, образуют ли растворимую или нерастворимую в воде соль
заданные кислота и металл. Фрагмент программы будет иметь следующий вид:
main()
30
{
/* Описание переменных перечисляемого типа - met и acd */
enum metall {K,Na,Mg,Fe,Co,Zn,Cu,Pb,Ag} met;
/* met - переменная перечисляемого типа metall */
enum acid {HCl,HNO3,H2SO4,H3PO4,H2CO3} acd;
/* acd - переменная перечисляемого типа acid (кислота) */
if ((met < Pb) && (acd < H3PO4))
printf("Соль растворимая");
if (met > Pb)
printf("Соль нерастворимая");
else
if (met==Pb)
{
if (acd > HNO3)
printf("Соль нерастворимая");
else
if (acd == HNO3)
printf("Соль растворимая");
else
printf("Соль малорастворимая");
};
if ((met < Pb) && (acd >= H3PO4))
if (met > Mg)
printf("Соль нерастворимая");
else
printf("Соль растворимая");
...
}

Эту же программу можно было написать, применяя другие типы данных,


например, int. При попытке вывода данных типа enum на печать или экран
появятся не именованные константы Na, Mg и т.д., а соответственно числа 1, 2 и
т.д. Основная причина использования типа enum заключается, как уже отмечалось,
в возможности программирования в терминах постановки задачи и вследствие
этого – в улучшении наглядности и читаемости программ.

31

Вам также может понравиться