Лекции по основам программирования на C/C++

Вид материалаЛекции

Содержание


Лекция 13. Создание и разрушение объектов
Конструкторы с параметрами.
Лекция 14.Наследование, виртуальные методы
Виртуальные методы.
Лекция 15. Указатели на объекты, конструктор по умолчанию, конструктор копий, операция присваивания и деструктор
Элементы класса, о которых всегда необходимо помнить.
Конструктор по умолчанию.
Конструктор копий.
Аргументы по умолчанию в конструкторе.
Виртуальный деструктор.
Подобный материал:
1   2   3   4   5   6

^ Лекция 13. Создание и разрушение объектов


Для автоматической инициализации создаваемых объектов в С++ используется функция - конструктор (constructor function), которая включается в описание класса.

Функция конструктор имеет тоже имя, что и класс и не возвращает ни какого значения.

Пример:


#include

// Объявление класса class1

class class1 {

int a;

public:

class1(); // Конструктор

void ft();

};


// Инициализация а конструктором при создании объекта pr

class1::class1()

{

a=100;

}


//Функция возведения в квадрат и печати

void class1::kwadrat()

{

cout << a*a;

}


main()

{

class1 obj//Создание объекта obj

obj.f(); //Вызов функции f()

return 0;

}

Как видно из примера конструктор вызывается при создании объекта obj.

Деструкторы. Функция деструктор (destructor)вызывается при удалении объекта для освобождения ресурсов (памяти и т.д.). Она также включается в объявление класса. Перед описанием деструктора ставится значок ~.

Пример.


#include

// Объявление класса class1

class class1 {

int a;

public:

class1(); // Конструктор

~class1(); //Деструктор

void f();

};


// Инициализация а конструктором при создании объекта obj

class1::class1()

{

a=100;

}

//Освобождение ресурсов деструктором

class1::~class1()

{

cout<<"Освобождение\n";

}


//Функция возведения в квадрат и печати

void class1::f()

{

cout << a<<"\n";

}


main()

{

class1 obj;//Создание объекта pr

obj.f(); //Вызов функции f()

return 0;

}


^ Конструкторы с параметрами.Конструктору можно передать параметры. Для этого нужно добавить необходимые параметры в объявление и определение конструктора. Затем при объявлении объекта параметры задаются в качестве аргумента.

Пример:


#include

class myclass {

int a;

public:

myclass(int x); // конструктор

void show();

};


myclass::myclass(int x)

{

cout << "В конструкторе\n";

a = x;

}


void myclass::show()

{

cout << a << "\n";

}


main()

{

myclass ob(4);

ob.show();

return 0;

}


Конструктор myclass имеет один параметр. Значение, передаваемое в myclass() используется для инициализации а. Аргумент 4 передается в ob(4) в качестве аргумента. Деструктор в отличие от конструктора параметров не имеет.

В данном примере конструктору мы передавали константы, но так же можно передавать переменные:

Пример:

#include

class myclass {

int i, j;

public:

myclass(int a, int b);

void show();

};

myclass::myclass(int a, int b)

{

i = a;

j = b;

}

void myclass::show()

{

cout << i << ' ' << j << "\n";

}

main()

{

int x, y;

cout << "Введите два целых: ";

cin >> x >> y;

myclass ob(x, y);// использование переменных для создания ob

ob.show();

return 0;

}

В программе рассмотрено важное свойство объектов. Они могут создаваться по мере необходимости.

