NexxDigital - компьютеры и операционные системы

Очередная модификация базового класса приводит к неожиданным последствиям. Эта модификация состоит в изменении спецификатора функции-члена базового класса. Мы (впервые!) используем спецификатор virtual в объявлении функции. Функции, объявленные со спецификатором virtual, называются виртуальными функциями. Введение виртуальных функций в объявление базового класса (всего лишь один спецификатор) имеет столь значительные последствия для методологии объектно-ориентированного программирования, что мы лишний раз приведём модифицированное объявление класса A:

Class A { public: virtual int Fun1(int); };

Один дополнительный спецификатор в объявлении функции и больше никаких (пока никаких) изменений в объявлениях производных классов. Как всегда, очень простая функция main(). В ней мы определяем указатель на объект базового класса, настраиваем его на объект производного типа, после чего по указателю мы вызываем функцию Fun1():

Void main () { A *pObj; A MyA; AB MyAB; pObj = &MyA; pObj->Fun1(1); AC MyAC; pObj = &MyAC; pObj->Fun1(1); }

Если бы не спецификатор virtual, результат выполнения выражения вызова

PObj->Fun1(1);

был бы очевиден: как известно, выбор функции определяется типом указателя.

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

Сразу надо заметить, что возможность вызова функции-члена производного класса по указателю на базовый класс не означает, что появилась возможность наблюдения за объектом "сверху вниз" из указателя на объект базового класса. Невиртуальные функции-члены и данные по-прежнему недоступны. И в этом можно очень легко убедиться. Для этого достаточно попробовать сделать то, что мы уже однажды проделали - вызвать неизвестную в базовом классе функцию-член производного класса:

//pObj->Fun2(2); //pObj->AC::Fun1(2);

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

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

Cout << "Размеры объекта: " << sizeof(MyAC) << "…" << endl;

Так что объект производного класса приобретает дополнительный элемент - указатель на таблицу виртуальных функций. Схему такого объекта можно представить следующим образом (указатель на таблицу мы обозначим идентификатором vptr, таблицу виртуальных функций - идентификатором vtbl):

MyAC::= vptr A AC vtbl::= &AC::Fun1

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

PObj->Fun1(1);

можно представить следующим образом:

(*(pObj->vptr)) (pObj,1);

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

Здесь буквально сказано следующее:

ВЫЗВАТЬ ФУНКЦИЮ, РАСПОЛОЖЕННУЮ ПО НУЛЕВОМУ ИНДЕКСУ ТАБЛИЦЫ ВИРТУАЛЬНЫХ ФУНКЦИЙ vtbl (в этой таблице у нас всего один элемент), АДРЕС НАЧАЛА КОТОРОЙ МОЖНО НАЙТИ ПО УКАЗАТЕЛЮ vptr.

В СВОЮ ОЧЕРЕДЬ, ЭТОТ УКАЗАТЕЛЬ ДОСТУПЕН ПО УКАЗАТЕЛЮ pObj, НАСТРОЕННОМУ НА ОБЪЕКТ MyAC. ФУНКЦИИ ПЕРЕДАЁТСЯ ДВА (!) ПАРАМЕТРА, ПЕРВЫЙ ИЗ КОТОРЫХ ЯВЛЯЕТСЯ АДРЕСОМ ОБЪЕКТА MyAC (значение для this указателя!), ВТОРОЙ - ЦЕЛОЧИСЛЕННЫМ ЗНАЧЕНИЕМ, РАВНЫМ 1.

Вызов функции-члена базового класса обеспечивается посредством квалифицированного имени.

PObj->A::Fun1(1);

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

Мы в очередной раз модифицируем объявление классов A, AB и объявляем новый класс ABC.

Модификация классов A и AB сводится к объявлению в них новых функций-членов:

Class A { public: virtual int Fun1(int key); virtual int Fun2(int key); }; ::::: int A::Fun2(int key) { cout << " Fun2(" << key << ") from A " << endl; return 0; } class AB: public A { public: int Fun1(int key); int Fun2(int key); }; ::::: int AB::Fun2(int key) { cout << " Fun2(" << key << ") from AB " << endl; return 0; } Класс ABC является производным от класса AB: class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC " << endl; return 0; }

