Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Упражнение 6.1. В чем разница между параметром и аргументом? Упражнение 6.2. Укажите, какие из следующих функций ошибочны и почему. Предложите способ их исправления. (a) int f() { string s; // ... return s; } (b) f2(int i) { /* ... */ } (c) int calc(int v1, int v1) /* ... */ } (d) double square(double x) return x * x; Упражнение 6.3. Напишите и проверьте собственную версию функции fact(). Упражнение 6.4. Напишите взаимодействующую с пользователем функцию, которая Page 264/1103 запрашивает число и вычисляет его факториал. Вызовите эту функцию из функции main(). Упражнение 6.5. Напишите функцию, возвращающую абсолютное значение ее аргумента. 6.1.1. Локальные объекты В языке С++ имя имеет область видимости (см. раздел 2.2.4), а объекты — продолжительность существования (object lifetime). Обе эти концепции важно понимать. • Область видимости имени — это часть текста программы , в которой имя видимо. • Продолжительность существования объекта — это время при выполнении программы , когда объект существует. Как уже упоминалось, тело функции — это блок операторов. Как обычно, блок формирует новую область видимости, в которой можно определять переменные. Параметры и переменные, определенные в теле функции, называются локальными переменными (local variable). Они являются локальными для данной функции и скрывают (hide) объявления того же имени во внешней области видимости. Объекты, определенные вне любой из функций, существуют на протяжении выполнения программы. Такие объекты создаются при запуске программы и не удаляются до ее завершения. Продолжительность существования локальной переменной зависит от того, как она определена. Автоматические объекты Объекты, соответствующие обычным локальным переменным, создаются при достижении процессом выполнения определения переменной в функции. Они удаляются, когда процесс выполнения достигает конца блока, в котором определена переменная. Объекты, существующие только во время выполнения блока, известны как автоматические объекты (automatic object). После выхода процесса выполнения из блока значения автоматических объектов, созданных в этом блоке, неопределенны. Параметры — это автоматические объекты. Место для параметров резервируется при запуске функции. Параметры определяются в пределах тела функции. Следовательно, они удаляются по завершении функции. Автоматические объекты, соответствующие параметрам функции, инициализируются аргументами, переданными функции. Автоматические объекты, соответствующие локальным переменным, инициализируются, если их определение содержит инициализатор. В противном случае они инициализируются значением по умолчанию (см. раздел 2.2.1), а это значит, что значения неинициализированных локальных переменных встроенного типа неопределенны. Локальные статические объекты Иногда полезно иметь локальную переменную, продолжительность существования которой не прерывается между вызовами функции. Чтобы получить такие объекты, при определении локальной переменной используют ключевое слово static. Каждый Page 265/1103 локальный статический объект (local static object) инициализируется прежде, чем выполнение достигнет определения объекта. Локальная статическая переменная не удаляется по завершении функции; она удаляется по завершении программы. В качестве простого примера рассмотрим функцию, подсчитывающую количество своих вызовов: size_t count_calls() { static size_t ctr = 0; // значение сохраняется между вызовами return ++ctr; } int main() { for (size_t i = 0; i != 10; ++i) cout << count_calls() << endl; return 0; } Эта программа выводит числа от 1 до 10 включительно. Прежде чем процесс выполнения впервые достигнет определения переменной ctr, она уже будет создана и получит исходное значение 0. Каждый вызов осуществляет инкремент переменной ctr и возвращает ее новое значение. При каждом запуске функции count_calls() переменная ctr уже существует и имеет некое значение, возможно, оставленное последним вызовом функции. Поэтому при втором вызове значением переменной ctr будет 1, при третьем — 2 и т.д. Если у локальной статической переменной нет явного инициализатора, она инициализируется значением по умолчанию (см. раздел 3.3.1), следовательно, локальные статические переменные встроенного типа инициализируются нулем. Упражнения раздела 6.1.1 Упражнение 6.6. Объясните различия между параметром, локальной переменной и локальной статической переменной. Приведите пример функции, в которой каждая из них могла бы быть полезной. Упражнение 6.7. Напишите функцию, которая возвращает значение 0 при первом вызове, а при каждом последующем вызове возвращает последовательно увеличивающиеся числа. 6.1.2. Объявление функций Как и любое другое имя, имя функции должно быть объявлено прежде, чем его можно будет использовать. Подобно переменным (см. раздел 2.2.2), функция может быть определена только однажды, но объявлена может быть многократно. За одним исключением, которое будет описано в разделе 15.3, можно объявить функцию, которая не определяется до тех пор, пока она не будет использована. Page 266/1103 Объявление функции подобно ее определению, но у объявления нет тела функции. В объявлении тело функции заменяет точка с запятой. Поскольку у объявления функции нет тела, нет никакой необходимости в именах параметров. Поэтому имена параметров зачастую отсутствуют в объявлении. Хоть имена параметров и не обязательны, они зачастую используются, чтобы помочь пользователям функции понять ее назначение: // имена параметров указывают, что итераторы обозначают диапазон // выводимых значений void print(vector<int>::const_iterator beg, vector<int>::const_iterator end); Эти три элемента объявления (тип возвращаемого значения, имя функции и тип параметров) описывают интерфейс (interface) функции. Они задают всю информацию, необходимую для вызова функции. Объявление функции называют также прототипом функции (function prototype).Объявления функций находятся в файлах заголовка Напомним, что объявления переменных располагают в файлах заголовка (см. раздел 2.6.3), а определения — в файлах исходного кода. По тем же причинам функции должны быть объявлены в файлах заголовка и определены в файлах исходного кода. Весьма соблазнительно (и вполне допустимо) размещать объявления функций непосредственно в каждом файле исходного кода, который использует функцию. Однако такой подход утомителен и приводит к ошибкам. Помещая объявления функций в файлы заголовка, можно гарантировать, что все объявления данной функции будут одинаковы. Если необходимо изменить интерфейс функции, достаточно модифицировать его только в одном объявлении. Файл исходного кода, в котором функция определена , должен подключать заголовок, в котором функция объявлена . Так компилятор сможет проверить соответствие определения и объявления.Упражнения раздела 6.1.2 Упражнение 6.8. Напишите файл заголовка по имени Chapter6.h, содержащий объявления функций, написанных для упражнений раздела 6.1 6.1.3. Раздельная компиляция По мере усложнения программ возникает необходимость хранить различные части программы в отдельных файлах. Например, функции, написанные для упражнений раздела 6.1, можно было бы сохранить в одном файле, а код, использующий их, в других файлах Page 267/1103 исходного кода. Язык С++ позволяет разделять программы на логические части, предоставляя средство, известное как раздельная компиляция (separate compilation). Раздельная компиляция позволяет разделять программы на несколько файлов, каждый из которых может быть откомпилирован независимо.Компиляция и компоновка нескольких файлов исходного кода Предположим, например, что определение функции fact() находится в файле fact.cc, а ее объявление — в файле заголовка Chapter6.h. Файл fact.cc, как и любой другой файл, использующий эту функцию, будет включать заголовок Chapter6.h. Функцию main(), вызывающую функцию fact(), будем хранить в еще одном файле factMain.cc. Чтобы создать исполнимый файл (executable file), следует указать компилятору, где искать весь используемый код. Эти файлы можно было бы откомпилировать следующим образом: $ CC factMain.cc fact.cc # generates factMain.exe or a.out $ CC factMain.cc fact.cc -o main # generates main or main.exe где CC — имя компилятора; $ — системная подсказка; # — начало комментария командной строки. Теперь можно запустить исполняемый файл, который выполнит нашу функцию main(). Если бы изменен был только один из наших файлов исходного кода, то перекомпилировать достаточно было бы только тот файл, который был фактически изменен. Большинство компиляторов предоставляет возможность раздельной компиляции каждого файла. Обычно этот процесс создает файл с расширением .obj (на Windows) или .o (на UNIX), указывающим, что этот файл содержит объектный код (object code). Компилятор позволяет скомпоновать (link) объектные файлы (object file) и получить исполняемый файл. На системе авторов раздельная компиляция программы осуществляется следующим образом: $ CC -с factMain.cc # generates factMain.o $ CC -c fact.cc # generates fact.o $ CC factMain.o fact.o # generates factMain.exe or a.out $ CC factMain.o fact.o -o main # generates main or main.exe Сверьтесь с руководством пользователя вашего компилятора, чтобы уточнить, как именно компилировать и запускать программы, состоящие из нескольких файлов исходного кода. Упражнения раздела 6.1.3 Упражнение 6.9. Напишите собственные версии файлов fact.cc и factMain.cc. Эти файлы должны включать заголовок Chapter6.h из упражнения предыдущего раздела. Используйте эти файлы чтобы понять, как ваш компилятор обеспечивает раздельную компиляцию. Page 268/1103 6.2. Передача аргументов Как уже упоминалось, при каждом вызове функции ее параметры создаются заново. Используемое для инициализации параметра значение предоставляет соответствующий аргумент, переданный при вызове. Параметры инициализируются точно так же, как и обычные переменные. Как и у любой другой переменной, взаимодействие параметра и его аргумента определяет тип параметра. Если параметр — ссылка (см. раздел 2.3.1), то параметр привязывается к своему аргументу. В противном случае, значение аргумента копируется. Когда параметр — ссылка, говорят, что его аргумент передается по ссылке (pass by reference) или что функция вызывается по ссылке (call by reference). Подобно любой другой ссылке, ссылочный параметр — это только псевдоним объекта, к которому он привязан, т.е. ссылочный параметр — псевдоним своего аргумента. Когда значение аргумента копируется, параметр и аргумент — независимые объекты. Говорят, что такие аргументы передаются по значению (pass by value) или что функция вызывается по значению (call by value). 6.2.1. Передача аргумента по значению При инициализации переменной не ссылочного типа значение инициализатора копируется. Изменения значения переменной никак не влияют на инициализатор: int n = 0; // обычная переменная типа int int i = n; // i - копия значения переменной n i = 42; // значение i изменилось, значение n - нет Передача аргумента по значению осуществляется точно так же; что бы функция не сделала с параметром, на аргумент это не повлияет. Например, в функции fact() (см. раздел 6.1) происходит декремент параметра val: ret *= val--; // декремент значения val Хотя функция fact() изменила значение val, это изменение никак не повлияло на переданный Page 269/1103 ей аргумент. Вызов fact(i) не изменяет значение переменной i. Параметры указателя Указатели (см. раздел 2.3.2) ведут себя, как любой не ссылочный тип. При копировании указателя его значение копируется. После создания копии получается два отдельных указателя. Однако указатель обеспечивает косвенный доступ к объекту, на который он указывает. Значение этого объекта можно изменить при помощи указателя (см. раздел 2.3.2): int n = 0, i = 42; int *p = &n, *q = &i; // p указывает на n; q указывает на i *p = 42; // значение n изменилось, значение p - нет p = q; // теперь p указывает на i; значения i и n // неизменны То же поведение характерно для указателей, являющихся параметрами: // функция получает указатель и обнуляет значение, на которое он // указывает void reset(int *ip) { *ip = 0; // изменяет значение объекта, на который указывает ip ip = 0; // изменяет только локальную копию ip; аргумент неизменен } После вызова функции reset() объект, на который указывает аргумент, будет обнулен, но сам аргумент-указатель не изменится: int i = 42; reset(&i); // изменяет значение i, но не адрес cout << "i = " << i << endl; // выводит i = 0 Page 270/1103 Программисты, привыкшие к языку С, зачастую используют параметры в виде указателей для доступа к объектам вне функции. В языке С++ для этого обычно используют ссылочные параметры. Упражнения раздела 6.2.1 Упражнение 6.10. Напишите, используя указатели, функцию, меняющую значения двух целых чисел. Проверьте функцию, вызвав ее и отобразив измененные значения. 6.2.2. Передача аргумента по ссылке Напомним, что операции со ссылками — это фактически операции с объектами, к которым они привязаны (см. раздел 2.3.1): int n = 0, i = 42; int &r = n; // r привязан к n (т.е. r - другое имя для n) r = 42; // теперь n = 42 r = i; // теперь n имеет то же значение, что и i i = r; // i имеет то же значение, что и n Ссылочные параметры используют это поведение. Обычно они применяются, чтобы позволить функции изменить значение одного или нескольких аргументов. Для примера можно переписать программу reset из предыдущего раздела так, чтобы использовать ссылку вместо указателя: // функция, получающая ссылку на объект типа int и обнуляющая его void reset(int &i) // i - только другое имя объекта, переданного // на обнуление { i = 0; // изменяет значение объекта, на который ссылается i } Page 271/1103 Подобно любой другой ссылке, ссылочный параметр связывается непосредственно с объектом, которым он инициализируется. При вызове этой версии функции reset() параметр i будет связан с любым переданным ей объектом типа int. Как и с любой ссылкой, изменения, сделанные с параметром i, осуществляются с объектом, на который она ссылается. В данном случае этот объект — аргумент функции reset(). Когда вызывается эта версия функции reset(), объект передается непосредственно; поэтому нет никакой необходимости в передаче его адреса: int j = 42; reset(j); // j передается по ссылке; значение в j изменяется cout << "j = " << j << endl; // выводит j = 0 В этом вызове параметр i — это только другое имя переменной j. Любое использование параметра i в функции reset() фактически является использованием переменной j. Использование ссылки во избежание копирования Копирование объектов больших классов или больших контейнеров снижает эффективность программы. Кроме того, некоторые классы (включая классы IO) не допускают копирования. Для работы с объектами, тип которых не допускает копирования, функции должны использовать ссылочные параметры. В качестве примера напишем функцию сравнения длин двух строк. Поскольку строки могут быть очень длинными и их копирования желательно избежать, сделаем параметры ссылками. Так как сравнение двух строк не подразумевает их изменения, сделаем ссылочные параметры константами (см. раздел 2.4.1): // сравнить длины двух строк bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size(); } Как будет продемонстрировано в разделе 6.2.3, для не подлежащих изменению ссылочных параметров функции должны использовать ссылки на константу. Ссылочные параметры, которые не изменяются в функции, должны быть объявлены как const. Использование ссылочных параметров для возвращения дополнительной информации Функция может возвратить только одно значение. Но что если функции нужно возвратить больше одного значения? Ссылочные параметры позволяют возвратить несколько результатов. В качестве примера определим функцию find_char(), которая возвращает позицию первого вхождения заданного символа в строке. Функция должна также возвращать количество этих символов в строке. Как же определить функцию, возвращающую и позицию, и количество вхождений? Можно Page 272/1103 было бы определить новый тип, содержащий позицию и количество. Однако куда проще передать дополнительный ссылочный аргумент, содержащий количество вхождений: // возвращает индекс первого вхождения с в s // ссылочный параметр occurs содержит количество вхождений string::size_type find_char(const string &s, char c, string::size_type &occurs) { auto ret = s.size(); // позиция первого вхождения, если оно есть occurs = 0; // установить параметр количества вхождений for (decltype(ret) i = 0; i != s.size(); ++i) { if (s[i] == c) { if (ret == s.size()) ret = i; // запомнить первое вхождение с ++occurs; // инкремент счетчика вхождений } } return ret; // количество возвращается неявно в параметре occurs } Когда происходит вызов функции find_char(), ей передаются три аргумента: строка, в которой осуществляется поиск, искомый символ и объект типа size_type (раздел 3.2.2), содержащий счетчик вхождений. Если s является объектом класса string, a ctr — объектом типа size_type, то функцию find_char() можно вызвать следующим образом: auto index = find_char(s, 'o', ctr); После вызова значением объекта ctr будет количество вхождений символа о, a index укажет на его первое вхождение, если оно будет. В противном случае значение index будет равно s.size(), a ctr — нулю. Упражнения раздела 6.2.2 Упражнение 6.11. Напишите и проверьте собственную версию функции reset(), получающую Page 273/1103 ссылку. Упражнение 6.12. Перепишите программу из упражнения 6.10 раздела 6.2.1 так, чтобы использовать ссылки вместо указателей при смене значений двух целочисленных переменных. Какая из версий, по вашему, проще в использовании и почему? Упражнение 6.13. Если Т — имя типа, объясните различие между функцией, объявленной как void f(Т) и как void f(Т&). Упражнение 6.14. Приведите пример, когда параметр должен быть ссылочным типом. Приведите пример случая, когда параметр не должен быть ссылкой. Упражнение 6.15. Объясните смысл каждого из типов параметров функции find_char(). В частности, почему s — ссылка на константу, a occurs — простая ссылка? Почему эти параметры ссылочные, а параметр с типа char нет? Что будет, сделай мы s простой ссылкой? |