Студопедия
Случайная страница | ТОМ-1 | ТОМ-2 | ТОМ-3
АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатика
ИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханика
ОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторика
СоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансы
ХимияЧерчениеЭкологияЭкономикаЭлектроника

Перегрузка операторов

Читайте также:
  1. Дата и номер приказа Ростуризма о внесении сведений в единый федеральный реестр туроператоров
  2. Квалиметрия технико-эргономических свойств подсистемы операторов
  3. Перегрузка конструктора
  4. Перегрузка методов. Вызов методов с одинаковым именем и разными аргументами
  5. Пример 7.9. Перегрузка функций
  6. Структура VHDL описания и типы операторов

Понятие класса

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

Описание класса можно привести, например, в виде:

// Класс dataset

class NameDataSet

{

public:

char firstName[128];

char lastName [128];

int creditCard;

};

// экземпляр класса dataset

NameDataSet nds;

Объявление класса начинается с ключевого слова class, после которого идет имя класса и пара фигурных скобок, открывающих и закрывающих тело класса.

После ключевого слова public идет описание полей класса. Как видно из листинга, класс NaneDaraSet содержит поля имени, фамилии и номера кредитной карты. Первые два поля являются символьными массивами, а третье имеет тип int (будем считать, что это и есть номер кредитной карты).

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

Обратиться к членам класса можно так:

NameDataSet nds;

nds.creditCard = 10;

cin >> nds.firstName;

cin >> nds.lastNairve;

Здесь nds — экземпляр класса NameDataSet (или отдельный объект типа NameDataSet); целочисленная переменная nds.creditCard— свойство объекта nds; член nds.crecitCard имеет тип int, тогда как другой член этого объекта, nds.firstName, имеет тип char. Видно, что обращение к элементам класса производится так же, как и обращение к элементам структуры или объединения.

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

В этом примере используется потоковая операция >>, где в качестве источника данных используется стандартное устройство ввода cin (обычно это клавиатура). Возможна также потоковая операция вывода <<, где в качестве получателя можно использовать стандартное устройство вывода cout (обычно это экран дисплея).

Формы описания полей класса

В нашем примере при описании полей класса мы использовали ключевое слово public. Существуют следующие формы описания полей класса, определяющие также права доступа к нему:

public – член класса, доступный извне этого класса, а также и для других членов того же класса.

private – член класса, доступный только для других членов того же класса (используется по умолчанию).

protected – член класса, доступный только для членов этого класса и его подклассов. Защищенные члены класса не являются общедоступными.

friend – функция или класс, которые не являются членом данного класса, однако имеют доступ к защищенным членам класса.

Основные свойства классов и их элементов

1. Инкапсуляция – это объединение данных и обрабатывающих их функций (методов) в одном классе как типе объектов. Методы могут быть определены внутри класса. Такая форма описания делает метод встроенным по умолчанию. Второй метод описания заключается в том, что внутри класса описывается прототип, а описание метода размещается вне тела класса. Пример класса суммы чисел:

class Sum

{

int x,y,s;

public:

void getx(int x1) { x=x1; } // описание метода

void gety(int y1) { y=y1; } // описание метода

void summa(); // прототип метода

};

// описание метода

void Sum::summa()

{

s=x+y;

cout<<”\nСумма “<<x<<” и ”<<y<<” равна: ”<<s;

}

Под переменные x, y, s при описании каждого объекта класса Sum отводится динамическая память. То есть, эти компоненты в каждом из объектов будут располагаться в разных ячейках памяти. Можно описать также статические объекты, которые будут доступны во всех объектах данного класса. В этом случае память под данный объект не выделяется, и память под такие объекты надо выделять вручную. Например, пусть в классе Student мы описали объект

static int noOfStudents;

Мы должны сами выделить для нее место, например, с помощью оператора

int Student::noOfStudents=0;

Статическими могут быть объявлены и функции. Понятно, что они не могут прямо обращаться к компонентам этого класса, так как эти компоненты принадлежат конкретным объектам данного класса.