include
class DEMO// Этот класс просто демонстрирует неявные вызовы
// конструктора и деструктора
{
public:
DEMO() { cout « "constructor" « endl; }

~DEMO() { cout « "destructor" « endl; }
};
void main(void)
{
DEMO staticDemo; // статическое размещение, деструктор вызывается при
// выходе за пределы области видимости
DEMO *dynamicDemo = new DEMO;
// динамическое размещение,
// деструктор вызывается при уничтожении объекта
delete dynamicDemo;
}
В этом примере определяется класс, не содержащий ничего, кроме открытых (public) деструктора и конструктора. Обе этих функции объявлены и определены в классе. При создании объекта неявно вызывается конструктор и печатается слово "constructor", а при вызове деструктора, соответственно, слово "destructor".
Внутри функции main() создаются два объекта, один статический, в стеке, а второй в куче (heap) - динамический. В результате выполнения этого примера на экран будет выведено следующее:
constructor
constructor
destructor
destructor
Первая строка выводится конструктором при создании объекта staticDemo. Вторая строка выводится конструктором при создании объекта dynamicDemo. Третья строка – результат вызова деструктора объекта dynamicDemo при его уничтожении оператором delete. Четвертая строка – результат вызова деструктора объекта staticDemo. Деструктор статического объекта был вызван последним, при выходе объекта из области видимости.


^ Лекция 14.Наследование, виртуальные методы

Наследование - это механизм, посредством которого один класс (производный) может наследовать свойства другого класса (базового).

Базовый класс определяет все качества, которые являются общими для всех производных классов.

Пример:

//Базовый класс

class B {

int i;

public:

void set_i(int n);

int get_i();

};

//Производный класс D

class D : public B {

int j;

public:

void set_j(int n);

int mul();

};

После имени класса D стоит двоеточие, за которым стоит ключевое слово public и имя класса B. Это означает, что класс D будет наследовать все компоненты класса B. Само ключевое слово public информирует компилятор о том, что т.к. B будет наследоваться, то все открытые элементы базового класса будут открытыми элементами производного класса. Однако все закрытые элементы базового класса остаются закрытыми.

// Простой пример наследования.

#include

// Задание базового класса

class base {

int i;

public:

void set_i(int n);

int get_i();

};

// Задание производного класса

class derived : public base {

int j;

public:

void set_j(int n);

int mul();

};

// Установка значения i в базовом классе

void base::set_i(int n)

{

i = n;

}

// Возврат значения i в базовом классе

int base::get_i()

{

return i;

}

// Установка значения j в производном классе

void derived::set_j(int n)

{

j = n;

}

// Возврат значения i из base и, одновременно, j из derived

int derived::mul()

{

// производный класс может вызывать методы базового класса

return j * get_i();

}

main()

{

derived ob;

ob.set_i(10); // загрузка i в base

ob.set_j(4); // загрузка j в derived

cout << ob.mul(); // вывод числа 40

return 0;

}

Важно! При определении mul() вызывается метод get_i()- базового класса B, а не производного D, что указывает на то, что открытые члены базового класса становятся открытыми членами производного. Но в методе mul() вместо прямого доступа к i, необходимо вызывать get_i(), потому что закрытые члены базового класса(i) остаются закрытыми для производных классов.

^ Виртуальные методы.Кратко проблема может быть сформулирована следующим образом: как будет вызываться метод производного класса, имеющий такое же название, что функция базового класса. Рассмотрим следующий пример.

#include


class base {

public:

int i;

base(int x); //конструктор

void func()

{

printf("Базовая функция %d",i);

return;

};

};

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

base::base(int x)

{

i=x;

return;

};

class derived: public base {

public:

derived(int x) :base(x) {}; //конструктор

void func()

{

printf("Метод из производного класса %d", i*i);

return;

}

};

main()

{

base * pc; //указатель на базовый класс

base ob(2); //создать экземпляр объекта базового класса

der1 ob1(2); //создать экземпляр объекта производного класса

pc=&ob; //указатель на объект базового класса

pc->func(); //вызов метода базового класса

pc=&ob1; //указатель на объект производного класса

pc->func(); //попытка вызова метода производного класса

return 0;

}


На первый взгляд кажется, что в первом случае будет вызываться метод базового класса, а во втором метод производного. Однако при проверке Вы легко убедитесь, что и в том и в другом случае будет вызван метод базового класса. В чем тут дело? Дело в том, что компилятору трудно понять, какой реально метод мы имеем в виду и он на стадии компилирования подставляет во всех тех случаях, где встречается имя func() адрес метода базового класса. Такой процесс установки адресов называется "ранним связыванием". Иногда употребляется термин "статическое связывание". Если же мы хотим, чтобы во втором случае, т.е. когда указатель pc указывал на производный класс вызывался метод этого класса, ее еще в базовом классе следует указать как виртуальный. В нашем случае вместо строки void func() следует написать virtual void func(). После этого наш пример будет работать как надо.

