Операторы передачи управления

 

В C++ имеется четыре оператора передачи управления, изменяющих естественный порядок выполнения вычислений:

· оператор безусловного перехода goto

· оператор выхода из цикла break

· оператор перехода к следующей итерации цикла continue

· оператор возврата из функции return

Оператор goto

Оператор безусловного перехода goto имеет формат:

goto метка;

В теле той же функции должна присутствовать ровно одна конструкция вида:

метка: оператор;

Метка ̶ это обычный идентификатор, областью видимости которого является функция, в теле которой он задан. Оператор goto передает управление на помеченный оператор.

Применение goto нарушает принципы структурного и модульного программирования, по которым все блоки, из кото­рых состоит программа, должны иметь только один вход и один выход.

Использование оператора goto оправдано, если требуется:

· принудительный выход вниз по тексту программы из нескольких вложенных циклов или переключателей,

· переход из нескольких мест функции в одно (например, если перед выходом из функции всегда необходимо выполнять какие-либо действия).

В остальных случаях использование goto приводит к усложнению структуры программы и затруднению отладки. И даже в приведенных случаях допустимо применять goto только, если в этих фрагментах кода не создаются локальные объекты. В противном случае возможно при­менение деструктора при пропущенном конструкторе, что приводит к серьезным ошиб­кам в программе.

В любом случае не следует передавать управление внутрь операторов if, switch и циклов. Нельзя переходить управление внутрь блоков, содержащих инициализацию переменных, на операторы, расположенные после нее, поскольку в этом случае инициализация не будет выполнена:

int к; ...

goto metka; ...

{int a = 3, b = 4;

k = a + b;

metka: int m = k + 1; ...

}

После выполнения этого фрагмента программы значение переменной m не опре­делено.

Оператор break

Оператор выхода из цикла break используется внутри операторов цикла или switch для обеспече­ния перехода в точку программы, находящуюся непосредственно за оператором, внутри которого находится break.

Пример: программа вычисляет значение гиперболического синуса вещественно­го аргумента x с заданной точностью eps с помощью разложения в бесконечный ряд:

sh х = x + х3/3! + х5/5! + х7/7! + ...

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

#include <iostream.h>

#include <math.h>

int main( ) {

const int MaxIter = 500; // ограничитель количества итераций

double х, eps;

cout << “Введите аргумент и точность: ”;

cin >> x >> eps;

bool flag = true; // признак успешного вычисления

double у = x, ch = x; // сумма и первый член ряда

for (int n = 0; fabs(ch) > eps; n++) {

ch *= x * x /(2 * n + 2)/(2 * n + 3); // очередной член ряда

у += ch;

if (n > MaxIter){ cout << “\nРяд расходится!”; flag = false; break;}

}

if (flag) cout << “\nЗначение функции: ” << у;

return 0;

}

Оператор continue

Оператор перехода к следующей итерации цикла continue пропускает все опера­торы, оставшиеся до конца тела цикла, и передает управление на начало следую­щей итерации.

Оператор return

Оператор возврата из функции return завершает выполнение функции и переда­ет управление в точку ее вызова. Вид оператора: return [ выражение ];

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

 

Тема 1.11

Указатели

 

При обработке оператора определения переменной (например, int i=10;) компилятор выделяет память в соответствии с типом переменной (int) и инициализирует ее указанным значением (10). Все обращения к переменной по ее име­ни (i) заменяются компилятором на адрес области памяти, в которой хранится значение переменной. Для обращения к переменной непосредственно по ее адресу в С++ используются указатели.

Указатель – переменная, предназначенная для хранения адресов областей памяти.

Различают три вида указателей: на объект, на функцию и на void. Они отличаются свойствами и набором допустимых операций.

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

Простейшее объявление указателя на объект имеет вид:

тип *имя;

тип может быть любым, кроме ссылки и битового поля, причем он может быть к этому моменту только объявлен, но еще не определен.

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

Например, в операторе

int *a, b, *с;

описываются два указателя на целое с именами а и с, а также целая переменная b.

