4.3. Указатели и массивы
4.3.1. Понятие указателя. Адресная арифметика
В языке С++ существует два способа доступа к переменной: обращение к переменной по имени и использование механизма указателей.
Указатель-переменная (или просто указатель) – это переменная, предназначенная для хранения адреса в памяти.
Указатель-константа – это значение адреса оперативной памяти. В языке С++ определены две специальные операции для доступа к переменным через указатели: операция & и операция *. Результатом операции & является адрес объекта, к которому операция применяется. Например, &var1 дает адрес, по которому var1 хранится в памяти (точнее, адрес первого байта var1). Операция * - это операция обращения к содержимому памяти по адресу, хранимому в переменной-указателе или равному указателю-константе.
Признаком переменной-указателя для компилятора является наличие в описании переменной двух компонентов:
типа объекта данных, для доступа к которому используется указатель (т. е. на который ссылается указатель);
символа * перед именем переменной.
В совокупности тип и * воспринимаются компилятором как особый тип данных – «указатель на что-либо». Таким образом, описание
int var1, *ptr;
приводит к появлению переменной var1 и указателя-переменной ptr. Переменная var1 будет занимать два байта памяти. Указатель ptr имеет тип int* т. е. тип «указатель на целое». Место, выделяемое под такой тип компилятором, зависит от модели памяти. Указатели при их описании могут, как и обычные переменные, получать начальное значение. Например:
int var1, ptr1 = (int*)200, *ptr2 = &var1;
Здесь описаны две переменные-указатели ptr1 и ptr2; ptr1 получает начальное значение 200, а ptr2 в качестве начального значения – адрес, по которому в памяти хранится var1.
Операцию * можно выразить словами: «взять содержимое по адресу, равного значению указателя». Например, оператор присваивания
*ptr = *ptr2 + 4;
можно интерпретировать так: взять содержимое памяти по адресу, равному значению указателя ptr2, прибавить к этому содержимому 4, а результат поместить по адресу, равному значению указателя ptr1. Число байтов, извлекаемых из памяти и участвующих в операции, определяется компилятором исходя из типа, на который указывает указатель. Запись типа
*234 = var1;
будет ошибкой, т. к. в левой части оператора присваивания записывается адрес константы, а константа в С++ не имеет адреса в памяти.
Существуют ограничения и на использование операции взятия адреса &:
нельзя определить адрес константы, например некорректным является
выражение
var1 = &0xff00;
2) нельзя определить адрес значения, получаемого при выполнении арифметического выражения, включающего знаки +, -, /, * и т. п. Например, некорректным является выражение
int var1, *ptr;
ptr = &(var1*3);
3) нельзя определить адрес переменной, описанной как register. Например, будет ошибкой попытка определить адрес var1:
unsigned register var1;
unsigned int *ptr;
ptr = &var1;
Сам указатель-переменная тоже имеет адрес. Поэтому, например, корректным будет такой фрагмент:
int var1, *ptr1, *ptr2 = &var1;
ptr1 = (int*)&ptr2;
Здесь описываются два указателя-переменные и ptr2 инициализируется значением адреса переменной var1. Затем ptr1 присваивается значение адреса, по которому в памяти располагается ptr2.
Указатель типа void* называют часто родовым (generic). Ключевое слово void говорит об отсутствии данных о размере объекта в памяти. Но компилятору для корректной интерпретации ссылки на память через указатель нужна информация о числе байтов, участвующих в операции. Поэтому во всех случаях использования указателя, описанного как void*, необходимо выполнить операцию явного приведения типа указателя. Например:
unsigned long block = 0xffeeddccL;
void *ptr = █
char ch;
unsigned two_bytes;
long int four_bytes;
ch = *(char*) ptr; /* ch = 0xcc; */
two_bytes = *(unsigned*) ptr; /* two_bytes = 0xddcc; */
four_bytes = *(long int*) ptr; /* four_bytes = 0xffeeddcc; */
В комментариях приведены значения переменных, полученных в результате присваивания. Напомним, что младший байт всегда располагается в памяти по меньшему адресу.
Для указателей-переменных разрешены некоторые операции: присваивание; инкремент или декремент; сложение или вычитание; сравнение.
Язык С++ разрешает операцию сравнения указателей одинакового типа. При выполнении присваивания значение указателя в правой части выражения пересылается в ячейку памяти, отведенную для указателя в левой части.
Важной особенностью арифметических операций с указателями является то, что физическое увеличение или уменьшение его значения зависит от типа указателя, т. е. от размера того объекта, на который указатель ссылается. Если к указателю, описанному как type*ptr; прибавляется или отнимается константа N, значение ptr изменяется на N*sizeof(type). Разность двух указателей type*ptr1, *ptr2 – это разность их значений, поделенная на sizeof(type).
В частности, арифметические операции над указателями типа char* (размер типа равен 1) выполняются как над обычными целыми числами с той лишь разницей, что значения, участвующие в операции, - это адреса в оперативной памяти. Однако для других типов указателей это не так. Например:
#include <stdio. h>
void main(void)
{
int near*ptr1 = (int*)100;
int near*ptr2 = (int*)200;
ptr1 ++; ptr2 -= 10;
printf("ptr2 = %d, ptr1 = %d, ptr2 – ptr1 = %d\n", ptr2, ptr1, ptr2 – ptr1);
}
Так как указатель имеет тип int* (длина типа 2 байта), то «единица изменения» указателя и «единица измерения разности» равны двум байтам. Для других типов указателей такие же вычисления дают следующий результат:
для long* и float*
ptr2 = 160, ptr1 = 104, ptr2 – ptr1 = 14,
для double*
ptr2 = 120, ptr1 = 108, ptr2 – ptr1 = 1,
для long double*
ptr2 = 100, ptr1 = 110, ptr2 – ptr1 = -1.
Такие правила арифметических операций с указателями вытекают из того, что указатель в Си неявно рассматривается как указатель на начало массива однотипных элементов. Продвижение указателя вперед или назад совпадают с увеличением или уменьшением индекса элемента.
4.3.2. Массивы. Их связь с указателями.
Массивы – это один из примеров структурированного типа данных.
Массив – это расположенные вплотную друг за другом в памяти элементы одного и того же типа. Каждый массив имеет имя. Доступ к отдельным элементам массива осуществляется по имени массива и индексу (порядковому номеру) элемента.
Основные свойства массива:
все элементы массива имеют один и тот же тип;
все элементы массива расположены в памяти друг за другом; индекс первого равен 0;
имя массива является указателем-константой, равной адресу начала массива (первого байта первого элемента массива).
Признаком массива при описании является наличие парных скобок [ ]. Константа или константное выражение в квадратных скобках задает число элементов массива. Например:
char buffer[81]; int Key[4];
При описании массива может быть выполнена инициализация его элементов.
Существует два метода инициализации массивов:
1) инициализация по умолчанию; она применяется только к статическим и внешним массивам (о классах хранения и внешних переменных см. в 8.2 и 8.4); по умолчанию все элементы внешних и статических массивов инициализируются компилятором нулями;
2) явная инициализация элементов; после описания массива помещается список начальных значений элементов массива, заключенный в фигурные скобки. Существуют две формы явной инициализации элементов массива:
а) явное указание числа элементов массива и список начальных значений, возможно с меньшим числом элементов.
Например:
char array[10] = { ‘A’, ‘B’, ‘C’, ‘D’ };
Здесь описывается массив из 10 элементов. Первые 4 элемента массива инициализируются символами ‘A’, ‘B’, ‘C’, ‘D’. Значения остальных 6 элементов либо равно 0, если массив внешний или статический, либо не определено. Если список начальных значений содержит больше элементов, чем число в квадратных скобках, Borland C++ генерирует сообщение об ошибке;
б) только со списком начальных значений. Компилятор определяет число элементов массива по списку инициализации. Например:
char array[] = { ‘A’, ‘B’, ‘C’, ‘D’ };
В результате создается массив ровно из 4 элементов, и эти элементы получают начальные значения из списка инициализации.
Если при описании массива отсутствуют значение в квадратных скобках и список начальных значений, компилятор обычно регистрирует ошибку. Однако в тех контекстах, где не требуется резервирование места в памяти, этого не происходит:
если массив объявляется как внешний при условии, что в том месте, где массив описывается, приведена информация о числе его элементов. Например:
extern char array[];
в описании функции, если указатель на первый элемент массива является аргументом функции. Например:
int function (int input_array[], int index)
{ /* тело функции */ }
в прототипах функций, использующих в качестве аргумента указатель на первый элемент массива.
Доступ к отдельным элементам массива может выполняться либо с помощью индекса, либо операцией *. В первом случае для ссылки на нужный элемент указывается его порядковый номер в массиве, заключенный в квадратные скобки. Самый первый элемент массива имеет порядковый номер 0. Пример:
int a[] = { 1, 2, 3, 4, 5 };
int index = 0, first, last, bad;
first = a[index]; /* first = 1 */
last = a[4]; /* last = 5 */
bad = a[index + 6]; /* bad случайно */
В языке Си для повышения производительности программы не выполняются многие проверки корректности вычислений, в том числе и контроль доступности значений индекса массива. Поэтому в приведенном примере значение bad - это содержимое двух байтов памяти сразу после описанного массива а. Содержимое этих байтов случайно.
Другой способ доступа к элементам массива - использование механизма указателей. Так как имя массива - это указатель-константа на первый байт первого элемента массива, то, используя операцию *, можно выполнить доступ к любому из элементов массива. Будут полностью эквивалентными ссылки на i-й элемент массива array (независимо от типа элемента) array[i] и *(array + i), причем оба способа для Borland C++ дают практически одинаковый по производительности код. Следующие выражения являются тождествами:
array == &array[0],
(array + i) == &array[i].
5. ОПЕРАТОРЫ
5.1. Общие сведения
Операторы управления вычислительным процессом позволяют выполнять ветвление, циклическое повторение одного или нескольких операторов, передачу управления в нужное место кода программы. Под вычислительным процессом понимают процесс выполнения операторов программы. Операторы программы могут быть простыми или составными. Простой оператор - это оператор, не содержащий другие операторы. Разделителем простых операторов служит точка с запятой. Специальным случаем простого оператора является пустой оператор, состоящий из единственного символа ‘;’. Составной оператор, или блок,- это любая совокупность простых операторов, заключенная в фигурные скобки {}. Составной оператор идентичен простому оператору и может находиться в любом месте программы, где синтаксис языка допускает наличие оператора, но дополнительно влияет на так называемую видимость переменных.
5.2. Оператор if
Операторы ветвления выбирают в программе из группы альтернатив возможное продолжение вычислительного процесса. Выбор выполняется исходя из значения заданного выражения. В С++ используются два оператора ветвления: if...else и switch.
Оператор if имеет следующую общую форму записи:
if (cond_expression) TRUE_statement
[else FALSE_statement]
При выполнении оператора if сначала вычисляется логическое выражение cond_expression. Если результат - ИСТИНА (любое отличное от нуля значение), выполняется оператор TRUE_statement. Если результат логического выражения – ЛОЖЬ (равен 0), то выполняется оператор FALSE_statement. Если ключевое слово else отсутствует, то в этом случае оператор TRUE_statement пропускается, а управление передается на следующий после if оператор.
Операторы TRUE_statement и FALSE_statement сами могут быть операторами if, образуя так называемые вложенные if. Компилятор интерпретирует вложенные if, сопоставляя каждое из ключевых слов else с последним встретившимся словом if, не имеющим " своего" else. Соответствие ищется в пределах блока, в который заключено слово if. Внутренние и внешние блоки при этом не рассматриваются. Если соответствия для if не найдено, компилятор полагает, что if не имеет ветви else. Например,
int a, b; то же самое, но int a, b;
if(b>0) в первом операторе if(b>О)
а=1; if отсутствует else а=1;
else if(b==0)
if(b==0) a=0;
а=0; else
else a=-1;
а=-1;
В результате переменной а будет присваиваться значение -1, если b<0, +1, если b>0, и 0, если b=0. Но ошибочной будет следующая последовательность операторов:
int x=1, у=1;
if(x==1)
if(y==1) puts("х равно 1 и y равно 1);
else
puts("х не равно 1");
Строка" х не равно 1" будет выводиться тогда, когда значение х на самом деле равно 1. Ошибка происходит из-за того, что компилятор сопоставляет if с ближайшим else так, как это показано тонкой сплошной чертой. Ошибка может быть исправлена, если использовать фигурные скобки, ограничивающие блок:
int x=1, у=1;
if(x==1)
{
if(y==1) puts("х равно 1 и y равно 1);
}
else
puts("х не равно 1");
Операция условия?: является частным случаем оператора if...else. Например, фрагмент
if(cond_expression)
x=y;
else x=y+4;
эквивалентен более короткой записи:
х=(cond_expression)? y:y+4;
5.3. Операторы switch и break
Часто возникающая в программировании задача – выбор одного варианта из многих. Можно это сделать с помощью вложенных if...else. Однако более удобный способ - использование оператора switch, общий формат которого таков:
switch(switch expression)
{
сase constantl: statementl; [break;]
.......
саse constanti: statementi; [break;]
........
case constantN: statementN; [break;]
[default: statementN+1;]
}
Оператор switch выполняется так. Сначала вычисляется значение выражения switch_expression. Тип значения должен быть одним из целых - char, int, unsigned int, long int и long unsigned. Вычисленное значение сравнивается со значениями констант или константных выражений сonstant1, ..., сonstantN. При совпадении значения switch expression с constani выполняется оператор statementi. Затем управление передается на оператор сразу после switch, если в i-й ветви присутствует оператор break. В противном случае выполняются операторы в ветвях i+1, i+2 и так далее до тех пор, пока в них не встретится оператор break или не будет выполнен оператор statementN+1.
Если значение switch_expression не совпало ни с одной из констант constant1, ..., constantN, выполняется оператор в ветви, помеченной default. При ее отсутствии выполняется следующий после switch оператор.
Приводимая далее программа выводит на экран меню из трех функций: Sin, Соs, Atan; по первой введенной с клавиатуры букве распечатывается информация о функции:
//Prim5_1.cpp
#include <stdio. h>
void main(void)
{
рuts("введите первую букву имени функции:\n"\
" S-Sin\n С-Сos\n А-Atan\n");
switch( getchar() )
{
case's': case'S':
рuts("вычиcление синуса аргумента в радианах");
break;
case'c': case'C':
рuts ("Вычисление косинуса аргумента в радианах");
break;
case'a': case'A':
рuts("Вычисление тангенса аргумента в радианах");
break;
default: рuts("Ошибка\а\n");
}
}
При отсутствии операторов break во всех ветвях происходило бы следующее:
1) ввод с клавиатуры буквы 's' или 'S' вызывал бы вывод на экран сразу четырех сообщений:
Вычисление синуса аргумента в радианах
Вычисление косинуса аргумента в радианах
Вычисление тангенса аргумента в радианах
Ошибка (звучит звуковой сигнал)
2) ввод с клавиатуры буквы 'с' или 'С' вызывал бы вывод на экран сразу трех сообщений:
Вычисление косинуса аргумента в радианах
Вычисление тангенса аргумента в радианах
Ошибка (звучит звуковой сигнал)
3) ввод с клавиатуры буквы 'а' или 'А' вызывал бы вывод на экран сразу двух сообщений:
Вычисление тангенса аргумента в радианах
Ошибка (звучит звуковой сигнал)
5.4. Операторы цикла
Язык С++ имеет удобные операторы организации циклов. Общая форма записи операторов цикла такова:
while(cond_expr) operator;
dо operator while(cond_expr);
for(init_expr; cond_expr; increment_ехрг) operator;
Оператор while организует повторение оператора operator до тех пор, пока логическое выражение cond_expr не примет значение ЛОЖЬ (0). Оператор while называют оператором цикла с предусловием, так как истинность cond_expr проверяется перед входом в цикл. Следовательно, возможна ситуация, когда operator не выполняется ни разу.
Если необходимо обеспечить выполнение цикла xотя бы один раз, используют оператор цикла с постусловием do... while;. Здесь сначала выполняется operator, а затем проверяется значение выражения cond_ехрг. Повторение тела цикла происходит до тех пор, пока cond_ехрг не примет значение ЛОЖЬ (0).
Наиболее сложная форма оператора цикла - это оператор for. Он эквивалентен следующему фрагменту:
init_ехрг
while(cond_expression)
{
operator
increment_expression
}
Перед вхождением в цикл выполняется init_expression. Затем проверяется значение cond_expression. Повторение тела цикла происходит до тех пор, пока cond_expression не примет значение ЛОЖЬ (0). Циклически повторяемый участок состоит из оператора, заданного после for, и increment_expression, указанного в самом операторе for. Как increment_expression, так и init_expression могут быть любой (в том числе и пустой) последовательностью простых операторов, разделяемых оператором запятая. Наличие инициализирующей части, состоящей из произвольного числа операторов, дает основание утверждать, что любую программу на языке Си можно представить в виде единственного оператора for, однако не следует этого делать.
Различные операторы циклов могут выражаться друг через друга. Выражение оператора for через while уже приводилось. А вот и другие эквивалентные пары:
for(; cond_expr;) эквивалентен while(cond ехрг)
operator operator
for(operator;cond_ехрr;) эквивалентен dо
operator
while(cond_expr);
Одна из самых неприятных особенностей операторов цикла заключается в возможности образования бесконечного цикла или, как говорят, в зацикливании программы. Причина этого в том, что значение cond_ехрr из-за ошибки в программе никогда не становится ложным. Вот простой пример такой ошибки, приводящей к зацикливанию:
int i=0;
while(i<60);
{
printf("i=%d\n", i); i++;
}
Причина зацикливания - в незаметном знаке';' после oператора while. Компилятор полагает, что повторяемый в цикле оператор - это не составной оператор, заключенный в фигурные скобки, а пустой оператор. Значение i не увеличивается, и логическое выражение i<60 при проверке всегда дает значение ИСТИНА.
Приводимая далее программа и иллюстрирует использование оператора цикла while. Она подсчитывает число русских гласных букв в слове, введенном с клавиатуры:
//Prim5_2.cpp
#include<stdio. h>
void main( )
{
int total=0, rus=0;
char ch;
while((ch=getchar( ))!='\n')
{
total++;
switch(ch)
{
case'A': case'a': case'E': case'e':
case'И': case'u': case'O': case'o':
case'У': case'y': case'Э': case'э':
сase'Ю': case'ю': саsе'Я': сasе'я':
rus++;
}
}
printf("Введено %d символов, из них %d "\
" русских гласных букв\n", total, rus);
}
Приводимая ниже программа демонстрирует использование цикла dо... while для вывода на экран приглашения
"Продолжаете? (Да/Нет). Esc - отмена выбора.",
приема с клавиатуры и анализа ответа пользователя. При нажатии клавиши с русской буквой 'д' выводится слово "Да",а при нажатии клавиши с русской буквой 'н' - слово "Нет". При нажатии клавиши Esc (Ключ) выводится сообщение "Отмена выбора". При нажатии любых других клавиш звучит сигнал динамика, указывая на ошибку. Программа использует в своей работе библиотечную функцию bioskey(0), возвращающую так называемый код нажатия клавиши.
Для того чтобы программа не зависела от регистра клавиатуры (Рус/Лат), расширено число меток для ветвей "Да" и "Нет". Здесь приведен вариант программы для стандартной русифицированной по альтернативной таблице клавиатуры со 101/102 клавишами:
//Prim5_3.cpp
#include<stdio. h>
#includе <bios. h>
#define YES 0
#define NO 1
#define ESC 27
int main(void)
{
char сh;
puts("Продолжаете? (Д/Н). Esc-отмена выборa");
do
{
сh=bioskey(0);
switch(ch)
{
case'д': case 'Д': case'l: саse 'L':
puts("ДА");
return(YES);
case'н': case 'Н': case'y': саse 'Y:
puts("Hem");
return(NO);
саcе ESC: break;
default: putchar('\a');
}
}
while(ch!=ESC);
рuts("Отмена выбора");
return(ESC);
5.5. Другие управляющие средства языка Си
Помимо операторов ветвления и цикла, выполняющих передачу управления в зависимости от того ли иного условия, С++ имеет группу операторов для безусловной передачи управления. Это операторы break, continue, goto и return.
break может содержаться внутри тела цикла любой вложенности. При его исполнении управление передается в тело охватывающего цикла или для самого внешнего цикла - на оператор, следующий после цикла. Поэтому для прекращения всего цикла по "инициативе" наиболее глубоко вложенного цикла приходится выполнять не один, а несколько операторов break.
Аварийный выход из вложенного цикла - оправданное применение оператора goto label. Он передает управление на оператор, помеченный меткой label. Другая редко возникающая необходимость использования оператора goto - включение в кодовый сегмент программы специальных областей данных. Как правило, это связано с использованием кода на ассемблере. Например:
goto mmm;
asm db 4 dup(0)
mmm: а-345;
Оператор continue может использоваться только внутри тела цикла. В результате его исполнения управление переносится: 1) для оператора for на секцию increment_expr; 2) для операторов do... while и whiIe на вычисление выражения cond_ехрг и определение необходимости завершения цикла.
Оператор return переносит управление из текущей функции в точку ее вызова.
6. Функции в языке С++
6.1. Описание функции
Функция – логически самостоятельная именованная часть программы, которой могут передаваться параметры, и которая может возвращать кокое-то значение. Функции – это основные конструкции для построения пользовательских программ, концептуально расширяющие язык С++.
Большинство языков программирования используют функции и процедуры. С++ не поддерживает формальных процедур. Все подпрограммы языка С++ являются функциями.
Программа на языке С++ состоит из одной или нескольких функций. Формат определения функции:
[тип_данных] имя_функции( список_аргументов | void)
{
описание данных
операторы
[return (выражение)]
}
Совокупность предложений в фигурных скобках часто называют телом функции. Встретив определение функции, компилятор создает самостоятельную секцию кода программы, которая на этапе компоновки объединяется с другими функциями. Синтаксис языка С++ запрещает внутри определения функций помещать определение еще одной функции.
Поле «тип_данных» задает тип возвращаемого функцией значения. Если оно отсутствует, считается, что функция возвращает значение типа int. Если поле «тип_данных» содержит ключевое слово void, функция не возвращает никакого значения. Так как на организацию возврата тратится несколько машинных команд, использование void при описании возвращаемого значения позволяет получать более короткий и производительный код программы. Естественно, это возможно тогда, когда функция действительно не должна возвращать значений или для возврата значений используются указатели либо внешние переменные.
Поле «имя_функции» – это особый тип указателя, называемого указатель на функцию. Его значением является адрес точки входа в функцию. Для каждой ссылки на функцию (точка вызова функции) компилятор подставляет машинную инструкцию вызова процедуры с возвратом. В результате управление передается в точку входа функции, выполняются операторы, составляющие тело функции. Любая функция содержит машинную инструкцию возврата RET. Выполнение инструкции RET возвращает управление в точку вызова.
Поле «список_аргументов» определяет аргументы (или параметры), передаваемые в функцию, и содержит любую комбинацию типов и имен. Если функция имеет несколько аргументов, их тип и имена разделяются запятой. Это поле в определении функции называют списком формальных аргументов (или параметров). Для каждого вызова функции с параметрами компилятор добавляет в точке вызова машинные инструкции записи копий формальных аргументов в стек. Копии аргументов в стеке называют фактическими аргументами (или параметрами). Значения фактических аргументов последовательно ставятся в соответствие формальным аргументам.
Поле «список_аргументов» – не обязательная часть в определении функции. Если в функцию не передаются никакие аргументы, это поле – пустое или содержит ключевое слово void.
Правила, которых нужно придерживаться при объявлении функций языка С++:
1) Тип результата функции С++ записывается до имени функции.
2) Если список параметров не пустой, используются круглые скобки. Язык С++ разрешает также использовать ключевое слово void для обозначения отсутствия параметров.
3) Список_типов_параметров содержит список типов параметров следующего формата:
[const] тип1 параметр1 [const] тип2 параметр2…
Этот формат показывает, что отдельный параметр объявляется как переменная: сначала сообщается тип, а затем индефикатор параметра. Параметры списка в языке С++ разделяются запятой. Нельзя группировать последовательность параметров с одинаковым типом данных. Каждый параметр должен быть объявлен в отдельности. Если параметр имеет ключевое слово const, то компилятор проверяет, действительно ли функция не изменяет аргументы параметра.
4) Тело функции в языке С++ заключается в фигурные скобки {}. После функции точка с запятой не ставится.
5) С++ поддерживает передачу аргументов либо по значению, либо по ссылке, либо с использованием указателей. По умолчанию параметры передают свои аргументы в виде значения. Следовательно, функции работают с копией данных, сохраняя исходные данные. Для объявления параметра-ссылки ставится символ & после типа данных этого параметра. Параметр-ссылка становится альтернативным именем своего аргумента, и любые его изменения влияют на аргумент. Параметр - ссылка записывается в виде:
[const] тип1& параметр1 [const] тип2 & параметр2…
6) С++ поддерживает локальные константы, типы данных и переменные. Эти элементы данных могут появляться во вложенных операторах, вложенные функции в языке С++ не поддерживаются.
7) Ключевое слово return возвращает значение функции.
8) Если тип результата – void, не обязательно использование ключевого слова return, если не требуется обеспечить выход в теле функции.
6.2. Прототипы функций
С++ предписывает либо описание, либо определение функции перед ее использованием. Описание функции, называемое прототипом, сообщает имя функции, тип результата, число и тип ее параметров. Включение имени параметра является необязательным. Необходимо ставить точку с запятой после закрывающей круглой скобки. Язык С++ требует объявления функции, если вы вызываете ее до определения. Ниже приведен простой пример прототипа:
//прототип функции square
double sqr ( double );
main ( )
{
cout << "5^2" <<sqr ( 5 ) << endl:
return 0;
}
//определение функции square
double sqr ( double z )
{
return z*z ;
}
Обычно объявление функции делается глобальным. Однако можно объявлять прототип функции внутри функции пользователя. Этот способ делает прототип недоступным для других функций.
Вызов функции требует, чтобы ее параметры были обеспечены аргументами. Аргументы записываются на место параметров в том порядке, в каком параметры объявлены. Аргументы должны иметь тип данных, который совпадает или совместим с типом данных параметров. Например, вы можете иметь функцию volume, определенную следующим образом:
double volume ( double length, double width, double heigth)
{
return length*width*heigth;
}
Для вызова функции volume необходимо, чтобы аргументы имели тип double или совместимый тип данных. Ниже приведено несколько примеров вызова функции volume:
double len=34, width=55,ht=100;
int i=3;
long j=44;
unsigned k=33;
cout<<volume(len, width, ht)<<endl<<<<volume(1,2,3)<<endl;
cout<<volume(len, j,22.3)<<endl;
C++ позволяет отбрасывать результат функции. Этот вид вызова функции используется тогда, когда внимание должно быть сосредоточено на действиях функции, а не на возвращаемом значении.
6.3. Локальные переменные в функциях
Методы структурного программирования способствуют утверждению идеи, что функция должна быть как независимой, так и, по возможности, многократно используемой. Следовательно, функции могут иметь собственные типы данных, константы и переменные с целью придания им независимости.
Переменные, которые определяются внутри функции, называются локальными переменными. Они существуют только тогда, когда вызывается функция, в которой они описаны. Как только функция завершит выполнение, специальный код удаляет локальные переменные. Следовательно, после выполнения функции локальные переменные теряют свои значения. Код функции может создавать локальные переменные только тогда, когда исполняется функция, в которой они определены.
Несколько рекомендаций при работе с локальными перемеными:
1) используйте локальные переменные для сохранения и изменения значений параметров, которые объявляются с модификатором const;
2) не объявляйте локальную переменную с тем же именем, что и у глобальной переменной, к которой необходимо иметь доступ в функции.
6.4. Статические переменные в функциях
Язык С++ позволяет объявлять переменную как статическую, поместив ключевое слово static слева от ее типа данных. Статические переменные обычно инициализируются. Это происходит при первом вызове функции, в которой они определены. Существует ряд методов программирования, которые требуют сохранения локальных переменных между вызовами функций. Эти специальные локальные переменные называются статическими переменными.
Когда функция, в которой определены статаческие переменные, завершает выполнение, эти переменные сохраняют свои значения. Компилятор поддерживает эту особенность языка, сохраняя статические переменные в специальном сегменте памяти, который не изменяется в процессе выполнения программы.
С++ программа, которая показывает применение статистических локальных переменных в функциях
//Prim6_ .cpp
include<iostream. h>
double mean (double x)
{
static double sum=0;
static double sumx=0;
sum = sum+1;
sumx = sumx+x;
return sumx / sum;
}
main()
{
cout<<"среднее = "<<mean(2)<<endl;
cout<<"среднее = "<<mean(1)<<endl;
cout<<"среднее = "<<mean(4)<<endl;
cout<<"среднее = "<<mean(10)<<endl;
cout<<"среднее = "<<mean(11)<<endl;
return 0;
}
6.5. Выход из функций
Преждевременный выход из тела функции обычно происходит из-за того, что некоторые условия не позволяют выполнить операторы этой функции. В языке С++ предусмотрен оператор return для выхода из функции. Если функция имеет тип void, то можно использовать оператор return, не включая после никаких выражений. Если нужно осуществить выход из непустой функции, то оператор return должен породить значение, которое укажет результат выхода из функции.
6.6. Передача параметров функции
Обрабатывая вызов функции, компилятор вставляет в код программы последовательность машинных команд, выполняющих следующие действия:
1) запись в стек копий переменных или констант, перечисленных в списке аргументов (если поле «список_аргументов» в прототипе функции не void);
2) вызов процедуры с возвратом (либо «близкий», либо «далекий» в зависимости от заданного явно или неявно типа функции).
Функция в процессе исполнения извлекает копии аргументов из стека. Таким образом, формальные и фактические аргументы изолируются друг от друга.
При передаче параметров в функцию (при их записи в стек) выполняется дополнительно преобразование типов фактических аргументов к типу, который указан в прототипе функции. Преобразование не выполняется, если используется классическое задание прототипа. В этом случае несоответсвие типов фактических и формальных параметров может служить причиной ошибок. Ошибки такого типа не возникают, если задается полный прототип вызываемой функции.
В общем случае существуют два стиля передачи параметров функции:
1) вызов функции с передачей значений (Call-By-Value);
2) вызов функции с передачей адресов переменных (Call-By-Reference).
Вызов с передачей значений – это простая передача копий переменных в функцию, не оставляющая никаких возможностей для воздействия функции на переменные в точке вызова. Например:
/* Prim6_2.cpp */
#include <stdio. h>
int sum( int, int);
void main( void)
{int a = 5, b = 4, c;
c = sum( a, b);
printf( "%d\n", c);
}
int sum( int k, int l)
{int m;
m = k + l;
return m;
}
Функции sum() передаются копии переменных a и b. Эти копии используются функцией в качестве значений для аргументов k и l.
Вызов с передачей адресов предполагает, что в качестве параметров функции передаются не копии переменных, а копии адресов переменных. Используя указатель, функция осуществляет доступ к нужным ячейкам памяти. Так как известен адрес в памяти, можно изменить его значение. Поэтому при вызове с передачей адресов функция может изменить значение переменных в точке вызова. Приведенная ранее программа при вызове с передачей адресов выглядит так:
/* Prim6_3.cpp */
#include <stdio. h>
int sum( int*, int*);
void main( void)
{int a = 5, b = 4, c;
c = sum( &a, &b);
printf( "%d\n", c);
}
int sum( int* k, int* l)
{int m;
m = *k + *l;
return m;
}
В приведенном примере в функцию передаются копии указателей на ячейки памяти (другими словами, адреса в памяти). Эти адреса используются для доступа к памяти.
Вызов функции с передачей адресов позволяет разрабатывать функции, имеющие доступ к массивам и другим протяженным объектам данных. Если в список переменных включено имя массива, то в функцию передается только адрес начала массива. Например, так выглядит функция, выполняющая копирование строки из одного места в памяти в другое (т. е. действия, аналогичные библиотечной функции strcpy()):
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 |


