Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP_C++ / 09

.htm
Скачиваний:
20
Добавлен:
02.02.2015
Размер:
36.71 Кб
Скачать

09 - Типы данных, определяемые пользователем Содержание     Предыдущее занятие     Следующее занятие

Занятие 09 Типы данных, определяемые пользователем 1 Перечисления Тип перечисление (enumeration) определяет множество целых констант - элементов перечисления. Перечисление описывается служебным словом enum и разделенным запятыми списком элементов перечисления. Этот список заключен в фигурные скобки. По умолчанию первому элементу присваивается значение 0, а каждому последующему - на единицу больше, чем значение предыдущего элемента.

Различают именованные и неименованные перечисления. Неименованные перечисления связывают некоторый список констант со значениями:

enum { red, green, blue }; Значение может явно присваиваться элементу перечисления. Если каким-то из элементов значение присваивается, а каким-то нет, то значение тех элементов, которым присваивания не было, на единицу больше предыдущего:

enum { red = 1, green, blue }; // green == 2, blue == 3 Именованные перечисления задают уникальный целочисленный тип и могут использоваться как спецификация типа для определения переменных.

enum Colors { red, green, blue }; Colors c = blue; Присвоение переменной с целых значений (0, 1 или 2) приводит к появлению ошибки компиляции (в Visual C++) или предупреждения (в C++Builder).

Для перечисления, как и для всех типов, определяемых пользователем, можно перегружать операции, такие как ++ и --. Допустим, имеется тип перечисления:

enum DayOfWeek { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; Для перегрузки префиксной операции ++ в том же пространстве имен, где определен тип DayOfWeek, должна быть реализована функция.

DayOfWeek& operator++(DayOfWeek& d) { switch (d) { case Sunday: d = Monday; break; case Monday: d = Tuesday; break; case Tuesday: d = Wednesday; break; case Wednesday: d = Thursday; break; case Thursday: d = Friday; break; case Friday: d = Saturday; break; case Saturday: d = Sunday; } return d; } Для перегрузки постфиксной операции ++ необходимо реализовать функцию, объявление которой имеет следующий вид:

DayOfWeek operator++(DayOfWeek& d, int); В списке формальных параметров int - тип неиспользуемого параметра, наличие которого позволяет компилятору отличить постфиксную операцию от префиксной.

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

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

struct Country { char name [20]; double area; long population; } France; // переменная France типа Country В отличие от массивов, в операции присваивания структуре другой структуры того же типа происходит поэлементное копирование.

Country someCountry; someCountry = France; // Поэлементное копирование Обращение к конкретным элементам структуры происходит с помощью оператора "." (точка).

cout << someCountry.area; К объектам структуры часто обращаются через указатели, используя оператор -> (разыменование указателя на структуру). Если p - это указатель, то p->m эквивалентно (*p).m.

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

double density(const Country& c) { return c.population / c.area; } Для хранения заданного числа битов можно объявить член структуры специального вида, называемый битовым полем. Он должен иметь целый тип данных, со знаком или без знака:

struct Flags { unsigned int logical : 1; // битовое поле unsigned int tinyInt : 3; // очень маленькое целое }; После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. Битовые поля, определенные в теле структуры подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Доступ к битовому полю осуществляется так же, как к прочим элементам структуры. Аналогично можно использовать битовые поля при описании классов.

3 Объединения Объединение (union) - это структура (struct), у которой все элементы расположены по одному и тому же адресу. В конкретный момент может храниться значение только одного из элементов.

union FloatValue { float d; long bytes; }; // Анализ внутреннего представления float: FloatValue dv; dv.d = 100; cout << dv.bytes << endl; Анонимное объединение не имеет имени и не определяет типа.

union { int a; char* p; }; a = 1; p = "name"; // a и p бессмысленно использовать одновременно Класс с конструктором, деструктором и операцией копирования не может быть членом объединения. Объединение может содержать функции-элементы.

4 Классы Классы используются в С++ для определения пользовательских типов данных. Класс аналогичен структуре. В описание классов входят элементы данных (data members) и функции-элементы (member functions).

Важным признаком класса являются уровни доступа; элементы класса можно объявлять как закрытые (private) - доступные только из функций-элементов данного класса, защищенные (protected) - доступные только из функций-элементов данного класса и классов, производных от него, и открытые (public) - доступные отовсюду (через имя объекта класса). Доступ к закрытым элементам через открытые функции-элементы называется инкапсуляцией.

Приведем определение класса Country:

class Country { private: string name; double area; long population; public: string getName(); double getArea(); long getPopulation(); void setName(char* value); void setArea(double value); void setPopulation(long value); double density(); }; Функции-элементы getName(), getArea(), getPopulation(), setName(), setArea() и setPopulation() обеспечивают доступ к закрытым элементам данных класса. Функция-элемент density() используется для вычисления плотности населения.

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

double Country::density() { return population / area; } Функцию-элемент можно не только объявить, но и определить в теле класса. Такая функция является inline-функцией.

class Country { private: string name; double area; long population; public: string getName() const { return name; } double getArea() const { return area; } long getPopulation() const { return population; } void setName(string value) { name = value; }; void setArea(double value) { area = value; } void setPopulation(long value) { population = value; } double density(); }; Указание после заголовков функций-элементов const обеспечивает невозможность изменения каких-либо элементов данных при выполнении функции.

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

Country someCountry; someCountry.setName("France"); someCountry.setArea(551500); someCountry.setPopulation(57981000); cout << someCountry.density(); При создании объекта класса или при размещении объекта в динамической памяти транслятор организует вызов специальной функции-элемента, называемой конструктором. Имя конструктора совпадает с именем класса.

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

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

Инициализация одного объекта класса другим объектом класса может происходить явно, при передаче объекта класса как параметра функции, при возврате объекта класса из функции. При этом используется конструктор копирования вида X::X(const X&), который либо создается автоматически, либо определяется пользователем. Конструктор копирования для класса X имеет вид:

X::X(const X&);

Следует различать инициализацию объекта другим объектом и присваивание существующему объекту.

Определение массива объектов класса аналогично определению массива любого типа. Для каждого элемента массива вызывается конструктор. При наличии конструкторов без параметров (или отсутствии явных конструкторов) допустима запись:

X x[10]; // Для 10 элементов вызываются конструкторы Если у конструктора ровно 1 параметр, можно применить запись, аналогичную инициализации массивов встроенных типов:

X x[10] = {1, 2, 3, 4, 5}; Для 5 элементов вызываются конструкторы с параметром int (или double), а для остальных - конструкторы без параметров.

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

X x[3] = {X(1, "a"), X(2, "b"), X(3, "c")}; Деструкторы элементов вызываются в обратном порядке.

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

MyClass f(MyClass t) // Вызов конструктора копирования { return t; } // Вызов деструктора Для того, чтобы избежать копирования объектов класса и некоторых других побочных эффектов, объекты класса необходимо передавать в функции с помощью ссылок. Если нежелательно менять в функции данный объект, то он может быть передан как ссылка на константу.

MyClass& f(MyClass& t) // Конструктор не вызывается { return t; } // Деструктор не вызывается Аналогично функция может возвращать ссылку на объект.

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

const MyClass& f(const MyClass& t) { return t; } Функция-элемент которая не может изменять объект, для которого вызвана, называется константной. Между списком параметров и телом функции необходимо задать служебное слово const. Если константная функция-элемент определена вне тела класса, то спецификатор const нужно задавать и в объявлении, и в определении функции.

Константная функция-элемент может быть вызвана для объекта-константы:

const Country c = "France"; cout << c.getName(); c.setName("Belgium"); // Ошибка Константную функцию-элемент можно перегружать неконстантной функций с тем же прототипом.

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

Функции-элементы, которые предоставляют пользователю доступ к закрытым данным класса, называются функциями доступа. Функции доступа могут обращаться к объекту класса только по чтению (как функции getName(), getArea(), getPopulation() для класса Country), или по записи (как функции setName(), setArea(), setPopulation() для того же класса). Чаще всего функции, предназначенные для чтения элементов, описывают со спецификатором const.

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

double Country::density() { return population / area; } переводится в обычную функцию (имя ее дано условно):

double Country_density(Country *const this) { return this->population / this->area; } Разыменовывая this (использованием *this), мы получаем сам объект. При необходимости к можно обращаться явно, например когда функция-элемент получает в качестве параметра объект или ссылку на объект своего класса и нужно проверить, не получила ли она тот объект, для которого вызвана:

bool Country::isEqual(Country& c) // сравнение двух стран { if (this == &c) . . . } Другой пример использования this - возвращение функцией-элементом объекта, для которого она была вызвана:

Country& Country::getCountry() { . . . return *this; }

Иногда также есть необходимость помещать указатель на свой объект в некоторый массив указателей.

Внутри функции с описателем const тип указателя this будет константным указателем на константу.

5 Примеры программ 5.1 Использование массива структур Необходимо изменить предыдущую программу, чтобы она позволила вычислять функцию следующего вида:

y = (f0(x) - y0)(f1(x) - y1) ... (fn - 1(x) - yn - 1)

fi(x) = (x - x0)(x - x1) ... (x - xi - 1)(x - xi + 1) ... (x - xn - 1)

так, чтобы пары точек xi и yi хранились в массиве структур. Заголовочный файл будет аналогичен файлу из предыдущего примера:

#ifndef UnitMath_h #define UnitMath_h bool getXY(char *fileName); double y(double x); bool writeTable(double left, double right, double step, char *fileName); #endif Изменения коснутся файла UnitMath.cpp. В частности, в нем необходимо описать тип структуры:

#include <fstream> #include <vector> #include "UnitMath.h" namespace { struct Point { double x, y; }; std::vector <Point> v; } bool getXY(char *fileName) { std::ifstream in(fileName); if (!in) return false; Point p; while (in >> p.x >> p.y) v.push_back(p); return true; } double y(double x) { double result = 1; for (unsigned int i = 0; i < v.size(); i++) { double f = 1; for (unsigned int j = 0; j < v.size(); j++) if (i != j) f *= x - v[j].x; result *= f - v[i].y; } return result; } bool writeTable(double left, double right, double step, char *fileName) { std::ofstream out(fileName); if (!out) return false; for (double x = left; x < right + step; x += step) out << x << "\t" << y(x) << "\n"; return true; } Основной модуль программы не изменится:

#include <iostream> #include "UnitMath.h" using namespace std; int main(int argc, char* argv[]) { if (getXY("points.txt") && writeTable(0, 2, 0.5, "results.txt")) cout << "OK"; else cout << "Error!"; return 0; } 5.2 Использование указателя this Допустим, имеется класс Point для представления точки на плоскости. Необходимо реализовать функцию сравнения двух точек. Перед тем, как сравнивать координаты, необходимо проверить, не сравнивается ли объект сам с собой.

Функцию сравнения можно объявить функцией-элементом класса Point:

class Point { private: double x, y; public: Point(double newX, double newY) { x = newX; y = newY; } bool isEqual(Point& p); }; bool Point::isEqual(Point& p) { return (this == &p) || (p.x == x) && (p.y == y); } В функции main() можно протестировать работу функции isEqual():

int main(int argc, char* argv[]) { Point p1(1, 1), p2(2, 2), p3(1, 1); cout << p1.isEqual(p1) << ' ' << p1.isEqual(p2) << ' ' << p1.isEqual(p3); return 0; } 5.3 Массив вещественных чисел Допустим, необходимо спроектировать класс, реализующий работу с массивом вещественных чисел. В отличие от встроенных массивов целесообразно предусмотреть контроль над выходом за границу массива. Для обеспечения работы с массивами переменной длины элементы целесообразно размещать в динамической памяти.

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

class Array { double *pa; int size; public: Array(int n); ~Array(); void setElem(int index, double elem); double getElem(int index) const; int getSize() const; }; Конструктор может иметь следующий вид:

Array::Array(int n) { pa = new double [size = n]; for (int i = 0; i < size; i++) pa[i] = 0; } В функции установки значения элемента необходимо проверять, не вышел ли индекс за границы массива:

void Array::setElem(int index, double elem) { if (index >= 0 && index < size) pa[index] = elem; else cout << "Error!\n"; } Функция получения элемента по индексу также должна проверять, не вышел ли индекс за границы массива. Кроме того, она должна вернуть заведомо неприемлемое значение, например максимальное значение типа double. Такое значение можно получить с помощью специализации шаблона класса numeric_limits (заголовочный файл limits) и получения искомого значения с помощью функции-элемента max():

double Array::getElem(int index) const { if (index >= 0 && index < size) return pa[index]; else { cout << "Error!\n"; return numeric_limits<double>::max(); } } Функциональные возможности класса можно расширить, добавив функцию дописывания элемента в конец массива. При расширении массива он должен заново размещаться в динамической памяти.

void Array::addElem(double elem) { double *temp = new double [size + 1]; if (pa) { for (int i = 0; i < size; i++) temp[i] = pa[i]; delete [] pa; } pa = temp; pa[size] = elem; size++; } Теперь понадобится конструктор без параметров и функция, возвращающая размер массива. Эти функции можно определить внутри класса:

#include <iostream> #include <limits> using namespace std; class Array { double *pa; int size; public: Array() { pa = 0; size = 0; } Array(int n); ~Array() { if (pa) delete [] pa; } void addElem(double elem); void setElem(int index, double elem); double getElem(int index) const; int getSize() const { return size; } }; Теперь работу класса можно протестировать в функции main()

int main(int argc, char* argv[]) { Array a; a.addElem(11); a.addElem(12.5); for (int i = 0; i < a.getSize(); i++) cout << a.getElem(i) << ' '; cout << endl; Array b(10); b.setElem(9, 35); for (int i = 0; i < b.getSize(); i++) cout << b.getElem(i) << ' '; cout << endl; b.setElem(10, 35); cout << b.getElem(10); return 0; } У рассмотренного класса Array должен присутствовать конструктор копирования, так как в противном случае вместо копирования элементов массива произойдет копирование указателя. В результате вместо двух массивов получится два указателя на одну и ту же структуру данных. В нашем случае конструктор копирования будет иметь следующий вид:

Array::Array(const Array& arr) { size = arr.size; pa = new double [size]; for (int i = 0; i < size; i++) pa[i] = arr.pa[i]; } Естественно, что соответствующий конструктор должен быть объявлен в соответствующем разделе определения класса:

class Array { . . . public: . . . Array(const Array& arr); . . . }; 5.4 Класс для представления функции Для представления рассмотренной ранее функции

y = (f0(x) - y0)(f1(x) - y1) ... (fn - 1(x) - yn - 1)

fi(x) = (x - x0)(x - x1) ... (x - xi - 1)(x - xi + 1) ... (x - xn - 1)

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

Создаем новое консольное приложение и добавляем в него модуль UnitMathClass. Заголовочный файл содержит определение класса SomeFunction. Векторы, содержащие xi и yi, становятся закрытыми элементами данных, функции getXY(), y() и writeTable() объявляем как открытые функции-элементы. Кроме того, можно добавить несколько новых функций:

clear() - для очистки объекта перед заполнением новыми парами значений;

addXY() - для добавления новой пары; новые числа должны добавляться в концы соответствующих векторов;

getX() и getY() для получения соответственно значений xi и yi по их номеру;

size() для получения количества введенных пар.

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

#ifndef UnitMathClass_h #define UnitMathClass_h #include <vector> class SomeFunction { private: std::vector<double> vx, vy; public: bool getXY(char *fileName); double y(double x) const; bool writeTable(double left, double right, double step, char *fileName) const; void clear(); void addXY(double x, double y); double getX(int i) const; double getY(int i) const;

int size() const; }; #endif В файл UnitMathClass.cpp помещаем реализацию функций-элементов класса:

#include <fstream> #include "UnitMathClass.h" bool SomeFunction::getXY(char *fileName) { std::ifstream in(fileName); if (!in) return false; clear(); double x, y; while (in >> x >> y) { vx.push_back(x); vy.push_back(y); } return true; } double SomeFunction::y(double x) const { double result = 1; for (unsigned int i = 0; i < vy.size(); i++) { double f = 1; for (unsigned int j = 0; j < vx.size(); j++) if (i != j) f *= x - vx[j]; result *= f - vy[i]; } return result; } bool SomeFunction::writeTable(double left, double right, double step, char *fileName) const { std::ofstream out(fileName); if (!out) return false; for (double x = left; x < right + step; x += step) out << x << "\t" << y(x) << "\n"; return true; } void SomeFunction::clear() { vx.clear(); vy.clear(); } void SomeFunction::addXY(double x, double y) { vx.push_back(x); vy.push_back(y); } double SomeFunction::getX(int i) const { return vx.at(i); } double SomeFunction::getY(int i) const { return vy.at(i); } int SomeFunction::size() const

{

return vx.size();

} Основной модуль программы будет выглядеть так:

#include <iostream> #include "UnitMathClass.h" using namespace std; int main(int argc, char* argv[]) { SomeFunction f; if (f.getXY("input.txt") && f.writeTable(0, 2, 0.5, "results.txt")) cout << "OK"; else cout << "Error!"; return 0; } Можно использовать созданный класс по-другому. Можно также создать несколько объектов-функций.

#include <iostream> #include "UnitMathClass.h" using namespace std; int main(int argc, char* argv[]) { SomeFunction f, g; f.addXY(1, 2); f.addXY(3, 4); g.addXY(2, 1); g.addXY(3, 0); for (double x = g.getX(0); x <= g.getX(g.size() - 1); x += 0.5) { cout << x << "\t" << f.y(x) << "\t" << g.y(x) << "\n"; } return 0; } 6 Задания на самостоятельную работу Задание 1 Реализовать программу построения таблицы функции, интерполированной с помощью полинома Лагранжа с использованием вектора структур.

Задание 2 Реализовать программу, содержащую класс для представления интерполяционного полинома Лагранжа.

Задание 3* Спроектировать класс, предназначенный для реализации двумерного массива вещественных чисел (матрицы). Данные, заносимые в массив, размещать в динамической памяти. Реализовать несколько вариантов конструкторов, в том числе конструктор по умолчанию, конструктор копирования, а также деструктор. Обеспечить сокрытие данных. Протестировать работу массива в функции main().

 

Содержание     Предыдущее занятие     Следующее занятие

 

© 2001 - 2006 Иванов Л.В.

Соседние файлы в папке OOP_C++