Как видите ситуация несколко напоминает проблему перегрузки. Однако перегружаемые функции отличаются друг от друга типом или аргументами, здесь же функции ,а, точнее, методы должны быть идентичны.

В случае использования виртуальных методов адрес вызываемого метода будет определяься в процессе выполнения кода программы. Такой процесс называется "поздним связыванием", употребляется также термин "динамическое связывание".

Для дальнейшего уяснения свойств виртуальных методов рассмотрим еще один пример, являющийся развитием первого.

#include

class base {

public:

int i;

base(int x); //конструктор

virtual void func()

{

printf("Базовый метод %d\n",i);

return;

};

};

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

base::base(int x)

{

i=x;

return;

};

class derived: public base {

public:

derived(int x) :base(x) {}; //конструктор

void func()

{

printf("Метод из производного класса %d\n", i*i);

return;

}

};

class derivel2: public base {

public:

derived2 (int x) :base(x) {}; //конструктор

};


main()

{

base * pc; //указатель на базовый класс

base ob(2); //создать экземпляр объекта базового класса

derived1 ob1(2); //создать экземпляр объекта производного класса 1

derived2 ob2(2); //создать экземпляр объекта производного класса 2

pc=&ob; //указатель на объект базового класса

pc->func(); //вызов метода базового класса

pc=&ob1; //указатель на объект производного класса 1

pc->func(); //попытка вызова метода производного класса

pc=&ob2; //указатель на объект производного класса 2

pc->func(); //попытка вызова метода производного класса

return 0;

}

Как видите мы ввели еще один производный класс. В нем метод func() не определен. В этом случае будет вызываться метод класса родителя. Т.е. появится строка: Базовый метод 2. Как видите принцип очень прост: если Вы хотите, чтобы вызывался метод родительского класса, не определяйте его в производном. Еще один вопрос может возникнуть в связи с данным примером: как быть, если мы хотим, чтобы для класса объектов derived2 вызывался метод класса derived1. Решение очень просто - сделайте класс derived2 наследником не класса base, а класса derived1.

И последнее. Как мы видели, в производных классах метод, определенный в базовом классе как виртуальный может определяться, а может и нет. Если Вы хотите, чтобы во всех производных классах обязательно была определена виртуальный метод, то в базовом классе его надо определить следующим образом:


virtual void func() = 0;


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

^ Лекция 15. Указатели на объекты, конструктор по умолчанию, конструктор копий, операция присваивания и деструктор


Доступ к члену объекта возможен не только через точку (.). Возможен доступ и через указатель на этот объект. В этом случае применяют стрелку (->).

Пример:


#include


class myclass {

int a;

public:

myclass(int x); // конструктор

int get();

};


myclass::myclass(int x)

{

a = x;

}


int myclass::get()

{

return a;

}


main()

{

myclass ob(120); // создание объекта

myclass *p; // создание указателя на объект

p = &ob; // передача адреса ob в p

cout << "Значение, получаемое при использовании объекта:" << ob.get();

cout << "\n";

cout << "Значение, получаемое при использовании указателя:" << p->get();

return 0;

}


В программе объявление myclass *p создает указатель на объект myclass(а не создает объект!).

Для передачи адреса ob в p используется выражение p=&ob.

Для получения доступа к объекту через указатель используется выражение p->get().

