Доступ к элементам-данным




Билеты по ООП. Ответы.

11-13.

Объектно-ориентированное программирование

Объектно-ориентированное программирование отличается от программирования на алгоритмических языках тем, что позволяет описывать не только алгоритмы, но и объекты. Все объектно-ориентированные языки имеют три общие концепции: инкапсуляция, полиморфизм и наследование.

11. Инкапсуляция

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

12. Полиморфизм

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

13. Наследование

Наследование представляет собой процесс, благодаря которому один объект может наследовать, приобретать свойства от другого объекта.

14. Классы С++

Определение класса

Общий вид объявления класса следующий

 

class имя_класса {

 

private:

данные и функции

protected:

защищённые данные и функции

public:

публичные данные и функции

} список объектов;

Управление доступом

В С++ можно ограничить видимость данных и функций класса при помощи меток public, protected, private. Метка-спецификатор доступа применяется ко всем элементам класса, следующим за ней, пока не встретится другая метка или кончится определение класса.

Метка-спецификатор public используется тогда, когда элементы-данные и функции-элементы класса доступны для функций-элементов и других функций программы, в которой имеется представитель класса.

Метка-спецификатор protected используется в том случае, когда элементы данных и функции-элементы доступны для функций-элементов данного класса и классов производных от него.

Метка-спецификатор private используется, если элементы-данные и функции-элементы должны быть доступны только для функций-элементов данного класса.

В классе элементы имеют по умолчанию доступ private.

Элементы класса

Элементы класса делятся на две основные категории:

1. данные, называемые элементами-данными;

2. код, называемый элементами-функциями или методами.

Элементы данных

Элементы-данные классов С++ такие же, как и элементы структур языка С++ с некоторыми дополнениями:

1. элементами-данными могут быть перечислимые типы, битовые поля или представители ранее объявленного класса;

2. элемент-данное класса может быть указателем или ссылкой на представителя этого класса.

Элементы функции

Элемент-функция является функцией, объявленной (описанной) внутри определения класса. Тело функции может также определяться внутри определения класса, в этом случае функция называется встроенной (inline) функцией-элементом. Когда тело функции определяется вне тела класса, перед именем функции ставится префикс из имени класса и операции разрешения видимости (::).

Например:

class queue {

int q[100];

int sloc, rloc;

public:

void init();

void qput(int i);

int qget();

};

 

void queue::init()

{

rloc=sloc=0;

}

 

void queue::qput(int i)

{

if (sloc==99) {

cout << "Queue is full.\n";

return;

}

sloc++;

q[sloc]=i;

}

 

int queue::qget()

{

if (rloc==sloc) {

cout << "Queue underflow.\n";

return 0;

}

rloc++;

return q[rloc];

}

Доступ к элементам-данным

Функции-элементы находятся в области действия класса, в котором они определены. Т.о. они могут обращаться к любому элементу класса, используя просто имя переменной. Обычные функции или функции-элементы другого класса могут получить доступ к элементам-данным с помощью операции. или ->, применяемых к представителю или указателю на представитель класса.

Пример.

class coord

{

public: int x, y; // координаты x и y

};

void main()

{

coord org; // представитель класса координат

coord *orgptr = &org; // указатель на представитель класса

org.x = 0; // задание значения координаты x

orgptr->y = 0; // задание значения координаты y

}

15. Перегрузка функций

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

Пример

 

#include <iostream>

using namespace std;

 

// sqr_it перегружается три раза

int sqr_it(int i);

double sqr_it(double d);

long sqr_it(long l);

 

int main()

{

cout << sqr_it(10) << “\n”;

cout << sqr_it(11.0) << “\n”;

cout << sqr_it(9L) << “\n”;

 

return 0;

}

 

int sqr_it(int i)

{

cout << “Inside the sqr_it() function that uses “;

cout << “an integer argument.\n”;

return i*i;

}

 

double sqr_it(double d)

{

cout << “Inside the sqr_it() function that uses “;

cout << “an double argument.\n”;

return d*d;

}

 

long sqr_it(long l)

{

cout << “Inside the sqr_it() function that uses “;

cout << “an long argument.\n”;

return l*l;

}

