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

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

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

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

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

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

· оператор генерации исключения throw.

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

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

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

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

goto метка;

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

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

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

Использование оператора безусловного перехода оправдано в двух случаях:

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

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

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

Исключительную ситуацию (или просто исключение) генерирует либо программист с помощью оператора throw, либо сама среда выполнения. Это происходит, когда во время выполнения программы возникают какие-либо ошибки, например, деление на ноль или переполнение. Механизм обработки исключений, реализованный в С++, позволяет реагировать на подобные ошибки и таким образом избегать аварийного завершения программы.

Лекция 3. Указатели и массивы

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

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

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

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

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

int (*fun) (double, double);

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

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

тип *имя;

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

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

int *a, b, *c;

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

Размер указателя зависит от модели памяти. Можно определить указатель на указатель, например, int **p ;. Значением такой переменной должен быть адрес указателя - получается двойное косвенное обращение. Количество звездочек (уровней косвенности) стандартом не ограничено.

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

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

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

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

int * p0 = &i; // 1 - указатель на переменную

// int * p1 = &ci; // 2 - ошибка трансляции!

int * p2 = const_cast<int*>( &ci ); // 2.1

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

int const * pc2 = &ci; // 3.1 - тоже указатель на константу

const int * pc1 = &i; // 4

int const * pc3 = &i; // 4.1

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

// int * const cp1 = &ci; // 6 - ошибка трансляции!

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

const int * const cpc1 = &i; // 8

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

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

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

Время жизни динамических переменных - от точки создания до конца программы или до явного освобождения памяти.

В С ++ используется два способа работы с динамической памятью. Первый использует семейство функций malloc и достался в наследство от С, второй использует операции new и delete.

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

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

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

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

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

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

значения другого инициализированного указателя:

int* r = p;

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

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

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

...

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

void (*pf)(int); // указатель на функцию

pf = f; // присваивание имени функции

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

char* vp = (char *)0xB8000000; // шестнадцатеричная константа

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

int* suxx = NULL; // не рекомендуется

int* rulez = 0; // так - лучше

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

с помощью операции 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

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

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

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

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

С указателями можно выполнять следующие операции: разадресация (*), присваивание, сложение с константой, вычитание, инкремент (++), декремент (- -), сравнение, приведение типов. При работе с указателями часто используется операция получения адреса (&).

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

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

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

/* и под динамическую переменную типа char */

// присваивание значения обеим переменным

*p = 'Ю'; a = *p;

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

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

unsigned long int A=0Xсс77ffaa;

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

Эта операция унаследована из С и считается устаревшей, поскольку не обеспечивает безопасности. Явное преобразование типов указателей лучше делать с помощью операций преобразования С++ static_cast и reinterpret_cast, например:

void *pointer = static_cast<void *>( &a );

pointer = reinterpret_cast<void *>( pa );

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

*p++ = 10;

То же самое можно записать подробнее:

*p = 10; p++;

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

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

Ссылки

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

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

тип & имя;

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

int kol;

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

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

Разрешается объявлять константную ссылку, то есть ссылку на константу, например:

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

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

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

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

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

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

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

Массивы

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

Конечная именованная последовательность однотипных величин называется массивом.

В С++ различают массивы фиксированного размера и массивы переменного размера ( динамические ). Количество элементов в массиве первого типа известно при написании программы и никогда не меняется. Память под такой массив выделяет компилятор. Количество элементов динамического массива на этапе компиляции не известно и, как правило, зависит от входных данных. Память под динамический массив выделяется во время выполнения программы с помощью операций выделения памяти.

Описание массива фиксированного размера отличается от описания простой переменной наличием после имени квадратных скобок, в которых задается количество элементов массива ( размерность ):

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

Тип элементов массива - любой допустимый тип С++, например, встроенный или библиотечный, кроме типа void. Элементы массива нумеруются с нуля. Глобальный массив по умолчанию инициализируется нулями. Локальный массив, как и обычная переменная, по умолчанию никак не инициализируется. Инициализирующие значения при желании задают в фигурных скобках:

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

Размерность массива может быть задана только целой положительной константой или константным выражением.

Для доступа к элементу массива после его имени указывается номер элемента ( индекс ) в квадратных скобках. В следующем примере подсчитывается сумма элементов массива.

#include <iostream>

using namespace std;

int main(){

const int n = 10;

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

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

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

Размерность массивов предпочтительнее задавать с помощью типизированных констант.

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

float *p = new float [100];

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

Преимущество динамических массивов состоит в том, что их размерность может не быть константой.

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

delete [] p; free (q);

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

float *p = new float [n]();

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

int matr [6][8];

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

Для доступа к элементу многомерного массива указываются все его индексы, например, matr[i][j].

Инициализация многомерного массива:

int mass2 [][] = { {1, 1}, {0, 2}, {1, 0} };

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

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

matr = new int [a] [b];

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

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

#include <сstdio>

#include <iomanip>

using namespace std;

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 );

}

Строки

