Классы. Понятие класса.


Класс — это определяемый пользователем тип. Описание класса очень похоже на описание структуры в Си. Рассмотрим реализацию понятия даты с использованием struct для того, чтобы определить представление даты date и множества функций для работы с переменными этого типа:

struct date {

int month, day, year; // дата: месяц, день, год

void set(int, int, int);

void get(int*, int*, int};

void next();

void print();

// …

};

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

date today; // сегодня

date my_burthday; // мой день рождения

void f()

{

my_burthday.set(30,12,1950);

today.set(18,1,1985);

my_burthday.print();

today.next();

}

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

void date::next()

{

if ( ++day > 28 ) {

// делает сложную часть работы

}

}

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

Описание date в предыдущем подразделе дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить используя вместо struct class:

class date {

int month, day, year;

public:

void set(int, int, int);

void get(int*, int*, int};

void next();

void print();

};

Метка public: делит тело класса на две части. Имена в первой, закрытой части, могут использоваться только функциями членами. Вторая, открытая часть, составляет интерфейс к объекту класса. Struct — это просто class, у которого все члены классы открытые, поэтому функции члены определяются и используются точно так же, как в предыдущем случае. Например:

void date::print() // печатает в записи, принятой в США

{

cout << month << "/" << day << "/" year;

}

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

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

class x {

int m;

public:

int readm() { return m; }

};

x aa;

x bb;

void f()

{

int a = aa.readm();

int b = bb.readm();

// …

}

В первом вызове члена member() m относится к aa.m, а во втором — к bb.m.

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

x* this;

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

class x {

int m;

public:

int readm() { return this->m; }

};

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

данного типа, она называется конструктором. Конструктор распознается по тому, что имеет то же имя, что и сам класс. Например:

class date {

// …

date(int, int, int);

};

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

date today = date(23,6,1983);

date xmas(25,12,0); // сокращенная форма

date my_burthday; // недопустимо, опущена

// инициализация

Часто бывает хорошо обеспечить несколько способов инициализации объекта класса. Это можно сделать, задав несколько конструкторов. Например:

class date {

int month, day, year;

public:

// …

date(int, int, int); // день месяц год

date(char); // дата в строковом представлении

date(int); // день, месяц и год сегодняшние

date(); // дата по умолчанию: сегодня

};

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

date today(4);

date july4("Июль 4, 1983");

date guy("5 Ноя");

date now; // инициализируется по

// умолчанию

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

class date {

int month, day, year;

public:

// …

date(int d =0, int m =0, int y =0);

date(char); // дата в строковом представлении

};

date::date(int d, int m, int y)

{

day = d ? d : today.day;

month = m ? m : today.month;

year = y ? y : today.year;

// проверка, что дата допустимая

// …

}

Определяемый пользователем тип чаще имеет, чем не имеет, конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие, деструктор, чтобы обеспечить соответствующую очистку объектов этого типа. Имя деструктора для класса X есть ~X() («дополнение конструктора»). В частности, многие типы используют некоторый объем памяти из свободной памяти, которая выделяется конструктором и освобождается деструктором.

// Программ 1, работающая с классом First

// Файл FIRST.H

#include <stdio.h>

#include <iostream.h>

class First

{

private:

int field1; // Подсчитывает обращения к классу First

float field2, field3;

void setField1( int anInt ) { field1 = anInt; }

public:

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

First( void )

{

cout << "\nIn void constructor for First";

setField1( 0 );

field2 = 0.0;

field3 = 0.0;

}

First( float aField2, float aField3 = 0.0 )

{

cout << "\nIn 2-parameter constructor for First";

setField1( 0 );

field2 = aField2;

field3 = aField3;

}

~First( void ) { cout << "\nIn destructor for First"; }

// Методы доступа

int getField1( void ) { return ++field1; }

float getField2( void ) { field1++; return field2; }

float getField3( void ) { field1++; return field3; }

void setField2( float aFloat ) { field1++; field2 = aFloat; }

void setField3( float aFloat ) { field1++; field3 = aFloat; }

void resetField1( void ) { setField1( 0 ); }

// Операции над полями

int compareFields( void ) { field1++; return field2 == field3; }

// Методы печати

void print( void )

{

char buffer[40];

field1++; // Печать это тоже доступ

cout << "\n Number of accesses to this object = " << field1;

sprintf( buffer , "\n Value of field2 = %.2f" , field2 );

cout << buffer;

sprintf( buffer , "\n Value of field3 = %.2f" , field3 );

cout << buffer;

}

};

// Файл FIRST.CPP

// Тестовая программа для класса First

#include "first.h"

void main()

{

char buffer[40];

First first1 , first2( 10.25 ) , first3( 12.6, -5.6 );

cout << "\n\nTest program results for class First\n";

cout << "\nPrint details of first1";

first1.print();

cout << "\nPrint details of first2";

first2.print();

cout << "\nPrint details of first3";

first3.print();

cout << "\n\nModify fields of first2 and verify";

first2.setField2( 15.8 );

first2.setField3( -8.0 );

first2.print();

cout << "\n\nAccess field2 and field3 of first1";

sprintf( buffer , "\n field2 = %.2f" , first1.getField2() );

cout << buffer;

sprintf( buffer , "\n field3 = %.2f" , first1.getField3() );

cout << buffer;

cout << "\n\nReset and print first3";

first3.resetField1();

first3.print();

cout << "\n\nCompare field2 and field3 of first1";

if( first1.compareFields() )

{

cout << "\n field2 = field3\n";

}

else

{

cout << "\n field2 != field3\n";

}

}