В этой программе создаются три сходные, но различные функции с именами sqr_it(), каждая из которых возвращает квадрат своего аргумента. Компилятор знает, какую функцию использовать, благодаря типу аргумента. Значение перегруженных функций заключается в том, что они позволяют обеспечивать доступ к целому набору функций, используя общее имя. Перегрузка функций важна потому, что она помогает в решении проблемы сложности. Хотя можно использовать одно и то же имя для перегрузки функций выполняющих совершенно разные задачи, делать этого не следует.

16. Конструкторы и деструкторы

Конструктор

Конструктор инициализирует представитель класса (объект) и является функцией-элементом с тем же именем, что и класс. Конструктор вызывается компилятором всегда, когда создается представитель класса.

Для конструкторов выполняются следующие правила:

1. для конструктора не указывается возвращаемый тип;

2. конструктор не может возвращать значение;

3. конструктор не наследуется.

Деструктор

Деструктор является дополнением конструктора. Он имеет то же имя, что и класс, но с префексом-тильдой (~). Он вызывается всякий раз, когда уничтожается представитель класса. Для деструктора существуют следующие правила:

1. деструктор не может иметь аргументов;

2. деструктор не может возвращать значения;

3. деструктор не наследуется.

17. Параметризированные конструкторы

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

Общая форма передачи аргументов конструктору:

ярлык_класса объект(список_аргументов);

Пример

 

#include <iostream>

using namespace std;

 

class widget {

int i;

int j;

public:

widget(int a, int b);

void put_widget();

};

 

widget::widget(int a, int b)

{

i = a;

j = b;

}

 

void widget::put_widget()

{

cout << i << “ “ << j << “\n”;

}

 

int main()

{

widget x(10, 20), y(0, 0);

 

x.put_widget();

y.put_widget();

 

return 0;

}

18. Дружественные функции

Разрешить обычной функции или функции-элементу другого класса полный доступ к элементам класса, объявленным private или protected, можно с помощью описания friend в определении данного класса.

Пример.

class myclass

{

friend void another_class::member(int);

friend void func_name(float);

};

Для друзей существуют следующие правила:

1. на описания friend не влияют спецификаторы public, protected или private;

2. описания friend не взаимны: если А объявляет В другом, то это не означает, что A является другом для B;

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

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

 

19. Inline-функции

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

Общая форма объявления inline-функции следующая:

inline объявление_функции

Причина использования inline-функций заключается в их эффективности. Скорость выполнения программы возрастает. При этом может возрасти размер исполняемого кода.

Создание inline-функций внутри класса

Всякая функция, определенная внутри объявления класса, является автоматически inline-функцией, если только это допустимо.

20. Передача объектов в функции

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

Пример

 

#include <iostream>

using namespace std;

 

class myclass {

int i;

public:

myclass(int n);

~myclass();

void set_i(int n) { i = n; }

int get_i() {return i; }

};

 

myclass:: myclass(int n)

{

i = n;

cout << "Constructing " << i << "\n";

}

myclass::~myclass()

{

cout << "Destroying " << i << "\n";

}

void f(myclass ob);

 

int main()

{

myclass o(1);

 

f(o);

cout << "This is i in main: ";

cout << o.get_i() << "\n";

 

system("PAUSE");

return 0;

}

void f(myclass ob)

{

ob.set_i(2);

 

 

cout << "This is local i: " << ob.get_i();

cout << "\n";

}

 

Эта программа выведет на экран следующие сообщения:

 

 

Имели место два вызова деструктора, в то время как конструктор вызывался только один раз. Хотя функция-конструктор не вызывалась при передаче объекта в функцию, необходимо вызывать деструктор при уничтожении копии. (Копия объекта уничтожается, как и любая локальная переменная, после окончания выполнения функции.)

Чтобы не возникало проблем необходимо определить оператор копирования для класса, который называется конструктором копирования.

21. Возвращение объектов функциями

Функция может возвращать объект в точку вызова.

Пример

 

#include <iostream.h>

using namespace std;

 

class myclass {

int i;

public:

void set_i(int n) { i = n; }

int get_i() {return i; }

};

 

myclass f(); // возвращение объекта типа myclass

 

int main()

{

myclass o;

 

o = f();

cout << o.get_i() << “\n”;

return 0;

}

myclass f();