Указатель на функциюсодержит адрес в сегменте кода, по которому располагает­ся исполняемый код функции, т.е. адрес, по которому передается управление при вызове функции.

Указатели на функции используются для:

· косвенного вызо­ва функции (не через ее имя, а через обращение к переменной, хранящей ее ад­рес),

· передачи имени функции в другую функцию в качестве пара­метра.

Объявление указателя на функцию имеет вид:

тип возвращаемого значения (*имя указателя) ( список_типов_аргументов );

Например, объявление

int (*fun) (double, double);

задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента типа double.

Указатель на voidприменяется, когда конкретный тип объекта, ад­рес которого требуется хранить, не определен (например, если в одной и той же переменной в разные моменты времени нужно хранить адреса объектов раз­личных типов).

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

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

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

Выделенные участки динами­ческой памяти называются динамическими переменными. Работа с такими переменными производится только через указатели. Время их жизни – от точки создания до конца программы или до явного освобождения памяти.

Инициализация указателей

При определении указателя желательно выполнить его инициализацию. Использование неини­циализированных указателей — источник ошибок в програм­мах. Инициализатор записывается после имени указателя либо в круглых скоб­ках, либо после знака равенства.

Существуют следующие способы инициализации указателя:

1. Присвоение указателю адреса существующего объекта:

■ с помощью операции получения адреса &:

int a = 5; // целая переменная

int *р = &а; //в указатель записывается адреса

int *p (&а); // то же самое другим способом

■ с помощью значения другого инициализированного указателя:

int *r = р;

■ с помощью имени массива или функции, которые трактуются как адрес:

int b[10]; // массив

int *t = b; // присваивание адреса начала массива

void f (int a ){ /* ... */ } // определение функции

void (*pf)(int); // объявление указателя на функцию

pf = f; // присваивание указателю адреса функции

2. Присваивание указателю адреса области памяти в явном виде:

char *vp = (char *)0xB8000000;

0xB8000000 – шестнадцатеричная константа, (char *) – операция приве­дения типа (константа преобразуется к типу «указатель на char»).

3. Присваивание пустого значения:

int *suxx = NULL;

int *rulez = 0;

Константа NULL определяется в некоторых за­головочных файлах С как указатель, равный нулю. Рекомендуется использо­вать просто 0, т.к. как это значение типа int будет правильно преобразовано стандартными способами в соответствии с контекстом.

Пустой указатель можно использо­вать для проверки, ссылается указатель на конкретный объект или нет, т.к. гарантиру­ется, что объектов с нулевым адресом нет.

4. Выделение участка динамической памяти и присваивание ее адреса указателю:

■ с помощью операции new:

int *n = new int; // 1

int *m = new int (10); // 2

int *q = new int [10]: // 3

■ с помощью функции malloc:

int *u = (int *)malloc(sizeof(int)); // 4

В операторе 1 выделяется участок динамической памяти достаточный для размещения величины типа int и адрес начала это­го участка записывается в переменную n. Память под саму переменную n, размера, достаточно­го для размещения указателя, выделяется на этапе компиляции.

В операторе 2, кроме описанных выше действий, производится инициализация выделенной динамической памяти значением 10.

В операторе 3 выделеляется память под 10 величин типа int (массива из 10 элементов) и адрес начала этого участка записываетсяв пере­менную q, которая может трактоваться как имя массива. Через имя можно обра­щаться к любому элементу массива.

Если память выделить не удалось порождается исключе­ние bad_alloc. Старые версии компиляторов могут возвращать 0.

В операторе 4 выполняются те же действия, что и в операторе 1, но с помощью функции выделения памяти malloc, унаследованной из библиотеки С. В функцию переда­ется один параметр – количество выделяемой памяти в байтах. Конструкция (int *) используется для приведения типа указателя, возвращаемого функцией, к требуемому типу. Если память выде­лить не удалось, функция возвращает 0. Для использования malloc, нужно подключить к программе заголовочный файл <malloc.h>.

Операцию new использовать предпочтительнее, чем функцию malloc, особенно при работе с объектами.

Освобождение памяти

