Предыдущая Оглавление Следующая
Конструкторы и деструкторы.
Недостатком рассмотренных ранее классов является отсутствие автоматической инициализации создаваемых объектов. Для каждого вновь создаваемого объекта необходимо было вызвать функцию типа set (как для класса complex), либо явным образом присваивать значения данным объекта. Однако для инициализации объектов класса в его определение можно явно включить специальную компонентную функцию, называемую конструктором. Формат определения конструктора следующий
имя_класса(список_форм_параметров) {операторы_тела_конструктора};
Имя этой компонентной функции по правилам языка С++ должно совпадать с именем класса. Такая функция автоматически вызывается при определении или размещении в памяти с помощью оператора new каждого объекта класса.
Пример.
Complex(double re1 = 0.0, double im1 = 0.0)
{
re = re1;
im = im1;
}
Конструктор выделяет память для объекта и инициализирует данные-члены класса.
Конструктор имеет ряд особенностей:
Для конструктора не определяется тип возвращаемого значения. Даже тип void не допустим.
Указатель на конструктор не может быть определен и соответственно нельзя получить адрес конструктора.
Конструкторы не наследуются.
Конструкторы не могут быть описаны с ключевыми словами virtual, static, const, mutable, volatile.
Конструктор всегда существует для любого класса, причем, если он не определен явно, он создается автоматически. По умолчанию создается конструктор без параметров и конструктор копирования. Если конструктор описан явно, то конструктор по умолчанию не создается. По умолчанию конструкторы создаются общедоступными (public).
В классе может быть несколько конструкторов, но только один с умалчиваемыми значениями параметров. Перегрузка чаще всего используется для передачи конструктору аргументов, предназначенных для инициализации данных- членов класса. Параметром конструктора не может быть его собственный класс, но может быть ссылка на него (T&). Без явного указания программиста конструктор всегда автоматически вызывается при определении (создании) объекта. В этом случае вызывается конструктор без параметров. Для явного вызова конструктора используются две формы:
имя_класса имя_объекта(фактические_параметры);
имя_класса(фактические_параметры);
Первая форма допускается только при не пустом списке фактических параметров. Она предусматривает вызов конструктора при определении нового объекта данного класса:
Complex ss(5.9, 0.15);
Вторая форма вызова приводит к созданию объекта без имени:
Complex ss = Complex(5.9, 0.15);
Существует два способа инициализации данных объекта с помощью конструктора. Ранее мы рассматривали первый способ, а именно передача значений параметров в тело конструктора. Второй способ предусматривает применение списка инициализаторов данного класса. Этот список помещается между списком параметров и телом конструктора. Каждый инициализатор списка относится к конкретному компоненту и имеет вид
имя_данного (выражение)
Пример.
class A
{
int i;
float e;
char c;
public:
A(int ii, float ee, char cc)
: i (8),
e (i * ee + ii),
с (cc)
{}
...
};
Пример.
#include <string.h>
#include <iostream.h>
class String
{
char *ch; // указатель на текстовую строку
int len; // длина текстовой строки
public:
// конструкторы
// создает объект – пустая строка
String(int N = 80)
: len (0)
{
ch = new char[N+1];
ch[0] = '\0';
}
// создает объект по заданной строке
String(const char *arch)
{
len = strlen(arch);
ch = new char[len + 1];
strcpy (ch, arch);
}
// компоненты-функции
// возвращает ссылку на длину строки
int &length(void)
{
return len;
}
// возвращает указатель на строку
char *str(void)
{
return ch;
}

...
};
Здесь у класса String два конструктора – перегружаемые функции.
По умолчанию создается также конструктор копирования вида T::T(const T &), где T - имя класса. Конструктор копирования вызывается всякий раз, когда выполняется копирование объектов, принадлежащих классу. В частности он вызывается:
когда объект передается функции по значению;
при построении временного объекта как возвращаемого значения функции;
при использовании объекта для инициализации другого объекта.
Если класс не содержит явным образом определенного конструктора копирования, то при возникновении одной из этих трех ситуаций, производится побитовое копирование объекта. Побитовое копирование не во всех случаях является адекватным. Именно для таких случаев и необходимо определить собственный конструктор копирования. Например, создадим два объекта типа String.
String s1("это - строка");
String s2 = s1;
Здесь объект s2 инициализируется объектом s1 путем вызова конструктора копирования, созданного компилятором по умолчанию. В результате эти объекты имеют одинаковое значение в полях ch, то есть эти поля указывают на одну и ту же область памяти. В результате при удалении объекта s1 будет освобождаться и область, занятая строкой, но она еще нужна объекту s2. Чтобы не возникало подобных ошибок, определим собственный конструктор копирования.
String(const String &st)
{
len = strlen(st.len);
ch = new char[len + 1];
strcpy(ch, st.ch);
}
Конструктор с одним аргументом может выполнять неявное преобразование типа своего аргумента в тип класса конструктора.
Например
class Complex
{
double re, im;
Complex (double r)
: re(r),
im(0)
{}
...
};
Этот конструктор реализует представление вещественной оси в комплексной плоскости.
Вызвать этот конструктор можно традиционным способом
Complex b(5);
Но можно вызвать его и так
Complex b = 5;
Здесь необходимо преобразование скалярной величины (типа аргумента конструктора) в тип Complex. Это осуществляется вызовом конструктора с одним параметром. Поэтому конструктор, имеющий один аргумент не нужно вызывать явно, а можно просто записать Complex b = 5, что означает Complex b = Complex (5).
Преобразование, определяемое пользователем, неявно применяется в том случае, если оно уникально. Например,
class Demo
{
Demo(char);
Demo(long);
Demo(char *);
Demo(int *);
...
};
Здесь в Demo a = 3 неоднозначность: вызов Demo(char) или Demo(long)?
А в Demo a = 0; также неоднозначность: вызов Demo(char *), Demo(int *), Demo(char) или Demo(long)?
В некоторых случаях необходимо задать конструктор, который можно вызвать только явно. Например,
class String
{
char *ch;
int len;
public:
String(int size)
{
len = size;
ch = new[len+1];
ch[0] = '\0';
}
};
В этом случае неявное преобразование может привести к ошибке. В случае String s = 'a'; создается строка длиной int('a'). Вряд ли это то, что мы хотели.
Неявное преобразование можно подавить, объявив конструктор с модификатором explicit. В этом случае конструктор будет вызываться только явно. В частности, там где конструктор копирования в принципе необходим, explicit-конструктор не будет вызываться неявно. Например:
class String
{
char *ch;
int len;
public:
explicit String(int size);
String(const char *ch);
};
String s1 = 'a'; // Ошибка, нет явного преобразования char в string
String s2(10); // Правильно, строка для хранения 10-ти символов
// - явный вызов конструктора
String s3 = 10; // Ошибка, нет явного преобразования int в string
String s9 = string(10); // Правильно, конструктор выpывается явно
String s5 = "строка"; // Правильно, неявный вызов конструктора
// s5 = string ("строка");
Можно создавать массив объектов, однако при этом соответствующий класс должен иметь конструктор по умолчанию (без параметров).
Это связано с тем, что при объявлении массива объектов невоuможно определить параметры для констрaкторов этих объектов и единственная возможность zызова конструкторов - это передача им параметров, заданных по умолчанию.
Массив объектов может инициализироваться либо автоматически конструктором по умолчанию, либо явным присваиванием значений каждому элементу массива.
class Demo
{
int x;
public:
Demo()
{
x = 0;
}
Demo(int i)
{
x = i;
}
};
int main(void)
{
Demo a[20]; //вызов конструктора без параметров
Demo b[2] = {
Demo(10),
Demo (100)
}; //явное присваивание
return 0;
}
При объявлении массива объектов невозможно определить параметры для конструкторов этих объектов и единственная возможность вызова конструкторов - это передача им параметров, заданных по умолчанию. Таким образом, для того чтобы создавать массив объектов соответствующий класс должен иметь заданный по умолчанию конструктор. Можно уменьшить количество конструкторов, если задать конструктор с аргументами по умолчанию. Он же будет конструкторов по умолчанию.
Динамическое выделение памяти для объекта создает необходимость освобождения этой памяти при уничтожении объекта. Например, если объект формируется как локальный внутри блока, то целесообразно, чтобы при выходе из блока, когда уже объект перестает существовать, выделенная для него память была возвращена. Желательно чтобы освобождение памяти происходило автоматически. Такую возможность обеспечивает специальный компонент класса – деструктор класса. Его формат:
~имя_класса () { операторы_тела_деструктора };
Имя деструктора совпадает с именем его класса, но предваряется символом "~" (тильда).
Деструктор не имеет параметров и возвращаемого значения. Вызов деструктора выполняется не явно (автоматически), как только объект класса уничтожается.
Например, при выходе за область определения или при вызове оператора delete для указателя на объект.
String *p = new String("строка");
delete p;
Если в классе деструктор не определен явно, то компилятор генерирует деструктор по умолчанию, который просто освобождает память занятую данными объекта. В тех случаях, когда требуется выполнить освобождение и других объектов памяти, например область, на которую указывает ch в объекте string, необходимо определить деструктор явно:
~String() { delete [] ch;}
Также как и для конструктора, не может быть определен указатель на деструктор.
Компоненты-данные и компоненты-функции
Данные - члены класса.
Определение данных класса внешне аналогично описанию переменных базовых или производных типов. Однако при описании данных класса не допускается их инициализация. Для их инициализации должен использоваться автоматический или явно вызываемый конструктор. Принадлежащие классу функции имеют полный доступ к его данным. Для доступа к элементам-данным из операторов, выполняемых вне определения класса нужно использовать операции выбора компонентов класса ( "." или "–>"). Данные класса не обязательно должны быть определены или описаны до их первого использования в принадлежащих классу функциях. Все компоненты класса "видны" во всех операторах его тела. Область доступа к компонентам-данным регулируется модификатором доступа.
Компоненты-данные могут быть описаны как const. В этом случае после инициализации они не могут быть изменены.
Компоненты-данные могут быть описаны как mutable. В этом случае они являются изменяемыми, даже если объект, содержащий их, описан как const.
Функции - члены класса.
Компонентная функция должна быть обязательно описана в теле класса. При определении классов их компонентные функции могут быть специфицированы как подставляемые (inline). Кроме явного использования слова inline для этого используются следующие соглашения. Если определение функции полностью размещено в теле класса, то эта функция по умолчанию считается подставляемой. Если в теле класса помещен только прототип функции, а ее определение – вне класса, то для того, чтобы функция была подставляемой, ее надо явно специфицировать словом inline. При внешнем определении функции в теле класса помещается прототип функции
тип имя_функции (спецификция_и_инициализация_параметров);
Вне тела класса функция определяется так
тип имя_класса::имя_функции (спецификция_формальных_параметров)
{ тело_функции }
Константные компоненты-функции.
Функции-члены класса могут быть описаны как const. В этом случае они не не могут изменять значения данных-членов класса и могут возвращать указатель или ссылку только на данные-члены класса, описанные как const. Они являются единственными функциями, которые могут вызываться для объекта-константы.
Например, в классе Complex
class Complex
{
public:
// ...
double real() const
{
return re;
}
double imag() const
{
return im;
}
protected:
double re;
double im;
};
Объявление функций real() и imag() как const гарантирует, что они не изменяют состояние объекта Complex. Компилятор обнаружит случайные попытки нарушить это условие.
Когда константная функция определяется вне класса указывать const надо обязательно:
double Complex::real() const
{
return re;
}
Константную функцию-член можно вызвать как для константного, так и для неконстантного объекта, в то время как неконстантную функцию-член можно вызвать только для объекта, не являющегося константой.
Статические члены класса
Каждый объект одного и того же класса имеет собственную копию данных класса. Это не всегда соответствует требованиям решаемой задачи. Например, счетчик объектов, указатели на первый и последний объект в списке объектов одного класса. Эти данные должны быть компонентами класса, но иметь их нужно только в единственном числе. Такие компоненты должны быть определены в классе как статические (static). Статические данные классов не дублируются при создании объектов, т.е. каждый статический компонент существует в единственном экземпляре. Доступ к статическому компоненту возможен только после его инициализации. Для инициализации используется конструкция
тип имя_класса::имя_данного инициализатор;
Например, int goods::percent = 12;
Это предложение должно быть размещено в глобальной области после определения класса. Только при инициализации статическое данное класса получает память и становится доступным. Обращаться к статическому данному класса можно обычным образом через имя объекта
имя_объекта.имя_компонента
Но к статическим компонентам можно обращаться и тогда, когда объект класса еще не существует. Доступ к статическим компонентам возможен не только через имя объекта, но и через имя класса
имя_класса::имя_компонента
Однако так можно обращаться только к public компонентам.
А как обратиться к private статической компоненте извне определения объекта? С помощью компонента-функции этого класса. Однако при вызове такой функции необходимо указывать имя объекта, а объект может еще не существовать. Эту проблему решают статические компоненты-функции. Эти функции можно вызвать через имя класса.
имя_класса::имя_статической_функции
Пример.
#include <iostream.h>
class Point
{
public:
// конструктор
Point(double x1 = 0.0, double y1 = 0.0)
{
N++;
x = x1;
y = y1;
}
// статический компонент-функция
static int&count()
{
return N;
}
protected:
double x, y;
static int N; // статический компонент-данное: количество точек
};
int Point::N = 0; // инициализация статического компонента-данного
int main(void)
{
Point A(1.0, 2.0);
Point B(4.0, 5.0);
Point C(7.0, 8.0);
cout << "\nОпределены " << Point::count() << "точки.";
return 0;
}
Пример. Моделирование списка.
class List
{
public:
List(int x1);
~List();
void add(); // объект добавляет себя в список
static void show(); // статическая функция для просмотра списка
protected:
int x; // информационное поле
List *next; // указатель на следующий элемент
static List *begin; // начало списка
};
List *List::begin = NULL;
int main(void)
{
List *p;
p = new List(5);
p->add(); // создаем первый объект и добавляем его в список
p = new List(8);
p->add(); // создаем второй объект и добавляем его в список
p = new List(35);
p->add(); // создаем третий объект и добавляем его в список
List::show(); // показываем весь список
return 0;
}
Пример
#include <iostream.h>
// Класс "товары"
class Goods
{
public:
void Input()
{
cout << "наименование: ";
cin >> name;
cout << "цена: ";
cin >> price;
}
void Print()
{
cout << "\n" << name;
cout << ", цена: ";
cout << long(price * (1.0 + percent * 0.01));
}
protected:
char name[40];
float price;
static int percent; // наценка
};
int goods::percent = 12;
int main(void)
{
Goods wares[5];
int k = 5;
for (int i = 0; i < k; i++)
{
wares[i].Input();
}
cout << "\nСписок товаров при наценке " << wares[0].percent << " % ";
for (int i = 0; i < k; i++)
{
wares[i].Print();
}
Goods::percent = 10;
cout << "\nСписок товаров при наценке " << Goods::percent << "%";
goods *pGoods = wares;
for (int i = 0; i < k; i++)
{
pGoods++–>Print ();
}
return 0;
}
Указатели на компоненты класса.
Указатели на компоненты-данные.
Можно определить указатель на компоненты-данные.
тип_данных (имя_класса::*имя_указателя)
В определении указателя можно включить его инициализатор
&имя_класса::имя_компонента
Пример. double (Complex::*pdat) = &Complex::re;
Естественно, что в этом случае данные-члены должны иметь статус открытых (public).
После инициализации указателя его можно использовать для доступа к данным объекта.
Complex c(10.2, 3.6);
c.*pdat = 22.2; // изменилось значение поля re объекта c
Указатель на компонент класса можно использовать в качестве фактического параметра при вызове функции.
Если определены указатели на объект и на компонент, то доступ к компоненту с помощью операции "–>*".
указатель_на_объект–>*указатель_на_компонент
Пример
double (Complex::*pdat) = &Complex::re;
Complex C(10.2, 3.6);
Complex *pcom = &C;
pcom–>*pdat = 22.2;
Можно определить тип указателя на компоненты-данные класса:
typedef double (Complex::*PDAT);
void f(Complex c, PDAT pdat)
{
c.*pdat = 0;
}
Complex c;
PDAT pdat = &Complex::re;
f(c, pdat);
pdat = &Complex::im;
f(c, pdat);
Указатели на компоненты-функции.
Можно определить указатель на компоненты-функции.
тип_возвр_значения (имя_класса::*имя_указателя_на_функцию)
(специф_параме-тров_функции);
Пример
// Определение указателя на функцию-член класса
double (Complex::*ptcom)();
// Настройка указателя
ptcom = &Complex::real;
// Теперь для объекта А
Complex A(5.2, 2.7);
// можно вызвать его функцию
cout << (A.*ptcom)();
// Если метод real определить типа ссылки
double &real(void)
{
return re;
}
//то используя этот метод можно изменить поле re
(A.*ptcom)() = 7.9;
// При этом указатель определяется так
double &(Complex::*ptcom)();
Можно определить также тип указателя на функцию
typedef double &(Complex::*PF)();
а затем определить и сам указатель
PF ptcom = &Complex::real;
Указатель this.
Когда функция-член класса вызывается для обработки данных конкретного объекта, этой функции автоматически и неявно передается указатель на тот объект, для которого функция вызвана. Этот указатель имеет имя this и неявно определен в каждой функции класса следующим образом
имя_класса *const this = адрес_объекта
Указатель this является дополнительным скрытым параметром каждой нестатической компонентной функции. При входе в тело принадлежащей классу функции thisинициализируется значением адреса того объекта, для которого вызвана функция. В результате этого объект становится доступным внутри этой функции.
В большинстве случаев использование this является неявным. В частности, каждое обращение к нестатической функции-члену класса неявно использует this для доступа к члену соответствующего объекта. Например, функцию add в классе Complex можно определить эквивалентным, хотя и более пространным способом:
void Complex add(Complex ob)
{
this->re = this->re + ob.re;
// или (*this).re = (*this).re + ob.re;
this->im = this->im + ob.im;
}
Если функция возвращает объект, который ее вызвал, используется указатель this.
Например, пусть функция add возвращает ссылку на объект. Тогда
Complex& Complex add(Complex& ob)
{
re = re + ob.re;
im = im + ob.im;
return *this;
}
Примером широко распространенного использования this являются операции со связанными списками.
Пример. Связанный список.
#include <iostream.h>
//Определение класса
class Item
{
public:
Item(char ch) // конструктор
{
symbol = ch;
}
void add(void); // добавить в начало
static void print(void);
protected:
static Item *begin;
Item *next;
char symbol;
};
// Реализация класса
void Item::add(void)
{
this–>next = begin;
begin = this;
}
void Item::print(void)
{
Item *p;
p = begin;
while (p)
{
cout << p–>symbol << "\t";
p = p–>next;
}
}
//Создание и просмотр списка
Item *Item::begin = NULL; // инициализация статического компонента
int main(void)
{
Item A('a');
Item B('b');
Item C('c');
// включение объектов в список
A.add();
B.add();
C.add();
// просмотр списка в обратном порядке
Item::print();
return 0;
}
Дружественная функция
Дружественная функция – это функция, которая, не являясь компонентом класса, имеет доступ к его защищенным и собственным компонентам. Такая функция должна быть описана в теле класса со спецификатором friend.
Пример.
class MyClass
{
public:
MyClass(int x1, int y1)
{
x = x1;
y = y1;
}
int sum(void)
{
return (x + y);
}
protected:
int x, y;
friend void set(MyClass *, int, int);
};
void set(MyClass *p, int x1, int y1)
{
p–>x = x1;
p–>y = y1;
}
int main(void)
{
MyClass A(5, 6);
MyClass B(7, 8);
cout << A.sum();
cout << B.sum();
set(&A, 9, 10);
set(&B, 11, 12);
cout << A.sum();
cout << B.sum();
}
Функция set описана в классе MyClass как дружественная и определена как обычная глобальная функция (вне класса, без указания его имени, без операции '::' и без спецификатора friend).
Дружественная функция при вызове не получает указатель this. Объекты класса должны передаваться дружественной функции только через параметр.
Итак, дружественная функция:
не может быть компонентной функцией того класса, по отношению к которому определяется как дружественная;
может быть глобальной функцией;
может быть компонентной функцией другого ранее определенного класса.Например
class Class1
{
...
int f(...);
...
};
class Class2
{
...
friend int Class1::f(...);
...
};
В этом примере класс Class1 с помощью своей компонентной функции f() получает доступ к компонентам класса Class2.
может быть дружественной по отношению к нескольким классам;Например,
// предварительное не полное определение класса
class CL2;
class CL1
{
...
friend void f(CL1, CL2);
...
};
class CL2
{
...
friend void f(CL1, CL2);
...
};
В этом примере функция f имеет доступ к компонентам классов CL1 и CL2.
Дружественный класс
Класс может быть дружественным другому классу. Это означает, что все компонентные функции класса являются дружественными для другого класса. Дружественный класс должен быть определен вне тела класса "предоставляющего дружбу".
Например
class X2 {
friend class X1;
...
};
class X1
{
...
void f1(...);
void f2(...);
...
};
В этом примере функции f1 и f2 класса X1 являются друзьями класса X2, хотя они описываются без спецификатора friend.
Пример
Рассмотрим класс Point – точка в n-мерном пространстве и дружественный ему класс Vector – радиус-вектор точки ("вектор с началом в начале координат n-мерного пространства"). В классе Vector определим функцию для определения нормы вектора, который вычисляется как сумма квадратов координат его конца.
class Point
{
public:
Point(int n, double d = 0.0);
protected:
int N; // размерность
double *x; // указатель на массив координат
friend class Vector;
};
Point::Point(int n, double d)
{
N = n;
x = new double[N];
for (int i = 0; i < N; i++)
{
x[i] = d;
}
}
class Vector
{
public:
Vector(Point, Point);
double norma();
protected:
double *xv;
int N;
};
Vector::Vector(Point begin, Point end)
{
N = begin.N;
xv = new double[N];
for (int i = 0; i < N; i++)
{
xv[i] = end.x[i]–begin.x[i];
}
}
double Vector::norma()
{
double dd = 0.0;
for (int i = 0; i < N; i++)
{
dd += xv[i]*xv[i];
}
return dd;
}
int main(void)
{
Point A(2, 4.0);
Point B(2, 2.0);
Vector V(A, B);
cout << V.norma();
return 0;
}
// Будет выведено – 8.
Недостатком предложенного класса Point является то, что значения всех координат точки x[i] одинаковы. Чтобы они были произвольными и разными, необходимо определить конструктор как функцию с переменным числом параметров, например так:
Point::Point(int n, double d, ...)
{
N = n;
x = new double[N];
double *p = &d;
for (int i = 0; i < N; i++)
{
x[i] = *p;
p++;
}
}
Определение классов и методов классов.
Определение классов обычно помещают в заголовочный файл.
Пример
// point.h
#ifndef __POINT_H__
#define __POINT_H__
class Point
{
public:
Point(int x1 = 0, int y1 = 0);
int &getx (void);
int &gety (void);
...
protected:
int x, y;
};
#endif
Т.к. описание класса Point в дальнейшем планируется включать в другие классы, то для предотвращения недопустимого дублирования описаний в текст включена условная препроцессорная директива #ifndef __POINT_H__. Тем самым текст описания класса Point может появляться в компилируемом файле только однократно, несмотря на возможность неоднократного появления директив #include "point.h".
Определить методы можно следующим образом
// point.cpp
#include "point.h"
Point::Point(int x1, int y1)
{
x = x1;
y = y1;
}
int &Point::getx(void)
{
return x;
}
int &Point::gety(void)
{
return y;
}
...
Программа, использующая объекты класса
#include "point.h"
...
int main(void)
{
Point A(5, 6);
Point B(7, 8);
...
}
К проекту должен быть подключен файл point.cpp.
Внешнее определение методов класса дает возможность, не меняя интерфейс объектов класса с другими частями программ, по-разному реализовать компонентные функции.
Предыдущая Оглавление Следующая