2. Наследование – это способность классов C++ перенимать свойства уже существующих классов. То есть, если при определении класса он включает в себя другой класс, то мы получаем доступ к объектам и этого класса.

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

Конструкторы и деструкторы

Объект можно проинициализировать на этапе его объявления, как сделал бы программист на С:

struct Student

{

int semesterHours;

double gpa;

};

void fnf)

{

Student s = {0,0};

//...продолжение функции...

}

Этот фрагмент кода не будет работать для настоящего класса C++, поскольку внешнее приложение не имеет доступа к защищенным членам класса. Приведенный ниже фрагмент некорректен.

class Student

{

public:

//...открытые члены...

protected:

int semesterHours;

double gpa;

};

void fn()

{

Student s = {0,0); // неправильно, так как

// данные-члены недоступны

//...продолжение функции...fn

}

В данном случае функция — не член fn() не имеет права обращаться к членам semesterHours и gpa, поскольку они являются защищенными (а функция не объявлена в качестве друга класса).

Можно использовать специально сконструированную функцию класса Student для определения начальных значений объектов, но это неудобно. Лучше использовать конструктор – это специальная функция-член, которая автоматически вызывается во время создания объекта. Конструктор должен иметь то же имя, что и класс. Таким образом компилятор определяет, что именно эта функция-член является конструктором. Еще одним свойством конструктора является то, что он не возвращает никакого значения, поскольку вызывается автоматически.

Класс с использованием конструктора продемонстрирован в следующем примере:

#include <iostream.h>

class Student

{

public:

Student()

{

сout << "Конструируем объект Student\n";

semesterHours = 0;

gpa = 0.;

}

//...остальные открытые члены...

protected:

int semesterHours;

double gpa;

}

void fn()

{

Student s; // создаем и инициализируем объект

//...продолжение функции...

}

В этом примере компилятор сам вызывает конструктор student::student() в том месте, где объявляется объект s.

Этот простой конструктор реализован в виде встроенной (inline) функции. Конструктор можно создать и как обычную функцию с телом, вынесенным из объявления класса:

#include <iostream.h>

class Student

{

public:

Student();

//...остальные открытые члены...

protected:

int semesterHours;

double gpa;

};

Student::Student()

{

cout << "Конструируем Student\n";

semesterHours = 0;

gpa = 0.0;

}

void fn ()

{

Student s; // создаем и инициализируем объект

//...продолжение функции...

}

int main(int arges, char *pArgs[])

{

fn();

return 0;

}

Конструктор может быть запущен только автоматически. Нельзя вызывать конструктор как обычную функцию-член, а значит, и повторно инициализировать объект класса Student:

void fn()

{

Student s; // создаем и инициализируем объект

//...продолжение функции...

s.Student(); // пытаемся реинициализировать его,

// но компилятор сообщит об ошибке

}

Объявление конструктора не должно содержать тип возвращаемого значения, даже типа void. Если класс имеет данные-члены, которые являются объектами другого класса, конструкторы для этих объектов также будут вызваны автоматически.

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

#include <iostream.h>

#include <string.h>

class Student

{

public:

Student(char *pName = "no name")

{

cout << "Создаем нового студента "<< pName << "\n";

strncpy(name, pName, sizeof(name));

name[sizeof(name) - 1] = '\0';

}

Student(Student &s)

{

cout << "Создаем копию от " << s.name << "\n";

strcpy(name, "Копия ");

strcat(name, s.name);

}

~Student()

{

cout << "Ликвидируем " << name << "\n";

}

protected:

char name[40];

};

class Tutor

{

public:

Tutor(Student &s):student(s)

// вызываем копирующий конструктор

// для члена student

{

cout << "Создаем учителя\n";

}

protected:

Student student;

};

void fn(Tutor tutor)

{

cout << "Это сообщение из функции fn()\n";

}

int main(int arges, char *pArgs[])