{

myclass x;

x.set_i(1);

return x;

}

 

Когда функция возвращает объект, автоматически создается временный объект, содержащий возвращаемое значение. После того, как значение возвращено, этот объект уничтожается. Уничтожение временного объекта может вызвать неожиданные побочные эффекты. Для того чтобы преодолеть эту проблему используется перегрузка оператора присваивания и определение конструктора копирования.

 

22. Массивы объектов

Можно создавать массивы объектов точно так же, как создаются массивы данных других типов.

Инициализация массивов объектов

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

Пример

#include <iostream>

using namespace std;

class cl{

int h;

int i;

public:

cl(int j, int k) {h=j; i=k;}

int get_i() {return i; }

int get_h() {return h;}

};

int main()

{

cl ob[3] = {

cl(1, 2),

cl(3, 4),

cl(5, 6)

};

int i;

for (i=0; i<3; i++) {

cout << ob[i].get_h();

cout << “, “;

cout << ob[i].get_i() << “\n”;

}

return 0;

}

Для создания неициализированных массивов необходимо иметь конструктор без параметров.

23. Указатели на объекты

 

В С++ можно получить доступ к объекту как непосредственно, так и используя указатель на объект.

Пример

#include <iostream>

using namespace std;

class P_example {

int num;

public:

void set_num(int val){num=val;}

void show_num();

};

void P_example::show_num()

{

cout << num << “\n”;

}

int main()

{

P_example ob, *p;

ob.set_num(1);

ob.show_num();

p=&ob;

p->show_num();

return 0;

}

24. Указатель this

Каждая нестатическая функция-элемент имеет доступ к объекту, для которого вызвана, через ключевое слово this. Типом this является:

тип_класса *.

Пример.

class simple

{

public:

simple();

void greet() { cout<<“ Hello!”;};

};

simple::simple()

{

greet(); // вызов

this->greet(); // функции

(*this).greet(); // greet()

}

Т.к. функции-элементы могут обращаться ко всем элементам класса просто по имени, в основном указатель this используется для возврата указателя (return this) или ссылки (return *this) на подразумеваемый объект.

25. Перегрузка операторов

 

С++ поддерживает перегрузку операторов. За небольшими исключениями большинство операторов С++ могут быть перегружены, в результате чего они получат специальное значение по отношению к определённым классам. Например, класс, определяющий связанный список, может использовать оператор + для того, чтобы добавлять объект к списку. Другой класс может использовать оператор + совершенно иным способом. Когда оператор перегружен, ни одно из его исходных значений не теряет смысла. Просто для определённого класса объектов определён новый оператор. Поэтому перегрузка оператора + для того чтобы обрабатывать связный список, не изменяет его действия по отношению к целым числам.

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

Для того, чтобы перегрузить оператор, необходимо определить, что именно означает оператор по отношению к тому классу, к которому он применяется. Общая форма записи функции-оператора для случая, когда она является членом классом класса, имеет вид:

 

тип имя_класса::operator#(список аргументов)

{

// действия, определённые применительно к классу

}

 

Здесь перегруженный оператор подставляется вместо символа #, а тип задает тип значений, возвращаемых оператором. Для того, чтобы упростить использование перегруженного оператора в сложных выражениях, в качестве возвращаемого значения часто выбирают тот же самый тип, что и класс, для которого перегружается оператор.

Пример

Листинг 1

Создаётся класс three_d, содержащий координаты объекта в трёхмерном пространстве. Следующая программа перегружает операторы + и = для класса three_d.

#include <iostream>

using namespace std;

 

class three_d {

int x,y,z;

public:

three_d operator+(three_d t);

three_d operator=(three_d t);

 

void show();

void assign(int mx, int my, int mz);

};

//

three_d three_d::operator+(three_d t)

{

three_d temp;

 

temp.x=x+t.x;

temp.y=y+t.y;

temp.z=z+t.z;

return temp;

}

//

three_d three_d::operator=(three_d t)

{

x=t.x;

y=t.y;

z=t.z;

return *this;

}

//

void three_d::show()

{

cout << x <<",";

cout << y <<",";

cout << z <<"\n";

}

//

void three_d::assign(int mx, int my, int mz)

{

x=mx;

y=my;

z=mz;

}

 

int main()