В этот класс входит объявление функции-члена Fun1, которая объявляется в косвенном базовом классе A как виртуальная функция. Кроме того, этот класс наследует от непосредственной базы функцию-член Fun2. Эта функция также объявляется в базовом классе A как виртуальная. Мы объявляем объект-представитель класса ABC:

ABC MyABC;

Его схему можно представить следующим образом:

MyABC::= vptr A AB ABC vtbl::= &AB::Fun2 &ABC::Fun1

Таблица виртуальных функций сейчас содержит два элемента. Мы настраиваем указатель на объект базового класса на объект MyABC, затем вызываем функции-члены:

PObj = &MyABC; pObj->Fun1(1); pObj->Fun2(2);

В этом случае невозможно вызвать функцию-член AB::Fun1(), поскольку её адрес не содержится в списке виртуальных функций, а с верхнего уровня объекта MyABC, на который настроен указатель pObj, она просто не видна. Таблица виртуальных функций строится конструктором в момент создания объекта соответствующего объекта. Безусловно, транслятор обеспечивает соответствующее кодирование конструктора. Но транслятор не в состоянии определить содержание таблицы виртуальных функций для конкретного объекта. Это задача времени исполнения. Пока таблица виртуальных функций не будет построена для конкретного объекта, соответствующая функция-член производного класса не сможет быть вызвана. В этом легко убедиться, после очередной модификации объявления классов.

Программа невелика, поэтому имеет смысл привести её текст полностью. Не следует обольщаться по поводу операции доступа к компонентам класса::. Обсуждение связанных с этой операцией проблем ещё впереди.

#include class A { public: virtual int Fun1(int key); }; int A::Fun1(int key) { cout << " Fun1(" << key << ") from A." << endl; return 0; } class AB: public A { public: AB() {Fun1(125);}; int Fun2(int key); }; int AB::Fun2(int key) { Fun1(key * 5); cout << " Fun2(" << key << ") from AB." << endl; return 0; } class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC." << endl; return 0; } void main () { ABC MyABC; // Вызывается A::Fun1(). MyABC.Fun1(1); // Вызывается ABC::Fun1(). MyABC.Fun2(1); // Вызываются AB::Fun2() и ABC::Fun1(). MyABC.A::Fun1(1); // Вызывается A::Fun1(). A *pObj = &MyABC; // Определяем и настраиваем указатель. cout << "==========" << endl; pObj->Fun1(2); // Вызывается ABC::Fun1(). //pObj->Fun2(2); // Эта функция через указатель недоступна!!! pObj->A::Fun1(2); // Вызывается A::Fun1(). }

Теперь в момент создания объекта MyABC

ABC MyABC;

из конструктора класса AB (а он вызывается раньше конструктора класса ABC), будет вызвана функция A::Fun1(). Эта функция является членом класса A. Объект MyABC ещё до конца не сформирован, таблица виртуальных функций ещё не заполнена, о существовании функции ABC::Fun1() ещё ничего не известно. После того, как объект MyABC будет окончательно сформирован, таблица виртуальных функций заполнится, а указатель pObj будет настроен на объект MyABC, вызов функции A::Fun1() через указатель pObj будет возможен лишь с использованием полного квалифицированного имени этой функции:

PObj->Fun1(1); // Это вызов функции ABC::Fun1()! pObj->A::Fun1(1); // Очевидно, что это вызов функции A::Fun1()!

Заметим, что вызов функции-члена Fun1 непосредственно из объекта MyABC приводит к аналогичному результату:

MyABC.Fun1(1); // Вызов функции ABC::Fun1().

А попытка вызова невиртуальной функции AB::Fun2() через указатель на объект базового класса заканчивается неудачей. В таблице виртуальных функций адреса этой функции нет, а с верхнего уровня объекта "посмотреть вниз" невозможно.

//pObj->Fun2(2); // Так нельзя!

Результат выполнения этой программки наглядно демонстрирует специфику использования виртуальных функций. Всего несколько строк…

Fun1(125) from A. Fun1(1) from ABC. Fun1(5) from ABC. Fun2(1) from AB. Fun1(1) from A. ========== Fun1(2) from ABC. Fun1(2) from A.

Один и тот же указатель в ходе выполнения программы может настраиваться на объекты-представители различных производных классов. В результате в буквальном смысле одно и то выражение вызова функции-члена обеспечивает выполнение совершенно разных функций. Впервые мы сталкиваемся с так называемым ПОЗДНИМ или ОТЛОЖЕННЫМ СВЯЗЫВАНИЕМ.

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

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

