1 Функции языка C++. 2 Язык C++ поставляется с обширными библиотеками полезных функций, однако истинное удовольствие от программирования, можно получить.

Презентация:



Advertisements
Похожие презентации
Практическое занятие 6. Функции. Большинство языков программирования используют понятия функции и процедуры. C++ формально не поддерживает понятие процедуры,
Advertisements

Работа с файлами Сазонов Д.О. ПМиЭММ Часть 2. Тема занятия: Работа с файлами через потоки Для реализации файлового ввода/вывода, необходимо включить в.
Основы информатики Лекция. Функции Заикин Олег Сергеевич
Объектно-ориентированный язык программирования. Переменная - эта поименованная ячейка памяти, хранящая какое-либо одно значение (одно число, один фрагмент.
Основы информатики Массивы. Указатели. Заикин Олег Сергеевич
Основы информатики Лекция. Массивы. Указатели. Заикин Олег Сергеевич
Массивы 9 класс. Основные теоретические сведения Примеры решения задач.
Циклы в C++. Иногда необходимо повторять одно и то же действие несколько раз подряд. Для этого используют циклы. В этом уроке мы научимся программировать.
Сортировка методом пузырька, выбором (Pascal) Кокарева Светлана Ивановна.
Функции Функция – именованная последовательность описаний и операторов, выполняющая некоторое действие. Может иметь параметры и возвращать значение. Функция.
План-конспект урока (информатика и икт, 9 класс) по теме: Переменные:тип, имя, значение
Функции Лекция 8. Назначение функций Функции - самостоятельные программные единицы, спроектированные для решения конкретной задачи. Функции по структуре.
Языки и методы программирования Преподаватель – доцент каф. ИТиМПИ Кузнецова Е.М. Лекция 5.
Одномерный массив. Цель урока: познакомить учащихся с понятием одномерный массив Задачи: дать определение массива дать представление: об описании массива.
Теперь, когда вы постигли азы программирования, будем учиться писать программы, которые позволяют вести диалог между компьютером и человеком (пользователем).
Лекция 4 Программирование на Паскале. Элементы языка Турбо Паскаль 7.0. Типы данных. Управляющие конструкции.
Переменные и операторы УРОК 2. Переменные ПЕРЕМЕННАЯ – ?... контейнер для хранения данных. Переменная имеет имя – это….? последовательность букв, цифр.
Министерство образования Республики Беларусь Белорусский государственный университет Управляющие структуры языков программирования.
Тема урока Переменная. Тип данных. Ввод и вывод данных.
Транксрипт:

1 Функции языка C++

2 Язык C++ поставляется с обширными библиотеками полезных функций, однако истинное удовольствие от программирования, можно получить только при создании собственных функций. Вы узнаете, как определять функции, как передавать им и получать от них информацию, как следует использовать функции при работе с массивами, строками и структурами. И, наконец, в этой лекции состоится первое знакомство с рекурсией и с указателями на функции.

3 Обзор функций Прежде всего, подведем итоги что мы уже знаем о функциях. Чтобы воспользоваться функцией в языке C++, необходимо выполнить следующие действия: o предусмотреть определение функции o предусмотреть прототип(объявление) функции o вызвать функцию.

4 Обзор функций Если вы используете функцию из библиотеки, значит эта функция уже определена и скомпилирована. Кроме того, можно воспользоваться заголовочным файлом стандартной библиотеки, содержащим прототип. Теперь остается только корректно вызвать функцию. Например, в стандартной библиотеке языка С имеется функция strlen(), предназначенная для определения длины строки. Ассоциированный с ней стандартный заголовочный файл cstring содержит прототип функции strlen(), а также несколько других функций, предназначенных для обработки строк. Благодаря этой предварительно проделанной работе программисты могут пользоваться в своих программах функцией strlen(), не испытывая затруднений.

5 Обзор функций // calling.стр -- определение, создание прототипов функции и ее вызов #include using namespace std; void simple(); // прототип функции int main() { cout « main() will call the simple() function:\n"; simple ();// вызов функции return 0 ; } // определение функции void simple() { cout « "I'm but a simple function. \n" ; } Результаты выполнения программы: main() will call the simple() function: I'm but a simple function.

6 Определение функции Функции можно разбить на две следующих категории: функции, которые не возвращают значений, и функции, возвращающие значения. Функции без возвращаемых значений называются функциями типа void, они имеют следующую общую форму: void имя Функции(список Аргументов) { оператор(ы) return;// необязательный оператор } В списке список Аргументов указываются типы и количество аргументов (параметров), передаваемых функции. Необязательный оператор return обозначает конец функции. При его отсутствии признаком завершения функции служит закрывающая фигурная скобка.

7 Определение функции Обычно функция void используется для того, чтобы выполнить какое-то действие. Например, функция, в задачу которой входит напечатать приветствие Cheers! заданное количество (п) раз, имеет следующий вид: void cheers(int n) // возвращаемое значение отсутствует { for (int i = 0; i < n; i++) cout « "Cheers! "; cout « "\n" ; ) Список аргументов int n означает, что при обращении к функции cheers( ) ей необходимо передать в качестве аргумента значение типа int.

8 Определение функции Функция с возвращаемым значением вычисляет значение, которое она возвращает вызывающей ее функции. Другими словами, если функция возвращает квадратный корень из числа 9,0 (sqrt(9.0)), то результатом ее выполнения будет число 3,0. При объявлении для такой функции указывается тот же тип, что и для возвращаемого ею значения. Общая форма подобной функции имеет следующий вид: имя Типa имя функции(список Аргументов) { операторы return значение;// тип возвращаемого значения имя Типа }

9 Определение функции В функциях с возвращаемыми значениями должен использоваться оператор return, который обеспечивает возврат полученного значения вызывающей функции. Само значение может быть константой, переменной или выражением более общего вида. К нему предъявляется единственное требование чтобы это выражение приводилось к значению, которое имеет тип имя Типа или могло быть преобразовано к этому типу (если объявленным типом возвращаемого значения является, скажем, double, а функция возвращает выражение типа int, то значение типа int преобразуется к типу double). Завершив свою работу, функция возвращает окончательное значение той функции, которая ее вызвала. В языке C++ на типы, которые могут быть использованы для возвращаемого значения, накладывается ограничение: возвращаемым значением не может быть массив. Все остальное целые числа, числа с плавающей точкой, даже структуры и объекты годится для применения! (Интересно отметить, что, хотя функция в C++ не может возвращать массив непосредственно, она может возвратить его как часть структуры или объекта.)

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

11 Определение функции Выполнение функции завершается после выполнения оператора возврата (return). Если функция включает более одного оператора возврата, например, в качестве альтернатив выбора в операторе if else, выполнение функции прекращается сразу после того, как она выполнит первый встреченный оператор возврата. int bigger(int a, int b) { if (a > b ) return a; // если а > b, выполнение функции завершается здесь else return b; // иначе выполнение функции завершается здесь } В рассматриваемом случае оператор else необязателен, однако он помогает понять суть дела.

12 Создание прототипов и вызов функций // protos.cpp -- использование прототипов и вызовов функций # include using namespace std; void cheers(int);// прототип: возвращаемое значение отсутствует double cube(double x) ;// прототип: возвращает значение типа double int main (void) { cheers (5);// вызов функции cout « "Give me a number: "; double side; cin » side; double volume = cube(side);// вызов функции cout « "A " « side «"-foot cube has a volume of "; cout « volume « " cubic feet.\n"; cheers (cube (2) ) ;// защита прототипа в действии return 0 ; } void cheers (int n) { for (int i = 0; i < n; i++) cout « "Cheers! " ; cout « "\n"; } double cube (double x) { return x * x * x; }

13 Создание прототипов и вызов функций Результаты выполнения программы: Cheers! Cheers! Cheers! Cheers! Cheers! Give me a number: 5 A 5-foot cube has a volume of 125 cubic feet. Cheers! Cheers! Cheers! Cheers! Обратите внимание на то, что main о обращается к функции cheers () типа void, используя для этой цели имя функции и аргументы, за которыми следует точка с запятой: cheers(5);. Это пример оператора обращения (или вызова) к функции. В то же время, поскольку функция cube() имеет возвращаемое значение, она может использоваться в main() в операторе присваивания: double volume = cube(side);

14 Создание прототипов и вызов функций Для чего нужны прототипы? Прототип описывает компилятору интерфейс функции. Другими словами, он сообщает компилятору тип возвращаемого значения функции, если таковое имеется, а также количество и типы аргументов. Рассмотрим, например, какое влияние оказывает прототип на вызов функции в программе из листинга 7.2: double volume = cube(side); Во-первых, прототип сообщает компилятору, что функция cube() должна иметь один аргумент типа double. Если программа не обеспечит передачу такого аргумента, благодаря прототипу компилятор зафиксирует соответствующую ошибку. Во-вторых, когда функция cube() завершит вычисления, она поместит возвращаемое значение в специально отведенное для этого место (в какой- либо регистр центрального процессора или в ячейку оперативной памяти). Впоследствии вызывающая функция (в рассматриваемом случае это main()) найдет данное значение именно в этом, известном ей месте. Поскольку согласно прототипу cube() имеет тип double, компилятор знает, сколько байтов нужно выбрать и как их интерпретировать.

15 Создание прототипов и вызов функций Синтаксис прототипа Прототип функции является оператором, поэтому он должен завершаться точкой с запятой. Простейший способ создать прототип скопировать из определения функции ее заголовок и добавить точку с запятой. Именно это сделано в программе для функции cube (): double cube(double x) ; // добавить точку с запятой, // чтобы получить прототип В прототипе функции не требуется указывать имена переменных; вполне достаточно списка типов. При создании прототипа функции cheers () в программе используется только тип аргумента: void cheers(int); //в прототипе можно опускать имена переменных В общем случае можно как включать имена переменных в список аргументов для прототипа, так и исключать их из этого списка. Имена переменных в прототипе проявляют себя как заполнители, они не обязательно должны совпадать с именами, содержащимися в определении функции.

16 Аргументы функции и передача по значению Пришло время для основательного изучения аргументов функции. Обычно в языке C++ аргументы передаются по значению, другими словами, числовое значение аргумента передается функции, где оно присваивается новой переменной. Например, в программе из листинга 7.2, продемонстрировано обращение к функции: double volume = cube(side); Здесь side это переменная, которая в процессе выполнения программы получает значение 5. Напомним, что заголовок функции cube () был таким: double cube(double x) Эта функция при обращении к ней создает новую переменную типа double с именем х и присваивает ей значение 5. Благодаря такому приему данные в функции main() изолируются от действий, которые происходят в функции cube(), так как cube() использует в своей работе всего лишь копию переменной side, а не саму эту переменную.

17 Аргументы функции и передача по значению Переменная, которая применяется для приема передаваемого значения, называется формальным аргументом или формальным параметром. Величина, передаваемая функции, называется фактическим аргументом или фактическим параметром. Чтобы не возникало путаницы, стандарт C++ предусматривает использование слова аргумент по отношению к фактическому аргументу или параметру, а термина параметр по отношению к формальному аргументу или параметру. Согласно этой терминологии, передача аргумента означает присвоение аргумента параметру. Переменные, в том числе параметры, объявленные внутри какой-либо функции, являются, если можно так сказать, ее собственностью. Во время вызова функции компьютер выделяет память, необходимую для этих переменных. По завершении выполнения функции память, которая использовалась переменными, высвобождается (в некоторых книгах по C++ выделение памяти переменным и ее высвобождение называется более драматично созданием и уничтожением переменных). Такие переменные называются локальными, поскольку сфера их действия не выходит за пределы функции. Как уже отмечалось, таким образом обеспечивается целостность данных.

18 Функции с несколькими аргументами Функция может иметь не один, а несколько аргументов. В обращении к такой функции аргументы отделяются друг от друга запятыми: n_chars('R', 25) ; В результате вызова функции n__chars (), определение которой будет приведено далее, ей передаются два аргумента. Аналогично при определении функции в ее заголовке указывается список объявляемых параметров с разделением запятыми: void n_chars(char с, int n) // два аргумента Этот заголовок функции свидетельствует о том, что функция n_chars () принимает один аргумент типа char и один аргумент типа int. Параметрам с и n присваиваются значения, передаваемые функции. Если функция принимает два параметра одного и того же типа, необходимо описать тип каждого параметра отдельно.

19 Функции с несколькими аргументами Нельзя объединять объявления аргументов так, как это допускается при объявлении обычных переменных: void fifi(float a, float b) // каждая переменная должна быть // объявлена отдельно void fufu(float a, b) // это НЕПРАВИЛЬНОЕ объявление Как и для других функций, чтобы получить прототип, следует добавить точку с запятой: void n__chars (char с, int n) ; // прототип, стиль 1 Как и в случае с одним аргументом, необязательно применять в прототипе те же имена переменных, которые были использованы в определении функции, их можно просто опустить: void n__chars (char, int);// прототип, стиль 2

20 Функции с несколькими аргументами // twoarg.cpp -- функция с двумя аргументами #include using namespace std; void n_chars (char, int) ; int main() { int times; char ch ; cout « "Enter a character: "; cin » ch; while (ch != 'q') // q для выхода из программы { cout « "Enter an integer: "; cin » times; n_chars(ch, _times); // функция с двумя аргументами cout « «\nEnter another character or press the" « " q-key to quit: "; cin » ch; } cout « "The value of times is " « times « ".\n"; cout « "Bye\n"; return 0 ; } void n_chars(char c, int n) // Отображает значение с п раз { while (n-- > 0)// продолжается, пока n не станет равным О cout « с; }

21 Функции с несколькими аргументами Ниже приводятся результаты выполнения этого примера: Enter a character: W Enter an integer: 50 WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW Enter another character or press the q-key to quit: a Enter an integer: 20 aaaaaaaaaaaaaaaaaaaa Enter another character or press the q-key to quit: q The value of times is 20. Bye

22 Функции с несколькими аргументами В функции main () цикл while используется для того, чтобы обеспечить повторяющийся ввод символов. Для считывания символов вместо конструкции cin.get (ch) или cin = cin.get () используется выражение cin » ch. Для этого есть веские причины. Пара функций cin.get () читает все введенные символы, включая пробелы и символы новой строки, в то время как конструкция cin » их пропускает. Когда пользователь в ответ на приглашение программы в конце каждой строки нажимает клавишу Enter, генерируется очередной символ новой строки. Конструкция cin » ch позволяет игнорировать такие символы, в то время как родственные функции cin.get () считывают символ новой строки, который следует за каждым вводящимся числом, как обычный подлежащий отображению символ. Функция n_chars() принимает два аргумента, которыми являются символ с и целое число n. Затем в ней используется цикл, чтобы выводить на экран заданный символ такое количество раз, которое задается целым числом. while (n-- > 0) // продолжается, пока n не станет равным 0 cout « с; Обратите внимание: программа ведет подсчет вводимых символов, уменьшая значение переменной n, где n формальный параметр из списка аргументов. Ему присваивается значение переменной times, которая определена в функции main(). Значение переменной n уменьшается в цикле while до нуля, однако, как показывает выполнение рассматриваемого примера, изменение величины n не оказывает никакого влияния на переменную times.

23 Функции и массивы До сих пор примеры рассматриваемых нами функций были простыми, в них в качестве аргументов возвращаемых величин использовались только основные типы данных. Однако функции могут играть ключевую роль и при использовании более сложных типов данных, таких как массивы и структуры. Ниже мы рассмотрим, как "уживаются" друг с другом массивы и функции. Предположим, что нам требуется знать, сколько булочек съел каждый участник пикника. Для этого мы используем массив, каждый индекс которого соответствует определенному участнику, а значение элемента массива количеству съеденных этим участником булочек. Теперь определим, сколько булочек было съедено всеми участниками. Это сделать легко: достаточно воспользоваться циклом, чтобы просуммировать все элементы массива. Но сложение элементов массива настолько часто встречающаяся операция, что имеет смысл разработать специальную функцию, способную выполнить эту процедуру в таком случае у вас не будет необходимости каждый раз приспосабливать цикл для суммирования элементов массивов.

24 Функции и массивы Поскольку функция вычисляет сумму, она должна возвращать ответ. Если нас интересуют только целые булочки, можно использовать функцию с возвращаемым значением типа int. Естественно, необходимо передать функции в качестве аргумента имя массива, подлежащего суммированию. А поскольку функция должна быть универсальной, не ограниченной массивом определенного размера, ей следует также передать размер массива. Все это мы уже умеем делать, новое здесь только одно объявление одного из формальных параметров как имени массива. Теперь выясним, как должны выглядеть эта, а также все остальные части заголовка функции: int sum_arr(int arr[], int n) // arr = имя массива, п = размер массива квадратные скобки указывают на то, что arr это массив, а тот факт, что скобки пусты - эту функцию можно использовать с массивом любых размеров. Однако не все так хорошо, как кажется: в действительности arr не массив, а указатель! Но не так все и плохо: остальную часть кода функции вы можете задавать так, как если бы переменная arr была массивом. Использование указателя в качестве имени массива проиллюстрировано в листинге. Программа инициализирует массив некоторыми значениями и использует функцию sum_arr() для вычисления суммы. Обратите внимание на то, что функция sum_arr() использует arr как имя массива.

25 Функции и массивы // arrfunl.cpp функции с аргументом типа array #include using namespace std; const int ArSize = 8; int sum__arr(int arr[], int n) ; // прототип int main() { int cookies[ArSize] = {1,2,4,8,16,32,64,128}; int sum = sum_arr(cookies, ArSize); cout « "Total cookies eaten: " « sum « "\n"; return 0 ; } // возвращает сумму элементов массива целых чисел int sum_arr(int arr[], int n) { int total = 0; for (int i = 0; i < ;i++) total = total + arr[i] ; return total; }

26 Применение указателей в функциях, обрабатывающих массивы в языке C++ в большинстве контекстов имя массива трактуется как указатель. Как упоминалось в главе 4, в C++ имя массива интерпретируется как адрес его первого элемента: cookies == &cookies[0] // именем массива является // адрес его первого элемента Программа из листинга 7.5 выполняет следующий вызов функции: int sum = sum_arr (cookies, ArSize) ; Здесь cookies имя массива, поэтому это адрес его первого элемента. Функция передает адрес. Поскольку элементы массива имеют тип int, массив cookies должен иметь тип указателъ-на-int, или тип int *. Отсюда следует, что корректный заголовок функции должен быть следующим: int sum_arr(int * arr, int n) // arr = имя массива, n = размер массива

27 Применение указателей в функциях, обрабатывающих массивы Здесь вместо выражения int arr[] использовано int *arr. Оба заголовка корректны, поскольку в C++ обозначения int *arr и int arr [] имеют одно и то же значение тогда и только тогда, когда они используются в заголовке или в прототипе функции. Обе записи означают, что arr представляет собой указатель-на-int. Обозначение массива вида (int arr []) в символической форме напоминает, что arr указывает не только на тип int, но и на первый элемент int в массиве элементов типа int. Мы используем обозначение массива, когда указатель ссылается на первый элемент массива, и обозначение указателя, когда указатель ссылается на изолированное значение. Не следует забывать, что во всех других контекстах обозначения int *arr и int arr[] не идентичны.

28 Применение указателей в функциях, обрабатывающих массивы При условии, что переменная arr фактически является указателем, имеет смысл и вся остальная часть функции. Как уже говорилось в главе 4, где рассматривались динамические массивы, для получения доступа к элементам массива можно использовать обозначение "массив со скобками" как с именем массива, так и с указателем. Независимо от того, является ли arr указателем или именем массива, выражение arr [3] обозначает четвертый элемент массива. И, по-видимому, будет полезно напомнить сейчас о том, что имеют место следующие два тождества: arr[i] == * (arr + i) // два обозначения одной и той же величины &arr[i] == arr + i // два обозначения одного и того же адреса

29 Особенности использования массивов в качестве аргументов При вызове функции sum_arr(cookies, ArSize) ей передается адрес первого элемента массива cookies и количество элементов этого массива. В функции sum_arr () адрес массива cookies присваивается переменной-указателю агг, а значение ArSize переменной п типа int. Это значит, что программа из листинга 7.5 на самом деле не передает указанной функции содержимое массива, а только сообщает ей, где находится массив (т.е. его адрес), что представляют собой его элементы (т.е. их тип) и сколько таких элементов содержит массив (переменная n). Обладая этой информацией, функция далее работает с исходным массивом.

30 Особенности использования массивов в качестве аргументов Если передать функции обычную переменную, то она будет работать с ее копией, но если передается массив, то она будет работать с его оригиналом. Однако, по сути дела, такое различие не нарушает принципа "передачи по значению", принятый в языке C++. Функция sum_arr() фактически передает значение, которое затем присваивается новой переменной. Разница лишь в том, что это значение является одиночным адресом, а не содержимым массива.

31 Особенности использования массивов в качестве аргументов Приносит ли соответствие между именами массивов и указателями какую-либо пользу? Приносит, и немалую. Использование адресов массивов в качестве аргументов позволяет экономить время и память, необходимые для копирования всего массива: ведь дополнительные расходы ресурсов для копирования массивов большого размера могут оказаться недопустимо высокими. Программа потребует не только дополнительных объемов оперативной памяти, ей придется тратить время копирование крупных блоков данных. С другой стороны, работа с исходными данными повышает вероятность непреднамеренного искажения данных.

32 Функции, в которых используются диапазоны массивов Как уже говорилось, для функций C++, обрабатывающих массивы, необходима информация о типе данных массива, адресе начала массива и количестве элементов в нем. Традиционный для языков С и C++ подход заключается в передаче указателя на начало массива (первый аргумент) и размера массива (второй аргумент) (указатель сообщает функции местонахождение массива и тип его данных). В результате функция располагает необходимыми сведениями для нахождения всех данных. Существует еще один метод предоставления функции необходимой информации -указание диапазона элементов. Это осуществляется путем передачи двух указателей: один идентифицирует начало массива, а второй конец. В стандартной библиотеке шаблонов C++ (представленной в главе 16), например, в основном реализовано использование диапазонов.

33 Функции, в которых используются диапазоны массивов В этой библиотеке применяется концепция указания границ диапазона "ячейка, следующая за последним элементом". Другими словами, в случае массива аргумент, указывающий его конец, представляет собой указатель на ячейку памяти, следующую за последним элементом. Для примера рассмотрим следующее объявление: double elbuod[20] ; В данном случае два указателя elboud и elboud + 20 определяют диапазон. Во-первых, слово elboud, как имя массива, указывает на его первый элемент. Выражение elboud + 19 указывает на последний элемент (т.е. elboud[19]), поэтому выражение elboud[20] указывает на ячейку, следующую за концом массива. Аргументы, обозначающие диапазон, указывают функции, какие элементы следует обрабатывать. Листинг 7.8 содержит вариант программы, в котором для задания диапазона используются два указателя.

34 Функции, в которых используются диапазоны массивов // arrfun4. стр функции, использующие диапазоны массивов #include using namespace std; const int ArSize = 8; int sum__arr( const int * begin, const int * end) ; int main() { int cookies[Ar8ize] ={1,2,4,8,16,32,64,128}; int sum = sum_arr(cookies, cookies + ArSize); cout « "Total cookies eaten: " « sum « "\n"; sum « sum_arr(cookies, cookies +3);// три первых элемента cout « "First three eaters ate " « sum « " cookies.\n"; sum = sum_acr(cookies + 4, cookies +8); // четыре последних элемента cout « "Last four eaters ate " « sum « " cookies.\n"; return 0 ; } // возврат суммы целочисленных элементов массива int sum__arr (const int * begin, const int * end) { const int * pt; int total = 0: for (pt = begin; pt != end; pt++) total = total + *pt; return total; }

35 Функции, в которых используются диапазоны массивов Вывод программы: Total cookies eaten: 255 First three eaters ate 7 cookies. Last four eaters ate 240 cookies. Примечания к программе Для начала обратите внимание на цикл for в массиве sum_array (): for (pt = begin; pt != end; pt++) total = total + *pt; Он присваивает переменной pt значение, указывающее на первый элемент, подлежащий обработке (begin) и прибавляет величину *pt (значение элемента) к переменной total. Затем цикл обновляет значение pt так, чтобы оно указывало на следующий элемент. Процесс продолжается, пока выражение pt !« end истинно. Когда переменная pt в итоге достигнет значения end, она будет указывать на ячейку, следующую за последним элементом массива, поэтому цикл завершится.

36 Функции, в которых используются диапазоны массивов Далее рассмотрим, как в отдельных вызовах функции указываются различные диапазоны массива: int sum = sum_arr(cookies, cookies + ArSize); int sum = sum_arr (cookies, cookies + 3); // три первых элемента int sum = sum_arr(cookies + 4, cookies + 8); // четыре последних элемента Значение cookies + ArSize указывает на ячейку, следующую за последним элементом (массив содержит ArSize элементов, поэтому выражение cookies [ArSize - 1] обозначает последний элемент с адресом cookies + ArSize - l). Итак, диапазон cookies, cookies + ArSize охватывает весь массив. Аналогично, выражение cookies, cookies + 3 обозначает три первых элемента и т.д. Обратите внимание на то, что согласно правилам вычитания указателей, выражение end - begin в функции sum_arr() представляет собой целочисленное значение, равное количеству элементов диапазона.

37 Указатели и спецификатор const При работе с указателями ключевое слово const можно применять двумя различными способами. Первый способ предусматривает ссылку указателя на постоянный объект, что предотвращает возможность использования указателя для изменения величины, на которую он указывает. Второй способ состоит в том, чтобы сам указатель сделать константой, и таким образом воспрепятствовать попыткам вносить изменения там, куда указывает указатель. Сначала объявим указатель pt, который указывает на константу: int age =39; const int * pt = &age; pt указывает на данные типа const int (в рассматриваемом случае 39), и, следовательно, невозможно использовать pt с целью изменения этого значения. Другими словами, значением *pt является константа (const), изменение которой не допускается: *pt += 1; // НЕПРАВИЛЬНО, поскольку pt указывает на тип const int cin » *pt; // НЕПРАВИЛЬНО по той же причине При таком объявлении указателя pt величина, на которую он указывает, не обязательно является константой, это константа лишь "с точки зрения" указателя pt. Например, pt указывает на значение age, однако тип age -не const. Значение age можно изменить непосредственно, используя для этой цели переменную age. Однако косвенное изменение посредством указателя pt невозможно: *pt = 20; // НЕПРАВИЛЬНО, поскольку pt указывает на const int age = 20; // ПРАВИЛЬНО, поскольку age не объявлена как const

38 Указатели и спецификатор const в C++ запрещено присваивать адрес величины со статусом const указателю, не имеющему статуса const. const float g_earth = 9.80; const float * ре = fig_earth; // ПРАВИЛЬНО const float g_moon = 1.63; float * pm = &g_moon;// НЕПРАВИЛЬНО В первом случае вы не можете воспользоваться ни константой g_earth, ни константой ре, чтобы изменить значение 9,80. В языке C++ второй вариант не допускается по одной простой причине присвоив pm адрес g_moon, уже нетрудно обмануть систему и использовать pm для изменения значения g_moon. Таким образом, спецификатор const величины g_moon превращается в фикцию.

39 Указатели и спецификатор const В случае использования указателей на указатели ситуация несколько усложняется. Присвоение указателю со спецификатором const адреса указателя без спецификатора const допустимо, если используется лишь один уровень косвенной адресации: int age = 39;// age++ допустимая операция int *pd = &age;// *pd = 41 допустимая операция const int *pt = pd; // *pt = 42 недопустимая операция Однако когда используется два уровня косвенной адресации, смешанное применение указателей-констант и указателей, не указывающих на константы, небезопасно для данных. Если бы подобные операции в среде C++ допускались, были бы возможны следующие действия: const int **pp2; int *pl; const int n = 13; pp2 = &p1; // операция недопустима, но предположим, что допустима *рр 2 = &n; // допустимо, оба значения константы, но p1 теперь указывает на n *pl =10; // допустимо, но изменяет значение константы n Здесь адрес, не являющийся константой (&p1), присваивается указателю-константе (рр 2), что позволяет использовать указатель p1 для изменения значения константы. Поэтому правило, допускающее присвоение адреса без спецификатора const указателю со спецификатором const, действует только при условии одного уровня косвенной адресации. Примером может служить применение указателя, который ссылается на базовый тип данных.

40 Указатели и спецификатор const Указателю-на-const можно присвоить адрес константы либо значение, не имеющее спецификатора const, если оно само по себе не является указателем. Однако указателю, не указывающему на const, можно присвоить только адрес данных, не имеющих спецификатора const. Предположим, что имеется массив данных со спецификатором const: const int months[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; Запрет на присвоение адреса массива величин со спецификатором const означает, что нельзя передать имя массива в качестве аргумента функции, использующей формальный аргумент, не являющийся константой: int sum(int arr[], int n) ; // правильное выражение: // const int arr[]... int j = sum(months, 12); // не допускается При вызове функции предпринимается попытка присвоить указателю (агг), не имеющему спецификатора const, указатель (months) со спецификатором const. Компилятор запрещает такой вызов функции.

41 Указатели и спецификатор const Еще одна тонкость связана с тем, что следующие объявления int age =39; const int * pt = &age; только предотвращают изменение значения, равного 39, на которое ссылается указатель pt. Изменению самого указателя pt ничего не препятствует. Иначе говоря, указа­телю pt можно присвоить новый адрес: int sage = 80; pt = fisage; // допускается ссылка на другую область памяти Однако указатель pt по-прежнему нельзя использовать для изменения значения, на которое он указывает (теперь оно составляет 80).

42 Указатели и спецификатор const Второй способ использования спецификатора const делает невозможным изменение значения самого указателя: int sloth = 3; const int * ps = &sloth; // указатель на тип const int int * const finger = &sloth; // неизменный указатель на тип int В последнем объявлении ключевое слово const стоит в другом месте. Такая форма объявления приводит к тому, что указатель finger указывает только на sloth. Однако указателем finger можно воспользоваться, чтобы изменить значение sloth. Второе объявление не дает возможности использовать указатель ps для изменения значения переменной sloth, однако оно позволяет сделать так, чтобы ps указывал на другую ячейку памяти. Итак, и finger, и *ps являются константами, a *finger и ps нет.

43 Указатели и спецификатор const Форма указателя-на-const часто используется для защиты данных в тех случаях, когда функции в качестве аргументов передаются указатели. Пример - прототип функции show_array() : void show__array (const double ar[], int n) ; Использование спецификатора const в этом описании означает, что функция show_array () не может изменять значения элементов ни в одном из передаваемых ей массивов. Этот прием действует при условии, что используется только один уровень непрямой адресации. В случает создания указателей на указатели, которые в свою очередь ссылаются на указатели, спецификатор const не используется.

44 Функции и двумерные массивы Чтобы создать функцию, принимающую в качестве аргумента двумерный массив, необходимо учитывать, что имя массива обрабатывается как его адрес. Поэтому соответствующий формальный параметр является указателем, как и в случае одномерных массивов. Секрет успеха кроется в правильном объявлении указателя. Предположим, что исходный фрагмент кода имеет следующий вид: int data[3] [t4] = {{1,2,3,4}, {9,8,7,6}, {2,4,6,8}}; int total = sum(data, 3); Как должен выглядеть прототип функции sum() ? Почему функции передается только количество строк (3), но не количество столбцов (4)? Здесь data представляет собой имя массива, содержащего три элемента. Первый элемент сам по себе является массивом четырех значений типа int. Таким образом, data является указателем на массив четырех значений типа int, поэтому для функции приемлем следующий прототип: int sum(int (*ar2)[4], int size); В этой записи скобки необходимы, поскольку объявление int *аг 2[4] относилось бы к массиву четырех указателей на данные типа int, а параметр функции не может быть массивом.

45 Функции и двумерные массивы Существует альтернативный формат записи прототипа, который имеет тот же смысл, но, возможно, проще воспринимается: int sum(int ar2[][4], int size); Оба варианта означают, что аг 2, представляет собой указатель, а не массив. Кроме того, отметим, что обозначение типа указателя сообщает, что он ссылается на массив четырех значений типа int. Таким образом, тип указателя определяет количество столбцов. Поэтому эта информация не передается в виде отдельного аргумента.

46 Функции и двумерные массивы Поскольку тип указателя информирует о количестве столбцов, функция sum() может обрабатывать только массивы с четырьмя столбцами. Однако количество строк задается переменной size, поэтому функция может обрабатывать различные количества строк. int а[100][4]; int b[6][4]; … int totall = sum (a, 100);// сумма всех элементов массива а int total2 = sum(b, 6);// сумма всех элементов массива b int total3 = sum(a, 10);// сумма первых десяти строк массива а int total4 = sum(a+10, 20);// сумма следующих двадцати строк массива а

47 Функции и двумерные массивы Если параметр аг 2 является указателем на массив, то как применять его в описании функции? Проще всего использовать его так, как будто он является именем двумерного массива. Ниже приводится один из возможных вариантов описания функции: int sum(int ar2[][4], int size) { int total = 0; for (int row = 0; row < size; row++) for (col = 0; col < 4; col++) total += ar2[row] [col] ; return total; }

48 Функции и двумерные массивы Обозначение двумерного массива в описании функции можно использовать по следующей причине. Поскольку аг 2 указывает на первый элемент (с индексом 0) массива, элементы которого представляют массивы четырех значений типа int, выражение аг 2 + row указывает на элемент с номером row. Поэтому ar2[row] элемент с номером row. Этот элемент сам по себе является массивом четырех значений типа int, следовательно, ar2 [row] имя массива четырех элементов типа int. Применение индекса к имени массива позволяет обратиться к элементу массива. Итак, ar2 [row] [col] элемент массива четырех значений типа int, а значит единичное значение типа int. Для получения данных следует дважды применить операцию разыменования к указателю аг 2. Проще всего использовать для этого выражение с двумя парами квадратных скобок. Отметим, что в описании функции sum() при объявлении параметра аг 2 не применяется спецификатор const. Дело в том, что он используется для указателей на базовые типы данных, а аг 2 является указателем на указатель.

49 Функции и строки в стиле языка С строка в стиле С состоит из последовательности символов, завершаемой пустым символом. Многое из того, что читатель узнал в процессе разработки функций, выполняющих обработку массивов, применимо и к функциям, выполняющим обработку строк. Однако при этом имеются несколько особенностей, обусловленных природой строк, к рассмотрению которых мы сейчас переходим. Предположим, необходимо передать функции в качестве аргумента строку. В нашем распоряжении имеется три варианта представления строк: Массив типа char Строковая константа, заключенная в кавычки (называется также строковым литералом) Указатель на значение типа char (сокращенно указатель-на-char), которому присвоен адрес начала строки

50 Функции и строки в стиле языка С Во всех трех вариантах используется тип указатель-на-char (или короче тип char *), так что все три можно использовать как аргументы функций, выполняющих обработку строк: char ghost[15] = "galloping"; char * str = "galumphing"; int nl = strlen(ghost); //ghostто же, что &ghost[0] int n2 = strlen(str);// указатель на символ int n3 = strlen("gamboling"); // адрес строки Проще говоря, в качестве аргумента передается строка, но на самом деле передается адрес первого символа строки. Отсюда следует, что в прототипе строковой функции в качестве типа формального параметра, представляющего строку, должен использоваться тип char*.

51 Функции и строки в стиле языка С Существенное различие между строкой и обычным массивом заключается в том, что в строке имеется встроенный символ завершения (напомним, что массив типа char, который содержит символы, среди которых нет null-символа, представляет собой массив, но не строку). Отсюда следует, что длину строки как аргумент передавать необязательно. Вместо этого в функции можно применить цикл, чтобы поочередно проанализировать каждый символ строки, пока цикл не достигнет завершающего null-символа. В листинге 7.9 демонстрируется эффективность такого подхода на примере функции, которая подсчитывает, сколько раз определенный символ появляется в заданной строке.

52 Функции и строки в стиле языка С // strgfun.cpp функция со строковым аргументом # include using namespace std; int c_in_str(const char * str, char ch) ; int main() { char romm[15] = "minimum";// строка в массиве char *wail = "ululate"; // wail указывает на строку int ms = c_in_str(xnmm, 'm'); int us = c_in_str(wail, ' u'); cout « ms « " m characters in " « mmm « "\n"; с out « us « " u characters in " « wail « "\n" ; return 0 ; } // эта функция подсчитывает число символов ch в строке str int c_in__str(const char * str, char ch) { int count = 0; while (*str) // выйти из программы, когда *str будет равен '\0' { if (*str == ch) count++; str++;// переместить указатель на следующий символ char } return count; }

53 Функции и строки в стиле языка С Результаты выполнения программы: 3 m characters in minimum 2 u characters in ululate Примечания к программе Поскольку функция c_int_str () не должна вносить изменений в исходную строку, в ней при объявлении формального параметра str используется спецификатор const. Если по ошибке функция попытается изменить какую-то часть строки, компилятор выявит ошибку. И, естественно, вместо объявления str в заголовке функции можно использовать обозначение массива: int c_in_str(const char str[], char ch) // также допустимо Однако использование обозначения указателя напоминает о том, что аргумент не обязательно должен быть именем массива, он может представлять собой другую разновидность указателя. В данной функции демонстрируется стандартный метод обработки символов в строке: while (*str) { операторы str++; }

54 Функции и строки в стиле языка С Первоначально str указывает на первый символ строки, так что выражение *str представляет собой первый символ. Например, сразу после первого обращения к функции *str имеет значение m первого символа в строке minimum. До тех пор пока таким символом не является null-символ (\0), выражение *str имеет ненулевое значение, следовательно, цикл продолжает выполняться. В конце каждого цикла выражение str++ увеличивает значение указателя на один байт, так, чтобы он указывал на следующий символ в строке. В конце концов, str укажет на завершающий строку null-символ, а выражение *str получит значение 0, которое является числовым кодом null-символа. В этом случае цикл прекращается.

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

56 Функции, возвращающие строки // strgback.cpp -- функция возвращает указатель на символ # include using namespace std; char * buildstr (char c, int n) ;// прототип int main() { int times; char ch ; cout « "Enter a character: "; cin » ch; cout « "Enter an integer: "; cin » times; char *ps = buildstr (ch, times); cout « ps « "\n" ; delete [] ps;// высвобождение памяти ps = buildstr('+', 20); // повторное использование указателя cout « ps « "-DONE-" « ps « "\n"; delete [] ps;// высвобождение памяти return 0 ; }

57 Функции, возвращающие строки // создает строку, состоящую из n символов с char * buildstr(char с, int n) { char * pstr = new char[n + 1]; pstr[n] = '\0';// завершить строку while (n--,> 0) pstr[n] = с;// заполнить остальную часть строки return pstr; } Результаты выполнения программы: Enter a character: V Enter an integer: 46 VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV DONE

58 Функции, возвращающие строки Примечания к программе Чтобы создать строку из n видимых символов, необходима область памяти для хранения п + 1 символов, поскольку нужно место для пустого символа. Таким образом, функция требует n + l байтов для хранения такой строки. После этого она отводит последний байт для null-символа и заполняет остальную часть массива в обратном направлении. Цикл while (n--, > 0) pstr[n] = с; повторяется n раз, пока значение n не уменьшится до нуля, при этом заполняются все n элементов массива. В начале заключительной итерации n имеет значение 1. Поскольку выражение п-- означает "использовать значение и уменьшить его на единицу", выражение проверки условия цикла while сравнивает значения 1 и 0, возвращает значение true, после чего цикл продолжает выполняться. Но по завершении проверки функция уменьшает значение n на 1, так что n становится равным 0, поэтому pstr[0] последний элемент, которому присваивается значение с.

59 Функции и структуры Функции для работы со структурами создавать легче, чем для работы с массивами. Хотя переменные-структуры и подобны массивам, структуры ведут себя по отношению к функциям как переменные базовых типов данных, принимающие только одно значение. В отличие от массива, структура объединяет данные в единую сущность, которая обрабатывается как цельный модуль. Напомним, что значение одной структуры можно присвоить другой структуре. Структуры можно передавать по значению точно так же, как и обычные переменные: в этом случае функция работает с копией исходной структуры. Кроме того, функция может возвращать структуру. Если имя массива, по сути, является адресом его первого элемента, то для структур все гораздо проще: имя структуры это просто имя структуры и ничего больше, и если необходимо получить ее адрес, следует воспользоваться операцией адресации &.

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

61 Передача и возврат структур Передача структур по значению имеет смысл, когда сама структура относительно компактна. Рассмотрим два соответствующих примера. В первом примере речь идет о времени путешествия. Согласно некоторым картам, чтобы добраться от водопада Тандер-Фолс до города Бинго, потребуется 3 часа 50 минут, а чтобы добраться от города Бинго до Гротескво 1 час 25 минут. Чтобы представить эти временные величины, можно воспользоваться структурой, в которой один элемент представляет значение часов, а второй значения минут. Сложение временных значений несколько усложняется за счет того, что приходится часть минут переносить в элемент, учитывающий-количество часов. Например, сумма двух указанных ранее временных величин составляет 4 часа 75 минут, что следует преобразовать в значение "5 часов 15 минут". Разработаем структуру, представляющую временные величины, а затем и функцию, которая принимает две такие структуры в качестве аргументов и возвращает структуру, которая представляет их сумму.

62 Передача и возврат структур Определение структуры не представляет трудностей: struct travel__time { int hours; int mins ; }; Далее рассмотрим прототип функции sum(), которая возвращает сумму двух таких структур. Возвращаемое значение, а также оба аргумента должны иметь тип travel_time. Таким образом, прототип имеет следующий вид: Travel_time sum (travel_time t1, travel_time t2) ; Чтобы сложить две временные величины, просуммируем сначала элементы, содержащие минуты. Целочисленное деление на 60 дает значение, которое нужно перенести в составляющую учета часов, а оператор деления по модулю (%) дает число минут в остатке. В программе, представленной в листинге 7.11, этот подход реализован с помощью функции sum (). Дополнительная функция show_time () производит отображение содержимого структуры travel_time.

63 Передача и возврат структур // travel.cpp использование структур в функциях #include sing namespace std; truct travel_time { int hours; int mins; }; const int Mins_per__hr = 60; travel_time sum(travel_time t1, travel_time t2) ; void show_time (travel_time t) ; int main() { travel_time day1 = {5, 45}; //5 часов 45 минут travel_time day2 = {4, 55}; //4 часа 55 минут travel_time trip = sum (day1, day 2) ; cout « "Two-day total: "; show_time (trip) ; travel_time day3= {4, 32} ; cout « "Three-day total: "; show__time (sum (trip, day3)) ; return 0; }

64 Передача и возврат структур travel_time sum(travel_time t1, travel_time t2) { travel_time total; total.mins = (t1. mins + t2.mins) % Mins_per_hr; total.hours = t1. hours + t2. hours + (t1. mins + t2.mins) / Mins_per_hr; return total; } void show_time(travel_time t) { cout « t.hours « " hours, " « t.mins « " minutes\n"; }

65 Передача и возврат структур Здесь переменная travel_time ведет себя так же, как имя стандартного типа данных (которое можно использовать для объявления переменных, типов возвращаемых функцией данных и типов аргументов функции). Переменные total и t1 структуры типа travel_time, поэтому к ним можно применить оператор принадлежности "точка". Обратите внимание на то, что поскольку функция sum() возвращает структуру travel_time, эту функцию можно использовать в качестве аргумента функции show__time (). А так как по умолчанию функции в языке C++ передают аргументы по значению, функция show_time (sum(trip, day3)) сначала вычисляет функцию sum(trip, day3), чтобы найти возвращаемое ею значение. Затем именно это значение, а не сама функция, передается функции show_time (). Ниже приводятся результа­ты выполнения программы: Two-day total: 10 hours, 40 minutes Three-day total: 15 hours, 12 minutes

66 Еще один пример Рассмотрим еще один пример. На этот раз мы будем иметь дело с пространством, а не со временем. В примере даются определения двух структур, представляющих собой различные способы задания местоположения точки, а затем разрабатываются функции, которые выполняют преобразование из одной формы в другую и отображают результат. Предположим, что требуется описать положение точки на экране или конкретного объекта на карте относительно некоторой начальной точки. Один из способов задать смещения объекта по горизонтали и вертикали относительно этой начальной точки. Для представления горизонтального смещения используется символ х, а для вертикального у. Можно определить структуру, состоящую из двух координат, задающих позицию точки: struct rect { double x; // расстояние от начальной точки по горизонтали double у; // расстояние от начальной точки по вертикали };

67 Еще один пример Второй способ описания местоположения точки заключается в том, что определяется ее расстояние от начальной точки и указывается, в каком направлении она находится (например, 40 градусов на северо-восток). По традиции в математике угол отсчитывается от горизонтальной оси против часовой стрелки. Это расстояние и угол вместе составляют полярные координаты точки. Можно объявить вторую структуру, представляющую собой такой метод определения местоположения точек: struct polar { double distance; double angle; };

68 Еще один пример Построим функцию, которая отображает содержимое структуры типа polar. Математические функции в библиотеке C++ рассчитаны на работу с углами в радианах, поэтому мы для измерения углов также будем пользоваться радианами. Однако для целей отображения преобразуем радианы в градусы. Это означает умножение на величину 180/р, которая приблизительно равна 57, Вот эта функция: // показать полярные координаты, представив // значение угла в градусах void show__polar (polar dapos) { const double Rad_to__deg = ; cout « "distance = " « dapos.distance; cout « ", angle = " « dapos.angle * Rad_to_deg; cout « " degrees\n"; } Обратите внимание на то, что тип формальной переменной polar. Когда этой функции передается структура типа polar, содержимое структуры копируется в структуру dapos, а функция далее использует эту копию в своей работе. Поскольку dapos является структурой, для идентификации элементов структуры в функции используется оператор принадлежности (точка).

69 Еще один пример Далее попытаемся сделать что-нибудь более существенное и создадим функцию, которая преобразует прямоугольные координаты в полярные. Пускай она принимает в качестве аргумента структуру rect и возвращает структуру polar вызывающей фун­кции. Для этого требуется воспользоваться функциями из математической библиотеки, поэтому в программу должен быть включен заголовочный файл math.h. Кроме того, в некоторых системах необходимо указать компилятору, что нужно загрузить библиотеку математических функций. Чтобы определить расстояния по горизонтальным и вертикальным координатам, можно воспользоваться теоремой Пифагора: distance = sqrt( х * х + у * у) Функция atan2 () из библиотеки математических функций вычисляет значение угла по значениям х и у: angle = atan2(y, x) Имея в распоряжении эти формулы, можно представить искомую функцию в следующем виде: // преобразование прямоугольных координат в полярные polar rect_to_polar(rect xypos)// тип polar { polar answers; answer.distance = sqrt( xypos.x * xypos.x + xypos.у * xypos.у); answer.angle = atan2(xypos.y, xypos.x); return answer;// возвращает структуру polar }

70 Еще один пример // strctfun.cpp -- функции, аргументами которых являются структуры #include using namespace std; // шаблоны структур struct polar { double distance;// расстояние от точки отсчета double angle;// направление от точки отсчета };}; struct rect { double x;// расстояние от точки отсчета по горизонтали double у;// расстояние от точки отсчета по вертикали }; // прототипы polar rect_to_polar(rect xypos); void show_polar(polar dapos); int main() { rect rplace; polar pplace; cout « "Enter the x and у values: ";

71 Еще один пример while (cin » rplace.x » rplace.y) // эффектное использование cin { pplace = rect__to_polar (rplace) ; show__polar (pplace) ; cout « "Next two numbers (q to quit) : } cout. « "Done. \n" ; return 0 ; } // преобразование прямоугольных координат в полярные polar rect__to__polar (rect xypos) { polar answer; answer.di s tance = sqrt( xypos.x * xypos.x + xypos.у * xypos.у); answer.angle = atan2(xypos. y, xypos.x); return answer;// возвращает структуру polar } // отображение полярных координат с представлением угла в градусах void show_polar (polar dapos) { const double Rad_to_deg = ; cout « "distance = " « dapos.distance; cout « ", angle = " « dapos. angle * Rad__to_deg; cout « " degrees\n" ; }

72 Еще один пример результат выполнения программы: Enter the x and у values: distance = 50, angle = degrees Next two numbers (q to quit): distance = , angle = 135 degrees Next two numbers (q to quit): q Примечания к программе Мы уже анализировали работу двух функций, примененных в этой программе, теперь же рассмотрим использование в ней объекта cin для управления циклом while: while (cin » rplace.x » rplace.y) Напомним, что cin это объект класса istream. Оператор извлечения (») спроектирован таким образом, что результатом выполнения конструкции cin » rplace.x будет также объект этого типа. Выражение cin » rplace.x приводит к тому, что программа в действительности вызывает функцию, которая возвращает значение типа istream. Если применить к конструкции cin » rplace.x оператор извлечения (подобно тому, как это делается в выражении cin » rplace.x » rplace.y), то снова получается объект класса istream. Следовательно, вычисление проверяемого выражения цикла while в конечном итоге сводится к вычислению объекта cin. Напомним, что в контексте проверяемого выражения при этом получается значение true или false типа bool в зависимости от успешности операции ввода. Например, в данном цикле объект cin ожидает, что пользователь введет два числа. Если вместо них ввести q, что мы в конце концов и сделали, оператор cin » распознает, что это не число. Данный оператор оставляет q в очереди ввода и возвращает значение, дающее false, вследствие чего выполнение цикла завершается.

73 Еще один пример Чтобы досрочно завершить выполнение цикла, необходимо ввести отрицательное число. Следовательно, ввод ограничивается неотрицательными значениями. Такое ограничение приемлемо для некоторых программ, но в большинстве случаев потребуется средство прерывания выполнения программы, которое не исключает использования каких-либо числовых значений. Использование конструкции cin » в качестве проверяемого условия позволяет избежать подобного рода ограничений, так как она правильно воспринимает ввод любого числового значения. Следует иметь это в виду в тех случаях, когда требуется создать цикл для ввода чисел. Кроме того, необходимо учитывать, что ввод нечислового значения вызывает состояние ошибки, которое делает невозможным дальнейшее чтение вводимых данных. Если в программе необходимо вводить данные уже после того, как будет выполнен входной цикл, следует воспользоваться функцией cin.clear (), чтобы сбросить флаг ошибки ввода. Затем может потребоваться удаление неверно введенных данных путем чтения их.

74 Передача адресов структур Почему бы не сэкономить время и память, передавая вместо самой структуры ее адрес? Для этого требуется изменить функции таким образом, чтобы они могли работать с указателями на структуры. Прежде всего перепишем функцию show_j>olar (). Необходимо внести следующие три изменения: При вызове функции передавать адрес структуры (&pplace), но не саму структуру (pplace). Объявить формальный параметр таким образом, чтобы он был указателем-на-polar, т.е. имел тип polar*. Поскольку функция не должна подвергать структуру изменениям, нужно пользоваться спецификатором const. Поскольку формальный параметр представляет собой указатель, а не структуру, следует воспользоваться оператором непрямой принадлежности (->) вместо операции принадлежности (точка).

75 Передача адресов структур После внесения указанных выше изменений функция принимает следующий вид: // отобразить полярные координаты, преобразуя радианы в градусы void showjpolar (const polar * pda) { const double Rad_to_deg = ; cout « "distance = " « pda->distance; cout « ", angle = " « pda->angle * Rad_to_deg; cout « " degrees\ n"; } Далее внесем изменения в функцию rect_to_polar (). Это более сложная задача, так как исходная функция rect_to_polar () возвращает структуру. Чтобы в полной мере воспользоваться преимуществами указателей, в качестве возвращаемого значения следует применить указатель. Для этого необходимо передать функции два указателя. Первый из них указывает на преобразуемую структуру, второй на структуру, в которой сохраняется результат преобразования. Вместо того, чтобы возвращать новую структуру, эта функция модифицирует существующую структуру в вызывающей функции. Отсюда следует, что если первый аргумент должен быть указателем со спецификатором const, то второй таковым быть не должен.

76 Передача адресов структур // strctptr.cpp функции с аргументами-указателями на структуры #include using namespace std; // шаблоны структур struct polar { double distance;// расстояние от точки отсчета double angle;// направление от точки отсчета >; struct rect { double x;// расстояние от точки расчета по горизонтали double у;// расстояние от точки отсчета по вертикали >; // прототипы void rect_to_jpolar (const rect * pxy, polar * pda); void showjpolar (const polar * pda);

77 Передача адресов структур int main() { rect rplace; polar pplace; cout « "Enter the x and у values: "; while (can » rplace.x » rplace.у) { rect_to_j?olar(&rplace,fipplace); // передача адресов show_polar (fipplace) ; // передача адресов cout « "Next two numbers (q to quit): " ; } cout « "Done./n"; return 0; } // отображение полярных координат, радианы приводятся к градусам void show_polar (const polar * pda) { const double Rad_to_deg « ; cout « "distance « " « pda->distance; cout « ", angle » ° « pda->angle * Rad_to_deg; cout « " degrees\n"; } // преобразование прямоугольных координат в полярные void rect_to_polar (const rect * pxy, polar * pda) { pda->distance = sqrt(pxy->x * pxy->x + pxy->y * pxy->y); pda->angle = atan2(pxy->y, pxy->x); }

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

79 Рекурсия Функция в C++ обладает весьма интересным свойством она может вызывать саму себя (в отличие от языка С, однако, в C++ не допускается обращение функции main() к самой себе). Такое свойство называется рекурсией. Рекурсия это исключительно важное инструментальное средство в некоторых областях программирования, в частности, при программировании искусственного интеллекта, однако сейчас мы лишь в общих чертах ознакомимся с тем, как она работает. Когда рекурсивная функция вызывает саму себя, только что вызванная функция также вызывает саму себя и т.д. до бесконечности, если, конечно, ее программный код не содержит ничего, что способно прервать эту последовательность вызовов. Обычно рекурсивный вызов реализуют как часть оператора if. Например, рекурсивная функция типа void с именем recurs () может иметь такой вид: void recurs(слисок_аргументов) { Операторы 1 if (проверка) recurs(аргументы) операторы 2 }

80 Рекурсия Пока проверяемое выражение оператора if остается истинным, в результате каждого вызова функции recurs () выполняется блок операторы 1, а затем производится очередной вызов функции recurs (), при этом блок операторы 2 остается вне досягаемости. Когда проверяемое выражение примет значение false, начинает выполняться блок операторы 2. Далее, по завершении выполнения текущего блока, управление программой передается к предыдущей версии функции recurs, которая его вызвала. Затем эта версия функции recurs () заканчивает выполнение своего блока операторы 2 и завершается сама, возвращая управление предыдущему вызову функции, и т.д. Таким образом, если функция recurs () подвергнется пяти рекурсивным вызовам, первый блок операторов операторы 1 будет выполнен пять раз в том порядке, в каком эти функции были вызваны, затем блок операторы 2 будет выполнен пять раз в порядке, обратном последовательности вызова функций. Погрузившись на пять уровней рекурсии, программа должна затем подняться на те же пять уровней.

81 Рекурсия // recur.cpp применение рекурсии #include using namespace std; void countdown(int n) ; int main() { countdown (4) ;// вызов рекурсивной функции return 0; } void countdown(int n) { cout « "Counting down... " « n « "\n"; if (n > 0) countdown(n-1); // функция вызывает саму себя cout « п « ": Kaboom!\n"; }

82 Рекурсия Counting down … 4 уровень 1, возрастание уровня рекурсии Counting down … 3 уровень 2 Counting down … 2 уровень 3 Counting down … 1 уровень 4 Counting down … 0 уровень 5, последний рекурсивный вызов 0: Kaboom! уровень 5, начало обратного прохода уровней рекурсии 1: Kaboom!уровень 4 2: Kaboom!уровень 3 3: Kaboom!уровень 2 4: Kaboom!уровень 1 Ниже приведен результат выполнения программы (с комментариями к этапам выполнения программы, они не выводятся на экран)

83 Рекурсия Обратите внимание на то, что в результате каждого рекурсивного вызова создается собственный набор переменных. Поэтому ко времени выполнения программой пятого вызова у нее уже будет пять отдельных переменных с именем п, при этом каждая со своим, отличным от других значением. Рекурсия особенно полезна в ситуациях, когда требуется многократное разбиение задачи на две аналогичные подзадачи меньшего масштаба. В качестве примера рассмотрим, как работает этот подход при изготовлении чертежа линейки. Отметим оба конца линейки, найдем среднюю точку и пометим ее. Затем применим эту процедуру сначала к левой половине линейки, а затем к правой. Если вы намерены продолжить разметку, нужно выполнить процедуру этого типа по отношению к очередному участку. Такой рекурсивный подход иногда называют стратегией "разделяй и властвуй". В программе из листинга 7.15 этот подход иллюстрируется на примере функции subdivide(). В ней используется строка, которая в исходном состоянии заполнена пробелами, если не считать символа |, обозначающего оба конца строки. Чтобы обратиться к функции subdivide () шесть раз, главная программа прибегает к помощи цикла, каждый раз увеличивая количество уровней рекурсии и выводя получающуюся при этом строку.

84 Рекурсия // ruler.cpp использование рекурсии для нанесения делений линейки #include using namespace std; const int Len = 66; const int Divs = 6; void subdivide (char ar[], int low, int high, int level); int main() { char ruler[Len]; int i; for (i = 1; i < Len - 2; i++) ruler [i] = ' ' ; ruler [Len - 1] = \O; int max = Len 2; int min = 0; ruler [min] = ruler [max] = ' |'; cout « ruler « "\n"; for (i = 1; i <= Divs; i++) { subdivide(ruler,min,max, i); cout « ruler « "\n"; for (int j = 1; j < Len - 2; j++) ruler[j] = ; // возврат к "чистой" линейке } return 0 ; } void subdivide (char ar[], int low, int high, int level) { if (level == 0) return; int mid = (high + low) / 2; ar[mid] = ' | ' ; subdivide (ar, low, mid, level 1) ; subdivide(ar, mid, high, level 1) ; }

85 Рекурсия Примечания к программе Для управления уровнями рекурсии в функции subdivide () используется переменная с именем level. Всякий раз, когда функция обращается к самой себе, она уменьшает значение переменной level на 1. Когда это значение достигает нуля, выполнение функции для данного вызова завершается. Обратите внимание на то, что функция subdivide () вызывает сама себя дважды: один раз для деления левой части и один раз для деления правой части. Исходная средняя точка становится правым концом для первого вызова и левым концом для второго вызова. Следует отметить, что число обращений возрастает в геометрической прогрессии. Другими словами, один вызов генерирует два последующих, которые, в свою очередь, генерируют уже четыре вызова, потом восемь и т.д. Поэтому вызов уровня 6 способен заполнить 64 элемента (26 = 64).

86 Указатели на функции Функции, как и элементы данных, имеют адреса. Адресом функции является адрес памяти, с которого начинается машинный код функции. Для пользователя знание такого адреса, как правило, не дает никаких преимуществ, однако для программы может оказаться полезным. Например, можно создать функцию, которая принимает адрес другой функции в качестве аргумента. Таким образом, первая функция может найти вторую и выполнить ее. Это более громоздкий подход в сравнении с непосредственным обращением первой функции ко второй, тем не менее, при его использовании обеспечивается возможность в разные моменты передавать адреса различных функций. Таким образом, один раз первая функция может вызвать вторую функцию, другой раз третью и т.п.

87 Назначение указателя на функцию Предположим, что необходимо разработать функцию estimate (), которая вычисляет время, необходимое для написания заданного числа строк программного кода. При этом желательно, чтобы этой функцией могли пользоваться разные программисты. Часть программного кода функции estimate() будет одинаковой для всех пользователей, однако данная функция позволит каждому программисту воспользоваться собственным алгоритмом вычисления времени. Механизм реализации этого подхода состоит в передаче функции estimate () адреса функции, реализующей определенный алгоритм, который пользователь желает использовать. Чтобы осуществить такой план, нужно выполнить следующие действия: Принять адрес функции. Объявить указатель на функцию. Использовать указатель на функцию для вызова этой функции.

88 Получение адреса функции Получить адрес функции несложно: достаточно воспользоваться именем функции без последующих скобок. Таким образом, если think () функция, то think адрес этой функции. Чтобы передать какую-либо функцию как аргумент, достаточно передать имя этой функции. Следует четко осознавать различие между передачей адреса функции и передачей возвращаемого значения функции: process (think) ; // передача функции process() адреса функции think() thought (think () ) ; // передача функции thought () значения, //возвращаемого функцией think() Приведенное выше обращение к функции process () обеспечивает вызов функции think() из функции process (). Обращение к функции thought () сначала приводит к вызову функции think (), а затем к передаче возвращаемого ей значения функции thought ().

89 Объявление указателя на функцию При объявлении указателя на данные необходимо точно определять, на данные какого типа он указывает. Аналогичным образом это делается и для функций: для указателя на функцию должно быть определено, на какой тип функции он указывает. Это означает, что такое объявление должно идентифицировать тип данных, возвращаемых функцией, равно как и сигнатуру функции (список ее аргументов). Другими словами, объявление указателя должно содержать те же сведения о функции, что и ее прототип. Предположим, что Памела написала функцию подсчета затраченного времени со следующим прототипом: double pam(int); // прототип Объявление указателя соответствующего типа выглядит следующим образом: double (*pf)(int); // pf указывает на функцию, которая принимает один // аргумент типа int и возвращает значение типа double это объявление во многом похоже на объявление раm(); при этом выражение (*pf) играет роль имени функции pam. Поскольку pam функция, таковой является и запись (*pf). А если (*pf) функция, то pf является указателем на функцию. В общем случае для того, чтобы объявить указатель на функцию определенного вида, сначала можно создать прототип обычной функции нужного типа, а затем заменить имя этой функции выражением вида (*pf). Благодаря этому pf становится указателем на функцию этого типа.

90 Объявление указателя на функцию В объявлении необходимо заключить выражение *pf в круглые скобки, чтобы обеспечить правильный порядок выполнения операторов. Круглые скобки имеют более высокий приоритет, чем оператор *, следовательно, *pf (int) означает, что pf () это функция, которая возвращает указатель, в то время как (*pf) (int) означает, что pf является указателем на функцию: double (*pf) (int); //pf указывает на функцию, //которая возвращает значение типа double double *pf(int); //pf() является функцией, которая //возвращает указатель на значение типа double После того объявления указателя pf можно присвоить ему адрес подходящей функции: double pam(int); double (*pf)(int); pf = pam;// теперь pf указывает на функцию pam() Обратите внимание на то, что функция рат() должна соответствовать pf как по сигнатуре, так и по типу возвращаемого значения. Компилятор отвергает операции присваивания, содержащие подобного рода несоответствия: double ned(double); int ted(int) ; double (*pf)(int); pf = ned;// неправильно несоответствие сигнатур pf = ted;// неправильно несоответствие возвращаемых типов

91 Объявление указателя на функцию Вернемся к функции estimate(), о которой шла речь выше. Предположим, что необходимо передать ей количество строк программного кода, который нужно написать, а также адрес алгоритма вычисления времени (например, адрес функции раm()). В этом случае может иметь место следующий прототип: void estimate(int lines, double (*pf) (int) ) ; Из такого объявления следует, что второй аргумент является указателем на функцию, которая принимает аргумент типа int и возвращает значение типа double. Чтобы заставить функцию estimate () использовать функцию рат(), следует передать ей адрес функции pam (): estimate(50, pam); // обращение к функции, //указывающий функции // estimate() использовать раm() Совершенно ясно, что самым сложным моментом при использовании указателей на функции является написание прототипов, в то время как передача адреса не представляет затруднений.

92 Использование указателя для вызова функции Перейдем завершению рассмотрения методики использования указателя для вызова функции. Ключевое значение здесь имеет объявление указателя. Напомним, что выражение (*pf) в этом случае играет ту же роль, что имя функции. Таким образом, все что нам нужно сделать, это использовать выражение (*pf), как имя функции: double pam(int); double (*pf)(int); рf = раm;// pf сейчас указывает на функцию раm() double х = раm(4);// вызов раm(), используя имя функции double у = (*pf)(5);// вызов раm(), используя указатель pf В C++ возможно также использование указателя pf таким образом, как если бы он был именем функции: double у = pf(5); // также вызывает раm(), используя указатель pf. Мы будем пользоваться первой формой. Она более громоздкая и неудобная, но, в то же время, служит наглядным напоминанием о том, что в программном коде используется указатель на функцию