Перегрузка
Использование класса
Реализация класса
Использование класса
В соответствии с определением языка С++, новый класс представляет собой новый тип данных, определенный пользователем. Использование этого типа, с одной стороны, определяется обычными правилами языка (действующими и для стандартных типов языка). С другой стороны, использование классов предполагает использование и новых возможностей.
Так, в соответствии с обычными правилами языка, любой объект, используемый в программе, должен быть предварительно определен с помощью предложений описания типа. Для созданного класса (нового типа) это определение выглядит следующим образом:
имя_класса имя_объекта; // элементный объект
имя_класса имя_объекта [количество]; // массив объектов
имя_класса *имя_объекта; // указатель на объект
Например:
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;
}