^ Элементы класса, о которых всегда необходимо помнить.Речь пойдет о четырех очень специальных членах, которые должны учитываться при создании каждого класса.
Это конструктор по умолчанию, конструктор копий, операция присваивания и деструктор. Заметьте, это вовсе не значит, что вы должны обязательно каждый раз писать все эти четыре метода для каждого класса, который вы создаете. Речь идет только о том, что вы должны принимать их во внимание при создании любого класса и делать выводы об их необходимости.
Эти четыре члена в принципе являются методами, но не типичными. Они выглядят совсем как методы, но, как несложно заметить, некоторые из них не возвращают никаких значений. Эти методы предназначены для создания (инициализации), копирования и удаления (разрушения) объектов класса.
Есть несколько разновидностей конструкторов, в их числе есть довольно своеобразные, но основное их назначение в любом случае одно и то же: обеспечение удобного способа создания объекта-экземпляра класса.
Мы рассмотрим конструктор по умолчанию, конструктор копий, аргументы по умолчанию в конструкторе и другие конструкторы.
Как уже было отмечено, конструкторы - это функции-члены, не возвращающие никаких значений (даже типа void). Другой их особенностью является то, что их имя должно в точности, включая регистр символов, совпадать с именем класса. То есть если класс называется Any_Class, то его конструктор также должен называться Any_Class.
^ Конструктор по умолчанию. Итак, конструктор по умолчанию (default constructor) - это конструктор, не принимающий (не имеющий) аргументов.
Таким образом, конструктор по умолчанию для некоего произвольного класса будет выглядеть так:
class ANY_CLASS
{
public:
ANY_CLASS(); //конструктор по умолчанию
... //тут все остальное
};
Обычно конструкторы объявляются в открытой (public) области класса, поскольку деятельность конструкторов заключается в создании объекта типа класса и они вызываются извне класса. Вызовы конструкторов, как правило, происходят неявно. Например, создание одиночного объекта типа ANY_CLASS может выглядеть следующим образом:
ANY_CLASS ас; // ас - это объект класса ANY_CLASS
Заметьте, что в этом операторе совершенно отсутствуют скобки, конструирование - это неявная операция.
Массив объектов типа ANY_CLASS может быть создан так:
ANY_CLASS aac[10]; // aас — это массив из 10 элементов
Как видите, синтаксис объявления массива объектов точно такой же, как и синтаксис объявления статического массива данных любого базового (встроенного) типа. Одна из задач языка C++ состоит в предоставлении пользователям возможности обращаться со сложными типами данных таким же образом, как и со встроенными. Благодаря неявной природе конструирования объектов достигается первый ее аспект: создание объекта выглядит точно так же, как и создание обычной переменной.
Кстати, создать массив объектов можно только в том случае, если для класса определен конструктор по умолчанию.
^ Конструктор копий. Работа конструктора копий (copy constructor) заключается в предоставлении возможности инициализации (создания) нового объекта из уже существующего. Общий синтаксис конструктора копий таков:
My_Class(const My_Class&); // здесь My_Class — это имя класса
В конструкторе копирования класса My_Class в качестве параметра используется ссылка на объект этого класса. Причём эта ссылка объявляется со спецификатором const. И в этом нет ничего странного. Как известно, выражение вызова метода с параметром типа X ничем не отличается от выражения вызова метода, у которого параметром является ссылка на объект типа X. При вызове такого метода не приходится копировать объекты как параметры. Передача адреса не требует копирования объекта.
Внутрь тела конструктора копий обычно помещается серия присваиваний, посредством которых каждому элементу объекта-аргумента присваивается значение соответствующего элемента вызывающего объекта.
^ Аргументы по умолчанию в конструкторе. У любых функций могут быть аргументы по умолчанию. Это относится к конструкторам в той же степени, как и к любым остальным функциям-членам и глобальным функциям. Аргумент по умолчанию - это значение, которое присваивается аргументу, если пользователь явно не задал иное значение. Аргументы по умолчанию удобны в тех случаях, когда известно определенное (или предпочтительное) значение аргумента, но при этом желательно сохранить возможность задания другого значения при создании объекта. Правило использования аргументов по умолчанию таково: аргументу можно задать значение по умолчанию, если он находится правее всех в списке аргументов, или если все аргументы правее него имеют значения по умолчанию.
Ниже приведены несколько фиктивных конструкторов, демонстрирующих примеры правильного и неправильного употребления аргументов по умолчанию:
DATA(int а=0, int b); // явная ошибка: DATA F( , 5) смотрится глупо...
DATA(int а, int b=10); // правильно, можно создать объекты DATA G(5);
// или DATA G(3, 4);
DATA(int a=0, int b=10);// правильно, можно создать объекты DATA Н(3, 4);
// или DATA R;
Правило для аргументов по умолчанию было введено для того, чтобы не возникало ситуаций типа "пробел запятая аргумент" (см. первый пример для объекта F( , 5)), которые весьма чреваты ошибками, да и выглядят неважно.
Необходимо также отметить следующее: конструктор, все аргументы которого снабжены значениями по умолчанию, может вызываться и с аргументами, и без аргументов, то есть при вызове выглядеть как обычный конструктор по умолчанию (см пример для DATA Н(3, 4); и DATA R;). Поэтому желательно избегать неопределенности, возникающей при одновременном задании в классе конструктора по умолчанию, то есть без аргументов, и конструктора, у которого все аргументы имеют значения по умолчанию.
Деструкторы. Деструкторы выполняют работу, обратную той, что проделывают конструкторы. Хотя класс может иметь несколько конструкторов, но деструктор может быть только один.
Синтаксис деструктора очень похож на синтаксис конструктора по умолчанию. Точно также деструктор не имеет аргументов, все различие заключается в том, что деструктор, будучи по своей сути функцией, парной конструктору, имеет то же имя, что и класс, но с приставкой в виде операции дополнения (~). То есть деструктор любого класса имеет такую форму:
class ANY_CLASS
{
public:
ANY_CLASS(); //конструктор по умолчанию
ANY_CLASS(int d); //еще один конструктор
~ANY_CLASS(); //а это - деструктор
};
Деструктор почти всегда вызывается неявно. Вызов деструктора происходит либо при выходе объекта за пределы своей области видимости, либо при уничтожении динамического созданного (операцией new) объекта операцией delete.

