Работа функций - приподнимаем завесу тайны

При вызове функции управление программой передается вызванной функции. Происходит передача параметров, после чего начинается выполнение операторов, составляющих тело функции. По завершении выполнения функции возвращается некоторое значение (если не определено, что функция возвращает тип void) и управление передается вызывающей функции.

Как же реализуется эта задача? Откуда программе известно, к какому блоку ей сейчас нужно перейти? Где хранятся переменные при передаче их в качестве аргументов? Что происходит с переменными, которые объявляются в теле функции? Как передается назад возвращаемое значение? Откуда программе известно, с какого места ей нужно продолжить работу?

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

Уровни абстракции

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

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

Однако вы должны понимать, как в компьютере организована память. Без четкого представления о том, где располагаются ваши переменные после их создания и как передаются значения между функциями, программирование останется для вас непостижимой тайной.

Разбиение памяти

Когда вы начинаете работу со своей программой, операционная система (например, DOS или Microsoft Windows) выделяет различные области памяти в ответ на требования компилятора. Как программисту на C++, вам часто придется интересоваться пространством глобальных имен, свободной памятью, регистрами, памятью сегментов программы и стеками.

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

Регистры представляют собой специальную область памяти, встроенную прямо в центральное процессорное устройство, или центральный процессор (Central Processing Unit — CPU). На их плечи возложена забота о выполнении внутренних вспомогательных функций, описание большей части которых выходит за рамки этой книги. Но мы все-таки остановимся на рассмотрении набора регистров, ответственных за указание на следующую строку программы в любой момент времени. Назовем эти регистры (все вместе) указателями команд. Именно на указатель команды ложится ответственность следить за тем, какая строка программы должна выполняться следующей.

Сама программа находится в памяти компьютера, которая специально отведена для того, чтобы хранить операторы программы в двоичном формате. Каждая строка исходного текста программы транслируется в ряд команд, а каждая из этих команд хранится в памяти по своему адресу. Указатель команды содержит адрес следующей команды, предназначенной для выполнения. Эта идея иллюстрируется на рис. 5.6.

Рис. 5.6. Указатель команды

Стек — это специальная область памяти, выделенная для хранения данных вашей программы, требуемых каждой вызываемой функцией. Она называется стеком потому, что представляет собой очередь типа "последним пришел — первым вышел" и напоминает стопку тарелок в руках официанта (рис. 5.7).

Принцип "последним пришел — первым вышел" означает, что элемент, добавленный в стек последним, будет вынут из него первым. Большинство же очередей функционирует подобно очереди в театр: первый, кто занял очередь, первым из нее и выйдет (и войдет в театр). Стек скорее напоминает стопку монет, удерживаемых специальным приспособлением. Если расположить в нем 10 монет достоинством в 1 копейку, а затем попытаться вынуть несколько монет, то первой вы достанете ту, что была вставлена последней.

При помещении данных в стек он расширяется, а при возвращении данных из стека — сужается. Невозможно из стопки достать одну тарелку, не вынув предварительно все тарелки, помещенные в стопку перед ней. То же справедливо для данных в стеке памяти.

Рис. 5.7. Стек

Аналогия со стопкой тарелок приводится чаще всего. Такое сравнение довольно наглядно, но не вполне верно в смысле техники выполнения. Более точное представление позволит создать ряд прямоугольных полей, выровненных сверху вниз. Вершиной стека будет служить любое поле, на которое указывает в данный момент указатель вершины стека (эту роль выполняет другой регистр).

Все поля имеют последовательные адреса, и один из этих адресов хранится в регистре указателя вершины стека. Все, что находится ниже вершины стека, относится к стеку. Все, что находится выше вершины стека, игнорируется, как показано на рис. 5.8.

Рис. 5.8. Указатель вершины стека

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

Рис. 5.9. Перемещение указателя вершины стека

Стек и функции

Ниже перечислены действия, происходящие с программой, выполняемой под управлением DOS, при переходе к телу функции.

1. Увеличивается адрес, содержащийся в указателе команды, чтобы указывать на инструкцию, следующую после вызова функции. Затем этот адрес помещается в стек и будет служить адресом возврата по завершении выполнения функции.

2. В стеке резервируется место для возвращаемого функцией значения объявленного вами типа. Если в системе с двухбайтовыми целыми для возвращаемого значения объявлен тип int, то к стеку добавляются еще два байта, но в эти байты ничего пока не помещается.

3. В указатель команды загружается адрес вызванной функции, который хранится в отдельной области памяти, отведенной специально для этих целей. Поэтому следующей выполняемой командой будет первый оператор вызванной функции.

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

5. В стек помещаются все аргументы, передаваемые функции.