Строка представляет собой массив символов, заканчивающийся нуль-символом. Нуль-символ - это символ с кодом, равным 0, что записывается в виде управляющей последовательности '\0'. По положению нуль-символа определяется фактическая длина строки. Строку можно инициализировать строковым литералом:

char str[10] = "Vasia";

В этом примере под строку выделяется 10 байт, 5 из которых занято под символы строки, а шестой - под нуль-символ. Если строка при определении инициализируется, ее размерность можно опускать (компилятор сам выделит соответствующее количество байт):

char str[] = "Vasia"; //Выделено и заполнено 6 байт

Оператор

char *str = "Vasia"

создает не строковую переменную, а указатель на строковую константу.

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

Для работы со строками С нужно подключить к программе заголовочный файл <cstring>. Ввод и вывод С-строк обеспечивает библиотека ввода-вывода <cstdio>.

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

Пример (программа запрашивает пароль не более трех раз):

#include <cstdio> // библиотека ввода-вывода

#include <cstring> // библиотека для работы со строками старого стиля

#include <clocale> // библиотека локализации

using namespace std;

int main()

{

setlocale( LC_ALL, "rus" );

char s[5],

passw[] = "kuku"; // passw - эталонный пароль

bool ok = false;

for ( int i = 0; ! ok && i < 3; ++i )

{

printf( "\nвведите пароль (4 символа):\n" );

gets( s ); // ввод строки c клавиатуры

if ( strcmp( s, passw ) == 0 )

ok = true; // сравнение строк

}

if ( ok )

printf( "\nпароль принят\n" );

else printf( "\nпароль не принят\n" );

}

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

При работе со строками часто используются указатели. Для примера рассмотрим копирование строки src в строку dest:

#include <iostream>

using namespace std;

int main()

{ char src[10];

cin >> src; // ввод строки до первого пробела

char *dest = new char [10],

*d = dest, *s = src;

while ( *d++ = *s++ ); // копирование строки

cout << dest;

}

Лекция 4. Типы данных, определяемые пользователем

Перечисления (enum)

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

enum [ имя_типа ] { список_констант };

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

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

enum Err { ERR_READ, ERR_WRITE, ERR_CONVERT};

Err error;

...

switch (error)

{ case ERR_READ: /* операторы */ break;

case ERR_WRITE: /* операторы */ break;

case ERR_CONVERT: /* операторы */ break;}

Константам ERR_READ, ERR_WRITE, ERR_CONVERT присваиваются значения 0, 1 и 2 соответственно.

Структуры (struct)

В отличие от массива, все элементы которого однотипны, структура может содержать элементы разных типов. В языке С++ структура является видом класса и обладает всеми его свойствами, но во многих случаях достаточно использовать структуры так, как они определены в языке С:

struct [ имя_типа ] {тип_1 элемент_1;тип_2 элемент_2;*тип_n элемент_n;}

[ список_описателей ];

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

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

struct

{

char fio[30];

int date, code;

float salary;

}stuff[100], *ps; /*определение массива структур и указателя на структуру */

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

struct Worker

{ //описание нового типа Worker

char fio[30];

int date, code;

float salary;

}; //описание заканчивается точкой с запятой

Worker stuff[100], *ps; /* определение массива типа Worker */

/* и указателя на тип Worker */

Для инициализации структуры значения ее элементов перечисляют в фигурных скобках в порядке их описания:

Struct

{

char fio[30];

int date, code;

float salary;

}worker = {"Страусенко", 31, 215, 3400.55};

Для переменных одного и того же структурного типа определена операция присваивания, при этом происходит поэлементное копирование.

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

Доступ к полям структуры выполняется с помощью операций выбора . (точка) при обращении к полю через имя структуры и -> при обращении через указатель, например:

Worker worker, stuff[100], *ps;

...

worker.fio = "Страусенко";

stuff[8].code = 215;

ps->salary = 0.12;

Битовые поля

Битовые поля - это особый вид полей структуры. Они используются для плотной упаковки данных, например, флажков типа "да/нет". При описании битового поля после имени через двоеточие указывается длина поля в битах (целая положительная константа):

struct Options {

bool centerX:1;

bool centerY:1;

unsigned int shadow:2;

unsigned int palette:4;};

Доступ к полю осуществляется по имени. Адрес поля получить нельзя.

Объединения (union)

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

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

Объединения применяют для экономии памяти, а также для разной интерпретации одного и того же битового представления:

struct Options {

bool centerX:1;

bool centerY:1;

unsigned int shadow:2;

unsigned int palette:4;

};

union {

unsigned char ch;

Options bit;

}option={0xC4};

cout << option.bit.palette;

option.ch &= 0xF0; // наложение маски

По сравнению со структурами на объединения налагаются некоторые ограничения:

· объединение может инициализироваться только значением его первого элемента;

· объединение не может содержать битовые поля;

· объединение не может содержать виртуальные методы, конструкторы, деструкторы и операцию присваивания;

· объединение не может входить в иерархию классов.

Наши рекомендации