СТРУКТУРНОЕ
ПРОГРАММИРОВАНИЕ НА С++
(Конспект лекций)
Общие сведения о языке C++
Прямым предшественником языка C++ является структурный язык про-граммирования C, который был предложен Дэнисом Ритчи (Dennis Ritchie) в начале 70-х годов ХХ века для операционной системы Unix. Впервые детальное описание языка C было выполнено в 1978 году в книге Брайана Кернигана (Brian Kernighan) и Дэниса Ритчи, в 1998 году вышел первый стандарт языка. Особенность языка C состоит в том, что он соединил в себе все основные признаки структурного языка высокого уровня (блок, переменная, операторы присваивания, ветвления, цикла) с низкоуровневыми средствами манипулирования с битами, байтами, адресами. Таким образом, язык C представляет собой мощный инструмент, который позволяет программисту иметь практически полную власть над компьютером. Однако следствием этого является невозможность поддерживать жесткий контроль за корректностью действий, инициируемых программным кодом, поэтому ответственность за последствия выполняемых программой действий возлагается на программиста.
К концу 70-х годов прошлого века стала ощущаться ограниченность структурных языков и структурной технологии программирования при работе над крупными проектами. Одним из новых технологических направлений, призванных разрешить эту проблему, явилось объектно-ориентированное программирование. В 1979 году Бьерн Страуструп (Bjarne Stroustrup) предложил объектно-ориентированное расширение расширение языка C – язык программирования, который сначала получил название «C с классами» (класс – одно из базовых понятий объектно-ориентированного программирования), а с 1983 года стал называться C++. Страуструп в качестве прямых предшественников созданного им языка называет язык C и объектно-ориентированный язык программирования Simula67. Язык C++ - высокоуровневое расширение C, объектно-ориентированный язык высокого уровня, сохранивший все основные структурные и низкоуровневые возможности языка С. С одной стороны объектно-ориентированные средства существенно расширили возможности языка при реализации крупных программных проектов, с другой стороны наличие низкоуровневых средств сохраняет за программистом основную долю ответственности за корректность программного кода. Первый стандарт языка C++, который существенно упорядочил и улучшил язык, был принят в 1998 году.
В настоящее время C++ сохраняет статус основного языка профессио-нального программирования, который является наиболее гибким и мощным инструментом при реализации крупных программных проектов. В свою очередь C++ может считаться родительским языком для таких популярных современных языков программирования как Java и C#, ориентированных на программирование в распределенной гетерогенной вычислительной среде.
Типы данных в C++
Тип данных определяет множество значений, множество операций и способ представления данных в памяти компьютера. Концепция типа данных является важнейшим базовым понятием языка программирования высокого уровня. В языках со строгой типизацией, к которым относится, например, Паскаль, существенно повышается надежность программного кода, поскольку значительное число ошибок несоответствия типов удается обнаружить еще на стадии компиляции.
Низкоуровневые возможности языка C++ не позволяют считать его строго типизированным языком, однако, тип данных является в нем одним из базовых понятий.
Скалярные типы
Основные скалярные типы C++:
char – символьный,
int – целый,
float – вещественный,
double – вещественный с двойной точностью,
bool – логический.
Могут также использоваться модификации этих базовых типов. Произ-водные (модифицированные) типы задаются с помощью 4 спецификаторов ти-па, изменяющих диапазон значений базового типа.
Спецификаторы длины:
short – короткий,
long – длинный.
Спецификаторы знака:
signed – знаковый (положительные и отрицательные значения),
unsigned –беззнаковый (только положительные значения).
Кроме того, в C++ определяется тип void – скалярный тип, множество значений которого пусто.
По умолчанию используются спецификаторы short и signed, т.е. отсутствие спецификатора длины соответствует значению «короткий», отсутствие спецификатора знака – значению «знаковый».
Особенность типа char в C++ состоит в двойственности трактовки. Зна-чения этого типа могут рассматриваться как целые числа, над которыми могут выполняться соответствующие операции, или как байтовый код символов. Значения типа char (символьные константы) заключаются в апострофы: ‘g’, ‘a’; к типу char относятся и некоторые двухсимвольные значения (спецсимволы), например, ‘\n’ - переход к следующей строке.
Замечание: в C++ строковые константы заключаются в двойные кавыч-ки, например, “stroka”. Поэтому ‘a’ – это символьное значение (литерал), а “a” – строковое значение, строка, состоящая из одного символа.
Размер типа int стандартом не определен и зависит от разрядности про-цессора и особенностей компилятора. Спецификатор short независимо от раз-рядности процессора устанавливает размер памяти для целого типа 2 байта, спецификатор long – 4 байта.
Объем памяти, соответствующий типу данных, можно определить с по-мощью функции sizeof:
sizeof(<имя типа>) – размер в байтах значения указанного типа,
sizeof <имя переменной> – размер в байтах типа, соответствующего ука-занной переменной.
Не устанавливая конкретных значений объема памяти для типов данных, стандарт определяет следующие соотношения между ними:
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long),
sizeof(float) <= sizeof(double) <= sizeof(long double).
Характеристика скалярных типов для 16-разрядного процессора
Тип Диапазон значений Размер в байтах
bool false (0), true (1) 1
char -128 .. 127 (256 символов) 1
unsigned char 0 .. 255 1
signed char -128 .. 127 1
int -32768 .. 32767 2
unsigned int 0 .. 65535 2
signed int -32768 .. 32767 2
short int (short) -32768 .. 32767 2
unsigned short int 0 .. 65535 2
signed short int -32768 .. 32767 2
long int (long) -2147483648 .. 2147483647 4
signed long int -2147483648 .. 2147483647 4
unsigned long int 0 .. 4294967295 4
float 3.4e-38 .. 3.4e+38 4
short float 3.4e-38 .. 3.4e+38 4
long float 1.7e-308 .. 1.7e+308 8
double 1.7e-308 .. 1.7e+308 8
short double 1.7e-308 .. 1.7e+308 8
long double 3.4e-4932 .. 3.4e+4932 10
Замечание: в некоторых системах программирования на C++ для типа char может быть определен диапазон значений 0 .. 255, в этом случае тип char совпадает с unsigned char, а не с signed char.
Очевидно, в этой таблице есть типы данных, имеющие различную спецификацию, но по существу ничем не отличающиеся, например, int и signed int. Это позволяет ограничиться минимальным набором наиболее простых спецификаций типа.
Тип Диапазон значений Размер в байтах
bool false (0), true (1) 1
char -128 .. 127 (256 символов) 1
unsigned char 0 .. 255 1
signed char -128 .. 127 1
int -32768 .. 32767 2
short -32768 .. 32767 2
long -2147483648 .. 2147483647 4
unsigned int 0 .. 65535 2
unsigned short 0 .. 65535 2
unsigned long 0 .. 4294967295 4
float 3.4e-38 .. 3.4e+38 4
double 1.7e-308 .. 1.7e+308 8
long double 3.4e-4932 .. 3.4e+4932 10
Перечисляемый тип
Перечисляемый тип вводится непосредственным перечислением списка значений определяемого типа. Значение перечисляемого типа представляет собой идентификатор (имя). Определение перечисляемого типа выглядит сле-дующим образом:
enum [имя типа] {<список значений>};
Примеры:
enum {spr, sum, aut, win};
или
enum season {spr, sum, aut, win};
Здесь season - имя определяемого перечисляемого типа. Значения перечисляемого типа считаются упорядоченными в соответствии с их последовательностью при определении типа. При этом каждому значению по умолчанию ставится в соответствие целое число – его порядковый номер в списке значений (нумерация значений начинается с нуля, а не с единицы).
При определении перечисляемого типа значения могут быть пронумеро-ваны в явном виде. Например,
enum season {spr, sum, aut, win};
равносильно
enum season {spr=0, sum=1, aut=2, win=3};
Можно нумеровать и не с нуля, при этом
enum season {spr=5, sum, aut, win};
равносильно
enum season {spr=5, sum=6, aut=7, win=8};
а
enum season {spr, sum, aut=4, win};
равносильно
enum season {spr=0, sum=1, aut=4, win=5};
Определение типа пользователя
В программе можно определить новый тип данных с помощью инструк-ции вида:
typedef <определение типа> <имя типа>;
Примеры:
typedef signed long int SLI;
typedef long double LD;
Комментарии
В программе на C++ могут использоваться комментарии двух видов:
// - это комментарий к одной строке, помещается в конце строки,
/*это комментарий, размещаемый в нескольких строках*/.
Константы и переменные
Константы могут использоваться как константа-значение или просто значение соответствующего типа (литеральная константа) и как именованная константа. Именованные константы и переменные идентифицируются по именам. Имя константы и переменной представляет собой идентификатор, при этом необходимо иметь в виду, что в отличие, скажем, от Паскаля в C++ строчные и прописные буквы различаются. Так, если в Паскале имена temp и Temp идентичны и ссылаются на одну и ту же переменную, то в C++ - это два различных имени, которые могут обозначать две различные переменные с совершенно разными свойствами.
Тип константы-значения определяется по ее виду. По умолчанию для целых значений используется тип int, long или unsigned long в зависимости от значения, для вещественных - double. Кроме того, тип значения можно задать с помощью суффиксов L, l, U, u, F, f. При этом суффиксы L, l соответствуют спецификатору длины long; U, u – спецификатору unsigned; F, f – типу float.
Примеры: 56l, 56L, 341u, 5.l, .34L.
Объявление переменных и констант
Прежде всего, отметим, что в C++ имеется различие между терминами «объявление» и «описание». При объявлении (определении) некоторого име-ни, например, имени переменной происходит выделение этому имени некото-рой области памяти, термин «описание» используется в том случае, когда па-мять не выделяется.
Переменные
Переменные объявляются с помощью инструкции следующего вида:
[спецификатор длины] [спецификатор знака] <тип> <список перемен-ных>;
При объявлении переменной можно инициализировать ее значение, по-местив в список переменных инициализатор - элемент вида
<имя переменной>=<значение>
или
<имя переменной> (<значение >).
Примеры:
short a, c=65, r(18), g;
signed char u=’t’, y, p;
unsigned short int a, f, i=1;
long double z(3.54), k=12., n, m(.25);
Замечание: в C++ точка с запятой не является разделителем, она завершает конструкцию и считается ее частью (например, частью оператора).
Именованные константы
При объявлении именованных констант перед указанием типа записыва-ется спецификатор const и обязательно инициализируется значение, которое в процессе работы программы не может изменяться.
Примеры:
const long int a=4, d=562;
const double r=.24, pi=3.1415;
можно и так:
const long int a(4), d(562);
Блоки. Область видимости переменных.
Аналогично Паскалю в C++ вводится понятие блока. Под блоком пони-мается часть программного кода, заключенная в фигурные скобки {…}.
Объявления имен (переменных, констант и т.д.) действуют в пределах блока, в котором они помещены. Переменные, объявленные в блоке, называются локальными переменными. Их могут использовать только инструкции, находящиеся в данном блоке. Блоки могут быть вложены друг в друга, при этом внутренний блок должен полностью размещаться внутри внешнего. Объявления имен, сделанные вне блока, действуют от места объявления и до конца файла – глобальные переменные. Если одно и то же имя объявлено внутри и вне блока (коллизия переменных) оно считается локальным, т.е. объявление вне блока игнорируется.
При объявлении переменной (или именованной константы) кроме типа может быть указан параметр, определяющий класс памяти. Класс памяти определяет время жизни и область видимости переменной. Если класс памяти не указан явно, то он определяется компилятором автоматически в соответствии с контекстом. Время жизни переменной может быть постоянным (в течение времени выполнения программы) и временным (в течение времени выполнения блока).
Класс памяти задается при определении переменных следующими 4 спе-цификаторами:
auto – автоматическая переменная. Память выделяется в стеке и освобо-ждается каждый раз при выходе из блока, в котором описана переменная. Для локальных переменных этот спецификатор используется по умолчанию.
register – аналогично auto, но память выделяется в регистрах процессора. Если такой возможности нет, используется режим auto, т.е. стековая память.
static – статическая переменная. Время жизни – постоянное, т.е. все вре-мя работы программы. В зависимости от места, где они объявлены, статические переменные могут быть и локальными, и глобальными.
extern – внешняя переменная, которая определена в другом месте про-граммы. Используется для переменных, которые могут быть доступны в лю-бом модуле программы, в котором они объявлены.
Выражения
Выражение задает правило вычисления некоторого значения. В зависи-мости от числа операндов входящие в выражение операции делятся на унарные (один операнд), бинарные (два операнда) и тернарные (три операнда). Операндами могут быть константы, переменные, функции и выражения. При записи в выражении функции список ее аргументов заключается в круглые скобки, например, sin(a) или NOD(m,n).
Примером унарной операции является операция изменения знака -5.24, бинарной – операция сложения 4+7.
Основные операции, используемые при записи выражений
Арифметические унарные
+ унарный плюс
- унарный минус (арифметическое отри-цание)
++ увеличение на 1 (инкремент)
-- уменьшение на 1 (декремент)
Арифметические бинарные
* умножение
/ деление
% деление по модулю (остаток от деле-ния)
+ сложение
- вычитание
Логические унарные
! отрицание
Логические бинарные
&& логическое И (конъюнкция)
|| логическое ИЛИ (дизъюнкция)
Операции отношения
< меньше
<= не больше
== равно
!= не равно
>= не меньше
> больше
Прочие
sizeof(…) размер типа
(…) ? … : … условное выражение
Унарные операции инкремента и декремента могут использоваться в двух формах: префиксной и постфиксной.
Префиксная форма
++х - инкремент, увеличение значения операнда на 1 до его использова-ния;
--х - декремент, уменьшение значения операнда на 1 до его использова-ния;
сначала изменяется переменная (x), а затем с учетом этого изменения вычисляется выражение, в которое входит инкремент (декремент).
Постфиксная форма
x++ - инкремент, увеличение значения операнда на 1 после его использования;
x-- - декремент, уменьшение значения операнда на 1 после его использо-вания;
сначала вычисляется выражение, в которое входит инкремент (декре-мент), при этом используется старое значение переменной (x), а затем изменя-ется переменная.
Порядок выполнения операций при вычислении выражений определяется с учетом приоритета операций и расставленных скобок. Приоритеты операций приведены в таблице, более высокий приоритет имеют группы операций, имеющих в таблице меньшее значение параметра «приоритет», т.е. расположенные выше.
Приоритет Обозначение операции Название операции
1 ++ постфиксный инкремент
-- постфиксный декремент
2 sizeof размер операнда в байтах
++ префиксный инкремент
-- префиксный декремент
! логическое НЕ
+ унарный плюс
- унарный минус
3 * умножение
/ деление
% деление по модулю
4 + сложение
- вычитание
5 < меньше
> больше
<= меньше или равно
>= больше или равно
6 == равно
!= не равно
7 && логическое И
8 || логическое ИЛИ
9 ? : условная
Примеры:
++x-y/4-(5*d-y)
(a+2*b)/y--
Условное выражение
(x<0) ? 35 : 18 //если x<0, то выражение принимает значение 35, иначе 18
(a<b) ? b : a // максимальное из значений двух переменных
Преобразование типов в выражениях
Результат операции 7/2 равен 3 (деление нацело, поскольку оба операнда целые). Если мы хотим, чтобы результат был равен 3.5, надо писать, например, так 7.0/3. Таким образом, знаком / обозначается как операция деления нацело, так и операция деления вещественных чисел. Характер операции в конкретном выражении определяется из контекста по типу операндов. Такой двойственный характер операции, обозначаемой одинаково, но интерпретируемой по-разному в зависимости от контекста, называется свойством перегрузки.
Если же мы делим две переменные x и y типа int и хотим получить ре-зультат 3.5 типа double, необходимо выполнить преобразование типа хотя бы одного из операндов: double(x)/y. Операция преобразования типа выражения в форме (double) x/y также преобразует его к типу double.
Операторы C++.
В C++ каждый оператор заканчивается знаком «точка с запятой». Любое выражение, которое заканчивается символом «точка с запятой» воспринимается компилятором как оператор.
Оператор (операция) присваивания
В C++ присваивание вводится как бинарная операция вида
<переменная> = <выражение>
в результате выполнения которой переменная, стоящая слева от знака операции принимает значение выражения, стоящего справа.
Замечание. Здесь слева от знака операции может стоять так называемое l-значение (left value), частным случаем которого является переменная. Под l-значением понимается ссылка на некоторую именованную область памяти, значение которой может быть модифицировано.
Оператором присваивания может называться конструкция вида
<переменная> = <выражение>;
которая завершается символом «точка с запятой» и в силу этого рассматривается как оператор.
Примеры:
b=(c-.86*z)+r/t;
x=4; y=x++; // здесь x будет иметь значение 5, а y – значение 4.
x=4; y=++x; // здесь x и y будут иметь значение 5.
Существует несколько модификаций операции присваивания, которые позволяют в более краткой форме записать операцию модификации перемен-ной, стоящей слева. Так, вместо x=x+a можно писать x+=a.
Аналогично можно использовать операции присваивания
-=, *=, /=, %=.
Вместо x=x+1 можно писать x+=1 или, используя инкремент, ++x.
Операцию присваивания можно использовать как выражение, значение которого равно значению левой части после выполнения операции присваива-ния, например, (y=++x)==5. Здесь при вычислении логического выражения используется значение, которое получит переменная y в результате присваивания ей значения выражения ++x.
Можно писать и так (множественное присваивание):
y=s=a+b;
при этом операции присваивания выполняются справа налево, т.е. эквивалент-ная запись выглядит так:
s=a+b; y=s;
Ввод-вывод в Си++
Ввод-вывод в стиле Си++ - ввод из потока ввода и вывод в поток вывода. Поток – это последовательность символов. Стандартный поток ввода - ввод с клавиатуры, поток вывода – вывод на дисплей. Операции обработки стандартных потоков ввода-вывода (потоковый ввод-вывод) содержатся в стандартной библиотеке потокового ввода-вывода iostream.h (так называемый заголовочный файл). Инструкция подключения библиотеки по-токового ввода-вывода:
#include <iostream.h>
помещается в начале файла с программным кодом.
Для стандартного ввода с клавиатуры используется поток ввода cin, для вывода на дисплей – поток вывода cout.
Ввод с клавиатуры
Операция ввода значения обозначается знаком >>. Ввод с клавиатуры (поток ввода cin) выполняется следующим оператором:
cin >>переменная >> переменная … >> переменная >>;
Помещаемые в поток ввода значения должны разделяться пробелом и быть согласованы с указанными в операторе переменными по количеству, по-рядку следования и типам.
Вывод на дисплей
Операция вывода значений обозначается знаком <<. Вывод на дисплей (поток вывода cout) выполняется следующим оператором:
cout <<выражение <<выражение …<<выражение;
Здесь следует напомнить, что частным случаем выражения является пе-ременная, константа или значение. Необходимо учитывать, что никакие разде-лители выводимых на экран значений автоматически в поток вывода не поме-щается, поэтому их надо включать в список выводимых значений (например, пробел ‘ ‘ или переход к началу следующей строки ‘\n’).
Примеры:
cout <<"\ninpute symbol: ";
cin >>symbol;
cout <<"number of symbol " <<symbol <<" is " <<number;
cout <<"input a b:"; cin >>a >>b;
cout <<"/ninput next chislo: "; cin >> cur;
Здесь наличие символа ‘/n’ в потоке вывода означает переход на сле-дующую строку.
Условный оператор
Общий вид условного оператора:
if (<выражение-условие >)
<оператор_1>;
else
<оператор_2>;
Если выражение-условие истинно (не равно нулю), выполняется опера-тор_1, в противном случае выполняется оператор_2.
Если после if или else необходимо выполнить несколько операторов, то аналогично Паскалю необходимо заключить их в фигурные скобки, т.е. использовать конструкцию «составной оператор» или «блок», при этом конструкция вида {…} обладает всеми правами простого оператора. Все операторы, входящие в конструкцию {…}, включая последний оператор, должны заканчиваться точкой с запятой, однако после самой конструкции точка с запятой не ставится, поскольку ограничителем для блока и составного оператора является закрывающаяся фигурная скобка. Заметим, что блок отличается от составного оператора наличием в нем объявлений или описаний локальных объектов (переменных, констант и т.д.).
В этом случае условный оператор будет выглядеть так:
if (<выражение-условие>)
{<оператор>; <оператор>; …;}
else
{<оператор>; <оператор>; …;}
Пример (вычисление наибольшего из двух чисел):
if(x<y)
max=y;
else
max=x;
Замечание. Аналогичный результат можно получить, используя услов-ное выражение:
max=(x<y)? y : x
Как и в Паскале допускается сокращенная форма условного оператора без else:
if (<выражение-условие>)
<оператор>;
В отличие от Паскаля, где в качестве выражения-условия может исполь-зоваться только логическое выражение, в C++ им может быть кроме логического любое арифметическое выражение целого типа или символьное. В этом случае значению «ложь» ставится в соответствие нулевое значение, а любое ненулевое трактуется как «истина».
Допускается вложение условных операторов. В конструкции с вложен-ным условным оператором вида
if (a<b)
{++x; y+=h;}
else if (a>b)
y*=5;
else
{y=x+h; x++;}
действует правило: «каждое else относится к ближайшему предшествующему ему if».
В качестве примера использования в условном операторе выражения-условия числового типа, значение которого может трактоваться как false (ну-левое значение) или true (ненулевое значение) можно привести алгоритм деления двух целых чисел:
int a,b;
cin >>a >>b;// ввод чисел a и b
if (b)
y=a/b;// b не равно нулю
else
cout << “\nделение на нуль”;// b равно нулю
Оператор выбора
Условный оператор осуществляет ветвление на два направления. Во многих случаях приходится сталкиваться с ситуацией, когда необходимо выбирать один вариант из нескольких возможных, т.е. осуществлять множественное ветвление. Использование в таких случаях вложенного условного оператора зачастую приводит к весьма громоздким конструкциям. В Паскале для более компактного описания такого множественного ветвления имеется оператор выбора case. Аналогичный оператор множественного ветвления в C++ имеет вид:
switch (<выражение-селектор>)
{
case <константа>: <оператор>; <оператор>; …; break;
case <константа>: <оператор>; <оператор>; …; break;
case <константа>: <оператор>; <оператор>;
default : <оператор>; <оператор>; …;
}
Здесь выражение-селектор (или переменная-селектор) может быть любо-го скалярного типа, кроме вещественного. Сначала вычисляется выражение-селектор, затем полученное значение этого выражения ищется среди констант целого типа, стоящих после case. Если вычисленное значение найдено, выполняются операторы, начиная с соответствующего case, и до default; в противном случае выполняются операторы, идущие после default.
Оператор break выполняет переход на первый оператор, стоящий после оператора switch. Его присутствие в конструкции не является обязательным, однако, без этого оператора последовательно выполнялись бы все операторы, соответствующие значениям селектора, следующим за вы-бранным.
Ветвь default не является обязательной, однако, из соображений безопасности ее не рекомендуется опускать.
Пример 1:
switch (x)
{
case 0: y+=h; break;
case 1: y*=3; z++; break;
case 2: y/=2; d=(h+y)/5;
default : y++;
}
Пример 2 (вычисление оценки в четырехбалльной системе по заданному значению стобалльного рейтинга):
int k;
cout <<”\nвведите ваш рейтинг: “; cin >> k;
k/=10;
switch (k)
{
case 0:
case 1:
case 2:
case 3:
case 4: cout <<”\nВаша оценка – неудовлетворительно”; break;
case 5:
case 6: cout <<”\nВаша оценка – удовлетворительно”; break;
case 7:
case 8: cout <<”\nВаша оценка – хорошо”; break;
case 9:
case 10: cout <<”\nВаша оценка – отлично”;
default : cout <<”недопустимое значение рейтинга”;
}
Операторы цикла
Оператор цикла используется для описания повторяющихся (цикличе-ских) вычислений.
Как и в Паскале в Си++ можно использовать 3 формы оператора цикла: с предусловием, с постусловием и с параметром (цикл for).
Цикл с предусловием
while (<выражение-условие>)
<оператор>;
Оператор - тело цикла повторяется до тех пор, пока выражение-условие сохраняет значение «истина». Таким образом, выражение-условие формулируется как условие продолжения цикла. При этом каждый шаг цикла начинается с вычисления выражения-условия, поэтому, если уже на первом шаге оно приняло значение «ложь», тело цикла не выполняется ни разу. В теле цикла должны входить конструкции, изменяющие хотя бы одну переменную, входящую в выражение-условие. В противном случае выполнение оператора цикла никогда не закончится (зацикливание).
Если тело цикла должно включать в себя несколько операторов, необходимо оформлять его как составной оператор или блок, заключая в фигурные скобки.
Пример (вычисление суммы последовательности целых чисел с числом нуль в качестве признака конца последовательности):
int S=0;
cout << “\ninput a: ”; cin >> a;
while (a!=0)
{
S+=a;
cout << “input a: ”; cin >> a;
}
Цикл с постусловием
do
<оператор>
while (<выражение-условие>);
Цикл с постусловием отличается от цикла с предусловием только тем, что на каждом шаге сначала выполняется оператор-тело цикла, а затем вычисляется выражение-условие и если оно принимает значение «ложь», выполнение оператора цикла завершается. Таким образом, в отличие от цикла с предусловием здесь оператор-тело цикла всегда выполняется хотя бы один раз.
Если тело цикла должно включать в себя несколько операторов, его не-обходимо оформлять как составной оператор или блок, заключая в фигурные скобки.
Пример (вычисление суммы последовательности целых чисел, последнее из которых равно нулю):
int S=0;
do
{
cout << “input a: ”; cin >> a;
S+=a;
}
while (a!=0);
Принципиальное отличие этого оператора в C++ от Паскаля состоит в том, что и в цикле с предусловием, и в цикле с постусловием итерации продолжаются до тех пор, пока выражение-условие сохраняет значение «истина», т.е. отлично от нуля. Таким образом, это выражение должно быть сформулировано как условие продолжения цикла.
Цикл с параметром ( цикл for)
for (<инициализация>; <выражение-условие>; <модификатор>)
<оператор>;
Модификатор – это выражение, которое изменяет модифицируемую пе-ременную цикла.
Если тело цикла должно включать в себя несколько операторов, его не-обходимо оформлять как составной оператор или блок, заключая в фигурные скобки.
Пример (вычисление суммы последовательности n целых чисел):
S=0;
for (i=1; i<=n; i++)
{
cout << “input a: ”; cin >> a;
S+=a;
}
Здесь модификатором является постфиксное выражение i++, согласно которому на каждом шаге значение переменной цикла i увеличивается на 1.
Можно инициализировать в цикле и переменную S:
for (S=0, i=1; i<=n; i++)
{
cout << “input a: ”; cin >> a;
S+=a;
}
Более того, внутри цикла можно и описать тип переменной, необходимо лишь помнить, что это описание будет действовать только в теле цикла.
for (S=0, int i=1; i<=n; i++)
{
cout << “input a: ”; cin >> a;
S+=a;
}
Оператор for может использоваться и без любого из разделов, т.е. может отсутствовать любая из конструкций: инициализатор- условие-модификатор.
Замечание: в отличие от Паскаля в теле цикла разрешается изменять па-раметр цикла и после завершения цикла значение параметра считается опреде-ленным.
Оператор break выполняет выход из цикла (досрочное завершение оператора), а оператор continue - досрочное завершение очередного шага цикла (досрочный переход к следующему шагу).
Структура программы на C++. Функции.
Программа на языке C++ состоит из функций, описаний и директив пре-процессора. Одна из функций должна иметь имя main, программа начинает выполняться с первого оператора этой функции. Тело функции является блоком. В отличие от Паскаля функции не могут быть вложенными.
Простейшее определение функции выглядит так:
<тип> <имя функции> (<спецификация формальных параметров>)
{
<операторы>
}
Функция может не возвращать никакого значения, в этом случае в каче-стве ее типа указывается void (например, функция main).
Возврат вычисленного значения функции осуществляется оператором
return <выражение>;
Если функция имеет тип void, оператор return может отсутствовать.
При вызове функции вместо списка формальных параметров помещается список фактических параметров:
<имя функции> (<список фактических параметров>);
Списки формальных и фактических параметров должны быть согласова-ны по количеству, порядку следования и типам.
В файле с исходным кодом до вызова функции должно быть помещено определение вызываемой функции или хотя бы ее описание (прототип функ-ции). Прототип состоит из заголовка функции со спецификацией формальных параметров, после которого ставится точка с запятой.
Замечание: в прототипе могут отсутствовать имена формальных параметров, достаточно лишь указать их типы.
Пример 1: Вычислить сумму последовательности отличных от нуля це-лых чисел, после которой следует число нуль (признак конца последовательности).
#include <iostream.h>
void main()
{
int a, S=0;
cout << “\nВведите a: ”; cin >> a;
while (a!=0)
{
S+=a;
cout << “\nВведите a: ”; cin >> a;
}
cout << “Сумма=” <<S <<’\n’;
}
Пример 2: В последовательности из n целых чисел вычислить произведение чисел, попадающих в отрезок [b, c].
#include <<iostream.h>
void main()
{
int n, a, b, c, P=1;
cout << “\nВведите n: ”; cin >> n;
cout << “\nВведите b c: ”; cin >> b >>c;
for ( int i=1; i<=n; i++)
{
cout << “Введите a: ”; cin >> a;
if ((a>=b)&&(a<=c)) P*=a;
}
cout << “Произведение=” <<P <<’\n’;
}
Замечание: в приведенном алгоритме предполагается, что хотя бы одно из чисел попадает в указанный отрезок.
Структурированные типы данных в C++
Массивы.
Как и в Паскале, массив представляет собой упорядоченную последова-тельность данных одного типа, занимающих последовательные ячейки памяти. Для упорядочивания используется индексация элементов массива, т.е. приписывание каждому элементу значения индекса. В отличие от Паскаля, где для выбора индексного типа имеется значительная свобода, в C++ индекс представляет собой целое положительное число или ноль. Таким образом, элементы массива упорядочиваются приписыванием каждому из них порядкового номера, причем нумерация начинается с нуля, а не с единицы, т.е. первый элемент массива имеет индекс ноль. Общий вид описания массива:
<базовый тип> <имя массива> [<целая константа>];
Пример:
long int a, b[5], c;
const int n=10;
double s[n];
Здесь определен массив b, состоящий из 5 элементов типа long int: b[0] b[1] b[2] b[3] b[4] и массив s из 10 элементов типа double: s[0] s[1] … s[9].
При определении массив может быть инициализирован:
int a[5]={2, -3, 5, 4, 38};
Во многих случаях удобно бывает определить новый тип-массив:
typedef long int mas[10];
mas a,b;
В программе элементы массива представляют собой переменные с индексами.
Пример (ввод и суммирование массива).
#include <iostream.h>
void main()
{
const int n=10;
int a[n], S=0;
cout << “\ninput array: ”;
for (int i=0; i<n; i++)
{
cin >> a[i];
S+=a[i];
}
cout << “Summa=” <<S <<’\n’;
}
При работе с массивами необходимо иметь в виду, что в C++ не контро-лируются значения индексов на предмет выхода за пределы массива. Это означает, что если при обращении к массиву в программе будет сформировано значение индекса, превышающее допустимое, могут быть изменены значения каких-либо других переменных, занимающих смежную с массивом область памяти. Контроль этой ситуации полностью возлагается на программиста.
Двумерные массивы определяются так:
int a [5][3];
В памяти двумерные массивы располагаются по строкам, т.е. так, что быстрее изменяется второй индекс. Это правило распространяется и на массивы большей размерности. При инициализации элементы двумерного массива располагаются в соответствующем порядке, т.е по строкам.
Пример:
int a [2][3]={{1, 6, -3}, //первая строка
{5, 24, 0}}; //вторая строка
Все операции над массивами, включая ввод-вывод и присваивание, вы-полняются поэлементно.
Пример (ввод двумерного массива):
for (int i=0; i<5; i++)
for (int j=0; j<3; j++)
cin >>a[i][j];
Символьные строки.
Символьная строка является аналогом типа string Паскаля. В C++ она объявляется как массив символов, но при этом дополняется нуль-символом ‘\0’, что позволяет определять текущую длину строки:
char str[10]=”alfa”; // str[0]=’a’ str[1]=’l’ str[2]=’f’ str[3]=’a’ str[4]=’\0’
При инициализации можно было бы опустить длину строки:
char str[]=”alfa”;
но при этом под нее было бы выделено не 10 байт, а только 5.
Строки можно использовать в операторах ввода (cin) и вывода (cout), однако при вводе строки с клавиатуры необходимо учитывать, что оператор >> в качестве признака конца строки рассматривает символы пробела, табуляции или Enter и, встретив один из этих символов, завершает ввод строки. Поэтому, если в ответ на запрос
Введите строку
следующего фрагмента программы:
char text[50];
cout <<”/nВведите строку”;
cin >> text;
cout <<’Вы ввели/n’ <<text;
вы введете
Иван Петров
на экран будет выведено
Вы ввели
Иван
Эта проблема может быть разрешена использованием библиотечной функции
gets(<имя массива>)
стандартной библиотеки stdio.h, которая должна быть подключена инструкцией #include <stdio.h>.
Поскольку строка является массивом, для нее не определены операции присваивания и сравнения. Эти операции могут выполняться поэлементно или с помощью функций стандартной библиотеки string.h, которая должна быть подключена инструкцией #include <string.h>:
strcpy(str1, str2) // копирование строки str2 в строку str1,
strlen(str) // длина строки без ноль-символа,
strcmp(str1, str2) // сравнение строк: совпадают – false(0), иначе – true(1),
strcat(str1, str2) // конкатенация строк (str1+str2).
В операции копирования длина строки str1 должна быть на один символ больше, чем str2.
Структуры.
Структура (struct) в C++ является аналогом типа record в Паскале. Структура позволяет группировать данные разных типов. Определение типа struct выглядит так:
struct <имя типа> {
<тип поля> <имя поля>;
<тип поля> <имя поля>;
……………..
<тип поля> <имя поля>;
}
Пример:
struct anketa {
char name[20];
enum {man, woman} pol;
int born;
}
anketa a[5], b;
Можно определять переменные типа struct, не определяя новое имя типа, т.е. используя анонимный тип. Соответствующее описание переменных типа struct будет выглядеть так:
struct {
<тип поля> <имя поля>;
<тип поля> <имя поля>;
……………..
<тип поля> <имя поля>;
} <список переменных>;
Пример:
struct {
char name[20];
enum {man, woman} pol;
int born;
} a[5], b;
Здесь, как и в предыдущем примере определен массив a[5] из 5 эле-ментов структурного типа и простая переменная b того же структурного типа. Одновременно с определением структурного типа можно описать и переменные этого типа, например:
struct anketa {
char name[20];
enum {man, woman} pol;
int born;
} a[5], b;
Обращение к элементам структуры, аналогично Паскалю выглядит так:
<имя переменной типа struct >.<имя поля>
Пример:
a[2].name=”Petrov”;
a[2].pol=man;
a[2].born=1985;
b.name=”Orlova”;
b.pol=woman;
b.born=1990;
Переменные типа struct можно использовать в операторе присваивания, но при этом действует правило именной эквивалентности типов.
Передача параметров функции. Перегрузка функций
Рассмотрим заголовок функции
int area(int a, int b)
здесь для формальных параметров – переменных целого типа a и b ис-пользуется механизм передачи «по значению», т.е. при вызове функции ей пе-редаются только значения соответствующих фактических параметров.
Поэтому, если в качестве фактических параметров указаны имена пере-менных, значения этих переменных функцией не могут быть изменены, т.к. соответствующая область памяти ей недоступна. Для того чтобы функция могла изменять значения переменных, передаваемых в качестве фактических параметров, необходимо передавать ей не значения, а адреса, в которых переменные-фактические параметры размещаются в памяти, т.е. использовать механизм передачи по ссылке (адресу). Для этого используется операция получения адреса переменной: &<имя переменной>. В этом случае рассмотренный заголовок функции будет выглядеть так:
int area(int& a, int& b)
Если используется механизм передачи параметров по значению, фактический параметр может быть выражением соответствующего типа, при этом вычисляется значение выражения, которое и передается в функцию. Если же параметр передается по ссылке, фактическим параметром может быть только переменная.
Пример (вычисление площади прямоугольника):
#include <iostream.h>
void get_dim(int& len, int& wid); // прототип функции get_dim
int area(int len, int wid); // прототип функции area
void main()
{
int a,b;
get_dim(a,b); // ввод длины и ширины
cout <<"\nlen=" <<a <<" wid=" <<b;
cout <<"\narea=" <<area(a,b); // вывод площади
}
void get_dim(int& len, int& wid) // определение функции get_dim
{
cout <<"\ninput len win: "; cin >>len >>wid;
}
int area(int len, int win) // определение функции area
{
return len*win;
}
Одним из характерных свойств объектно-ориентированного языка, в том числе и C++, является полиморфизм – использование одного имени для выполнения различных действий над различными объектами. Применительно к функциям это называется перегрузкой. Со свойством перегрузки мы уже сталкивались при рассмотрении основных операций C++, например, у деления существует только одно имя, "/", но его можно применять для деления как целых, так и вещественных значений.
Перегруженные функции имеют одинаковые имена, но разные списки параметров и возвращаемые значения.
Пример: (вычисление площади прямоугольника и объема параллелепипеда - полиморфизм):
#include <iostream.h>
void get_dim(int& len, int& wid); //прототип-1 функции get_dim
void get_dim(int& len, int& wid, int& hig); //прототип-2 функции get_dim
int ar_vol(int len, int wid); // прототип-1 функции ar_vol
int ar_vol(int len, int wid, int hig); // прототип-2 функции ar_vol
void main()
{
int a,b,c;
get_dim(a,b); //вызов-1 функции get_dim
cout <<"\nlen=" <<a <<" wid=" <<b;
cout <<"\narea=" <ar_vol(a,b); //вызов-1 функции ar_vol
get_dim(a,b,c); //вызов-2 функции get_dim
cout <<"\nlen=" <a <<" wid=" <<b <<" hig=" <<c;
cout <<"\nvolume=" <<ar_vol(a,b,c); //вызов-2 функции ar_vol
}
void get_dim(int& len, int& wid) // определение-1 функции get_dim
{
cout <<"\ninput len win: "; cin >>len >>wid;
}
void get_dim(int& len, int& wid, int& hig) //определение-2 функции get_dim
{
cout <<"\ninput len wid hig: "; cin >>len >>wid >>hig;
}
int ar_vol(int len, int win) // определение-1 функции ar_vol
{
return len*win;
}
int ar_vol(int len, int wid, int hig) // определение-2 функции ar_vol
{
return len*wid*hig;
}
Текстовые файлы в C++. Файловые потоки ввода-вывода.
Поток – это последовательный логический интерфейс, который используется для чтения (ввода) данных из файла и записи (вывода) данных в файл.
Мы уже имели дело со стандартными потоками ввода-вывода cin и cout при вводе данных с клавиатуры и выводе на экран дисплея. Важным свойством потока является последовательное помещение элементов в поток. При этом потоки ввода cin и вывода cout являются однонаправленными: получить (прочитать) данные в программе можно из потока ввода, отправить (записать) данные из программы можно в поток вывода. Стандартные потоки ввода cin и вывода cout подключаются к клавиатуре и экрану дисплея соответственно сразу после начала выполнения программы. Модель потока ввода-вывода можно представить как полубесконечную ленту с перемещающимся над ней «окном», через которое мы получаем доступ к текущему элементу потока. За последним элементом помещается метка конца потока. Такая модель соответствует файлу последовательного доступа. При этом данные удобно хранить и работать с ними в символьном виде, поэтому мы будем рассматривать текстовые файлы, элементами которых являются символы. Такие файлы могут быть подготовлены и просмотрены с помощью любого текстового процессора (редактора).
Как и в Паскале, данные в C++ могут быть введены в программу из любого файла, а не только из стандартного потока ввода cin. Аналогично обстоит дело и с выводом данных. Таким образом, кроме стандартных потоков cin и cout в программе можно работать с файловыми потоками ввода-вывода.
Список функций для работы с файловыми потоками хранится в библио-теке (заголовочном файле) fstream.h. Поэтому во всех рассматриваемых ниже фрагментах программ предполагается, что в начале программы есть соответствующая директива #include:
#include <fstream.h>
Создание потока ввода-вывода
Прежде чем начать работать с потоком необходимо его создать. Поток ввода создается инструкцией
ifstream <имя потока ввода>;
Поток вывода создается инструкцией
ofstream <имя потока вывода>;
Пример:
ifstream input;
ofstream output;
эти инструкции создают поток ввода input и поток вывода output.
Открытие и закрытие файла
После создания потока его можно подключить к файлу (открыть файл) инструкцией
<имя потока>.open (<имя файла>);
Здесь <имя файла> - текстовая константа или переменная.
Например, для подключения потока ввода ifstream с именем input к файлу data.txt надо выполнить инструкцию
input.open (“data.txt”);
действие которой аналогично инструкции подготовки файла к чтению в Паскале reset.
Аналогичная инструкция
output.open (“data.txt”);
подключит поток вывода output к файлу data.txt – файл подготовлен к записи данных. Важно отметить, что, как и при выполнении оператора подго-товки файла к записи в Паскале rewrite, прежние данные из файла data.txt бу-дут удалены.
Для отключения потока ввода-вывода от файла надо выполнить инструкцию закрытия файла:
<имя потока>.close ();
Так, инструкции
input.close();
output.close ();
отключают потоки ввода input и вывода output от файла data.txt, к которому они были подключены в предыдущих примерах. При закрытии файла вывода в конец файла записывается метка конца end_of_file.
Обработка ошибок
При выполнении операций над файлами, например, открытие и закрытие файлов, достаточно высока вероятность возникновения ошибочных ситуаций.
Один из простейших способов контроля корректности выполнения фай-ловых операций заключается в вызове функии
<имя потока>.fail()
например,
input.fail()
Эта инструкция выполняется как вызов булевской функции, которая возвращает значение false (0), если последняя операция с потоком input завершилась успешно и возвращает значение true (1), если последняя операция с потоком input привела к ошибке (например, была попытка открытия несуществующего файла). В случае возникновения ошибки поток может быть поврежден, поэтому работу с ним продолжать нельзя.
Распознать ошибку можно и с помощью перегруженной операции отри-цания. Выражение !<имя потока> также принимает значение false (0), если последняя операция с потоком завершилась успешно и принимает значение true (1), если последняя операция с потоком привела к ошибке.
Пример:
ifstream input;
input.open (“data.txt”);
if(!input) exit(1); // завершение работы программы
функция exit() описана в библиотеке stdlib.h.
Чтение-запись символов в файл
После того, как файл открыт для ввода данных, из него можно считывать отдельные символы. Чтение текущего символа из потока ввода выполняется инструкцией
<имя потока>.get(<имя переменной>);
Например, после выполнения инструкции input.get(ch); произойдет сле-дующее: переменной ch будет присвоено значение текущего символа (шаг 1), и поток input будет подготовлен для чтения следующего символа (шаг 2).
Аналогично после того, как файл открыт для вывода данных, в него можно записывать отдельные символы. Запись символа в поток вывода выполняется инструкцией
<имя потока>.put(<имя переменной>);
Например, после выполнения инструкции output.put(ch); произойдет следующее: в поток output будет помещено значение символьной переменной ch (шаг 1), и поток output будет подготовлен для записи следующего символа (шаг 2). Вместо имени переменной можно указать значение выводимого в по-ток символа: инструкция output.put(‘t’); выводит в поток output символ ‘t’.
При чтении данных из файла (из потока ввода, связанного с файлом) не-обходимо уметь определять конец файла. В C++, как и в Паскале, для этой цели используется функция eof(). Логическое выражение <имя потока>. eof() принимает значение true (1), если конец файла достигнут и значение false (0), если можно продолжать чтение.
Пример (посимвольное чтение данных из файла и вывод на экран):
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
void main()
{
char ch;
ifstream input;
input.open(“a:/text.txt”);
if(!input)
{cout <<”\nmistake of open\n”; exit(1);} //Ошибка открытия файла
while (!input.eof()){
input.get(ch); //чтение очередного символа
cout <<ch; //вывод символа на экран
}
input.close();
}
Ввод-вывод с преобразованием типов
Для того чтобы программа могла работать с числовыми данными, кото-рые записаны в текстовом файле, необходимо при вводе выполнять преобразование символьной записи чисел (внешнее представление) в их внутреннее представление в памяти компьютера, т.е. выполнять преобразование типов данных при вводе. Аналогично при выводе на экран числа должны преобразовываться из внутреннего (двоичного) во внешнее представление. В Паскале такое преобразование автоматически выполнялось операторами ввода-вывода (read/write).
В C++ преобразование числовых данных из внешнего (символьного) представления во внутреннее (двоичное) выполняет оператор >>. Обратное преобразование выполняет оператор <<. Эти операторы мы уже использовали при работе со стандартными потоками ввода-вывода cin и cout.
Инструкция ввода данных из потока ввода с преобразованием типов вы-глядит так:
поток ввода >>переменная >>переменная … >>переменная;
Например,
input >>a >>b >>c;
Аналогично выглядит инструкция вывода данных из потока вывода с преобразованием типов:
поток вывода <<выражение <<выражение … <<выражение;
Например,
output <<a <<b+c <2*m[5];
Пример (вывод-ввод в файл числовой последовательности):
#include <iostream.h>
#include <fstream.h>
#include <stdlib.h>
void main()
{
int n, a;
cout <<"\ninput n: "; cin >>n;
ifstream input;
ofstream output;
output.open (“D:/data.txt”);
if(!input)
{cout <<”\nmistake of open\n”; exit(1);} //Ошибка открытия файла
for (int i=1; i<=n; i++){
cout <<"\ninput next number: "; cin >>a;
output <<a;
if (i!=n) output <<' '; // output.put(' ')
}
output.close ();
input.open ("D:/data.txt");
input >>a;
while (!input.eof()){
cout <<a <<' '; input >>a;
}
input.close ();
}
При выводе в текстовый файл чисел необходимо предусмотреть разделитель. В приведенном примере в качестве разделителя в файл вы-водится пробел.
Чтение символьных строк из потока ввода
Для ввода строк из потока ввода (например, с клавиатуры) пригоден оператор >>, но его применение ограничено, поскольку этот оператор считает пробелы разделителями. Допустим, в программе содержатся операторы:
char name[20];
cout << “/ninput name: “;
cin >> name;
Если в сеансе работы с программой в ответ на запрос
input name:
ввести текст: Петя Иванов, переменной name будет присвоено только значение “Петя”, т.к. оператор >> считает пробел разделителем, который сиг-нализирует о завершении ввода значения.
Очевидно, использовать здесь посимвольный ввод с помощью рассмот-ренной выше функии get тоже неудобно.
Для ввода символьных строк часто более удобной оказывается функция getline(...), имеющая 2 параметра:
<имя потока>. getline(<имя строковой переменной>, n);
Здесь n - длина строки без учета нуль-символа ‘\0’.
Например, оператор:
input.getline(str, 80);
позволяет считать из потока ввода input строку с пробелами длиной до 79 символов (последний, 80-й символ строки – служебный нуль-символ). Аналогичный оператор
cin.getline(str, 80);
позволяет получить такую же строку от пользователя с клавиатуры.
Указатели
Во всех рассмотренных нами программах использовалось статическое распределение памяти: переменные объявлялись так, что компилятор резерви-ровал для каждой из них некоторое количество памяти (в соответствии с типом данных) еще на этапе компиляции. Язык C++ включает в себя мощные средства для работы с оперативной памятью: динамическое выделение и освобождение памяти, доступ к отдельным ячейкам памяти по их адресам, механизм указателей, который позволяет работать с динамическими структурами данных, размер которых не известен на этапе компиляции и может меняться во время выполнения программы.
Указатель представляет собой адрес переменной в оперативной памяти. Переменная указательного типа (переменная-указатель или просто указатель) – это переменная, размер которой достаточен для хранения адреса оперативной памяти.
Объявление указателей
Переменные-указатели объявляются с помощью символа *, который до-бавляется после названия обычного типа данных (или перед именем перемен-ной-указателя). Например, описание:
int* a;
объявляет переменную-указатель a, которая может принимать значение адреса переменной целого типа int. Здесь int является базовым типом для переменной-указателя a.
Несколько указателей одного типа можно объявить так:
int *a, *b, *c;
Однако если в программе используется много указателей одного типа, целесообразно определить новый тип:
typedef int* ref_int;
ref_int a, b, c;
Здесь объявлен указательный тип ref_int и три переменные a, b, c этого типа.
Операции над указателями
С указателями можно использовать 4 арифметические операции: +, -, ++ и --, т.е. 2 бинарные (сложения и вычитания) и 2 унарные (инкремент и декре-мент), а также операции отношения: ==, < и >. При этом важно иметь в виду, что арифметические операции над указателями выполняются по особым правилам.
Поскольку значение указателя представляет собой адрес переменной не-которого типа, при выполнении арифметических операций происходит изменение (смещение) адреса на величину кратную длине соответствующего базового типа. Так, если переменная-указатель описана как short *a, и в процессе выполнения программы переменная a приняла значение 1600 (адрес памяти), то после выполнения инкремента a++ она примет значение 1602, т.е. адрес увеличится на величину, равную длине памяти, занимаемой базовым типом short. Иначе говоря, произойдет смещение указателя на адрес, расположенный непосредственно за значением переменной, на которую указывает a. К переменной-указателю можно прибавлять (или вычитать) целое число. При этом происходит смещение указателя на соответствующее базовому типу число адресов памяти.
При выполнении операций отношения необходимо учитывать, что допускается только сравнение указателей, имеющих один и тот же базовый тип.
Для получения значения переменной по ее указателю используется унар-ная операция разыменования (разадресации) *. Выражение *<переменная-указатель> представляет собой переменную (значение переменной), адрес ко-торой равен значению переменной-указателя. По существу операция разыменования является обратной рассмотренной ранее операции взятия адреса &. Таким образом, операция * означает «получить значение переменной, расположенной по этому адресу», а операция & – «получить адрес этой переменной». Отсюда, например, следует, что выражение *&*&*&a принимает значение переменной a. Операция разыменования позволяет использовать указатели в выражениях базового для указателя типа и в операторе присваивания.
Пример:
include <iostream.h>
void main()
{
int a=5, b=8;
int *m, *n;
m=&a; n=&b;
*m+=5;
*n=*m+b;
cout <<”\na= <<a <<” b=” <b;
}
В результате выполнения этой программы на экран будет выведено:
a=10 b=18.
Можно считать, что в программе для переменной a использовалось два имени: a и *m, а для переменной b: b и *n.
Следует заметить, что операции * и & имеют более высокий приоритет, чем любая из арифметических операций, кроме унарного минуса, приоритет которой такой же, как у этих двух операций.
Динамические переменные
Аналогично Паскалю механизм указателей C++ позволяет работать в программе с динамическими переменными. При этом динамическая перемен-ная создается инструкцией
<переменная-указатель> =new <базовый тип>;
Этот оператор выделяет память для динамической переменной базового типа, присваивая указателю адрес этой переменной. Доступ к созданной дина-мической переменной можно получить с помощью операции разыменования.
Для освобождения памяти, выделенной под динамическую переменную, выполняется оператор
delete <переменная-указатель>;
Пример:
include <iostream.h>
void main()
{
int *a, *b;
a=new int;
b=new int;
*a=5;
*b=8;
*a+=5;
*b+=*a;
cout <<”\na= <a <<” b=” <<b;
delete a;
delete b;
}
Как и в предыдущем примере в результате выполнения этой программы на экран будет выведено:
a=10 b=18.
Отличие от предыдущего примера состоит в том, что после использова-ния двух целых динамических переменных занимаемая ими память была осво-бождена.
С использованием механизма указателей и динамических переменных связана одна опасная ситуация, которую необходимо учитывать. Если значение указателя не определено, то применение к нему операции разыменования недопустимо и может привести к непоправимым последствиям. К сожалению в C++ отсутствует встроенный механизм проверки таких ситуаций. Для того чтобы повысить безопасность программы, работающей с указателями, можно использовать нулевое значение указателя NULL. Нуль-значение целесообразно присваивать переменной-указателю одновременно с ее определением (инициализация указателя):
int *a=NULL, *b=NULL;
Вместо NULL можно использовать целое число 0:
int *a=0, *b=0;
Нуль-значение NULL описано в библиотечном заголовочном файле <stddef.h>, который должен быть подключен в начале программы инструкцией
#include <stddef.h>
Если для создания динамической переменной не хватает свободной опе-ративной памяти, в результате выполнения оператора
a=new int;
переменной-указателю a автоматически будет присвоено значение NULL. Этот факт можно использовать для проверки успешного создания динамической переменной.
Пример:
#include <iostream.h>
#include <stdlib.h>
#include <stddef.h>
void main()
{
int *a=NULL;
a = new int;
if (a== NULL) {
cout << “\nНедостаточно оперативной памяти”;
exit(1);
}
……
}
Указатели можно передавать в качестве параметров функций.
Указатели и массивы
Передача массивов в качестве параметров функции.
Поскольку массивы являются одной из наиболее распространенных структур, используемых для размещения данных, во многих программах целесообразно оформлять обработку хранящихся в массиве данных в виде функций. При этом массивы должны передаваться этим функциям в качестве параметров. Возможны несколько способов передачи параметров-массивов.
В качестве примера рассмотрим простейшую функцию вычисления сум-мы массива.
Пример (вычисление суммы):
#include <iostream.h>
const int n=10;
typedef int array[n];
int sum(array mas);
void main()
{
array a;
cout <<”\ninput array: “;
for(int i=0; i<n; i++)
cin >>a[i];
cout <<”\nsumm of array “;
for(i=0; i<n; i++)
cout <a[i] <<’ ‘;
cout <<”equil “ <<sum(a) <<’\n’;
}
int sum(array mas)
{
int s=0;
for(int i=0; i<n; i++) s+=mas[i];
return s;
}
Недостаток такого подхода состоит в том, что при определении типа массива array мы зафиксировали размер массива и, следовательно, он фиксирован и в определении функции. Размер массива можно не фиксировать при определении функции, передавая его в качестве параметра, что позволяет сделать функцию более универсальной.
Пример:
#include <iostream.h>
int sum(int mas[], int n); //здесь размер передается как параметр
void main()
{
const int n=10;
int a[n];
cout <<”\ninput array: “;
for(int i=0; i<n; i++)
cin >>a[i];
cout <<”\nsumm of array “;
for(i=0; i<n; i++)
cout <<a[i] <<’ ‘;
cout <<”equil “ <sum(a, n) <<’\n’;
}
int sum(int mas[], int n)
{
int s=0;
for(int i=0; i<n; i++) s+=mas[i];
return s;
}
В этом примере показан распространенный способ определения функций, работающих с массивами: в одном параметре передается длина массива, а в другом – сам массив, причем в описании параметра-массива не указывается его длина. Очевидно, такой подход более универсален.
Параметры-массивы всегда передаются по ссылке, а не по значению, хо-тя при их описании в заголовке функции символ & не указывается. Это правило введено для того, чтобы функции не делали собственных внутренних копий переданных им массивов – для больших массивов это могло бы привести к нерациональному использованию памяти. Следовательно, все изменения, которые функция произведет над элементами массива, будут видны и в вызывающей функции. Это позволяет определять функции, результатом которых будет изменение элементов массива, например, нормирование массива делением всех его элементов на их сумму.
Однако в этом есть и определенная опасность случайно изменить элементы массива, содержащего входные данные. Один из возможных способов защиты от такого случайного изменения – использование при описании формальных параметров для массивов, элементы которых не должны изменяться функцией, спецификатора const. Например, прототип функции, которая вычисляет массив c как сумму двух других массивов a и b, может выглядеть так:
void sum_mas (const int a[], const int b[], int c[], int n);
Теперь компилятор не будет обрабатывать ни один оператор в определении функции sum_mas, который пытается модифицировать элементы константных массивов a или b.
Связь указателей и массивов. Операции над указателями
В C++ существует тесная связь между указателями и массивами. Масси-вы реализованы так, как будто имя массива является указателем, значение ко-торого равно адресу первого элемента массива (элемента с нулевым индексом). Таким образом, конструкции <имя массива>, <&имя массива> и <&имя массива[0]> ссылаются на один и тот же адрес памяти. Поэтому, если, например, поместить в программу объявление целочисленного указателя:
int a[10];
int* ref_mas;
то ему можно присвоить адрес массива (т.е. адрес первого элемента мас-сива):
ref_mas =a;
После выполнения этого оператора обе переменные ref_mas и a будут указывать на целочисленную переменную a[0], т.е. имена a[0], *a и *ref_mas являются тремя различными именами одной и той же переменной.
У переменных a[0], a[1], a[2], и т.д. также появляются новые имена:
*a, *(a + 1), *(a + 2) ...
или
*ref_mas, *( ref_mas + 1), *( ref_mas + 2) ...
В данном случае +2 означает: добавить к адресу указателя смещение, со-ответствующее двум целым значениям.
Теперь функцию вычисления суммы элементов массива из последнего примера можно определить так:
int sum(int* mas, int n)
{
int s=0;
for(int i=0; i<n; i++) s+=mas[i];
return s;
}
или так:
int sum(int* mas, int n)
{
int s=0;
for(int i=0; i<n; i++) s+=*(mas+i);
return s;
}
В этих примерах мы выполняли арифметические операции (сложение) над указателями. К указателям часто применяется сложение и вычитание (в том числе операции инкремента и декремента ++ и --), а умножение и деление не используются. Значения однотипных указателей можно вычитать друг из друга.
Главное, что нужно запомнить относительно сложения и вычитания зна-чений из указателя - в выражениях с указателями указывается не число, ко-торое нужно вычесть (или добавить) из адреса, а количество переменных за-данного типа, на которые нужно сместить адрес.
Динамические массивы
Рассмотренные правила создания и уничтожения динамических переменных скалярных типов int, char, double и т.п. распространяются и на динамические массивы.
Динамический массив из 10 целых чисел можно создать с помощью оператора new следующим образом:
int* ref_mas;
ref_mas=new int[10];
Теперь обращаться к элементам созданного динамического массива можно так:
ref_mas[0], ref_mas[1], ... ref_mas[9]
или:
*ref_mas, *( ref_mas + 1), ... *( ref_mas + 9)
Для уничтожения динамического массива применяется оператор delete c квадратными скобками []:
delete[] ref_mas;
Скобки [] играют важную роль; они сообщают оператору, что требуется уничтожить все элементы массива, а не только первый.
Динамические массивы, как и обычные, можно передавать в качестве па-раметров функций.
Пример (вычисление суммы динамического массива):
#include <iostream.h>
int sum(int* mas, int n);
void main()
{
int *a, n;
cout <<”\ninput n: “; cin >>n;
a=new int[n];
cout <<”\ninput array: “;
for(int i=0; i<n; i++)
cin >>a[i];
cout <<”\nsumm of array “;
for(int i=0; i<n; i++)
cout <<a[i] <<’ ‘;
cout <<”equil “ <<sum(a, n) <<’\n’;
delete[] a;
}
int sum(int* mas, int n)
{
int s=0;
for(int i=0; i<n; i++) s+=mas[i];
return s;
}
Работа со списочными структурами
Линейный однонаправленный список
Так же как и в Паскале, используя указатели, в C++ можно создавать ли-нейные динамические структуры – линейные списки. В качестве примера рас-смотрим простейшую линейную динамическую структуру – линейный однона-правленный список. Базовый элемент для линейного однонаправленного списка должен содержать два поля: указательного типа (указатель на следующий базовый элемент) и информационное поле - для записи значения, помещаемого в список. Будем рассматривать списки, информационное поле которых имеет целый тип int. В этом случае базовый тип для линейного однонаправленного списка можно определить так:
struct node {
node * next;
int el;
};
Здесь структура node определена рекурсивно, при этом в явном виде указательный тип (указатель на node) не определен. Лучше определить базовую структуру node так, чтобы указатель на нее был также определен явно. При этом по аналогии с прототипом (предописанием) функции используют прототип структуры:
struct node; //прототип структуры
typedef node* ref; //явное определение указательного типа ref
struct node{ //определение структуры
ref next;
int el;
};
Такая базовая структура позволяет построить линейный однонаправлен-ный список, поскольку содержит поле-указатель. Список может быть сформи-рован как с заглавным элементом, так и без заглавного элемента.
Пусть теперь в программе объявлен указатель на структуру node и создана сама динамическая структура:
ref a;
a=new node; //динамическая структура
Доступ к полям этой динамической структуры возможен с помощью операции разадресации:
(*a).next – указатель на следующий элемент,
(*a).el - информационное поле.
Скобки здесь обязательны, поскольку в C++ действует мнемоническое правило: «суффикс всегда сильнее префикса», т.е. применительно к нашему примеру при отсутствии скобок сначала выполнялась бы точка, а затем - звез-дочка.
Более кратко эти выражения записываются с помощью специального операто-ра ->, который предназначен для доступа к полям структуры через указатель:
a->next – указатель на следующий элемент,
a->el - информационное поле.
В качестве примера рассмотрим операции формирования линейного однона-правленного списка с заглавным элементом с последующим выводом его на экран.
Пример (формирование и вывод линейного однонаправленного списка):
#include <iostream.h>
struct node;
typedef node* ref;
struct node{
ref next;
int el;
};
void main()
{
ref list=NULL, cur=NULL; // указатели на первый и текущий элемент
list=new node; //инициализация первого элемента
cout <<"\ninput number: "; cin >>(* list).el; //ввод первого числа
(*list).next=NULL; //указатель на следующий элемент
cur=list; //указатель на текущий элемент списка
while (a!=0) { // последнее число равно нулю – признак конца ввода
(*cur).next=new node; //инициализация следующего элемента
cur=(*cur).next; //перевод указателя на следующий элемент
cout <<"\ninput number: "; cin >>(*cur).el; //ввод следующего числа
}
(*cur).next=NULL;//поле-указатель последнего элемента
cur=list; //указатель на первый элемент
while (cur!=NULL){ //элемент существует
cout <<=(*cur).el <<' '; //вывод числа на экран
cur=(*cur).next; //переход к следующему элементу списка
}
}
Недостаток такой организации списка состоит в том, что первый элемент, содержащий данные в информационном поле el, формируется вне цикла. Избежать этого неудобства позволяет использование списка с заглавным элементом, в котором информационное поле формируемого вне цикла первого элемента остается свободным или используется для размещения некоторого идентификатора списка. Это позволяет все помещаемые в список данные обрабатывать однотипно внутри цикла. Списки с заглавным элементом во многих случаях более удобны для обработки и поэтому используются очень часто. В дальнейшем мы также в большинстве случаях будем рассматривать именно такие списки.
Алгоритм формирования и вывода линейного однонаправленного списка с заглавным элементом может быть записан в следующем виде:
#include <iostream.h>
struct node;
typedef node* ref;
struct node{
ref next;
int el;
};
void main()
{
ref list=NULL, cur=NULL; // указатели на заглавный и текущий эле-мент
list=new node; //инициализация заглавного элемента
cur=list; //указатель на текущий элемент списка
(*list).next=NULL; //указатель на следующий элемент
do{
(*cur).next=new node; //инициализация следующего элемента
cur=(*cur).next; //перевод указателя на следующий элемент
cout <<"\ninput number: "; cin >>(*cur).el; //ввод следующего числа
} while (a!=0); // последнее число равно нулю – признак конца ввода
(*cur).next=NULL;//поле-указатель последнего элемента
cur=(*list).next; //указатель на первый элемент после заглавного
while (cur!=NULL){ //элемент существует
cout <<(*cur).el <<' '; //вывод числа на экран
cur=(*cur).next; //переход к следующему элементу списка
}
}
Здесь использован цикл с постусловием, поскольку признак конца вводимой последовательности (число 0) входит в эту последовательность, т.е. является ее последним членом.
Аналогично может быть определена любая операция над линейным однонаправленным списком: инициализация, поиск, вставка и удаление элемента. Будем обозначать эти операции InitList, SeekList, InsList и DelList соответственно. Будем рассматривать эти операции применительно к списку с заглавным элементом в предположении, что типы ref и node определены глобально.
void InsertList(ref &cur, int a){ //вставка после cur значения a
ref q;
q=new node; //новый элемент
q->el=a; //занесение значения a в новый элемент
q->next=cur->next;
cur->next=q; //включение нового элемента в список
}
Динамический стек
Пример (стек - помещение и выбор слова):
#include <iostream.h>
struct node;
typedef node* ref;
struct node{
ref next;
char el;
};
enum bool{false, true};
void InitStack(ref &top);
bool CheckStack(ref top);
void PutStack(ref &top, char a);
char OutStack(ref &top);
void main()
{
ref Top;
char a;
InitStack(Top);
cout <<"\ninput word: ";
do{
cin >>a;
PutStack(Top,a);
}
while (a!='.');
while (CheckStack(Top)==true){
a=OutStack(Top);
cout <<a;
}
}
void InitStack(ref &top){
top=NULL;
}
bool CheckStack(ref top){
if (top==NULL) return false;
else return true;
}
void PutStack(ref &top, char a){
ref q;
q=new node;
(*q).el=a;
(*q).next=top;
top=q;
}
char OutStack(ref &top){
ref q;
char a=(*top).el;
q=top;
top=(*top).next;
delete q;
return a;
}
Оценка алгоритмов
Пример 1 (поиск максимального/минимального элемента массива):
const int n;
int mas[n];
….
int max=mas[0];
for (int i=1; i<n; i++)
if (mas[i]>max) max=mas[i];
Пример 2 (поиск первого/последнего элемента по условию):
const int n;
int mas[n], a;
….
int i=0;
while ((i<n) && (mas[i]!=a)) i++;
Пример 3 (замена каждого элемента суммой последующих):
Вариант 1
const int n;
int mas[n];
….
for (int i=0; i<n; i++){
mas[i]=0;
for (int j=i+1; j<n; j++) mas[i]+=mas[j];
}
Вариант 2
const int n;
int mas[n];
….
int sum=0;
for (int i=n-1; i>=0; i--){
int p=sum; sum+=mas[i]; mas[i]=p;
}
Пример 4 (умножение матриц):
const int n;
typedef array int[n][n];
array a,b,c;
….
for (int i=0; i<n; i++)
for (int j=0; j<n; j++){
c[i][j]=0;
for (int k=0; k<n; k++)
c[i][j]= c[i][j]+a[i][k]*b[k][j];
}
Рекурсия
Пример (факториал и НОД):
#include <iostream.h>
long int fact(int n);
void fact(int n, long int &nfact);
int NOD(int m, int n);
void main()
{
int m, n;
long int k;
cout <<"\ninput n: "; cin >>n;
cout <<"\nFACT=" <<fact(n) <<'\n';
fact(n, k);
cout <<"\nFACT=" <<k <<'\n';
cout <<"\ninput m n:"; cin >>m >>n;
cout <<"\nNOD(" <<m <<',' <<n <<")=" <<NOD(m,n) <<'\n';
}
long int fact(int n){
if (n<0) {cout <<"\nmistake "; return n;}
else if (n==0) return 1;
else return n*fact(n-1);
}
void fact(int n, long int &nfact){
long int m;
if (n<0) {cout <<"\nmistake "; nfact=n;}
else if (n==0) nfact=1;
else {fact(n-1,m); nfact=n*m;}
}
int NOD(int m, int n){
if (m==n) return m;
else if (m<n) return NOD(m, n-m);
else return NOD(m-n, n);
}
Пример (Ханойская башня):
#include <iostream.h>
void Hanoi(char x, char y, char z, int n);
void main()
{
int n;
cout <<"\ninput n: "; cin >>n;
Hanoi('A', 'B', 'C', n);
}
void Hanoi(char x, char y, char z, int n)
{
if (n>0){
Hanoi(x, z, y, n-1);
cout <<"\nfrom "<<x<<" to "<<y;
Hanoi(z, y, x, n-1);
}
}
Пример (тур коня):
#include <iostream.h>
const n=8;
int D[n][n];
void step(int i, int x, int y, int &q);
void main()
{
int i, j, x0, y0, q;
for(i=0; i<n; i++)
for(j=0; j<n; j++) D[i][j]=0;
cout <<"input xo yo: "; cin >>x0,y0;
D[x0][y0]=1;
step(2,x0,y0,q);
if(q==1)
for(i=0; i<n; i++){
cout <<'\n';
for(j=0; j<n; j++) cout <<D[i][j] <<' ';
}
else cout <<" no result";
}
void step(int i, int x, int y, int &q)
{
int u,v,k,q1;
int dx[8]={2,1,-1,-2,-2,-1,1,2}, dy[8]={1,2,2,1,-1,-2,-2,-1};
k=-1;
do{
k++; q1=0;
u=x+dx[k]; v=y+dy[k];
if((D[u][v]==0) && (u>=0) && (u<n) && (v>=0) && (v<n)){
D[u][v]=i;
if(i==n*n) q1=1;
else{
step(i+1,u,v,q1);
if (q1==0) D[u][v]=0;
}
}
}
while ((q1==0) && (k<7));
q=q1;
}
Поиск
Пример (алгоритмы поиска):
Линейный поиск
#include <iostream.h>
const n=10, n1=11;
int SeekLine(int* mas, int n, int a, int &ind);
int SeekBar(int* mas, int n, int a, int &ind);
void main()
{
int num, a, m[n], m1[n1];
cout <<"\ninput "<<n<<" number: ";
for(int i=0;i<n;i++){
cin >>m[i]; m1[i]=m[i];
}
cout <<"input a: "; cin >>a;
if (SeekLine(m, n, a, num)==0) cout <<"\nNO\n";
else cout <<'\n'<<num+1<<'\n';
num=0;
if (SeekBar(m1, n, a, num)==0) cout <<"\nNO\n";
else cout <<'\n<num+1<<'\n';
}
int SeekLine(int* mas, int n, int a, int &ind){
ind=0;
while((mas[ind]!=a) && (ind<=n)) ind++;
if(ind<n) return 1;
else return 0;
}
int SeekBar(int* mas, int n, int a, int &ind){
mas[n]=a;
ind=0;
while(mas[ind]!=a) ind++;
if(ind<n) return 1;
else return 0;
}
Двоичный поиск (дихотомия)
#include <iostream.h>
const n=10;
int Dixotom(int* mas, int n, int a, int &ind);
void main()
{
int num, a, m[n];
cout <<"\ninput "<<n<<" number: ";
for(int i=0; i<n; i++)
cin >>m[i];
cout <<"input a: "; cin >>a;
num=0;
if (Dixotom(m, n, a, num)==0) cout <<"\nNO\n";
else cout <<'\n'<<num+1<<'\n';
}
int Dixotom(int* mas, int n, int a, int &ind){
int l=0, r=n-1;
do{
ind=(l+r)/2;
if(mas[ind<a) l=ind+1;
else r=ind-1;
}
while((l<=r) && (mas[ind]!=a));
if(mas[ind]==a) return 1;
else return 0;
}
Сортировка
Пример (алгоритмы внутренней сортировки):
#include <iostream.h>
#include <cstdlib>
#include <iostream>
using namespace std;
void IncludeSort(int* mas, int n);
void SelectSort(int* mas, int n);
void BubbleSort(int* mas, int n);
void ModBubbleSort(int* mas, int n);
void QuickSort(int* mas, int n);
int main()
{
const int n=10;
int i;
int a[n];
cout <<"\nВведите " <<n <<" чисел: ";
for(i=0;i<n;i++) cin >>a[i];
cout <<"\nНомер алгоритма сортировки: ";
cin >>i;
switch(i){
case 1: IncludeSort(a,n); break;
case 2: SelectSort(a,n); break;
case 3: BubbleSort(a,n); break;
case 4: ModBubbleSort(a,n); break;
case 5: QuickSort(a,n); break;
default: cout << "\nНекорректный номер\n";
}
for(i=0;i<n;i++) cout <<' '<<a[i];
cout <<'\n';
system("PAUSE");
return EXIT_SUCCESS;
}
void IncludeSort(int* mas,int n){
int b;
for (int i=1;i<n;i++){
b=mas[i];
int j=i-1;
while ((j>=0) && (mas[j]>b)){
mas[j+1]=mas[j];
j--;
}
mas[j+1]=b;
}
}
void SelectSort(int* mas, int n){
int m,b;
for(int i=0;i<n-1;i++){
m=i;
for(int j=i+1;j<n;j++)
if(mas[j]<mas[m]) m=j;
b=mas[i]; mas[i]=mas[m]; mas[m]=b;
}
}
void BubbleSort(int* mas,int n){
int b;
for(int i=1;i<n;i++)
for(int j=n-1;j>=i;j--)
if(mas[j]<mas[j-1]){
b=mas[j]; mas[j]=mas[j-1];mas[j-1]=b;
}
}
void ModBubbleSort(int* mas,int n){
int b,sig=0;
int i=1;
while((i<n) && (sig==0)){
sig=1;
for(int j=n-1;j>=i;j--)
if(mas[j]<mas[j-1]){
b=mas[j];mas[j]=mas[j-1];mas[j-1]=b;sig=0;
}
i++;
}
}
void Sort(int* mas,int n,int l,int r){
int i,j,b;
i=l; j=r;
int m=mas[(l+r)/2];
do{
while(mas[i]<m) i++;
while(mas[j]>m) j--;
if(i<=j){
b=mas[i]; mas[i]=mas[j]; mas[j]=b;
i++; j--;
}
}
while (i<=j);
if(l<j) Sort(mas,n,l,j);
if(i<r) Sort(mas,n,i,r);
}
void QuickSort(int* mas,int n){
Sort(mas,n,0,n-1);
}