{

three_d a,b,c;

 

a.assign(1,2,3);

b.assign(10,10,10);

 

a.show();

b.show();

 

c=a+b;

c.show();

 

c=a+b+c;

c.show();

 

c=b=a;

c.show();

b.show();

 

return 0;

}

 

Если рассмотреть эту программу внимательно, может вызвать удивление, что обе функции-оператора имеют только по одному параметру, несмотря на то, что они перегружают бинарный оператор. Это связано с тем, что при перегрузке бинарного оператора с использованием функции-члена ей передаётся явным образом только один аргумент. Вторым аргументом служит указатель this, который передаётся ей неявно. Так, в строке

 

temp.x = x+t.x;

 

x соответствует this->x, где x ассоциировано с объектом, который вызывает функцию-оператор. Объект, стоящий справа от знака операции, передаётся функции.

При перегрузке унарной операции функция-оператор не имеет параметров, а при перегрузке бинарной операции функция-оператор имеет один параметр. (Нельзя перегрузить триадный оператор.) Во всех случаях объект, активизирующий функцию-оператор, передаётся неявным образом с помощью указателя this.

Тщательно проанализируем, как работает программа из листинга 1, начиная с перегруженного оператора +. Когда два объекта типа three_d подвергаются воздействию оператора +, значения их соответствующих координат складываются, как показано в функции operator+(), ассоциированной с данным классом. Обратим внимание на то, что функция не модифицирует значений операндов. Вместо этого она возвращает объект типа three_d, содержащий результат выполнения операции.

Другим ключевым моментом перегрузки оператора сложения является то, что он возвращает объект типа three_d. Хотя функция может иметь в качестве значения любой допустимый тип языка С++, то, что она возвращает объект типа three_d, позволяет использовать оператор + в более сложных выражениях, таких как a+b+c. Здесь a+b создаёт результат типа three_d. Это значение затем прибавляется к c.

В противоположность оператору +, оператор присваивания модифицирует свои аргументы. Поскольку функция operator=() вызывается объектом, стоящим слева от знака равенства, то именно этот объект модифицируется. Однако даже оператор присваивания обязан возвращать значение, поскольку оператор присваивания порождает величину, стоящую с правой стороны равенства. Так, для того чтобы выражение следующего вида

 

a = b = c = d;

 

было допустимым, необходимо, чтобы оператор operator=() возвращал объект, на который указывает this и который будет объектом, стоящим с левой стороны. Если сделать таким образом, то можно выполнить множественное присваивание.

Следующие операторы не могут быть перегружены:

 

.::.*?

26. Ссылки

Ссылка по существу является неявным указателем. Она используется как другое имя объекта.

Параметры-ссылки

По умолчанию С++ передаёт аргументы в функцию по значению.

В С++ можно создавать передаваемые по ссылке параметры. Для этого при объявлении функции перед параметром ставится знак амперсанта &.

Пример

void f(int &f)

{ f = rand(); }

Инструкция

f = rand();

не использует оператор *. При объявлении параметра-ссылки компилятор С++ знает, что это неявный указатель, и обрабатывает его соответствующим образом. Во фрагменте кода

int val;

f(val);

printf(“%d”, val);

адрес переменной val, а не значение этой переменной, передаётся функции f(). Благодаря этому функция f() может модифицировать значение переменной val. При вызове функции f() нет необходимости ставить оператор взятия адреса & перед переменной val.

 

На применение переменных ссылочного типа имеется ряд ограничений:

  1. Нельзя взять ссылку от переменной ссылочного типа, иными словами, нельзя взять её адрес.
  2. Нельзя создать массив ссылок.
  3. Нельзя создать указатель на ссылку.
  4. Ссылки на битовые поля не допускаются.

27. Наследование и спецификаторы доступа

Наследование

Наследование является одной из главных особенностей объектно-ориентированного программирования. В С++ наследование поддерживается за счет того, что одному классу разрешается при своем объявлении включать в себя другой класс. Наследование позволяет построить иерархию классов от более общего к более частным. Этот процесс включает в себя определение базового класса, определяющего общие качества всех объектов, которые будут выведены затем из базового класса. Базовый класс представляет собой наиболее общее описание. Выведенные из базового класса классы обычно называют производными классами.