6. Выполняется команда, адрес которой находится в данный момент в указателе команды, т.е. первая строка кода функции.

7. По мере определения в стеке размещаются локальные переменные и функции.

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

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

Некоторые детали этого процесса изменяются при переходе от компилятора к компилятору или от компьютера к компьютеру, но основная идея остается прежней независимо от операционной среды. В общем случае при вызове функции адрес возврата и ее параметры всегда помещаются в стек: На протяжении жизненного цикла функции в стек добавляются локальные переменные. По возвращении из функции все они удаляются из стека.

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

Резюме

На этом занятии вы познакомились с функциями. Функция в действительности представляет собой подпрограмму, которой можно передавать параметры и из которой можно возвращать значение. Каждый запуск программы C++ начинается с выполнения функции main(), которая, в свою очередь, может вызывать другие функции.

Функция объявляется с помощью прототипа функции, который описывает возвращаемое значение, имя функции и типы ее параметров. При желании функцию можно объявить подставляемой (с помощью ключевого слова inline). В прототипе функции можно также объявить значения, используемые по умолчанию для одного или нескольких параметров функции.

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

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

Вопросы и ответы

Почему бы не сделать все переменные глобальными?

Когда-то именно так и поступали. Но по мере усложнения программ стало очень трудно находить в них ошибки, поскольку значения глобальных переменных могли быть изменены любой из функций, поэтому сложно было определить, какой именно блок программы виновен в ошибке. Многолетний опыт убедил программистов, что данные должны храниться локально (насколько это возможно) и доступ к изменению данных должен быть определен как можно более узким кругом.

Когда следует использовать в прототипе функции ключевое слово inline?

Если функция невелика (занимает не более одной-двух строк) и встраивание ее в код программы по всем местам вызова не увеличит существенно размер этой программы, то, возможно, имеет смысл объявить ее как inline.

Почему изменения, вносимые в теле функции в переменные, переданные как аргументы, не отражаются на значениях этих переменных в основном коде программы?

Аргументы обычно передаются в функцию как значения, т.е. аргумент в функции является на самом деле копией оригинального значения. Данная концепция подробно разъяснялась на этом занятии.

Как поступить, если необходимо, чтобы изменения, внесенные в функции, сохранились после возвращения из функции?

Эта проблема рассматривается на занятии 8. Использование указателей не только решает эту проблему, но также предоставляет способ обойти ограничение на возврат только одного значения из функции.

Что произойдет, если объявить следующие две функции:

int Area (int width, int length = 1); int Area (int size);

Будут ли они перегруженными? Условие уникальности списков параметров соблюдено, но в первом варианте для параметра определено значение, используемое по умолчанию.

Эти объявления будут скомпилированы, но, если вызвать функцию Area () с одним параметром, будет сгенерирована ошибка компиляции, обусловленная неопределенностью между функциями Area(int, int) и Area(int).

Коллоквиум

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

Контрольные вопросы

1. В чем разница между объявлением прототипа функции и определением функции?

2. Должны ли имена параметров, указанные в прототипе, определении и вызове функции соответствовать друг другу?

3. Если функция не возвращает значение, как следует объявить такую<функцию?

4. Если не объявить тип возврата, то какой тип будет принят по умолчанию для возвращаемого значения?

5. Что такое локальная переменная?

6. Что такое область видимости?

7. Что такое рекурсия?

8. Когда следует использовать глобальные переменные?

9. Что такое перегрузка функции?

10. Что такое полиморфизм?

Упражнения

1. Запишите прототип для функции с именем Perimeter, которая возвращает значение

ranaunsigned long int ипринимаетдвапараметратипаипБгдпей short int.

2. Запишите определение функции Perimeter согласно объявлению в упражнении 1. Два принимаемых ею параметра представляют длину и ширину прямоугольника, а функция возвращает его периметр (удвоенная длина плюс удвоенная ширина).

3. Жучки: что неправильно в этой функции?

#include <iostream.h>

void myFunc(unsigned short int x);

int main()

{

unsigned short int x, y;

y = myFunc(int);

cout << "x: " << x << " y: " << y << "\n";

}

void myFunc(unsigned short int x)

{

return (4-х);

}

4. Жучки: что неправильно в этой функции?

#include <iostrearc.h>

int myFunc(unsigned short int x);

int main()

{

unsigned short int x, у;

у = myFunc(x);

cout << "x: " << x << " у: " << у << "\n";

}

int myFunc(unsigned short int x);

{

return (4*x);

}

5. Напишите функцию, которая принимает два параметра типа unsigned short int и возвращает результат деления первого параметра на второй. Функция не должна выполнять операцию деления, если второе число равно нулю, но в этом случае она должна возвратить значение -1.