И ещё один маленький пример, демонстрирующий изменение поведение объекта-представителя производного класса после того, как одна из функция базового класса становится виртуальной.

#include class A { public: void funA () {xFun();}; /*virtual*/void xFun () {cout <<"this is void A::xFun();"<< endl;}; }; class B: public A { public: void xFun () {cout <<"this is void B::xFun ();"<

В начале спецификатор virtual а определении функции A::xFun() закомментирован. Процесс выполнения программы состоит в определении объекта-представителя objB производного класса B и вызова для этого объекта функции-члена funA(). Эта функция наследуется из базового класса, она одна и очевидно, что её идентификация не вызывает у транслятора никаких проблем. Эта функция принадлежит базовому классу, а это означает, что в момент её вызова, управление передаётся "на верхний уровень" объекта objB. На этом же уровне располагается одна из функций с именем xFun(), и именно этой функции передаётся управление в ходе выполнения выражения вызова в теле функции funA(). Мало того, из функции funA() просто невозможно вызвать другую одноименную функцию. В момент разбора структуры класса A транслятор вообще не имеет никакого представления о структуре класса B. Функция xFun() - член класса B оказывается недостижима из функции funA().

Но если раскомментировать спецификатор virtual в определении функции A::xFun(), между двумя одноименными функциями установится отношение замещения, а порождение объекта objB будет сопровождаться созданием таблицы виртуальных функций, в соответствии с которой будет вызываться замещающая функция член класса B. Теперь для вызова замещаемой функции необходимо использовать её квалифицированное имя:

Void A::funA () { xFun(); A::xFun(); }

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

Для объявления виртуальной функции используется ключевое слово virtual . Функция-член класса может быть объявлена как виртуальная, если

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#include
using namespace std;
class X
{
protected :
int i;
public :
void seti(int c) { i = c; }
virtual void print() { cout << endl << "class X: " << i; }
};
class Y: public X // наследование
{
public :
void print() { cout << endl << "class Y: " << i; } // переопределение базовой функции
};
int main()
{
X x;
X *px = &x; // Указатель на базовый класс
Y y;
x.seti(10);
y.seti(15);
px->print(); // класс X: 10
px = &y;
px->print(); // класс Y: 15
cin.get();
return 0;
}

Результат выполнения

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

Если в строке 9 (см. код выше) убрать ключевое слово virtual , то результат выполнения будет уже другим, т.к. связывание функций будет происходить на этапе компиляции:

В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.

Пример : выбор виртуальной функции

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#include
using namespace std;
class figure
{
protected :
double x, y;
public :
figure(double a = 0, double b = 0) { x = a; y = b; }
virtual double area() { return (0); } // по умолчанию
};
class rectangle: public figure
{
public :
rectangle(double a = 0, double b = 0) : figure(a, b) {};
double area() { return (x*y); }
};
class circle: public figure
{
public :
circle(double a = 0) : figure(a, 0) {};
double area() { return (3.1415*x*x); }
};
int main()
{
figure *f;
rectangle rect(3, 4);
circle cir(2);
double total = 0;
f = ▭
f = ○
total = f->area();
cout << total << endl;
total += f->area();
cout << total << endl;
cin.get();
return 0;
}

Результат выполнения


Чистая виртуальная функция

Базовый класс иерархии типа обычно содержит ряд виртуальных функций, которые обеспечивают динамическую типизацию. Часто в самом базовом классе сами виртуальные функции фиктивны и имеют пустое тело. Определенное значение им придается лишь в порожденных классах. Такие функции называются чистыми виртуальными функциями .

Чистая виртуальная функция - это метод класса, тело которого не определено.

В базовом классе такая функция записывается следующим образом.

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

Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется переопределением метода (method overriding) . Чтобы пересмотреть текущий дизайн, нужно понять значение ключевых слов virtual и override.

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

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

Метод объявляется как виртуальный в базовом классе с помощью ключевого слова virtual, указываемого перед его именем. Когда же виртуальный метод переопределяется в производном классе, то для этого используется модификатор override . А сам процесс повторного определения виртуального метода в производном классе называется переопределением метода. При переопределении метода - имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract.

Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов: динамической диспетчеризации методов , которая представляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благодаря ей в C# реализуется динамический полиморфизм.

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

И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к индексаторам.

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