Пример

 

#include <iostream>

using namespace std;

 

class road_vehicle {

int wheels;

int passengers;

public:

void set_wheels(int num);

int get_wheels();

void set_pass(int num);

int get_pass();

};

 

class truck: public road_vehicle {

int cargo;

public:

void set_cargo(int size);

int get_cargo();

void show();

};

enum type {car, van, wagon};

 

class automobile: public road_vehicle {

enum type car_type;

public:

void set_type(enum type t);

enum type get_type();

void show();

};

 

void road_vehicle::set_wheels(int num)

{

wheels = num;

}

 

int road_vehicle::get_wheels()

{

return wheels;

}

 

void road_vehicle::set_pass(int num)

{

passengers = num;

}

int road_vehicle::get_pass()

{

return passengers;

}

void truck::set_cargo(int num)

{

cargo = num;

}

int truck::get_cargo()

{

return cargo;

}

 

void truck::show()

{

cout << “Wheels: “ << get_wheels() << “\n”;

cout << “Passengers: “ << get_pass() << “\n”;

cout << “Cargo capacity in cubic feet:“ << cargo << “\n”;

}

 

void automobile::set_type(enum type t)

{

car_type = t;

}

 

enum type automobile::get_type()

{

return car_type;

}

 

void automobile::show()

{

cout << “Wheels: “ << get_wheels() << “\n”;

cout << “Passengers: “ << get_pass() << “\n”;

cout << “Type: “;

switch(get_type()) {

case van: cout << “Van\n”;

break;

case car: cout << “Car\n”;

break;

case wagon: cout << “Wagon\n”;

}

}

 

Общая форма записи наследования имеет следующий вид

 

class имя_нового_класса: спецификатор_доступа наследуемый_класс{

// тело нового класса

};

Использование спецификатора доступа факультативно.

 

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

 

Спецификаторы доступа

 

В С++ члены класса классифицируются в соответствии с правами доступа на следующие три категории: публичные (public), частные (private) и защищённые (protected). Любая функция программы имеет доступ к публичным членам. Доступ к частному члену имеют только функции-члены класса или функции-друзья класса. Защищённые члены аналогичны частным членам. Разница между ними проявляется только при наследовании классов.

Частные члены базового класса не доступны внутри производного класса.

Например

class X {

int i;

int j;

public:

void get_ij();

void put_ij();

};

 

class Y: public X {

int k;

public:

int get_k();

void make_k();

};

 

Класс Y наследует и имеет доступ к публичным функциям get_ij() и put_ij() класса Х, но не имеет доступа к i и j, поскольку они являются частными членами Х.

Для того, чтобы оставить член частным и разрешить использовать его производным классам используется ключевое слово – protected.

Пример

 

class X {

protected:

int i;

int j;

public:

void get_ij();

void put_ij();

};

 

class Y: public X {

int k;

public:

int get_k();

void make_k();

};

 

Здесь класс Y имеет доступ к i и j, и в то же время они остаются недоступными для другой части программы.

 

Спецификатор доступа при наследовании базового класса

 

От того, с каким спецификатором доступа объявляется наследование базового класса, зависит статус доступа к членам производного класса. Общая форма наследования классов имеет следующий вид:

 

class имя_класса: доступ имя_класса {... };

 

Здесь доступ определяет, каким способом наследуется базовый класс. Спецификатор доступ может принимать три значения – private, public, protected. В случае если спецификатор доступа опущен, то по умолчанию подразумевается на его месте спецификатор public. Если спецификатор доступ принимает значение public, то все публичные и защищённые члены базового класса становятся соответственно публичными и защищёнными членами производного класса. Если доступ private, то все публичные и защищённые члены базового класса становятся частными членами производного класса. Если спецификатор доступа protected, то все публичные и защищённые члены базового класса становятся защищёнными членами производного класса. Рассмотрим пример:

 

#include <iostream>

using namespace std;

 

class X {

protected:

int i;

int j;

public:

void get_ij(){

cout << "Enter two numbers ";

cin >> i >> j;

}

void put_ij(){ cout << i << " " << j << "\n";}

};

 

// В классе Y, i и j класса X становятся защищёнными членами

class Y: public X {

int k;

public:

int get_k(){ return k;}

void make_k(){ k=i*j;}

};

 