6. Напишите программу, которая запрашивает у пользователя два числа и вызывает функцию, записанную при выполнении упражнения 5. Выведите результат или сообщение об ошибке, если функция возвратит значение, равное -1.

7. Напишите программу, которая запрашивает число и показатель степени. Напишите рекурсивную функцию, которая возводит число в степень путем многократного умножения числа на самое себя, т.е. если число равно 2, а показатель степени равен 4, то эта функция должна возвратить число 16.

День 6-й. Базовые классы

Базовые классы расширяют встроенные средства языка C++, что способствует решению сложных проблем, которые ставит перед программистами реальная жизнь. Сегодня вы узнаете:

• Что представляют собой классы и объекты

• Как определить новый класс и создать объекты этого класса

• Что представляют собой функции-члены и переменные-члены

• Что такое конструктор и как его использовать

Создание новых типов

Вы уже познакомились с типами переменных, включая беззнаковые целые и символы. Тип переменной несет в себе немало информации. Например, если объявить переменные Height и Width как беззнаковые короткие целые (unsigned short int), то каждая из них сможет хранить целое число в диапазоне 0—65 535, занимая при этом только два байта. Если же вы попытаетесь присвоить такой переменной значение, отличное от беззнакового целого числа, то получите сообщение об ошибке. Это значит, что с помощью такой переменной вы не сможете хранить свое имя, так что даже и не пытайтесь сделать это.

Лишь объявив переменные Height и Width беззнаковыми короткими целыми, вы получаете возможность сложить их или присвоить одной из них значение другой. Итак, тип переменной определяет:

• ее размер в памяти;

• тип данных, которые она может хранить;

• операции, которые могут выполняться с ее участием.

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

Зачем создавать новый тип

Программы обычно пишут для решения таких реальных проблем, как отслеживание информации о служащих или имитация работы отопительной системы. И хотя решать сложные проблемы можно с помощью программ, написанных только с использованием одних целочисленных значений и символов, решения выглядели бы значительно проще, если бы можно было создавать уникальные типы для различных объектов. Другими словами, имитацию работы отопительной системы было бы гораздо легче реализовать, если бы можно было создавать переменные, представляющие помещения, тепловые датчики, термостаты и бойлеры. И чем ближе эти переменные соответствуют реальности, тем легче написать такую программу.

Классы и члены классов

Новый тип создается путем объявления класса. Класс — это просто коллекция переменных (причем часто различных типов), скомбинированная с набором связанных функций.

Автомобиль можно представлять себе по-разному, например как коллекцию, состоящую из колес, дверей, сидений, окон и т.д. Или же, думая об автомобиле, можно представить себе его способность двигаться, увеличивать скорость, тормозить, останавливаться, парковаться и т.д. Класс позволяет инкапсулировать различные запчасти автомобиля и его разнообразные функции в одну коллекцию, которая называется объектом.

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

Класс может состоять из любой комбинации типов переменных, а также типов других классов. Переменные в классе называют переменными-членами или данными- членами. Класс Car может иметь переменные-члены, представляющие сидения, радиоприемник, шины т.д.

Переменные-члены, известные также как данные-члены, принадлежат только своему классу. Переменные-члены — это такие же составные части класса, как колеса и мотор — составные части автомобиля.

Функции в классе обычно выполняют действия над переменными-членами. Они называются функциями-членами или методами класса. В число методов класса Car могут входить Start() и Break(). Класс Cat может иметь такие данные-члены, которые представляют возраст и вес животного, а функциональная часть этого класса может быть представлена методами Sleep(), Meow() и ChaseMice().

Функции-члены принадлежат своему классу, как и переменные-члены. Они оперируют переменными-членами и определяют функциональные возможности класса.

Объявление класса

Для объявления класса используйте ключевое слово class, за которым следует открывающая фигурная скобка, а за ней — список данных-членов и методов класса. Объявление завершается закрывающей фигурной скобкой и точкой с запятой. Вот, например, как выглядит объявление класса Cat:

class Cat

{

unsigned int itsAge;

unsigned int itsWeight;

void Meow();

};

При объявлении класса Cat память не резервируется. Это объявление просто сообщает компилятору о существовании класса Cat, о том, какие данные он содержит (itsAge и itsWeight), а также о том, что он умеет делать (метод Meow()). Кроме того, данное объявление сообщает компилятору о размере класса Cat, т.е. сколько места должен зарезервировать компилятор для каждого объекта класса Cat. Поскольку в данном примере для целого значения требуется четыре байта, то размер объекта Cat составит восемь байтов (четыре байта для переменной itsAge и четыре — для itsWeight). Метод Meow() не требует выделения памяти в объекте.

Наши рекомендации