Если память выделена с помощью new, освобождаться она должна с помощью операции delete, если с помощью функции malloc – посредством функции free. Если память выделялась под массив с помощью new [], для освобождения используется delete []. Размерность массива при этом не указывается.

Приведенные выше динамические переменные уничтожа­ются следующим образом:

delete n; delete m; delete [] q; free (u);

После освобождения памяти переменная-указатель сохраняется и может инициали­зироваться повторно.

При работе с динамической памятью возможны ошибки, не вызывающие сообщений компилятора об ошибке, но приводящие к появлению ячеек памяти, недоступных для дальнейших операций. Они называются «мусором».

Мусор может появляться в случае:

· Присвоения инициализированному указателю значение другого указателя. Старое значение при этом теряется.

· Использования delete вместо delete []. Помечен как свободный будет только первый элемент массива, а остальные окажутся не­доступны.

· При выходе из области действия переменной-указателя. Память, отведенная под нее освобождается, а, следовательно, динамическая переменная, на которую ссылался ука­затель, становится недоступной. При этом память из-под самой динамической переменной не освобождается.

Указатель может быть константой или переменной, а также указывать на кон­станту или переменную:

int i; // целая переменная

const int ci = 1; // целая константа

int *pi; // указатель на целую переменную

const int *pci; // указатель на целую константу

int * const cp = &i; // указатель-константа на целую переменную

const int * const cpc = &ci; // указатель-константа на целую константу

Модификатор const, находящийся между именем указа­теля и звездочкой, относится к самому указателю и запрещает его изменение, a const слева от звездочки задает постоянство значения, на которое он указывает.

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

Квадратные и круглые скобки имеют одинаковый приоритет, больший, чем звездочка, и рассматриваются слева направо. Для изменения по­рядка рассмотрения используются круглые скобки.

При интерпретации сложных описаний применяется правило «изнутри наружу»:

· если справа от имени имеются квадратные скобки – это массив, если скобки круглые – это функция;

· если слева есть звездочка, это указатель на проинтерпретированную ранее конструкцию;

· если справа встречается закрывающая круглая скобка, необходимо приме­нить приведенные выше правила внутри скобок, а затем переходить наружу;

· в последнюю очередь интерпретируется спецификатор типа.

· Например, в операторе

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

5 4 2 1 3 // порядок интерпретации описания

объявляется массив из 10 указателей на функции без параметров, возвращающих указатели на int.

 

Тема 1.12

Операции с указателями

 

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

· присваивание (при их инициализации)

· разадресация (разыменование) или кос­венное обращение к объекту (*)

· получения адреса (&)

· приведение типов

· сложение с константой

· вычита­ние

· инкремент (++)

· декремент (--)

· сравнение

Операция разадресации предназначена для доступа к ве­личине, адрес которой хранится в указателе. Эту операцию можно использовать для получения и для изменения значения величины (если она не объяв­лена как константа):

char a; // переменная типа char

char *р = new char; // выделение памяти под указатель и под динамическую

// переменную типа char

*р = ‘Ю’; а = *р; // присваивание значения обеим переменным

Конструкцию *имя_указателя можно использовать в левой части оператора присваивания, т.к. как она является L-значением (определяет адрес области памяти). Для простоты эту конструкцию можно считать именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован).

На одну и ту же область памяти может ссылаться несколько указателей различного типа. Примененная к ним операция разадресации даст разные результаты.

Пример:

#include <stdio.h>

int main( ){

unsigned long int A = 0Xcc77ffaa;

unsigned short int *pint = (unsigned short int*) &A;

unsigned char *pchar = (unsigned char *) &A;

printf (“ | %x | %x | %x |”, A, *pint, *pchar);

return 0;

}

На экран выведется строка | cc77ffaa | ffaa | аа |. Значения указателей pint и pchar одинаковы, но разадресация pchar дает в результате один младший байт по этому адресу, a pint – два младших байта.

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

При инициализации указателей была использована операция приведения типов.Для явного приведения типа необходимо перед именем переменной в скобках указать тип, к которому ее требуется преобразовать.

Явное приведение типа требуется, если тип указателя не совпадает с типом инициализатора. При этом не гарантируется сохранение информации, поэтому в общем случае явных преобразований типа следует избегать.

