Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
591 }; Поскольку класс не считается определенным, пока не закончилось его тело, то в нем не может быть данных-членов его собственного типа. Однако класс считается объявленным, как только распознан его заголовок, поэтому в нем допустимы члены, являющиеся ссылками или указателями на его тип. Например: }; Упражнение 13.1 Пусть дан класс Person со следующими двумя членами: string _address; и такие функции-члены: string address() { return _address; } Какие члены вы объявили бы в секции public, а какие – в секции private? Поясните свой выбор. Упражнение 13.2 Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение? 13.2. Объекты классов Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen: class Screen; // объявление class StackScreen { int topStack; // правильно: указатель на объект Screen Screen *stack; void (*handler)(); class LinkScreen { Screen window; LinkScreen *next; LinkScreen *prev; string _name; Person( const string &n, const string &s ) : _name( n ), _address( a ) { } string name() { return _name; } С++ для начинающих 592 }; то определение Screen myScreen; выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen. Область видимости объекта класса зависит от его положения в тексте программы. Он определяется в иной области, нежели сам тип класса: } Тип Screen объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main(). Объект класса также имеет время жизни. В зависимости от того, где (в области видимости пространства имен или в локальной области) и как (статическим или нестатическим) он объявлен, он может существовать в течение всего времени выполнения программы или только во время вызова некоторой функции. Область видимости объекта класса и его время жизни ведут себя очень похоже. (Понятия области видимости и времени жизни введены в главе 8.) Объекты одного и того же класса можно инициализировать и присваивать друг другу. По умолчанию копирование объекта класса эквивалентно копированию всех его членов. Например: // bufScreen._screen = myScreen._screen; class Screen { public: // функции-члены private: string _screen; string:size_type _cursor; short _height; short _width; class Screen { // список членов }; int main() { Screen mainScreen; Screen bufScreen = myScreen; // bufScreen._height = myScreen._height; // bufScreen._width = myScreen._width; // bufScreen._cursor = myScreen._cursor; С++ для начинающих 593 Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.) } По умолчанию объект класса передается по значению, если он выступает в роли аргумента функции или ее возвращаемого значения. Можно объявить формальный параметр функции или возвращаемое ею значение как указатель или ссылку на тип класса. (В разделе 7.3 были представлены параметры, являющиеся указателями или ссылками на типы классов, и объяснялось, когда их следует использовать. В разделе 7.4 с этой точки зрения рассматривались типы возвращаемых значений.) Для доступа к данным или функциям-членам объекта класса следует пользоваться соответствующими операторами. Оператор “точка” (.) применяется, когда операндом является сам объект или ссылка на него; а “стрелка” (->) – когда операндом служит указатель на объект: } isEqual() – это не являющаяся членом функция, которая сравнивает два объекта Screen . У нее нет права доступа к закрытым членам Screen, поэтому напрямую обращаться к ним она не может. Сравнение проводится с помощью открытых функций- членов данного класса. Для получения высоты и ширины экрана isEqual() должна пользоваться функциями- членами height() и width() для чтения закрытых членов класса. Их реализация тривиальна: int main() { Screen myScreen, bufScreen[10]; Screen *ptr = new Screen; myScreen = *ptr; delete ptr; ptr = bufScreen; Screen &ref = *ptr; Screen &ref2 = bufScreen[6]; #include "Screen.h" bool isEqual( Screen& s1, Screen *s2 ) { // возвращает false, если объекты не равны, и true - если равны if (s1.height() != s2->height() || s2.width() != s2->width() ) return false; for ( int ix = 0; ix < s1.height(); ++ix ) for ( int jy = 0; jy < s2->width(); ++jy ) if ( s1.get( ix, jy ) != s2->get( ix, jy ) ) return false; return true; // попали сюда? значит, объекты равны С++ для начинающих 594 }; Применение оператора доступа к указателю на объект класса эквивалентно последовательному выполнению двух операций: применению оператора разыменования (*) к указателю, чтобы получить адресуемый объект, и последующему применению оператора “точка” для доступа к нужному члену класса. Например, выражение s2->height() можно переписать так: (*s2).height() Результат будет одним и тем же. 13.3. Функции-члены класса Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen такой набор состоит из следующих объявленных в нем функций-членов: }; Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре: groupScreen.home(); При вызове функции home() для объекта myScreen происходит обращение к его члену _cursor . Когда же эта функция вызывается для объекта groupScreen, то она обращается class Screen { public: int height() { return _height; } int width() { return _width; } // ... private: short _heigh, _width; // ... class Screen { public: void home() { _cursor = 0; } char get() { return _screen[_cursor]; } char get( int, int ); void move( int, int ); bool checkRange( int, int ); int height() { return _height; } int width() { return _width; } // ... Screen myScreen, groupScreen; myScreen.home(); С++ для начинающих 595 к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе. 13.3.1. Когда использовать встроенные функции-члены Обратите внимание, что определения функций home(), get(), height() и width() приведены прямо в теле класса. Такие функции называются встроенными. (Мы говорили об этом в разделе 7.6.) Функции-члены можно объявить в теле класса встроенными и явно, поместив перед типом возвращаемого значения ключевое слово inline: }; Определения home() и get() в приведенных примерах эквивалентны. Поскольку ключевое слово inline избыточно, мы в этой книге не пишем его явно для функций- членов, определенных в теле класса. Функции-члены, состоящие из двух или более строк, лучше определять вне тела. Для идентификации функции как члена некоторого класса требуется специальный синтаксис объявления: имя функции должно быть квалифицировано именем ее класса. Вот как выглядит определение функции checkRange(), квалифицированное именем Screen: } Прежде чем определять функцию-член вне тела класса, необходимо объявить ее внутри тела, обеспечив ее видимость. Например, если бы перед определением функции checkRange() не был включен заголовочный файл Screen.h, то компилятор выдал бы сообщение об ошибке. Тело класса определяет полный список его членов. Этот список не может быть расширен после закрытия тела. class Screen { public: // использование ключевого слова inline // для объявления встроенных функций-членов inline void home() { _cursor = 0; } inline char get() { return _screen[_cursor]; } // ... #include #include "screen.h" // имя функции-члена квалифицировано именем Screen:: bool Screen::checkRange( int row, int col ) { // проверить корректность координат if ( row < 1 || row > _height || col < 1 || col > _width ) { cerr << "Screen coordinates ( " << row << ", " << col << " ) out of bounds.\n"; return false; } return true; С++ для начинающих 596 Обычно функции-члены, определенные вне тела класса, не делают встроенными. Но объявить такую функцию встроенной можно, если явно добавить слово inline в объявление функции внутри тела класса или в ее определение вне тела, либо сделав то и другое одновременно. В следующем примере move() определена как встроенная функция- член класса Screen: } Функция get(int, int) объявляется встроенной с помощью слова inline: }; Определение функции следует после объявления класса. При этом слово inline можно опустить: } Так как встроенные функции-члены должны быть определены в каждом исходном файле, где они вызываются, то встроенную функцию, не определенную в теле класса, следует поместить в тот же заголовочный файл, в котором определен ее класс. Например, представленные ранее определения move() и get() должны находиться в заголовочном файле Screen.h после определения класса Screen. 13.3.2. Доступ к членам класса Говорят, что определение функции-члена принадлежит области видимости класса независимо от того, находится ли оно вне или внутри его тела. Отсюда следуют два вывода: • в определении функции-члена могут быть обращения к любым членам класса, открытым или закрытым, и это не нарушает ограничений доступа; • когда функция-член обращается к членам класса, операторы доступа “точка” и “стрелка” не необходимы. inline void Screen::move( int r, int c ) { // переместить курсор в абсолютную позицию if ( checkRange( r, c ) ) // позиция на экране задана корректно? { int row = (r-1) * _width; // смещение начала строки _cursor = row + c - 1; } class Screen { public: inline char get( int, int ); // объявления других функций-членов не изменяются char Screen::get( int r, int c ) { move( r, c ); // устанавливаем _cursor return get(); // вызываем другую функцию-член get() С++ для начинающих 597 Например: } Хотя _screen, _height, _width и _cursor являются закрытыми членами класса Screen, функция-член copy() работает с ними напрямую. Если при обращении к члену отсутствует оператор доступа, то считается, что речь идет о члене того класса, для которого функция-член вызвана. Если вызвать copy() следующим образом: } то параметр sobj внутри определения copy() соотносится с объектом s1 из функции main() . Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2 . В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this. 13.3.3. Закрытые и открытые функции-члены Функцию-член можно объявить в любой из секций public, private или protected тела класса. Где именно это следует делать? Открытая функция-член задает операцию, которая может понадобиться пользователю. Множество открытых функций-членов #include { // если этот объект и объект sobj - одно и то же, // копирование излишне // мы анализируем указатель this (см. раздел 13.4) if ( this != &sobj ) { _height = sobj._height; _width = sobj._width; _cursor = 0; // создаем новую строку; // ее содержимое такое же, как sobj._screen _screen = sobj._screen; } #include "Screen.h" int main() { Screen s1; // Установить s1 Screen s2; s2.copy(s1); // ... С++ для начинающих 598 составляет интерфейс класса. Например, функции-члены home(), move() и get() класса Screen определяют операции, с помощью которых программа манипулирует объектами этого типа. Поскольку мы прячем от пользователей внутреннее представление класса, объявляя его члены закрытыми, то для манипуляции объектами типа Screen необходимо предоставить открытые функции-члены. Такой прием – сокрытие информации – защищает написанный пользователем код от изменений во внутреннем представлении. Внутреннее состояние объекта класса также защищено от случайных изменений. Все модификации объекта производятся с помощью небольшого набора функций, что существенно облегчает сопровождение и доказательство правильности программы. До сих пор мы встречались лишь с функциями, поддерживающими доступ к закрытым членам только для чтения. Ниже приведены две функции set(), позволяющие пользователю модифицировать объект Screen. Добавим их объявления в тело класса: }; Далее следуют определения функций: } В реализации класса Screen мы предполагаем, что объект Screen не содержит двоичных нулей. По этой причине set() не позволяет записать на экран нуль. Представленные до сих пор функции-члены были открытыми, их можно вызывать из любого места программы, а закрытые вызываются только из других функций-членов (или class Screen { public: void set( const string &s ); void set( char ch ); // объявления других функций-членов не изменяются void Screen::set( const string &s ) { // писать в строку, начиная с текущей позиции курсора int space = remainingSpace(); int len = s.size(); if ( space < len ) { cerr << "Screen: warning: truncation: " << "space: " << space << "string length: " << len << endl; len = space; } _screen.replace( _cursor, len, s ); _cursor += len - 1; } void Screen::set( char ch ) { if ( ch == '\0' ) cerr << "Screen: warning: " << "null character (ignored).\n"; else _screen[_cursor] = ch; С++ для начинающих 599 друзей) класса, но не из программы, обеспечивая поддержку другим операциям в реализации абстракции класса. Примером может служить функция-член remainingSpace класса Screen(), использованная в set(const string&). }; remainingSpace() сообщает, сколько места осталось на экране: } (Детально защищенные функции-члены будут рассмотрены в главе 17.) Следующая программа предназначена для тестирования описанных к настоящему моменту функций-членов: } class Screen { public: // объявления других функций-членов не изменяются private: inline int remainingSpace(); inline int Screen::remainingSpace() { int sz = _width * _height; return ( sz - _cursor ); #include "Screen.h" #include Screen sobj(3,3); // конструктор определен в разделе 13.3.4 string init("abcdefghi"); cout << "Screen Object ( " << sobj.height() << ", " << sobj.width() << " )\n\n"; // Задать содержимое экрана string::size_type initpos = 0; for ( int ix = 1; ix <= sobj.width(); ++ix ) for ( int iy = 1; iy <= sobj.height(); ++iy ) { sobj.move( ix, iy ); sobj.set( init[ initpos++ ] ); } // Напечатать содержимое экрана for ( int ix = 1; ix <= sobj.width(); ++ix ) { for ( int iy = 1; iy <= sobj.height(); ++iy ) cout << sobj.get( ix, iy ); cout << "\n"; } return 0; С++ для начинающих 600 Откомпилировав и запустив эту программу, мы получим следующее: Screen Object ( 3, 3 ) abc def ghi 13.3.4. Специальные функции-члены Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором new. В объявлении конструктора его имя совпадает с именем класса. Вот, например, объявление конструктора класса Screen, в котором заданы значения по умолчанию для параметров hi, wid и bkground: }; Определение конструктора класса Screen выглядит так: } Каждый объявленный объект класса Screen автоматически инициализируется конструктором: } class Screen { public: Screen( int hi = 8, int wid = 40, char bkground = '#'); // объявления других функций-членов не изменяются Screen::Screen( int hi, int wid, char bk ) : _height( hi ), // инициализировать _height значением hi _width( wid ), // инициализировать _width значением wid _cursor ( 0 ), // инициализировать _cursor нулем _screen( hi * wid, bk ) // размер экрана равен hi * wid // все позиции инициализируются // символом '#' { // вся работа проделана в списке инициализации членов // этот список обсуждается в разделе 14.5 Screen s1; // Screen(8,40,'#') Screen *ps = new Screen( 20 ); // Screen(20,40,'#') int main() { Screen s(24,80,'*'); // Screen(24,80,'*') // ... С++ для начинающих 601 (В главе 14 конструкторы, деструкторы и операторы присваивания рассматриваются более подробно. В главе 15 обсуждаются конвертеры и функции управления памятью.) 13.3.5. Функции-члены со спецификаторами const и volatile Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например: blank = '\n'; // ошибка Однако объект класса, как правило, не модифицируется программой напрямую. Вместо этого вызывается та или иная открытая функция-член. Чтобы не было “покушений” на константность объекта, компилятор должен различать безопасные (те, которые не изменяют объект) и небезопасные (те, которые пытаются это сделать) функции-члены: blankScreen.set( '*' ); // ошибка: модифицирует объект класса Проектировщик класса может указать, какие функции-члены не модифицируют объект, объявив их константными с помощью спецификатора const: }; Для класса, объявленного как const, могут быть вызваны только те функции-члены, которые также объявлены со спецификатором const. Ключевое слово const помещается между списком параметров и телом функции-члена. Для константной функции-члена, определенной вне тела класса, это слово должно присутствовать как в объявлении, так и в определении: const char blank = ' '; const Screen blankScreen; blankScreen.display(); // читает объект класса class Screen { public: char get() const { return _screen[_cursor]; } // ... |