// Класс Z имеет доступ к i и j класса X, но не к

// k класса Y, поскольку он является частным

class Z: public Y {

public:

void f();

};

 

// i и j доступны отсюда

void Z::f()

{

i=2;

j=3;

}

int main()

{

Y var;

Z var2;

 

var.get_ij();

var.put_ij();

var.make_k();

cout << var.get_k();

cout << "\n";

 

var2.f();

var2.put_ij();

 

return 0;

}

Поскольку класс Y наследует класс X со спецификатором доступа public, то защищённые элементы класса X становятся защищёнными элементами класса Y. Это означает, что они могут далее наследоваться классом Z, и эта программа будет откомпилирована и выполнена корректно. Однако, если изменить статус X при объявлении Y на private, то класс Z не имеет доступа к i, j и функциям get_ij() и put_ij(), поскольку они стали частными членами Y.

 

28. Конструкторы и деструкторы производных классов

Как базовый класс, так и производный класс могут иметь конструкторы. Когда базовый класс имеет конструктор, этот конструктор исполняется перед конструктором производного класса.

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

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

 

29. Множественное наследование. Передача параметров в конструктор базового класса

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

 

порождённый_конструктор(список_аргументов): базовый1(список_аргументов), базовый2(список_аргументов), …, базовыйN(список_аргументов)

{...};

 

Здесь под базовый1, базовый2, …, базовыйN обозначены имена базовых классов, наследуемые производным классом. Обратим вниманием, что с помощью двоеточия конструктор производного класса отделяется от списка конструкторов базового класса. Список аргументов, ассоциированный с базовыми классами, может состоять из констант, глобальных переменных или параметров конструктора производного класса.

 

30. Указатели и ссылки на производные типы

 

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

 

31. Виртуальные функции

 

Виртуальная функция – это функция, объявленная с ключевым словом virtual в базовом классе и переопределенная в производных классах. Виртуальные функции являются особыми функциями, потому что при вызове объекта производного класса с помощью указателя или ссылки на него С++ определяет во время исполнения программы, какую функцию вызвать, основываясь на типе объекта. Для разных объектов вызываются разные версии одной и той же виртуальной функции. Класс, содержащий одну или более виртуальных функций, называется полиморфным классом.

 

Пример использования виртуальных функций

 

#include <iostream>

using namespace std;

 

class Base {

public:

virtual void who(){// определение виртуальной функции

cout << "Base\n";

}

};

 

class first_d: public Base {

public:

void who(){// определение who применительно к first_d

cout << "First derivation\n";

}

};

 

class second_d: public Base {

void who(){// определение who применительно к second_d

cout << "Second derivation\n";

}

};

 

int main()

{

Base base_obj;

Base *p;

first_d first_obj;

second_d second_obj;

 

p = &base_obj;

p->who();

p = &first_obj;

p->who();

p = &second_obj;

p->who();

 

return 0;

}

Наиболее распространённым способом вызова виртуальной функции служит использование параметра функции.

 

 

Пример

/* Здесь ссылка на базовый класс используется для доступа

к виртуальной функции */

#include <iostream>

using namespace std;

 

class Base {

public:

virtual void who(){// определение виртуальной функции

cout << "Base\n";

}

};

 

class first_d: public Base {

public:

void who(){// определение who применительно к first_d

cout << "First derivation\n";

}

};

 

class second_d: public Base {

void who(){// определение who применительно к second_d

cout << "Second derivation\n";

}

};

 

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

void show_who(Base &r) {

r.who();

}

 

int main()

{

Base base_obj;

first_d first_obj;

second_d second_obj;

 

show_who(base_obj);

show_who(first_obj);

show_who(second_obj);

 

return 0;

}

 

Для чего нужны виртуальные функции?

Виртуальные функции в комбинации с производными типами позволяют С++ поддерживать полиморфизм времени исполнения. Этот полиморфизм важен для ООП, поскольку он позволяет переопределять функции базового класса в классах потомках с тем, чтобы иметь их версию применительно к данному конкретному классу. Таким образом, базовый класс определяет общий интерфейс, который имеют все производные от него классы, и вместе с тем полиморфизм позволяет производным классам иметь свои собственные реализации методов. Благодаря этому полиморфизм часто определяют фразой «один интерфейс – множество методов».

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

