Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

582

class EvenPair { public:

bool operator()( int val1, val2 ) { return ! (val2 % val1 ); }

};

ilist.unique( EvenPair() );

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

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

Упражнение 12.8

Измените программу из раздела 12.2, используя список вместо вектора.

С++ для начинающих

583

Часть IV

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

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

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

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

когда объект класса инициализируется или ему присваивается значение другого объекта того же класса.

Вглаве 15 мы расскажем о перегрузке операторов, которая позволяет использовать операнды типа класса со встроенными операторами, описанными в главе 4. Таким образом, работа с объектами типа класса может быть сделана столь же понятной, как и работа со встроенными типами. В начале главы 15 представлены общие концепции и соображения, касающиеся проектирования перегрузки операторов, а затем рассмотрены конкретные операторы, такие, как присваивание, взятие индекса, вызов, а также специфичные для классов операторы new и delete. Иногда необходимо объявить перегруженный оператор, как друга класса, наделив его специальными правами доступа, в данной главе объясняется, зачем это нужно. Здесь же представлен еще один специальный вид функций-членов конвертеры, которые позволяют программисту определить стандартные преобразования. Конвертеры неявно применяются компилятором, когда объекты класса используются в качестве фактических аргументов функции или операндов встроенного либо перегруженного оператора. Завершается глава изложением правил разрешения перегрузки функций с учетом аргументов типа класса, функций-членов и перегруженных операторов.

Тема главы 16 – шаблоны классов. Шаблон это предписание для создания класса, в котором один или несколько типов параметризованы. Например, vector может быть параметризован типом элементов, хранящихся в нем, а buffer типом элементов в буфере или его размером. В этой главе объясняется, как определить и конкретизировать шаблон. Поддержка классов в C++ теперь рассматривается иначе в свете наличия шаблонов, и снова обсуждаются функции-члены, объявления друзей и вложенные типы. Здесь мы еще раз вернемся к модели компиляции шаблонов, описанной в главе 10, чтобы показать, какое влияние оказывают на нее шаблоны классов.

С++ для начинающих

584

13

13. Классы

Механизм классов в C++ позволяет пользователям определять собственные типы данных. По этой причине их часто называют пользовательскими типами. Класс может наделять дополнительной функциональностью уже существующий тип. Так, например, IntArray, введенный в главе 2, предоставляет больше возможностей, чем тип массив int”. С помощью классов можно создавать абсолютно новые типы, например Screen (экран) или Account (расчетный счет). Как правило, классы используются для абстракций, не отражаемых встроенными типами адекватно.

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

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

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

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

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

class Screen { /* ... */ };

такого определения должны стоять точка с запятой или список объявлений: class Screen { /* ... */ } myScreen, yourScreen;

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

Каждое определение вводит новый тип данных. Даже если два класса имеют одинаковые списки членов, они все равно считаются разными типами:

С++ для начинающих

585

class First { int memi; double memd;

};

class Second { int memi; double memd;

};

class First obj1;

Second obj2 = obj1; // ошибка: obj1 и obj2 имеют разные типы

Тело класса определяет отдельную область видимости. Объявление членов внутри тела помещает их имена в область видимости класса. Наличие в двух разных классах членов с одинаковыми именами не ошибка, эти имена относятся к разным объектам. (Подробнее об областях видимости классов мы поговорим в разделе 13.9.)

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

написать ключевое слово class, а после него имя класса. В предыдущем примере объект obj1 класса First объявлен именно таким образом;

указать только имя класса. Так объявлен объект obj2 класса Second из приведенного примера.

Оба способа сослаться на тип класса эквивалентны. Первый заимствован из языка C и остается корректным методом задания типа класса; второй способ введен в C++ для упрощения объявлений.

13.1.1. Данные-члены

Данные-члены класса объявляются так же, как переменные. Например, у класса Screen

#include <string>

 

class Screen {

_screen;

string

string::size_type _cursor;

short

_height;

short

_width;

могут быть следующие данные-члены:

};

//string( _height * _width )

//текущее положение на экране

//число строк

//число колонок

Поскольку мы решили использовать строки для внутреннего представления объекта класса Screen, то член _screen имеет тип string. Член _cursor это смещение в строке, он применяется для указания текущей позиции на экране. Для него использован переносимый тип string::size_type. (Тип size_type рассматривался в разделе 6.8.)

Необязательно объявлять два члена типа short по отдельности. Вот объявление класса Screen, эквивалентное приведенному выше:

С++ для начинающих

586

class Screen { /*

*_ screen адресует строку размером _height * _width

*_cursor указывает текущую позицию на экране

*_height и _width - соответственно число строк и колонок

*/

string _screen;

string::size_type _cursor;

short _height, _width;

};

class StackScreen { int topStack;

void (*handler)(); // указатель на функцию vector<Screen> stack; // вектор классов

Член класса может иметь любой тип:

};

Описанные данные-члены называются нестатическими. Класс может иметь также и статические данные-члены. (У них есть особые свойства, которые мы рассмотрим в разделе 13.5.)

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