{

Student randy("Randy");

Tutor tutor(randy);

cout << "Вызываем функцию fn()\n";

fn(tutor);

cout << "Вернулись из функции fn()\n";

return 0;

}

Запуск этой программы приведет к выводу таких сообщений:

Создаем нового студента Randy

Создаем копию от Randy

Создаем учителя

Вызываем функцию fn()

Создаем копию от Копия Randy

Это сообщение из функции fn()

Ликвидируем Копия Копия Randy

Вернулись из функции fn(}

Ликвидируем Копия Randy

Ликвидируем Randy

Конструирование объекта randy приводит к вызову конструктора student, который выводит первое сообщение. Объект tutor создается с использованием конструктора Tutor(Students). Этот конструктор инициализирует член Tutor::student, вызывая копирующий конструктор для student. Он и выводит вторую строку с сообщением. Вызов функции fn() требует создания копии tutor. Поскольку Tutor не обеспечен копирующим конструктором, каждый член объекта tutor копируется посредством конструктора, созданного C++ по умолчанию. При этом, как отмечалось ранее, для копирования члена tutor.student вызывается конструктор копирования класса Student.

Копирующий конструктор, создаваемый в С++ по умолчанию, просто копирует адрес исходного объекта в конечный объект.

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

Деструктор имеет то же имя, что и класс, но только с предшествующим ему символом тильды (~). Как и конструктор, деструктор не имеет типа возвращаемого значения. С учетом сказанного деструктор класса Student будет выглядеть так:

class Student

{

public:

Student()

{

semesterHours = 0;

gpa = 0.0;

}

~Student()

{

//любые используемые ресурсы освобождаются здесь

}

//...остальные открытые члены...

protected:

int semesterHours;

double gpa;

};

Деструктор вызывается автоматически, когда объект уничтожается или, если говорить языком C++, происходит его деструкция. Можно также сказать "когда объект выходит из области видимости". Локальный объект выходит из области видимости, когда функция, создавшая его, доходит до команды return. Глобальный или статический объект выходит из области видимости, когда прекращается работа программы. Если уничтожается более одного объекта, деструкторы вызываются в порядке, обратном вызову конструкторов. То же касается и случая, когда уничтожаемый объект содержит объекты-члены.

C++ позволяет программисту определить конструктор с аргументами, например:

#include <iostream.h>

#include <string.h>

class Student

{

public:

Student(char *pName)

{

сout << "Конструируем студента" << pName << "\n";

strncpy(name, pName, sizeof(name));

name[sizeof(name) - 1] = '\0';

}

//...остальные открытые члены...

protected:

char name[40];

int semesterHours;

double gpa;

};

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

Идея использования аргументов проста. Как известно, функции-члены могут иметь аргументы, поэтому конструктор, будучи функцией-членом, тоже может иметь аргументы. При этом нельзя забывать, что конструктор вызывается не как нормальная функция и передать ему аргумент можно только в момент создания объекта. Так, приведенная ниже программа создает объект s класса student, вызывая конструктор Student (char *). Объект s уничтожается в момент возврата из функции main ().

#include <iostream.h>

#include <string.h>

class Student

{

public:

Student(char *pName)

{

cout << "Конструируем студента " << pName << "\n";

strncpy (name, pName, sizeof (name));

name[sizeof(name) - 1] = '\0';

semesterHours = 0;

gpa = 0.0;

}

~Student()

{

cout << "Ликвидируем " << name << "\n";

// неплохо бы стереть имя уничтожаемого студента,

name[0] = '\0';

}

//...остальные открытые члены...

protected:

char name[40];

int semesterHours;

double gpa;

};

int main(int arges, char *pArgs[])

{

Students("Danny"); // Создаем студента Дэнни

return 0;

} //а теперь “избавимся” от него

Конструкторы можно перегружать. Это означает, что определено несколько функций с одинаковым именем, но разными типами аргументов. C++ выбирает вызываемый конструктор, исходя из аргументов, передаваемых при объявлении объекта. Например, для класса Student определим три конструктора:

#include <iostream.h>

#include <string.h>

class Student

{

public:

Student()

{

сout << "Создаем студента без имени\n";

semesterHours = 0;

gpa = 0.0;

name[0] = '\0';

}

Student (char *pName)

{

сout << "Создаем студента " <<" pName << "\n";

strncpy (name, pName, sizeof(name));

name [sizeof(name) – 1] = ' \0 ';

semesterHours = 0;

gpa = 0.0;

}

Student (char *pName, int xfrHours, double xfrGPA)

{

cout << "Создаем студента " << pName << "\n";

strncpy (name, pName, sizeof(name));

name [sizeof(name) - 1 ] ='\0';

semesterHours = xfrНоurs;

gpa = xfrGPA;

}

~Student()

{

cout << "Ликвидируем студента\n";

}

//...остальные открытые члены...

protected:

char name[40];

int semesterHours;

double gpa;

};

// приведенный ниже фрагмент по очереди

// вызывает каждый из конструкторов

int main (int arges, char *pArqs[])

{

Student noName;

Student freshMan("Smell E. Fish");

Student xfer("Upp R. Classman", 80, 2.5);

return 0;

}

Можно заметить, что все три конструктора очень похожи. Добавив значения по умолчанию в последний конструктор, все три можно объединить в один, что и сделано в приведенном ниже коде:

#include <iostream.h>

#include <string.h>

class Student

{

public:

Student(char *pName ="no name",

int xfrHours = 0,

float xfrGPA = 0.0)

{

cout << "Создаем студента " << pName << "\n";

strncpy{name, pName, sizeof(name));

name[sizeof(name) - 1] = '\0';

semesterHours = xfrHours;

gpa = xfrGPA;

}

~Student()

{

cout << "Ликвидируем студента\n";

}

//...остальные открытые члены...

protected:

char name[40];

int semesterHours;

double gpa;

}

int main(int arges, char *pArgs[])

{

Student noName;

Student freshMan("Smell E. Fish");

Student xfer("Upp R. Classman", 80, 2.5);

return 0;

}

Теперь все три объекта строятся с помощью одного и того же конструктора, а значения по умолчанию используются для аргументов, отсутствующих в объектах freshMan и noName.

Для обеспечения совместимости с существующим кодом С, который ничего не знает о конструкторах, C++ автоматически создает конструктор по умолчанию, который инициализирует все данные-члены объекта нулями.

Наследование

Механизм наследования позволяет определять новые классы на основе уже имеющихся. Класс, на основе которого создается новый класс, называют базовым (родительским), а новый – производным (наследником). При наследовании важную роль играет статус доступа к компонентам класса.

Используются следующие соглашения:

– private-компоненты доступны только внутри того класса, в котором они определены;

– protected-компоненты доступны внутри того класса, в котором они определены, а также во всех производных классах;

– public-компоненты видны во всех частях программы.

При описании можно изменить статус доступа к наследуемым компонентам (только в направлении ужесточения). Формат описания производного класса с единичным наследованием имеет вид:

class имя_производного_класса: [модификатор] имя_базового_класса

{компоненты_производного_класса};

В качестве модификатора могут использоваться ключевые слова private, protected, public. Статусы доступа производных классов приведены ниже в таблице.

Пример:

class Circle: public Point

{

// элементы производного класса Circle

}

В данном случае мы получаем при использовании класса Circle доступ к элементам класса Point.

Формат описания производного класса с множественным наследованием имеет вид:

class имя_производного_класса: [модификатор] имя_базового_класса_1,…,

[модификатор] имя_базового_класса_n

{компоненты_производного_класса};

Полиморфизм

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

– перегрузку функций и операций;

– виртуальные функции;

– обобщенные функции или шаблоны.

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

int max(int,int);

double max(double,double);

В этом случае возможно использование программы:

double x,y;

int x1,y1;

x=12.7; y=2.e3;

x1=5; y1=3;

cout<<”Максимум ”<<max(x,y)<<endl;