^ Виртуальный деструктор. Если класс может иметь наследников, то предпочтительнее использовать виртуальный деструктор. Синтаксис виртуального деструктора точно такой же, как и у любого другого деструктора, за исключением того, что его объявление начинается с ключевого слова virtual.
class ANY_CLASS
{
public:
ANY_CLASS(); //конструктор по умолчанию
vrtual ~ANY_CLASS(); //а это – виртуальнный деструктор
};
Объявление деструктора виртуальным не отразится на производительности. так что имеет смысл всегда делать его виртуальным, если нет очень веских причин воздержаться от этого. Отказ от применения виртуальных деструкторов может привести к утечкам памяти в производных классах.

Приведенный ниже пример придуман исключительно в целях демонстрации неявных вызовов конструктора и деструктора при создании экземпляра класса. При вызове конструктора или деструктора программа просто печатает соответствующее сообщение. include
// Этот класс просто демонстрирует неявные вызовы
// конструктора и деструктора
class DEMO
{
public:
DEMO() { cout « "constructor" « endl; }
virtual ~DEMO() { cout « "destructor" « endl; }
};
void main(void)
{
DEMO staticDemo; // статическое размещение, деструктор вызывается при
// выходе за пределы области видимости
DEMO *dynamicDemo = new DEMO;
// динамическое размещение,
// деструктор вызывается при уничтожении объекта
delete dynamicDemo;
}
В этом примере определяется класс, не содержащий ничего, кроме открытых (public) деструктора и конструктора. Оба они объявлены и определены в классе. При создании объекта неявно вызывается конструктор и печатается слово "constructor", а при вызове деструктора, соответственно, слово "destructor".
Внутри функции main() создаются два объекта, один статический, в стеке, а второй в куче (heap) - динамический. В результате выполнения этого примера на экран будет выведено следующее:
constructor
constructor
destructor
destructor
Первая строка выводится конструктором при создании объекта staticDemo. Вторая строка выводится конструктором при создании объекта dynamicDemo. Третья строка – результат вызова деструктора объекта dynamicDemo при его уничтожении оператором delete. Четвертая строка – результат вызова деструктора объекта staticDemo. Деструктор статического объекта был вызван последним, при выходе объекта из области видимости.