Несколько замечаний о назначении программирования
Язык программирования С
Основы Си++
Несколько замечаний о назначении программирования
Программирование – это техническая творческая деятельность, цель которой
заключается в решении важных для человека задач или выполнении определенных
действий с помощью компьютера. На рис. 1 представлена идеализированная схема
решения типичной задачи программирования.
Подробное описание
задачи или необходимых
действий
КОМПЬЮТЕР
Решение задачи или
выполнение действий
Рис. 1. Схема решения задачи с помощью компьютера.
В рамках такой схемы необходимыми компонентами компьютера являются
центральный процессор, устройства ввода/вывода и память (рис. 2).
Рис. 2. Основные компоненты компьютера.
Конечно, в действительности дело обстоит не так просто, как показано на
рис. 1. Например, "подробное описание (спецификация) задачи" на естественном язы-
ке для компьютера не годится (в настоящее время). Более того, для решения задачи на
компьютере недостаточно полного описания задачи, необходимо также снабдить
компьютер информацией о том, как именно следует решать задачу – т.е. составить ал-
горитм. Для описания алгоритмов решения задач или алгоритмов выполнения каких-
либо действий (например, управление роботом-манипулятором) с помощью компью-
тера применяются языки программирования.
На рис. 3 показана более подробная схема решения задачи с помощью компью-
тера, в которой учтена необходимость использования языка программирования. Ил-
люстрация этой схемы на конкретном примере приведена в таблице 1.
Существует большое количество различных языков программирования и много
способов их классификации. Например, "языками высокого уровня" считаются те
языки, синтаксис которых сравнительно близок к естественному языку, в то время как
синтаксис "низкоуровневых" языков содержит много технических подробностей, свя-
занных с устройством компьютера и процессора.
7
|
Рис. 3. Схема решения задачи на компьютере с использованием языка программирования.
Таблица 1. Основные этапы решения задачи по проверке числа на простоту.
Спецификация задачи
Алгоритм
Описание алгоритма на
Языке высокого уровня
Объектный код (внут-
Ренний код конкретного
компьютера)
Исполняемый файл для
Конкретного компьютера
Требуется определить, является ли данное число простым.
Ввести x
Для каждого целого числа z из диапазоне от 1 до x
Если остаток от деления x на z равен 0, то
вывести сообщение "число не простое" и закончить работу
Если такого числа z не найдено, то
вывести сообщение "число простое" и закончить работу
#include <iostream.h>
int main()
{
int x;
cout << "Введите число:\n";
cin>> x;
for (int z=2; z<x; z++)
if (x % z == 0)
{
cout << "Это не простое число.\n";
return 0;
}
cout << "Это простое число.\n";
return 0;
}
Двоичные команды процессора (частично)
Двоичные команды процессора (полностью)
"Императивные" или "процедурные" языки позволяют программисту описать, в
какой последовательности компьютер будет выполнять отдельные шаги алгоритма и,
таким образом, решать задачу и выдавать на экран результат. "Декларативные" языки
предназначены больше для описания самой задачи и желаемого результата, а не дей-
ствий компьютера.
"Объектно-ориентированные языки" рассчитаны на применение особого под-
хода к описанию задач, согласно которому в задаче выделяются некоторые "объекты"
с характерным для них "поведением" и взаимодействующие между собой. Один из
|
первых объектно-ориентированных языков – Смоллток, он предназначен исключи-
тельно для объектно-ориентированного программирования. В отличие от него, язык
Си++ обладает как объектно-ориентированными возможностями, так и средствами
традиционного процедурного программирования.
Радикальные приверженцы различных языков и стилей программирования
иногда делают экстравагантные заявления, выделяющие семейство языков или один
язык как исключительный и идеально подходящий для любых задач. Например, до-
вольно распространено мнение, что объектно-ориентированный подход наиболее
близок к способу решения задач человеком. По этому поводу вы со временем сможе-
те составить собственное мнение, т.к. абсолютно истинного, очевидно, нет.
2. Происхождение языка Си++
Язык Си++ был разработан в начале 1980-х гг. Бьерном Страуструпом из ком-
пании AT&T Bell Laboratories. Си++ основан на языке Си. Два символа "++" в назва-
нии – это игра слов, символами "++" в языке Си обозначается операция инкремента
(увеличение значения переменной на 1). Т.о., Си++ был задуман как язык Си с рас-
ширенными возможностями. Большая часть языка Си вошла в Си++ как подмножест-
во, поэтому многие программы на Си можно скомпилировать (т.е. превратить в набор
низкоуровневых команд, которые компьютер может непосредственно выполнять) с
помощью компилятора Си++.
При классификации языков программирования язык Си вызывает некоторые
трудности. По сравнению с ассемблером, это высокоуровневый язык. Однако Си со-
держит много низкоуровневых средств для непосредственных операций с памятью
компьютера. Поэтому язык Си отлично подходит для написания эффективных "сис-
темных" программ. Но программы других типов на Си могут оказаться довольно
сложными для понимания, и есть ряд ошибок, которым программы на Си особенно
подвержены. Дополнительные объектно-ориентированные возможности Си++ были
добавлены в Си, в частности, для устранения этих недостатков.
3. Стандарт ANSI Си++
Национальный Институт Стандартизации США (American National Standards
Institution, ANSI) разработал "официальные" стандарты для многих языков програм-
мирования, в том числе для Си и Си++. Эти стандарты стали общепринятыми и они
имеют очень большое значение. Программу, целиком написанную на ANSI Си++, га-
рантированно можно запустить на любом компьютере, для которого имеется компи-
лятор ANSI Си++. Другими словами, стандарт гарантирует переносимость программ
на языке ANSI Си++.
В действительности большинство версий Си++ представляют собой стандарт-
ный ANSI Си++, дополненный некоторыми машинно-зависимыми возможностями.
Эти специфические средства предназначены для облегчения взаимодействия про-
грамм с конкретными операционными системами. Вообще, в программах, которые
должны быть переносимыми, подобными специфическими возможностями следует
пользоваться как можно реже. В таких случаях части программы на Си++, в которых
используются не-ANSI компоненты языка, целесообразно особым образом помечать,
так, чтобы их легко можно было отделить от основной части программы и модифи-
цировать для других компьютеров и операционных систем.
4. Средаразработки Microsoft Developer Studio Visual С++
Известно, что лучший способ изучения языка программирования заключается в
том, чтобы писать на нем программы и проверять, как они работают на компьютере.
Для этого необходимы несколько программ:
Текстовый редактор, с помощью которого можно набирать и редактировать
исходный текст программ на Си++.
Компилятор. Эта программа выполняет преобразование исходного текста в
машинные команды, которые компьютер может непосредственно выпол-
нять.
Компоновщик, который собирает отдельные скомпилированные части про-
граммы в единое целое и, при необходимости, добавляет к ним компоненты
из готовых библиотек. В результате компоновки получается готовая к за-
пуску программа – исполняемый файл.
Отладчик, с помощью которого легче искать ошибки в программе. Ошибки
могут обнаружиться как при компиляции, так и во время работы програм-
мы.
В данном курсе изучения Си++ практические упражнения предполагается вы-
полнять в среде разработки программMicrosoft Developer Studio Visual C++для
IBM-совместимых ПК под управлениемWindows 95/NT. В этом пакете интегрирова-
ны редактор, компилятор, компоновщик и отладчик. Все вместе они образуют единую
удобную среду программирования. Краткое описание работы со средойVisual C++
приведено в Приложении.
5. Пример программы на Си++
Ниже приведен исходный текст простой программы на Си++.
В языке Си++ с двойной косой черты начинаются комментарии
(например, как эта строка). Компилятор игнорирует комментарии,
начиная от первой черты и до конца строки.
Второй способ записи комментариев – после косой черты со звездочкой.
После текста комментария надо поставить звездочку, а затем – косую
черту. Комментарии, записанные подобным образом, могут занимать
больше одной строки. */
/* В программе ОБЯЗАТЕЛЬНО должно быть достаточное количество
комментариев! */
/* Эта программа запрашивает у пользователя текущий год, возраст
пользователя и еще один год. Затем программа вычисляет возраст
пользователя, который будет у него во втором введенном году.*/
#include <iostream.h>
int main()
{
int year_now, age_now, another_year, another_age;
cout << "Введите текущий год и нажмите ENTER.\n";
cin >> year_now;
cout << "Введите свой возраст (в годах).\n";
cin >> age_now;
|
|
|
|
cout << "Введите год, для которого вы хотите узнать свой возраст.\n";
cin>> another_year;
another_age = another_year - (year_now - age_now);
if (another_age >= 0)
{
cout<< "В " <<another_year<< " году вам будет ";
cout<< another_age << "\n";
}
else
{
cout << "В " << another_year << " вы еще не родились!\n";
}
return 0;
Программа 5.1.
Некоторые свойства программы 5.1 являются обычными для большинства про-
грамм на Си++. Программа начинается (после комментариев) с оператора
#include <iostream.h>
Этот оператор называется "директивой include". До компилятора исходный
текст обрабатывается препроцессором – специальной программой, которая модифи-
цирует текст программы по специальным командам – директивам. Директивы пре-
процессора начинаются с символа "#". Директива include предназначена для вклю-
чения в исходный текст содержимого другого файла. Например, в программу 5.1
включается файл iostream.h, содержащий описания функций стандартной библиоте-
ки ввода/вывода для работы с клавиатурой и экраном. (Стандартные библиотеки язы-
ка Си++ будут рассматриваться позже).
Алгоритм, записанный в программе 5.1, очень простой. Поэтому структуру
программы легко представить в виде списка последовательно выполняемых команд
(операторов). Схематично программу, содержащуюся после директивы #include,
можно представить в виде:
int main()
{
Первый оператор;
...
...
Последний оператор;
return 0;
Подобная структура является общей для всех программ на Си++. Каждый опе-
ратор в теле программы завершается точкой с запятой. В хорошо разработанной
большой программе большинство операторов являются обращениями (вызовами) к
подпрограммам, которые записываются после функции main() или в отдельных
файлах. Каждая подпрограмма (функция) имеет структуру, подобную функции
main(). Но функция main() в каждой программе только одна. Именно с нее начина-
ется выполнение программы. (Подробнее функции будут рассматриваться далее.)
В конце функции main() записана строка:
|
|
return 0;
Эта строка значит "вернуть операционной системе в качестве сигнала об ус-
пешном завершении программы значение 0". Оператор возврата return применяется
не только при завершении программы, но и при завершении отдельных подпрограмм.
В любом случае этот оператор возвращает определенное значение на более высокий
уровень управления.
В программе-примере используются четыре переменные:
year_now, age_now, another_year и another_age
Переменные в программировании отличаются от математических переменных.
Они используются как символические имена "фрагментов оперативной памяти ком-
пьютера". При выполнении программы в различные моменты времени переменные
могут хранить различные значения. В программе 5.1 первое упоминание четырех пе-
ременных содержится в строке с оператором описания переменных:
int year_now, age_now, another_year, another_age;
Этот оператор уведомляет компилятор, что для хранения четырех переменных
типа "целое число" (integer – int) требуется выделить необходимое количество памя-
ти. Эта область памяти будет зарезервирована в течение выполнения оставшейся час-
ти программы. Переменные всегда должны быть описаны до первого использования.
В программировании хорошим стилем считается описание всех переменных, исполь-
зуемых в подпрограмме, в начале этой подпрограммы. В Си++ есть несколько раз-
личных типов переменных, и они будут обсуждаться немного позже.
Else
{
cout << "Вы сдали тест нечестно!\n";
total_test_score = 0;
}
...
...
Массивы
Назначение массивов
В программировании часто возникают задачи, связанные с обработкой боль-
ших объемов данных. Для постоянного хранения этих данных удобно пользоваться
файлами. Например, в программе для ввода и сортировки длинных числовых списков
данные можно ввести с клавиатуры один раз и сохранить в файле для последующего
многократного использования. Но до сих пор не было рассмотрено удобного способа
представления больших объемов данных внутри программ. Для этой цели в Си++
часто применяются массивы – простейшая разновидность структурных типов дан-
ных (о более сложных структурах данных будет говориться в следующих лекциях).
Массив – это набор переменных одного типа ("int", "char" и др.). При объявле-
нии массива компилятор выделяет для него последовательность ячеек памяти, для
обращения к которым в программе применяется одно и то же имя. В то же время мас-
сив позволяет получить прямой доступ к своим отдельным элементам.
1.1 Объявление массивов
Оператор описания массива имеет следующий синтаксис:
<тип данных><имя переменной>[<целое значение>];
Допустим, в программе требуется обрабатывать данные о количестве часов, от-
работанных в течении недели группой из 6-ти сотрудников. Для хранения этих дан-
ных можно объявить массив:
int hours[6];
или, лучше, задать численность группы с помощью специальной константы:
const int NO_OF_EMPLOYEES = 6;
int hours[NO_OF_EMPLOYEES];
Если подобные массивы будут часто встречаться в программе, то целесообраз-
ноопределитьновыйтип:
const int NO_OF_EMPLOYEES = 6;
typedef int Hours_array[NO_OF_EMPLOYEES];
Hours_array hours;
Hours_array hours_week_two;
В любом из трех перечисленных вариантов, в программе будет объявлен мас-
сив из 6 элементов типа "int", к которым можно обращаться с помощью имен:
hours[0]
hours[1]
hours[2]
hours[3]
hours[4]
hours[5]
Каждое из этих имен является именем элемента массива. Числа 0, ..., 5 назы-
ваются индексами элементов. Отличительная особенность массива заключается в том,
что его элементы – однотипные переменные – занимают в памяти компьютера после-
довательные ячейки памяти (рис. 1).
Рис. 1.. Расположение элементов массива в оперативной памяти (направление
сверху вниз соответствует возрастанию адресов ячеек памяти).
1.2 Использование элементов массивов в выражениях
С элементами объявленного массива можно выполнять все действия, допусти-
мые для обычных переменных этого типа (выше был приведен пример целочисленно-
го массива, т.е. типа "int"). Например, возможны операторы присваивания наподо-
бие:
hours[4] = 34;
hours[5] = hours[4]/2;
или логические выражения с участием элементов массива:
if (number < 4 && hours[number] >= 40) { ...
Присвоить значения набору элементов массива часто бывает удобно с помо-
щью циклов "for" или "while". В программе 1.1 в цикле у оператора запрашивается
количество часов, отработанных каждым сотрудником группы за неделю. Хотя более
естественной может показаться нумерация сотрудников от 1 до 6, а не от 0 до 5, но
необходимо помнить, что индексация массивов в Си++ начинается с 0. Поэтому про-
грамма 1.1 вычитает 1 из порядкового номера сотрудника, чтобы вычислить индекс
соответствующего элемента массива.
#include <iostream.h>
const int NO_OF_EMPLOYEES = 6;
typedef int Hours_array[NO_OF_EMPLOYEES];
int main()
{
Hours_array hours;
int count;
for ( count = 1; count <= NO_OF_EMPLOYEES; count++ )
{
cout << "Сколько часов отработал сотрудник";
cout<< " номер " << count << "?: ";
cin>> hours[count - 1];
}
return 0;
Программа 1.1.
В типичном сеансе работы программа 1.1 выведет на экран подобные сообщения:
Сколько
Сколько
Сколько
Сколько
часов
часов
часов
часов
отработал
отработал
отработал
отработал
сотрудник
сотрудник
сотрудник
сотрудник
номер
номер
номер
номер
1?:
2?:
3?:
4?:
|
|
Сколько часов отработал сотрудник номер 5?:38
Сколько часов отработал сотрудник номер 6?:37
На рис. 2. показано состояние целочисленного массива после ввода этих данных.
Рис. 2.. Состояние массива после присвоения значений его элементам.
Полезно разобраться, что произошло бы, если бы в программе 1.1 внутри цикла
"for" в операторе "cin ..." отсутствовало бы вычитание 1 из переменной "count".
Компилятор Си++ (в отличие, например, от Паскаля) не обнаруживает ошибки выхо-
да за пределы массива, поэтому участок памяти компьютера с массивом и сразу за
ним оказался бы в состоянии, показанном на рис. 3.
Рис. 3.. Ошибка выхода за пределы массива.
Другими словами, значение "37" было бы размещено в ячейке памяти, доста-
точной для хранения целого числа, которая расположена сразу после массива "hours".
Это чрезвычайно нежелательная ситуация, потому что компилятор может зарезерви-
ровать эту ячейку памяти для другой переменной (например, для переменной
"count").
Массивы могут быть любого типа, не обязательно типа "int". Ниже приведена
программа 1.2, в которой символьный ("char") массив применяется для печати собст-
венного исходного файла на экране в обратном порядке.
#include <iostream.h>
#include <fstream.h>
const int MAX_LEN = 1000;
typedef char File_array[MAX_LEN];
int main()
{
char character;
File_array file;
int count;
ifstream in_stream;
in_stream.open("prg6_1_2.cpp");
in_stream.get(character);
for ( count = 0; !in_stream.eof() && count < MAX_LEN; count++ )
{
file[count] = character;
|
|
in_stream.get(character);
}
in_stream.close();
while (count > 0)
cout<< file[--count];
return 0;
Программа 1.2.
В заголовке цикла "for" обратите внимание на условие "... && count < MAX
...", специально предусмотренное для предотвращения выхода за пределы массива.
Фрагмент программы 2.1.
Эту функцию можно сделать более универсальной, если размер массива не
фиксировать в ее определении, а передать в качестве второго параметра:
float average( int list[], int length )
{
float total = 0;
int count;
for ( count = 0 ; count < length; count++ )
total += float(list[count]);
return ( total/length );
}
В этом примере показан очень распространенный способ оформления функций,
работающих с массивами: в одном параметре передается длина массива, а в другом –
сам массив, причем в описании параметра-массива не указывается длина массива (на-
пример, "int list[]").
Параметры-массивы всегда передаются по ссылке (а не по значению), хотя при
их описании в заголовке функции символ "&" не указывается. Правило "массивы все-
|
|
гда передаются по ссылке" введено для того, чтобы функции не делали собственных
внутренних копий переданных им массивов – для больших массивов это могло бы
привести к нерациональным затратам памяти. Следовательно, как и параметры по
ссылке, рассматривавшиеся в 3-ей лекции, все изменения элементов параметров-
массивов внутри функций будут видны и в вызывающей функции.
Далее показана функция (фрагмент программы 2.2), принимающая в качестве
параметров три массива. После ее вызова каждый элемент третьего массива будет ра-
вен сумме двух соответствующих элементов первого и второго массивов.
void add_lists( int first[], int second[], int total[],
int length )
int count;
for ( count = 0; count < length; count++ )
total[count] = first[count] + second[count];
Фрагмент программы 2.2.
В целях безопасности, для защиты от случайного изменения элементов масси-
ва, в описание первых двух параметров функции добавим модификатор типа "const":
void add_lists( const int fst[], const int snd[], int tot[],
int len )
int count;
for ( count = 0; count < len; count++ )
tot[count] = fst[count] + snd[count];
Теперь компилятор не будет обрабатывать ни один оператор в определении
функции, который пытается модифицировать элементы константных массивов "fst"
или "snd". Фактически, ограничение, накладываемое модификатором "const" в дан-
ном контексте, в некоторых ситуациях оказывается слишком строгим. Например,
компилятор выдаст ошибку при обработке следующих двух функций:
void no_effect( const int list[] )
{
do_nothing( list );
}
void do_nothing( int list[] )
{
;
}
Фрагмент программы 2.3.
Ошибка компиляции программы 2.3 объясняется тем, что, хотя фактически
функция "do_nothing(...)" не выполняет никаких действий, но в ее заголовке отсут-
ствует модификатор "const". Когда компилятор в теле функции "no_effect(...)"
встретит вызов функции "do_nothing(...)", то он обратится только к заголовку функ-
|
|
|
|
ции "do_nothing(...)", и выдаст ошибку о невозможности передачи константного
массива в эту функцию.
Сортировка массивов
По отношению к массивам часто возникает задача сортировки по убыванию
или возрастанию. Разработано много различных алгоритмов сортировки, например,
пузырьковая сортировка и быстрая сортировка. В этом параграфе кратко рассматри-
вается один из простейших алгоритмов сортировки – сортировка методом выбора
наименьшего элемента.
Основные действия алгоритма для сортировки массива длиной L заключаются
в следующем:
Для каждого значения индекса i выполнить два действия:
Среди элементов с индексами от i до (L-1) найти
элемент с минимальным значением.
Обменять значения i-го и минимального элемен-
тов.
Работу этого алгоритма рассмотрим на примере сортировки массива из 5 целых
чисел:
int a[5];
Значения элементов неотсортированного массива показаны на рис. 4.
Рис. 4.. Начальное состояние массива.
В процессе сортировки методом выбора наименьшего элемента массив будет
последовательно переходить в состояния, показанные на рис. 5 (слева направо). Каж-
дое состояние получается из предыдущего путем перестановки двух элементов, поме-
ченных на рис. 5 кружками.
Рис. 5.. Последовательные шаги сортировки массива методом выбора наи-
меньшего элемента.
На Си++ алгоритм сортировки можно реализовать в виде трех функций. Функ-
ция высокого уровня будет называться "selection_sort(...)" (у нее два параметра –
сортируемый массив и его длина). Сначала эта функция вызывает вспомогательную
функцию "minimum_from(array,position,length)", котораявозвращаетиндексмини-
мального элемента массива "array", расположенного в диапазоне между индексом
"position" и концом массива. Затем для обмена двух элементов массива вызывается
функция "swap(...)".
void selection_sort( int a[], int length )
|
|
|
|
{
for ( int count = 0; count < length - 1; count++ )
swap( a[count], a[minimum_from(a,count,length)] );
}
int minimum_from( int a[], int position, int length )
{
int min_index = position;
for ( int count = position + 1; count < length; count++ )
if ( a[count] < a[min_index] )
min_index = count;
return min_index;
}
void swap( int& first, int& second )
int temp = first;
first = second;
second = temp;
Фрагмент программы 3.1.
Двумерные массивы
Массивы в Си++ могут иметь более одной размерности. В данном параграфе
кратко описаны двумерные массивы. Они широко применяются для хранения дву-
мерных структур данных, например, растровых изображений и матриц.
При обработке изображений часто используются битовые маски – вспомога-
тельные двуцветные (черно-белые) изображения, состоящие только из 0 (белая точка)
и 1 (черная точка) (или, логических значений "false" и "true"). Предположим, что
требуется маска размером 64х32 пиксела. Для описания соответствующего массива
возможны операторы:
const int BITMAP_HEIGHT = 64;
const int BITMAP_WIDTH = 32;
bool bitmap[BITMAP_HEIGHT][BITMAP_WIDTH];
При обращении к элементам массива "bitmap" необходимо указывать два ин-
декса. Например, чтобы изменить значение 2-го пиксела слева в 4-й строке надо запи-
сать оператор:
bitmap[3][1] = true;
Все сказанное во 2-м параграфе относительно одномерных массивов как пара-
метров функций верно и для двумерных массивов, но у них есть еще одна особен-
ность. В прототипах и заголовках функций можно опускать первую размерность мно-
гомерного массива-параметра (т.е. записывать пустые квадратные скобки "[]"), но все
остальные размерности надо обязательно указывать. Далее в качестве примера приве-
дена функция, заполняющая битовую карту черными пикселами.
void clear_bitmap( bool bitmap[][BITMAP_WIDTH],
int bitmap_height )
{
for ( int row = 0; row < bitmap_height; row++ )
|
|
for ( int column = 0; column < BITMAP_WIDTH; column++ )
bitmap[row][column] = false;
}
Указатели
Назначение указателей
Язык Си++ унаследовал от языка Си мощные средства для работы с оператив-
ной памятью: динамическое выделение и освобождение блоков памяти, доступ к от-
дельным ячейкам памяти по их адресам. Эти возможности делают язык Си++, как и
Си, удобным для разработки системного программного обеспечения и прикладных
программ, в которых применяются динамические структуры данных (т.е. такие, раз-
мер которых не известен на этапе компиляции и может меняться во время выполне-
ния программы).
Во всех программах из предыдущих лекций переменные объявлялись так, что
компилятор резервировал для каждой из них некоторое количество памяти (в соот-
ветствии с типом данных) еще на этапе компиляции. В начале выполнения блока опе-
раторов, внутри которого объявлена переменная, автоматически выполняется выде-
ление памяти для этой переменной, а при выходе из блока – освобождение памяти.
В данной лекции подробно рассматривается понятие указателя, – средства, ко-
торое дает программисту наиболее гибкий способ контроля операций выделе-
ния/освобождения памяти во время выполнения программ.
1.1 Объявление указателей
Указателем называется адрес переменной в оперативной памяти. Переменная
указательного типа (часто она называется переменная-указатель или просто указа-
тель) – это переменная, размер которой достаточен для хранения адреса оперативной
памяти. Переменные-указатели объявляются с помощью символа "*", который добав-
ляется после названия обычного типа данных. Например, оператор описания (его
можно прочитать как "указатель на целочисленную переменную"):
int* number_ptr;
объявляет переменную-указатель "number_ptr", которая может хранить адреса пере-
менных типа "int".
Если в программе используется много однотипных указателей, то дляих объ-
явления можно ввести новый тип. Это делается с помощью оператора описания ново-
го типа "typedef". Например, если записать оператор:
typedef int* IntPtrType;
то в программе можно будет объявлять сразу несколько переменных-указателей, не
применяя для каждой из них символ "*":
IntPtrType number_ptr1, number_ptr2, number_ptr3;
1.2 Применение символов "*" и "&" в операторах присваивания значений указателям
В операторах присваивания допускается совместное использование обычных и
указательных переменных одного типа (например, "int"). Для получения значения
переменной по ее указателю предназначена операция разыменования указателя "*". У
нее есть обратная операция – операция взятия адреса "&". Упрощенно, операция "*"
означает "получить значение переменной, расположенной по этому адресу", а опера-
ция "&" – "получить адрес этой переменной". Для пояснения смысла этих операций
далее приводится программа 1.1.
#include <iostream.h>
typedef int* IntPtrType;
int main()
IntPtrType ptr_a, ptr_b;
int num_c = 4, num_d = 7;
ptr_a = &num_c;
ptr_b = ptr_a;
/* СТРОКА 10 */
/* СТРОКА 11 */
cout<< *ptr_a << " " << *ptr_b << "\n";
ptr_b = &num_d; /* СТРОКА 13 */
cout<< *ptr_a << " " << *ptr_b << "\n";
*ptr_a = *ptr_b; /* СТРОКА 15 */
cout<< *ptr_a << " " << *ptr_b << "\n";
cout<< num_c << " " << *&*&*&num_c << "\n";
return 0;
Программа 1.1.
Программа 1.1 выдает на экран следующие сообщения:
Графически состояние программы 1.1 после выполнения операторов присваи-
вания в 10-11-й строках, 15-й и 19-й показано, соответственно, на рис. 1, 2 и 3.
Рис. 1.. Состояние про-
граммы 1.1 после выпол-
нения 10-й и 11-й строк.
Рис. 2.. Состояние про-
граммы 1.1 после выпол-
нения 13-й строки.
Рис. 3.. Состояние про-
граммы 1.1 после выпол-
нения 15-й строки.
Операции "*" и "&", в некотором смысле, являются взаимно обратными, так что
выражение "*&*&*&num_c" означает то же самое, что и "num_c".
1.3 Операторы "new" и "delete". Константа "NULL"
Рассмотрим оператор присваивания в 10-й строке программы 1.1:
ptr_a = &num_c;
Можно считать, что после выполнения этого оператора у переменной "num_c"
появляется еще одно имя – "*ptr_a". Часто в программах бывает удобно пользоваться
переменными, у которых есть только такие имена – динамическими переменными. Не-
зависимых имен у них нет. К динамическим переменным можно обращаться только
через указатели с помощью операции разыменования (например, "*ptr_a" и "*ptr_b").
Динамические переменные "создаются" с помощью оператора распределения
динамической памяти "new", а "уничтожаются" (т.е. занимаемая ими память освобож-
|
|
|
|
|
дается для дальнейшего использования) с помощью оператора "delete". Действие
этих операторов показано в программе 1.2 (она очень похожа на программу 1.1).
#include <iostream.h>
typedef int* IntPtrType;
int main()
{
IntPtrType ptr_a, ptr_b;
ptr_a = new int;
*ptr_a = 4;
ptr_b = ptr_a;
/* СТРОКА 7 */
/* СТРОКА 9 */
/* СТРОКА 10 */
/* СТРОКА 11 */
cout << *ptr_a << " " << *ptr_b << "\n";
ptr_b = new int;
*ptr_b = 7;
/* СТРОКА 15 */
/* СТРОКА 16 */
cout << *ptr_a << " " << *ptr_b << "\n";
delete ptr_a;
ptr_a = ptr_b;
/* СТРОКА 21 */
cout << *ptr_a << " " << *ptr_b << "\n";
delete ptr_a;
/* СТРОКА 25 */
return 0;
}
Программа 1.2.
Программа 1.2 печатает на экране следующие сообщения:
4 4
4 7
7 7
На рисунках 4-8 показаны состояния программы 1.2 после выполнения 7-й, 9-
11-й, 15-16-й, 21-й и 25-й строк.
Рис. 4.. Состояние про-
граммы 1.2 после выпол-
нения 7-й строки с описа-
ниями переменных.