cout<<”Максимум ”<<max(x1,y1)<<endl;

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

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

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

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

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

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

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

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

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

class parent

{ //объявление базового класса

public:

int k;

parent(int a) // конструктор класса

{

k=a,

}

virtual void fv() // виртуальная функция

{

cout<<”Вызов fv базового класса: ”;

cout<<k<<endl;

}

void f () //обычная функция

{

cout<<"Вызов f () базового класса: “;

cout<<k*k<<endl;

}

};

class childl: public parent

{

public:

childl(int x): parent(x) {}

void fv()

{

cout<<"Вызов fv() из производного класса childl: ";

cout<<(k+1)<<endl;

}

void f() //обычная функция

{

cout<<”Вызов f0 из производного класса childl: “;

cout<<(k-1)<<endl;

}

};

int main()

{

parent *p; // указатель на базовый класс

parent ez(5); // объявление объекта базового класса

childl chezl(15); // объявление объекта производного класса

p = &ez; // указатель ссылается на объект базового класса

p->fv(); // вызов функции fv() базового класса

p->f(); // вызов функции f() базового класса

р = &chezl; // указатель ссылается на объект производного класса

p->fv(); // вызов виртуальной функции fv() производного класса

p->f(); // вызов обычной функции f() производного класса

return 0;

}

При выполнении на экран будут выданы следующие результаты:

Вызов fv() базового класса: 5

Вызов f() базового класса: 25

Вызов fv() из производного класса childl: 16

Вызов f() базового класса: 225

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

virtual тип имя_функции (список параметров) = 0;

Присвоение имени функции значения 0 указывает на отсутствие тела функции.

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

Шаблон создается при помощи ключевого слова template и угловых скобок со списком параметров, непосредственно за которым следует описание класса или функции:

template <class фиктивное_имя_1, class фиктивное_имя_2, …> определение;

Вместо ключевого слова class допускается использование ключевого слова typename.

Рассмотрим пример:

template <class T> T max(T x, T y)

{

return (x>y) x: y;

};

void main(void)

{

int j=max(10,5);

cout<<”j=”<<j<<endl;

double d=max(12.09,17.03);

cout<<”d=”<<d<<endl;

}

Результатом работы данной программы будет:

j=10

d=17.03

Перегрузка операторов

Вообще, оператор – это встроенная функция с определенным синтаксисом. Мы не можем переопределить операторы для уже существующих типов данных, но можем определить их для своих классов. Определим операторы для типа “российская денежная единица” – рубли и копейки:

// класс “денежная единица”, состоящая из рублей и копеек

class RURb

{

friend RURb operator+(RURb&, RURb&);

friend RURb operator++(RURb&);

public:

RURb(unsigned int a, unsigned int b);

protected:

unsigned int rub;

unsigned int kop;

};

// конструктор

RURb::RURb(unsigned int a, unsigned int b)

{

rub=a;

kop=b;

while (kop>99)

{

rub++;

kop-=100;

}

}

// оператор + складывает a и b и возвращает результат

// в виде нового объекта

RURb operator+(RURb& a, RURb& b)

{

unsigned int rub=a.rub+b.rub;

unsigned int kop=a.kop+b.kop;

RURb c(rub,kop);

return c;

}

// оператор ++ увеличения на 1 копейку

RURb operator++(RURb& a)

{

a.kop++;

while (a.kop>99)

{

a.rub++;

a.kop-=100;

}

}

// использование

int main(int arges, char *pArgs)

{

RURb d1(1,60); // конструкторы

RURb d2(2,50);

RURb d3(0,0);

d3=d1+d2; // действия

++d3;

return 0;

}

Оператор можно сделать и нестатической функцией-членом.


Дата добавления: 2015-10-28; просмотров: 86 | Нарушение авторских прав


<== предыдущая страница | следующая страница ==>
Исходные данные| MICROSOFT WORD

mybiblioteka.su - 2015-2024 год. (0.093 сек.)