Розовая методичка. Огнева М. В., Кудрина Е. В. Структуры данных и алгоритмы программирование на языке c
Скачать 1.93 Mb.
|
Магические числа Магическими в программировании принято называть числовые значения, появляющиеся в программе с целью, понятной только их создателю. Если вы видите в чужой программе числовое значение и не можете мгновенно сказать, зачем оно здесь нужно, знайте — перед вами «магическое число» (или неименованная константа). Использование в программе магических чисел несет в себе три потенциальных угрозы. Во-первых, когда не ясно, зачем это число нужно, то при модификации программы придется затратить некоторое время на выяснение вопроса, что будет, если его поменять. Во-вторых, если одно и то же магическое число встречается в программе несколько раз, то, при необходимости его изменить, придется внимательно просматривать весь код. В- третьих, если в коде встречаются два магических числа с одинаковыми значениями, то потребуется провести целый эксперимент, чтобы установить, как изменение одного магического числа влияет на второе и на программу в целом. 9 Избежать использования этих чисел можно только заменой их константами. Сравните, например, два фрагмента кода и ответьте на вопрос, какой из них понятнее. Фрагмент 1: if (pow(x - 1 , 2 ) + pow(y - 1 , 2 ) = = pow(2, 2)) { cout << "yes" ; } else { cout << "no" ; } Фрагмент 2: //центр заданной окружности int а = 1; int b = 1; //радиус заданной окружности int radius = 2; if (pow(x - a, 2) + pow(y - b, 2) == pow(radius, 2)) { cout << "yes" ; } else { cout << "no" ; } 3. КЛАССЫ И ОБЪЕКТЫ 3.1. Основные понятия Определение класса выглядит следующим образом: class <имя класса> { <тело класса> }; Таким образом, определение класса начинается с ключевого слова class, за которым следует идентификатор, обозначающий имя данного класса. Затем в фигурных скобках определяется тело класса. Завершается описание класса точкой с запятой. В теле класса (которое может быть и пустым, тогда это пустой класс) определяют данные и операции. Данные и операции, являющиеся частью класса, называются членами класса. Данные также называются членами- данными (полями), а операции - членами- функциями. Класс может содержать любое количество разделов, помеченных модификаторами доступа private (закрытый), public (открытый), protected 10 (защищенный). Модификаторы доступа private и public указывают, будет ли член класса доступен вне объекта данного класса; protected будет рассмотрен позже. Модификатор доступа остается в силе до тех пор, пока в описании не встретится другой модификатор или не закончится описание класса. По умолчанию все члены класса автоматически считаются закрытыми. Рассмотрим пример класса MyClass, который содержит поле data и две функции: SetData - которая задает значение поля data, и ShowData, которая отображает значение поля data. class MyClass { private : int data; public : void SetData( int d) { data = d; } void ShowData() { cout << "Data= " << data << endl; } }; Так как для компилятора класс — это пользовательский тип данных, то для работы с классом необходимо создать экземпляр класса (объект), аналогично тому как создается (объявляется) переменная требуемого типа. Например: int х; //объявление целочисленной переменной MyClass у; //создание экземпляра клacca Чтобы обратиться к члену класса, необходимо указать имя объекта, точку, а затем - имя функции или поля класса: int main() { //объявление объектов класса MyClass MyClass firstObject; MyClass secondObject; firstObject.SetData(5); //вызов метода SetData для первого объекта secondObject.SetData(10); //вызов метода SetData для второго объекта firstObject.ShowData(); //вызов метода ShowData для первого объекта secondObject.ShowData(); //вызов метода ShowData для второго объекта return 0; } Результат работы программы: Data= 5 Data= 10 В данном примере мы объявили два объекта класса MyClass: firstObject и secondObject. Поскольку поле data класса MyClass помечено модификатором 11 доступа private, невозможно обратиться к нему извне класса, в частности, из функции main. Например, невозможно присвоить значение данному полю напрямую: firstObject.data=5; //Ошибка!!! Вместо этого мы должны использовать метод SetData. Возможность сокрытия данных является ключевой особенностью ООП. Этот термин понимается в том смысле, что данные, помеченные модификатором private, доступны только внутри класса. Сокрытие данных позволяет избежать многих ошибок. Приведем такой пример. Пусть какое-то поле класса может принимать значения из интервала от 0 до 10. Если присвоить ему значение, меньшее 0 или большее 10, функции класса будут работать некорректно. В том случае, когда присвоение значения полю класса возможно только с помощью функции того же класса, данное ограничение будет заложено внутри функции. Например, при вводе некорректного значения, оно будет считаться нулем, или пользователя попросят ввести значение еще раз. Если же доступ к этому полю будет осуществляться из любого другого места программы, то нет никакой гарантии, что ограничение будет соблюдаться. Объекты класса разрешается определять в качестве полей другого класса. Объекты класса можно передавать в качестве аргументов любыми способами (по значению, по указателю, по ссылке) и получать как результат функции. Итак, предположим, что мы определили класс. По завершении определения все члены класса должны быть известны. В результате объем памяти, необходимый для хранения объекта данного класса, тоже будет известен. Класс можно объявить, но не определять, указав только его имя. Например, class myClass ; //объявление класса Такое объявление иногда называют предварительным. После объявления, но до определения данный класс является незавершенным типом, т.е. известно, что это класс, но неизвестно, что он содержит. Объект такого класса создать нельзя. Как правило, предварительное объявление используют, когда необходимо создать взаимозависящие классы. Замечание. Возможность определения членов класса вне определения класса, а также возможность создания взаимозависящих классов изучите самостоятельно, используя дополнительную литературу [7, 8, 11]. Функции класса находятся в памяти в единственном экземпляре и используются всеми объектами одного класса совместно, поэтому необходимо обеспечить работу функций с полями именно того объекта, для которого они были вызваны. Для этого в любую функцию класса автоматически передается скрытый параметр this, в котором хранится ссылка на вызвавший функцию экземпляр класса. В явном виде параметр this применяется для того, чтобы возвратить из функции ссылку на вызвавший объект, а также для идентификации поля в случае, если его имя совпадает с именем параметра функции, например: 12 void SetData( int data) { this ->data = data; } 2.5. Конструкторы Для инициализации полей объекта класса MyClass мы использовали функцию SetData. Её необходимо вызывать явно каждый раз при создании соответствующего объекта. Существует вероятность того, что программист забудет вызвать данную функцию, и поле не будет проинициализировано, что приведет к неправильной работе программы. Для решения данной проблемы используется конструктор. Конструктор - это специальная член-функция, которая выполняется каждый раз, когда создается новый объект класса. Конструктор предназначен для того, чтобы присвоить каждому члену-данному начальное значение. Конструктор отличается от других членов-функций тем, что его имя совпадает с именем класса. Кроме того, конструкторы не имеют типа возвращаемого значения, но имеют список параметров (который может быть пуст) и тело. Каждый класс может иметь несколько конструкторов, которые должны отличаться друг от друга количеством или типом параметров. Параметрами конструктора являются значения, используемые для инициализации членов- данных класса при создании объекта. Общий вид конструктора следующий: <имя конструктора> (<список параметров>): <имя поля 1> (<инициализирующее значение для поля 1 >),...,<имя поля N> (<инициализирующее значение для поля N>) { <тело конструктора> } Для нашего класса можно определить следующий конструктор: MyClass(): data(0) { } В данном случае поле data будет инициализироваться значением 0. Тогда определение класса выглядит следующим образом: class MyClass { private : int data; public : MyClass(): data(0) {} 13 void SetData ( int data) { this ->data = data; } void ShowData() { cout << "Data= " << data << endl; } }; Работа с этим классом: int main() { MyClass firstObject; MyClass secondObject; firstObject.ShowData(); secondObject.ShowData(); return 0; } Результат работы программы: Data= 0 Data= 0 Обратите внимание на две особенности данного конструктора: 1. Конструктор содержит пустой список параметров, поэтому он вызывается неявным образом в момент создания объекта (экземпляра класса). Конструктор с непустым списком параметров будет рассмотрен позже 2. Конструктор должен быть расположен в разделе public. Если бы он был расположен в разделе private, то создать объект такого класса было бы невозможно. Конструктор и другие члены-функции класса можно перегружать как обычные функции. Рассмотрим, например, конструктор класса MyClass, который будет инициализировать поле data заданным значением. MyClass( int d): data(d) { } В данном случае используется конструктор с параметром d, который используется для инициализации поля data. Допустимо, чтобы имя параметра совпадало с именем поля. Например: MyClass( int data): data(data) { } Теперь определение класса выглядит следующим образом: 14 class MyClass { private : int data; public : MyClass(): data(O) //конструктор 1 { } MyClass( int data): data(data) //конструктор 2 { } void SetData( int data) { this ->data = data; } void ShowData() { cout << "Data= " << data << endl; } }; Работа с данным классом: int main() { MyClass firstObject; MyClass secondObject(10); firstObject.ShowData(); secondObject.ShowData(); return 0; } Результат работы программы: Data= 0 Data= 10 Замечание В том случае, когда в классе не определен ни один конструктор, компилятор создает так называемый синтезируемый стандартный конструктор. Синтезируемый стандартный конструктор инициализирует члены-данные по тем же правилам, что и обычные переменные, а именно: 1) члены-данные, являющиеся объектами класса, инициализируются их собственным конструктором; 2) члены-данные встроенного или составного типа, такие как указатели или массивы, инициализируются только для тех объектов, которые определены в глобальной области видимости; 3) объекты встроенных типов (например, типа int) или составных типов, определенные в локальной области видимости, остаются неинициализированными, следовательно, использовать их для чего-либо отличного от присвоения (или ввода) значения нельзя 15 3.3. Деструкторы Как мы уже видели, существует особая функция класса - конструктор, которая вызывается автоматически при создании объекта. Существует еще другая функция, автоматически вызываемая при уничтожении объекта и называемая деструктором. Напомним, что уничтожение локальных объектов происходит при выходе из блока, а для объектов, которые создаются посредством операции new, - при вызове операции delete. Деструктор имеет имя класса, перед которым стоит значок (тильда). Деструктор не возвращает результат и не имеет параметров. Деструктор в классе может быть один - перегрузка деструкторов не разрешается. Если деструктор не определен явно, он создается системой автоматически. Рассмотрим пример деструктора: MyClass() { cout << "Delete: data= " << data; } Выполним предыдущую реализацию функции main еще раз. Результат работы программы: Data=0 Data=10 Delete: data=10 Delete: data=0 Задание Попробуйте объяснить, почему дескрукторы были вызваны именно в таком порядке. Обычно деструктор явно используется только тогда, когда при уничтожении необходимо выполнить некоторые специальные действия, например, закрыть файл, открытый в конструкторе, или освободить память, выделенную динамическим образом под некоторый член-данных. Если же ваш класс не используется для обеспечения доступа к какому-либо системному ресурсу, использование деструкторов настоятельно не рекомендуется. 3.4. Статические члены класса Зачастую случается такая ситуация, когда, объект класса должен располагать информацией, сколько еще объектов этого класса существует на данный момент. Например, если объектами являются участники соревнований, необходимо, чтобы каждый участник знал о том, сколько всего человек участвуют в соревнованиях. В этом случае необходимо использовать статическую переменную - член класса. Эта переменная будет видна всем объектам данного класса и для всех будет иметь одинаковое значение. Статическое поле описывается с помощью ключевого слова static. Статическое поле существует даже в том случае, когда не создано ни одного объекта класса. Поэтому статические поля нельзя инициализировать в конструкторе. Определение начального значения статических полей происходит вне класса (после определения класса) и выглядит следующим образом: 16 <тип поля> <имя класса>::<Имя поля> = <инициализатор>; или <тип поля> <имя класса>::<имя поля>; Рассмотрим пример использования класса MyClass, в котором определим статическое поле count для подсчета количества объектов данного класса: #include "iostream" using namespace std; class MyClass { private : int data; static int count; //объявление статического поля public : MyClass(): data(O) { count++; //увеличение значения статического поля } MyClass( int data): data(data) { count++; //увеличение значения статического поля } int GetCount() //функция, возвращающая значение статического поля { return count; } void SetData ( int data) { this ->data = data; } void ShowData() { cout << "Data= " << data << endl; } }; int MyClass ::count = 0 ; //определение начального значения статического поля int main() { MyClass firstObject; MyClass secondObject(10); cout << "Count= " << firstObject.GetCount() << endl; cout << "Count= " << secondObject.GetCount() << endl; return 0; } 17 Результат работы программы: Count= 2 Count= 2 Таким образом, значение счетчика одинаково для любого объекта данного класса. На самом деле, начальное значение счетчику присвоено до создания экземпляров класса, а каждый раз при объявлении объекта данного класса неявно вызывается конструктор, который увеличивает это значение на 1. В данном примере было объявлено два объекта MyClass, следовательно конструктор класса вызывался два раза, именно поэтому значение счетчика равно 2. 3.5. Перегрузка операций Перегрузка операций в C++ позволяет переопределить большинство операций так, чтобы при использовании их объектами конкретного класса, выполнялись действия, отличные от стандартных. Это дает возможность применять объекты собственных типов данных в составе выражений, например: MyClass х, у, z; … z = х + у; //используется операция сложения, переопределенная для класса MyClass Перегрузка операций обычно применяется для классов, для которых семантика операций делает программу более понятной. Если назначение операции интуитивно непонятно, перегружать такую операцию не рекомендуется. Начнем изучение перегрузки с унарных операций. Унарные операции имеют только один операнд. Примерами унарных операций могут служить операции инкремента (++) и декремента (--). Синтаксис перегрузки унарной операции: <тип результата> operator <унарная_операция>() { <реализация перегрузки> } Добавим в раздел public класса MyClass следующую реализацию перегрузки операции инкремент. void operator ++() { ++data; } Продемонстрируем работу данной операции. int main() { MyClass firstObject; firstObject.ShowData(); ++firstObject; firstObject.ShowData(); return 0; } 18 Результат работы программы: Data= 0 Data= l Однако при попытке выполнить следующие команды: MyClass firstObject; MyClass secondObject = ++firstObject; //Ошибка!!! компилятор выдаст сообщение об ошибке «невозможно преобразовать void в MyClass». Почему? Потому, что мы определили тип результата при перегрузке операции инкремента как void. Для того чтобы иметь возможность использовать предложенную нами версию перегрузки инкремента, нам необходимо правильно определить тип возвращаемого значения. Это можно сделать следующим образом: MyClass operator ++() { return MyClass (++data); } Здесь функция operator++ вначале увеличивает на единицу свое поле data, а затем создает новый объект класса MyClass, используя новое значение поля data для инициализации. В качестве результата функция operator++ возвращает ссылку на новый объект. Продемонстрируем работу перегрузки операции на примере: int main() { MyClass firstObject; MyClass secondObject = ++firstObject; ++secondObject; firstObject.ShowData(); secondObject.ShowData(); return 0; } Результат работы программы: Data= l Data= 2 Задание. Мы определяли операцию инкремента, используя префиксную запись. Самостоятельно разработайте вариант для постфиксной записи инкремента. Аналогичным образом можно перегрузить бинарные операции, например, операцию сложения: MyClass operator +( MyClass temp) { return MyClass (data + temp.data); //1 } Рассмотрим реализацию данной перегрузки на примере: 19 int main() { MyClass firstObject (1); MyClass secondObject(2); MyClass thirdObject = firstObject + secondObject; thirdObject.ShowData(); return 0; } Результат работы программы: Data= 3 Объявление функции operator+() в классе MyClass выглядит следующим образом: MyClass operator +( MyClass temp) Эта функция возвращает значение типа MyClass и принимает один аргумент типа MyClass. В выражении : thirdObject = firstObject + secondObject; важно понимать, к каким объектам будут относиться аргументы и возвращаемые значения. Когда компилятор встречает это выражение, то он просматривает типы аргументов. Обнаружив только аргументы типа MyClass, он выполняет операции выражения, используя функцию класса MyClass operator+() по правилу: объект, стоящий с левой стороны от знака операции (в нашем случае firstObject), вызывает функцию перегрузки бинарной операции + . Объект, стоящий справа от знака операции, передается в функцию в качестве аргумента (в нашем случае secondObject). Операция возвращает значение, которое записывается в thirdObject. В функции operator+() (строка 1) мы имеем прямой доступ к левому операнду, используя поле data, так как именно этот объект вызывает функцию. К правому операнду мы имеем доступ как к аргументу функции, то есть temp.data. Таким образом, перегуженной операции требуется количество аргументов на один меньше, чем количество операндов, так как один из оперантов является объектом, вызывающим функцию перегрузки. Именно поэтому для функций перегрузки унарных операций не нужны аргументы. |