Базовые конструкции объектно-ориентированных программ
Объекты
Базовыми блоками ООП являются объект и класс. Объект С++ - абстрактное описание некоторой сущности, например, запись о человеке. Формально объект определить достаточно сложно. Grady Booch определил объект через свойства: состояние и поведение, которые однозначно идентифицируют объект. Класс – это множество объектов, имеющих общую структуру и поведение.
Рассмотрим некоторые конструкции языка С.
int a,b,c;
Здесь заданы три статических объекта целого типа, имеющих общую структуру, которые только могут получать значения и о поведении которых ничего не известно. Таким образом, о типе int можно говорить как об имени класса – некоторой абстракции, используемой для описания общей структуры и поведения множества объектов.
Рассмотрим пример описания структуры в языке С:
struct str
{ char a[10];
double b;
};
str s1,s2;
Структура позволяет описать АТД (тип, определенный программистом), являющийся абстракцией, так как в памяти при этом место под структуру выделено не будет. Таким образом, str – это класс.
В то же время объекты а, b, c и s1, s2 – это уже реальность, существующая в пространстве (памяти ЭВМ) и во времени. Таким образом, класс можно определить как общее описание для множества объектов.
Определим теперь понятия состояние, поведение и идентификация объекта. Состояние объекта объединяет все его поля данных (статическая компонента) и текущие значения каждого из этих полей (динамическая компонента). Поведение объекта определяет, как объект изменяет свои состояния и взаимодействует с другими объектами. Идентификация объекта позволяет выделить объект из числа других объектов.
Процедурный подход к программированию предполагает разработку взаимодействующих подпрограмм, реализующих некоторые алгоритмы. Объектно-ориентированный подход представляет программы в виде взаимодействующих объектов. Взаимодействие объектов осуществляется посредством сообщений. Под передачей сообщения объекту понимается вызов некоторой функции (компонента этого объекта).
Говоря об объекте, можно выделить две его характеристики: интерфейс и реализацию.
Интерфейс показывает, как объект общается с внешней средой. Он может быть ассоциирован с окном, через которое можно заглянуть внутрь объекта и получить доступ к функциям и данным объекта.
Все данные делятся на локальные и глобальные. Локальные данные недоступны (через окно). Доступ к ним и их модификация возможна только из компонент-функций этого объекта. Глобальные данные видны и модифицируемы через окно (извне). Для активизации объекта (чтобы он что-то выполнил) ему посылается сообщение. Сообщение проходит через окно и активизирует некоторую глобальную функцию. Тип сообщения определяется именем функции и значениями передаваемых аргументов. Локальные функции (за некоторым исключением) доступны только внутри класса.
Говоря о реализации объекта, мы подразумеваем особенности реализации функций соответствующего класса (особенности алгоритмов и кода функций).
Объект включает в себя все данные, необходимые, чтобы описать сущность, и функции или методы, которые манипулируют этими данными.
Понятие класса
Одним из основных, базовых понятий объектно-ориентированного программирования является понятие класса.
Основная идея класса как абстрактного типа заключается в разделении интерфейса и реализации.
Интерфейс показывает, как можно использовать класс. При этом совершенно неважно, каким образом соответствующие функции реализованы внутри класса.
Реализация – внутренняя особенность класса. Одно из требований к реализации – критерий изоляции кода функции от воздействия извне. Это достигается использованием локальных компонент данных и функций.
Интерфейс и реализация должны быть максимально независимы друг от друга, то есть изменение кода функций класса не должно изменять интерфейс.
По синтаксису класс аналогичен структуре. Класс определяет новый тип данных, объединяющих данные и код (функции обработки данных), и используется для описания объекта. Реализация механизма сокрытия информации (данные и функциональная часть) в С++ обеспечивается механизмом, позволяющим содержать публичные (public), частные (private) и защищенные (protected) части. Защищенные (protected) компоненты класса можно пока рассматривать как синоним частных (private) компонент, но реальное их назначение связано с наследованием. По умолчанию все части класса являются частными. Все переменные, объявленные с использованием ключевого слова public, являются доступными для всех функций в программе. Частные (private) данные доступны только в функциях, описанных в данном классе. Этим обеспечивается принцип инкапсуляции (пакетирования). В общем случае доступ к объекту из остальной части программы осуществляется с помощью функций со спецификатором доступа public. В ряде случаев лучше ограничить использование переменных, объявив их как частные, и контролировать доступ к ним через функции, имеющие спецификатор доступа public.
Сокрытие данных – важная компонента ООП, позволяющая создавать легче отлаживаемый и сопровождаемый код, так как ошибки и модификации локализованы в нем. Для реализации этого компоненты-данные желательно помещать в private-секцию.
#include <iostream.h>
Class kls
{ int sm; // по умолчанию для класса предполагается
int m[5]; // атрибут private
public:
void inpt(int); // прототип функции ввода данных
int summ(); // прототип функции summ
};
void kls::inpt(int i) // описание функции inpt
{ cin >> m[i]; }
int kls::summ() // описание функции summ
{ sm=0; // инициализация компоненты sm класса
for(int i=0; i<5; i++) sm+=m[i];
return sm;
}
void main()
{ kls k1,k2; // объявление объектов k1 и k2 класса kls
int i;
cout<< ”Вводите элементы массива ПЕРВОГО объекта : ”;
for(i=0;i<5; k1.inpt(i++)); // ввод данных в первый объект
cout<< ”Вводите элементы массива ВТОРОГО объекта : ”;
for(i=0;i<5; k2.inpt(i++)); // во второй объект
cout << "\n Сумма элементов первого объекта (k1) = " << k1.summ();
cout << "\n Сумма элементов второго объекта (k2) = " << k2.summ();
}
Результат работы программы:
Вводите элементы массива ПЕРВОГО объекта : 2 4 1 3 5
Вводите элементы массива ВТОРОГО объекта : 2 4 6 4 9
Сумма элементов первого объекта (k1) = 15
Сумма элементов второго объекта (k2) = 25
В приведенном примере описан класс, для которого задан пустой список объектов. В main() функции объявлены два объекта описанного класса. При описании класса в него включаются прототипы функций для обработки данных класса. Текст самой функции может быть записан как внутри описания класса (функция inpt()), так и вне его (функция summ()).
Знак :: называется областью действия оператора. Он используется для информирования компилятора о том, что описываемая функция (в примере это summ) принадлежит классу, имя которого расположено слева от знака ::.
Если при описании класса некоторые функции объявлены со спецификатором public, а часть со спецификатором private, то доступ к последним из функции main() возможен только через функции этого же класса. Например:
#include <iostream.h>
Class kls
{ int max,a,b,c;
public:
void init(void);
void out();
private:
int f_max(int,int,int);
};
void kls::init(void) // инициализация компонент a, b, c класса
{ cout << “Введите 3 числа ";
cin >> a >> b >> c;
max=f_max(a,b,c); // обращение к функции, описанной как private
}
void kls::out(void) // вывод max числа на экран
{ cout <<"\n MAX из 3 чисел = " << max <<' endl;
}
int kls::f_max(int i,int j, int k) ); // функция нахождения наибольшего из
{ int kk; // трех чисел
if(i>j) if(i>k) return i;
else return k;
else if(j>k) return j;
else return k;
}
void main()
{ kls k; // объявление объекта класса kls
k.init(); // обращение к public функциям ( init и out)
k.out();
}
Отметим ошибочность использования инструкции
k.max=f_max(k.a,k.b,k.c);
в main функции, так как данные max, a, b, c и функция f_max класса kls являются частными и недоступны через префикс k из функции main(), не принадлежащей классу kls.
В примере для работы с объектом k класса kls выполнялась инициализация полей a, b и c путем присвоения им некоторых начальных значений. Для этого использована функция init. В языке С++ имеется возможность одновременно с описанием (созданием) объекта выполнять и его инициализацию. Эти действия выполняются специальной функцией, принадлежащей этому классу. Эта функция носит специальное название: конструктор. Название этой функции всегда должно совпадать с именем класса, которому она принадлежит. При использовании конструктора функцию init в описании класса можно заменить на
kls(int A, int B, int C) {a=A; b=B; c=C;} // функция конструктор
Конструктор представляет собой обычную функцию, имя которой совпадает с именем класса, в котором он объявлен и используется. Он никогда не должен возвращать никаких значений. Количество и имена фактических параметров в описании функции конструктора зависят от числа полей, которые будут инициализированы при объявлении объекта (экземпляра) данного класса. Кроме отмеченной формы записи конструктора в программах на С++ можно встретить и форму записи конструктора в следующем виде:
kls(int A, int B, int C) : a(A), b(B), c(C) { }
В этом случае после двоеточия перечисляются инициализируемые данные и в скобках - инициализирующие их значения (точнее, через запятую перечисляются конструкторы объектов соответствующих типов). Возможна комбинация отмеченных форм.
Наряду с перечисленными выше формами записи конструктора существует конструктор, либо не имеющий параметров, либо все аргументы которого заданы по умолчанию – конструктор по умолчанию:
kls(){ } это, для примера выше, аналогично kls() : a(), b(), c() { }
kls(int=0, int=0, int=0){ } это аналогично kls() : a(0), b(0), c(0) { }
Каждый класс может иметь только один конструктор по умолчанию. Более того, если при объявлении класса в нем отсутствует явно описанный конструктор, то компилятором автоматически генерируется конструктор по умолчанию. Конструктор по умолчанию используется при создании объекта без инициализации его, а также незаменим при создании массива объектов. Если при этом конструкторы с параметрами в классе есть, а конструктора по умолчанию нет, то компилятор зафиксирует синтаксическую ошибку.
Существует еще один особый вид конструкторов – конструктор копирования, но о нем разговор будет идти несколько позже.
Противоположным по отношению к конструктору является деструктор - функция, приводящая к разрушению объекта соответствующего класса и возвращающая системе область памяти, выделенную конструктором. Деструктор имеет имя, аналогичное имени конструктора, но перед ним ставится знак ~:
~kls(void){} или ~kls(){} // функция-деструктор
Рассмотрим использование конструктора и деструктора на примере программы подсчета числа встреч некоторой буквы в символьной строке.
#include <iostream.h>
#include <string.h>
#define n 10
Class stroka
{ int m;
char st[20];
public:
stroka(char *st); // конструктор
~stroka(); // деструктор
void out(char);
int poisk(char);
};
stroka::stroka(char *s)
{ cout << "\n работает конструктор";
strcpy(st,s);
}
stroka::~stroka(void)
{cout << "\n работает деструктор";}
void stroka::out(char c)
{ cout << "\n символ " << c << " найден в строке "<< st<<m<<” раз”;
}
int stroka::poisk(char c)
{ m=0;
for(int i=0;st[i]!='\0';i++)
if (st[i]==c) m++;
return m;
}
void main()
{ char c; // символ для поиска его в строке
cout << "введите символ для поиска его в строке ";
cin >> c;
stroka str("abcadbsaf"); // объявление объекта str и вызов конструктора
if (str.poisk(c)) // подсчет числа вхождений буквы с в строку
str.out(c); // вывод результата
else cout << "\n буква"<<c<<" не найдена"<<endl;
}
В функции main(), при объявлении объекта str класса stroka, происходит вызов функции конструктора, осуществляющего инициализацию поля st этого объекта символьной строкой "abcadbsaf":
stroka str("abcadbsaf");
Все функции-компоненты класса stroka объявлены со спецификатором public и, следовательно, являются глобальными и могут быть вызваны из функции main() . Вызов функции осуществляется с использованием префикса str.
str.poisk(c);
str.out(c);
Поля данных класса stroka объявлены со спецификатором private по умолчанию и, следовательно, являются локальными по отношению к классу stroka. Обращение внутри любой функции-компоненты класса к полям этого же класса производится без использования префикса. За пределами видимости класса эти поля недоступны. Таким образом, обращение к ним из функции main(), например, str.st[1] или str.m, являются ошибочными.
Необходимо также отметить, что количество конструкторов класса может быть более одного. Это возможно, если в объявлении класса имеется несколько полей данных и при определении нескольких объектов этого класса необходимо инициализировать некоторые (определенные для каждого объекта) поля (группы полей). Рассмотренный выше пример программы может быть изменен следующим образом:
#include <iostream.h>
#include <string.h>
#define n 10
Class stroka
{ public:
int m;
char st[20];
stroka(){} // конструктор по умолчанию
stroka(char *); // конструктор 1
stroka(int); // конструктор 2
~stroka(void); // деструктор
void out(char);
int poisk(int);
};
void stroka::stroka(char *s) // описание конструктора 1
{ cout << "работает конструктор 1"<<endl;
strcpy(st,s);
}
stroka(int M) : m(M) // описание конструктора 2
{ cout << "работает конструктор 2"<<endl; }
void stroka::~stroka(void)
{cout << "работает деструктор"<<endl;}
void stroka::out(char c)
{ cout << "символ " << c << " встречен в строке "<<st<< ' '<<m<<" раз\n";
}
int stroka::poisk(int k)
{ m=0;
for(int i=0;st[i]!='\0';i++)
if (st[i]==st[k]) m++;
return m;
}
void main()
{ int i;
cout << "введите номер (0-8) символа для поиска в строке"<<endl;
cin >> i;
stroka str1("abcadbsaf"); // описание и инициализация объекта str1
stroka str2(i); // описание и инициализация объекта str2
if (str1.poisk(str2.m)) // вызов функции поиска символа в строке
str1.out(str1.st[str2.m]); // вызов функции вывода результата
else cout << "символ не встречен в строке "<<str1.st<<" ни разу"<<endl;
}
Как и любая функция, конструктор может иметь как параметры по умолчанию, так и явно описанные.
#include <iostream.h>
Class kls
{ int n1,n2;
public:
kls(int,int=2);
};
kls::kls(int i1,int i2) : n1(i1),n2(i2)
{ cout<<n1<<' '<<n2<<endl;}
void main()
{ kls k(1);
. . .
}
Следует отметить, что компоненты-данные класса желательно описывать в private секции, что соответствует принципу инкапсуляции и запрещает несанкционированный доступ к данным класса (объекта).
Отметим основные свойства и правила использования конструкторов:
- конструктор – функция, имя которой совпадает с именем класса, в котором он объявлен;
- конструктор предназначен для создания объекта (массива объектов) и инициализации его компонент-данных;
- конструктор вызывается, если в описании используется связанный с ним тип:
class cls{ . . .};
main()
{ cls aa(2,.3); // вызывает cls :: cls(int,double)
extern cls bb; // объявление, но не описание, конструктор не вызывается
}
- конструктор по умолчанию не требует никаких параметров;
- если класс имеет члены, тип которых требует конструкторов, то он может иметь их определенными после списка параметров для собственного конструктора. После двоеточия конструктор имеет список обращений к конструкторам типов, перечисленным через запятую;
- если конструктор объявлен в private-секции, то он не может быть явно вызван (из main функции) для создания объекта класса.
Далее выделим основные правила использования деструкторов:
- имя деструктора совпадает с именем класса, в котором он объявлен с префиксом ~;
- деструктор не возвращает значения (даже типа void);
- деструктор не наследуется в производных классах;
- деструктор не имеет параметров (аргументов);
- в классе может быть только один деструктор;
- деструктор может быть виртуальным (виртуальной функцией);
- невозможно получить указатель на деструктор (его адрес);
- если деструктор отсутствует в описании класса, то он автоматически генерируется компилятором (с атрибутом public);
- библиотечная функция exit вызывает деструкторы только глобальных объектов;
- библиотечная функция abort не вызывает никакие деструкторы.
В С++ для описания объектов можно использовать не только ключевое слово class , но также применять ключевые слова struct и union. Различие между АТД class и struct, union состоит в том, что все компоненты struct и union по умолчанию имеют атрибут доступа public. Ниже приведен пример использования union и struct.
#include<iostream.h>
#include<string.h>
union my_union // объявления объединения
{ char str[14]; // компонента - символьный массив
struct my_struct // компонета-структура
{ char str1[5]; // первое поле структуры
char str2[8]; // второе поле структуры
} my_s; // объект типа my_struct
my_union(char *s); // прототип конструктора
void print(void);
};
my_union::my_union(char* s) // описание функции конструктора
{ strcpy (str,s);}
void my_union::print(void)
{ cout<<my_s.str2<<"\n"; // вывод второго поля объекта my_s
my_s.str2[0]=0; // вставка ноль-символа между полями
cout<<my_s.str1<<"\n"; // вывод первого поля (до ноль-символа)
}
void main(void)
{ my_union obj ("MinskBelarus");
obj.print();
}
Результат работы программы:
Belarus
Minsk
Деструктор генерируется автоматически компилятором. Для размещения данных объекта obj выделяется область памяти размером максимального поля union (14 байт) и инициализируется символьной строкой. В функции print() информация, занесенная в эту область, выводится на экран в виде двух слов, при этом используется вторая компонента объединения – структура (точнее два ее поля).
Перечислим основные свойства и правила использования структур и объединений:
- в С++ struct и union можно использовать так же, как класс;
- все компоненты struct и union имеют атрибут public;
- можно описать структуру без имени struct{…} s1,s2,…;
- объединение без имени и без списка описываемых объектов называется анонимным объединением;
- доступ к компонентам анонимного объединения осуществляется по их имени;
- глобальные анонимные объединения должны быть объявлены статическими;
- анонимные объединения не могут иметь компонент-функций;
- компоненты объединения нельзя специфицировать как private, public или protected, они всегда имеют атрибут public;
- union можно инициализировать, но всегда значение будет присвоено первой объявленной компоненте.
Ниже приведен текст программы с разработанным классом. В программе выполняются операции помещения символьных строк на вершину стека и чтение их с вершины стека. Более подробно механизм работы со списками будет рассмотрен в разделах «Шаблоны» и «Контейнерные классы».
#include <iostream.h>
#include <string.h>
class stack // класс СТЕК
{ char *inf; // компонента – данное (симв. строка)
stack *nx; // компонента – данное (указатель на элемент стека)
public:
stack(){}; // конструктор
~stack(){}; // деструктор
stack *push(stack *,char *); // занесение информации на вершину стека
char *pop(stack **); // чтение информации с вершины стека
};
// помещаем информацию (строку) на вершину стека
// возвращаем указатель на вершину стека
stack * stack::push(stack *head,char *a)
{ stack *PTR;
if(!(PTR=new stack))
{ cout << "\nНет памяти"; return NULL;}
if(!(PTR->inf=new char[strlen(a)]))
{ cout << "\nНет памяти"; return NULL;}
strcpy(PTR->inf,a); // инициализация созданной вершины
PTR->nx=head;
return PTR; // PTR -новая вершина стека
}
// pop удаляет информацию (строку) с вершины стека и возвращает
// удаленную строку. Изменяет указатель на вершину стека
char * stack::pop(stack **head)
{ stack *PTR;
char *a;
if(!(*head)) return '\0'; // если стек пуст, возвращаем \0
PTR=*head; // в PTR - адрес вершины стека
a=PTR->inf; // чтение информации с вершины стека
*head=PTR->nx; // изменяем адрес вершины стека (nex==PTR->next)
delete PTR; // освобождение памяти
return a;
}
void main(void)
{ stack *st=NULL;
char l,ll[80];
while(1)
{ cout <<"\n выберите режим работы:\n 1-занесение в стек"
<<"\n 2-извлечь из стека\n 0- завершить работу"<<endl;
cin >>l;
switch(l)
{ case '0': return; break;
case '1': cin >> ll;
if(!(st=st->push(st,ll))) return;
break;
case '2': cout << st->pop(&st); break;
default: cout << " error " << endl;
}
}
}
В данной реализации в main() создается указатель st на объект класса stack. Далее методами класса stack выполняется модификация указателя st. При этом создается множество взаимосвязанных объектов класса stack, образующих список (стек). Подход, более отвечающий принципам объектно-ориентированного программирования, предполагает создание одного или нескольких объектов, каждый из которых уже сам является, например, списком. Дальнейшее преобразование списка осуществляется внутри каждого конкретного объекта. Это может быть продемонстрировано на примере программы, реализующей некоторые функции бинарного дерева.
#include <string.h>
#include <iostream.h>
#define N 20
class tree// класс бинарное дерево
{ struct node// структура - узел бинарного дерева
{ char *inf; // информационное поле
int n; // число встреч информационного поля в бинарном дереве
node *l,*r;
};
node *dr; // указатель на корень дерева
public:
tree(){ dr=NULL;}
void see(node *); // просмотр бинарного дерева
int sozd(); // создание+дополнение бинарного дерева
node *root(){return dr;} // функция возвращает указатель на корень
};
void main(void)
{ tree t;
int i;
while(1)
{ cout<<"вид операции: 1 - создать дерево"<<endl;
cout<<" 2 - рекурсивный вывод содержимого дерева"<<endl;
cout<<" 3 - нерекурсивный вывод содержимого дерева"<<endl;
cout<<" 4 - добавление элементов в дерево"<<endl;
cout<<" 5 - удаление любого элемента из дерева"<<endl;
cout<<" 6 - выход"<<endl;
cin>>i;
switch(i)
{ case 1: t.sozd(); break;
case 2: t.see(t.root()); break;
case 6: return;
}
}
}
int tree::sozd() // функция создания бинарного дерева
{ node *u1,*u2;
if(!(u1=new node))
{ cout<<"Нет свободной памяти"<<endl;
return 0;
}
cout<<"Введите информацию в узел дерева ";
u1->inf=new char[N];
cin>>u1->inf;
u1->n=1; // число повторов информации в дереве
u1->l=NULL; // ссылка на левый узел
u1->r=NULL; // ссылка на правый узел
if (!dr)
dr=u1;
else
{ u2=dr;
int k,ind=0; // ind=1 - признак выхода из цикла поиска
do
{ if(!(k=strcmp(u1->inf,u2->inf)))
{ u2->n++; // увеличение числа встреч информации узла
ind=1; // для выхода из цикла do ... while
}
else
{ if (k<0) // введ. строка < строки в анализируемом узле
{ if (u2->l!=NULL) u2=u2->l; // считываем новый узел дерева
else ind=1; // выход из цикла do ... while
}
else // введ. строка > строки в анализируемом узле
{ if (u2->r!=NULL) u2=u2->r; // считываем новый узел дерева
else ind=1; // выход из цикла do ... while
}
}
} while(!ind);
if (k) // не найден узел с аналогичной информацией
{ if (k<0) u2->l=u1; // ссылка в dr1 налево
else u2->r=u1; // ссылка в dr1 направо
}
}
return 1;
}
void tree::see(node *u) // функция рекурсивного вывода бинарного дерева
{ if(u)
{ cout<<"узел содержит: "<<u->inf<<" число встреч "<<u->n<<endl;
if (u->l) see(u->l); // вывод левой ветви дерева
if (u->r) see(u->r); // вывод правой ветви дерева
}
}
При небольшой модификации в функции main() можно создать несколько объектов класса tree и, например, используя указатель на объект класса tree, вызывать методы класса для работы с каждым из созданных объектов (бинарных деревьев). Это преобразование предлагается выполнить самостоятельно.
Конструктор копирования
Необходимость использования конструктора копирования вызвана тем, что объекты наряду со статическими могут содержать и динамические данные. В тоже время, например, при передаче объекта в качестве параметра функции в ней создается локальная (в пределах функции), копия этого объекта. При этом указатели обоих объектов будут содержать один и тот же адрес области памяти. При выводе локального объекта из поля видимости функции для его разрушения вызывается деструктор. В функцию деструктора входит также освобождение динамической памяти, адрес которой содержит указатель. При окончании работы программы (при вызове деструкторов), производится повторная попытка освободить уже освобожденную ранее память. Это приводит к ошибке. Для устранения этого в класс необходимо добавить конструктор копирования, который в качестве единственного параметра получает ссылку на объект класса. Общий вид конструктора копирования имеет следующий вид:
имя_класса (const имя_класса & );
В этом конструкторе выполняется выделение памяти и копирование в нее информации из объекта получаемого по ссылке. Таким образом, указатели для обоих объектов содержат разные адреса памяти, и при вызове деструктора выполняется освобождение соответствующей каждому из объектов памяти.
#include <iostream.h>
#include <stdlib.h>
#define n 3
//------------------ объявление konstr_copy.h
class cls
{ char *str;
int dl;
// другие данные класса
public:
cls (); // конструктор по умолчанию
cls(cls &); // копирующий конструктор
~cls(); // деструктор
// другие методы класса
};
//------------------ реализация konstr_copy.cpp
#include "konstr_copy.h"
cls::cls ()
{ dl=10;
str=new char[dl];
}
cls::cls(cls & obj1) // копирующий конструктор из obj1 в obj
{ dl=obj1.dl; // копирование длины строки
str=new char[dl]; // выделение памяти “под” отроку длинной dl
strcpy(str,obj1.str); // копирование строки
}
cls::~cls()
{ delete [] str;
cout<<"деструктор"<<endl;
}
void fun(cls obj1)
{ // код функции
cout<<" выполняется функция "<<endl;
}
void main(void)
{ cls obj;
// . . .
fun(obj);
// . . .
}
Если для класса конструктор копирования явно не описан, то компилятор сгенерирует его. При этом значения компоненты-данного одного объекта будут скопированы в компоненту-данное другого объекта. Это допустимо для объектов простых классов и недопустимо для объектов, имеющих динамические компоненты-данные (конструируются с использованием операторов динамического выделения памяти). Таким образом, даже если в классе не используются динамические данные, желательно явно описывать конструктор копирования. Далее использование конструктора копирования будет рассмотрено в пункте ”особенности перегрузки операции =”.
Конструктор explicit
В С++ компилятор для конструктора с одним аргументом может автоматически выполнять неявные преобразования. В результате этого тип, получаемый конструктором, преобразуется в объект класса, для которого определен данный конструктор.
#include <iostream.h>
class array // класс массив целых чисел
{ int size; // размерность массива
int *ms; // указатель на массив
public:
array(int = 1);
~array();
friend void print(const array&);
};
array::array(int kl) : size(kl)
{ cout<<"работает конструктор"<<endl;
ms=new int[size]; // выделение памяти для массива
for(int i=0; i<size; i++) ms[i]=0; // инициализация
}
array::~array()
{ cout<<"работает деструктор"<<endl;
delete [] ms;
}
void print(const array& obj)
{ cout<<"выводится массив размерностью"<<obj.size<<endl;
for(int i=0; i<obj.size; i++)
cout<<obj.ms[i];
cout<<endl;
}
void main()
{ array obj(10);
print(obj); // вывод содержимого объекта obj
print(5); // преобразование 5 в array и вывод
}
В результате выполнения программы получим:
работает конструктор
выводится массив размерностью 10
0 0 0 0 0 0 0 0 0 0
работает конструктор
выводится массив размерностью 5
0 0 0 0 0
работает деструктор
работает деструктор
В данном примере в инструкции:
array obj(10);
определяется объект obj и для его создания (и инициализации) вызывается конструктор array(int). Далее в инструкции:
print(obj); // вывод содержимого объекта obj
выводится содержимое объекта obj, используя friend-функцию print(). При выполнении инструкции:
print(5); // преобразование 5 в array и вывод
компилятором не находится функция print(int) и выполняется проверка на наличие в классе array конструктора, способного выполнить преобразование в объект класса array. Так как в классе array имеется конструктор array(int), а точнее просто конструктор с одним параметром, то такое преобразование возможно (создается временный объект, содержащий массив из пяти чисел).
В некоторых случаях такие преобразования являются нежелательными или, возможно, приводящими к ошибке. В С++ имеется ключевое слово explicit для подавления неявных преобразований. Конструктор, объявленный как explicit:
explicit array(int = 1);
не может быть использован для неявного преобразования. В этом случае компилятором (в частности Microosft C++) будет выдано сообщение об ошибке:
Compiling...
error C2664: 'print' : cannot convert parameter 1 from 'const int' to 'const class array &'
Reason: cannot convert from 'const int' to 'const class array'
No constructor could take the source type, or constructor overload resolution was ambiguous
Если необходимо при использовании explicit-конструктора все же создать массив и передать его в функцию print, то надо использовать инструкцию
print(array(5));
Правильно сконструировав классы можно достичь, чтобы создание объектов было разрешено, а нежелательные неявные преобразования типов запрещены. Рассмотрим пример, в котором целочисленный размер массива может быть передан в качестве параметра конструктору и недопустимы преобразования целых чисел во временный объект.
class array // класс массив целых чисел
{ public:
class array_size// класс
{ public:
array_size(int _kl):kl(_kl){}
int size() const { return kl;}
private:
int kl;
};
array(int n,int m)
{ this->n=n;
ms=new int[this->n];
for(int i=0;i<this->n;i++) ms[i]=m;
}
array(array_size _size)
{ n=_size.size();
ms=new int[n];
}
~array(){ delete [] ms;}
private:
int n;
int *ms;
};
main()
{ array a(1);
. . .
};
Компилятор для объекта a генерирует вызов конструктора array(int). Но такого конструктора не существует. Компиляторы могут преобразовать аргумент типа int во временный объект array_size, поскольку в классе array_size имеется конструктор с одним параметром. Это обеспечивает успешное создание объекта.