Глава 6. СОЗДАНИЕ ФУНКЦИЙ...81
Глава 7. ХРАНЕНИЕ ПОСЛЕДОВАТЕЛЬНОСТЕЙ В МАССИВАХ...92
Глава 8. ПЕРВОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ В С++...105
Глава 9. ВТОРОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ...117
Глава 10. ОТЛАДКА ПРОГРАММ НА С++...128
В этой части...
Выполнять операции сложения или умножения ( и даже логические операции ) — это одно, а писать настоящие программы — это нечто совсем иное. Из этой части вы узнаете о том, как стать настоящим программистом.
Программу BUDGET1 вы сможете найти на прилагаемом компакт-диске. Эта программа демонстрирует концепцию функционального программирования. После того как вы разберётесь с рассматриваемыми в этой части концепциями, имеет смысл обратиться к указанной программе и соответствующей документации.
_________________
80 стр. Часть 2. Становимся функциональными программистами
В этой главе...
►Написание и использование функций 81
►Определение прототипов функций 89
►Хранение переменных в памяти 90
►Использование заголовочных файлов 91
Очень часто при написании программ возникает необходимость разделить большую программу на меньшие части, отлаживать которые намного легче. Программы из предыдущих глав слишком малы, чтобы можно было по-настоящему оценить пользу такого разделения. Но реальные программы из больших проектов состоят из тысяч ( и даже миллионов! ) строк. Поэтому большие программы просто невозможно написать, не разбивая их на отдельные модули.
С++ позволяет разделить код программ на части, называемые функциями. Сами функции могут быть записаны и отлажены отдельно от остального кода программы.
Возможность разбивать программу на части с последующей отладкой каждой функции в отдельности существенно снижает сложность создания больших программ. Этот подход является, по сути, простейшей формой инкапсуляции ( см. главу 15, "Защищённые члены класса: не беспокоить!", где вопросы инкапсуляции рассматриваются подробнее. )
Функции лучше всего изучать на примерах. Эта часть начинается с программы FunctionDemo, которая показывает, как упростить рассмотренную в главе 5 программу NestDemo, определив дополнительную функцию. На примере программы FunctionDemo я постараюсь объяснить, как определять и использовать функции. Эта программа будет служить образцом для их дальнейшего изучения.
NestDemo содержит два цикла. Во внутреннем цикле суммируется последовательность введённых пользователем чисел. Он включён во внешний цикл, который повторяет процесс, пока пользователь не изъявит желания его прекратить. Разделение этих двух циклов делает программу более наглядной.
В программе FunctionDemo показано, как упростить программу NestDemo с помощью создания функции sumSequence( ).
_________________
81 стр. Глава 6. Создание функций
«Согласно синтаксису С++ справа от имени функции должны присутствовать две круглые скобки. В них обычно указываются параметры функций.»
[Советы]
/* FunctionDemo — демонстрация использования функций. */
/* Внутренний цикл программы оформлен как отдельная функция */
#include
#include
#include
using namespace std ;
/* sumSequence — суммирует последовательность чисел, введённых с клавиатуры, пока пользователь не введёт отрицательное число. Возвращает сумму введённых чисел */
int sumSequence( void )
{
/* Бесконечный цикл */
int accumulator = 0 ;
for ( ; ; )
{
/* Ввод следующего числа */
int value = 0 ;
cout << "Введите следующее число: " ;
cin >> value ;
/* Если оно отрицательное... */
if ( value < 0 )
{
/* ...тогда выходим из цикла */
break ;
}
/* ...иначе добавляем число к переменной accumulator */
accumulator = accumulator + value ;
}
/* Возвращаем значение суммы */
return accumulator ;
}
int main( int argc , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ". 1251" ) ; /* печать русских текстов */
cout << "Эта программа суммирует последовательности "
<< "чисел. Каждая\nпоследовательность"
<< "заканчивается отрицательным числом.\n"
<< "Ввод серий завершается вводом "
<< "двух отрицательных чисел подряд\n" ;
/* Суммируем последовательности чисел... */
int accumulatedValue ;
_________________
82 стр. Часть 2. Становимся функциональными программистами
for ( ; ; )
{
/* Суммируем последовательности чисел, введённых с клавиатуры */
cout << "\nВведите следующую последовательность\n" ;
accumulatedValue = sumSequence( ) ;
if ( accumulatedValue == 0 ) { break ; }
/* Вывод общей суммы на экран */
cout << "\nОбщая сумма равна "
<< accumulatedValue
<< "\n" ;
} ;
cout << "Программа завершена\n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Определение этой функции начинается с инструкции int sumSequence( void ). Заключённый в фигурные скобки блок кода называется телом функции. Как видите, тело функции sumSequence( ) идентично внутреннему циклу программы NestDemo.
Главная часть программы сконцентрирована в фигурных скобках, следующих после объявления функции main( ). Эта часть кода очень напоминает программу NestDemo.
Различие состоит в том, что внутри функции main( ) содержится выражение accumulatedValue = sumSequence( ). В правой части этого выражения вызывается функция sumSequence( ). Возвращаемое функцией значение сохраняется в переменной accumulatedValue, а затем выводится на экран. Главная программа выполняет цикл до тех пор, пока значение суммы, возвращаемой внутренней функцией, остаётся отличным от 0. Нулевое значение говорит о том, что пользователь закончил вычисление сумм последовательностей.
Программа FunctionDemo выделяет внутренний цикл в функцию sumSequence( ). Такое выделение отнюдь не произвольно. Функция sumSequence( ) играет свою, отдельную роль.
«Хорошую функцию можно легко описать одним предложением с минимальным количеством слов "и" и "или". Например, функция sumSequence( ) суммирует последовательность целочисленных значений, введённых пользователем. Это определение весьма компактно и легко воспринимается.»
[Советы]
Сравните это определение с описанием программы ContinueDemo: суммирование последовательности положительных значений И генерация ошибки при вводе пользователем отрицательного значения И вывод суммы И повтор выполнения до тех пор, пока пользователь не введёт две суммы нулевой длины.
_________________
83 стр. Глава 6. Создание функций
Вот как выглядит пример работы рассмотренной программы.
Эта программа суммирует последовательности чисел. Каждая
последовательность заканчивается отрицательным числом.
Ввод серий завершается вводом двух отрицательных чисел подряд
Введите следующую последовательность
Введите следующее число: 1
Введите следующее число: 2
Введите следующее число: 3
Введите следующее число: -1
Общая сумма равна 6
Введите следующую последовательность
Введите следующее число: 1
Введите следующее число: 2
Введите следующее число: -1
Общая сумма равна 3
Введите следующую последовательность
Введите следующее число: -1
Программа завершена
Press any key to continue...
Функции являются первоосновой программ С++. Поэтому каждый программист должен отчётливо понимать все нюансы их определения, написания и отладки.
Функцией называют логически обособленный блок кода С++, имеющий следующий вид:
< тип возвращаемого значения > name( < аргументы функции > )
{
// . . .
return < выражение > ;
}
Аргументами функции называются значения, которые можно передать ей при вызове. В возвращаемом значении указывается результат, который функция возвращает по окончании работы. Например, в вызове функции возведения в квадрат square ( 10 ) 10 — это аргумент, а возвращаемое значение равно 100.
И аргументы, и возвращаемое значение функции необязательны. Если какой-либо элемент отсутствует, вместо него используется ключевое слово void. Значит, если вместо списка аргументов используется void, то при вызове функция не получает никаких аргументов ( как и в рассмотренной программе FunctionDemo ). Если же возвращаемый тип функции — void, то вызывающая программа не получает от функции никакого значения.
В программе FunctionDemo используется функция с именем sumSequence( ), которая не имеет аргументов и возвращает значение целочисленного типа.
«Тип аргументов функции по умолчанию — void, поэтому функцию int fn( void ) можно записать как int fn( ).»
[Советы]
Использование функции позволяет работать с каждой из двух частей программы FunctionDemo в отдельности. При написании функции sumSequence( ) я концентрирую внимание на вычислении суммы чисел и не думаю об остальном коде, вызывающем эту функцию.
_________________
84 стр. Часть 2. Становимся функциональными программистами
При написании функции main( ) я работаю с возвращаемой функцией sumSequence( ) — суммой последовательности ( на этом уровне абстракции я знаю только, что выполняет функция, а не как именно она это делает ).
Функция sumSequence( ) возвращает целое значение. Функции могут возвращать значение любого стандартного типа, например double или char ( типы переменных рассматриваются в главе 2, "Премудрости объявления переменных" ).
Если функция ничего не возвращает, то возвращаемый тип помечается как void.
«Функции различаются по типу возвращаемого значения. Так, целочисленной функцией называют ту, которая возвращает целое значение. Функция, которая ничего не возвращает, известна как void-функция. Далее приведён пример функции, выполняющей некоторые действия, но не возвращающей никаких значений.»
[Советы]
void echoSquare( )
{
cout << "Введите значение:" ;
cin >> value ;
cout << " \n" << value*value << "\n" ;
return ;
}
Сначала управление передаётся первой инструкции после открывающей скобки, затем поочередно выполняются все команды до инструкции return ( которая в данном случае не требует аргумента ). Эта инструкция передаёт управление вызывающей функции.
«Инструкция return в void-функциях является необязательной. Если она отсутствует, то выполнение функции прекращается при достижении закрывающей фигурной скобки.»
[Советы]
Функции без аргументов используются редко, так как связь с такими функциями односторонняя, т.е. осуществляется только посредством возвращаемых значений. Аргументы функций позволяют установить двустороннюю связь — через передаваемые параметры и возвращаемые значения.
Функции с одним аргументом...85
Аргументами функции называют значения, которые передаются функции во время вызова. В следующем примере определяется и используется функция square( ), которая возвращает квадрат переданного в качестве аргумента числа типа double:
/* SquareDemo — демонстрирует использование функции с аргументом */
#include
#include
#include
using namespace std ;
_________________
85 стр. Глава 6. Создание функций
/* square — возвращает квадрат аргумента doubleVar — введённое значение return — квадрат doubleVar */
double square( double doubleVar )
{
return doubleVar * doubleVar ;
}
/* sumSequence — суммирует последовательность чисел, введённых с клавиатуры и возведённых в квадрат, пока пользователь не введёт отрицательное число. Возвращает сумму квадратов введённых чисел */
double sumSequence( void )
{
/* Бесконечный цикл */
double accumulator=0.0 ;
for ( ; ; )
{
/* Ввод следующего числа */
double dValue = 0 ;
cout << "Введите следующее число: " ;
cin >> dValue ;
/* Если оно отрицательное... */
if ( dValue < 0 )
{
/* ...то выходим из цикла */
break ;
}
/* ...иначе вычисляем квадрат числа */
double value = square( dValue ) ;
/* Теперь добавляем квадрат к accumulator */
accumulator = accumulator + value ;
}
/* Возвращаем сумму */
return accumulator ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
cout << "Эта программа суммирует "
<< "несколько последовательностей чисел.\n"
<< "Ввод каждой последовательности "
<< "заканчивается\nвводом "
<< "отрицательного числа. \n"
<< "Последовательности вводятся "
<< "до тех пор, пока\nне встретятся "
<< "два отрицательных числа\n" ;
/* Продолжаем суммировать числа... */
_________________
86 стр. Часть 2. Становимся функциональными программистами
double accumulatedValue ;
for ( ; ; )
{
/* Суммируем последовательность чисел, введённых с клавиатуры */
cout << " \nВведите следующую последовательность\n" ;
accumulatedValue = sumSequence( ) ;
/* Выход из цикла */
if ( accumulatedValue <= 0.0 ) { break ; }
/* Выводим результат суммирования */
cout << "\nОбщая сумма равна "
<< accumulatedValue
<<" \n" ;
}
cout << "Программа завершена\n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
По сути, перед вами всё та же программа FunctionDemo, но теперь она суммирует квадраты введённых чисел. В функции square( ) играющее роль аргумента число возводится в квадрат. Проведены незначительные изменения и в функции sumSequence( ): если раньше мы суммировали введённые числа, то теперь суммируем значения, возвращаемые функцией square( ).
Функции с несколькими аргументами...87
Функции могут иметь не один аргумент. В этом случае аргументы разделяются запятыми. Например, следующая функция возвращает произведение двух аргументов:
int product( int arq1 , int arg2 )
{
return arg1 * arg2 ;
}
Функция main( )...87
Служебное слово main( ) в нашей стандартной программе означает не что иное, как функцию ( возможно, с необычными для вас аргументами ), не требующую прототипа.
При компиляции программы С++ добавляет некоторый стандартный программный код, выполняемый до того, как начинает выполняться функция main( ). Этот код настраивает программную среду, в которой выполняется ваша программа, например открывает потоки ввода и вывода по умолчанию.
После настройки среды выполнения этот код вызывает функцию main( ), и лишь тогда происходит выполнение операторов программы. При завершении программа выходит из функции main( ), после чего вторая часть стандартного кода С++ освобождает занятые программой системные ресурсы и передаёт управление операционной системе.
_________________
87 стр. Глава 6. Создание функций
С++ позволяет программистам называть несколько разных функций одним и тем же именем. Эта возможность называется перегрузкой функций ( function overloading ).
Вообще говоря, две функции в одной программе не могут иметь одинаковых имён , так как компилятор С++ просто не сможет их различить.
Однако используемое компилятором внутреннее имя функции включает число и типы аргументов ( но не возвращаемое значение ), и поэтому следующие функции являются разными:
void someFunction( void )
{
/* ...Выполнение некоторой функции */
}
void someFunction( int n )
{
/* ...Выполнение другой функции */
}
void someFunction( double d )
{
/* ...Выполнение ещё одной функции */
}
void someFunction( int n1 , int n2 )
{
/* ...Выполнение ещё одной функции, отличной от предыдущих */
}
Компилятор С++ знает, что функции someFunction( void ), someFunction( int ), someFunction( double ), someFunction( int , int ) не одинаковы. В мире компьютеров встречается довольно много таких вещей, для которых можно найти аналогии в реальном мире. Существуют они и для перегрузки функций.
Вы знаете, что тип аргумента void указывать не обязательно. Например, SumFunction( void ) и SumFunction( ) вызывают одну и ту же функцию. Фактически функция может иметь сокращённое название, в нашем случае someFunction( ), так же как и меня можно называть просто Стефаном. Если бы Стефанов больше нигде не было, меня всегда могли бы называть только по имени. Но в действительности нас побольше, и если кому-то посчастливится попасть в общество нескольких Стефанов, то к ним придётся обращаться по имени и фамилии ( кстати, напомню, что меня зовут Стефан Дэвис ). Пользуясь полным именем, никто не ошибётся, даже если Стефанов будет несколько. Поэтому, пока полные имена функций в программе уникальны, конфликты между ними невозможны.
Подобные аналогии между компьютерным и реальным миром не должны вас удивлять, так как компьютерный мир создан людьми.
Типичное приложение, использующее перегрузку функций, может выглядеть следующим образом:
int intVariable1 , intVariable2 ;
double doubleVariable ;
/* Функции различаются по типу передаваемых аргументов */
someFunction( ) ; /* Вызов someFunction( void ) */
someFunction( intVariable1 ) ; /* Вызов someFunction( int ) */
someFunction( doubleVariable ) ; /* Вызов someFunction( double ) */
someFunction( intVariable1 , intVariable2 ) ; /* Вызов someFunction( int , int ) */
/* С константами функции работают аналогично */
someFunction( 1 ) ; /* Вызов someFunction( int )*/
someFunction( 1.0 ) ; /* Вызов someFunction( double ) */
someFunction( 1 , 2 ) ; /* Вызов someFunction( int , int ) */
_________________
88 стр. Часть 2. Становимся функциональными программистами
В каждом случае типы аргументов соответствуют тем, которые значатся в полном имени каждой функции.
«Тип возвращаемого значения в полное имя функции ( называемое также её сигнатурой ) не входит. Следующие две функции имеют одинаковые имена ( сигнатуры ) и поэтому не могут использоваться в одной программе:
int someFunction( int n ) ;
/* Полным именем этой функции является someFunction( int ) */
double someFunction( int n ) ; /* Имеет то же полное имя */
»
[Атас!]
«В выражениях с перегруженными функциями могут использоваться переменные разных типов, поскольку при определении полного имени играют роль только аргументы. Следующий код вполне допустим:
int someFunction( int n ) ;
double d = someFunction( 10 ) ;
/* Преобразуем тип полученного значения */
»
[Помни!]
В этом фрагменте возвращаемые функцией значения типа int преобразуются в double. Но следующий код некорректен:
int someFunction( int n ) ;
double someFunction( int n ) ;
double d = someFunction( 10 ) ;
/* В этом случае мы преобразуем тип полученного целочисленного значения или используем вторую функцию? */
В этом случае С++ не поймёт, какое значение он должен использовать — возвращаемое double-функцией или её целочисленным вариантом. Поэтому такие функции в одной программе использоваться не могут.
Как уже отмечалось, любой фрагмент кода программист может оформить как функцию, присвоив ей полное имя, таким образом объявляя её для дальнейшего использования.
Функции sumSequence( ) и square( ), с которыми вы встречались в этой главе, были определены до того, как вызывались. Но это не означает, что нужно всегда придерживаться именно такого порядка. Функция может быть определена в любой части модуля ( модуль — это другое название исходного файла С++ ).
Однако должен использоваться какой-то механизм, уведомляющий функцию main( ) о функциях, которые она может вызывать. Рассмотрим следующий код:
int main( int argc , char* pArgs[ ] )
{
someFunc( 1 , 2 ) ;
}
int someFunc( double arg1 , int arg2 )
{
/* ...выполнение каких-то действий */
}
_________________
89 стр. Глава 6. Создание функций
При вызове функции someFunc( ) внутри main( ) полное её имя неизвестно. Можно предположить, что именем функции является someFunc( int , int ) и возвращаемое ею значение имеет тип void. Однако, как видите, это вовсе не так.
Согласен, компилятор С++ мог бы быть не таким ленивым и просмотреть весь модуль для определения сигнатуры функции. Но он этого не сделает, и с этим приходится считаться[ 12 ]. Таков мир: любишь кататься — люби и саночки возить.
Поэтому нам нужно проинформировать main( ) о полном имени вызываемой функции до обращения к ней. Для этого используют прототипы функций.
Прототип функции содержит её полное имя с указанием типа возвращаемого значения. Использование прототипов рассмотрим на следующем примере:
int someFunc( double , int ) ;
int main( int argc , char* pArgs[ ] )
{
someFunc( 1 , 2 ) ;
}
int someFunc( double arg1 , int arg2 )
{
/* ...выполнение каких-то действий */
}
Использованный прототип объясняет миру ( по крайней мере той его части, которая следует после этого объявления ), что полным именем функции someFunc( ) является someFunc( double , int ). Теперь при её вызове в main( ) компилятор поймёт, что 1 нужно преобразовать к типу double. Кроме того, функция main( ) осведомлена, что someFunc( ) возвращает целое значение.
Переменные функции хранятся в трёх разных местах. Переменные, объявленные внутри функции, называются локальными. В следующем примере переменная localVariable является локальной по отношению к функции fn( ):
int globalVariable ;
void fn( )
{
int localVariable ;
static int staticVariable ;
}
До вызова fn( ) переменной localVariable не существует. После окончания работы функции она оставляет этот бренный мир и её содержимое навсегда теряется. Добавлю, что доступ к ней имеет только функция fn( ), остальные использовать её не могут.
А вот переменная globalVariable существует на протяжении работы всей программы и в любой момент доступна всем функциям.
Статическая переменная staticVariable является чем-то средним между локальной и глобальной переменными. Она создаётся, когда программа при выполнении достигает описания переменной ( грубо говоря, когда происходит первый вызов функции ). К тому же staticVariable доступна только из функции fn( ). Но, в отличие от localVariable, переменная staticVariable продолжает существовать и после окончания работы функции. Если в функции fn( ) переменной staticVariable присваивается какое-то значение, то оно сохранится до следующего вызова fn( ).
________________
12Более того, как вы узнаете позже, тела функции в данном модуле может и не оказаться. — Прим. ред.
_________________
90 стр. Часть 2. Становимся функциональными программистами
Обычно прототипы функций помещаются в отдельный файл ( называемый включаемым, или заголовочным ), который программист затем включает в исходный файл С++. При компиляции препроцессор С++ ( который выполняется до стадии компиляции программы ) вставляет содержимое такого файла в программу в том месте, где встречает соответствующую директиву #include"filename".
Вот как может выглядеть простой заголовочный файл с определением математических функций с именем math:
/* Заголовочный файл math содержит прототипы функций, которые могут использоваться несколькими программами. */
/* Функция abs возвращает абсолютное значение аргумента */
double abs( double d ) ;
/* Функция square возвращает квадрат аргумента */
double square( double d ) ;
Программа использует заголовочный файл math следующим образом:
/* Программа с математическими вычислениями */
#include "math"
using namespace std ;
// Код программы
Директива #include требует от препроцессора заменить её содержимым указанного в ней файла.( Между # и include можно ставить пробел, а вот между < и iostream нельзя.— Прим. рер. )
Эта директива имеет вид, отличный от формата инструкций С++, поскольку она обрабатывается до компиляции программы. Директива должна располагаться на одной строке и начинаться с символа # в первой позиции строки. Имя файла может быть заключено либо в кавычки, либо в угловые скобки ( последние используются для библиотечных файлов С++ ). Для пользовательских заголовочных файлов применяются кавычки.
С++ предоставляет программисту стандартные заголовочные файлы, такие как cstdio или iostream. В частности, в файле iostream содержится прототип использованной в главе 4, "Выполнение логических операций", функции setf( ) для вывода чисел в шестнадцатеричном виде.
«Так сложилось, что годами программисты использовали расширение .h для заголовочных файлов. Однако в последние годы это соглашение для заголовочных файлов стандартной библиотеки С++ было отменено стандартом ( например, заголовочный файл cstdio ранее назывался stdio.h ). Однако многие программисты продолжают давать расширение .h своим заголовочным файлам. Даже в программировании есть традиции!»
[Атас!]
_________________
91 стр. Глава 6. Создание функций
В этой главе...
►Использование символьных массивов 98
►Тип string 103
Массивом называется последовательность переменных одного типа, использующая одно имя; для ссылки на конкретное значение применяется индекс. Массивы удобны для хранения больших количеств взаимосвязанных значений. Например, голы, забитые каждым игроком футбольной команды, естественно сохранять именно в массивах. В С++ допускаются и многомерные массивы. Например, массивы с количеством голов можно сохранить в массиве месяцев — это позволит работать с количеством голов, забитых каждым игроком в определённом месяце.
Из этой главы вы узнаете, как инициализировать и использовать массивы не только для работы, но и для развлечения. А ещё я расскажу об очень полезном виде массивов — строках, которые в С++ являются массивом значений типа char.
Рассмотрим следующую проблему. Вам нужна программа, которая сможет считывать последовательность чисел, введённых с клавиатуры. Будем использовать уже привычное правило, согласно которому ввод чисел завершается после первого отрицательного значения. Однако данная программа, в отличие от уже рассмотренных в главах 5, "Операторы управления программой", и 6, "Создание функций", после того, как все числа прочитаны, отображает их на стандартном устройстве вывода.
Можно попытаться хранить числа в независимых переменных, например:
cin >> value1 ;
if ( value1 >= 0 )
{
cin >> value2 ;
if ( value2 >= 0 )
{
...
Однако нетрудно заметить, что этот подход позволит управлять последовательностью, которая будет состоять всего лишь из нескольких чисел, а кроме того, такая запись выглядит довольно уродливо. В нашем случае нужна такая структура данных, которая, как и любая переменная, имеет своё имя, но может содержать больше одной переменной. Для этого как раз и используются массивы.
_________________
92 стр. Часть 2. Становимся функциональными программистами
С помощью массивов можно легко решить проблему работы с подобными последовательностями. В приведённом далее фрагменте объявляется массив valueArray, в котором можно хранить до 128 целых значений. Затем он заполняется числами, введёнными с клавиатуры,
int value ;
/* объявление массива, способного содержать до 128 чисел типа int */
int valueArray[ 128 ] ;
/* определение индекса, используемого для доступа к элементам массива; его значение не должно превышать 128 */
for ( int i = 0 ; i < 128 ; i++ )
{
cin >> value ;
/* выходим из цикла, если пользователь вводит отрицательное число */
if ( value < 0 ) break ;
valueArray[ i ] = value ;
}
Во второй строке кода ( без учёта комментариев ) объявлен массив valueArray. Первым в объявлении указывается тип элементов массива ( в нашем случае это int ), за ним следует имя массива, последним элементом являются открывающая и закрывающая квадратные скобки, в которых записывается максимальное число элементов массива. В нашем случае массив valueArray может содержать до 128 целочисленных значений.
Компьютер считывает число с клавиатуры и сохраняет его в следующем элементе массива valueArray. Доступ к элементам массива обеспечивается с помощью имени массива и индекса, указанного в квадратных скобках. Первый элемент массива обозначается как valueArray[ 0 ], второй — как valueArray[ 1 ] и т.д.
Запись valueArray[ i ] представляет собой i-й элемент массива. Индексная переменная i должна быть перечислимой, т.е. её типом может быть char , int или long. Если valueArray — массив целых чисел, то элемент valueArray[ i ] имеет тип int.
В представленной ниже программе осуществляется ввод последовательности целых чисел ( до первого отрицательного числа ), затем эта последовательность и сумма её элементов выводятся на экран.
/* ArrayDemo — демонстрирует использование массивов. Считывает последовательность целых чисел и отображает их по порядку */
#include
#include
#include
using namespace std ;
/* объявления прототипов функций */
int sumArray( int integerArray[ ] , int sizeOfloatArray ) ;
void displayArray( int integerArray[ ] , int sizeOfloatArray ) ;
_________________
93 стр. Глава 7. Хранение последовательностей в массивах
int main( int nArg , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Описываем счётчик цикла */
int nAccumulator = 0 ;
cout << "Эта программа суммирует числа,"
<< " введённые пользователем\n" ;
cout << "Цикл прерывается, когда"
<< " пользователь вводит\n"
<< "отрицательное число\n" ;
/* Сохраняем числа в массиве */
int inputValues[ 128 ] ;
int numberOfValues = 0 ;
for ( numberOfValues = 0 ; numberOfValues < 128 ; numberOfValues++ )
{
/* Ввод очередного числа */
int integerValue ;
cout << "Введите следующее число: " ;
cin >> integerValue ;
/* Если оно отрицательное... */
if ( integerValue < 0 )
{
/* ...тогда выходим из цикла */
break ;
}
/* ...иначе сохраняем число в массиве */
inputValues[ numberOfValues ] = integerValue ;
}
/* Теперь выводим значения и их сумму */
displayArray( inputValues , numberOfValues ) ;
cout << "Сумма введённых чисел равна "
<< sumArray( inputValues , numberOfValues )
<< endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
/* displayArray — отображает элементы массива integerArray длиной sizeOfloatArray */
void displayArray( int integerArray[ ] , int sizeOfArray )
{
cout << "В массиве хранятся"
<< " следующие значения:\n" ;
for ( int i = 0 ; i < sizeOfArray ; i++ )
{
cout.width( 3 ) ;
cout << i << ": " << integerArray[ i ] << endl ;
}
cout << endl ;
}
_________________
94 стр. Часть 2. Становимся функциональными программистами
/* sumArray — возвращает сумму элементов целочисленного массива */
int sumArray( int integerArray[ ] , int sizeOfArray )
{
int accumulator = 0 ;
for ( int i = 0 ; i < sizeOfArray ; i++ )
{
accumulator += integerArray[ i ] ;
}
return accumulator ;
}
Программа ArrayDemo начинается с объявления прототипов функций sumArray( ) и displayArray( ), которые понадобятся нам позже. Главная часть программы содержит довольно скучный цикл ввода значений. На этот раз вводимые значения сохраняются в массиве inputValues.
Если введённое значение отрицательно, цикл прерывается при помощи инструкции break, если же нет — оно копируется в массив. Целочисленная переменная numberOfValues используется в качестве индекса массива. Она инициализирована нулём в начале цикла for. При каждой итерации индекс увеличивается. В условии выполнения цикла for осуществляется контроль за тем, чтобы количество введённых чисел не превышало 128, т.е. размера массива ( после введения 128 чисел программа переходит к выводу элементов массива на экран независимо от того, ввёл пользователь отрицательное число или нет ).
«В объявлении массива inputValues было указано, что его максимальная длина равна 128. При записи большего числа данных, чем определено в объявлении, ваша программа может работать неправильно и даже аварийно завершать работу. Поэтому лучше застраховаться и оставить больше места для хранения данных. Неважно, насколько велик массив; всегда нужно следить за тем, чтобы операции с массивом не приводили к выходу за его пределы.»
[Атас!]
Функция main заканчивается выводом на экран содержимого массива и суммы его элементов.
«Среда Dev-C++ может помочь вам в работе с исходными текстами, в которых имеется много функций. На рис. 7.1 показано содержимое вкладки Classes ( Классы ), в которой перечисляются все функции в исходном файле. Двойной щелчок на имени функции переносит вас в окне редактирования в строку с этой функцией.»
[Советы]
Функция displayArray( ) содержит обычный цикл for, который используется для прохождения по массиву. Каждый очередной элемент массива добавляется к переменной accumulator. Передаваемый функции параметр sizeOfArray включает количество значений, содержащихся в массиве.
Напомню ещё раз, что индекс массива в С++ отсчитывается от 0 , а не от 1. Кроме того, обратите внимание, что цикл for прерывается в тот момент, когда значение i становится равным sizeOfArray. Вы же не хотите добавлять все 128 элементов массива integerArray к accumulator? Ни один элемент массива, индекс которого больше или равен числу sizeOfArray, учитываться не будет. Вот как выглядит пример работы с этой программой:
_________________
95 стр. Глава 7. Хранение последовательностей в массивах
Эта программа суммирует числа, введённые пользователем
Цикл прерывается, когда пользователь вводит
отрицательное число
Введите следующее число: 1
Введите следующее число: 2
Введите следующее число: 3
Введите следующее число: -1
В массиве хранятся следующие значения:
0: 1
1: 2
2: 3
Сумма введённых чисел равна 6
Press any key to continue...
Рис. 7.1. Вкладка Классы выводит информацию о функциях, составляющих программу
Локальная переменная нежизнеспособна до тех пор, пока ей не присвоят значение. Другими словами, пока вы в ней что-то не сохраните, она будет содержать мусор. Локальное описание массива происходит так же: пока каждому элементу не присвоят какие-либо значения, в ячейках массива будет содержаться мусор. Локальную переменную следует инициализировать при её объявлении, и ещё в большей степени это справедливо для массивов. Слишком уж легко наткнуться на неработоспособную ячейку в неинициализированном массиве.
К счастью, массив может быть инициализирован сразу во время объявления, например:
float floatArray[ 5 ] = { 0.0 , 1.0 , 2.0 , 3.0 , 4.0 } ;
В этом фрагменте элементу floatArray[ 0 ] присваивается значение 0 , floatArray[ 1 ] — 1 , floatArray[ 2 ] — 2 и т.д.
_________________
96 стр. Часть 2. Становимся функциональными программистами
Размер массива может определяться и количеством инициализирующих констант. Например, перечислив в скобках значения инициализаторов, можно ограничить размер массива floatArray пятью элементами. С++ умеет очень хорошо считать ( по крайней мере, его можно с уверенностью использовать для этого ). Так, следующее объявление идентично представленному выше:
float floatArray[ ] = { 0.0 , 1.0 , 2.0 , 3.0 , 4.0 } ;
Все элементы массива можно инициализировать одним и тем же значением, указав его только один раз. Например, далее все 25 элементов массива floatArray инициализируются значением 1.0.
float floatArray[ 25 ] = { 1.0 } ;
Математики перечисляют содержимое массивов, начиная с элемента номер 1. Первым элементом математического массива х является х( 1 ). Во многих языках программирования также начинают перечисление элементов массива с 1. Но в С++ массивы индексируются начиная с 0! Первый элемент массива С++ обозначается как valueArray[ 0 ]. Первый индекс массива С++ нулевой; поэтому последним элементом 128-элементного целочисленного массива является integerArray[ 127 ], а не integerArray[ 128 ].
К сожалению, в С++ не проверяется выход индекса за пределы массива. Этот язык будет рад предоставить вам доступ к элементу integerArray[ 200 ]. Более того, С++ позволит вам обратиться даже к integerArray[ -15 ]. Приведём такую аналогию. Имеется улица, на которой 128 жилых домов. Если мы захотим найти 200-й дом, идя вдоль улицы и пересчитывая дома, то его просто может не быть. Тут могут быть заброшенные руины или, хуже того, дом, стоящий уже на другой улице! Чтение значения элемента integerArray[ 200 ] может дать некоторое непредсказуемое значение или даже привести к ошибке нарушения защиты ( вы вторглись в частные владения, куда вас не звали... ), а запись — к совершенно непредсказуемым результатам. Может, ничего и не случится — вы просто попадёте в заброшенный дом, а может, вы сотрёте тем самым какие-то жизненно важные данные. Словом, случиться может что угодно — вплоть до полного краха программы.
«Самая распространённая ошибка — неправильное обращение к последнему элементу по адресу integerArray[ 128 ]. Хотя это всего лишь следующий за концом массива элемент, записывать или считывать его не менее опасно, чем любой другой некорректный адрес.»
[Атас!]
Разумеется, программа ArrayDemo делает то же самое, что и не основанные на массивах программы, которые рассматривались раньше. Правда, в этой версии несколько изменён ввод множества чисел, но вы вряд ли будете потрясены этой особенностью.
И всё же в возможности повторного отображения введённых значений кроется значительное преимущество использования массивов. Массивы позволяют программе многократно обрабатывать серии чисел. Главная программа была способна передать массив входных значений функции displayArray( ) для отображения, а затем в SumArray( ) для суммирования.
_________________
97 стр. Глава 7. Хранение последовательностей в массивах
Массивы представляют собой весьма удобную структуру для хранения последовательности чисел. В некоторых приложениях приходится работать с последовательностью последовательностей. Классический пример такой матричной конфигурации — крупноформатная таблица, распланированная по образцу шахматной доски ( каждый её элемент имеет две координаты — x и у ).
В С++ матрицы определяются следующим образом:
int intMatrix[ 10 ][ 5 ] ;
Эта матрица может иметь 10 элементов в одном измерении и 5 в другом, что в сумме составляет 50 элементов. Другими словами, intMatrix является 10-элементным массивом, каждый элемент которого — массив из 5 элементов. Легко догадаться, что один угол матрицы обозначается intMatrix[ 0 ][ 0 ], тогда как второй — intMatrix[ 9 ][ 4 ].
Индексы intMatrix можно рассматривать в любом удобном порядке. По какой оси отложить длину 10 — решайте сами, исходя из удобства представления. Матрицу можно инициализировать так же, как и массив:
int intMatrix[ 2 ][ 3 ] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } } ;
Здесь фактически выполняется инициализация двух трёхэлементных массивов: intMatrix[ 0 ] — значениями 1, 2 и 3, a intMatrix[ 1 ] — 4, 5 и 6 соответственно.
Элементы массива могут быть любого типа. В С++ возможны массивы любых числовых типов — float, double, long, однако символьные массивы имеют особое значение.
Слова разговорной речи могут быть интерпретированы как массивы символов. Массив символов, содержащий моё имя, таков:
char sMyName[ ] = { 'S' , ' t' , 'e' , 'p' , 'h' , 'e' , 'n' } ;
Моё имя можно отобразить с помощью следующей небольшой программы:
/* CharDisplay — выводит на экран массив символов в окне MS DOS */
#include
#include
#include
using namespace std ;
/* Объявления прототипов */
void displayCharArray( char stringArray[ ] , int sizeOfloatArray ) ;
int main( int nArg , char* pszArgs[ ] )
{
char charMyName[ ] = { 'S' , 't' , 'e' , 'p' , 'h' , 'e' , ' n' } ;
displayCharArray( charMyName , 7 ) ;
cout << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ;
return 0 ;
}
_________________
98 стр. Часть 2. Становимся функциональными программистами
/* displayCharArray — отображает массив символов, по одному при каждой итерации */
void displayCharArray( char stringArray[ ] , int sizeOfloatArray )
{
for ( int i = 0 ; i < sizeOfloatArray ; i++ )
{
cout << stringArray[ i ] ;
}
}
В программе объявлен фиксированный массив символов, содержащий, как вы могли заметить, моё имя. Этот массив передаётся в функцию displayCharArray( ) вместе с его длиной. Функция displayCharArray( ) идентична функции displayArray( ) из нашего предыдущего примера, но в этом варианте вместо целых чисел она выводит символы.
Программа работает довольно хорошо; но одно неудобство всё-таки есть: всякий раз вместе с самим массивом необходимо передавать его длину. Однако можно придумать правило, которое поможет решить нашу проблему. Если бы мы знали, что в конце массива находится специальный кодовый символ, то не потребовалось бы передавать размеры массива.
В С++ для этой цели зарезервирован нулевой символ. Мы можем использовать '\0' для того, чтобы пометить конец символьного массива. ( Числовое значение '\0' равно нулю, однако тип '\0' — char. )
«Символ \у является символом, числовое значение которого равно у. Изменим предыдущую программу, используя это правило:»
[Помни!]
/* DisplayString — выводит на экран массив символов в окне MS DOS */
#include
#include
#include
using namespace std ;
/* Объявления прототипов */
void displayString( char stringArray[ ] ) ;
int main( int nArg , char* pszArgs[ ] )
{
char charMyName[ ] ={ 'S' , 't' , 'e' , 'p' , 'h' , 'e' , 'n' , 0 } ;
displayString( charMyName ) ;
cout << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
99 стр. Глава 7. Хранение последовательностей в массивах
/* displayString — посимвольно выводит на экран строку */
void displayString( char stringArray[ ] )
{
for ( int i = 0 ; stringArray[ i ] != 0 ; i++ )
{
cout << stringArray[ i ] ;
}
}
Массив charMyName объявляется как массив символов с дополнительным нулевым символом ( \0 ) в конце. Программа displayString итеративно проходит по символьному массиву, пока не встретит нуль-символ.
Поскольку в функции displayString( ) больше нет необходимости передавать куда-либо длину символьного массива, использовать её проще, чем displayCharArray( ). Включать нулевой символ в символьные массивы очень удобно, и в языке С++ он используется повсеместно. Для таких массивов даже придумали специальное имя.
«Строка символов — это символьный массив с завершающим нулевым символом. Зачастую его называют просто “строкой”, хотя в С++ имеется отдельный тип string для работы со строками.»
[Помни!]
Выбор нулевого символа в качестве завершающего не был случаен. Это связано с тем, что в С++ только нулевое значение преобразуется в логическое значение false, а все остальные — в true. Это означает, что цикл for можно записать ( что обычно и делается ) следующим образом:
for ( int i = 0 ; stringArray[ i ] ; i++ )
Инициализировать строку в С++ можно с использованием двойных кавычек. Этот способ более удобен, чем тот, в котором используются одинарные кавычки для каждого символа. Следующие объявления идентичны:
char szMyName[ ] = "Stephen" ;
char szAlsoMyName[ ] = { 'S' , 't' , 'e' , 'p' , 'h' , 'e' , 'n' , '\0' } ;
В соглашении об использовании имён для обозначения строк с завершающим нулём рекомендуется применять префикс sz. Такая запись является соглашением и не более.
«Строка "Stephen" содержит восемь, а не семь символов — не забывайте о нулевом символе!»
[Помни!]
Для работы со строками в С++ можно использовать стандартные библиотечные функции. Некоторые из них намного сложнее, чем может показаться с первого взгляда. В табл. 7.1 перечислен ряд таких стандартных функций.
_________________
100 стр. Часть 2. Становимся функциональными программистами
Таблица 7.1. Функции, обрабатывающие строки
_________________
Название — Действие
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
int strlen( string ) — Возвращает количество символов в строке ( без учёта нулевого символа )
char* strcat( target , source ) — Присоединяет строку source к концу строки target
char* strcpy( target , source ) — Копирует строку source в target
char* strncat( target , source , n ) — Присоединяет не более n символов строки source к концу строки target
char* strncpy( target , source , n ) — Копирует не более n символов строки source в target
char* strstr( source1 , source2 ) — Находит первое вхождение строки source2 в source1
int strcmp( source1 , source2 ) — Сравнивает две строки
int stricmp( source1 , source2 ) — Сравнивает две строки без учёта регистра символов
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Чтобы использовать функции работы со строками, нужно добавить в начале программы директиву #include
«Текущий стандарт С++ предлагает избегать использования функций str...( ). В настоящее время компиляторы С++ поддерживают эти функции, но в один прекрасный день могут и перестать это делать. Именно с тем, что это устаревшие функции, связано использование расширения .h в директиве #include
[Атас!]
В качестве примера использования функций str...( ) рассмотрим следующую программу, которая получает две строки, вводимые с клавиатуры, и объединяет их в одну строку.
/* Concatenate — объединение двух строк, которые разделяются символом " — " */
#include
#include
#include
using namespace std ;
/* Включаем файл, необходимый для использования функций работы со строками */
#include
int main( int nArg , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Считываем первую строку... */
char szString1[ 256 ] ;
cout << "Введите строку #1: " ;
cin >> szString1 ;
/* Более безопасный вариант: cin.getline( szString1 , 128 ) ; */
/* ...теперь вторую... */
char szString2[ 128 ] ;
cout << "Введите строку #2: " ;
cin >> szString2 ;
/* Более безопасный вариант: cin.getline( szString2 , 128 ) ; */
_________________
101 стр. Глава 7. Хранение последовательностей в массивах
/* Объединяем строки */
char szString[ 260 ] ;
/* Копируем первую строку в буфер */
strncpy( szString , szString1 , 128 ) ;
/* Добавляем разделитель */
strncat( szString , " — " , 4 ) ;
/* ...теперь добавим вторую строку... */
strncat( szString , szString2 , 128 ) ;
/* ...и выведем результат на экран */
cout << "\n" << szString << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
«Порядок аргументов функций str...( ) может показаться "обратным". Хотя смотря что считать правильным порядком. Например, strcat( targer , source ) дописывает source к концу target, что выглядит вполне логично.»
[Помни!]
Вот пример работы программы:
Введите строку #1: Шарик
Введите строку #2: собака
Шарик - собака
Press any key to continue...
Программа начинается со считывания вводимой с клавиатуры строки cin >> szString1. При этом информация считывается до первого пробела, пробелы пропускаются, и оставшаяся часть строки будет считана в следующей инструкции cin >>.
«Кроме того, инструкция cin >> ничего не знает о длине строки. Она может прочесть тысячу символов и попытаться запихнуть их в массив, размер которого только 256 символов. Это опасно, кроме прочего, ещё и тем, что может послужить дырой, через которую хакеры смогут проникнуть в ваш компьютер...»
[Атас!]
«С++ предоставляет массу возможностей обойти такие узкие места. Например, функция getline( ) считывает строку текста, но при этом она знает максимальное количество символов, которые можно считать:
cin.getline( string , lengthOfTheString ) ;
( Пока что не обращайте внимания на странную приставку cin.. )»
[Советы]
Функции strncpy( ) и strncat( ) в качестве одного из аргументов получают длину целевого буфера. Вызов strncpy( szString , szString1 , 128 ) означает "копировать в szString символы из szString1, пока не будет скопирован нулевой символ или пока не будет скопировано 128 символов". Это не означает, что всякий раз будет копироваться ровно 128 символов.
_________________
102 стр. Часть 2. Становимся функциональными программистами
«Имеются версии функций с передаваемой длиной буфера и без неё. Последние следует использовать, когда вы твёрдо знаете, что переполнение целевого буфера возникнуть не может.»
[Атас!]
ANSI С++ предоставляет программисту тип string, облегчающий работу с символьными строками.
«Я использую термин строка для обозначения массива с завершающим нулевым символом; говоря о строках ANSI С++, я говорю о типе string. Тип string включает операции копирования, конкатенации, перевода строчных символов в прописные и т.п. функции. Они определены в заголовочном файле
[Советы]
Вот как выглядит предыдущая программа с использованием типа string.
/* StringConcatenate — конкатенация двух строк с разделителем " - " */
#include
#include
#include
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Считываем первую строку... */
string string1 ;
cout << "Введите строку #1:" ;
cin >> string1 ;
/* Считываем вторую строку... */
string string2 ;
cout << "Введите строку #2:" ;
cin >> string2 ;
/* Объединяем их в одном буфере */
string buffer ;
string divider = " - " ;
buffer = string1 + divider + string2 ;
/* ...и выводим результат */
cout << "\n" << buffer << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Здесь определены две переменные string1 и string2. Эти переменные не имеют определённой длины — они могут расти и уменьшаться в зависимости от того, сколько символов в них находится, вплоть до всей оперативной памяти.
Обратите внимание, что некоторые операции выполняются в этой программе не так, как их арифметические эквиваленты. Например, операция сложения двух переменных типа string приводит к их конкатенации. Кроме того, как видите, С++ может легко конвертировать строку с завершающим нулём в тип string, без каких-либо предупреждений и сообщений.
_________________
103 стр. Глава 7. Хранение последовательностей в массивах
«Тип string не является встроенным типом С++, как int или float, т.е. операции с этим типом не встроены в синтаксис языка, а определены в заголовочном файле string. Детальнее класс string рассматривается в главе 27, "Шаблоны С++" ; здесь же я упомянул о нём только как о более простом средстве работы со строками.»
[Атас!]
_________________
104 стр. Часть 2. Становимся функциональными программистами
В этой главе...
►Что такое адрес 106
►Передача указателей функциям 111
По сравнению с другими языками С++ достаточно обычен. Конечно, в ряде языков программирования отсутствуют специальные логические операторы, для которых в С++ имеются свои обозначения, но концептуально это достаточно традиционный язык программирования. Одним из отличий С++ является использование указателей. Указатель — это переменная, которая содержит адрес другой переменной ( т.е. её расположение в памяти ).
В этой главе представлены основы работы с указателями. Сначала рассматриваются концепции, с которыми необходимо обязательно ознакомиться для работы с указателями, затем поясняется синтаксис указателей и некоторые причины их высокой популярности в С++.
Память в компьютере измеряется в байтах и битах. Вот текст программы, которая даст вам представление о том, чему равен размер переменных разных типов.
/* VariableSize — вывод информации о размерах переменных различных типов */
#include
#include
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
bool b ;
char c ;
int n ;
long l ;
float f ;
double d ;
cout << "sizeof a bool = " << sizeof b << endl ;
cout << "sizeof a char = " << sizeof c << endl ;
cout << "sizeof an int = " << sizeof n << endl ;
_________________
105 стр. Глава 8. Первое знакомство с указателями в С++
cout << "sizeof a long = " << sizeof l << endl ;
cout << "sizeof a float = " << sizeof f << endl ;
cout << "sizeof a double= " << sizeof d << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Оператор sizeof представляет собой специальную инструкцию С++, которая возвращает размер своего аргумента в байтах. Вот как выглядит вывод данной программы, скомпилированной Dev-C++:
sizeof a bool = 1
sizeof a char = 1
sizeof an int = 4
sizeof a long = 4
sizeof a float = 4
sizeof a double= 8
Press any key to continue...
«He удивляйтесь, если при использовании другого компилятора вы получите другие результаты. Например, может оказаться, что размер int меньше размера long. Стандарт С++ не оговаривает точные значения размера тех или иных типов — он говорит только о том, что размер типа int не превышает размера long, а размер double не может быть меньше размера float. Размеры, приведённые выше, типичны для 32-битовых процессоров ( типа Pentium ).»
[Советы]
Очевидно, что каждая переменная С++ расположена где-то в памяти компьютера. Память разбита на байты, каждый из которых имеет свой адрес — 0, 1, 2 и т.д.
Переменная intRandy может находиться по адресу 0x100 , a floatReader — по адресу 0x180 ( адреса в памяти принято записывать в шестнадцатеричном виде ). Понятно, что эти переменные могут находиться где угодно, и только компьютер по долгу службы точно знает, где именно они располагаются — и то только в процессе выполнения программы.
Здесь можно провести определённую аналогию с отелем. Когда вы бронируете место, вам может быть предоставлен, например, номер 0x100 ( я понимаю, что номера в отеле никто не записывает в шестнадцатеричной форме, но отвлечёмся на минуту от этого факта ). Ваш друг при бронировании может оказаться в номере 0х180 — так и каждая переменная получает место в памяти при создании ( немного подробнее об этом будет рассказано далее, когда мы будем рассматривать область видимости ).
В табл. 8.1 приведены два оператора, связанные с указателями. Оператор & по сути говорит "скажи мне номер комнаты в отеле", а * — "кто в этой комнате живёт".
_________________
106 стр. Часть 2. Становимся функциональными программистами
Таблица 8.1. Адресные операторы
_________________
Оператор — Описание
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
& — ( унарный ) Адрес
* — ( унарный ) ( В выражении ) то, на что указывает указатель
* — ( унарный ) ( В объявлении ) указатель на
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Следующая программа демонстрирует использование оператора & и показывает расположение переменных в памяти.
/* Layout — демонстрация расположения переменных в памяти компьютера */
#include
#include
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
int end ;
int n ;
long l ;
float f ;
double d ;
/* Вывод в шестнадцатеричном формате */
cout.setf( ios::hex ) ;
cout.unsetf( ios::dec ) ;
/* Вывод адресов каждой из переменных. Обратите внимание на расположение переменных в памяти компьютера */
cout << "--- = " << &end << " \n" ;
cout << "&n = " << &n <<" \n" ;
cout << "&l = " << &l <<" \n" ;
cout << "&f = " << &f <<" \n" ;
cout << "&d = " << &d <<" \n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Программа объявляет ряд переменных, к которым затем применяется оператор & для того, чтобы получить их местоположение в памяти. Вот как выглядит результат выполнения этой программы, скомпилированной Dev-C++.
--- = 0x28ff34
&n = 0x28ff30
&l = 0x28ff2c
&f = 0x28ff28
&d = 0x28ff20
Press any key to continue...
_________________
107 стр. Глава 8. Первое знакомство с указателями в С++
«Результат выполнения программы на вашем компьютере может отличаться от приведённого. Абсолютные адреса переменных зависят от массы различных факторов. Вообще говоря, результаты могут отличаться при различных запусках одной и той же программы.»
[Советы]
Обратите внимание на то, что переменная n располагается ровно в 4 байтах от первой объявленной переменной end. Переменная l располагается ещё на 4 байта ниже, а переменная типа double занимает 8 байт. Для каждой переменной выделяется память, необходимая для её типа.
«Стандарт С++ не требует от компилятора последовательного "беспросветного" размещения переменных в памяти. Dev-C++ может разместить переменные в памяти и по-другому.»
[Атас!]
Переменная-указатель содержит адрес, обычно это адрес другой переменной. Используя аналогию с отелем: я могу сказать сыну, что во время путешествия я буду в комнате 0x100. Мой сын выполняет роль указателя — его можно спросить, в какой комнате отеля я нахожусь, и он даст точный ответ.
Вот псевдокод, описывающий данную ситуацию:
mySon = &DadsRoom ; /* Теперь сын знает комнату отца */
room = *mySon ; /* "Номер комнаты равен" */
Пример работы с операторами на С++ привёден в следующем листинге:
void fn( )
{
int intVar ;
int* pintVar ;
pintVar = &intVar ; /* Теперь pintVar указывает на intVar */
*pintVar =10 ; /* Сохраняет 10 в переменной типа int по адресу, находящемуся в pintVar */
}
Функция fn( ) начинается с объявления переменной intVar ; в следующей строке объявляется pintVar — указатель на переменную типа int.
Указатели объявляются как обычные переменные, но в объявление добавляется унарный оператор *, который может быть использован совместно с именем любого типа. В данной строке этот символ используется вместе с именем фундаментального типа int. Однако этот оператор может использоваться для добавления к любому имени переменной типа.
При написании программ желательно придерживаться соглашений об именах, в соответствии с которыми первый символ в названии переменной указывает на её тип. Например, можно использовать n для int, d для double и т.д. С учётом этого соглашения имена указателей далее в книге будут начинаться с буквы р.
Унарный оператор & в выражении означает "взять адрес переменной". Таким образом, в первой строке приведённого кода находится команда сохранения адреса переменной intVar в переменной pintVar.
Представим себе, что функция fn( ) начинается с адреса 0x100 , переменная intVar расположена по адресу 0x102, а указатель pintVar — 0x106 ( такое расположение намного проще результатов работы программы Layout ; на самом деле вряд ли переменные будут храниться в памяти именно в таком порядке ).
_________________
108 стр. Часть 2. Становимся функциональными программистами
Первая команда программы сохраняет значение &intVar ( 0x102 ) в указателе pintVar. Вторая строка отвечает за присвоение значения 10 переменной, хранящейся по адресу, который содержится в указателе pintVar ( в нём находится число 0x102, т.е. адрес переменной intVar ).
Указатели похожи на адреса домов. Ваш дом имеет уникальный адрес, и каждый байт в памяти компьютера тоже имеет уникальный адрес. Почтовый адрес содержит набор цифр и букв. Например, он может выглядеть так: 123 Main Street ( конечно же, это не мой адрес! Я не люблю нашествий поклонников, если только они не женского пола ).
Можно хранить диван в доме по адресу 123 Main Street, и точно так же можно хранить число в памяти по адресу 0x123456. Можно взять лист бумаги и написать на нём адрес — 123 Main Street. Теперь диван хранится в доме, который находится по адресу, написанному на листке бумаги. Так работают сотрудники службы доставки: они доставляют диваны по адресу, который указан в бланке заказа, независимо от того, какой именно адрес записан в бланке ( я ни в коем случае не смеюсь над работниками службы доставки — просто это самый удобный способ объяснить указатели ).
Использовав синтаксис С++, это можно записать так:
House myHouse ;
House* houseAddress ;
houseAddress = &myHouse ;
*houseAddress = couch ;
Эта запись обозначает следующее: myHouse является домом, a houseAddress — адресом дома. Надо записать адрес дома myHouse в указатель houseAddress и доставить диван по адресу, который находится в указателе houseAddress. Теперь используем вместо дома переменную типа int:
int myInt ;
int* intAddress ;
intAddress = &myInt ;
*intAddress = 10 ;
Аналогично предыдущей записи, это поясняется так: myInt — переменная типа int. Следует сохранить адрес myInt в указателе intAddress и записать 10 в переменную, которая находится по адресу, указанному в intAddress.
Каждое выражение, как и переменная, имеет свой тип и значение. Тип выражения &intVar — указатель на переменную типа int, т.е. это выражение имеет тип int*. При сравнении его с объявлением указателя pintVar становится очевидно, что они одинаковы:
int* pintVar = &intVar ; /* Обе части этого присвоения имеют тип *int */
Аналогично pintVar имеет тип int* , a *pintVar — тип int:
*pintVar = 10 /* Обе части этого присвоения имеют тип int */
Тип переменной, на которую указывает pintVar, — int. Это эквивалентно тому, что если houseAddress является адресом дома, то, как ни странно, houseAddress указывает дом. Указатели на переменные других типов объявляются точно так же:
double doubleVar
double* pdoubleVar = &doubleVar
*pdoubleVar = 10.0
_________________
109 стр. Глава 8. Первое знакомство с указателями в С++
В компьютере класса Pentium размер указателя равен четырём байтам, независимо от того, на переменную какого типа он указывает[ 13 ].
Очень важно следить за соответствием типов указателей. Представьте, что может произойти, если компилятор в точности выполнит такой набор команд:
int n1 ;
int* pintVar ;
pintVar = &n1 ;
*pintVar = 100.0 ;
Последняя строка требует, чтобы по адресу, выделенному под переменную размером в четыре байта, было записано значение, имеющее размер восемь байтов. На самом деле ничего страшного не произойдёт, поскольку в этом случае компилятор приведёт 100.0 к типу int перед тем, как выполнить присвоение.
Привести переменную одного типа к другому явным образом можно так:
int iVar ;
double dVar = 10.0 ;
iVar = ( int )dVar ;
Так же можно привести и указатель одного типа к другому:
int* piVar ;
double dVar = 10.0 ;
double* pdVar ;
piVar = ( int* )pdVar ;
Трудно предсказать, что может случиться, если сохранить переменные одного типа по адресам, выделенным под переменные другого типа. Сохранение переменных, имеющих большую длину, вероятно, приведёт к уничтожению переменных, расположенных рядом. Такая ситуация наглядно продемонстрирована с помощью программы Layout Error:
/* LayoutError — демонстрирует результат неаккуратного обращения с указателями */
#include
#include
#include
using namespace std ;
int main( int intArgc , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
int upper = 0 ;
int n = 0 ;
int lower = 0 ;
/* Вывод адресов каждой из переменных. Обратите внимание на расположение переменных в памяти компьютера */
cout << "&upper = 0x" << &upper<< "\n" ;
cout << "&n = 0x" << &n << "\n" ;
cout << "&lower = 0x" << &lower << "\n" ;
/* Выводим значения объявленных переменных */
cout << "upper = " << upper << "\n" ;
cout << "n = " << n << "\n" ;
cout << "lower = " << lower << "\n" ;
/* Сохраняем значение типа double в памяти, выделенной для int */
cout << "\nСохранение double в int\n\n" ;
cout << "\nСохранение 13.0 по адресу &n\n\n" ;
double* pD = ( double* )&n ;
*pD = 13.0 ;
/* Показываем результаты */
cout << "upper = " << upper << "\n" ;
cout << "n = " << n << "\n" ;
cout << "lower = " << lower << "\n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system ( "PAUSE" ) ; return 0 ;
}
__________
13Размер указателя зависит не только от типа процессора, но и от операционной системы, используемого компилятора и так называемой модели памяти создаваемой программы. — Прим. ред.
_________________
110 стр. Часть 2. Становимся функциональными программистами
В первых трёх строках функции main( ) происходит объявление трёх переменных типа int. Допустим, что в памяти эти переменные находятся друг за другом.
Следующие три строки выводят значения этих переменных на экран. Не удивительно, что все три оказываются равными нулю. После этого происходит присвоение *pD = 13.0 ; , в результате которого число, имеющее тип double, записывается в переменную n, имеющую тип int. Затем все три переменные снова выводятся на экран.
После записи действительного числа в целочисленную переменную n переменная upper оказалась "забитой" каким-то мусором, что видно из результата работы программы:
upper = 0
n = 0
lower = 0
Сохранение double в int
upper = 1076494336
n = 0
lower = 0
Press any key to continue...
На языке домов и адресов эта программа будет выглядеть так:
house* houseAddress = &"123 Main Street" ;
hotel* hotelAddress ;
hotelAddress = ( hotel* )houseAddress ;
*hotelAddress = TheRitz ;
Указатель houseAddress инициализирован как указатель на мой дом. Переменная hotelAddress содержит адрес отеля. После этого вместо адреса моего дома записывается адрес отеля. Затем отель "Ритц" устанавливается по адресу моего дома. Однако поскольку "Ритц" куда больше моего дома, не удивительно, что он уничтожит не только мой дом, но и дома моих соседей ( хоть что-то приятное в результате ошибки! ).
Типизация указателей предохраняет программиста от неприятностей, связанных с сохранением данных большего размера в меньшем объёме памяти. Присвоение *pintVar = 100.0 не вызывает никаких проблем, поскольку С++ известно, что pintVar указывает на целочисленную переменную и приводит 100.0 перед присвоением к тому же типу.
Одним из путей использования указателей является передача аргументов функции. Для того чтобы понять всю важность этого метода, необходимо разобраться, как происходит передача аргументов функциям.
_________________
111 стр. Глава 8. Первое знакомство с указателями в С++
Вы могли заметить, что обычно нельзя изменить значение переменной, которая передавалась функции как аргумент. Рассмотрим следующий фрагмент кода:
void fn( intArg )
{
int intArg = 10 ;
/* Здесь значение intArg равно 10 */
}
void parent( void )
{
int n1 = 0 ;
fn( n1 ) ;
/* Здесь n1 равно 0 */
}
Функция parent( ) инициализирует переменную n1 нулём. После этого значение n1 передаётся в качестве аргумента функции fn( ). В fn( ) переменной intArg присваивается значение 10 , тем самым в fn( ) осуществляется попытка изменить аргумент функции. Поскольку в качестве аргумента выступает переменная n1, можно ожидать, что после возврата в parent( ) эта переменная должна иметь значение 10. Тем не менее n1 остаётся равной 0.
Дело в том, что С++ передаёт функции не переменную, а значение, которое в момент вызова функции находится в переменной. При вызове функции происходит вычисление значения передаваемого функции выражения, даже если это просто переменная.
«Некоторые программисты, стараясь не быть многословными, говорят что-то вроде "передаём переменную х функции fn( )". На самом деле это означает, что функции fn( ) передаётся значение выражения х.»
[Атас!]
Указатель, как и любая другая переменная, может быть передан функции в качестве аргумента.
void fn( int* pintArg )
{
*pintArg = 10 ;
}
void parent( void )
{
int n = 0 ;
fn( &n ) ; /* Так передаётся адрес n */
/* теперь n равно 10 */
}
В этом случае вместо значения n функции fn( ) передаётся адрес этой переменной. Чем отличается передача значения переменной от передачи значения указателя на переменную, станет понятно, если рассмотреть присвоение, выполняющееся в функции fn( ).
Предположим, что n находится по адресу 0x102. В этом случае функции fn( ) передаётся аргумент, равный 0x102. Внутри fn( ) присвоение *pintArg = 10 выполняет запись целого значения 10 в переменную типа int, которая находится по адресу 0x102. Таким образом, нуль в переменной n заменяется на 10 , поскольку в данном случае 0x102 и есть адрес переменной n.
_________________
112 стр. Часть 2. Становимся функциональными программистами
В С++ возможна сокращённая запись приведённого выше фрагмента, которая не требует от программиста непосредственной работы с указателями. В представленном ниже примере переменная n передаётся по ссылке.
void fn( int& intArg )
{
intArg = 10 ;
}
void parent( void )
{
int n = 0 ;
fn ( n )
/* Теперь значение n равно 10 */
}
В этом примере функция fn( ) получает не значение переменной n, а ссылку на неё и, в свою очередь, записывает 10 в переменную типа int, на которую ссылается intArg.
Куча ( heap ) — это блок памяти изменяемого размера, который при необходимости может использоваться программой. Далее в этом разделе поясняется, зачем нужна куча и как ею пользоваться.
«Visual С++ .NET позволяет программисту писать код, который работает в т.н. управляемом режиме ( managed mode ), когда выделение и освобождение памяти обрабатывает компилятор. Поскольку таким режимом отличается только Visual С++ .NET, в данной книге он не рассматривается.»
[Атас!]
Очевидно, что если можно передать функции указатель, то можно и вернуть его как результат работы функции. Функция, которая должна вернуть некоторый адрес, объявляется следующим образом:
double* fn( void ) ;
При работе с возвращаемыми указателями следует быть очень осторожным. Чтобы понимать, чем чревато неаккуратное использование указателей, следует познакомиться с концепцией области видимости переменных ( т.е. с тем, где именно от переменных остаётся только видимость... ).
Кроме значения и типа, переменные в С++ имеют ещё одно свойство — область видимости, т.е. часть программы, в которой эта переменная определена. Рассмотрим следующий фрагмент кода:
/* Эта переменная доступна для всех функций и существует на протяжении всего времени работы программы ( глобальная область видимости ) */
int intGlobal ;
/* Переменная intChild доступна только в функции child( ) и существует только во время выполнения функции child( ) или вызываемой ею ( область видимости функции ) */
_________________
113 стр. Глава 8. Первое знакомство с указателями в С++
void child( void )
{
int intChild ;
}
/* Переменная intParent имеет область видимости функции */
void parent( void )
{
int intParent = 0 ;
child( ) ;
int intLater = 0 ;
intParent = intLater ;
}
int main( int nArgs , char* pArgs[ ] )
{
parent( ) ;
}
Программа начинает выполнять функцию main( ). В первой же строке main( ) вызывает функцию parent( ). В этой функции объявляется переменная intParent, которая имеет область видимости, ограниченную функцией. Такая переменная называется локальной и доступна только в этой функции.
Во второй строке parent( ) вызывается функция child( ). Эта функция также объявляет локальную переменную — intChild, областью видимости которой является функция child( ). При этом intParent функции child( ) недоступна ( и область видимости intParent не распространяется на функцию child( ) ), но сама переменная продолжает существовать.
После окончания работы функции child( ) переменная intChild выходит из области видимости и уничтожается, т.е. она не только недоступна, но и не существует ( память, которую занимала эта переменная, возвращена в пул свободной памяти для дальнейшего использования ).
После возврата из функции child( ) продолжается выполнение подпрограммы parent( ), и в следующей строке объявляется переменная intLater, которая имеет область видимости, ограниченную функцией parent( ). В момент возврата в функцию main( ) переменные intLater и intParent выходят из области видимости и уничтожаются.
Кроме локальных переменных, программист может объявлять переменные вне всех функций. Такие переменные называются глобальными, они доступны из любого места программы ( их область видимости — вся программа ).
Поскольку intGlobal в приведённом коде объявлена глобально, она доступна на протяжении работы всей программы и внутри любой из трёх функций.
Приведённый ниже фрагмент программы будет скомпилирован, но не будет корректно работать.
double* child( void )
{
double dLocalVariable ;
return &dLocalVariable ;
}
void parent( void )
{
double* pdLocal ;
pdLocal = child( ) ;
*pdLocal = 1.0 ;
}
_________________
114 стр. Часть 2. Становимся функциональными программистами
Проблема в том, что переменная dLocalVariable объявлена внутри функции child( ). Следовательно, в момент возврата адреса dLocalVariable из child( ) самой переменной уже не существует и адрес ссылается на память, которая вполне может быть занята для каких-то других целей.
«Ошибки подобного типа встречаются довольно часто, а способы их появления весьма разнообразны. К сожалению, такой тип ошибки пропускается компилятором и зачастую не вызывает аварийной остановки программы. Программа может отлично работать большую часть времени, пока память, которая в прошлом выделялась под dLocalVariable, не будет выделена другой переменной. Труднее всего найти ошибки, проявляющиеся спонтанно.»
[Атас!]
Ошибки области видимости возникают потому, что С++ освобождает выделенную для локальных переменных память автоматически. Для решения этой проблемы необходим блок памяти, контролируемый непосредственно программистом. В этом блоке можно выделять память под переменные и удалять их независимо от того, что по этому поводу "думает" С++. Такой блок памяти называется кучей ( heap ).
Память в куче можно выделить, используя оператор new ; он пишется вместе с типом объекта, под который нужно выделить память. Приведённый ниже пример выделяет из кучи память для переменной типа double.
double* child( void )
{
double* pdLocalVariable = new double ;
return pdLocalVariable ;
}
Теперь, несмотря на то что переменная pdLocalVariable имеет область видимости в пределах функции child( ), память, на которую указывает эта переменная, не будет освобождена после выполнения функции. Выделение и освобождение памяти в куче осуществляется только явно. Освобождение памяти в куче выполняется с помощью команды delete.
void parent( void )
{
/* функция child( ) возвращает адрес переменной в куче */
double* pdMyDouble = child( ) ;
/* сохранение значения в созданной переменной */
*pdMyDouble = 1.1 ;
// ...
/* возврат памяти куче */
delete pdMyDouble ;
pdMyDouble = 0 ;
// ...
}
_________________
115 стр. Глава 8. Первое знакомство с указателями в С++
В этой программе указатель, возвращённый функцией child( ), используется для записи значения типа double в память, выделенную в куче. После того как функция выполнила все необходимые действия с этой памятью, она освобождается, а указатель pdMyDouble устанавливается равным нулю. Обнуление указателя не обязательно, но крайне желательно. В этом случае, если программист ошибётся и попытается опять записать что-либо по адресу, на который указывает pdMyDouble, произойдёт аварийный останов программы.
«Ошибку, в результате которой происходит аварийный останов программы, найти гораздо проще, чем проявляющуюся спонтанно.»
[Советы]
_________________
116 стр. Часть 2. Становимся функциональными программистами
В этой главе...
►Объявление и использование массивов указателей 124
Язык С++ позволяет работать с указателями так, как если бы они были переменными простых типов. ( Концепция указателей изложена в главе 8, "Первое знакомство с указателями в С++". ) Однако операции над указателями требуют знания некоторых тонкостей; именно они и рассматриваются в этой главе.
Некоторые из операций, описанных в главе 3, "Выполнение математических операций", могут быть применены и к указателям. В этом разделе рассматривается использование арифметических операций при работе с указателями и массивами ( с массивами вы познакомились в главе 7, "Хранение последовательностей в массивах" ). В табл. 9.1 приведены базовые операции над указателями.
Таблица 9.1. Три операции над указателями
_________________
Операция — Результат — Действие
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
pointer+offset — Указатель — Вычисляет адрес элемента, расположенного через offset элементов после pointer
pointer-offset — Указатель — Операция, противоположная сложению
pointer2-pointer1 — Смещение — Вычисляет количество элементов между pointer1 и pointer2
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
В этой таблице offset имеет тип int ( здесь не приведены операции, близкие к сложению и вычитанию, такие как ++ и +=, которые также могут применяться к указателям ).
Модель памяти, построенная на примере домов ( так эффективно использованная в предыдущей главе ), поможет понять, как работают приведённые в таблице операции с указателями. Представьте себе квартал, в котором все дома пронумерованы по порядку. Дом, следующий за домом 123 Main Street, будет иметь адрес 124 Main Street ( или 122 Main Street, если вы идёте в противоположную сторону, поскольку вы левша или живёте в Англии ).
Очевидно, что в таком случае через четыре дома от моего будет находиться дом с адресом 127 Main Street. Адрес этого дома можно записать как
123 Main Street + 4 = 127 Main Street
_________________
117 стр. Глава 9. Второе знакомство с указателями
И наоборот, если поинтересоваться, сколько домов находится между домом 123 и 127, ответом будет четыре:
127 Main Street - 123 Main Street = 4
Понятно, что любой дом находится относительно самого себя на расстоянии нуль домов:
123 Main Street - 123 Main Street = 0
Продолжая рассуждения, становится понятно, что складывать дома 123 и 127 не имеет никакого смысла. Соответственно, суммирование двух указателей является в С++ некорректной операцией. Вы также не можете умножать или делить адреса, возводить их в квадрат или извлекать квадратный корень — словом, надеюсь, вы поняли, что я хотел сказать.
Обратимся к странному и мистическому миру массивов. Ещё раз воспользуемся в качестве примера домами моих соседей. Массив тоже очень похож на городской квартал. Каждый элемент массива выступает в качестве дома в этом квартале. Дома — элементы массива — отсчитываются по порядку от начала квартала. Дом на углу улицы отстоит на 0 домов от угла, следующий дом отстоит на 1 дом от угла и т.д. Пользуясь терминологией массивов, можно сказать, что cityBlock[ 0 ] представляет собой дом по адресу 123 Main Street, cityBlock[ 1 ] — дом по адресу 124 Main Street и т.д.
Теперь представим себе массив из 32-х однобайтовых значений, имеющий имя charArray. Если первый элемент массива находится по адресу 0x110 , тогда массив будет продолжаться вплоть до адреса 0x12f. Таким образом, элемент массива charArray[ 0 ] находится по адресу 0x110 , charArray[ 1 ] — по адресу 0x111 , charArray[ 2 ] — по адресу 0x112 и т.д.
Можно создать указатель ptr на нулевой элемент массива. После выполнения строки ptr = &charArray[ 0 ] ; указатель ptr будет содержать адрес 0x110. Можно прибавить к этому адресу целочисленное смещение и перейти к необходимому элементу массива. Операции над массивами с использованием указателей приведены в табл. 9.2. Эта таблица демонстрирует, каким образом добавление смещения n вызывает переход к следующему элементу массива charArray.
Таблица 9.2. Добавление смещения
_________________
Смещение — Результат — Соответствует
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+0 — 0x110 — charArray[ 0 ]
+1 — 0x111 — charArray[ 1 ]
+2 — 0x112 — charArray[ 2]
... — ... — ...
+n — 0х110+n — charArray[ n ]
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Как видите, добавление смещения к указателю на массив равнозначно переходу к соответствующему значению.
Таким образом, если char* ptr — &charArray[ 0 ] ;, то *( ptr + n ) соответствует элементу charArray [ n ].
«Поскольку * имеет более высокий приоритет, чем сложение, операция *ptr + n привела бы к сложению n со значением, на которое указывает ptr. Чтобы выполнить сначала сложение и лишь затем переход к переменной по указателю, следует использовать скобки. Выражение *( ptr + n ) возвращает элемент, который находится по адресу ptr плюс n элементов.»
[Атас!]
_________________
118 стр. Часть 2. Становимся функциональными программистами
В действительности соответствие между двумя формами выражений настолько строго, что С++ рассматривает элемент массива array[ n ] как *( ptr + n ), где ptr указывает на первый элемент array. С++ интерпретирует array[ n ] как *( &аrray [ 0 ] +n ). Таким образом, если дано char charArray[ 20 ], то charArray определяется как &charArray[ 0 ].
Имя массива, записанное без индекса элемента, интерпретируется как адрес нулевого элемента массива ( или просто адрес массива ). Таким образом, можно упростить приведённую выше запись, поскольку array[ n ] С++ интерпретирует как *( array + n ).
Концепция соответствия между индексацией массива и арифметикой указателей весьма полезна.
Например, функция displayArray( ), которая выводит содержимое целочисленного массива, может быть реализована следующим образом:
/* displayArray — отображает элементы массива, имеющего длину nSize */
void displayArray( int intArray[ ] , int nSize )
{
cout << "Значения элементов массива равны:\n" ;
for ( int n = 0 ; n < nSize ; n++ )
{
cout << n << ": " << intArray[ n ] << "\n" ;
}
cout << "\n" ;
}
Эта версия функции использует операции над массивами, которые знакомы нам по предыдущим главам. Если воспользоваться для написания этой функции указателями, программа приобретёт такой вид:
/* displayArray — отображает элементы массива, имеющего длину nSize */
void displayArray( int intArray[ ] , int nSize )
{
cout << "Значения элементов массива равны:\n" ;
int* pArray = intArray ;
for ( int n = 0 ; n < nSize ; n++ , pArray++ )
{
cout << n << ": " << *pArray << "\n" ;
}
cout << "\n" ;
}
Этот вариант функции displayArray начинается с создания указателя на первый элемент массива intArray.
«Буква р в начале имени переменной означает, что эта переменная является указателем, однако это только соглашение, а не стандарт языка С++.»
[Помни!]
_________________
119 стр. Глава 9. Второе знакомство с указателями
После этого функция считывает все элементы массива по порядку. При каждом выполнении оператора for происходит вывод текущего элемента из массива intArray. Этот элемент находится по адресу рArray, который увеличивается на единицу при каждом выполнении цикла.
Убедиться в работоспособности описанной функции можно, используя её в следующей функции main( ):
int main( int nNumberOfArgs , char* pszArgs[ ] )
{
int array[ ] = { 4 , 3 , 2 , 1 } ;
displayArray( array , 4 ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Результат работы этой программы имеет следующий вид:
Значения элементов массива равны:
0: 4
1: 3
2: 2
3: 1
Press any key to continue...
Можно сказать, что функция почти не изменилась и выполняет такие же операции, как и предыдущая версия, однако использование указателей — более распространённая практика, чем работа с массивами. По ряду причин программисты избегают работать с массивами. Чаще всего указатели используются для работы с символьными массивами.
Строку с завершающим нулевым символом можно рассматривать как массив символов, в котором последний символ равен нулю ( язык С++ использует нуль как символ конца строки ). Такие нуль-завершённые массивы можно рассматривать как отдельный тип ( точнее, квази-тип ), о котором шла речь в главе 7, "Хранение последовательностей в массивах". В С++ для работы со строками часто используются указатели. В приведённых ниже примерах показано, каковы отличия в работе со строками в случае применения массивов и указателей.
С помощью указателей можно работать с символьными массивами так же, как и с массивами любого другого типа. То, что в конце любой строки находится символ конца строки, делает их особенно удобными для работы с помощью указателей, что видно на примере следующей программы.
/* DisplayString — вывод символьного массива с использованием указателей и индексов массива */
#include
#include
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Объявляем строку */
char* szString = "Randy" ;
_________________
120 стр. Часть 2. Становимся функциональными программистами
cout << "Массив ' " << szString << " ' " << endl ;
/* Выводим szString как массив */
cout << "Выводим строку как массив: " ;
for ( int i = 0 ; i < 5 ; i++ )
{
cout << szString[ i ] ;
}
cout << endl ;
/* Воспользуемся арифметикой указателей */
cout << "Выводим строку с помощью указателя: " ;
char* pszString = szString ;
while ( *pszString )
{
cout << *pszString ;
pszString++ ;
}
cout << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Программа сначала проходит по массиву szString с использованием индекса массива. Цикл for прекращает работу, когда индекс достигает значения 5, равного длине строки.
Второй цикл выводит ту же строку с использованием указателя. Программа устанавливает указатель pszString равным адресу первого символа массива. Затем цикл проходит по всем символам строки до тех пор, пока символ, на который указывает pszString, не станет равен false, другими словами, пока указатель не станет указывать на нулевой символ.
«Целое значение 0 в С++ рассматривается как false, прочие целочисленные значения — как true.»
[Помни!]
Программа выводит символ, на который указывает pszString, а затем увеличивает его значение, с тем чтобы указатель указывал на очередной символ строки перед выполнением очередной итерации.
«Разыменование и инкремент могут быть объединены ( обычно так и делается ) в единое выражение следующим образом:
cout << *pszString++ ;
»
[Помни!]
Вывод данной программы выглядит так:
Массив ' Randy '
Выводим строку как массив: Randy
Randy Выводим строку с помощью указателя: Randy
Press any key to continue...
_________________
121 стр. Глава 9. Второе знакомство с указателями
Иногда некоторая запутанность работы с указателями вызывает у читателя вполне резонный вопрос: почему рекомендуется использовать указатели? Иными словами, что делает использование указателя char* предпочтительнее более простого для чтения варианта с массивами и индексами?
Ответ следует искать отчасти в человеческой природе, отчасти в истории развития С++. Компилятор языка С, прародителя С++, в те времена, когда язык появился на свет, был довольно примитивен. Тогда компиляторы не были столь сложными, как сейчас, и не могли так хорошо оптимизировать код. Код while ( *pszTarget++ = *pszSource++ ){ } может показаться читателю сложным, однако после компиляции с помощью даже самого древнего компилятора он будет состоять буквально из нескольких машинных инструкций.
Старые компьютеры были не очень быстрыми по современным меркам. Во времена С экономия нескольких машинных инструкций значила очень много, что и привело к превосходству С над другими языками того времени, такими как FORTRAN, который не поддерживал работу с указателями.
Именно тогда и зародилась традиция писать компактные и эффективные, правда, подчас несколько загадочные на вид программы на С++, и с тех пор никто не хочет возвращаться к индексам.
«Не надейтесь, что, написав сложное и запутанное выражение на С++, вы сэкономите несколько машинных команд. В С++ нет прямой связи между количеством команд в исходном и конечном коде.»
[Советы]
Нетрудно сообразить, что szTarget+n указывает на элемент szTarget[ n ], если szTarget является массивом однобайтовых значений. Если szTarget начинается по адресу 0x100 , то шестой элемент массива будет находиться по адресу 0x105.
Однако положение элемента в массиве становится не столь очевидным, если массив состоит из элементов типа int, которые занимают по четыре байта каждый. Если первый элемент такого массива находится по адресу 0x100 , то шестой будет находиться по адресу 0x114( 0x100 + ( 5 * 4 ) = 0x114 ).
Но, к счастью для нас, выражение вида array + n будет всегда указывать на элемент array[ n ], независимо от размера элемента, поскольку в таком выражении С++ самостоятельно учитывает длину элемента.
И вновь обратимся за аналогией к моему дому. Третий дом от 123 Main Street будет иметь адрес 126 Main Street, независимо от размеров стоящих на Main Street домов.
В использовании массива и указателя есть несколько отличий. Во-первых, объявление массива вызывает выделение памяти для всего массива, тогда как объявление указателя не требует выделения памяти для массива.
void arrayPointer( )
{
/* Выделение памяти для 128 символов */
char charArray[ 128 ] ;
/* Выделение памяти для указателя, но не для объекта, на который он указывает */
char* pArray ;
}
_________________
122 стр. Часть 2. Становимся функциональными программистами
В этом примере для charArray выделяется 128 байт, а для pArray — четыре, ровно столько, сколько необходимо для хранения указателя. Приведённая ниже функция работать не будет.
void arrayVsPointer( )
{
/* Этот фрагмент будет работать нормально */
char charArray[ 128 ] ;
charArray[ 10 ] = '0' ;
*( charArray + 10 ) = '0' ;
/* Этот фрагмент не будет работать так, как надо */
char* pArray ;
pArray[ 10 ] = '0' ;
*( pArray + 10 ) = '0' ;
}
Выражения charArray[ 10 ] и *( charArray + 10 ) с позиции компилятора эквивалентны и вполне законны. Те же выражения с использованием pArray являются бессмысленными. Несмотря на то что для С++ они являются законными, pArray не инициализирован как указатель на массив, а значит, память была выделена только для указателя. Таким образом, рАггау[ 10 ] и *( рАггау + 10 ) указывают на неизвестные и непредсказуемые значения.
«Неправильно инициализированные указатели обычно вызывают ошибку нарушения сегмента ( segment violation ). Эту ошибку вы иногда встречаете в повседневной работе со своими любимыми приложениями в своей любимой ( а может, и не очень ) операционной системе.»
[Советы]
Второе отличие между указателями и индексами массива состоит в том, что charArray — константа, тогда как pArray — нет. Приведённый ниже цикл for, который должен инициализировать значения элементов массива, тоже не будет работать.
void arrayVsPointer( )
{
char charArray[ 10 ] ;
for ( int i = 0 ; i < 10 ; i++ )
{
*charArray = '\0' ; /* Эта строка имеет смысл... */
charArray++ ; /* ... а эта нет */
}
}
Выражение charArray++ имеет не больше смысла, чем 10++. Правильно следует написать так:
void arrayVsPointer( )
{
char charArray[ 10 ] ;
char* pArray = charArray ;
for ( int i = 0 ; i < 10 ; i++ )
{
*pArray = '\0' ; /* Этот вариант будет работать так, как надо */
pArray++ ;
}
}
_________________
123 стр. Глава 9. Второе знакомство с указателями
Если есть указатели на массивы, можно предположить, что существуют и массивы указателей. Именно их мы сейчас и рассмотрим.
Поскольку массив может содержать данные любого типа, он может состоять и из указателей. Массив указателей объявляется так:
int* pInts[ 10 ] ;
Таким образом, элемент pInts[ 0 ] является указателем на переменную типа int. Следовательно, приведённый ниже код корректен:
void fn( )
{
int n1 ;
int* pInts[ 3 ] ;
pInts[ 0 ] = &n1 ;
*pInts[ 0 ] = 1 ;
}
Как и этот:
void fn( )
{
int n1 , n2 , n3 ;
int* pInts[ 3 ] = { &n1 , &n2 , &n3 } ;
for ( int i = 0 ; i < 3 ; i++ )
{
*pInts[ i ] = 0 ;
}
}
И даже этот:
void fn( )
{
int n1 , n2 , n3 ;
int* pInts[ 3 ] = { ( new int ) ,
( new int ) ,
( new int ) } ;
for ( int i = 0 ; i < 3 ; i++ )
{
*pInts[ i ] = 0 ;
}
}
В последнем варианте память под переменные выделяется из кучи.
Массивы указателей чаще всего используются для работы с массивами строк. Приведённые далее примеры показывают, почему это удобно.
Допустим, мне понадобилась функция, возвращающая название месяца по его номеру. Например, если этой функции передать число 1, она вернёт название первого месяца — "Январь". Номер месяца будет считаться неправильным, если он окажется меньше 1 или больше 12.
_________________
124 стр. Часть 2. Становимся функциональными программистами
Эту функцию можно написать следующим образом:
/* int2month( ) — возвращает название месяца */
char* int2month( int nMonth )
{
char* pszReturnValue ;
switch( nMonth )
{
case 1 : pszReturnValue = "Январь" ;
break ;
case 2 : pszReturnValue = "Февраль" ;
break ;
case 3 : pszReturnValue = "Март" ;
break ;
/* и так далее... */
default : pszReturnValue = "Неверный номер месяца"
}
return pszReturnValue ;
}
«Оператор switch( ) действует так же, как совокупность операторов if.»
[Помни!]
Эту задачу можно решить более элегантно, использовав номер месяца как индекс в массиве указателей, представляющих названия месяцев. Тогда программа приобретёт такой вид:
/* int2month( ) — возвращает название месяца */
char* int2month( int nMonth )
{
/* проверка правильности номера месяца */
if ( nMonth < 1 || nMonth > 12 )
{
return "invalid" ;
}
/* nMonth имеет корректное значение */
/* Вернём имя месяца */
char* pszMonths[ ] = { "Ошибка" ,
"Январь" ,
"Февраль" ,
"Март" ,
"Апрель" ,
"Май" ,
"Июнь" ,
"Июль" ,
"Август" ,
"Сентябрь" ,
"Октябрь" ,
"Ноябрь" ,
"Декабрь" } ;
return pszMonths[ nMonth ] ;
}
Сначала в этой программе проверяется корректность аргумента nMonth, т.е. что его значение лежит в диапазоне между 1 и 12 включительно ( в предыдущей программе проверка производилась, по сути, оператором default ). Если значение nMonth правильное, оно используется как смещение внутри массива, содержащего названия месяцев.
_________________
125 стр. Глава 9. Второе знакомство с указателями
«Такой способ обращения к строкам по индексу особенно полезен при написании программы, работающей на разных языках. Например, массив названий месяцев может инициализироваться во время работы с названиями на разных языках, так что ptrMonth[ 1 ] всегда будет указывать на январь независимо от используемого языка.»
[Советы]
Второй аргумент функции main( ) — массив указателей на строки. Эти строки содержат аргументы, передаваемые программе при вызове. Допустим, я ввёл следующее в командной строке MS DOS:
MyProgram file.txt /w
MS DOS запустит программу, которая находится в файле MyProgram.ехе, и передаст ей как аргументы file.txt и /w. Аргументы, начинающиеся с косой черты ( / ) или дефиса ( - ), обрабатываются операционной системой, как и любые другие: они передаются программе, чтобы та разбиралась с ними сама. Аргументы, которые начинаются с <, >, >> или || ( а иногда и некоторые другие ), представляют особый интерес для операционных систем и программе не передаются.
Аргументы программы являются одновременно аргументами функции main( ). Переменная pszArgs, передаваемая main( ), содержит массив указателей на аргументы программы, a nArg — их количество.
Ниже приведён пример считывания аргументов из командной строки.
/* PrintArgs — выводит аргументы программы в стандартный вывод операционной системы */
#include
#include
#include
using namespace std ;
int main( int nArg , char* pszArgs[ ] )
{
setlocale (LC_ALL,".1251"); /* печать русских текстов */
/* Выводим заголовок */
cout << "Аргументами программы " << pszArgs[ 0 ]
<< " являются\n" ;
/* Выводим аргументы программы */
for ( int i = 1 ; i < nArg ; i++ )
{
cout << i << ": " << pszArgs[ i ] << "\n" ;
}
// Вот и всё
cout << "Вот и всё \n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
126 стр. Часть 2. Становимся функциональными программистами
Как всегда, функция main( ) получает два аргумента. Первый — переменная типа int, которую я назвал nArg. Эта переменная содержит количество передаваемых программе аргументов. Вторая переменная содержит массив указателей типа char*; её я назвал pszArgs. Каждый из этих указателей ссылается на один из аргументов программы.
Аргументы в DOS...127
Если запустить программу PrintArgs с аргументами
PrintArgs arg1 arg2 arg3 /w
из командной строки MS DOS, nArg будет равняться 5 ( по количеству аргументов ). Первый аргумент — имя самой программы. Таким образом, pszArgs [ 0 ] будет указывать на имя запускаемого файла, а остальные четыре указателя — на оставшиеся четыре аргумента ( в данном случае это "arg1", "arg2", "arg3" и "/w" ). Поскольку MS DOS никак не выделяет символ /, последний аргумент будет представлять собой строку "/w".
Аргументы в Dev-C++...127
Для того, чтобы передать аргументы программе при запуске её в среде Dev-C++, можно воспользоваться командой меню Debugs => Parameters ( Отладкам => Параметры ). Введите нужные вам параметры и запустите программу на выполнение при помощи меню Ехесute => Run ( Выполнить => Выполнить ) либо клавиш
Аргументы в Windows...127
Windows использует аргументы как средство коммуникации с программой. Проведите следующий эксперимент. Соберите описанную программу и найдите её с помощью Windows Explorer. Например, пусть она имеет имя X:\Cpp_Program\Chap09\PrintArgs.exe. Возьмите произвольный файл и перетащите его на имя файла программы — после этого запустится программа PrintArgs, и вы увидите имя перемещённого файла. Попробуйте перетащить несколько файлов одновременно ( выделив их при нажатой клавише
Вот как выглядит вывод программы, если перетащить на неё файлы из папки Dev-C++.
Аргументами программы E:\Tmp\PrintArgs.exe являются
1: C:\Dev-Cpp\devcpp.exe
2: C:\Dev-Cpp\copying.txt
3: C:\Dev-Cpp\NEWS.txt
4: C:\Dev-Cpp\Packman.exe
5: C:\Dev-Cpp\uninstall.exe
6: C:\Dev-Cpp\vRoach.exe
7: C:\Dev-Cpp\vUpdate.exe
Вот и всё
Press any key to continue...
Обратите внимание, что каждое имя файла представлено как отдельный аргумент; кроме того, как видите, Windows передаёт в качестве параметра полное имя файла.
_________________
127 стр. Глава 9. Второе знакомство с указателями
В этой главе...
►Использование отладочной печати 128
Не часто случается ( особенно с "чайниками" ), что программа идеально работает с первого раза. Крайне редко удаётся написать нетривиальную программу и не допустить ни одной ошибки.
Чтобы избавиться от ошибок, можно пойти двумя путями. Первый — стереть программу и написать её заново, а второй — найти и исправить ошибки. Освоение первого пути я оставляю читателю, а в этой главе расскажу о том, как выследить и исправить ошибку в программе.
Можно выделить два типа ошибок: те, которые компилятор может найти, и те, которые не может. Первый тип называют ошибками компиляции ( compile-time error ). Их довольно легко найти, поскольку компилятор сам указывает место в программе, где встретилась ошибка. Правда, иногда описание ошибки бывает не совсем точным ( компьютер так легко сбить с толку! ), однако, зная капризы своего компилятора, нетрудно разобраться в его жалобах.
Ошибки, которые компилятор не может найти, проявляются при запуске программы и называются ошибками времени исполнения ( run-time error ). Их найти намного труднее, поскольку, кроме сообщения об ошибке, нет и намёка на то, какая именно ошибка возникла и где ( сообщения, которые генерируются при возникновении ошибок выполнения, вполне достойны "звания" ошибочных ).
Для выявления "жучков" в программе обычно используется два метода. Первый — добавить отладочные команды, выводящие ключевые значения в ключевых точках программы. Увидев значения переменных в месте возникновения ошибки, можно понять, что именно неправильно в данной программе. Второй метод заключается в использовании специальной программы — отладчика. Отладчик позволяет отслеживать процесс выполнения программы.
Добавление команд вывода в ключевых точках помогает понять, что происходит в программе, и называется методом отладочной печати ( иногда именуемым WRITE ). Метод WRITE появился во времена, когда программы писались на языке FORTRAN, в котором вывод осуществляется с помощью команды WRITE.
_________________
128 стр. Часть 2. Становимся функциональными программистами
Приведённая ниже "дефектная" программа наглядно демонстрирует применение отладочных команд. Эта программа должна считывать последовательность чисел с клавиатуры и выводить их среднее арифметическое значение. Однако она не делает этого, поскольку содержит две ошибки, одна из которых вызывает аварийный останов, а вторая приводит к неправильному результату.
«Данная программа имеется на прилагаемом компакт-диске под именем ErrorProgram1.срр.»
[Диск]
/* ErrorProgram — эта программа усредняла бы ряд чисел, если бы не содержала как минимум одну фатальную ошибку */
#include
#include
#include
using namespace std ;
int main( int nNumberOfArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
cout << "Эта программа содержит ошибки!\n" ;
int nSum ;
int nNums ;
/* Суммируем ряд чисел, пока пользователь не введёт отрицательное число, после чего выводим среднее */
nNums = 0 ;
while ( true )
{
/* Ввод очередного числа */
int nValue ;
cout << "Введите следующее число:" ;
cin >> nValue ;
cout << endl ;
/* Если это число отрицательное... */
if ( nValue < 0 )
{
/* ...то вывести среднее значение */
cout << "Среднее равно: "
<< nSum / nNums
<< endl ;
break ;
}
/* Введеённое число не отрицательно — добавляем его к накапливаемой сумме */
nSum += nValue ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
129 стр. Глава 10. Отладка программ на С++
После ввода этой программы создайте выполнимый файл ( клавиша
«Рис. 10.1 может отличаться в зависимости от используемых операционной системы и компилятора.»
[Помни!]
Рис. 10.1. Первоначальная версия программы содержит как минимум одну ошибку
Очень сложно разобраться, в чём именно проблема, по столь неинформативному сообщению. Но в некоторых операционных системах попроще вы получите сообщение ( поверьте на слово ), в котором будет туманный намёк относительно деления на нуль. Впрочем, мы и сами можем догадаться о том, что проблема где-то рядом, поскольку ошибка произошла после того, как мы ввели отрицательное число, но до того, как на экран был выведен результат. Давайте ещё раз посмотрим на так и не выполнившуюся инструкцию:
cout << "Среднее равно: "
<< nSum / nNums
<< endl ;
«Кстати говоря, хотя это и единственное деление, использованное нами в программе, это ещё не означает, что ошибка именно в нём. Компилятор может сгенерировать команду деления в результате обработки некоторой иной инструкции, написанной программистом. Кроме того, делений хватает и в стандартной библиотеке С++.»
[Советы]
Давайте посмотрим, чему равно значение nNums перед выполнением деления, изменив код следующим образом:
while ( true )
{
cout << "nNums = " << nNums << endl ;
/* Остальная часть программы остаётся неизменной */
_________________
130 стр. Часть 2. Становимся функциональными программистами
Такое дополнение нашей программы приводит к следующему выводу на экран:
Эта программа содержит ошибки!
nNums = 0
Введите следующее число: 1
nNums = 0
Введите следующее число: 2
nNums = 0
Введите следующее число: 3
nNums = 0
Введите следующее число:
Как видите, nNums инициализировано нулевым значением, но оно не увеличивается в процессе ввода новых чисел. Это неверно, и именно в этом состоит ошибка в программе. Очевидно, что количество введённых чисел nNums должно увеличиваться, чего можно легко достичь, заменив цикл while циклом for:
for ( int nNums = 0 ; ; nNums++ )
Теперь, когда найдена и исправлена ошибка № 1, можно запустить программу, введя числа, заставившие её в прошлый раз аварийно завершиться. На этот раз сообщение об ошибке не появится и программа вернёт нулевой код выхода, но будет работать не так, как ожидалось. Вместо так горячо ожидаемой двойки будет выведено какое-то нелепое число.
Эта программа содержит ошибки:
Введите следующее число: 1
Введите следующее число: 2
Введите следующее число: 3
Введите следующее число: -1
Среднее равно: 229523
Очевидно, какая-то из переменных — nNums или nSum ( а возможно, и обе ) содержит неверное значение. Для того чтобы исправить ошибку, необходимо узнать, какая именно из этих переменных содержит неверную информацию. Не помешало бы также знать, что содержится в переменной nValue, поскольку она используется для подсчёта суммы в nSum.
Для этого воспользуемся методом отладочной печати. Чтобы узнать значения nValue, nSum и nNums, перепишите тело цикла for так, как показано в следующем листинге ( версия программы имеется на прилагаемом компакт-диске в файле с именем ErrorProgram2.срр ).
/* ErrorProgram — эта программа усредняла бы ряд чисел, если бы не содержала как минимум одну фатальную ошибку */
#include
#include
#include
using namespace std ;
int main ( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
cout << " Эта программа содержит ошибки!"
<< endl ;
/* Суммируем ряд чисел, пока пользователь не введёт отрицательное число, после чего выводим среднее */
int nSum ;
for ( int nNums = 0 ; ; nNums++ )
_________________
131 стр. Глава 10. Отладка программ на С++
{
/* Ввод следующего числа */
int nValue ;
cout << "Введите следующее число:" ;
cin >> nValue ;
cout << endl ;
/* Если введённое число отрицательно... */
if ( nValue < 0 )
{
/* ...выводим результат усреднения */
cout << "\nСреднее равно: "
<< nSum / nNums
<< "\n" ;
break ;
}
/* Вывод отладочной информации */
cout << "nSum = " << nSum << "\n" ;
cout << "nNums= " << nNums << "\n" ;
cout << "nValue= " << nValue << "\n" ;
cout << endl ;
/* Введённое число не отрицательно, суммируем его */
nSum += nValue ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Обратите внимание на то, что информация о состоянии отслеживаемых переменных nValue, nSum и nNums выводится в каждом цикле.
Ответ программы на ввод уже привычных 1, 2, 3 и -1 приведён ниже. При первом же проходе nSum принимает какое-то несуразное значение, хотя оно должно равняться нулю ( поскольку к этой переменной пока что ничего не прибавлялось ).
Эта программа содержит ошибки!
Введите следующее число:1
nSum = -858993460
nNums = 0
nValue= 1
Введите следующее число:2
nSum = -858993459
nNums= 1
nValue= 2
Введите следующее число:3
nSum = -858993457
nNums = 2
nValue= 3
Введите следующее число:
_________________
132 стр. Часть 2. Становимся функциональными программистами
Внимательно присмотревшись к программе, можно заметить, что nSum была объявлена, но не проинициализирована. Для того чтобы исправить эту ошибку, объявление переменной необходимо изменить следующим образом:
int nSum = 0 ;
Примечание. Пока переменная не проинициализирована, её значение непредсказуемо.
«Теперь, когда вы нашли все ошибки, перепишите программу так, как показано в следующем листинге ( эта программа имеется на прилагаемом компакт-диске в файле ErrorProgram3.срр ).»
[Диск]
/* ErrorProgram — эта программа усредняет ряд чисел и не содержит ошибок */
#include
#include
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
/* Суммируем ряд чисел, пока пользователь не введёт отрицательное число, после чего выводим среднее */
int nSum = 0 ;
for ( int nNums = 0 ; ; nNums++ )
{
/* Ввод следующего числа: */
int nValue ;
cout << "Введите следующее число:" ;
cin >> nValue ;
cout << endl ;
/* Если введённое число отрицательно... */
if ( nValue < 0 )
{
/* ...выводим усреднённое значение */
cout << "\nСреднее равно: "
<< nSum / nNums
<< "\n" ;
break ;
}
/* Введённое число не отрицательно, суммируем его */
nSum += nValue ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Теперь вывод программы будет правильным. Протестировав эту программу с другими наборами чисел, я убедился, что она работает без ошибок.
Введите следующее число: 1
Введите следующее число: 2
Введите следующее число: 3
Введите следующее число: -1
Среднее равно: 2
Press any key to continue...
_________________
133 стр. Глава 10. Отладка программ на С++
В небольших программах метод отладочной печати работает довольно неплохо. Добавление отладочных команд — достаточно простой и не влияющий на время компиляции способ нахождения ошибок, с помощью которого можно быстро отыскать ошибку, если программа невелика.
В больших программах зачастую программист даже не знает, куда нужно добавлять отладочные команды. Работа по добавлению отладочных команд, перезапуску программы, повторному добавлению отладочных команд и т.д. становится утомительной. Кроме того, после каждого переписывания программу нужно собирать заново. Не забывайте, что в большой программе один только процесс сборки может занять немало времени.
В конце концов, с помощью этого метода почти невозможно найти ошибку, связанную с указателями. Указатель, выведенный на экран в шестнадцатеричном виде, малоинформативен, и, пока программист поймёт, что нужно сделать для исправления ошибки, программа успеет морально устареть.
Второй, более изощрённый метод — использование отдельной утилиты, которая называется отладчиком. С помощью отладчика можно избежать трудностей, возникающих при использовании методики отладочной печати ( однако, если вы хотите использовать отладчик, вам придётся научиться с ним работать ).
Отладчик — это утилита, встроенная, например, в Dev-C++ или Microsoft Visual Studio .NET ( в этих приложениях программы отладчиков отличаются, однако работают они по одному принципу ).
Программист управляет отладчиком с помощью команд так же, как, например, при редактировании или компиляции программы. Команды отладчика можно выполнять с помощью контекстных меню или горячих клавиш.
Отладчик позволяет программисту контролировать работу программы по ходу её выполнения. С помощью отладчика можно выполнять программу в пошаговом режиме, останавливать её в любой точке и просматривать содержимое любой переменной. Чтобы оценить удобство отладчика, его нужно увидеть в действии.
В отличие от стандартизированного языка С++, набор команд, поддерживаемый отладчиком, варьируется от производителя к производителю. К счастью, большинство отладчиков поддерживают некоторый базовый набор команд. Необходимые нам команды есть как в Dev-С++, так и в Microsoft Visual С++ .NET; в них также имеется возможность вызова этих команд с помощью меню и функциональных клавиш. В табл. 10.1 приведён список основных команд и клавиш их вызова.
Таблица 10.1. Команды отладчиков Microsoft Visual С++ .NET и Dev-C++
_________________
Команда — Visual С++ — GNU С++ ( rhide )
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Отладка —
Шаг внутрь ( Step In ) —
Следующий шаг ( Step Over ) —
Продолжить выполнения —
Просмотр переменной ( View Variable ) — Только в меню — Только в меню
Установка точки останова ( Set Breakpoint )* —
Добавить в наблюдаемые ( Add watch ) — Только в меню —
Перезагрузка программы ( Program Reset ) —
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
__________
*Щелчок слева от строки исходного текста С++ в окне редактора представляет собой альтернативный путь установки точек останова.
_________________
134 стр. Часть 2. Становимся функциональными программистами
«Лучший способ исправить ошибки в программе — пройти её пошагово. Приведённая ниже программа содержит несколько ошибок, которые надо найти и исправить. Эта программа имеется на прилагаемом компакт-диске в файле Concatenate1.срр.»
[Диск]
/* Concatenate - конкатенация двух строк */
/* со вставкой " - " между ними. В этой версии имеются ошибки. */
#include
#include
#include
#include
using namespace std ;
void stringEmUp( char* szTarget ,
char* szSource1 ,
char* szSource2 ,
int nLength ) ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
cout << "Конкатенация двух строк со вставкой \" - \"\n"
<< "( В этой версии имеются ошибки. )" << endl ;
char szStrBuffer[ 256 ] ;
/* Создание двух строк одинаковой длины... */
char szString1[ 16 ] ;
strncpy( szString1 , "This is a string" , 16 ) ;
char szString2[ 16 ] ;
strncpy( szString2 , "THIS IS A STRING" , 16 ) ;
/* ...и объединение их в одну */
stringEmUp( szStrBuffer ,
szString1 ,
szString2 ,
16 ) ;
// Вывод результата
cout << "<" << szStrBuffer << ">" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
void stringEmUp(char* szTarget,
char* szSource1,
char* szSource2,
int nLength)
{
strcpy( szTarget , szSource1 ) ;
strcat( szTarget , " - " ) ;
strcat( szTarget , szSource2 ) ;
}
_________________
135 стр. Глава 10. Отладка программ на С++
Соберите и запустите программу. Вместо объединения двух строк программа может вернуть всё, что угодно. Нам надо обратиться к отладчику, чтобы разобраться, что же в этой программе не так.
Первое, что стоит сделать при поиске ошибки с помощью отладчика, — это выполнить программу в отладочном режиме. Попытка выполнить эту программу в отладочном режиме в Dev-C++ ( с помощью клавиши
«Подобное сообщение об ошибке обычно говорит о некорректной работе с указателями того или иного типа.»
[Советы]
Команда Остановить выполнение заставляет отладчик заново начать работу с программой ( а не с того места, где вы находитесь ). Никогда не вредно перезагрузить отладчик перед началом работы.
Для того, чтобы увидеть, где именно таится проблема, выполните только часть программы. Отладчик позволяет сделать это посредством так называемых точек останова ( breakpoints ). Отладчик всякий раз прекращает выполнение программы при прохождении через точку останова, и передаёт управление программисту.
Установим точку останова на первой выполнимой инструкции, щёлкнув слева от строки вывода в cout или воспользовавшись клавишами
Теперь продолжим выполнение программы под отладчиком, либо выбирая команду меню Debug => Debug ( Отладка => Отладка ), либо щелчком на соответствующей пиктограмме в панели отладки, либо при помощи клавиши
Теперь вы можете выбрать в меню команду Debug => Next Step ( Отладка => Следующий шаг ) либо нажать клавишу
Синяя подсветка перемещается к следующей выполнимой инструкции, пропуская два объявления переменных. ( Объявления не являются выполнимыми командами; они всего лишь выделяют память для объявляемых переменных. ) Такое выполнение одной инструкции С++ называется пошаговым выполнением программы. Вы можете переключиться в окно консоли программы и посмотреть, что именно вывела программа при выполнении этой инструкции ( рис. 10.3 ).
_________________
136 стр. Часть 2. Становимся функциональными программистами
Рис. 10.2. Точку останова легко опознать по маленькому красному кружку
Рис. 10.3. В любой момент вы можете переключиться на окно выполняемой программы
Выполнение двух последующих инструкций приводит нас к вызову функции StringEmUp( ).
Если опять выбрать команду Debug => Next Step ( Отладка => Следующий шаг ), программа аварийно завершится. Теперь мы знаем, что проблема кроется в этой функции.
«Если сбой происходит при вызове некоторой функции, то либо ошибка содержится в коде функции, либо ей передаются некорректные аргументы.»
[Советы]
_________________
137 стр. Глава 10. Отладка программ на С++
Команда Debugs => Next Step ( Отладка => Следующий шаг ) рассматривает вызов функции как единую команду. Однако на самом деле функция состоит из ряда отдельных инструкций С++, и для отладки нам надо пройти их пошагово. Такая функциональность обеспечивается командой Debug => Step Into ( Отладка => Шаг внутрь ).
Перегрузите программу при помощи пункта меню Debug => Program Reset ( Отладка => Остановить выполнение ) либо соответствующей пиктограммы на панели отладки или клавиш
Рис. 10.4. Установка точки останова на вызове функции stringEmUp( )
«Вы можете установить в программе столько точек останова, сколько вам требуется.»
[Советы]
Теперь перезапустите программу на выполнение, и она остановится на вызове функции StringEmUp( ).
Войдите в функцию, воспользовавшись командой Debug => Step into ( Отладка => Шаг внутрь ), как показано на рис. 10.5.
Допустим, вы обнаружили, что ваша программа время от времени работает некорректно. Чтобы лучше понять, почему это происходит, желательно знать, какие аргументы передаются в рассматриваемую функцию. Для этого нужна функция наблюдения за переменными, предоставляемая отладчиком, которая позволяет ознакомиться с содержимым всех переменных при каждом останове выполнения. Проще всего установить наблюдение за переменной, выбрав её в окне редактора и нажав клавишу
_________________
138 стр. Часть 2. Становимся функциональными программистами
Рис. 10.5. Команда Debug => Step Into ( Отладка => Шаг внутрь ) позволяет выполнить вызов функции пошагово
Рис. 10.6. Отладчик позволяет следить за значениями переменных
_________________
139 стр. Глава 10. Отладка программ на С++
Числа возле имён переменных в окне отладки — адреса, которые в данном случае малоинформативны. Строка szTarget пока что пуста, что вполне закономерно, так как мы ещё ничего в неё не скопировали. Значение строки szString1 также выглядит вполне корректно, но вот значение szString2 содержит сразу две строки — и "This is a string", и "THIS IS A STRING", чего вроде бы быть не должно.
Ответ находится в четвёртой переменной. Дело в том, что длина этих двух строк не 16 символов, а 17! Программа не выделила память для завершающего нуля, что и приводит к сбою при выполнении функции StringEmUp( ).
«Длина строки всегда включает завершающий нулевой символ.»
[Помни!]
Давайте изменим программу, исправив ошибку. Пусть теперь С++ сам рассчитывает размер строк. Получившаяся в результате программа Concatenate2.срр, приведённая ниже, работает вполне корректно.
/* Concatenate — конкатенация двух строк со вставкой " - " между ними. */
#include
#include
#include
#include
using namespace std ;
void StringEmUp( char* szTarget ,
char* szSource1 ,
char* szSource2 ) ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
cout << "Конкатенация двух строк со вставкой \" - \"\n"
<< "( В этой версии нет ошибки. )" << endl ;
char szStrBuffer[ 256 ] ;
/* Определение двух строк... */
char szString1[ ] = "This is a string" ;
char szString2[ ] = "THIS IS A STRING" ;
/* ...и объединение их в одну */
StringEmUp( szStrBuffer ,
szString1 ,
szString2 ) ;
/* Вывод результата */
cout << "<" << szStrBuffer << ">" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
140 стр. Часть 2. Становимся функциональными программистами
void StringEmUp( char* szTarget ,
char* szSource1 ,
char* szSource2 )
{
strcpy( szTarget , szSource1 ) ;
strcat( szTarget , " - " ) ;
strcat( szTarget , szSource2 ) ;
}
Вот вывод этой программы — именно такой, какой мы и ожидали:
Конкатенация двух строк со вставкой " - "
( В этой версии нет ошибки. )
Press any key to continue...
! ! ! ! ! ! ! ! ! ! ! ! ! !
Поздравляю! Вам удалось отладить программу.
! ! ! ! ! ! ! ! ! ! ! ! ! !
_________________
141 стр. Глава 10. Отладка программ на С++