Использование прототипов функций и
Полных определений функций
В классическом стиле программирования на С++ вы определяете функцию, специфицируя имя и тип возвращаемого результата.
Например, вы определяете функцию swap следующим образом:
int swap();
Никакой информации о параметрах не передается: ни о количестве их, ни о типе. Классическое определение функции выглядит так:
int swap(a, b)
int *a, *b;
{
/* Тело функции */
}
Результат такого определения – отсутствие контроля над ошибками при использовании параметров, что приводит к появлению хитрых и труднообнаружимых ошибок.
Избегайте такого стиля.
Современный стиль включает использование прототипов функций (для описания функций) и списка параметров (для определения функций). Давайте переопределим swap, используя прототипы.
int swap (int *a, int *b);
Теперь при выполнении ваша программа получает всю необходимую информацию, требуемую для выполнения контроля над ошибками при любом обращении к swap. Теперь вы можете использовать простой формат и при определении функции:
int swap (int *a, int *b)
{
/* тело функции */
}
Современный стиль программирования повышает вероятность обнаружения ошибки, даже если вы не используете прототипов функций; если же вы используете прототипы, то компилятор автоматически отслеживает и обеспечивает соответствие описаний и определений.
Комментарии
Итак, вы хотите внести в программу некоторые пояснения, которые напомнили бы вам (или информировали кого-нибудь другого), что означает та или иная переменная, что дает та или иная функция или оператор и т.д. Эти пояснения носят название “комментарий” (comments). С++ также, как и большинство других языков программирования, позволяет вносить комментарии в программы.
Начало комментария обозначается комбинацией знаков слеш и звездочка (/*). После того, как компилятор обнаружит эту комбинацию знаков, он игнорирует участок программы вплоть до последовательности знаков */.
Комментарий может занимать несколько строк программы:
/* Это очень длинный комментарий,
занимающий более одной строки программы */
Проанализируйте еще раз расширенную версию RATIO.CPP и добавьте в нее комментарии.
Обзор структур данных
Основные типы данных мы рассмотрели в прошлой главе. К ним относятся числа – целые и с плавающей точкой, символы и их разновидности. Теперь мы поговорим о том, как использовать эти элементы для построения структур данных. Но сначала исследуем одно из важнейших понятий С++ – указатели.
Указатели
Большинство переменных, рассмотренных вами, в основном содержали данные, т.е. текущую информацию для работы вашей программы. Но иногда важнее знать место расположения данных, чем собственно их значение. Именно для этого и используются указатели.
Если вы имеете слабое представление о понятиях “адрес” и “память”, то вам просто необходимо ознакомиться с их кратким описанием, которое мы приводим ниже.
Итак, ваш компьютер содержит в своей памяти (часто называемой RAM – Random Access Memory – память произвольного доступа) вашу программу и совокупность данных. На самом нижнем уровне память вашего компьютера состоит из битов, мельчайших электронных схем которые могут “запомнить” (пока компьютер включен) одно из двух значений, обычно интерпретируемое как “0” и “1”.
Восемь бит группируются в один байт. Большим группам бит как правило, присваивается имя: обычно два байта составляют слово, четыре байта составляют длинное слово; для IBM PC шестнадцать байт составляют параграф.
Каждый байт в памяти вашего компьютера имеет собственный уникальный адрес, так же, как каждый дом на любой улице. Но, в отличие от большинства домов, последовательные байты имеют последовательные адреса: если данный байт имеет адрес N, то предыдущий байт имеет адрес N-1, а следующий – N+1.
Указатель – это переменная, содержащая адрес некоторых данных, а не их значение. Зачем это нужно?
Во-первых, мы можем использовать указатель места расположения различных данных и различных структур данных. Изменением адреса, содержащегося в указателе, вы можете манипулировать (создавать, считывать, изменять) информацией в различных ячейках. Это позволит вам, например, связать несколько зависимых структур данных с помощью одного указателя.
Во-вторых, использование указателей позволит вам создавать новые переменные в процессе выполнения программы. С++ позволяет вашей программе запрашивать некоторое количество памяти (в байтах), возвращая адреса, которые можно запомнить в указателе. Этот прием известен как динамическое распределение; используя его, ваша программа может приспосабливаться к любому объему памяти, в зависимости от того как много (или мало) памяти доступно вашему компьютеру.
В-третьих, вы можете использовать указатели для доступа к различным элементам структур данных, таким как массивы, строки или структуры. Указатель, в сущности, указывает место в памяти вашего компьютера (а используя смещение относительно начального адреса, можно указать целый сегмент памяти), в котором размещены те или иные данные. Индексируя указатель, вы получаете доступ к некоторой последовательности байтов. которая может представлять, например, массив или структуру.
Теперь вы, несомненно, убеждены в удобстве указателей. А как же их использовать в С++? Для начала вы должны их объявить.
Рассмотрим следующую программу:
void main()
{
int ivar, *iptr;
iptr = &ivar;
ivar = 421;
printf("Размещение ivar: %p\n", &ivar);
printf("Содержимое ivar: %d\n", ivar);
printf("Содержимое iptr: %p\n", iptr);
printf("Адресуемое значение; %d\n", *iptr);
}
В ней объявлены две переменные: ivar и iptr. Первая, ivar – это целая переменная, т.е. содержащая значение типа int. Вторая, iptr – это указатель на целую переменную, следовательно она содержит адрес значения типа int. Можно также сказать, что переменная iptr – это указатель, так как перед ее описанием стоит звездочка (*). В языке С++ эта звездочка называется косвенным оператором.
В основном, данная программа делает следующее:
– адрес переменной ivar присваивается iptr;
– целое значение 421 присваивается ivar.
Адресный оператор (&), как это было показано в предыдущей главе, позволяет получить адрес, по которому размещено значение переменной ivar.
Введя эту программу в свой компьютер и выполнив ее, вы получите следующий результат:
Размещение ivar: 166Е
Содержимое ivar: 421
Содержимое iptr: 166Е
Адресуемое значение: 421
Первые две строки указывают адрес и содержимое ivar. Третья представляет адрес, содержащийся в iptr. Как видите, это адрес переменной ivar, т.е. место в памяти, где ваша программа решила создать переменную с идентификатором ivar. В последней строке печатается то, что хранится по этому адресу – те же самые данные, которые уже присвоены ivar.
Заметим, что в третьем обращении к функции printf используется выражение iptr, содержимое которого есть адрес ivar. В последнем обращении к printf используется выражение *iptr, которое позволяет получить данные, хранящиеся по этому адресу.
Рассмотрим теперь небольшую вариацию предыдущей программы:
void main()
{
int ivar, *iptr;
iptr = &ivar;
*iptr = 421;
printf("Размещение ivar: %p\n", &ivar);
printf("Содержимое ivar: %d\n", ivar);
printf("Содержимое iptr: %p\n", iptr);
printf("Адресуемое значение: %d\n", *iptr);
}
В этой программе также адрес переменной ivar присваивается iptr, но вместо присваивания числа 421 переменной ivar, это значение присваивается по указателю *iptr. Каков результат? Точно такой же, как и в предыдущей программе. Почему? Потому что выражения *iptr и ivar суть одна и та же ячейка памяти – поэтому в этом случае оба оператора заносят значение 421 в одну и ту же ячейку памяти.
Динамическое распределение
Изменим еще раз нашу программу:
#include <alloc.h>
void main()
{
int *iptr;
iptr = (int*)malloc(sizeof(int));
*iptr = 421;
printf("Содержимое iptr; %p\n", iptr);
printf("Адресуемое значение: %d\n", *iptr);
}
Эта версия позволяет вполне обойтись без описания переменной ivar, которое непременно фигурировало в предыдущих примерах. Вместо адреса этой переменной iptr присваивается значение (тоже адрес некоторой ячейки памяти), возвращаемое некоторой функцией, которая называется malloc, и описана в библиотеке alloc.h (отсюда появление директивы #include в начале программы). Затем по этому адресу присваивается значение 421 и переменная *iptr вновь, как и в предыдущем примере, принимает значение 421.
Обратите внимание, что если вы теперь выполните программу, то получите иное значение iptr, чем раньше, но значение *iptr останется равным 421.
Разберем теперь, что же делает оператор
iptr = (int*)malloc(sizeof(int));
Разобьем его на три части:
– выражение sizeof(int) возвращает количество байтов, требуемое для хранения переменной типа int; для компилятора С++, работающего на IBM PC, это возвращаемое значение равно 2;
– функция malloc(num) резервирует num последовательных байтов доступной (свободной) памяти в компьютере, а затем возвращает начальный адрес размещения в памяти этой последовательности байтов.
– выражение (int*) указывает, что этот начальный адрес суть указатель на данные типа int. Выражение такого вида известно как выражение приведения типа (type casting).
В данном случае С++ не требует обязательного его применения. Но в связи с тем, что для других компиляторов С++ это выражение является обязательным, при его отсутствии вы получите сообщение об ошибке:
“Non-portable pointer assignment.”
(“Непереносимое в другие системы присваивание указателя.”)
Из соображений переносимости программного обеспечения лучше всегда предусматривайте явное приведение типов в своих программах.
Наконец, адрес, полученный с помощью функции malloc, запоминается в iptr. Таким образом, вами получена динамически созданная целая переменная к которой вы можете обращаться при помощи идентификатора *iptr.
Весь этот блок можно описать следующим образом: “выделить в памяти компьютера некоторый участок для переменной типа int, затем присвоить начальный адрес этого участка переменной iptr, являющейся указателем на переменную типа int”.
Необходимо ли все это? Да. Почему? Потому что без этого вас нет гарантии, что iptr указывает на свободный участок памяти. iptr будет содержать некоторое значение, которое вы будете использовать в качестве адреса, но вам не будет известно, не используется ли этот раздел памяти для других целей. Правило использования указателей простое: указатель всегда должен иметь адрес до своего использования в программе.
Иными словами, не присваивайте целое значение *iptr без предварительного присвоения адреса в iptr.
Указатели и функции
В прошлой главе мы объяснили, как объявить параметры функций. Возможно, теперь вам более понятна причина использования указателей в качестве формальных параметров функции, значения которых вы можете изменять.
Рассмотрим, например, следующую функцию:
void swap(int *a, int *b)
{
int temp;
temp = *a; *a = *b; *b =temp;
}
Эта функция swap (обмен) объявляет два формальных параметра a и b, как указатели на некие данные типа int. Это означает, что функция swap работает с адресами целых переменных (а не с их значениями). Поэтому будут обработаны данные, адреса которых переданы функции во время обращения к ней.
Далее представлена программа, вызывающая swap:
void main()
{
int i, j;
I = 421;
j = 53;
printf("До обращения: i=%4d, j=%4d\n", i, j);
swap(&i, &j);
printf("После обращения: i =%4d, j=%4d\n", i, j);
}
Вы видите, что эта программа действительно заменяет значение i на значение j (переставляет их местами). Заменим эту программу на аналогичную ей, раскрыв процедуру swap в теле программы:
void main()
{
int i, j;
int *a, *b, temp;
i = 421;
j = 53;
printf("До обработки: i = %4d j = %4d\n", i, j);
a = &i;
b = &I;
temp = *a; *a = *b; *b = temp;
printf("После обработки: I = %4d j = %4d\n",
i, j);
}
Эта программа, конечно, приводит к тому же результату, что и предыдущая, поскольку не отличается от нее. При вызове функции swap(&i, &j); значения двух фактических параметров (&i, &j) присваиваются двум формальным параметрам (a и b), обрабатываемым непосредственно операторами функции swap.
Адресная арифметика
Каким образом вам необходимо поступить, если вы хотите так модифицировать программу, чтобы iptr указывала на три переменных типа int вместо одной? Далее представлено одно из возможных решений:
#include <alloc.h>
void main()
{
#define NUMINTS 3
int *list, i;
list = (int*)calloc(NUMINTS, sizeof(int));
*list = 421;
*(list+1) = 53;
*(list+2) = 1806;
printf("Список адресов:");
for (i=0; i<NUMINTS; i++);
printf("%4p ", (list + i));
printf("\nСписок значений: ");
for (i=0; i<NUMINTS; i++)
printf("%4p ", *(list + i));
printf("\n");
}
Вместо функции malloc эта программа использует функцию calloc с двумя параметрами: первый показывает, для скольких элементов будет происходить резервирование памяти, второй – величину каждого элемента в байтах. После обращения к функции calloc, list указывает на участок памяти размером 6 (3*2) байта, достаточный для хранения переменных типа int.
Более подробно рассмотрим следующие три оператора. Первый оператор вам знаком – *list = 421;. Он означает: “запомнить 421 в переменной типа int, расположенной по адресу, хранящемуся в переменной list”.
Следующий *(list + 1) = 53; – особенно важен для понимания. На первый взгляд, его можно интерпретировать так: “запомнить 53 в переменной типа int, расположенной байтом дальше адреса, хранящегося в list”. Если это так, то вы окажетесь в середине предыдущей int-переменной (которая имеет длину 2 байта). Это, естественно, испортит ее значение.
Однако, ваш компилятор с С++ не сделает такой ошибки. Он “понимает”, что list – это указатель на тип int, и поэтому выражение list + 1 представляет собой адрес байта, определенного выражением list + (1*sizeof(int)), и, поэтому, значение 53 не испортит значения 421 (т.е. для этого значения будет выделена другая ячейка памяти).
Аналогично, (list + 2) = 1806; представляет адрес байта list + (2*sizeof(int)) и 1806 запоминается, не затрагивая два предыдущих значения.
В общем, ptr + i представляет адрес памяти, определяемый выражением ptr + (i*sizeof(int)).
Введите и выполните вышеописанную программу; на выходе вы получите следующее:
Список адресов: 066A 06AC 06AE
Список значений: 421 53 1806
Заметьте, что адреса различаются не на один байт, а на два, и все три значения хранятся отдельно.
Подведем итог. Если вы используете ptr, указатель на тип type, то выражение (ptr + 1) представляет адрес памяти (ptr + (1*sizeof(type)), где sizeof(type) возвращает количество байт, занимаемых переменной типа type.