class First {

// ошибка

int

memi = 0;

double

memd = 0.0;

// ошибка

членов, нельзя явно инициализировать в теле класса:

};

Данные-члены класса инициализируются с помощью конструктора класса. (Мы рассказывали о конструкторах в разделе 2.3; более подробно они рассматриваются в главе 14.)

13.1.2. Функции-члены

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

Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен в разделе 8.5.) Например:

С++ для начинающих

587

class Screen { public:

void home();

void move( int, int ); char get();

char get( int, int );

void checkRange( int, int ); // ...

};

class Screen { public:

//определения функций home() и get() void home() { _cursor = 0; }

char get() { return _screen[_cursor]; }

//...

Определение функции-члена также можно поместить внутрь тела класса:

};

home() перемещает курсор в левый верхний угол экрана; get() возвращает символ, находящийся в текущей позиции курсора.

Функции-члены отличаются от обычных функций следующим:

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

ptrScreen->home();

одного из операторов доступа к членам точки (.) или стрелки (->): myScreen.home();

(в разделе 13.9 область видимости класса обсуждается более детально);

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

Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:

С++ для начинающих

588

class Screen { public:

// объявления перегруженных функций-членов get() char get() { return _screen[_cursor]; }

char get( int, int ); // ...

};

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

13.1.3. Доступ к членам

Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen показал, что для его объектов всегда задается размер экрана 80 × 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более

class Screen { public:

//функции-члены private:

//инициализация статических членов (см. 13.5)

static const int

_height = 24;

static const int

_width = 80;

string

_screen;

string::size_type

_cursor;

эффективным:

};

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

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

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

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

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

С++ для начинающих

589

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

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

защищенный член ведет себя как открытый по отношению к производному классу и как закрытый по отношению к остальной части программы. (В главе 2 мы видели пример использования защищенных членов в классе IntArray. Детально они рассматриваются в главе 17, где вводится понятие наследования.)

class Screen { public:

void home() { _cursor = 0; }

char get() { return _screen[_cursor]; } char get( int, int );

void move( int, int );

// ...

 

private:

_screen;

string

string::size_type _cursor;

short

_height, _width;

Вследующем определении класса Screen указаны секции public и private:

};

Согласно принятому соглашению, сначала объявляются открытые члены класса. (Обсуждение того, почему в старых программах C++ сначала шли закрытые члены и почему этот стиль еще кое-где сохранился, см. в книге [LIPPMAN96a].) В теле класса может быть несколько секций public, protected и private. Каждая секция продолжается либо до метки следующей секции, либо до закрывающей фигурной скобки. Если спецификатор доступа не указан, то секция, непосредственно следующая за открывающей скобкой, по умолчанию считается private.

13.1.4. Друзья

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

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

class Screen { friend istream&

operator>>( istream&, Screen& ); friend ostream&

operator<<( ostream&, const Screen& ); public:

// ... оставшаяся часть класса Screen

подобные объявления сразу после заголовка класса:

};

С++ для начинающих

590

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

#include <iostream>

ostream& operator<<( ostream& os, const Screen& s )

{

// правильно: можно обращаться к _height, _width и _screen os << "<" << s._height

<< "," << s._width << ">"; os << s._screen;

return os;

класса Screen. Простая реализация оператора вывода выглядит следующим образом:

}

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

13.1.5. Объявление и определение класса

О классе говорят, что он определен, как только встретилась скобка, закрывающая его тело. После этого становятся известными все члены класса, а следовательно, и его размер.

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

class Screen; // объявление класса Screen

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

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

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

Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:

С++ для начинающих

591

 

 

class Screen; // объявление

 

 

 

 

 

 

class StackScreen {

 

 

 

int topStack;

 

 

 

// правильно: указатель на объект Screen

 

 

 

Screen *stack;

 

 

 

void (*handler)();

 

 

 

};

 

 

 

 

 

 

 

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

 

может быть данных-членов его собственного типа. Однако класс считается объявленным,

 

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

 

 

 

class LinkScreen {

 

 

 

 

 

 

Screen window;

 

 

 

LinkScreen *next;

 

 

 

LinkScreen *prev;

 

ссылками или указателями на его тип. Например:

 

 

 

};

 

 

 

 

 

 

 

Упражнение 13.1

 

 

 

string _name;

 

 

 

 

Пусть дан класс Person со следующими двумя членами:

 

 

 

string _address;

 

 

 

Person( const string &n, const string &s )

 

 

 

 

 

 

 

 

 

: _name( n ), _address( a ) { }

 

 

 

string name() { return _name; }

 

и такие функции-члены:

 

 

 

string address() { return _address; }

 

 

 

 

 

 

 

 

Какие члены вы объявили бы в секции public, а какие в секции private? Поясните

 

свой выбор.

 

Упражнение 13.2

 

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

 

использовать объявление класса? А определение?

 

13.2. Объекты классов

Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen: