- •29 Марта 2005 г., протокол № 5
- •Введение
- •1. Классы. Первое знакомство
- •2. Классы и функции
- •Указатель this
- •3. Наследование, виртуальные функции и полиморфизм
- •4. Массивы объектов, указатели и ссылки
- •5. Шаблоны и исключительные ситуации
- •6. Потоки и классы ввода/вывода
- •7. Статические и константные члены, локальные классы
- •Объектно-ориентированное
- •220050, Минск, пр. Независимости, 4.
- •220004, Минск, ул. Освобождения, 9.
2. Классы и функции
При реализации класса используются функции-члены класса (методы класса), функции-“друзья” класса, конструкторы, деструкторы, функции-операторы. Функции-члены класса объявляются внутри класса. Определение функции обычно помещается вне класса. При этом перед именем функции помещается операция доступа к области видимости имя_класса::. Таким образом, определение функции-члена класса имеет вид:
тип имя_класса:: имя_функции (список аргументов) { }
Определение функции может находиться и внутри класса вслед за его объявлением. Такие функции называются inline-функциями. Рассмотрим пример:
#include <iostream.h> // пример 20
#include <string.h>
#include <stdlib.h>
#include <conio.h>
#define SIZE 255
class Strtype {
char *p;
int len;
public:
Strtype(){// инициализация объекта, inline конструктор
p=new char [SIZE];
if(!p) {cout << "ошибка выделения памяти\n";
exit(1);}
*p='\0';
len=0;
}
~Strtype(); //деструктор
void set(char *ptr);
char *get(){return p;}
void show();
};
// освобождение памяти при удалении объекта строка
inline Strtype::~Strtype(){ //inline-деструктор
cout << "освобождение p\n";
delrete [] p;
}
void Strtype::set(char *ptr) {
if(strlen(ptr) >= SIZE) {
cout << "строка слишком велика \n";
return;
}
strcpy(p, ptr);
len = strlen(p);
}
void Strtype::show(){
cout << p << " длина : " << len<< "\n";
}
int main(){
{Strtype s1,s2;
s1.set("This is a test");
s2.set("I love C++");
s1.show();
s2.show();
cout<<s2.get()<<endl; }
while (!kbhit());
return 0;
}
В результате будет выведено:
This is a test длина: 14
I love C++ длина: 10
I love C++
Освобождение р
Освобождение р
Конструктор Strtype() и функция get() являются inline-функциями. Деструктор ~Strtype() также является inline – функцией, хотя и определяется вне класса. Ключевое слово inline содержит указание компилятору создать код функции, подставляемый в точку вызова. При этом время вызова сокращается, хотя код может увеличиться.
Вызов функций-членов класса осуществляется одним из двух способов:
имя_объекта.имя_функции(аргументы);
указатель_на_объект -> имя_функции(аргументы);
Еще раз обращаем внимание на то, что при определении функции- члена класса перед именем функции стоит имя класса, а при вызове –имя объекта класса.
Указатель this
Обращение к данным класса осуществляется с помощью функций, которые могут вызываться различными объектами класса. Возникает вопрос: откуда функция “знает”, какой объект его вызвал и какие данные при этом должны быть использованы? Например, в примере 20 вызовы функции s1.show() и s2.show() приводят к выводу различных для s1и s2 значений строк р. Это происходит вследствие того, что каждый объект имеет скрытый указатель this на самого себя, объявляемый неявно. Значением этого указателя, который автоматически передается функциям-членам класса при их вызове, является адрес начала объекта. В результате функция show() в реальности содержит инструкцию:
cout << this->p << " длина : " << this->len<< "\n";
При этом *this представляет сам объект, this->имя_члена – ссылка на данные объекта класса. Рассмотрим пример:
#include<iostream.h> // пример 21
#include<conio.h>
class MyClass {
int x,y;
public:
void set(int xx, int yy) { //устанавливает данные
x=xx;
y=yy;} //равносильно this->x=xx;this->y=yy;
MyClass f(MyClass &); //возвращает объект
MyClass *ff(){ //возвращает указатель на вызвавший объект
x=y=100;
return this;}
void display(){
cout<<x<<'\t'<<y<<endl;}
};
MyClass MyClass::f(MyClass &M){
x+=M.x; y+=M.y;
return *this;}
int main(){
MyClass a, b;
a.set(10,20);
b.ff()->display();//результат 100,100
b.display(); //результат 100,100
a.display(); //результат 10,20
a.f(b).display(); //результат 110,120
a.display(); //результат 110,120
while (!kbhit());
return 0;
}
Так как функция b.ff() возвращает указатель this на объект b, то можно вызвать функцию display() как this->display(). При этом выводятся поля данных объекта b(100,100). При вызове a.f(b).display() функция f(b) возвращает значение вызвавшего ее объекта a как *this. Затем вызывается функция display() как a.display().
Отметим, что в функцию лучше передавать не значение объекта, а ссылку на него. Это дает экономию стековой памяти, поскольку объект не копируется, и более безопасно для объектов, использующих динамическую память. Уничтожение копий аргументов при выходе из функции не может разрушить такие объекты при освобождении динамической памяти.
Функции-друзья класса, объявляемые со спецификатором friend, указатель this не содержат. Объекты, с которыми работают такие функции, должны передаваться в качестве их аргументов.
Конструкторы и деструкторы
Конструктор – это функция-член класса, которая вызывается автоматически для создания и инициализации экземпляра класса. Известно, что экземпляры структур можно инициализировать при объявлении:
struct Student{
int semhours;//public по умолчанию
char subj; }
Student s={0,”c” };//объявление и инициализация
При использовании класса приложение не имеет доступа к его защищенным элементам, поэтому для класса подобную инициализацию выполнить нельзя. Для этой цели используются специальная функция-член класса, инициализирующая экземпляр класса и указанные переменные при создании объекта. Подобная функция вызывается всегда при создании объекта класса. Это и есть конструктор – функция-член класса, которая имеет то же имя, что и класс. Конструктор может быть подставляемой (inline) и неподставляемой функциями. Рассмотрим пример:
#include<iostream.h> // пример 22
class Student{
int semhours;
char subj;
public:
Student() //inline-конструктор 1 без параметров
{semhours=0; subj='A';}
Student(int, char); //объявление конструктора 2 с параметрами
};
Student::Student(int hours,char g)//конструктор 2
{semhours=hours; subj=g;}
int main(){
student s(100,'A'); //конструктор 2
student s1[5]; //конструктор1 вызывается 5 раз
return 0;
}
Там, где находятся объявления s и s1, компилятор помещает вызов соответствующего конструктора Student().
Конструктор не имеет типа возвращаемого значения, хотя может иметь аргументы и быть перегружаемым. Он вызывается автоматически при создании объекта, выполнении оператора new или копировании объекта. Если конструктор отсутствует в классе, компилятор С++ генерирует конструктор по умолчанию.
Деструктор вызывается автоматически при уничтожении объекта, и имеет то же имя, что и класс, но перед ним стоит символ “~”. Деструктор можно вызывать явно в отличие от конструктора. Конструкторы и деструкторы не наследуются, хотя производный класс может вызывать конструктор базового класса.
В следующем примере используется конструктор копирования, который необходимо вводить из-за проблемы, связанной с динамической памятью. Когда объект передается в функцию по значению, то создается его копия, при этом конструктор не вызывается. При выходе из функции объект уничтожается и вызывается деструктор, освобождающий его динамическую память. В этом случае исходный объект, использующий ту же динамическую память, что и копия, может быть поврежден. Аналогично, если функция возвращает объект, содержащий динамические переменные, при выходе из функции копия этого объекта разрушается вызовом деструктора. Возвращаемый объект при этом также может быть разрушен. Равным образом это повторяется и при инициализации объявляемого объекта вида:
Class_type B=A;
При этом происходит побитовое копирование объекта А в объект В. Однако для массива в объекте B память динамически не выделяется, а происходит только копирование указателя на массив. Если затем объект А, содержащий динамический массив, разрушается вызовом деструктора, то объект В также разрушается. Поэтому при таком объявлении должен вызваться конструктор копирования.
Общая форма конструктора копирования имеет вид:
имя_класса (const имя_класса &ob) {
/*выделение памяти и копирование в нее ob*/
}
Здесь ob является ссылкой на объект в правой части инициализации.
Рассмотрим пример использования конструктора копирования при создании безопасного динамического массива.
/*создается класс "безопасный" массив. Когда один объект-массив используется для инициализации другого, для выделения памяти вызывается конструктор копирования*/
#include <iostream.h> // пример 23
#include <stdlib.h>
#include <conio.h>
class Array {
int *p;
int size;
public:
Array (int s) { cout << "constructor1 \n";
size=s;
p=new int[size];
if(!p) exit(1);
}
~Array() {delete [ ] p;}
Array(const Array &a);//конструктор копирования
void put(int i, int j) {
if(i>=0 && i<size) p[i]=j;
}
int get(int i) {
return p[i]; }
};
/* конструктор копирования. Память выделяется для копии, адрес памяти передается p.*/
Array:: Array(const Array &a) {
p=new int[a.size]; //выделение памяти для копии
if(!p) exit(1);
for(int i=0;i<a.size;i++)
p[i]=a.p[i];//копирование содержимого в память для копии
cout << "constructorcopy2\n";}
int main(){
Array y(5); //вызов обычного конструктора
for(int i=0;i<5;i++){
y.put(i, i+5); //помещение в массив нескольких значений
cout << y.get(i); //вывод на экран num
}
cout << "\n";
Array x=y; /*создание массива x и инициализация, вызов конструктора копирования */
// другой способ вызова - объявление: Array x(y);
y=x;//конструктор не вызывается !!!
while(!kbhit());
return 0;
}
Результат:
constructor1
56789
constructorcpy2
Объект y используется при инициализации объекта х с помощью конструктора копирования. При этом выделяется динамическая память, адрес которой в x.p, и производится копирование y в x. После этого y и x не разделяют одну и ту же динамическую память. Если же присваивание осуществляется не при инициализации, то конструктор копирования не вызывается, а происходит побитовое копирование объекта. В результате двумя объектами используется одна и та же динамическая память. Так происходит в случае присваивания y=x в примере.
В следующем примере конструктор копирования вызывается при передаче объекта-строки в качестве аргумента функции show():
#include <iostream.h> // пример 24
#include <stdlib.h>
#include <conio.h>
class Strtype {
char *p;
int l;
public:
Strtype(char *s='\0') //конструктор
{ l=strlen(s);
p=new char[l+1];
if(!p) {cout << "ошибка при выделении памяти\n";exit(1); }
strcpy(p, s);
cout<<"constr1\n";
}
Strtype(const Strtype &o)//конструктор копирования
{ l=strlen(o.p);
p=new char[l+1];//выделение памяти для новой копии
if(!p) {cout << "ошибка памяти\n";exit(1);}
strcpy(p, o.p); //передача строки в копию
cout<<"constrcpy\n";
}
~Strtype() {delete [ ] p;cout<<"destr\n";} //деструктор
char *get() {return p;}
};
void show(Strtype x)
{ char *s;
s=x.get();cout << s << "\n";}
int main(){
Strtype a("Hello"), b("There");//constr1,constr1
show(a);//конструктор копирования constrcpy,Hello,destr
show(b);//конструктор копирования constrcpy,There,destr
while(!kbhit());
return 0;
}
Когда функция show() завершается и х выходит из области видимости, то освобождается память х.р, однако не та, которая используется передаваемым в функцию объектом.
Организация внешнего доступа к компонентам класса. Функции “друзья” класса.
В языке С++ одна и та же функция не может быть компонентом двух разных классов. Чтобы предоставить функции возможность выполнения действий над различными классами можно определить обычную функцию языка С++ и предоставить ей право доступа к элементам класса типа private, protected. Для этого нужно в описании класса поместить заголовок функции, перед которым поставить ключевое слово friend. Рассмотрим пример:
//пример 25
//функция sameColor() сравнивает цвет прямоугольника и линии
class Line;//неполное объявление класса
class Box {
int color; // цвет прямоугольника
int upx, upy; // левый верхний угол
int lowx, lowy; //правый нижний угол
public:
friend int sameColor(line l, box b); // объявление friend-функции
void setColor(int c);
void defBox(int x1, int y1, int x2, int y2);
} ;
class Line {
int color;
int startx, starty;
int endx,endy;
public:
friend int sameColor(Line l, Box b);
void setColor(int c);
void defLine(int x, int y, int l);
} ;
int sameColor(Line l, Box b){
if(l.color==b.color) return 1;
return 0;
}
Если не использовать friend-функцию, то необходимо сначала вернуть цвета объектов, а затем написать функцию их сравнения. В приведенном примере класс Box использует класс Line, объявленный позже. Поэтому перед объявлением класса Box ставится предварительное неполное объявление вида:
class имя_класса;.
Хотя дружественная функция “знает” элементы класса, доступ к ним возможен только через объект класса, т. к. указатель this ей не передается. В следующем примере дружественная функция использует указатель на класс в качестве параметра и для доступа к элементам i и k через указатель. Прямой доступ к ним невозможен.
class X{ //пример 26
int i,k;
public:
void memFunc(int);
friend void frFunc(X*, int);
};
void X::memFunc(int a) {
i=a; k=a;}//прямой доступ
void fr_func(x *xptr, int a) {
xptr->i=a; xptr->k=a;
}//доступ к i и k через указатель, ошибкa, если записать к=а
int main(){
X xobj;//объявление объекта
frFunc(&xobj, 6);//вызов дружественной функции
xobj.memFunc(6);//вызов члена класса
return 0;
}
Разрешается объявлять функции-члены класса дружественными для другого класса. В следующем примере функция memF() член класса X получает доступ к защищенным полям класса Y.
Class Y;
class X{//...
void memF(Y ob); };
class Y {//…
friend void X::memF(Y ob);
};
Операторы-функции
Операторы-функции используются для введения операций, связанных с символами:
+ , - , * , / , % , ^ , & , | , ~ , ! , = , < , > ,+= , [ ] , -> , ( ) , new, delete.
Оператор-функция является членом класса или дружественной классу. Общая форма оператора-функции члена класса:
возвращаемый_тип имя_класса::operator#(список_аргум){
/*выполняемые действия */ }
После этого вместо operator#(a,b) можно писать a#b. Здесь символ # представляет один из введенных выше символов. В качестве примера можно привести операции >>, <<, перегружаемые для ввода-вывода. Отметим, что при перегрузке нельзя менять приоритет операторов и число операндов. Если оператор-функция, являющаяся членом класса перегружает бинарный оператор, у функции будет только один параметр-объект, находящийся справа от знака оператора. Объект слева вызывает оператор-функцию и передается неявно с помощью указателя this. Например:
// перегрузка +, = и ++ для класса coord. // пример 27
#include <iostream.h>
#include <conio.h>
class Coord {
int x, y; // значения координат
public:
Coord(int i=0, int j=0) { x = i; y = j; }
void getXY(int &i, int &j) { i = x; j = y; }
Coord operator+(Coord ob2);
Coord operator=(Coord ob2);
Coord operator++(); // префиксная форма
};
Coord Coord::operator+(Coord ob2) //перегрузка +
{ Coord temp;
temp.x = x + ob2.x; // temp.x=this->x+ob2.x
temp.y = y + ob2.y; // temp.y=this->y+ob2.y
return temp;
}
Coord Coord::operator=(Coord ob2) //перегрузка =
{ x = ob2.x; // this->x=ob2.x
y = ob2.y; // this->y=ob2.y
return *this;//возвращение объекта, которому присвоено значение
}
Coord Coord::operator++() //перегрузка ++, унарный оператор
{ x++; y++;
return *this;
}
int main(){
Coord o1(10, 10), o2(5, 3), o3;
int x, y;
o3 = o1 + o2; //сложение двух объектов. Вызов operator+()
o3.getXY(x, y);
cout << "(o1 + o2) X: " << x << ", Y: " << y << "\n";
o3 = o1; //присваивание объектов
o3.getXY(x, y);
cout << "(o3 = o1) X: " << x << ", Y: " << y << "\n";
++o1; //инкрементация объекта
o1.getXY(x, y);
cout << "(++o1 ) X: " << x << ", Y: " << y << "\n";
while(!kbhit());
return 0;
}
Результат:
(о1+о2) X:15 ,y:13
(о3=о1) X:10, y:10
(++o1) X:11, Y:11
При перегрузке унарного оператора ++ единственный параметр-объект передается через указатель this.
В следующем примере вводятся класс “множество” и операции && – пересечения, << – вывод множества.
#include <iostream.h> //пример 28
#include <conio.h>
class Set {
char *pl; //указатель на множество элементов типа char
public:
Set(char *pl)//конструктор
{pi=new char[strlen(pl)+1];strcpy(pi,pl);}//конструктор
Set &operator &&(Set &); //перегрузка &&-пересечение
friend ostream &operator<<(ostream &stream,Set &ob);//перегрузка<<
~Set(){delete [] pi;}//деструктор
} ;
Set& Set::operator &&(Set &s) // пересечение A=A^B
{int l=0;
for (int j=0;pi[j]!=0;j++)
for (int k=0;s.pi[k]!=0;k++)
if (pi[j]==s.pi[k]) {
pi[l]=pi[j];l++;break;}
pi[l]=0;
return *this;
}
ostream &operator<<(ostream &stream, Set &ob) {
stream << ob.pi << '\n'; /*перегрузка вывода */
return stream;
}
int main(){
Set s1("1f2bg5e6"),s2("abcdef");
Set s3=s2;
cout<<(s1&&s2)<<endl;//результат fbe
cout<<s3<<endl;//результат abcdef
while(!kbhit());
return 0;
}
Оператор присваивания
Когда одному объекту присваивается значение другого, происходит его копирование. Если побитовое копирование нежелательно из-за того, что копии объектов ссылаются на одну и ту же динамическую память, можно использовать собственную функцию копирования operator=(). Рассмотрим пример:
Strtype &Strtype::operator=(const Strtype &ob){
if(&ob==this) return *this;
if(l < ob.l) { //требуется выделение дополнительной памяти
delete[ ] p;
p = new char [ob.l+1];
if(!p) { cout << "ошибка выделения памяти \n";exit(1);}
}
l = ob.l;
strcpy(p, ob.p);
return *this;
}
Здесь operator=() возвращает ссылку на объект. Параметром также является ссылка, что запрещает создание копии объекта, стоящего справа от операции присваивания. Добавление этого оператора в класс позволяет копировать объекты с помощью оператора присваивания, который не наследуется и должен быть членом класса.
Использование дружественных оператор-функций
В дружественную функцию не передается указатель this, поэтому при перегрузке для унарного оператора в оператор-функцию передается один параметр, для бинарного – два. Оператор присваивания не может быть дружественной функцией, а только членом класса. В следующем примере рассматривается использование дружественных оператор-функций +, *, /, <<, >> для класса Complex.
#include <iostream.h>//пример 29
#include <stdio.h>
#include <conio.h>
class Complex {
double re, im;
public:
Complex (double r=0,double i=0)//конструктор
{re=r;im=i;}
friend Complex operator+(Complex, Complex);
friend Complex operator*(Complex, Complex);
friend Complex operator/(Complex, Complex);
friend ostream & operator<<(ostream &stream, Complex ob);
friend istream & operator>>(istream &stream, Complex ob);
};
ostream &operator<<(ostream &stream, Complex ob) {
stream <<"("<< ob.re << ", " << ob.im <<")"<< '\n';
return stream;
}
istream &operator>>(istream &stream, Complex &ob){
//double a,b;
//scanf("(%lf,%lf)",&a,&b);
//ob=Complex(a,b);
stream >> ob.re >> ob.im;
return stream;
}
Complex operator+(Complex a1, Complex a2){
return Complex(a1.re+a2.re,a1.im+a2.im);
}
Complex operator*(Complex a1, Complex a2){
return Complex(a1.re*a2.re-a1.im*a2.im, a1.re*a2.im+a1.im*a2.re);
}
Complex operator/(Complex a1, Complex a2){
double r=a2.re,i=a2.im;
double tr=r*r+i*i;
return Complex((a1.re*r+a2.im*i)/tr,(a1.im*r-a1.re*i)/tr);
}
int main(){
Complex a(1.,2.),b(2.,2.),c;
c=a+b;
cout<<c<<a*b<<a/b<<endl;
cout<<"Enter complex number:";
cin>>c;
cout<<c;
while(!kbhit());
return 0;
}
Вывод:
(3,4) (-2,6) (0.75,0.25)
//Enter complex number:(5,7)<ввод>
//(5,7)
Операторы-функции, объявленные как friend внутри класса Complex, не являются членами класса, но подобно им они имеют доступ к закрытым элементам класса. Объект класса при этом должен быть параметром, иначе функция не знает, с каким объектом работать (this не передается). В C++ используется определенный в файле complex.h библиотечный класс complex, который представляет тип данных, включающий и другие операции над комплексными числами.
Рассмотрим еще один пример использования операций при создании бинарного дерева.
#include <iostream.h>//пример 29а
#include <conio.h>
struct Node{ // узел дерева
char info; // информационная часть узла
int count; // число повторений информации
Node* nextl,*nextr; // указатели на левое и правое поддеревья
Node(char newinfo='a') {
info=newinfo;
nextl=nextr=0;
count=1;}
};
class Tree { //класс дерево
public:
Node* root; //указатель на корень дерева
Tree() { // конструктор формирует пустое дерево
root=0;}
void push(Node * &wer, char dat) { // добавление элемента
if(wer==0){
wer = new Node;
wer->nextl=0;
wer->nextr=0;
wer->info = dat;}
else if(dat<wer->info) push(wer->nextl, dat);
else if(dat>wer->info) push(wer->nextr, dat);
else wer->count++; //root=wer;
}
void insert(char dat) { // вставка в дерево
if (root==0) root=new Node(dat);
else push(root, dat);
}
void look(node *&wer) { // обход дерева
if (wer!=0) {
look(wer->nextl);
cout<<" "<<wer->info<<" - "<<wer->count;
look(wer->nextr);
}
}
Tree operator+(char c) {
Tree temp;
insert(c);
temp.root=root;
return temp;}
Tree operator=(Tree ob2) {
root=ob2.root;
return *this;}
friend ostream &operator<<(ostream &stream,Tree ob);
};
ostream &operator<<(ostream &stream,tree ob) {
ob.look(ob.root);
return stream;}
int main(){
Tree q;
Node *n;
char c,key='a' ;
do {
cout << "Input a char until *: ";
cin >> c;
q.insert(c); }//ввод
while(c != '*');
q.look(q.root); //просмотр
cout << "Input a char :";
cin>>c;
q=q+c;
cout<<q;
while(!kbhit());
return 0;
}