// Реализуем класс содержащий информацию о шрифтах // и использующий виртуальные методы, свойства и индексаторы using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { // Базовый класс class Font { string TypeFont; short FontSize; public Font() { TypeFont = "Arial"; FontSize = 12; } public Font(string TypeFont, short FontSize) { this.TypeFont = TypeFont; this.FontSize = FontSize; } public string typeFont { get { return TypeFont; } set { TypeFont = value; } } public short fontSize { get { return FontSize; } set { FontSize = value; } } // Создаем виртуальный метод public virtual string FontInfo(Font obj) { string s = "Информация о шрифте: \n------------------\n\n" + "Тип шрифта: " + typeFont + "\nРазмер шрифта: " + fontSize + "\n"; return s; } } // Производный класс 1 уровня class ColorFont: Font { byte Color; public ColorFont(byte Color, string TypeFont, short FontSize) : base(TypeFont, FontSize) { this.Color = Color; } // Переопределение для виртуального метода public override string FontInfo(Font obj) { // Используется ссылка на метод, определенный в базовом классе Font return base.FontInfo(obj) + "Цвет шрифта: " + Color + "\n"; } // Создадим виртуальное свойство public virtual byte color { set { Color = value; } get { return Color; } } } // Производный класс 2 уровня class GradientColorFont: ColorFont { char TypeGradient; public GradientColorFont(char TypeGradient, byte Color, string TypeFont, short FontSize) : base(Color, TypeFont, FontSize) { this.TypeGradient = TypeGradient; } // Опять переопределяем виртуальный метод public override string FontInfo(Font obj) { // Используется ссылка на метод определенный в производном классе FontColor return base.FontInfo(obj) + "Тип градиента: " + TypeGradient + "\n\n"; } // Переопределим виртуальное свойство public override byte color { get { return base.color; } set { if (value

Виртуальная функция

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

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

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

Пример

Пример виртуальной функции в Delphi

Достаточно часто виртуальные методы забывают перекрыть с помощью ключевого слова override . Это приводит к закрытию метода. В этом случае замещения методов в VMT не произойдет и требуемая функциональность не будет получена.

Эта ошибка отслеживается компилятором, который выдаёт соответствующее предупреждение.

Вызов метода предка из перекрытого метода

Бывает необходимо вызвать метод предка в перекрытом методе.

Объявим два класса. Предка(Ancestor):

TAncestor = class private protected public {Виртуальная процедура.} procedure VirtualProcedure; virtual; end;

и его потомка (Descendant):

TDescendant = class (TAncestor) private protected public {Перекрытие виртуальной процедуры.} procedure VirtualProcedure; override; end;

Обращение к методу предка реализуется с помощью ключевого слова ""inherited""

procedure TDescendant.VirtualProcedure; begin inherited; end;

Стоит помнить, что в Delphi, деструктор должен быть обязательно перекрытым. ""override""; и содержать вызов деструктора предка

TDescendant = class (TAncestor) private protected public destructor Destroy; override; end; destructor TDescendant. Destroy; begin inherited; end;

В языке C++, не нужно вызывать конструктор и деструктор предка, деструктор должен быть виртуальным. Деструкторы предков вызовутся автоматически. Чтобы вызвать метод предка, нужно явно вызвать метод:

Class Ancestor { public : virtual void function1 () { printf ("Ancestor::function1" ) ; } } ; class Descendant: public Ancestor { public : virtual void function1 () { printf ("Descendant::function1" ) ; Ancestor::function1 () ; // this will print the text "Ancestor::function1" } } ;

Для вызова конструктора предка нужно указать конструктор:

Class Descendant: public Ancestor { public : Descendant() : Ancestor() ; } ;

См. также

Ссылки

  • C++ FAQ Lite: Виртуальные функции в C++ (англ.)

Wikimedia Foundation . 2010 .

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


Поделитесь работой в социальных сетях

Если эта работа Вам не подошла внизу страницы есть список похожих работ. Так же Вы можете воспользоваться кнопкой поиск


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

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

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

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

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

Согласно данному правилу указатель типа А может ссылаться на любой из этих трех объектов. То есть, возможна следующая форма записи:

Несмотря на то, что указатель point_to_Object имеет тип А*, а не С* (или В*), он может ссылаться на объекты типа С (или В). А теперь рассмотрим вариант неправильной записи:

Примечание: Может быть правило будет более понятным, если вы будете думать об объекте С, как особом виде объекта А. Ну, например, пингвин - это особая разновидность птиц, и он все-таки остается птицей, хоть и не летает. Конечно, эта взаимосвязь объектов и указателей работает только в одном направлении. Объект типа С - особый вид объекта А, но вот объект А не является особым видом объекта С. Возвращаясь к пингвинам смело можно сказать, что если бы все птицы были особым видом пингвинов - они бы просто не умели летать!

Этот принцип становится особенно важным, когда в классах, связанных наследованием определяются виртуальные функции. Виртуальные функции имеют точно такой же вид и программируются так же, как и самые обычные функции. Только их объявление производится с ключевым словом virtual . Например, наш базовый класс А может объявить виртуальную функцию v_function() .

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

Когда в классе, подобном В, определяется виртуальная функция, имеющая одинаковое имя с виртуальной функцией класса-предка, такая функция называется замещающей. Виртуальная функция v_function()в В замещает виртуальную функцию с тем же именем в классе А.

Вернемся к указателю point_to_Object типа А*, который ссылается на объект object_В типа В*. Давайте внимательно посмотрим на оператор, который вызывает виртуальную функцию v_function()для объекта, на который указывает point_to_Object.

Указатель point_to_Object может хранить адрес объекта типа А или В. Значит во время выполнения этот оператор point_to_Object-gt;v_function(); вызывает виртуальную функцию класса на объект которого он в данный момент ссылается. Если point_to_Object ссылается на объект типа А, вызывается функция, принадлежащая классу А. Если point_to_Object ссылается на объект типа В, вызывается функция, принадлежащая классу В. Итак, один и тот же оператор вызывает функцию класса адресуемого объекта. Это и есть действие, определяемое во время выполнения программы. Иначе говоря, реализация полиморфизма.

Применение.

Предположим на минуту, что мы собираемся написать компьютерную игру. И не просто в игру, а, например, шутер (стрелялки). Что понадобиться в первую очередь? Конечно же, оружие! Причем, в нашей игре будет огромное количество разновидностей оружия. Поэтому, вполне логичным решением будет - завести базовый класс. Скажем - так:

Не вдаваясь в подробности этого класса, можно сказать, что самыми важными, пожалуй, будут функции Use1() и Use2(), которые описывают поведение (или применение) этого оружия. От этого класса можно порождать любые виды вооружения. Будут добавляться новые данные-члены (типа количества патронов, скорострельности, уровня энергии, длинны лезвия и т.п.) и новые функции. А переопределяя функции Use1() и Use2(), мы будем описывать различие в применении оружия (для ножа это может быть удар и метание, для автомата - стрельба одиночными и очередями). Коллекцию вооружения надо где-то хранить. Видимо, проще всего организовать для этого массив указателей типа Weapon*. Для простоты предположим, что это глобальный массив Arms, на 10 видов оружия, и все указатели для начала инициализированы нулем.

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

int TypeOfWeapon;

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

Вот и всё. Мы создали код, который описывает оружие еще до того, как решили, какие его типы будут использоваться. Более того. У нас вообще еще нет ни одного реального типа вооружения! Дополнительная (иногда очень важная) выгода - этот код можно будет скомпилировать отдельно и хранить в библиотеке. В дальнейшем вы (или другой программист) можете вывести новые классы из Weapon, сохранить их в массиве Arms и использовать. При этом не потребуется перекомпиляции вашего кода. Особо заметьте, что этот код не требует от вас точного задания типов данных объектов на которые ссылаются указатели Arms, требуется только, чтобы они были производными от Weapon. Объекты определяют во время выполнения, какую функцию Use() им следует вызвать.

Некоторые особенности применения

А, теперь, давайте вернемся к началу - к классам А, В и С.

Класс С на данный момент стоит у нас в самом низу иерархии, в конце линии наследования. В классе С точно также можно определить замещающую виртуальную функцию. Причем применять ключевое слово virtual совсем необязательно, поскольку это конечный класс в линии наследования. Функция и так будет работать и выбираться как виртуальная. Но, если вам понадобится вывести некий класс D из класса С, да еще и изменить поведение функции v_function(), то тут как раз ничего и не выйдет. Для этого в классе С функция v_function() должна быть объявлена, как virtual. Отсюда правило:

Ключевое слово virtual лучше не отбрасывать - вдруг пригодится?

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

Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, но с другим набором параметров, то эта функция производного класса уже не будет виртуальной. Даже если вы сопроводите ее ключевым словом virtual, она таковой не будет. В этом случае с помощью указателя на базовый класс при любом значении этого указателя будет выполняться обращение к функции базового класса. Вспомните правило о перегрузке функций! Это просто разные функции. У вас получится совсем другая виртуальная функция. Отсюда еще одно правило.

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

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

Таблица виртуальных функций.

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

Раннее и позднее связывание. Статический и динамический полиморфизм.

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

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

А, теперь, рассмотрим этот пример с точки зрения программирования. При применении раннего связывания, мы как бы говорим компилятору: "Я точно знаю, чего я хочу. Поэтому статически связывай все вызовы функций". При применении механизма позднего связывания мы как бы говорим компилятору: "Я пока не знаю чего я хочу. Когда придет время, я сообщу что и как я хочу".

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

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

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

И, напоследок, давайте чётко сформулируем определение того свойства ООП, реализацией которого является связывание:

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

Абстрактные классы.

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

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

Слово "чисто" – в данном случае используется в контексте "пусто". Иными словами, чисто виртуальная функция - функция пустая. Синтаксис создания её таков:

class A { public: // чисто виртуальная функция virtual void v_function()=0; };

Как видите, все отличие только в том, что появилась конструкция «=0», которая называется «чистый спецификатор». Чисто виртуальная функция абсолютно ничего не делает и недоступна для вызовов. Ее назначение – служить основой (если хотите, шаблоном) для замещающих функций в производных классах.

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

Примечание: Приведем пример. Все животные в своем поведении имеют такие функции, как «есть», «пить», «спать», «издавать звук». Имеет смысл определить базовый класс, в котором сразу объявить все эти функции и сделать их чисто виртуальными. А потом из этого класса выводить классы, описывающие конкретных животных (или виды), со своим специфичным поведением. А базовый класс при этом действительно получается абстрактным. Ведь он не описывает никакое более-менее конкретное животное (даже вид животных). Это может быть и рыба и птица....

По сравнению с обычными классами, абстрактные классы пользуются «ограниченными правами».

  • Как и всякий класс, абстрактный класс может иметь явно определенный конструктор. Из конструктора можно вызывать методы класса. Но обращение из конструктора к чистым виртуальным функциям приведут к ошибкам во время выполнения программы.
  • Как уже говорилось, невозможно создать объект абстрактного класса.
  • Абстрактный класс нельзя применять для задания типа параметра функции, или в качестве типа возвращаемого значения.
  • Его нельзя использовать при явном приведении типов. Зато можно определять ссылки и указатели на абстрактные классы.

Пример.

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

#include

#include

using namespace std;

// абстрактный базовый класс

class Animal

Public:

// кличка животного

Char Title;

//простой конструктор

Animal(char *t){

Strcpy(Title,t);

//чисто виртуальная функция

virtual void speak()=0;

// класс лягушка

class Frog: public Animal

Public:

Frog(char *Title): Animal(Title){};

Virtual void speak(){

Cout<

// класс собака

class Dog: public Animal

Public:

Dog(char *Title): Animal(Title){};

Virtual void speak(){

Cout<

// класс кошка

class Cat: public Animal

Public:

Cat(char *Title): Animal(Title){};

Virtual void speak(){

Cout<

// класс лев

class Lion: public Cat

Public:

Lion(char *Title): Cat(Title) {};

/*virtual void speak(){

Cout<

}*/

/*virtual int speak(){

Cout<

Return 0;

}*/

Virtual void speak(int When){

Cout<

void main ()

// объявим массив указателей на базовый класс Animal

// и сразу его заполним указателями, создавая объекты

// c писок животных

Animal *animals = {new Dog("Bob"),

New Cat("Murka"),

New Frog("Vasya"),

New Lion("King")};

For(int k=0; k<4; k++)

animals[k]->speak();

В качестве базового класса создан абстрактный класс Animal. Он имеет единственный член Title, описывающий кличку животного. В нем есть явно определенный конструктор, который присваивает животному его «имя». И, единственная чисто виртуальная функция speak(), которая описывает, какие звуки издает животное.

От этого класса отнаследованны все остальные. Кроме одного. Класс «лев» порожден от класса «кошка» (львы это тоже кошки). Это сделано для демонстрации тонкостей применения виртуальных функций. Но об этом классе немного позже.

Во всех производных классах также описана собственная замещающая виртуальная функция speak(), которая печатает на экран, звуки, которые издает конкретное животное.

В основном теле программы объявлен массив animals указателей типа Animal*. И сразу же созданы динамические объекты классов и заполнен массив указателей. А в цикле for() по указателю просто вызывается виртуальная функция speak().

Результат работы программы таков:

Bob say "gav-gav"

Murka say "myau-myau"

Vasya say "kwa-kwa"

King say "rrr-rrr"

Теперь обратимся к описанию класса Lion (лев). В нем вместо одной виртуальной функции speak() содержится сразу три. Две из них закомментированы.

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

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

Bob say "gav-gav"

Murka say "myau-myau"

Vasya say "kwa-kwa"

King say "myau-myau"

Лев у нас уже не рычит, а мяукает. Это потому, что работает уже совсем другая функция! То есть, раз в данном классе нет правильно определенной виртуальной функции, то по указателю вызывается виртуальная функция speak() из базового класса. А в нашем случае базовым для класса Lion является класс Cat. Вот лев и замяукал.

Виртуальный базовый класс.

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

Class A {

Public:

Int val;

Class B: public A {...};

Class C: public A {...};

Class D: public B, public C{

Public:

Int Get_Val(){

Return val; // ошибка !

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

int Get_Val(){

Return B::val;

Либо использовать виртуальный базовый класс . Разберем на примере, как это можно сделать. Определим дерево иерархии следующим образом:

Class A {

Public:

Int val;

Class B: public virtual A {...};

Class C: public virtual A {...};

Class D: public B, public C {

Public:

Int Get_Val() {

Return val; // все работает корректно

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

Вывод

Итак, виртуальный базовый класс нужен тогда, когда производный класс наследует два (или более) класса, каждый из которых сам унаследовал один и тот же базовый класс. Без виртуального базового класса в последнем производном классе существовало бы две (или более) копии общего базового класса. Однако, благодаря тому, что исходный базовый класс делается виртуальным, в последнем производном классе представлена только одна копия базового.

Виртуальный деструктор.

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

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

class Base

Private:

Char *sp1;

Int size;

Public:

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

Base(const char *S, int s){

Size=s;

Sp1=new char;

// деструктор

~Base(){

Cout<<"Base";

Deletesp1;

Итак. Конструктор класса выделяет память для строки путем обращения к конструкции new и сохраняет адрес новой строки в указателе sp1. Деструктор класса освобождает эту память, когда объект класса Base выходит из области видимости. Далее, из базового класса выведем новый класс. Вот такой:

class Derived: public Base

Private:

Char *sp2;

Int size2;

Public:

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

Derived(const char *S1,int s1,const char *S2, int s2): Base(S1,s1){

Size2=s2;

Sp2=new char;

// деструктор

~Derived(){

Cout<<"Derived";

Deletesp2;

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

Теперь где-то в программе мы можем создать объект такого класса:

Derived MyStrings(“string 1”,9,“string 2”,9);

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

Рассмотрим другой вариант. Предположим, что мы объявили указатель на базовый класс Base, но присвоили ему адрес объекта класса Derived. Это вполне допустимо, мы уже обсуждали этот вопрос ранее. То есть, это будет выглядеть в программе так:

Что же произойдет, когда в программе будет удален объект, на который ссылается указатель pBase?

delete pBase;

Компилятор "видит", что указатель pBase должен ссылаться на объекты класса Base (откуда бы ему узнать, что именно присвоено этому указателю?). И вполне естественно программа вызовет только деструктор базового класса, и он удалит одну строку, но оставит в памяти другую. Ведь деструктор класса Derived не вызывался. Получается классическая утечка памяти). И, вот здесь, появляется виртуальный деструктор.

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

virtual ~Base(){

Cout<<"Base";

Deletesp1;

virtual ~Derived(){

Cout<<"Derived";

Deletesp2;

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

Чисто виртуальный деструктор.

Ну и, наконец, последняя порция информации о виртуальных функциях. Может так случиться, что в некоторых случаях будет очень удобно определить в классе чисто виртуальный деструктор.

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

Приведем пример.

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

Something::~Something() {};

Это необходимо сделать, поскольку виртуальный деструктор работает таким образом, что вначале вызывается деструктор производного класса, а затем последовательно деструкторы классов, находящихся выше в цепи наследования, вплоть до базового абстрактного. Это означает, что компилятор будет генерировать вызов ~Something(), даже когда класс является абстрактным, поэтому тело функции надо определять обязательно. Если этого не сделать, компоновщик просто выдаст ошибку отсутствия символа. И сделать это все равно придется.

Несколько советов.

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

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

Примечание: Кстати! Конструкторы не могут быть виртуальными. Будьте бдительны!

Другие похожие работы, которые могут вас заинтересовать.вшм>

3932. Функции в PHP 8.83 KB
В отличие от имен переменных регистр букв в именифункции не учитывается т. Для любого параметра функции может быть задано его значение по умолчанию константное выражение.Если для части параметров функции значение по умолчанию не задается а для части параметров задается то сначала должны быть заданы все параметры не имеющие значений по умолчанию а затем параметры имеющие значения по умолчанию например...
4293. Элементарные функции 5.24 KB
Функции перечисленные ниже сгруппированы по функциональному назначению. Все функции могут использоваться в конструкции вида y=funcx где func имя функции. Обычно в такой форме задается информация о функции в системе MTLB.
4284. Символьные функции 37.14 KB
Когда в MthCD используется символьная математика то результатом преобразований является не число а новое выражение. Если набрать выражение затем в Главном меню выбрать пункт Symbolics Символы далее подпункт Evlute Расчеты а затем опцию Symboliclly Символические то в результате выполненных машиной упрощений на экране появится выражение...
9023. ПРОИЗВОДЯЩИЕ ФУНКЦИИ 141.44 KB
Определение производящей функции. Определение производящей функции Производящей функцией или обычной производящей функцией последовательности чисел называется формальный ряд где формальная переменная. Алгебра степенных рядов определяющих экспоненциальные производящие функции известна как символическое исчисление Блиссара.
4573. Функции менеджмента 31.55 KB
В упрощенном понимании, менеджмент - это умение добиваться поставленных целей, используя труд, интеллект, мотивы поведения других людей. Менеджмент - по-русски “управление” - функция, вид деятельности по руководству людьми в самых разнообразных организациях
20470. ФУНКЦИИ МЕНЕДЖМЕНТА 51.52 KB
Всякая организующая и регулирующая деятельность людей в этих сферах которая приводит к намеченной цели выступает как управление. Кроме того этим людям понадобится искусство составления определенных сценариев поведения людей играющих различные роли в организациях и распределить эти роли как обязанности между ними. Целью любой организации является получение прибыли как конечного результата путем преобразование различных ресурсов денежных материальных трудовых. Иначе говоря управленческое решение должно содержать ответ на вопрос –...
8405. Дружественные функции 14.22 KB
Дружественной функцией класса называется функция которая не являясь его компонентом имеет доступ к его защищенным privte и собственным protected компонентам. Функция не может стать другом класса без его согласия. Для получения прав друга функция должна быть описана в теле класса со спецификатором friend. Функция friend_put описана в классе rect как дружественная и определена как обычная глобальная функция вне класса без указания его имени без операции:: и без спецификатора friend.
10740. Функции денег 13.23 KB
Мера стоимости возникает стихийно, изменяется в зависимости от количества общественного труда, вложенного в данный товар, и заключается в оценке стоимости товаров путем установления цены. Именно при определении цены деньги выполняют функцию меры стоимости.
6899. Функции Конституции РФ 7.46 KB
Роль конституции в обществе реализуется в ее функциях. Политическая функция Конституции РФ заключается прежде всего в признании и закреплении политического многообразия многопартийности идеологического плюрализма. Следующая функция Конституции РФ правовая.
21546. Функции финансов 24.34 KB
Небольшое количество специалистов приводит к тому что бюджет у нас составляют и принимают люди имеющие лишь посредственное представление о том что такое финансы. Предмет исследования сущности финансов актуален еще тем что до сих пор не дано четкое представление что такое финансы границы их распространения. 1 Понятие и сущность финансов Необходимость и сущность финансов Финансы являются одной из важнейших экономических категорий отражающей экономические отношения в процессе создании и использования денежных средств. Финансы...


Если заметили ошибку, выделите фрагмент текста и нажмите Ctrl+Enter
ПОДЕЛИТЬСЯ:
NexxDigital - компьютеры и операционные системы