Присваивание указателям типа void* осуществляется без приведения типа, т.к. преобразование к этому типу происходит неявно. Значение 0 неявно преобразуется к указателю на любой тип.

Указатель может неявно преобразовываться в значение типа bool (например, в выражении условного оператора), при этом ненулевой указатель преобразуется в true, а нулевой в false.

Присваивание указате­лей на объекты указателям на функции, и наоборот, недопустимо. Запрещено присваивать значения указателям-константам. Присваивать значения указателям на константу и переменным, на кото­рые ссылается указатель-константа, допускается.

Арифметические операции с указателями (сложение с константой, вычитание, инкремент и декремент) автоматически учитывают размер типа величин, адре­суемых указателями. Эти операции применимы только к указателям одного типа и имеют смысл в основном при работе со структурами данных, последовательно размещенными в памяти, например, с массивами.

Инкрементперемещает указатель к следующему элементу массива, декрементк предыдущему. Фактически значение указателя изменяется на величину sizeof (тип).

Если указатель на определенный тип увеличивается или уменьшает­ся на константу, его значение изменяется на величину этой константы, умножен­ную на размер объекта данного типа, например:

short *p = new short [5];

p++; // значение р увеличивается на 2

long *q = new long [5];

q++; // значение q увеличивается на 4

Разностьдвух указателей ̶ это разность их значений, деленная на размер типа в байтах (в применении к массивам разность указателей, например, на третий и шестой элементы равна 3).

Суммирование двух указателей не допускается.

При записи выражений с указателями следует обращать внимание на приорите­ты операций. В качестве примера рассмотрим последовательность действий, за­данную в операторе *р++ = 10;

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

*р = 10; р++;

Выражение (*р)++; инкрементирует значение, на которое ссылается указатель.

 

Тема 1.13

Ссылки

 

Ссылочный тип (ссылка, псевдоним) служит для задания объекту дополнительного имени. Ссылка, аналогично указателю, позволяет косвенно манипулировать объектом. Однако при этом не требуется специального синтаксиса, необходимого для указателей (применения операции разыменования). Таким образом, ссылку можно рассматривать как указатель, который автоматически разымено­вывается.

Ссылка, в отличие от указателя, не занимает дополнительного пространства в па­мяти и является просто другим именем величины.

Формат объявления ссылки:

тип &имя=инициализатор;

где тип — это тип величины, на которую указывает ссылка, & - оператор ссылки (означает, что следующее за ним имя является именем переменной ссылочного типа).

int kol;

int &pal = kol; // ссылка pal - альтернативное имя для kol

const char &CR = '\n'; // ссылка на константу

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

int ival = 1024;

int &refVal = ival; // правильно: refVal - ссылка на ival

int &refValue; // ошибка: ссылка должна быть инициализирована

Ссылка, в отличие от указателей, инициализируется не адресом объекта, а его значением. Таким объектом может быть и указатель:

int ival = 1024;

int &refVal = &ival; // ошибка: refVal имеет тип int, а не int*

int *pi = &ival;

int *&ptrVal2 = pi; // правильно: ptrVal - ссылка на указатель

После определения ссылки ее нельзя изменить, связав с другим объектом (поэтому ссылка должна быть инициализирована при определении).

int min_val = 0;

refVal = min_val; // значение min_val присваивается переменной ival

// refVal не меняет значение

В данном случае оператор присваивания не меняет значения ссылки refVal, новое значение присваивается переменной ival – той, которую адресует refVal.

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

refVal += 2; // прибавляет 2 к ival – переменной,

// на которую ссылается refVal.

int ii = refVal; //присваивает переменной ii текущее значение ival,

int *pi = &refVal; // инициализирует переменную pi адресом ival.

При определении ссылок в одном операторе через запятую, перед каждым объектом типа ссылки должен стоять знак & (как и для указателей).

int ival = 1024, ival2 = 2048; // определено два объекта типа int

int &rval = ival, rval2 = ival2; // определена одна ссылка и один объект

int inal3 = 1024, *pi = ival3, &ri = ival3; // определен один объект, один // указатель и одна ссылка

int &rval3 = ival3, &rval4 = ival2; // определены две ссылки

Константная ссылка может быть инициализирована объектом другого типа (если существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например:

double dval = 3.14159;

const int &ir = 1024; // верно только для константных ссылок

const int &ir2 = dval;

const double &dr = dval + 1.0;

Если в приведенном примере не указывать спецификатор const, то все три определения ссылок вызовут ошибку компиляции.

Для литералов это вызвано тем, что у программиста не должно быть возможности поменять значение литерала косвенно (используя указатели или ссылки).

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

Например, если записать:

double dval = 1024;

const int &ri = dval;

то компилятор преобразует это так:

int temp = dval;

const int &ri = temp;

Если программист будет иметь возможность присвоить новое значение ссылке ri, то реально будет изменяться не dval, а temp. Значение dval осталось бы тем же. Это неочевидно для программиста, поэтому компилятор запрещает такие действия, и единственная возможность проинициализировать ссылку объектом другого типа – объявить ее как const.

В следующем примере ссылку, также нельзя изменить. Необходимо определить ссылку на адрес константного объекта

const int ival = 1024; // ошибка: нужна константная ссылка

int *&pi_ref = &ival;

 

const int ival = 1024; // все равно ошибка

const int *&pi_ref = &ival;

Причина ошибки, в том pi_ref является ссылкой на константный указатель на объект типа int. Хотя нужно определить неконстантный указатель на константный объект, поэтому правильной будет следующая запись:

const int ival = 1024;

int *const &piref = &ival;

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

Например, если записано:

int *pi = 0;

указатель pi инициализируется нулевым значением (это значит, что он не указывает ни на какой объект).

Запись же:

const int &ri = 0;

означает примерно следующее:

int temp = 0;

const int &ri = temp;

Для операции присваивания, если работать с указателями

int ival = 1024, ival2 = 2048;

int *pi = &ival, *pi2 = &ival2;

pi = pi2;

переменная ival, на которую указывает pi, остается неизменной, а pi получает значение адреса переменной ival2. И pi, и pi2 теперь указывают на один и тот же объект ival2.

Если же мы работаем со ссылками:

int &ri = ival, &ri2 = ival2;

ri = ri2;

то само значение ival меняется, но ссылка ri по-прежнему адресует ival.

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

· Переменная-ссылка должна явно инициализироваться при ее описании, кро­ме случаев, когда она является параметром функции, описана как extern или ссылается на поле данных класса.

· После инициализации ссылке не может быть присвоена другая переменная.

· Тип ссылки должен совпадать с типом величины, на которую она ссылается.

· Нельзя определять указатели на ссылки, создавать массивы ссылок и ссылки на ссылки.

Выше было рассмотрено самостоятельное использование объектов ссылочного типа. В реальных С++ программах ссылки редко используются как самостоятельные объекты, а применяются в качестве параметров функций и типов воз­вращаемых функциями значений.

Ссылки позволяют использовать в функциях переменные, передаваемые по адресу, без операции разадресации, что улучшает читаемость программы.

 

Тема 1.14

Массивы

 

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

Описание массива имеет вид:

[класс памяти] [const] тип имя [[размерность]] [инициализатор]

Размерность – количество элементов массива. Внешние квадратные скобки являются элементом синтаксиса, внутренние – указывают на необязательность конструкции (размерность может не указываться). Остальные элементы, аналогичны тем, что задаются при описании переменной.

float a [10]; // описание массива из 10 вещественных чисел

Элементы массива различаются по порядковому номеру и нумеруются с нуля. Для доступа к элементу после имени массива указывается номер элемента (индекс)в квадратных скобках. Значение индекса изменяется от 0 до n – 1 (n – размерность массива). При этом автоматический контроль выхода индекса за грани­цу массива не производится, что может привести к ошибкам.

Для инициализации элементов массива в фигурных скобках указывается список инициализирующих значении. Значения элементам присваиваются по порядку. Если элементов в массиве больше, чем инициализаторов, элементы, для которых значения не ука­заны, обнуляются:

int b[5] = {3, 2, 1}; // b[0] =3, b[1] = 2, b[2] =1, b[3] = 0, b[4] = 0

Размерность массива и тип элементов определяет объем памяти, не­обходимый для размещения массива. Выделение памяти выполняется на этапе компиляции, поэтому размерность может быть задана только целой положительной констан­той или константным выражением.

Если размерность не указана, должен присутствовать инициализатор. В этом случае компилятор выделит память по количеству инициализирующих значений. Также размерность может быть опущена в списке формальных пара­метров.

Пример:подсчет суммы элементов массива.

#include <iostream.h>

int main( ) {

const int n = 10;

int i, sum;

int marks[n] = {3, 4, 5, 4, 4};

for (i = 0, sum = 0; i < n; i++) sum += marks[i];

cout << “Сумма элементов: ” << sum;

return 0; }

Размерность массивов предпочтительнее задавать с помощью именованных кон­стант,как это сделано в примере. При этом для изменения размерности достаточно скорректировать значение константы всего лишь в одном месте программы.

Пример. Сортировка целочисленного массива методом выбора.Алгоритм состо­ит в том, что выбирается наименьший элемент массива и меняется местами с первым элементом, затем рассматриваются элементы, начиная со второго, и наи­меньший из них меняется местами со вторым элементом, и так далее n-1 раз. При последнем проходе цикла при необходимости меняются местами предпоследний и последний элементы массива.

#include <iostream.h>

int main( ) {

const int n = 20; // количество элементов массива

int b[n]; // описание массива

int i;

for (i = 0; i < n; i++) cin >> b[i]; // ввод массива

for (i = 0; i < n-l; i++){ // n-1 раз ищем наименьший элемент

// принимаем за наименьший первый из рассматриваемых элементов:

int imin = i;

// сравниваем все остальные элементы с первым:

for (int j = i + 1; j < n; j++)

// если нашли меньший элемент, запоминаем его номер:

if (b[j] < b[imin]) imin = j;

int a = b[i]; // обмен элементов

b[i] = b[imin]; // с номерами

b[imin] = a; // i и imin

}

for (i =0; i < n; i++) cout << b[i] << ‘ ’; // вывод упорядоченного массива:

return 0;

}

Процесс обмена элементов массива с номерами i и imin через буферную перемен­ную а на i-м проходе цикла проиллюстрирован на рисунке 1.7. Цифры около стрелок обозначают порядок действий.

Рисунок 1.7 - Обмен значений двух переменных

Идентификатор массива является константным указателем на его нулевой эле­мент. Например, для предыдущего примера имя b — это то же самое, что &b[0], а к i-му элементу можно обратиться, используя выражение *(b+i).

Можно описать указатель, присвоить ему адрес начала массива и работать с массивом через указатель.

Пример: скопировать все эле­менты массива а в массив b.

int а[100], b[100];

int *ра = а; // или int *pa = &а[0];

int *pb = b;

for (int i = 0; i < 100; i++)

*pb++ = *pa++; // или pb[i] = pa[i];

Многомерные массивызадаются указанием каждого измерения в квадратных скобках.

Например, оператор

int matr [6][8];

задает описание двумерного массива из 6 строк и 8 столбцов.

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

Для доступак элементу многомерного мас­сива указываются все его индексы, например, matr[i][j]. Имя массива matr является указателем на его первый элемент, а matr[i]-на начало i-ой строки, к элементу массива можно обращаться через указатели: *(matr[i]+j)или *(*(matr+i)+j

Инициализация многомерного массива может осуществляться двумя способами:

· Как массива массивов, каждый из которых заключается в отдельные фигурные скобки. В этом случае левую размерность при описании можно не указывать: int mass [][2] = { {1, 1}, {0, 2}, {1, 0} };

· Заданием общего списка элементов в том порядке, в котором они располагаются в памяти:

int mass [3][2] = {1, 1, 0, 2, 1, 0};

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

#include <stdio.h>

int main( ) {

const int nstr = 4, nstb = 5; // размерности массива

int b[nstr][nstb]; // описание массива

int i, j;

for (i =0; i < nstr; i++) // ввод массива

for (j = 0; j < nstb; j++) scanf(“%d”, &b[i][j]);

int istr = -1; // номер искомой строки

MaxKol = 0; // максимальное количество нулей

for (i = 0; i < nstr; i++) { // просмотр массива по строкам

int Kol = 0; // количество нулей в текущей строке

for (j = 0; j < nstb; j++)

if (b[i][j] == 0) Kol++;

if (Kol > MaxKol){istr = i; MaxKol = Kol;}

}

printf(“Исходный массив :\n”);

for (i =0; i < nstr; i++) {

for (j = 0; j <nstb; j++) printf(“%d”, b[i][j]);

printf ("\n");

}

if (istr == -1) printf(“Нулевых элементов нет”);

else printf(“Номер строки: %d”, istr);

return 0;

}

Массив просматривается по строкам, в каждой из них подсчитывается количество нулевых элементов. Переменная Kol обнуляется перед просмотром каждой строки. Наи­большее количество и номер соответствующей строки запоминаются.

Динамические массивы

Динамические массивы – массивы, размещаемые в динамической памяти во время выполнения программы.

Динамические массивы создают с помощью операции new, при этом необходимо указать тип элементов массива и размерность:

int n = 100;

float *р = new float [n];

Здесь создается переменная-указатель на float,в динамической памяти отводится непрерывная область, достаточная для размещения 100 элементов типа float, и адрес ее начала записывается в указатель р.

Для создания динамического массива может также использоваться функция mallос библиотеки С:

int n = 100;

float *q = (float *) malloc(n * sizeof(float));

Операция преобразования типа, записанная перед обращением к функции malloc, требуется потому, что функция возвращает значение указателя типа void*, а ини­циализируется указатель на float.

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

Память, зарезервированная под динамический массив с помощью операции new [], должна освобождаться оператором delete [], а память, выделенная функцией malloc — посредством функции free. Для приведенных выше массивов:

delete [] p; free (q);

Размерность массива в операции delete не указывается, но квадрат­ные скобки обязательны.

Доступ к элементам динамического массива осу­ществляется точно так же, как и к элементам статического. Например, к пятому элементу при­веденного выше массива можно обратиться как р[5] или *(р+5).

Для создания динамического многомерного массива необходимо указать в опера­ции new все его размерности (самая левая размерность может быть переменной), например:

int nstr = 5;

int **m = (int **) new int [nstr][10];

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

int nstr, nstb;

cout << “Введите количество строк и столбцов: ”;

cin >> nstr >> nstb;

int **a = new int *[nstr]; // 1

for (int i = 0; i < nstr; i++) // 2

a[i] = new int [nstb]; // 3

В операторе 1 объявляется переменная типа «указатель на указатель на int» и вы­деляется память под массив указателей на строки массива (количество строк — nstr). В операторе 2 организуется цикл для выделения памяти под каждую строку массива. В операторе 3 каждому элементу массива указателей на строки присваи­вается адрес начала участка памяти, выделенного под строку двумерного масси­ва. Каждая строка состоит из nstb элементов типа int (рисунок 1.8).

Освобождение памятииз-под массива с любым количеством измерений выпол­няется с помощью операции delete []. Указатель на константу удалить нельзя.

 

Рисунок 1.8 - Выделение памяти под двумерный массив

Для правильной интерпретации объявлений полезно запомнить мнемоническое правило: «суффикс привязан крепче префикса». Если при описании переменной используются од­новременно префикс * (указатель) и суффикс [] (массив), то переменная интерпретируется как массив указателей, а не указатель на массив:

int *р[10]; — массив из 10 указателей на int.

 

Тема 1.15

Функции

 

Функция — это именованная последовательность описаний и операторов, выполняющая какое-либо законченное действие. Функция начинает выполняться в момент вызова.

Описание функции

Определение функции состоит из двух частей - заголовка (прототипа, сигнатуры) и тела:

[ класс ] тип имя ( [ список параметров ])[throw ( исключения )]

{ тело функции }

Объявление функции содержит только ее прототип, завершаемый точкой с запятой:

[ класс ] тип имя ( [ список параметров ])[throw ( исключения )];

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

Необязательный модификатор класс явно задает область видимости функции:

extern - глобальная видимость во всех модулях программы (значение по умолчанию);

static – видимость в пределах модуля, в котором определена функция.

Спецификатор тип определяет тип возвращаемого функцией значения. Он может быть любым, кроме массива и функции, но может быть указателем на массив или функцию. Если функция не должна возвращать значение, указывается тип void. В случае отсутствия спецификатора типа предполагается, что функция возвращает целое значение (типа int).

Список параметров определяет величины, передаваемые в функцию при ее вызове. Для каждого параметра указывается его тип и имя. В объявлении имена можно опускать. Элементы списка параметров разделяются запятыми. При отсутствии параметров список может быть пустым (наличие скобок обязательно) или иметь спецификатор void.

Тип возвращаемого значения и типы параметров совместно определяют тип функции.

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

Тело функции представляет собой последовательность операторов и описаний.

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

return [выражение];

В функции может быть несколько операторов return, если этого требует алгоритм. Для функции типа void и для функции main он может отсутствовать. В этом случае возврат в точку вызова происходит после выполнения последнего оператора тела функции.

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

Примеры:

int f1( ){return 1;} //правильно

void f2( ){return 1;} // неправильно - f2 не должна возвращать значение

double f3 ( ){return 1;} // правильно - 1 преобразуется к типу double

Вызов функции

Если функция не возвращает значения, ее вызов должен быть оператором, если возвращает - выражением (может входить в состав других выражений).

Оператор вызова функции имеет вид:

имя функции (список аргументов);

Выражение вызова функции имеет вид:

имя функции (список аргументов)

Список аргументов содержит разделенные запятыми имена переменных с указанием их типа. При вызове функции перечисленные аргументы ставятся на место параметров, указанных при описании функции. Аргументы также называют фактическими параметрами функции, а параметры – формальными параметрами.

Число и типы аргументов и параметров должны совпадать. На имена параметров ограничений по соответствию не накладывается, поскольку функцию можно вызывать с различными аргументами, а в прототипах имена компилятором игнорируются.

Пример: функция, возвращает сумму двух целых величин:

#include <iostream.h>

int sum(int a, int b); // объявление функции

int main( ) {

int a = 2, b = 3, c, d;

с = sum(a, b); // вызов функции

cin >> d;

cout << sum(c, d); // вызов функции

return 0;

}

int sum (int а, int b) { // определение функции

return (а + b);

}

Локальные переменные

Описанные внутри функции величины и ее параметры являются локальными. Областью их действия является функция.

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

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

Пример:

#include <iostream.h>

void f (int a){

int m = 0;

cout << "n m p\n";

while (a--) {

static int n = 0;

int p = 0;

cout << n++ << ' ' << m++ << ' ' << p++ < '\n';

}

}

int main( ){ f(3); f(2); return 0;}

Статическая переменная n размещается в сегменте данных и инициализируется один раз при первом выполнении оператора, содержащего ее определение. Автоматическая переменная m инициализируется при каждом входе в функцию. Автоматическая переменная р инициализируется при каждом входе в блок цикла.

Программа выведет на экран:

n m р

0 0 0

1 1 0

2 2 0

n m р

3 0 0

4 1 0

Нельзя возвращать из функции указатель на локальную переменную, поскольку память, выделенная локальным переменным при входе в функцию, освобождается после возврата из нее.

Пример:

int* f ( ) {

int а = 5;

return &а; // нельзя!

}

Функцию можно определить как встроенную указавмодификатор inline перед типом функции. Для такой функции компилятор вместо обращения к функции помещает ее код непосредственно в каждую точку вызова. Использовать встроенные функции целесообразно для коротких функций, чтобы снизить накладные расходы на вызов (сохранение и восстановление регистров, передача управления). При этом увеличивается объем исполняемой программы.

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

 

Тема 1.16

Параметры функции

 

Механизм параметров является основным способом обмена информацией между вызываемой и вызывающей функциями.

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



/footer.php"; ?>