Компонентные функции структурированных объектов
Как мы знаем, все фундаментальные типы данных, или лучше сказать определенный с помощью этих типов объекты (переменные), характеризуются во-первых, количеством байт отводимых под объект, а во-вторых множеством операций определённых над объектом. Иногда, множество операций над объектами называют «поведением» объекта.
По аналогии с фундаментальными типами данных, с помощью ключевых слов struct, class, union, строятся структурированные (определённые программистом) типы данных, которые также характеризуются этими двумя признаками: объемом памяти и поведением. Как мы знаем объем занимаемый, структурированным объектом зависит от количества компонентных данных включённых в определение структурированного типа, а вот поведение объекта реализуется с помощью так называемых компонентных функций, описание или определение которых также должно присутствовать в определении структурированного объекта. В общем случае определение структурированного объекта, который содержит и компонентные данные, и компонентные функции выглядит следующим образом
struct имя_нового_типа {
определение компонентного данного_1;
…
определение компонентного данного _N;
определение или описание компонентной функции_1;
…
определение или описание компонентной функции_K;
};
Приведём конкретный пример определения структурированного типа, например, комплексное число, в котором поведение объекта (множество операций) реализуем с помощью набора компонентных функций. По мере того как будет меняться наше представление о правильном стиле реализации структурированных объектов, мы будем переделывать определение структурированного типа комплексное число, взятого нами в качестве примера.
struct comp {
// Компонентные данные
double Re, Im;
//Компонентные функции
comp (){Re= 0; Im= 0;} //Конструктор по умолчанию
comp(double r, double i) {Re = r; Im = i;} //Конструктор
comp(comp &T) { Re= T.Re; Im = T.Im;} // Конструктор копии
comp(double r) {Re = r; Im=0;}// Конструктор преобразования типа
comp(comp &&T) { Re= T.Re; Im = T.Im;} ;}// Конструктор переноса
~comp(){} // Деструктор
void display (){ cout<< “\n Re =”<< Re<<”\t Im = “<<Im;}
};
Существует два способа определения компонентных функций структурированного объекта. Первый способ – это записать определение функций непосредственно в определении структурированного типа, как и сделано в примере, в этом случае компилятор пытается сделать эти функции inline, т.е. так называемыми встраиваемыми функциями. Как мы знаем, функция может стать inline функцией, если её определение удовлетворяет достаточно жёсткому набору требований, например, определение не должно содержать операторы цикла, переключатели или операции перехода. Другими словами компонентную функцию можно определить прямо в определении структурированного типа, если она очень проста (в противном случае компилятор выдаст ошибку), если нет, если функция достаточно сложная, то существует второй способ определения компонентной функции. Он заключается в том, что в определении типа остаётся только описание функции, а её определение размещается за его пределами. Покажем это на примере конструктора по умолчанию comp () и функции display ().
Кроме того добавим в класс comp статическое поле static int count и модифицируем конструкторы и деструктор так, чтобы в этом поле хранилось текущее на данный момент число объектов типа comp. Добавим также статическую функцию GetCountComp(), которая при вызове возвращает значение count.
struct comp {
// Компонентные данные
double Re, Im;
static int count;
//Компонентные функции
comp (); //Описание комп. функции (конструктора по умолчанию)
comp(double r, double i) {Re = r; Im = i; count++;}//Конструктор
comp(comp &T) { Re= T.Re; Im = T.Im; count++;}// Конструктор копии
comp(double r) {Re = r; Im=0; count++;} // Конструктор преобразования типа
~comp(){count--;} // Деструктор
void display(); //Описание компонентной функции display()
static int GetCountComp(){return count; }
};
int comp::count = 0;//Инициализация статического компонентного данного
//----------------------------------------------------------------
// Определение компонентной функции (конструктора по умолчанию)
comp::comp(){Re= 0; Im= 0; count++;}
//-----------------------------------------------------------------
//A.display();
//D.display();
// Определение компонентной функции display
void comp::display(){ cout<<“\n Re=”<< Re<<”\t Im=“<< Im;}
//------------------------------------------------------------------
//Определение обычной (не компонентной) функции sum()
comp sum(comp A, comp& С){
comp D;
D.Re = A.Re +С.Re;
D.Im = A.Im +С.Im;
return D;
}
Как видно из примера, определение компонентной функции display() ничем не отличается от определения обычной функции sum(), за исключением того, что перед именем функции стоит имя типа с двумя двоеточиями (comp::). Это «приставка» и сообщает компилятору, что функция display() является компонентной функцией класса comp.
Функция sum() – это обычная (не компонентная) функция, которая принимает в качестве параметров объект и ссылку на объект типа comp и возвращает объект типа comp. Как видно из текста, эта функция в точку вызова возвращает комплексное число, равное сумме двух комплексных чисел, переданных ей в качестве формальных параметров.
Способы вызова компонентных функций. Компонентные функции могут быть вызваны с помощью тех же самых операций:
имя_объекта.имя_функции(фактические параметры);
указатель_на_объект->имя_функции(фактические параметры);
указатель_на_объект->*указатель_на_функцию (фактические параметры);
(*указатель_на_объект).имя_функции(фактические параметры);
или используя полное имя
имя_объекта. имя_типа::имя_функции(фактические параметры);
указатель_на_объект->имя_типа::имя_функции(фактические параметры);
указатель_на_объект->имя_типа::*указатель_на_функцию (фактические параметры);
(*указатель_на_объект).имя_типа::имя_функции(фактические параметры);
которые мы использовали для доступа к полям данных структурированного объекта. Например:
comp sum(comp A, comp& С){ //2.1
comp D; //2.2
D.Re = A.Re +С.Re; //2.3
D.Im = A.Im +С.Im; //2.4
return D; //2.5
//2.6
}
void main (){
сomp A, B(2,3); //1 A.comp(), B.comp(2,3);
A = sum(B, B); //2
A.display(); //3
B.display(); //4
comp * pB = &B; //5
pB->display(); //6 или pB->comp::display(), или (*pB).comp::display() это всё одно и тоже
cout<< “\n GetCountComp()=“<<comp::GetCountComp();// 7 A.GetCountComp()
//8
}
Надо отчётливо понимать, что компонентные функции всегда вызываются с помощью конкретного объекта (или указателя на этот конкретный объект) и для этого конкретного объекта, и следовательно, функции выполняют действия над полями именно этого конкретного объекта. Так, записав А.display() мы вызываем функцию display() для печати полей объекта А, а B.display() – для печати полей объекта В.
void comp::display(){ cout<< “\n Re =”<< Re<<”\t Im = “<<Im;}
Встаёт вопрос, как компонентная функции, например display, во время вызова передаётся информация о том, для какого объекта она вызвана. Другими словами, откуда при вызове A.display() функция display знает, что печатать надо поля Re и Im именно объекта А.
Оказывается во все компонентные функции (за исключением статических) при вызове, неявно передаётся один параметр, имеющий имя this, который является константным указателем на объект, для которого эта компонентная функция была вызвана. И компилятор, опять же неявно, с помощью этого указателя внутри компонентной функции узнаёт, поля какого объекта используются. Фактически, определение функции display() можно переписать в виде:
void comp::display(){ cout<< “\n Re =”<<this->Re<<”\t Im = “<<this->Im;}
и естественно оно будет работать, а компилятору ничего не надо будет неявно подставлять. Причём при вызове A.display() указатель this будет настроен на объект А, а при В.display() – на объект В.
Конструкторы и деструктор. Особое место среди компонентных функций занимают так называемые конструкторы и деструктор. Конструкторы имеют следующие свойства:
имя конструктора совпадает с именем типа (в нашем случае comp) ;
конструктор не возвращает никакого значения (даже void) ;
конструкторы вызываются (обычно неявно), чтобы создать объект соответствующего типа.
Таким образом, если мы в определении структурированного типа данных видим описание или определение функций, которые не возвращают никакого значения и их имя совпадает с именем типа, то мы можем сразу сказать ,что это конструкторы (см. пример выше). Конструкторов в определении типа может быть несколько, но все они должны быть уникальными, т.е. иметь разное количество или типы формальных параметров. Именно по количеству и типу фактических параметров, компилятор определяет какой из конструкторов вызвать. Другими словами в случае с конструкторами мы имеем дело с перегрузкой функций. Напомню, что перегрузкой функций называется ситуация, когда существуют несколько функций имеющих одинаковое имя, но различающихся количеством или типом формальных параметров, и компилятор по типу и количеству фактических параметров в вызове функции определяет, какую конкретно функцию ему необходимо вызвать. По количеству и типу параметров различают следующие типы конструкторов:
Конструктор по умолчанию - конструктор не имеющий ни одного параметра comp(), вызывается компилятором при создании объекта структурированного типа без параметров;
Конструктор копии – конструктор имеющий один параметр, который является ссылкой на объект этого же самого типа comp(comp& T) , вызывается компилятором в двух случаях, при вызове функции, имеющей формальным параметром объект структурированного типа. Задача конструктора копии – при вызове функции создать на месте формального параметра функции локальный объект структурированного типа и скопировать в него содержимое фактического параметра. Вызывается компилятором во втором случае, если функция возвращает по значению объект структурированного типа. В этом случае в задачу конструктора копии входит создание в точке вызова неименованного структурированного объекта и копирование в него содержимое локального структурированного объекта, стоящего после оператора return.
Конструктор преобразования типа – конструктор имеющий один параметр (comp (double r)). С его помощью ( или с их помощью, так как конструкторов преобразования типа может быть несколько, например comp (int r)) осуществляется реализация возможности неявное преобразование объектов типа double к объекту типу comp в тех местах программы, в которых вместо объекта comp был использован объект double, например:
comp sum(comp A, comp& С)
double k = 7.1;
comp B, F(3, 2); /* Для объекта В вызывается конструктор по умолчанию, так как у него нет параметров. А для создания объекта F вызывается обычный конструктор, который имеет два параметра, в нашем случае comp(double r, double i). Заметим, что фактические параметры F это целые числа (3, 2), а не вещественные, но тем не менее ошибки нет, так как они по умолчанию преобразуются к типу double. Можно сказать, что в типе double имеется конструктор преобразования типа из int в double */
B = sum (k, F);
Первый параметр в соответствии с определением и описанием функции sum должен быть типа comp, но в примере использован double. В обычном случае компилятор проверяет соответствие типов формальных и фактических параметров в вызове функции, и если они не совпадают, выдаёт сообщение об ошибке, но так как мы определили в типе comp конструктор преобразования типа, то он будет неявно вызван и преобразует объект double к объекту типа comp и сообщение об ошибке выдаваться не будет.
Обычные конструкторы – это конструкторы с несколькими параметрами, они естественно должны отличаться друг от друга количеством или типом формальных параметров. Вызываются такие конструкторы при создании объекта структурированного типа с параметрами.
Деструктор - компонентная функция не возвращающая никакого значения (даже void) и не имеющая ни одного параметра, имя которой совпадает с именем типа перед которым стоит символ ~, в нашем случае ~comp(). Деструктор вызывается компилятором перед уничтожением объекта, что даёт возможность программисту выполнить какую-то последовательность действий перед тем как объект будет уничтожен. Обычно в деструкторе освобождают динамическую память, которая была ранее выделена под поля структурированного объекта. В нашем примере деструктор пустой, так как в объектах типа comp не используется динамическая память и нет указателей, которые бы указывали на фрагменты динамической памяти. Если конструкторов может быть много, то деструктор только один.
Рассмотрим подробно, какие компонентные функции явно или неявно вызываются в нашем примере функции main, приведённой выше. Для создания объекта А вызывается конструктор по умолчанию; для создания объекта В – конструктор с двумя параметрами. При вызове функции sum( B, &B) для первого фактического параметра вызывается конструктор копии. Его задача создать локальный объект А в функции sum и скопировать в него содержимое объекта В. А вот для второго фактического параметра B конструктор копии в вызове функции sum(B, B) не вызывается, так как второй формальный параметр С является ссылкой и , следовательно, нет необходимости в создании нового локального объекта , так как ссылка является ещё одним именем для уже существующего объекта, то есть С становиться В.
При выполнении операторов тела функции sum вызывается конструктор по умолчанию для создания объекта D. Далее выполняется суммирование и оператор return D. Мы с вами знаем, как выполняется оператор return при возврате по значению из функции объектов фундаментальных типов. В точке вызова создаётся неименованный объект, совпадающий с типом функции, в который копируется содержимое выражения, стоящего после оператора return. Тоже самое происходит и со структурированными объектами, в точке вызова функции sum() конструктор копии создаёт неименованный объект типа comp, в который копирует содержимое локального объекта D.
Далее содержимое этого неименованного объекта с помощью операции присваивания записывается в объект А, и начинают вызываться деструкторы. После выполнения второй строки программы вызываются два деструктора, чтобы разрушить локальный объект D и неименованный объект. Далее вызывается два раза компонентная функция display() для вывода на экран полей объектов А и В.В следующей строке comp * pB = &B; определяется и инициализируется указатель pB типа comp. В этой строке никаких конструкторов не вызывается, так как pB – это указатель, а не объект типа comp. Далее с помощью указателя для объекта В вызывается функция display() После выполнения последней строки программы будут вызваны ещё два деструктора для уничтожения локальных внутри функции main() объектов A и В.
Конструкторы и деструктор являются несколько специфичными, но тем не менее обычными компонентными функциями, поэтому им также неявно передаётся указатель this, и они могут быть вызваны, как и обычные компонентные функции явно с помощью имени объекта и операции точка. Другими словами первую строку сomp A, B(2,3); можно было бы переписать в виде comp A.comp(), B.comp(2,3);
Статические компонентные функции. Статическими компонентными функциями называются функции, перед определением или описанием которых в определении класса стоит ключевое слово static. Перечислим основные сходства и различия между обычными и статическими компонентными функциями:
• статические компонентные функции могут быть вызваны с помощью имени класса имя_класса::имя_статической_функции(фактические параметры), т.е. статическая компонентная функция может быть вызвана до того как будет создан первый объект структурированного типа имя_класса (для обычных компонентных функций это невозможно);
• статические компонентные функции могут быть вызваны и как обычные компонентные функции с помощью имени объекта или указателя на объект, например имя_объекта.имя_статической_функции(фактические параметры);
• в статические компонентные функции не передаётся скрытно указатель this, поэтому в теле функции непосредственно мы можем обращаться (модифицировать) только к статическим компонентным данным.
Фактически, статические компонентные функции были созданы для работы со статическими компонентными данными и именно там они обычно и используются.
Дата добавления: 2020-12-11; просмотров: 374;