Пример

#include <iostream>

using namespace std;

 

class figure {

protected:

double x, y;

public:

void set_dim(double i, double j) {

x = i; y = j;

}

 

virtual void show_area() {

cout << "No area computation defined ";

cout << "for this class.\n";

}

};

 

class triangle: public figure {

public:

void show_area() {

cout << "Triangle with height ";

cout << x << " and base " << y;

cout << " has an area of ";

cout << x*0.5*y << ".\n";

}

};

 

class square: public figure {

public:

void show_area() {

cout << "Square with dimensions ";

cout << x << "x" << y;

cout << " has an area of ";

cout << x*y << ".\n";

}

};

 

int main()

{

figure *p;

triangle t;

square s;

 

p = &t;

p->set_dim(10.0, 5.0);

p->show_area();

 

p = &s;

p->set_dim(10.0, 5.0);

p->show_area();

 

return 0;

}

 

32. Чисто виртуальные функции и абстрактные типы

Когда виртуальная функция не переопределена в производном классе, то при вызове её в объекте производного класса вызывается версия из базового класса. Однако во многих случаях невозможно ввести содержательное определение виртуальной функции в базовом классе. Есть два способа действий в этой ситуации. Первый заключается в выводе какого-либо предупреждающего сообщения. Но этот способ не универсален, поскольку информация о том, что функция не переопределена, проявится на этапе выполнения. Необходим метод, гарантирующий, что производные классы действительно определят все необходимые функции. В качестве решения этой проблемы используются чисто виртуальные функции.

Чисто виртуальная функция является функцией, которая объявляется в базовом классе, но не имеет в нём определения. Поскольку она не имеет определения в базовом классе, то всякий производный класс обязан иметь свою собственную версию определения. Синтаксис:

virtual тип имя_функции(список_параметров) = 0;

Пример, следующая версия функции show_area() класса figure является чисто виртуальной функцией.

class figure {

protected:

double x, y;

public:

void set_dim(double i, double j=0) {

x = i;

y = j;

}

 

virtual void show_area() = 0;

};

При введении чисто виртуальной функции в производном классе обязательно необходимо определить свою собственную реализацию этой функции.

 

Пример, если попытаться откомпилировать эту версию программы, то будет выдана ошибка:

#include <iostream>

using namespace std;

 

class figure {

protected:

double x, y;

public:

void set_dim(double i, double j=0) {

x = i;

y = j;

}

 

virtual void show_area() = 0;

};

 

class triangle: public figure {

public:

void show_area() {

cout << "Triangle with height ";

cout << x << " and base " << y;

cout << " has an area of ";

cout << x*0.5*y << ".\n";

}

};

 

class square: public figure {

public:

void show_area() {

cout << "Square with dimensions ";

cout << x << "x" << y;

cout << " has an area of ";

cout << x*y << ".\n";

}

};

 

class circle: public figure {

// определение show_area() отсутствует и поэтому

// выдаётся ошибка

};

 

int main()

{

figure *p;

triangle t;

square s;

 

p = &t;

p->set_dim(10.0, 5.0);

p->show_area();

 

p = &s;

p->set_dim(10.0, 5.0);

p->show_area();

 

return 0;

}

Если какой-либо класс имеет хотя бы одну чисто виртуальную функцию, то такой класс называется абстрактным. Важной особенностью абстрактных классов является то, что не существует ни одного объекта данного класса. Абстрактный класс служит базовым для других классов. Даже если базовый класс является абстрактным, всё равно можно объявлять указатели или ссылки на него, с помощью которых затем поддерживается полиморфизм времени исполнения.

33. Раннее и позднее связывание

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

Позднее связывание означает, что объект связывается с вызовом функции только во время исполнения программы, а не раньше. Позднее связывание достигается в С++ с помощью использования виртуальных функций и производных классов. Его достоинством является высокая гибкость.

34. Классы потоков С++

С++ обеспечивает поддержку ввода-вывода в заголовочном файле iostream. Поскольку используются стандартные классы-шаблоны, необходима инструкция using namespace std. В файле iostream определены 2 иерархии классов, поддерживающ



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2021-12-15 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: