Перегрузка

Использование класса

Реализация класса

Использование класса

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

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

имя_класса имя_объекта; // элементный объект

имя_класса имя_объекта [количество]; // массив объектов

имя_класса *имя_объекта; // указатель на объект

Например:

Rational a;

Rational b[4];

Rational *ptr;

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

а) элементные объекты

имя_класса имя_объекта, // пустой конструктор

имя_объекта(значение), // одноаргументный инициализирующий

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

имя_объекта(знач1, знач2), // 2-х аргументный инициализирующий

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

имя_объекта1(имя_объекта2); // копирующий конструктор

Например, для класса Rational это будет выглядеть так:

Rational a1, // пустой конструктор

a2(2), // одноаргументный инициализирующий конструктор

a3(2, 5); // 2-х аргументный инициализирующий конструктор

Rational b(a1); // копирующий конструктор; эквивалентная запись –

// Rational b = a1;

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

имя_класса имя_объекта[количество] = {знач1, знач2, . . .};

знач1, знач2, . . . – константные значения соответствующего класса. Так как в языке не определены константы для создаваемого нового класса, в качестве таких значений используется явный вызов какого-либо конструктора. Это приведет к созданию временного объекта нового типа (класса), который после использования будет уничтожен.

Например, для класса Rational это выглядит так:

Rational mas[4] = {Rational(), // пустой конструктор

Rational(2), // одноаргументный конструктор

Rational(3, 8), // 2-х аргументный конструктор

Rational(a1) // копирующий конструктор

};

Если определен указатель на новый класс, объект может быть построен динамически с помощью оператора new. Использование оператора традиционное:

имя_класса *имя_объекта;

имя_объекта = new имя_класса; – выделяется память под один экземпляр класса; будет вызван пустой конструктор

имя_объекта = new имя_класса(арг, . . .); – также выделяется память под один экземпляр; для его инициализации будет вызван указанный конструктор

имя_объекта = new имя_класса [количество]; – выделяется память под массив экземпляров; для каждого экземпляра массива будет вызван пустой конструктор

Примеры для класса Rational:

Rational *p1, *p2, *p3;

p1 = new Rational; // инициализация пустым конструктором

p2 = new Rational(2, 3); // инициализация 2-х аргументным конструктором

p3 = new Rational[5]; // массив из 5 элементов; для каждого элемента вызывается

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

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

тип_результата имя_функции (тип пар1, …)// заголовок функции

{

тело_функции

}

Функцию можно определить со спецификатором inline. Такие функции называются встроенными:

inlineтип_результата имя_функции (тип пар1, …)

{

тело_функции

}

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

Рис. 2-7. Использование обычных и встроенных функций

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

тип_результата имя_класса::имя_функции (тип пар1, …)

{

тело_функции

}

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

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

Внутри функций-членов класса определен специальный неявный параметр this – он имеет тип “указатель на данный класс”; его значение определяет адрес конкретного объекта (экземпляра класса), которому посылается соответствующее сообщение (или для которого вызывается соответствующая функция-член класса). Возможен доступ к членам класса по этому указателю:

this->имя_члена

Если нет никаких неясностей и неопределенностей, имя_класса и/или this-> могут быть опущены.

Пример: реализация класса Rational

Рассмотрим реализацию класса Rational, определенного выше.

class Rational{

private:

int num, den; // состояние класса – числитель и знаменатель дроби

int gcd() const; // метод класса – нахождение наибольшего общего делителя

void reduce(); // метод класса – сокращение дроби

void correct(); // метод класса – коррекция дроби

protected:

public:

/* Конструкторы класса: пустой; инициализирует дробь значением 0 */

Rational(){num = 0; den = 1; }

/* Инициализирующий с 1 аргументом; инициализирует дробь целым значением */

Rational(int num){Rational::num = num; den = 1; }

/* Инициализирующий с 2 аргументами; инициализирует дробь заданным значением */

Rational(int num, int den) {num = n; den = d; correct(); }

/* Деструктор класса */

~Rational(){}

/* Методы класса: селекторы */

void print() const;

Rational add(const Rational &opd) const;

/* Модификатор */

void assign(int x, int y);

};

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

Реализация методов класса

inline void Rational::correct()

{

if(!den)

den = 1;

if(den < 0)

num = -num, den = -den;

}

inline void Rational::assign(int x, int y)

{

num = x;

den = y;

correct();

}

Видно, что две функции – двух аргументный конструктор и assign – имеют одинаковые коды; но это функционально разные функции: конструктор будет вызываться при объявлении и инициализации данных типа Rational, тогда как assign можно вызывать неоднократно – каждый раз, когда с помощью присваивания нужно изменить значение уже существующего экземпляра класса. Отличие такое же, как и в случае использования базовых типов: int x = 1; ... x = 1; ...

// Нахождение наибольшего общего делителя для числителя и знаменателя дроби.

// Известно, что знаменатель дроби всегда > 0

int Rational::gcd() const

{

int n = abs(num), d = den, r;

while(r = n % d) // вычисляется остаток от деления и сравнивается с 0

n = d, d = r; // переопределяются делимое и делитель

return r;

}

// Сокращение дроби

void Rational::reduce()

{

int div = gcd();

num /= div;

den /= div;

}

// Сложение дробей

Rational Rational::add(const Rational &opd) const

{

Rational temp;

temp.num = num * opd.den + den * opd.num;

temp.den = den * opd.den;

temp.reduce();

return temp;

}

// Вывод значения дроби в выходной поток

void Rational::print() const

{

cout << num;

if(den > 1)

cout << ’/’<< den;

}

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

· Простые переменные:

Rational a, /* пустой конструктор; конструкция Rational a() определяет
обычную функцию, возвращающую значение типа Rational */

d(5), /* одно аргументный инициализирующий конструктор */

b(3,8); /* двух аргументный инициализирующий конструктор */

Возможна и традиционная инициализация экземпляров класса:

Rational c = 8, /* В результате будет создана дробь со значением 8/1 */

p = Rational(3,8); /* Так как при классической инициализации требуются значения соответствующего типа, а в языке не определены константы типа Rational, нужно построить такую константу, явно вызвав конструктор класса */

· Массивы:

Rational x[3], /* Используется пустой конструктор для создания каждого элемента
массива */

y[] = {2, 1, Rational(3,8)}; /* Обычный синтаксис при инициализации массива, обязательно используются значения соответствующего типа */

· Использование свободной памяти

Rational *ptr1, *ptr2;

ptr1 = new Rational(1,3); /* Классическое использование операции new, в которой указывается имя нового типа; при этом возможна сразу и инициализация выделенной области памяти за счет работы соответствующего конструктора */

ptr2 = new Rational[4]; /* Если выделяется память под массив, работает только пустой конструктор; инициализация памяти не выполняется */

Важное правило: если создается тем или иным способом массив экземпляров класса, класс должен иметь пустой конструктор.

Пример использования класса

main()

{

Rational a(2), b[3], x, y;

const Rational c(5,8);

// Вывод значения дроби a

a.print(); cout << endl;

// Вывод значения элемента массива b

b[1].print(); cout << endl;

// Сложение значений дробей a и c

x = a.add(c);

// Вывод результата сложения

x.print(); cout << endl;

// Сложение дроби x с дробью 3/5 и вывод результата

x.add(Rational(3,5)).print(); cout << endl;

/* Для свободной памяти */

Rational *ptr;

ptr = new Rational(3,8);

(*ptr).print(); cout << endl; /* Возможна и запись ptr->print(); */

}

Ошибки:

a.gcd()

a.reduce()

и т.п.

Еще пример – решение основной задачи (система двух уравнений с двумя неизвестными). Предполагается, что для класса Rational определены все арифметические операции: сложения (add), вычитания (sub), умножения (mul) и деления (div).

Решить систему вида:

Значения коэффициентов системы приведены в таблице:

a b c d e f
-1

Решение имеет вид:

определитель системы det = a * e - d * b;

x = (c * e - b * f) / det; y = (a * f - d * c) / det;

Чтобы умножить a на e, нужно экземпляру a послать сообщение: “умножь себя (свое значение) на e”: a.mul(e);

 

main()

{

Rational a(2), b(3), c(-1), d(5), e(2), f(3), x, y;

Rational det;

det = (a.mul(e)).sub(d.mul(b));

x = (c.mul(e)).sub(b.mul(f)).div(det);

y = (a.mul(f)).sub(d.mul(c)).div(det);

x.print(); cout << ’,’; y.print(); cout << endl;

}