Глава 11. ЗНАКОМСТВО С ОБЪЕКТНО-ОРИЕНТИРОВАННЫМ ПРОГРАММИРОВАНИЕМ...145
Глава 13. РАБОТА С КЛАССАМИ...154
Глава 14. УКАЗАТЕЛИ НА ОБЪЕКТЫ...167
Глава 15. ЗАЩИЩЁННЫЕ ЧЛЕНЫ КЛАССА: НЕ БЕСПОКОИТЬ!...181
Глава 16. СОЗДАНИЕ И УДАЛЕНИЕ ОБЪЕКТОВ...188
Глава 17. АРГУМЕНТАЦИЯ КОНСТРУИРОВАНИЯ...198
Глава 18. КОПИРУЮЩИЙ КОНСТРУКТОР...213
Глава 19. СТАТИЧЕСКИЕ ЧЛЕНЫ...224
В этой части...
Основным отличием С++ от других языков является возможность объектно-ориентированного программирования. Термин объектно-ориентарованный — один из самых популярных в современном компьютерном мире. Языки программирования, редакторы и базы данных — буквально все претендуют на звание объектно-ориентированных. Иногда так оно и есть, но часто такое определение даётся исключительно в рекламных целях.
На прилагаемом компакт-диске имеется программа BUDGET2, которая поможет вам разобраться в этих объектно-ориентированных концепциях.
В этой главе...
►Микроволновые печи и уровни абстракции 145
►Классификация микроволновых печей 146
►Зачем нужна классификация 147
Что такое объектно-ориентированное программирование вообще? Объектно-ориентированное программирование, или ООП, базируется на двух принципах, которые вам известны ещё с младенческого возраста: абстракция и классификация. Чтобы пояснить, что имеется в виду, я расскажу вам одну историю.
Когда мы с сыном смотрим футбол, я подчас испытываю непреодолимую тягу к вредным для здоровья, но таким вкусным мексиканским блюдам. Я бросаю на тарелку чипсы, бобы, сыр, приправы и пять минут зажариваю эту массу в микроволновой печи.
Для того чтобы воспользоваться печью, я открываю её дверцу, забрасываю внутрь полуфабрикат и нажимаю несколько кнопок на передней панели. Через пару минут блюдо готово ( я стараюсь не стоять перед печью, чтобы мои глаза не начали светиться в темноте ).
Обратите внимание на то, чего я не делал, используя свою микроволновую печь.
■■■
■ Не перепрограммировал процессор внутри печи, даже если прошлый раз готовилось абсолютно другое блюдо.
■ Не смотрел внутрь печи.
■ Не задумывался бы над внутренним устройством печи во время приготовления блюд даже в том случае, если бы был главным инженером по производству печей и знал о них всё, включая каждую программу.
■■■
Это не просто пространные рассуждения. В повседневной жизни нас постоянно преследуют стрессы. Чтобы уменьшить их число, мы начинаем обращать внимание только на события определённого масштаба. В объектно-ориентированном программировании уровень детализации, на котором вы работаете, называется уровнем абстракции. Например, чтобы объяснить этот термин, я абстрагируюсь от подробностей внутреннего устройства микроволновой печи.
_________________
145 стр. Глава 11. Знакомство с объектно-ориентированным программированием
Во время приготовления блюда я смотрел на микроволновую печь просто как на железный ящик. И пока я управляю печью с помощью интерфейса, я не могу её сломать, "подвесить" или, что ещё хуже, превратить своё блюдо в угли.
Представьте себе, что я попросил бы своего сына написать алгоритм приготовления мною закусок. Поняв наконец, чего я от него добиваюсь, он бы, наверное, написал что-то вроде "открыть банку бобов, натереть сыра, посыпать перцем" и т.д. Когда дело дошло бы непосредственно до приготовления в печи, он в лучшем случае написал бы нечто подобное: "готовить в микроволновой печи пять минут".
Этот рецепт прост и верен. Но с помощью такого алгоритма "функциональный" программист не сможет написать программу приготовления закусок. Программисты, работающие с функциями, живут в мире, лишённом таких объектов, как микроволновая печь и прочие удобства. Они заботятся о последовательности операций в функциях. В "функциональном" решении проблемы закусок управление будет передано от моих пальцев кнопкам передней панели, а затем внутрь печи. После этого программе придётся решать, на какое время включать печь и когда следует включить звуковой сигнал готовности.
При таком подходе очень трудно отвлечься от сложностей внутреннего устройства печи. В этом мире нет объектов, за которые можно спрятать всю присущую микроволновой печи сложность.
Применяя объектно-ориентированный подход к приготовлению блюд, я первым делом определю объекты, используемые в задаче: сыр, бобы, чипсы и микроволновая печь. После этого я начинаю моделировать эти объекты в программе, не задумываясь над деталями их использования.
При этом я работаю ( и думаю ) на уровне базовых объектов. Я должен думать о том, как приготовить блюдо, не волнуясь о деталях работы микроволновой печи — над этим уже подумали её создатели ( которым нет дела до моих любимых блюд ).
После создания и проверки всех необходимых объектов можно переключиться на следующий уровень абстракции. Теперь я начинаю думать на уровне процесса приготовления закуски, не отвлекаясь на отдельные куски сыра или банку бобов. При таком подходе я легко переведу рецепт моего сына на язык С++.
В концепции уровней абстракции очень важной частью является классификация. Если бы я спросил моего сына: "Что такое микроволновая печь?" — он бы наверняка ответил: "Это печь, которая...". Если бы затем я спросил: "А что такое печь?" — он бы ответил что-то вроде: "Ну, это кухонный прибор, который...". ( Если бы я попытался выяснить, что такое кухонный прибор, он наверняка бы спросил, почему я задаю так много дурацких вопросов. )
Из ответов моего сына становится ясно, что он видит нашу печь как один из экземпляров вещей, которые называются микроволновыми печами. Кроме того, печь является подразделом духовок, а духовки относятся к типу кухонных приборов.
_________________
146 стр. Часть 3. Введение в классы
«В ООП моя микроволновая печь является экземпляром класса микроволновых печей. Класс микроволновых печей является подклассом печей, который, в свою очередь, является подклассом кухонных приборов.»
[Помни!]
Люди склонны заниматься классификацией. Всё вокруг увешано ярлыками. Мы делаем всё, для того чтобы уменьшить количество вещей, которые надо запомнить. Вспомните, например, когда вы первый раз увидели "Пежо" или "Рено". Возможно, в рекламе и говорилось, что это суперавтомобиль, но мы-то с вами знаем, что это не так. Это ведь просто машина. Она имеет все свойства, которыми обладает автомобиль. У неё есть руль, колёса, сиденья, мотор, тормоза и т.д. Могу поспорить, что я смог бы даже водить такую штуку без инструкции.
Я не буду тратить место в книге на описание того, чем этот автомобиль похож на другие. Мне нужно знать лишь то, что это "машина, которая...", и то, чем она отличается от других машин ( например, ценой ). Теперь можно двигаться дальше. Легковые машины являются таким же подклассом колёсных транспортных средств, как грузовики и пикапы. При этом колёсные транспортные средства входят в состав транспортных средств наравне с кораблями и самолётами.
Зачем вообще нужна эта классификация, это объектно-ориентированное программирование? Ведь оно влечёт за собой массу трудностей. Тем более, что у нас уже есть готовый механизм функций. Зачем же что-то менять?
Иногда может показаться, что легче разработать и создать микроволновую печь специально для некоторого блюда и не строить универсальный прибор на все случаи жизни. Тогда на лицевую панель не надо будет помещать никаких кнопок, кроме кнопки СТАРТ. Блюдо всегда готовилось бы одинаковое время, и можно было бы избавиться от всех этих бесполезных кнопок типа РАЗМОРОЗКА или ТЕМПЕРАТУРА ПРИГОТОВЛЕНИЯ. Всё, что требовалось бы от такой печи, — это чтобы в неё помещалась одна тарелка с полуфабрикатом. Да, но что же тогда получится? Ведь при этом один кубический метр пространства использовался бы для приготовления всего одной тарелки закуски!
Чтобы сэкономить место, можно освободиться от этой глупой концепции — "микроволновая печь". Для приготовления закуски хватит и внутренностей печи. Тогда в инструкции достаточно написать примерно следующее: "Поместите полуфабрикат в ящик. Соедините красный и чёрный провод. Установите на трубе излучателя напряжение в 3000 вольт. Должен появиться негромкий гул. Постарайтесь не стоять близко к установке, если вы хотите иметь детей". Простая и понятная инструкция!
Но такой функциональный подход создаёт некоторые проблемы.
■■■
■ Слишком сложно. Я не хочу, чтобы фрагменты микроволновой печи перемешивались с фрагментами закуски при разработке программы. Но поскольку при данном подходе нельзя создавать объекты и упрощать написание, работая с каждым из них в отдельности, приходится держать в голове все нюансы каждого объекта одновременно.
■ Не гибко. Когда-нибудь мне потребуется поменять свою микроволновую печь на печь другого типа. Я смогу это сделать без проблем, если интерфейс печи можно будет оставить старым. Без чётко очерченных областей действия, а также без разделения интерфейса и внутреннего содержимого становится крайне трудно убрать старый объект и поставить на его место новый.
_________________
147 стр. Глава 11. Знакомство с объектно-ориентированным программированием
■ Невозможно использовать повторно. Печи делаются для приготовления разных блюд. Мне, например, не хочется создавать новую печь всякий раз, когда требуется приготовить новое блюдо. Если задача уже решена, неплохо использовать её решение и в других программах.
■■■
В оставшихся главах этой части демонстрируется, каким образом можно решить все эти проблемы при помощи объектно-ориентированного программирования.
_________________
148 стр. Часть 3. Введение в классы
В этой главе...
►Формат класса 149
►Обращение к членам класса 150
Очень часто программы имеют дело с совокупностями данных: имя, должность, табельный номер и т.д. Каждая отдельная составляющая не описывает человека, смысл имеет только вся вместе взятая информация. Простая структура, такая как массив, прекрасно подходит для хранения отдельных значений, однако совершенно непригодна для хранения совокупности данных разных типов. Таким образом, массив недостаточен для хранения комплексной информации.
По причинам, которые вскоре станут понятными, я буду называть такие совокупности информации объектами. Микроволновая печь — объект. Вы также объект ( и я тоже, хотя уже и не так уверен в этом ). Ваше имя, должность и номер кредитной карты, содержащиеся в базе данных, тоже являются объектом.
Для хранения разнотипной информации о физическом объекте нужна специальная структура. В нашем простейшем примере эта структура должна содержать поля имени, фамилии и номера кредитной карты.
В С++ структура, которая может объединить несколько разнотипных переменных в одном объекте, называется классом.
Класс, описывающий объект, который содержит имя и номер кредитной карты, может быть создан так:
/* Класс dataset */
class NameDataSet
{
public :
char firstName[ 128 ] ;
char lastName [ 128 ] ;
int creditCard ;
} ;
/* Экземпляр класса dataset */
NameDataSet nds ;
_________________
149 стр. Глава 12. Классы в С++
Объявление класса начинается с ключевого слова class, после которого идёт имя класса и пара фигурных скобок, открывающих и закрывающих тело класса.
После открывающей скобки находится ключевое слово public. ( Не спрашивайте меня сейчас, что оно значит, — я объясню его значение немного позже. В следующих главах поясняются разные ключевые слова, такие как public или private. А до тех пор пока я не сделаю private публичным, значение public останется приватным :-). )
«Можно использовать альтернативное ключевое слово struct, которое полностью идентично class, с предполагаемым использованием объявлений public.»
[Советы]
После ключевого слова public идёт описание полей класса. Как видно из листинга, класс NameDataSet содержит поля имени, фамилии и номера кредитной карты. Первые два поля являются символьными массивами, а третье имеет тип int ( будем считать, что это и есть номер кредитной карты ).
«Объявление класса содержит поля данных, необходимые для описания единого объекта.»
[Помни!]
В последней строке этого фрагмента объявляется переменная nds, которая имеет тип NameDataSet. Таким образом, nds представляет собой запись, описывающую отдельного человека.
Говорят, что nds является экземпляром класса NameDataSet и что мы создали этот экземпляр, реализовав класс NameDataSet. Поля firstName и остальные являются членами, или свойствами класса.
Обратиться к членам класса можно так:
NameDataSet nds ;
nds.creditCard = 10 ;
cin >> nds.firstName ;
сin >> nds.lastName ;
Здесь nds — экземпляр класса NameDataSet ( или отдельный объект типа NameDataSet ); целочисленная переменная nds.creditCard — свойство объекта nds; член nds.creditCard имеет тип int, тогда как другой член этого объекта, nds.firstName, имеет тип char[ ].
Если отбросить компьютерный сленг, приведённый пример можно объяснить так: в этом фрагменте программы происходит объявление объекта nds, который затем будет использован для описания покупателя. По каким-то соображениям программа присваивает этому человеку кредитный номер 10 ( понятно, что номер фиктивный — я ведь не собираюсь распространять номера своих кредитных карт! ).
Затем программа считывает имя и фамилию из стандартного ввода.
«Здесь я использую для хранения имени массив символов вместо типа string.»
[Помни!]
_________________
150 стр. Часть 3. Введение в классы
Теперь программа может работать с объектом nds как с единым целым, не обращаясь к его отдельным частям, пока в этом не возникает необходимость.
/* DataSet — хранение связанных данных в массиве объектов */
#include
#include
#include
#include
using namespace std ;
/* NameDataSet — класс для хранения имени и номера кредитной карты */
class NameDataSet
{
public :
char firstName[ 128 ] ;
char lastName [ 128 ] ;
int creditCard ;
} ;
/* Прототипы функций: */
bool getData( NameDataSet& nds ) ;
void displayData( NameDataSet& nds ) ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы если Вы не используете программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
/* Выделяем память для 25 экземпляров */
const int MAX = 25 ;
NameDataSet nds[ MAX ] ;
/* Загружаем имя, фамилию и номер социального страхования */
cout << "Считываем информацию о пользователе\n"
<< "Введите 'exit' для выхода из программы"
<< endl ;
int index = 0 ;
while ( getData( nds[ index ] ) && index < MAX)
{
index++ ;
}
/* Выводим считанные имя и номер */
cout << "\nЗаписи:" << endl ;
for ( int i = 0 ; i < index ; i++ )
{
displayData( nds[ i ] ) ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
/* getData — заполнение объекта информацией */
bool getData( NameDataSet& nds )
_________________
151 стр. Глава 12. Классы в С++
{
cout << "\nВведите имя:" ;
cin >> nds.firstName ;
/* Проверяем, не пора ли выйти из программы */
if ( stricmp( nds.firstName , "exit" ) == 0 )
{
return false ;
}
cout << "Введите фамилию:" ;
cin >> nds.lastName ;
cout << "Введите номер кредитной карты:" ;
cin >> nds.creditCard ;
return true ;
}
/* displayData — Вывод набора данных */
void displayData( NameDataSet& nds )
{
cout << nds.firstName
<< " "
<< nds.lastName
<< " /"
<< nds.creditCard
<< endl ;
}
В функции main( ) создаётся массив из 25 объектов класса NameDataSet, после чего программа приглашает пользователя ввести необходимую информацию. Затем в теле цикла while происходит вызов функции getData( ), которая ожидает ввода с клавиатуры содержимого элементов массива. Цикл прерывается, если getData( ) возвращает false или если количество заполненных объектов достигло максимального значения ( в данном случае — 25 ). После этого созданные объекты передаются функции displayData, которая выводит их на экран.
Функция getData( ) принимает аргумент типа NameDataSet, которому внутри функции присваивается имя nds. Пока что не обращайте внимания на символ "&" — о нём речь пойдёт в главе 14, "Указатели на объекты".
Внутри функции getData( ) происходит считывание строки из устройства стандартного ввода с последующей его записью в член firstName. Если stricmp( ) находит, что введённая строка — "exit", функция getData( ) возвращает false функции main( ), сигнализируя, что пора выходить из цикла ввода информации. ( Функция stricmp( ) сравнивает строки, не обращая внимания на регистр. Строки "EXIT", "exit" и другие считаются идентичными. ) Если введена строка, отличная от "exit", функция считывает из стандартного ввода фамилию и номер кредитной карты и записывает их в объект nds. Функция displayData( ) выводит на экран все члены объекта nds. Результат работы этой программы выглядит следующим образом.
Считываем информацию о пользователе
Введите 'exit' для выхода из программы
Введите имя: Stephen
Введите фамилию: Davis
Введите номер кредитной карты: 123456
_________________
152 стр. Часть 3. Введение в классы
Введите имя: Marshall
Введите фамилию: Smith
Введите номер кредитной карты: 567890
Введите имя: exit
Записи:
Stephen Davis/123456
Marshall Smith/567890
Для продолжения нажмите любую клавишу...
Вывод программы начинается с пояснения, как с ней работать. В первой строке я ввёл своё имя ( видите, какой я скромный! ). Поскольку меня не зовут exit, программа продолжает выполнение. Далее я ввёл свою фамилию и номер кредитной карты. Следующим элементом массива я ввёл имя Marshall Smith и номер его кредитной карты. Затем я ввёл строку exit и таким образом прервал цикл заполнения объектов. Как видите, эта программа не делает ничего, кроме вывода только что введённой информации.
_________________
153 стр. Глава 12. Классы в С++
В этой главе...
►Разрешение области видимости 161
►Определение функции-члена 162
►Определение функций-членов вне класса 164
►Перегрузка функций-членов 165
Программисты используют классы для объединения взаимосвязанных данных в один объект. Приведённый ниже класс Savings объединяет в себе баланс и уникальный номер счёта.
class Savings
{
public :
unsigned accountNumber ;
float balance ;
} ;
Каждый экземпляр класса Savings содержит одинаковые элементы:
void fn( void )
{
Savings a ;
Savings b ;
a.accountNumber = 1 ; /* этот счёт не тот же, что и... */
b.accountNumber = 2 ; /* ...этот */
}
Переменная а.accountNumber отличается от переменной b.accountNumber. Эти переменные различаются между собой так же, как баланс моего банковского счёта отличается от вашего ( хотя они оба называются балансами ).
Классы используются для моделирования реально существующих объектов. Чем ближе объекты С++ к реальному миру, тем проще с ними работать в программах. На словах это звучит довольно просто, однако существующий сейчас класс Savings не предпринимает ничего, чтобы хоть в чём-то походить на настоящий банковский счёт.
_________________
154 стр. Часть 3. Введение в классы
Реальные объекты имеют свойства-данные, например номера счетов и балансы. Но кроме этого, реальные объекты могут выполнять действия: микроволновые печи готовят, сберегательный счёт начисляет проценты, полицейский выписывает штраф и т.д.
Функционально ориентированные программы выполняют все необходимые действия с помощью функций. Программа на С++ может вызвать функцию strcmp( ) для сравнения двух строк или функцию getLine( ) для ввода строки. В главе 24, "Использование потоков ввода-вывода", будет показано, что даже операторы работы с потоками ввода-вывода ( cin >> и cout << ) являются не чем иным, как особым видом вызова функции.
Для выполнения действий классу Savings необходимы собственные активные свойства:
class Savings
{
public :
unsigned deposit( unsigned amount )
{
balance += amount ;
return balance ;
}
unsigned int accountNumber ;
float balance ;
} ;
В приведённом примере помимо номера и баланса счёта в класс Savings добавлена функция deposit( ). Теперь класс Savings может самостоятельно управлять своим состоянием. Так же, как класс MicrowaveOven ( микроволновая печь ) содержит функцию cook( ) ( готовить ), класс Savings содержит функцию deposit( ). Функции, определённые в классе, называются функциями-членами.
Почему мы должны возиться с функциями-членами? Что плохого в таком фрагменте:
class Savings
{
public :
unsigned accountNumber ;
float balance ;
} ;
unsigned deposit( Savings& s , unsigned amount )
{
s.balance += amount ;
return s.balance ;
}
Ещё раз напомню: пока что не обращайте внимания на символ "&" — его смысл станет понятен позже.
В этом фрагменте deposit( ) является функцией "вклада на счёт". Эта функция поддержки реализована в виде внешней функции, которая выполняет необходимые действия с экземпляром класса Savings. Конечно, такой подход имеет право на существование, но он нарушает наши правила объектно-ориентированного программирования.
Микроволновая печь имеет свои внутренние компоненты, которые "знают", как разморозить и приготовить продукты или сделать картошку хрустящей. Данные-члены класса схожи с элементами микроволновой печи, а функции-члены — с программами приготовления.
_________________
155 стр. Глава 13. Работа с классами
Когда я делаю закуску, я не должен начинать приготовление с подключения внутренних элементов микроволновой печи. И я хочу, чтобы мои классы работали так же, т.е. чтобы они без всякого внешнего вмешательства знали, как управлять своими "внутренними органами". Конечно, такие функции-члены класса Savings, как deposit( ), могут быть реализованы и в виде внешних функций. Можно даже расположить все функции, необходимые для работы со счетами, в одном месте файла. Микроволновую печь можно заставить работать, соединив необходимые провода внутри неё, но я не хочу, чтобы мои классы ( или моя микроволновая печь ) работали таким образом. Я хочу иметь класс Savings, который буду использовать в своей банковской программе, не задумываясь над тем, какова его рабочая "кухня".
Эта процедура включает два аспекта: создание функции-члена и её именование ( звучит довольно глупо, не правда ли? ).
Чтобы продемонстрировать работу с функциями-членами, начнём с определения класса Student следующим образом:
class Student
{
public :
/* Добавить пройденный курс к записи */
float addCourse( int hours , float grade )
{
/* Вычислить среднюю оценку с учётом времени различных курсов */
float weightedGPA ;
weightedGPA = semesterHours * gpa ;
/* Добавить новый курс */
semesterHours += hours ;
weightedGPA += grade * hours ;
gpa = weightedGPA / semesterHours ;
/* Вернуть новую оценку */
return gpa ;
}
int semesterHours ;
float gpa ;
} ;
Функция addCourse( int , float ) является функцией-членом класса Student. По сути, это такое же свойство класса Student, как и свойства semesterHours и gpa.
Для функций или переменных в программе, которые не являются членом какого-либо класса, нет специального названия, однако в этой книге я буду называть переменные или функции не членами класса, если они не были явно описаны в составе какого-либо класса.
«Функции-члены не обязаны предшествовать членам-данным в объявлении класса, как сделано в приведённом выше примере. Члены класса могут быть перечислены в любом порядке. Просто я предпочитаю первыми размещать функции.»
[Советы]
_________________
156 стр. Часть 3. Введение в классы
«По историческим причинам функции-члены называют также методами. Такое название имеет смысл в других объектно-ориентированных языках программирования, но бессмысленно в С++. Несмотря на это, термин приобрёл некоторую популярность и среди программистов на С++, наверное, поскольку его проще выговорить, чем выражение "функция-член" ( то, что это звучит гораздо внушительнее, никого не волнует ). Так что если во время вечеринки ваши друзья начнут сыпать словечками вроде "методы класса", просто мысленно замените "методы" выражением "функции-члены", и всё встанет на свои места. Поскольку термин "метод" смысла в С++ не имеет, я не буду использовать его в этой книге.»
Функция-член во многом похожа на члена семьи. Полное имя нашей функции addCourse( int , float ) пишется как Student::addCourse( int , float ), так же как моё полное имя — Стефан Дэвис. Краткое имя этой функции — addCourse( int , float ), а моё краткое имя — Стефан. Имя класса в начале полного имени означает, что эта функция является членом класса Student ( :: между именами функции и класса является просто символом-разделителем ). Фамилия Дэвис после моего имени означает, что я являюсь членом семьи Дэвисов.
«Существует и другое название полного имени — расширенное имя.»
[Советы]
Мы можем определить функцию addCourse( int , float ), которая не будет иметь ничего общего с классом Student ; точно так же, как могут существовать люди с именем Стефан, которые не имеют ничего общего с моей семьёй ( это можно понимать и дословно: я знаю несколько Стефанов, которые не хотят иметь ничего общего с моей семьёй ).
Вы можете создать функцию с полным именем Teacher::addCourse( int , float ) или даже с именем Golf::addCourse( int , float ). Имя addCourse( int , float ) без имени класса означает, что это обычная функция, которая не является членом какого-либо класса.
«Расширенное имя функции, не являющейся членом какого-либо класса, имеет вид ::addCourse( int , float ).»
[Советы]
Прежде чем вызывать функции-члены класса, вспомните, как мы обращались к данным-членам классов:
class Student
{
public :
int semesterHours ;
float gpa ;
} ;
Student s ;
void fn( void )
{
/* Обращение к данным-членам объекта s */
s.semesterHours = 10 ;
s.gpa = 3.0 ;
}
_________________
157 стр. Глава 13. Работа с классами
Обратите внимание, что наряду с именем переменной необходимо указать имя объекта. Другими словами, приведённый ниже фрагмент программы не имеет смысла.
Student s ;
void fn( void )
{
/* Этот пример ошибочен */
semesterHours = 10 ;
/* Член какого объекта и какого класса? */
Student::semesterHours = 10 ;
/* Теперь ясно, какого класса, однако до сих пор не ясно, какого объекта */
}
Формально между данными-членами и функциями-членами нет никакого различия. Следующая программа показывает, как можно использовать функцию-член addCourse( ).
/* CallMemberFunction — определение и вызов */
/* функции-члена класса */
#include
#include
#include
using namespace std ;
class Student
{
public :
/* Добавить пройденный курс к записи */
float addCourse( int hours , float grade )
{
/* Вычислить среднюю оценку с учётом времени различных курсов */
float weightedGPA ;
weightedGPA = semesterHours * gpa ;
/* Добавить новый курс */
semesterHours += hours ;
weightedGPA += grade * hours ;
gpa = weightedGPA / semesterHours ;
/* Вернуть новую оценку */
return gpa ;
}
int semesterHours ;
float gpa ;
} ;
int main( int nNumberofArgs , char* pszArgs[ ] )
_________________
158 стр. Часть 3. Введение в классы
{
Student s ;
s.semesterHours = 10 ;
s.gpa = 3.0 ;
/* Значения до вызова */
cout << "До: s = ( " << s.semesterHours
<< ", " << s.gpa
<< " )" << endl ;
s.addCourse( 3 , 4.0 ) ; /* Вызов функции-члена */
/* Изменённые значения */
cout << "После: s = ( " << s.semesterHours
<< ", " << s.gpa
<< " )" << endl ;
/* Обращение к другому объекту */
Student t ;
t.semesterHours = 6 ;
t.gpa = 1.0 ;
t.addCourse( 3 , 1.5 ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Как видите, синтаксис вызова функции-члена такой же, как и синтаксис обращения к переменной-члену класса. Часть выражения, которая находится справа от точки, не отличается от вызова обычной функции. Единственное отличие — присутствие слева от точки имени объекта, которому принадлежит функция.
Факт вызова этой функции можно определить так: "s является объектом, на который действует addCourse( ) " ; или, другими словами, объект s представляет собой студента, к записи которого добавляется новый курс. Вы не можете получить информацию о студенте или изменить её, не указав, о каком конкретно студенте идёт речь.
Вызов функции-члена без указания имени объекта имеет не больше смысла, чем обращение к данным-членам без указания объекта.
Я так и слышу, как вы повторяете про себя: "Нельзя обратиться к функции-члену без указания имени объекта! Нельзя обратиться к функции-члену без указания имени объекта! Нельзя..." Запомнив это, вы смотрите на тело функции-члена Student::addCourse( ) и... что это? Ведь addCourse( ) обращается к членам класса, не уточняя имени объекта!
Возникает вопрос: так всё-таки можно или нельзя обратиться к члену класса, не указывая его объекта? Уж поверьте мне, что нельзя. Просто когда вы обращаетесь к члену класса Student из addCourse( ), по умолчанию используется тот экземпляр класса, из которого вызвана функция addCourse( ). Вы ничего не поняли? Вернёмся к примеру.
int main( int nNumberofArgs , char* pszArgs[ ] )
{
Student s ;
s.semesterHours = 10 ;
s.gpa = 3.0 ;
s.addCourse( 3 , 4.0 ) ; /* Вызов функции-члена */
Student t ;
t.semesterHours = 6;
t.gpa = 1.0 ;
t.addCourse( 3 , 1.5 ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
159 стр. Глава 13. Работа с классами
Когда addCourse( ) вызывается для объекта s, все сокращённые имена в теле этой функции считаются членами объекта s. Таким образом, обращение к переменной semesterHours внутри функции s.addCourse( ) в действительности является обращением к переменной s.semesterHours, а обращение к gpa — обращением к s.gpa. В следующей строке функции main( ), когда addCourse( ) вызывается для объекта t того же класса Student, происходит обращение к членам класса t.semesterHours и t.gpa.
«Объект, для которого вызывается функция-член, называется "текущим", и все имена членов, записанные в сокращённом виде внутри функции-члена, считаются членами текущего объекта. Другими словами, сокращённое обращение к членам класса интерпретируется как обращение к членам текущего объекта.»
[Помни!]
«Как функция-член определяет, какой объект является текущим? Это не магия и не шаманство — просто адрес этого объекта всегда передаётся функции-члену как скрытый первый аргумент. Другими словами, при вызове функции-члена происходит преобразование такого вида:
s.addCourse( 3 , 2.5 ) равносильно Student::addCourse( &s , 3 , 2.5 )
( команда, приведённая в правой части выражения, синтаксически неверна; она просто показывает, как компилятор видит выражение в левой части во внутреннем представлении ).»
Внутри функции, когда нужно узнать, какой именно объект является текущим, используется этот указатель. Тип текущего объекта — указатель на объект соответствующего класса. Всякий раз, когда функция-член обращается к другому члену класса, не называя имени его объекта явно, компилятор считает, что данный член является членом этого ( this ) объекта. При желании вы можете явно обращаться к членам этого объекта, используя ключевое слово this. Так что функцию Student::addCourse( ) можно переписать следующим образом:
float Student::addCourse( int hours , float grade )
{
float weightedGPA ;
weightedGPA = this -> semesterHours * this -> gpa ;
/* добавим новый курс */
this -> semesterHours += hours ;
weightedGPA += hours * grade ;
this -> gpa = weightedGPA / this -> semesterHours ;
return this -> gpa ;
}
Независимо от того, добавите ли вы оператор this -> в тело функции явно или нет, результат будет одинаков.
_________________
160 стр. Часть 3. Введение в классы
Символ :: между именем класса и именем его члена называют оператором разрешения области видимости, поскольку он указывает, какой области видимости принадлежит член класса. Имя класса перед двоеточиями похоже на фамилию, тогда как название функции после двоеточия схоже с именем — такой порядок записи принят на востоке.
С помощью оператора :: можно также описать функцию — не член, использовав для этого пустое имя класса. В этом случае функция addCourse( ) должна быть описана как ::addCourse( int , float ).
Обычно оператор :: не обязателен, однако в некоторых ситуациях это не так. Рассмотрим следующий фрагмент кода:
/* addCourse — перемножает количество часов и оценку */
float addCourse( int hours , float grade )
{
return hours * grade ;
}
class Student
{
public :
int semesterHours ;
float gpa ;
/* Добавить пройденный курс к записи */
float addCourse( int hours , float grade )
{
/* Вызвать внешнюю функцию */
weightedGPA = addCourse( semesterHours , gpa ) ;
/* Вызвать ту же функцию для подсчёта оценки с учётом нового курса */
weightedGPA += addCourse( hours , grade ) ;
gpa = weightedGPA / semesterHours ;
/* Вернуть новую оценку */
return gpa ;
}
} ;
В этом фрагменте я хотел, чтобы функция-член Student::addCourse( ) вызывала функцию — не член ::addCourse( ). Без оператора :: вызов функции addCourse( ) внутри класса Student приведёт к вызову функции Student::addCourse( ).
«Функция-член может использовать для обращения к другому члену класса сокращённое имя, подразумевающее использование имени текущего экземпляра класса.»
[Помни!]
В данном случае вызов функции без указания имени класса приводит к тому, что она вызывает саму себя. Добавление оператора :: в начале имени заставляет осуществить вызов глобальной версии этой функции ( что нам и нужно ):
/* addCourse — перемножает количество часов и оценку */
float addCourse( int hours , float grade )
{
return hours*grade ;
}
_________________
161 стр. Глава 13. Работа с классами
class Student
{
public :
int semesterHours ;
float gpa ;
/* Добавить пройденный курс к записи */
float addCourse( int hours , float grade )
{
/* Вызвать внешнюю функцию */
weightedGPA = ::addCourse( semesterHours , gpa ) ;
/* Вызвать ту же функцию для подсчёта оценки с учётом нового курса */
weightedGPA += ::addCourse( hours , grade ) ;
gpa = weightedGPA / semesterHours ;
/* Вернуть новую оценку */
return gpa ;
}
} ;
Это похоже на то, как если бы я звал Стефана в собственном доме. Все решили бы, что я зову самого себя: ведь в моём доме, естественно, подразумевается фамилия Дэвис. Если же я имею в виду какого-то другого Стефана, то должен сказать "Стефан Спупендайк" или "Стефан Мак-Суини" либо использовать какую-нибудь другую фамилию. Так же действует и оператор разрешения области видимости.
«Расширенное имя функции включает в себя её аргументы. Теперь же мы добавляем к полному имени ещё и имя класса, к которому принадлежит функция.»
[Помни!]
Функция-член может быть определена как внутри класса, так и отдельно от него. Когда функция определяется внутри класса, это выглядит так, как в приведённом далее файле Savings.h:
/* Savings — определение класса с возможностью делать вклады */
class Savings
{
public :
/* Объявляем и определяем функции-члены */
float deposit( float amount )
{
balance += amount ;
return balance ;
}
unsigned int accountNumber ;
float balance ;
} ;
Использование такого заголовочного файла проще простого — его надо включить в программу и пользоваться определённым в нём классом, как вам заблагорассудится, например, как в приведённой далее программе SavingsClassInline.
_________________
162 стр. Часть 3. Введение в классы
/* SavingsClassInline — вызов фукции-члена, объявленной и определённой в классе Savings */
#include
#include
#include
using namespace std ;
#include " Savings.h "
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
Savings s ;
s.accountNumber = 123456 ;
s.balance = 0.0 ;
/* Добавляем немного на счёт... */
cout << "Вкладываем на счёт 10 монет"
<< s.accountNumber << endl ;
s.deposit( 10 ) ;
cout << "Состояние счёта "
<< s.balance << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Так использовать класс Savings может теперь любой программист, которому доступен соответствующий заголовочный файл, причём ему совершенно не надо вдаваться в детали реализации этого класса.
«Директива #include заставляет препроцессор перед началом компиляции вставить вместо неё содержимое указанного в ней файла.»
[Помни!]
«Встраиваемые функции-члены
Функция-член, определённая непосредственно внутри класса, по умолчанию считается встраиваемой ( подставляемой, inline ) функцией ( если только не оговорено обратное, например с помощью опций командной строки компилятора ). Функции-члены, определённые в классе, по умолчанию считаются inline-функциями, потому что большинство функций-членов, определённых внутри класса, довольно малы, а такие маленькие функции являются главными кандидатами на подстановку. Тело встраиваемой функции подставляется компилятором непосредственно вместо оператора её вызова. Такая функция выполняется быстрее, поскольку от процессора не требуется осуществлять переход к телу функции. Однако при этом программы, использующие встроенные функции, занимают больше места, поскольку копии таких встраиваемых функций определяются один-единственный раз, а подставляются вместо каждого вызова.
Есть ещё одна техническая причина, по которой функции-члены класса лучше делать встраиваемыми. Как вы помните, все структуры языка С обычно определяются в составе включаемых файлов с последующим использованием в исходных .срр-файлах при необходимости. Такие включаемые файлы не должны содержать данных или тел функций, поскольку могут быть скомпилированы несколько раз. Использование же подставляемых функций во включаемых файлах вполне допустимо, поскольку их тела, как и макросы, подставляются вместо вызова в исходном файле. То же относится и к классам С++. Подразумевая, что функции-члены, определённые в описании классов, встраиваемые, мы избегаем упомянутой проблемы многократной компиляции.»
_________________
163 стр. Глава 13. Работа с классами
Для больших функций встраивание тела функции непосредственно в определение класса может привести к созданию очень больших и неудобочитаемых определений классов. Чтобы избежать этого, С++ предоставляет возможность определять тела функций-членов вне класса.
В этом случае в заголовочном файле имеется только объявление, но не определение функции.
/* Savings — определение класса с */
/* возможностью делать вклады */
class Savings
{
public :
/* Объявляем, но не определяем функции-члены */
float deposit( float amount ) ;
unsigned int accountNumber ;
float balance ;
} ;
Теперь объявление класса содержит только прототип функции deposit( ). При этом само тело функции находится в другом месте. Для простоты я определил функцию в том же файле, где находится и функция main( ).
«Так можно делать, но подобное расположение функции не очень распространено. Обычно класс определяется в заголовочном файле, а тело функции находится в отдельном исходном файле. Сама же использующая этот класс программа располагается в файле, отличном от этих двух ( подробнее об этом будет рассказано в главе 22, "Разложение классов" ).»
[Советы]
/* SavingsClassOutline — вызов фукции-члена, */
/* объявленной в классе Savings ( заголовочном файле ), но определённой */
/* в программе SavingsClassOutline или */
/* тело функции находится в отдельном исходном файле */
#include
#include
#include
using namespace std ;
#include " Savings.h "
/* Определение функции-члена Savings::deposit( ) ( обычно содержится в отдельном файле ) */
float Savings::deposit( float amount )
{
balance += amount ;
return balance ;
}
/* Основная программа */
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale (LC_ALL,".1251");
Savings s ;
s.accountNumber = 123456 ;
s.balance = 0.0 ;
_________________
164 стр. Часть 3. Введение в классы
/* Добавляем немного на счёт... */
cout << "Вкладываем на счёт 10 монет"
<< s.accountNumber << endl ;
s.deposit( 10 ) ;
cout << "Состояние счёта "
<< s.balance << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Определение класса содержит только прототип функции deposit( ), а её тело определено в другом месте. Такое объявление аналогично объявлению любого другого прототипа.
Обратите внимание, что при определении функции-члена deposit( ) потребовалось указание её полного имени
float Savings::deposit( float amount )
; сокращённого имени при определении вне класса недостаточно.
Функции-члены могут перегружаться так же, как и обычные функции ( обратитесь к главе 6, "Создание функций", если забыли, что это значит ). Как вы помните, имя класса является частью полного имени, и все приведённые ниже функции вполне корректны.
class Student
{
public :
/* grade — возвращает текущую среднюю оценку */
float grade( ) ;
/* grade — устанавливает новое значение оценки и возвращает предыдущее */
float grade( float newGPA )
/* ...прочие члены-данные... */
} ;
class Slope
{
public :
/* grade — возвращает снижение оценки */
float grade( ) ;
/* ...прочие члены-данные... */
} ;
/* grade — возвращает символьный эквивалент оценки */
char grade( float value ) ;
int main( int argcs , char* pArgs[ ] )
{
Student s ;
s.grade( 3.5 ) ; /* Student::grade( float ) */
float v = s.grade( ) ; /* Student::grade( ) */
char с = grade( v ) ; /* ::grade( float ) */
Slope o ;
float m = о.grade( ) ; /* Slope::grade( ) */
return 0 ;
}
_________________
165 стр. Глава 13. Работа с классами
Полные имена вызываемых из main( ) функций указаны в комментариях.
Когда происходит вызов перегруженной функции, составляющими её полного имени считаются не только аргументы функции, но и тип объекта, который вызывает функцию ( если она вызывается объектом ). Такой подход позволяет устранить неоднозначность при вызове функции.
В приведённом примере первые два вызова обращаются к функциям-членам Student::grade( float ) и Student::grade( ) соответственно. Эти функции отличаются списками аргументов. Вызов функции s.grade( ) обращается к Student::grade( ), поскольку тип объекта s — Student.
Третья вызываемая функция в данном примере — функция ::grade( float ), не имеющая вызывающего объекта. Последний вызов осуществляется объектом типа Slope, и соответственно вызывается функция-член Slope::grade( float ).
_________________
166 стр. Часть 3. Введение в классы
В этой главе...
►Определение массивов и указателей 167
►Объявление массивов объектов 168
►Объявление указателей на объекты 169
►Передача объектов функциям 171
►Зачем использовать указатели и ссылки 174
►Возврат к куче 175
►Сравнение указателей и ссылок 175
►Почему ссылки не используются вместо указателей 175
►Использование связанных списков 176
►Списки в стандартной библиотеке 180
Программисты на С++ всё время создают массивы чего-либо. Формируются массивы целочисленных значений, массивы действительных значений; так почему бы не создать массив студентов? Студенты всё время находятся в списках ( причём гораздо чаще, чем им хотелось бы ). Концепция объектов Student, стройными рядами ожидающих своей очереди, слишком привлекательна, чтобы можно было пройти мимо неё.
Массив является последовательностью идентичных объектов и очень похож на улицу с одинаковыми домами. Каждый элемент массива имеет индекс, который соответствует порядковому номеру элемента от начала массива. При этом первый элемент имеет нулевое смещение от начала массива, а значит, имеет индекс 0.
Массивы в С++ объявляются с помощью квадратных скобок, в которых указывается количество элементов в массиве.
int array[ 10 ] ; /* Объявление массива из 10 элементов */
К отдельному элементу массива можно обратиться, подсчитав смещение от начала массива:
array[ 0 ] = 10 ; /* Присвоить 10 первому элементу */
array[ 9 ] = 20 ; /* Присвоить 20 последнему элементу */
В этом фрагменте первому элементу массива ( элементу под номером 0 ) присваивается значение 10 , а последнему — 20.
«Не забывайте, что в С++ массив начинается элементом с индексом 0 и заканчивается элементом, имеющим индекс, равный длине массива минус 1.»
[Помни!]
_________________
167 стр. Глава 14. Указатели на объекты
Если продолжить аналогию с домами, получится, что имя массива — это название улицы, а номер дома равнозначен номеру элемента в массиве. Таким же образом можно отождествить переменные с их адресом в памяти компьютера. Эти адреса могут быть определены и сохранены для последующего использования.
/* Объявление целочисленной переменной */
int variable ;
/* Сохранить её адрес в pVariable */
int* pVariable = &variable
/* Присвоить 10 целочисленной переменной, на которую указывает pVariable */
*pVariable = 10 ;
Указатель pVariable был объявлен для того, чтобы хранить в нём адрес переменной variable. После этого целочисленной переменной, находящейся по адресу pVariable, присваивается значение 10.
Использовав аналогию с домами в последний раз ( честное слово, в последний! ), мы получим:
■■■
■ variable — это дом;
■ pVariable — это листок с адресом дома;
■ в последней строке примера отправляется сообщение, содержащее 10 , по адресу, который находится на листке бумаги. Всё почти так же, как на почте ( единственное отличие состоит в том, что компьютер не ошибается адресом ).
■■■
В главе 7, "Хранение последовательностей в массивах", описаны основы работы с массивами простых ( встроенных ) типов, а в главах 8, "Первое знакомство с указателями в С++", и 9, "Второе знакомство с указателями", подробно рассматриваются указатели.
Массивы объектов работают так же, как и массивы простых переменных. В качестве примера можно использовать следующий фрагмент:
/* ArrayOfStudents — определение массива */
/* объектов Student и обращение */
/* к его элементам */
#include
#include
#include
using namespace std ;
class Student
{
public :
int semesterHours ;
float gpa ;
float addCourse( int hours , float grade ){ return 0.0 ; }
} ;
void someFn( )
{
/* Объявляем массив из 10 студентов */
Student s[ 10 ] ;
/* Пятый студент получает 5.0 ( повезло! ) */
s[ 4 ].gpa = 5.0 ;
/* Добавим ещё один курс пятому студенту, который на этот раз провалился... */
s[ 4 ].addCourse( 3 , 0.0 ) ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
system( "PAUSE" ) ;
return 0 ;
}
_________________
168 стр. Часть 3. Введение в классы
В данном фрагменте s является массивом объектов типа Student. Запись s[ 4 ] означает пятый элемент массива, а значит, s[ 4 ].gpa является усреднённой оценкой пятого студента. В следующей строке с помощью функции s[ 4 ].addCourse( ) пятому студенту добавляется ещё один прослушанный и несданный курс.
Указатели на объекты работают так же, как и указатели на простые типы.
/* ObjPtr — Определение и использование */
/* указателя на объект Student */
#include
#include
#include
using namespace std ;
class Student
{
public :
int semesterHours ;
float gpa ;
float addCourse( int hours , float grade ) { return 0.0 ; } ;
} ;
int main( int argc , char* pArgs[ ] )
{
/* Создание объекта Student */
Student s ;
s.gpa = 3.0 ;
/* Создание указателя на объект Student */
Student* pS ;
/* Заставляем указатель указывать на наш объект */
pS = &s ;
cout << "s.gpa = " << s.gpa << "\n"
<< "pS -> gpa = " << pS -> gpa << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
В программе объявляется переменная s типа Student, после чего создаётся переменная pS, которая является "указателем на объект типа Student" ; другими словами, указателем Student*. Программа инициализирует значение одного из членов-данных s, и присваивает адрес s переменной pS. Затем программа обращается к объекту s — один раз по имени, а затем с использованием указателя на объект. Странную запись pS -> gpa я объясню немного позже в этой главе.
По аналогии с указателями на простые переменные можно решить, что в приведённом ниже примере происходит обращение к усреднённой оценке студента s.
_________________
169 стр. Глава 14. Указатели на объекты
int main( int argc , char* pArgs[ ] )
{
/* Этот пример некорректен */
Student s ;
Student* pS= &s ; /* Создаём указатель на объект s */
/* Обращаемся к члену gpa объекта, на который указывает pS ( этот фрагмент неверен ) */
*pS.gpa = 3.5 ;
return 0 ;
}
Как верно сказано в комментарии, этот код работать не будет. Проблема в том, что оператор "." будет выполнен раньше оператора "*".
Для изменения порядка выполнения операторов в С++ используют скобки. Так, в приведённом ниже примере компилятор сначала выполнит сложение, а затем умножение.
int i = 2 * ( 1 + 3 ) ; /* сложение выполняется до умножения */
В применении к указателям скобки выполняют те же функции.
int main( int argc , char* pArgs[ ] )
{
Student s ;
Student* pS = &s ; /* Создаём указатель на объект s */
/* Обращаемся к члену gpa того объекта, на который указывает pS ( теперь всё работает правильно ) */
( *pS ).gpa = 3.5 ;
return 0 ;
}
Теперь *pS вычисляет объект, на который указывает pS, а следовательно, .gpa обращается к члену этого объекта.
Использование для разыменования указателей на объекты оператора * со скобками будет прекрасно работать. Однако даже самые твёрдолобые программисты скажут вам, что такой синтаксис разыменования очень неудобен.
Для доступа к членам объекта С++ предоставляет более удобный оператор -> , позволяющий избежать неуклюжей конструкции со скобками и оператором *; таким образом, pS -> gpa эквивалентно ( *pS ).gpa. В результате получаем следующий преобразованный код рассмотренной ранее программы.
int main( int argc , char* pArgs[ ] )
{
Student s ;
Student* pS = &s ; /* Создаём указатель на объект s */
/* Обращаемся к члену gpa того объекта, на который указывает pS ( теперь всё работает правильно ) */
pS -> gpa = 3.5 ;
return 0 ;
}
Этот оператор используется гораздо чаще, поскольку его легче читать ( хотя обе формы записи совершенно тождественны ).
_________________
170 стр. Часть 3. Введение в классы
Передача указателей функциям — один из способов выразить себя в области указателей.
Как вы знаете, С++ передаёт аргументы в функцию по ссылке при использовании в описании символа & ( см. главу 8, "Первое знакомство с указателями в С++" ). Однако по умолчанию С++ передаёт функции только значения аргументов. ( Обратитесь к главе 6, "Создание функций", если вы этого не знали. ) То же касается и составных, определённых пользователем объектов: они также передаются по значению.
/* PassObjVal — попытка изменить значение объекта в функции оказывается неуспешной при передаче объекта по значению */
#include
#include
#include
using namespace std ;
class Student
{
public :
int semesterHours ;
float gpa ;
} ;
void someFn( Student copyS )
{
copyS.semesterHours = 10 ;
copyS.gpa = 3.0 ;
cout << "Значение copyS.gpa = "
<< copyS.gpa << "\n" ;
}
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student s ;
s.gpa = 0.0 ;
/* Вывод значения s.gpa до вызова someFn( ) */
cout << "Значение s.gpa = " << s.gpa << "\n" ;
/* Передача существующего объекта */
cout << "Вызов someFn( Student )\n" ;
someFn( s ) ;
cout << "Возврат из someFn( Student )\n" ;
/* Значение s.gpa остаётся равным 0 */
cout << "Значение s.gpa = " << s.gpa << "\n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
171 стр. Глава 14. Указатели на объекты
В этом примере функция main( ) создаёт объект s, а затем передаёт его в функцию someFn( ).
«Осуществляется передача по значению не самого объекта, а его копии.»
[Помни!]
Объект copyS начинает своё существование внутри функции someFn( ) и является точной копией объекта s из main( ). При этом любые изменения содержимого объекта copyS никак не отражаются на объекте s из функции main( ). Вот что даёт программа на выходе.
Значение s.gpa = 0
Вызов someFn( Student )
Значение copyS.gpa = 3
Возврат из someFn( Student )
Значение s.gpa = 0
Press any key to continue...
Вместо того чтобы передавать объект по значению, можно передавать в функцию указатель на объект.
/* PassObjPtr — изменение значения объекта в функции при передаче указателя на объект */
#include
#include
#include
using namespace std ;
class Student
{
public :
int semesterHours ;
float gpa ;
} ;
void someFn( Student* pS )
{
pS -> semesterHours = 10 ;
pS -> gpa = 3.0 ;
cout << "Значение pS -> gpa = "
<< pS -> gpa << "\n" ;
}
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student s ;
s.gpa = 0.0 ;
/* Вывод значения s.gpa до вызова someFn( ) */
cout << "Значение s.gpa = " << s.gpa << "\n" ;
/* Передача существующего объекта */
cout << "Вызов someFn( Student* )\n" ;
someFn( &s ) ;
cout << "Возврат из someFn( Student* )\n" ;
/* Значение s.gpa теперь равно 3.0 */
cout << "Значение s.gpa = " << s.gpa << "\n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
172 стр. Часть 3. Введение в классы
В этом примере аргумент, передаваемый в someFn( ), имеет тип указателя на объект Student, что записывается как Student* ( это отражает способ вызова программой функции someFn( ) ). Теперь вместо значения объекта s в функцию someFn( ) передаётся указатель на объект s. При этом соответственно изменяется и способ обращения к аргументам функции внутри её тела: теперь для разыменования указателя pS используются операторы-стрелки.
На этот раз вывод программы имеет следующий вид.
Значение s.gpa = 0
Вызов someFn( Student* )
Значение pS -> gpa = 3
Возврат из someFn( Student* )
Значение s.gpa = 3
Press any key to continue...
Оператор ссылки описан в главе 9, "Второе знакомство с указателями", и может применяться для пользовательских объектов так же, как и для всех остальных.
/* PassObjRef — изменение значения объекта в функции при передаче с использованием ссылки */
#include
#include
#include
using namespace std ;
class Student
{
public :
int semesterHours ;
float gpa ;
} ;
void someFn( Student& refS )
{
refS.semesterHours = 10 ;
refS.gpa = 3.0 ;
cout << "Значение refS.gpa = "
<< refS.gpa << "\n" ;
}
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student s ;
s.gpa = 0.0 ;
_________________
173 стр. Глава 14. Указатели на объекты
/* Вывод значения s.gpa до вызова someFn( ) */
cout << "Значение s.gpa = " << s.gpa << "\n" ;
/* Передача существующего объекта */
cout << "Вызов someFn( Student& )\n" ;
someFn( s ) ;
cout << "Возврат из someFn ( Student& )\n" ;
/* Значение s.gpa теперь равно 3.0 */
cout << "Значение s.gpa = " << s.gpa << "\n" ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Значение s.gpa = 0
Вызов someFn( Student& )
Значение refS.gpa = 3
Возврат из someFn( Student& )
Значение s.gpa = 3
Press any key to continue...
В этой программе в функцию someFn( ) передаётся не копия объекта, а ссылка на него. Изменения, внесённые функцией someFn( ) в s, сохраняются внутри main( ).
«Передача объекта по ссылке — всего лишь другой способ передачи в функцию адреса объекта s. С++ самостоятельно отслеживает адрес ссылки, в то время как при передаче указателя вы должны заниматься этим сами.»
[Советы]
Итак, передать объект в функцию можно разными способами. Но почему бы нам не ограничиться одним, простейшим способом — передачей по значению?
Один ответ мы уже получили, когда изучали способы передачи в этой главе, — при передаче по значению вы не можете изменить исходный объект, поскольку в функции работаете с копией объекта.
А вот и вторая причина — некоторые объекты могут оказаться действительно очень большими. Передача такого объекта по значению приводит к копированию большого объёма информации в память функции.
«Область, используемая для передачи аргументов функции, называется стеком вызова.»
При вызове из функции другой функции объект копируется вновь, и в результате нескольких вложенных вызовов вы получите десяток объектов в памяти и программу, работающую медленнее загрузки Windows.
«Проблема на самом деле ещё сложнее, чем описано здесь. В главе 18, "Копирующий конструктор", вы убедитесь, что создание копии объекта представляет собой существенно более сложную задачу, чем простое копирование участка памяти из одного места в другое.»
[Атас!]
_________________
174 стр. Часть 3. Введение в классы
Проблемы, возникающие при работе с указателями на простые переменные, распространяются и на указатели на объекты. В частности, необходимо гарантировать, что указатель ссылается на существующий корректный объект. Так, нельзя возвращать указатель на локально определённый объект, как это сделано в данном примере:
MyClass* myFunc( )
{
/* Эта функция не будет работать правильно */
MyClass mc ;
MyClass* рМС = &mc ;
return рМС ;
}
После возврата из myFunc( ) объект mc выходит из области видимости, а значит, указатель, который возвращает myFunc( ), указывает на несуществующий объект.
«Проблемы, связанные с возвратом памяти, которая выходит из области видимости, рассматривались в главе 9, "Второе знакомство с указателями".»
[Помни!]
Использование кучи позволяет решить эту проблему:
MyClass* myFunc( )
{
MyClass* рМС = new MyClass ;
return рМС ;
}
«С помощью кучи можно выделять память для объектов в самых разнообразных ситуациях.»
[Помни!]
Очень часто новички в программировании спрашивают, зачем нужны и указатели, и ссылки, и нельзя ли обойтись чем-то одним?
«В принципе, можно обойтись чем-то одним. Тот же С#, да и многие другие языки обходятся без указателей. Однако С++ — язык крайне широкого применения, и имеется множество задач, решение которых существенно упрощается при использовании указателей. Указатели — неотъемлемая часть стандартного, не ограниченного узкими рамками Visual Studio .NET языка программирования С++.»
[Советы]
Синтаксис работы со ссылками аналогичен синтаксису, используемому при работе с обычными объектами. Так почему бы не перейти на использование только ссылок и никогда не использовать указатели?
_________________
175 стр. Глава 14. Указатели на объекты
Объекты и их адреса — это "две большие разницы", и зачастую синтаксис для ссылок оказывается более сложным, чем синтаксис при работе с указателями. Рассмотрим следующий пример.
class Student
{
public :
int semesterHours ;
float gpa ;
Student valFriend ;
Student& refFriend ;
Student* ptrFriend ;
} ;
int main( int nNumberOfArgs , char* pszArgs[ ] )
{
/* Ссылка на объект в куче */
Student& student = *new Student ;
student.gpa = 10 ;
// To же
Student& studentFriend = *new Student ;
studentFriend.gpa = 20 ;
/* Копирование значения одного объекта типа Student в другой */
student.valFriend = studentFriend ;
/* Этот код не будет работать */
Student& refFriend ;
refFriend = studentFriend ;
/* Этот код корректен */
student.ptrFriend = &studentFriend ;
return 0 ;
}
Как видите, я модифицировал класс Student так, чтобы он мог указать своего лучшего друга[ 14 ]. Для этого я пытаюсь воспользоваться ссылочной переменной. В функции main( ) я создаю двух студентов и пытаюсь сделать одного из них другом другого.
Первое присвоение копирует объект в тело другого объекта, так что принимающий объект просто содержит копию. Второе присвоение не будет работать, так как С++ не в состоянии отличить присвоение ссылке от присвоения самому объекту, так что корректно работать будет только третье присвоение, приводя к желаемому результату.
Связанный список является второй по распространённости структурой после массива. Каждый объект в связанном списке указывает на следующий, образуя цепочку в памяти.
___________
14Это сделано некорректно; как минимум член valFriend не может быть определён в классе того же типа, не считая массы других ошибок. Поэтому к данному примеру следует относиться как к не более чем поясняющей сугубо теоретической модели, которая никогда не будет даже скомпилирована. — Прим. ред.
_________________
176 стр. Часть 3. Введение в классы
К связанному списку легко добавить ещё один элемент — путём изменения указателя в последнем объекте списка. В этом заключается основное преимущество связанного списка — отсутствие необходимости задавать фиксированный размер на этапе компиляции: связанный список может уменьшаться и увеличиваться в зависимости от потребностей программы. Цена этой гибкости — скорость работы со списком, поскольку обратиться к какому-то из элементов списка можно, только пройдя по всем предыдущим.
Не всякий класс может быть использован для создания связанного списка. Связываемый класс объявляется так, как показано в приведённом ниже фрагменте.
class LinkableClass
{
public :
LinkableClass* pNext ;
/* Прочие члены класса */
} ;
Ключевым в этом классе является указатель на объект класса LinkableClass. На первый взгляд несколько необычно выглядит то, что класс содержит указатель сам на себя. В действительности в этом объявлении подразумевается, что каждый объект класса содержит указатель на другой объект этого же класса.
Указатель pNext и есть тот элемент, с помощью которого дети объединяются в цепочки. Фигурально выражаясь, можно сказать, что список детей состоит из некоторого количества объектов, каждый из которых имеет тип "ребёнок". Каждый ребёнок указывает на следующего ребенка.
Головной указатель является указателем типа LinkableClass*, и если использовать аналогию с цепочкой детей, держащихся за руки, то можно сказать, что учитель указывает на объект класса "ребёнок" ( любопытно отметить, что сам учитель не является ребёнком — головной указатель не обязательно должен иметь тип LinkableClass ).
«Не забывайте инициализировать указатели значением 0. Указатель, содержащий нуль, так и называется — нулевым. Обычно попытка обращения по адресу 0 вызывает аварийную остановку программы.»
[Атас!]
«Преобразование целочисленного нуля в тип LinkableClass* не обязательно. С++ воспринимает 0 как значение любого типа ( в частности, как "универсальный указатель" ).»
[Советы]
Чтобы увидеть, как связанные списки работают на практике, рассмотрим следующую функцию, которая добавляет переданный ей аргумент в начало списка.
void addHead( LinkableClass* pLC )
{
pLC -> pNext = pHead
pHead = pLC ;
}
Здесь после выполнения первой строки поле pNext указывает на первый член списка, а после второй строки заголовок списка указывает на добавленный элемент, что делает его первым элементом списка.
Добавление объекта в начало списка — самая простая операция со связанным списком. Хорошее представление о работе связанного списка даёт процедура прохода по нему до конца списка.
_________________
177 стр. Глава 14. Указатели на объекты
/* Проход по связанному списку */
LinkableClass* pL = pHead ;
while ( pL )
{
/* Выполнение некоторых операций */
/* Переход к следующему элементу */
pL = pL -> pNext ;
}
Сначала указатель pL инициализируется адресом первого объекта в списке ( который хранится в переменной pHead ). Затем программа входит в цикл while. Если указатель pL не нулевой, он указывает на некоторый объект LinkableClass. В этом цикле программа может выполнить те или иные действия над объектом, после чего присвоение pL = pL -> pNext "перемещает" указатель к следующему объекту списка. Если указатель становится нулевым, список исчерпан.
Программа LinkedListData использует связанный список для хранения списка объектов, содержащих имена людей. Программу очень легко расширить, добавив, например, номера социального страхования или вес. Просто я старался сделать программу максимально простой.
/* LinkedListData — хранение данных в связанном списке */
#include
#include
#include
#include
using namespace std ;
/* NameDataSet — хранит имя человека ( этот объект можно легко расширить для хранения другой информации ). */
class NameDataSet
{
public :
char szName[ 128 ] ;
/* Указатель на следующую запись в списке */
NameDataSet* pNext ;
} ;
/* Указатель на первую запись списка */
NameDataSet* pHead = 0 ;
/* Добавление нового члена в список */
void add( NameDataSet* pNDS )
{
pNDS -> pNext = pHead ;
/* Заголовок указывает на новую запись */
pHead = pNDS ;
}
/* getData — чтение имени */
NameDataSet* getData( )
{
_________________
178 стр. Часть 3. Введение в классы
// Читаем имя
char nameBuffer [ 128 ] ;
cout << "\nВведите имя:" ;
cin >> nameBuffer ;
/* Если это имя — 'exit'... */
if ( ( stricmp( nameBuffer , "exit" ) == 0 ) )
{
/* ...вернуть нулевое значение */
return 0 ;
}
/* Новая запись для заполнения */
NameDataSet* pNDS = new NameDataSet ;
/* Заполнение поля имени и обнуление указателя */
strncpy( pNDS -> szName , nameBuffer , 128 ) ;
pNDS -> szName[ 127 ] = '\0' ;
pNDS -> pNext = 0 ;
/* Возврат адреса созданного объекта */
return pNDS ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
cout << "Читаем имена студентов\n"
<< "Введите 'exit' для выхода\n" ;
/* Создание объекта NameDataSet */
NameDataSet* pNDS ;
while ( pNDS = getData( ) )
{
/* Добавление в конец списка */
add( pNDS ) ;
}
/* Итерация списка для вывода записей */
cout << "Записи:\n" ;
pNDS = pHead ;
while ( pNDS )
{
/* Вывод текущей записи */
cout << pNDS -> szName << "\n" ;
/* Получение следующей записи */
pNDS = pNDS -> pNext ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Несмотря на внушительную длину, программа LinkedListData относительно проста. Функция main( ) начинается с вызова функции getData( ), которая считывает элемент NameDataSet с клавиатуры. Если пользователь вводит строку "exit", getData( ) возвращает нуль. Функция main( ) вызывает функцию add( ), чтобы добавить элемент, который вернула getData( ), в конец связанного списка.
_________________
179 стр. Глава 14. Указатели на объекты
Если от пользователя больше не поступает элементов NameDataSet, функция main( ) выводит на экран все элементы списка, используя функцию displayData( ).
Функция getData( ) выделяет из кучи пустой объект класса NameDataSet. После этого getData( ) ожидает ввода имени для записи его в соответствующее поле нового объекта. Если пользователь вводит в поле имени строку "exit", функция уничтожает последний созданный объект и возвращает 0. В противном случае getData( ) считывает фамилию и номер социального страхования, после чего обнуляет указатель pNext и передаёт управление вызывающей функции.
«Никогда не оставляйте связывающие указатели не проинициализированными! Старая поговорка программистов гласит: "Не уверен — обнули".»
[Помни!]
Функция getData( ) возвращает адрес объекта.
Каждый объект, который возвращает функция getData( ), добавляется в начало списка, на который указывает глобальная переменная-указатель pHead. Когда функция getData( ) возвращает нулевое значение, происходит выход из цикла while, после чего в следующем цикле while осуществляется проход по списку с выводом информации о каждом элементе списка. По достижении последнего элемента списка происходит выход из второго цикла while и программа завершает работу.
«Вывод программы представляет собой введённые имена в обратном порядке. Это происходит потому, что добавление элементов выполняется в начало списка. Возможна вставка элементов в конец списка, однако эта задача посложнее.»
[Советы]
Точно так, как ребёнок должен научиться ходить перед тем как ездить на автомобиле, считать перед тем как использовать калькулятор, программисту необходимо научиться писать программы, работающие со связанными списками, перед тем как использовать классы списков, написанные другими. В главе 28, "Стандартная библиотека шаблонов", будут описаны классы-контейнеры, предоставляемые в распоряжение программиста средами С++, среди которых есть и класс, представляющий собой связанный список.
_________________
180 стр. Часть 3. Введение в классы
В этой главе...
►Чем хороши защищённые члены 183
►Обращение к защищённым членам 184
В главе 12, "Классы в С++", рассматривалась концепция классов. Ключевое слово public было описано как часть объявления класса, т.е. просто как то, что следует делать. В этой главе вы узнаете, что ключевому слову public есть альтернативы.
Члены класса могут быть помечены как защищённые, что делает их недоступными извне класса. В отличие от защищённых, открытые ( public ) члены класса доступны для всех.
«Термин "недоступные" не следует понимать буквально. Любой программист может немного повозиться с исходным текстом и убрать ключевое слово protected. Возможность сделать член защищённым разработана для того, чтобы защитить программиста от обращения к члену просто по невнимательности.»
[Помни!]
Для того чтобы понять смысл защиты членов класса, нужно вспомнить, каковы цели объектно-ориентированного программирования.
■■■
■ Защита внутренних элементов класса от внешних функций. Ведь когда вы проектируете микроволновую печь ( или что-нибудь другое ), то оснащаете её по возможности простым интерфейсом с внешним миром и прячете содержимое в металлический ящик. Так делается для того, чтобы другие не могли поломать микроволновую печь. Защита членов класса выполняет роль железного ящика.
■ Создание класса, способного полноценно управлять своими внутренними членами. Несколько непоследовательно требовать от класса полноценной работы и ответственности за её результаты и одновременно позволять внешним функциям манипулировать его внутренними членами ( это то же самое, что и требовать от создателя микроволновой печи нести ответственность за мои непрофессиональные манипуляции с элементами её внутреннего устройства ).
_________________
181 стр. Глава 15. Защищённые члены класса: не беспокоить!
■ Сокращение до минимума внешнего интерфейса класса. Гораздо проще изучать и использовать класс, который имеет ограниченный интерфейс ( а интерфейсом класса являются его открытые члены ). Защищённые члены скрыты от пользователя, и их не надо помнить ( в противном случае интерфейсом становится весь класс ). Такой подход называется абстракцией, которая описана в главе 11, "Знакомство с объектно-ориентированным программированием".
■ Уменьшение уровня взаимосвязи между классом и внешней программой. Ограничив взаимосвязь класса с внешним кодом, при необходимости гораздо проще заменить класс каким-либо другим.
■■■
Я так и слышу, как поклонники функционального подхода говорят: "Не нужно делать ничего противоестественного! Достаточно потребовать от программиста, чтобы он попросту не трогал некоторые члены класса".
Всё это верно в теории, однако не оправдывается на практике. Программисты начинают писать программы, будучи переполненными благих намерений, однако приближение сроков сдачи программы заставляет их всё больше разочаровываться в отсутствии возможности прямого доступа к защищённым членам класса.
Добавление в класс ключевого слова public делает все находящиеся за ним члены класса открытыми, а значит, доступными для функций — не членов класса. Использовав ключевое слово protected, вы делаете все последующие члены класса защищёнными, т.е. недоступными для функций, которые не являются членами класса. Переключаться между защищёнными и открытыми членами класса можно сколько угодно раз.
Допустим, у нас есть класс Student. В приведённом ниже примере представлены все необходимые возможности, которые нужны классу, описывающему студента ( за исключением разве что функций spendMoney ( тратить деньги ) и drinkBeer ( пить пиво ), которые тоже являются свойствами студента ):
addCourse ( int hours , float grade ) — добавить пройденный курс;
grade ( ) — вернуть текущую среднюю оценку;
hours ( ) — вернуть количество прослушанных часов.
Оставшиеся члены класса Student можно объявить как защищённые, чтобы другие функции не могли "лезть" во внутренние дела класса Student.
class Student
{
public :
/* grade — возвращает текущую среднюю оценку */
float grade( )
{
return gpa ;
}
/* hours — возвращает количество прослушанных часов */
int hours( )
{
return semesterHours ;
}
/* addCourse — добавляет к записи студента прослушанный курс */
float addCourse( int hours , float grade )
/* Приведённые ниже члены недоступны для внешних функций */
protected :
int semesterHours ; /* Количество прослушанных часов */
float gpa ; /* Средняя оценка */
} ;
_________________
182 стр. Часть 3. Введение в классы
Теперь члены semesterHours и gpa доступны только из других членов класса Student, и приведённый ниже пример работать не будет.
Student s ;
int main( int argc , char* pArgs[ ] )
{
/* Повысим свой рейтинг ( но не слишком сильно, иначе никто не поверит ) */
s.gpa = 3.5 ; /* Вызовет ошибку при компиляции */
float gpa = s.grade( ) ; /* Эта открытая функция считывает значение переменной, но вы не можете непосредственно изменить её значение извне */
return 0 ;
}
При попытке этой программы изменить значение gpa на этапе компиляции будет выдано сообщение об ошибке.
«Считается признаком хорошего тона не полагаться на значение защиты по умолчанию, а определить в самом начале объявления класса ключевое слово public или private. Обычно класс начинают описывать с открытых членов, формируя интерфейс класса. Описание защищённых членов класса выполняется позже.»
[Советы]
«Члены класса могут быть защищены с помощью ещё одного ключевого слова — private. Кстати, по умолчанию при описании класса его члены считаются описанными именно как private. Разница между protected и private станет ясной при изучении наследования.»
Теперь, когда вы немного познакомились с защищёнными членами, я приведу аргументы, обосновывающие их использование.
Ключевое слово protected позволяет исключить возможность установки gpa равным не допустимому для этой величины значению. Внешнее приложение сможет добавить курс, но не сможет изменить значение среднего балла непосредственно. Если имеется необходимость непосредственного изменения значения gpa, класс может предоставить открытую функцию, предназначенную для этой цели, например:
class Student
{
public :
/* grade — делает то же, что и раньше */
float grade( )
{
return gpa ;
}
/* Даём возможность изменения средней оценки */
float grade( float newGPA )
{
float oldGPA = gpa ;
/* Проверяем допустимость значения */
if ( newGPA > 0 && newGPA <= 4.0 )
{
gpa = newGPA ;
}
return oldGPA ;
}
/* ...всё остальное остаётся без изменений */
protected :
int semesterHours ; /* Количество прослушанных часов */
float gpa ;
} ;
_________________
183 стр. Глава 15. Защищённые члены класса: не беспокоить!
Добавление новой функции grade( float ) позволяет внешним приложениям изменять содержимое gpa. Заметьте, что класс всё равно не позволяет внешним функциям полностью контролировать содержимое своих защищённых членов. Внешнее приложение не может присвоить gpa любое значение, а только то, которое лежит в диапазоне между 0 и 4.0.
Теперь класс Student обеспечивает внешний доступ к своим внутренним членам, одновременно не позволяя присвоить им недопустимое значение.
Теперь наш класс предоставляет ограниченный интерфейс. Чтобы использовать класс, достаточно знать, каковы его открытые члены, что они делают и какие аргументы принимают. Это значительно уменьшает объём информации, которую необходимо помнить для работы с классом.
Кроме того, иногда изменяются условия работы программы либо выявляются новые ошибки, и программист должен изменить содержимое членов класса ( если не логику его работы ). В этом случае изменение только защищённых членов класса не вызывает изменений в коде внешнего приложения.
Ещё одна причина — едва ли не самая важная — в ограниченности человеческих возможностей удержать в голове большое количество объектов и связей между ними. Использование строго ограниченного интерфейса класса позволяет программисту отвлечься от деталей реализации, скрытых за этим интерфейсом. Соответственно, разработчик класса может не думать о том, как именно будет использоваться интерфейс разрабатываемого им класса.
Может случиться так, что потребуется предоставить некоторым внешним функциям возможность обращения к защищённым членам класса. Для такого доступа можно воспользоваться ключевым словом friend ( друг ).
Иногда внешним функциям требуется прямой доступ к данным-членам. Без некоторого механизма "дружественности" программист был бы вынужден объявлять такие члены открытыми для всех, а значит, обращаться к этим членам могла бы любая внешняя функция.
Это похоже на то, как вы порой оставляете соседям ключ от своего дома на время отпуска, чтобы они иногда проверяли его. Давать ключи не членам семьи не совсем хорошо, однако это куда лучше, чем оставлять дом открытым.
Объявление друзей должно находиться в классе, который содержит защищённые члены ( что является ещё одним аргументом в пользу того, чтобы функций-друзей было как можно меньше ). Подобное объявление выполняется почти так же, как и объявление обычных прототипов, и должно содержать расширенное имя друга, включающее типы аргументов и возвращаемого значения. В приведённом ниже примере функция initialize( ) получает доступ ко всем членам класса Student.
_________________
184 стр. Часть 3. Введение в классы
class Student ;
{
friend void initialize( Student* ) ;
public :
/* Те же открытые члены, что и раньше */
protected :
int semesterHours ; /* Количество часов в семестре */
float gpa ;
} ;
/* Эта функция — друг класса Student и имеет доступ к его защищённым членам */
void initialize( Student *pS )
{
pS -> gpa =0 ; /* Теперь эти строки законны */
pS -> semesterHours = 0 ;
}
Одна и та же функция может одновременно быть объявлена другом нескольких классов. Это может быть удобно, например, для связи двух классов. Правда, такого рода связь не очень приветствуется, поскольку делает оба класса зависимыми друг от друга. Однако, если два класса взаимосвязаны по своей природе, их объединение может оказаться не столь плохим решением.
class Student ;
class Teacher
{
friend void registration( Teacher& , Student& ) ;
public :
void assignGrades( ) ;
protected :
int noStudents ;
Student *pList[ 100 ] ;
} ;
class Student
{
friend void registration( Teacher& , Student& ) ;
public :
/* Те же открытые члены, что и раньше */
protected :
Teacher *рТ ;
int semesterHours ; /* Количество часов в семестре */
float gpa ;
} ;
void registration( Teacher& , Student& )
{
/* Инициализация объекта Student */
s.semesterHours = 0 ;
s.gpa = 0 ;
/* Если есть место... */
if ( t.noStudents < 100 )
{
/* Добавляем в конец списка */
t.pList[ t.noStudents ] = &s ;
t.noStudents++ ;
}
}
_________________
185 стр. Глава 15. Защищённые члены класса: не беспокоить!
В данном примере функция registration( ) может обращаться к обоим классам — и Student и Teacher, связывая их на этапе регистрации, но при этом не входя в состав этих классов.
«Обратите внимание, что в первой строке примера объявляется класс Student, но не объявляются его члены. Запомните: такое описание класса называется предварительным и в нём описывается только имя класса. Предварительное описание нужно для того, чтобы другие классы, такие, например, как Teacher, могли обращаться к классу Student. Предварительные описания используются тогда, когда два класса должны обращаться один к другому.»
[Помни!]
Функция-член одного класса может быть объявлена как друг некоторого другого класса следующим образом:
class Teacher
{
/* Те же члены, что и раньше */
public :
void assignGrades( ) ;
} ;
class Student
{
friend void Teacher::assignGrades( ) ;
public :
/* Те же открытые члены, что и раньше */
protected :
/* Количество часов в семестре */
int semesterHours ;
float gpa ;
} ;
void Teacher::assignGrades( ) ;
{
/* Эта функция имеет доступ к защищённым членам класса Student */
}
В отличие от примера с функциями — не членами, функция-член класса должна быть объявлена перед тем, как класс Student объявит её другом.
Существующий класс может быть объявлен как друг некоторого иного класса целиком. Это означает, что все функции-члены класса становятся друзьями другого класса, например:
_________________
186 стр. Часть 3. Введение в классы
class Student ;
class Teacher
{
protected :
int noStudents ;
Student *pList [ 100 ] ;
public :
void assignGrades( ) ;
} ;
class Student
{
friend class Teacher ;
public :
/* Те же открытые члены, что и раньше */
protected :
Teacher *рТ ;
/* Количество часов в семестре */
int semesterHours ;
float gpa ;
} ;
Теперь любая функция-член класса Teacher имеет доступ ко всем защищённым членам класса Student. Объявление одного класса другом другого неразрывно связывает два класса.
_________________
187 стр. Глава 15. Защищённые члены класса: не беспокоить!
В этой главе...
►Использование конструкторов 189
Объекты в программе создаются и уничтожаются так же, как и объекты реального мира. Если класс сам отвечает за своё существование, он должен обладать возможностью управления процессом уничтожения и создания объектов. Программистам на С++ повезло, поскольку С++ предоставляет необходимый для этого механизм ( хотя, скорее всего, это не удача, а результат разумного планирования языка ). Прежде чем начинать создавать и уничтожать объекты в программе, обсудим, что значит "создавать объекты".
Некоторые подчас теряются в терминах класс и объект. В чём разница между этими терминами? Как они связаны?
Я могу создать класс Dog, который будет описывать соответствующие свойства лучшего друга человека. К примеру, у меня есть две собаки. Это значит, что мой класс Dog содержит два экземпляра — Труди и Скутер ( надеюсь, что два: Скутера я не видел уже несколько дней... ).
«Класс описывает тип предмета, а объект — это экземпляр класса. Dog является классом, а Труди и Скутер — объектами. Каждая собака представляет собой отдельный объект, но существует только один класс Dog, при этом не имеет значения, сколько у меня собак.»
[Помни!]
Объекты могут создаваться и уничтожаться, а классы попросту существуют. Мои собаки Труди и Скутер приходят и уходят, а класс Dog ( оставим эволюцию в стороне ) вечен.
Различные типы объектов создаются в разное время. Когда программа начинает выполняться, создаются глобальные объекты. Локальные объекты создаются, когда программа сталкивается с их объявлением.
«Глобальный объект является объектом, объявленным вне каких-либо функций. Локальный объект объявляется внутри функции, а следовательно, является локальным для функции. В приведённом ниже примере переменная me является глобальной, а переменная noМе — локальной по отношению к pickOne( ).»
[Помни!]
int me = 0 ;
void pickOne( )
{
int noMe ;
}
_________________
188 стр. Часть 3. Введение в классы
«Согласно правилам языка глобальные объекты по умолчанию инициализируются нулевыми значениями. Локальные объекты, т.е. объекты, объявленные внутри функций, не имеют инициализирующих значений. Такой подход, вообще говоря, для классов неприемлем.»
С++ позволяет определить внутри класса специальную функцию-член, которая автоматически вызывается при создании объекта этого класса. Эта функция-член называется конструктором и инициализирует объект, приводя его в некоторое необходимое начальное состояние. Кроме конструктора, в классе можно определить деструктор, который будет вызываться при уничтожении объекта. Эти две функции и являются предметом обсуждения данной главы.
Конструктор — это функция-член, которая вызывается автоматически во время создания объекта соответствующего класса. Основная задача конструктора заключается в инициализации объекта, приводящей его в некоторое корректное начальное состояние.
Объект можно проинициализировать на этапе его объявления, как сделал бы программист на С:
struct Student
{
int semesterHours ;
float gpa ;
} ;
void fn( )
{
Student s1 = { 0 , 0.0 } ;
// или
Student s2 ;
s2.semesterHours = 0 ;
s2.gpa = 0.0 ;
/* ...продолжение функции... */
}
Этот фрагмент кода не будет работать для настоящего класса С++, поскольку внешнее приложение не имеет доступа к защищённым членам класса. Обойти это ограничение можно, воспользовавшись специальной инициализирующей функцией, например, так:
class Student
{
public :
void init( )
{
semesterHours = 0 ;
gpa = 0.0 ;
}
/* ...прочие открытые члены... */
protected :
int semesterHours ;
float gpa ;
} ;
void fn( )
{
/* Создание объекта... */
Student s ;
/* ...и его инициализация */
s.init( ) ;
/* ...продолжение функции... */
}
_________________
189 стр. Глава 16. Создание и удаление объектов
Это решение имеет большой недостаток: класс должен полагаться на то, что приложение обязательно вызовет инициализирующую функцию. Если же эта функция не будет вызвана, в объекте будет содержаться мусор, и последствия этого совершенно непредсказуемы.
Для того чтобы избежать этой неприятности, ответственность за вызов инициализирующей объект функции необходимо переложить с приложения на компилятор. Всякий раз при создании объекта компилятор может вставлять в код специальную инициализирующую функцию — а это и есть конструктор!
Конструктор — это специальная функция-член, которая автоматически вызывается во время создания объекта. Конструктор должен иметь то же имя, что и класс. Таким образом компилятор сможет определить, что именно эта функция-член является конструктором. Конечно, создатели С++ могли сформулировать это правило как угодно, например, так: "Конструктором является функция с именем init( )". Как именно определено правило, не имеет значения; главное — чтобы конструктор мог быть распознан компилятором. Ещё одним свойством конструктора является то, что он не возвращает никакого значения, поскольку вызывается автоматически ( если бы конструктор и возвращал значение, его всё равно некуда было бы записать ).
Класс с использованием конструктора продемонстрирован в следующем примере.
//
/* Constructor — пример вызова конструктора */
//
#include
#include
#include
using namespace std ;
class Student
{
public :
Student( )
{
cout << "Конструируем Student" << endl ;
semesterHours = 0 ;
gpa = 0.0 ;
}
/* ...прочие открытые члены... */
protected :
int semesterHours ;
float gpa ;
} ;
int main ( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
cout << "Создание нового объекта Student" << endl ;
Student s ;
cout << "Создание нового объекта Student в куче" << endl ;
Student* pS = new Student ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
190 стр. Часть 3. Введение в классы
В этом примере компилятор сам вызывает конструктор Student::Student( ) в том месте, где объявляется объект s. Тот же эффект имеет и создание объекта Student в куче, что видно из вывода данной программы.
Создание нового объекта Student
Конструируем Student
Создание нового объекта Student в куче
Конструируем Student
Press any key to continue...
Этот простой конструктор реализован в виде встроенной ( inline ) функции. Конструктор можно создать и как обычную функцию с телом, вынесенным из объявления класса:
class Student
{
public :
Student( ) ;
/* ...Остальные открытые члены... */
protected :
int semesterHours ;
float gpa ;
} ;
Student::Student( )
{
cout << "Конструируем Student\n" ;
semesterHours = 0 ;
gpa = 0.0 ;
}
«В данном примере добавлена небольшая функция main( ), чтобы эту тестовую программу можно было запустить. Настоятельно рекомендую пройти эту программу в пошаговом режиме отладчика перед тем, как двигаться дальше. О том, как это сделать, вы можете прочесть в главе 10 , "Отладка программ на С++".»
[Советы]
Выполняя этот пример в пошаговом режиме, дойдите до строки с объявлением объекта s. Выполните команду отладчика Шаг внутрь ( Step into ), и управление как по волшебству перейдёт к функции Student::Student( ). Продолжайте выполнение конструктора в пошаговом режиме. Когда функция закончится, управление перейдёт к следующей за объявлением объекта класса строке.
«В некоторых случаях команда Шаг внутрь ( Step into ) выполняет весь конструктор сразу, за один шаг. В таком случае вы можете просто установить в нём точку останова, что сработает в любом случае.»
[Атас!]
_________________
191 стр. Глава 16. Создание и удаление объектов
Каждый элемент массива конструируется отдельно. Внесём в программу Constructor небольшие изменения.
//
/* ConstructArray — пример вызова конструкторов */
/* для массива объектов */
//
#include
#include
#include
using namespace std ;
class Student
{
public :
Student( )
{
cout << "Конструируем Student" << endl ;
semesterHours = 0 ;
gpa = 0.0 ;
}
/* ...прочие открытые члены... */
protected :
int semesterHours ;
float gpa ;
} ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
cout << "Создание массива из 5 объектов Student" << endl ;
Student s[ 5 ] ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Вывод этой программы выглядит следующим образом:
Создание массива из 5 объектов Student
Конструируем Student
Конструируем Student
Конструируем Student
Конструируем Student
Конструируем Student
Press any key to continue...
Если класс имеет данные-члены, которые являются объектами другого класса, конструкторы для этих объектов также будут вызваны автоматически. Рассмотрим следующий пример, в который добавлены команды вывода сообщений, позволяющие увидеть, в каком порядке создаются объекты.
_________________
192 стр. Часть 3. Введение в классы
//
/* ConstructMembers — объекты-члены класса */
/* конструируются до конструирования */
/* класса, содержащего эти объекты */
//
#include
#include
#include
using namespace std ;
class Course
{
public :
Course( ) /* пятый ход */
{
cout << "Конструируем Course" << endl ;
}
} ;
class Student
{
public :
Student( ) /* второй ход */
{
cout << "Конструируем Student" << endl ;
semesterHours = 0 ;
gpa = 0.0 ;
}
protected :
int semesterHours ;
float gpa ;
} ;
class Teacher
{
public :
Teacher( ) /* шестой ход */
{
cout << "Конструируем Teacher" << endl ;
}
protected :
Course c ; /* четвёрый ход */
} ;
class TutorPair
{
public :
TutorPair( ) /* седьмой ход */
{
cout << "Конструируем TutorPair" << endl ;
noMeetings = 0 ;
}
protected :
Student student ; /* первый ход*/
Teacher teacher ; /* третий ход */
int noMeetings ;
} ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
cout << "Создаём объект TutorPair" << endl ;
TutorPair tp ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
193 стр. Глава 16. Создание и удаление объектов
В результате работы этой программы на экран будут выведены следующие сообщения:
Создаём объект TutorPair
Конструируем Student
Конструируем Course
Конструируем Teacher
Конструируем TutorPair
Press any key to continue...
Создание объекта tp в main( ) автоматически вызывает конструктор TutorPair. Перед тем как управление будет передано телу конструктора TutorPair, вызываются конструкторы для объектов-членов student и teacher.
Конструктор Student вызывается первым, поскольку объект этого класса объявлен первым. Затем вызывается конструктор Teacher.
Конструирование члена 'с' класса Teacher ( тип этого члена — Course ) является частью процесса построения объекта класса Teacher. Каждый объект внутри класса должен быть сконструирован до того, как будет вызван конструктор класса-контейнера ( в противном случае этот конструктор не будет знать, в каком состоянии находятся члены-данные ).
Только после создания всех этих объектов управление переходит к конструктору класса TutorPair, который теперь может конструировать оставшуюся часть объекта.
«Это не означает, что TutorPair отвечает за инициализацию Student и Teacher. Каждый класс отвечает за инициализацию своего объекта, где бы тот ни создавался.»
[Помни!]
Объекты класса уничтожаются так же, как и создаются. Если класс может иметь конструктор для выполнения начальных установок, то он может содержать и специальную функцию для уничтожения объекта. Такая функция-член называется деструктором.
Класс может затребовать для своего объекта некоторые ресурсы с помощью конструктора; эти ресурсы должны быть освобождены при уничтожении объекта. Например, если конструктор открывает файл, перед окончанием работы с объектом класса или программы этот файл следует закрыть. Возможен и другой вариант: если конструктор берёт память из кучи, то она должна быть освобождена перед тем, как объект перестанет существовать. Деструктор позволяет делать это автоматически, не полагаясь на вызов необходимых функций-членов в программе.
Деструктор имеет то же имя, что и класс, но только с предшествующим ему символом тильды ( ~ ) ( С++ последователен и здесь: ведь символ тильды не что иное, как символ оператора "нет", т.е. деструктор — это отрицание конструктора ).
_________________
194 стр. Часть 3. Введение в классы
Как и конструктор, деструктор не имеет типа возвращаемого значения. С учётом сказанного деструктор класса Student будет выглядеть так:
class Student
{
public :
Student( )
{
semesterHours = 0 ;
gpa = 0.0 ;
}
~Student( )
{
/* Все используемые ресурсы освобождаются здесь */
}
/* ...остальные открытые члены... */
protected :
int semesterHours ;
float gpa ;
} ;
Деструктор вызывается автоматически, когда объект уничтожается или, если говорить языком С++, происходит его деструкция. Чтобы избежать тавтологии ( "деструктор вызывается для деструкции объекта" ), я по возможности старался не применять этот термин. Можно также сказать "когда объект выходит из области видимости". Локальный объект выходит из области видимости, когда функция, создавшая его, доходит до команды return. Глобальный или статический объект выходит из области видимости, когда прекращается работа программы.
Что касается объектов, создаваемых в куче, то указатель может выйти из области видимости, но память при этом не освобождается. По определению, память не является частью функции. Объект, созданный в куче, уничтожается ( а память возвращается в кучу ) при помощи оператора delete. Всё это продемонстрировано в следующей программе.
//
/* DestructMembers — демонстрация использования */
/* конструкторов и деструкторов */
//
#include
#include
#include
using namespace std ;
class Course
{
public :
Course( ) { cout << "Конструктор Course" << endl ; }
~Course( ) { cout << "Деструктор Course" << endl ; }
} ;
class Student
{
public :
Student( )
{
cout << "Конструктор Student" << endl ;
semesterHours = 0 ;
gpa = 0.0 ;
_________________
195 стр. Глава 16. Создание и удаление объектов
}
~Student( ) { cout << "Деструктор Student" << endl ; }
protected :
int semesterHours ;
float gpa ;
} ;
class Teacher
{
public :
Teacher( )
{
cout << "Конструктор Teacher" << endl ;
pC = new Course ;
}
~Teacher( )
{
cout << "Деструктор Teacher" << endl ;
delete pC ;
}
protected :
Course* pC ;
} ;
class TutorPair
{
public :
TutorPair( )
{
cout << "Конструктор TutorPair" << endl ;
noMeetings = 0 ;
}
~TutorPair( ) { cout << "Деструктор TutorPair" << endl ; }
protected :
Student student ;
Teacher teacher ;
int noMeetings ;
} ;
TutorPair* fn( )
{
cout << "Создание объекта TutorPair в функции fn( )"
<< endl ;
TutorPair tp ;
cout << "Создание объекта TutorPair в куче" << endl ;
TutorPair* pTP = new TutorPair ;
cout << "Возврат из функции fn ( )" << endl ;
return pTP ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Вызов функции fn( ) и возврат объекта TutorPair в куче */
TutorPair* pTPReturned = fn( ) ;
cout << "Получен объект в куче" << endl ;
delete pTPReturned ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
196 стр. Часть 3. Введение в классы
Функция main( ) вызывает функцию fn( ), которая создаёт объект tp ( область видимости этого объекта ограничена функцией ), а также объект в куче, возвращаемый функции main( ), которая и уничтожает его, возвращая память в кучу.
При выполнении программы вы увидите на экране следующее.
Создание объекта TutorPair в функции fn( )
Конструктор Student
Конструктор Teacher
Конструктор Course
Конструктор TutorPair
Создание объекта TutorPair в куче
Конструктор Student
Конструктор Teacher
Конструктор Course
Конструктор TutorPair
Возврат из функции fn( )
Деструктор TutorPair
Деструктор Teacher
Деструктор Course
Деструктор Student
Получен объект в куче
Деструктор TutorPair
Деструктор Teacher
Деструктор Course
Деструктор Student
Press any key to continue...
Здесь создаются два объекта TutorPair. Первый, tp, является локальным объектом функции fn( ), а второй, рТР, размещается в куче. Первый объект выходит из области видимости при возврате из функции и уничтожается автоматически, а второй остаётся до тех пор, пока функция main( ) не уничтожает его явным образом.
«Последовательность вызовов деструкторов при уничтожении объекта всегда имеет порядок, обратный порядку вызова конструкторов при создании этого объекта.»
[Помни!]
_________________
197 стр. Глава 16. Создание и удаление объектов
В этой главе...
►Как снабдить конструктор аргументами 198
►Определение конструкторов по умолчанию 203
►Конструирование членов класса 204
►Управление последовательностью конструирования 208
Класс представляет тип объекта в реальном мире. Например, мы использовали класс Student для представления студента и его свойств. Точно так же, как и студенты, классы считают себя абсолютно самостоятельными. Однако, в отличие от студентов, класс действительно сам "ухаживает" за собой — он должен всё время поддерживать себя в приемлемом состоянии.
Конструктора по умолчанию, описанного в главе 16, "Создание и удаление объектов", достаточно не всегда. Например, конструктор может инициализировать идентификатор студента нулевым значением — просто чтобы идентификатор не оказался случайным значением, но это нулевое значение может быть некорректным.
Одним словом, программистам на С++ часто нужны конструкторы, которые могут принимать различные аргументы, для того чтобы инициализировать объекты значениями, отличными от значений по умолчанию. В данной главе рассматриваются именно такие конструкторы.
С++ позволяет программисту определить конструктор с аргументами, например:
class Student
{
public :
Student( char *pName ) ;
/* Продолжение класса Student */
} ;
Возможность добавления аргументов к конструктору не требует особой, простите за каламбур, аргументации, но я всё же приведу несколько аргументов, аргументируя пользу применения аргументов. Во-первых, их использование в конструкторе достаточно удобно. Было бы несколько непоследовательно требовать от программиста сначала конструировать объект, а затем вызывать инициализирующую функцию с тем, чтобы она проводила инициализацию, специфичную для данного объекта. Конструктор, поддерживающий аргументы, похож на супермаркет: он предоставляет полный сервис.
_________________
198 стр. Часть 3. Введение в классы
Другая, более важная причина использования аргументов в конструкторе состоит в том, что иногда это единственный способ создать объект с необходимыми начальными значениями. Вспомните, что работа конструктора заключается в создании корректного ( в смысле требований данного класса ) объекта. Если какой-то созданный по умолчанию объект не отвечает требованиям программы, значит, конструктор не выполняет свою работу.
Например, банковский счёт без номера не является приемлемым ( С++ всё равно, каков номер счёта, но это почему-то волнует банк ). Можно создать объект BankAccount без номера, а затем потребовать от приложения вызвать некоторую функцию-член для инициализации номера счёта перед использованием. Однако это нарушает наши правила, поскольку при таком подходе класс вынужден полагаться на то, что эти действия будут выполнены внешним приложением.
Идея использования аргументов проста. Как известно, функции-члены могут иметь аргументы, поэтому конструктор, будучи функцией-членом, тоже может иметь аргументы.
При этом нельзя забывать, что вы вызываете конструктор не как нормальную функцию и передать ему аргумент можно только в момент создания объекта. Так, приведённая ниже программа создаёт объект s класса Student, вызывая конструктор Student( char* ). Объект s уничтожается в момент возврата из функции main( ).
//
/* ConstructorWArg — конструктор с аргументами */
//
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
class Student
{
public :
Student( char* pName )
{
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] = '\0' ;
semesterHours = 0 ;
gpa = 0.0 ;
}
/* ...прочие открытые члены... */
protected :
char name[ MAXNAMESIZE ] ;
int semesterHours ;
float gpa ;
} ;
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student s( "О. Danny Boy" ) ;
Student* pS = new Student( "E. Z. Rider" ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
199 стр. Глава 17. Аргументация конструирования
В этом примере конструктор выглядит почти так же, как и конструктор из главы 16, "Создание и удаление объектов", с тем лишь отличием, что он принимает аргумент pName, имеющий тип char*. Этот конструктор инициализирует все данные-члены нулевыми значениями, за исключением члена name, который инициализируется строкой pName.
Объект s создаётся в функции main( ). Аргумент, передаваемый конструктору, находится в строке объявления s сразу же за именем объекта. Благодаря такому объявлению студент s получил имя Danny. Закрывающая фигурная скобка функции main( ) вызывает гром и молнию деструктора на голову несчастного Danny. Аналогично создаётся объект в куче.
«Многие конструкторы в этой главе нарушают правило "функции размером больше трёх строк не должны быть inline-функциями". Я просто решил облегчить вам чтение ( а теперь — ваши аплодисменты! ).»
[Атас!]
Поскольку в этой главе проводятся параллели между конструктором и обычными функциями-членами, я позволю себе ещё одну параллель: конструкторы можно перегружать.
«Словосочетание "перегруженная функция" означает, что определено несколько функций с одинаковым именем, но разными типами аргументов. Если вы немного подзабыли этот термин, освежите память, обратившись к главе 6, "Создание функций".»
[Помни!]
С++ выбирает вызываемый конструктор, исходя из аргументов, передаваемых при объявлении объекта. Например, класс Student может одновременно иметь три конструктора, что продемонстрировано в следующем примере:
/* OverloadConstructor — несколько способов */
/* создать объект путём */
/* перегрузки конструктора */
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
class Student
{
public :
Student( )
_________________
200 стр. Часть 3. Введение в классы
{
cout << "Конструктор Student( )" << endl ;
semesterHours = 0 ;
gpa = 0.0 ;
name[ 0 ] = '\0' ;
}
Student( char *pName )
{
cout << "Конструктор Student( " << pName
<<" )" << endl ;
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] = '\0' ;
semesterHours = 0 ;
gpa = 0 ;
}
Student( char *pName , int xfrHours , float xfrGPA )
{
cout << "Конструктор Student( " << pName << ","
<< xfrHours << "," << xfrGPA << " )" << endl ;
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] = '\0' ;
semesterHours = xfrHours ;
gpa = xfrGPA ;
}
~Student( )
{
cout << "Деструктор Student" << endl ;
}
protected :
char name[ 40 ] ;
int semesterHours ;
float gpa ;
} ;
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Вызов трёх разных конструкторов */
Student noName ;
Student freshman( "Marian Haste" ) ;
Student xferStudent( "Pikumup Andropov" , 80 , 2.5 ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Поскольку объект noName реализован без аргументов, он конструируется с использованием Student::Student( ), который называется конструктором по умолчанию или пустым конструктором. ( Я предпочитаю последнее название, но, поскольку первое более распространённое, в этой книге будет использоваться именно оно. ) Объект freshMan создаётся с использованием конструктора, которому нужен только один аргумент типа char*; объекту xfеr требуется конструктор с тремя аргументами.
Заметьте, что все три конструктора ( и особенно два последних ) очень похожи. Единственное отличие второго конструктора от третьего заключается в том, что он обнуляет поля semesterHours и gpa, в то время как третий конструктор может присваивать им передаваемые в качестве аргументов значения.
_________________
201 стр. Глава 17. Аргументация конструирования
С++ позволяет в объявлении функции указать значения аргументов по умолчанию, т.е. используемые в том случае, если программист их не указал. Если применить этот метод в третьем конструкторе, то все три конструктора можно объединить в один, как это сделано в представленной ниже программе.
/* ConstructorWDefaults — несколько конструкторов */
/* зачастую могут быть */
/* бъединены в один */
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
class Student
{
public :
Student( char *pName = "no name" ,
int xfrHours = 0 ,
float xfrGPA = 0.0 )
{
cout << "Конструктор Student( " << pName<< ","
<< xfrHours << "," << xfrGPA << " )" << endl ;
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] = '\0' ;
semesterHours = xfrHours ;
gpa = xfrGPA ;
}
~Student( )
{
cout << "Деструктор Student" << endl ;
}
/* ...прочие открытые члены... */
protected :
char name[ MAXNAMESIZE ] ;
int semesterHours ;
float gpa ;
} ;
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Вызов одного и того же конструктора */
Student noName ;
Student freshman( "Marian Haste" ) ;
Student xferStudent( "Pikumup Andropov" , 80 , 2.5 ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Теперь все три объекта строятся с помощью одного и того же конструктора, а значения по умолчанию используются для аргументов, отсутствующих в объектах freshMan и noName.
_________________
202 стр. Часть 3. Введение в классы
«В ранних версиях С++ вы не смогли бы создать конструктор по умолчанию, предусмотрев значения по умолчанию для всех аргументов. Конструктор по умолчанию должен был быть определён явно. Так что будьте готовы к тому, что некоторые старые версии компиляторов могут потребовать явного определения конструктора по умолчанию.»
[Помни!]
Стоит отметить, что в С++ каждый класс должен иметь свой конструктор. Казалось бы, С++ должен генерировать сообщение об ошибке в случае, когда класс не оснащён конструктором, однако этого не происходит. Дело в том, что для обеспечения совместимости с существующим кодом С, который ничего не знает о конструкторах, С++ автоматически создаёт конструктор по умолчанию ( так сказать, умалчиваемый конструктор по умолчанию ), который инициализирует все данные-члены объекта нулями.
Если ваш класс имеет конструктор, С++ не будет автоматически его создавать ( как только С++ убеждается в том, что это не программа на С, он снимает с себя всю ответственность по обеспечению совместимости ).
«Вывод: если вы определили конструктор для вашего класса и при этом хотите, чтобы класс имел конструктор по умолчанию, то должны явно определить такой конструктор сами.»
[Атас!]
Приведённый ниже фрагмент демонстрирует сказанное. Этот пример вполне корректен.
class Student
{
/* ...то же, что и раньше, только без конструкторов */
} ;
int main( int argcs , char* pArgs[ ] )
{
Student noName ;
return 0 ;
}
Приведённый далее пример компилятор с негодованием отвергнет.
class Student
{
public :
Student( char *pName ) ;
} ;
int main( int argcs , char* pArgs[ ] )
{
Student noName ;
return 0 ;
}
To, что здесь добавлен конструктор Student ( char* ), выглядит безобидно, но при этом заставляет С++ отказаться от автоматической генерации конструктора по умолчанию.
_________________
203 стр. Глава 17. Аргументация конструирования
Не попадитесь в ловушку
♦♦♦♦♦
Ещё раз взгляните на объявление объектов класса student из приведённого выше примера:
Student noName ;
Student freshMan( "Smell E. Fish" ) ;
Student xfer( "Upp R. Classman" , 80 , 2.5 ) ;
Все объекты типа student, за исключением noName, объявлены со скобками, в которых находятся передаваемые классу аргументы. Почему же объект noName объявлен без скобок? С точки зрения приверженцев последовательности и аккуратности, лучше было бы объявлять этот объект так:
Student noName( ) ;
Конечно, можно сделать и так, но это не приведёт к ожидаемому результату. Вместо объявления объекта noName, создаваемого с помощью конструктора по умолчанию для класса student, будет объявлена функция, возвращающая по значению объект класса student. Мистика! Приведённые ниже два объявления демонстрируют, как похожи объявления объекта и функции в формате С++. ( Я-то считаю, что это можно было сделать и по-другому, но кто будет со мной считаться?.. ) Единственное отличие заключается в том, что при объявлении функции в скобках стоят названия типов, а при объявлении объекта в скобках содержатся объекты.
Student thisIsAFunc( int ) ;
Student thisIsAnObject( 10 ) ;
Если скобки пусты, невозможно однозначно сказать, что объявляется — функция или объект. Для обеспечения совместимости с языком С в С++ считается, что объявление с пустыми скобками — это объявление функции ( более надёжной альтернативой было бы требование наличия ключевого слова void при объявлении функции, но тогда нарушалось бы условие совместимости с существующими программами на С... ).
♦♦♦♦♦
В предыдущих примерах использовались данные-члены простых типов, такие как float или int. Переменные таких простых типов легко инициализировать, передав необходимое значение конструктору. Но что, если класс содержит данные-члены, которые являются объектами других классов? Рассмотрим приведённый ниже пример.
/* ConstructingMembers — передача параметров */
/* конструктору члена */
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
int nextStudentId = 0 ;
class StudentId
{
public :
StudentId( )
{
value = ++nextStudentId ;
cout << "Присвоение id " << value << endl ;
}
protected :
_________________
204 стр. Часть 3. Введение в классы
int value ;
} ;
class Student
{
public :
Student( char* pName )
{
cout << "Конструктор Student( " << pName
<< " )" << endl ;
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] = '\0' ;
semesterHours = 0 ;
gpa = 0.0 ;
}
/* ...прочие открытые члены... */
protected :
char name[ MAXNAMESIZE ] ;
int semesterHours ;
float gpa ;
StudentId id ;
} ;
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student s( "Chester" ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
В момент создания объекту типа Student присваивается собственный идентификатор. В данном примере идентификаторы "раздаются" последовательно, с помощью глобальной переменной nextStudentId.
Наш класс Student содержит член id, который является экземпляром класса StudentId. Конструктор класса Student не может присвоить значение члену id, поскольку Student не имеет доступа к защищённым членам класса StudentId. Можно было бы сделать Student другом класса StudentId, но такой подход нарушил бы положение объектно-ориентированного программирования, утверждающее, что каждый класс должен заниматься своим делом. Нам нужна возможность вызывать конструктор класса StudentId в процессе создания класса Student.
С++ делает это автоматически, инициализируя член id с помощью конструктора по умолчанию StudentId::StudentId( ). Это происходит после вызова конструктора класса Student, но до того, как управление передаётся первой строке этого конструктора. ( Выполните в пошаговом режиме приведённую выше программу, и вы поймёте, о чём я говорю. ) Выполнение приведённой выше программы выведет на экран следующие строки:
Присвоение id 1
Конструктор Student( Chester )
Press any key to continue...
Обратите внимание: сообщение от конструктора StudentId появилось раньше, чем сообщение от конструктора Student. ( Поскольку у нас все конструкторы выводят информацию на экран, вы можете решить, что они всегда должны поступать подобным образом. На самом деле подавляющее большинство конструкторов работают "молча". )
_________________
205 стр. Глава 17. Аргументация конструирования
Если программист не обеспечит свой класс конструктором, то конструктор по умолчанию, созданный С++, вызовет конструкторы всех данных-членов для их инициализации. То же касается и уничтожения объекта. Деструктор класса автоматически вызывает деструкторы всех данных-членов ( у которых они определены ).
Теперь мы знаем, что будет с конструктором по умолчанию. Но что, если мы захотим вызвать другой конструктор? Куда в этом случае нужно поместить объект? Я имею в виду следующее: представьте себе, что вместо автоматической генерации идентификатора студента, необходимо передать его конструктору Student, который в свою очередь, должен передать его конструктору StudentId.
«Для начала я покажу вам способ, который работать не будет. ( Здесь приведена только существенная для понимания часть кода — полностью программа ConstructSeparateID.cpp находится на прилагаемом компакт-диске . )»
[Диск]
class Student
{
public :
Student( char *pName = "no name" , int ssId = 0 )
{
cout << "Конструктор Student( " << pName
<< " )" << endl ;
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] = '\0' ;
semesterHours = 0 ;
gpa = 0.0 ;
/* Вот это можно и не пытаться делать - толку не будет */
StudentId id( ssId ) ;
}
protected :
char name[ MAXNAMESIZE ] ;
StudentId id ;
} ;
Конструктор класса StudentId был переписан так, чтобы он мог принимать внешнее значение ( значение по умолчанию необходимо для того, чтобы приведённый фрагмент откомпилировался без ошибок, которые появятся в противном случае; почему — станет понятно чуть позже ). Внутри конструктора Student программист ( т.е. я ) попытался невиданным доселе способом сконструировать объект id класса StudentId.
Если вы внимательно посмотрите на сообщения, которые выдаются в результате работы этой программы, то поймёте, в чём проблема.
Присвоение id 0
Конструктор Student( Chester )
Присвоение id 1234
Деструктор id 1234
Сообщение из функции main( )
Press any key to continue...
Деструктор id 0
Первая проблема заключается в том, что конструктор класса StudentId вызывается дважды: сначала с нулём и только затем с ожидаемым числом 1234. Кроме того, объект с идентификатором 1234 ликвидируется перед выводом сообщения от main( ). Очевидно, объект класса StudentId ликвидируется внутри конструктора класса Student.
_________________
206 стр. Часть 3. Введение в классы
Объяснить такое странное поведение программы довольно просто. Член id уже существует к моменту перехода управления к телу конструктора Student. Поэтому вместо инициализации уже существующего члена id объявление в последней строке конструктора Student вызывает создание локального объекта с таким же именем. Этот локальный объект и уничтожается при выходе из конструктора.
«Очевидно, нужен некий механизм конструирования не нового объекта, а уже существующего. Этот механизм должен работать перед открытием фигурной скобки конструктора. Для этого в С++ определена конструкция, использованная в программе ConstructDataMember.»
[Диск]
//
/* ConstructDataMember — передача параметра */
/* конструктору члена */
//
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
class StudentId
{
public :
StudentId( int id = 0 )
{
value = id ;
cout << "Присвоение id " << value << endl ;
}
~StudentId( )
{
cout << "Деструктор id " << value << endl ;
}
protected :
int value ;
} ;
class Student
{
public :
Student( char *pName = "no name" , int ssId = 0 )
: id( ssId )
{
cout << "Конструктор Student( " << pName
<< " )" << endl ;
strncpy( name , pName , MAXNAMESIZE ) ;
name[ MAXNAMESIZE - 1 ] - '\0' ;
}
protected :
char name[ 40 ] ;
StudentId id ;
} ;
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student s( "Chester" , 1234 ) ;
cout << "Сообщение из функции main" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
207 стр. Глава 17. Аргументация конструирования
Обратите особое внимание на первую строку конструктора. В этой строке есть кое-что, с чем вы до этого не встречались. Следующая за двоеточием команда вызывает конструкторы членов данного класса. Компилятор С++ прочтёт эту строку так: "Вызвать конструктор для члена id с аргументом ssId. Все остальные данные-члены, не вызванные явно, строить с использованием конструктора по умолчанию".
Результат работы этой программы свидетельствует, что всё получилось так, как мы и хотели.
Присвоение id 1234
Конструктор Student( Chester )
Сообщение из функции main
Press any key to continue...
Деструктор id 1234
Ещё одна проблема возникает при инициализации членов, объявленных как const. Вспомним, что переменная, объявленная как const, инициализируется при объявлении и после этого не может быть изменена. Каким же образом конструктор может присвоить значение константному члену? Проблема решается путём использования синтаксиса с двоеточием:
class Mammal
{
public :
Mammal( int nof ) : numberOfFeet( nof )
{ }
protected :
const int numberOfFeet ;
} ;
Объект класса Mammal ( млекопитающее ) имеет постоянное количество ног ( ампутации не рассматриваются ). Таким образом, это количество следует объявить как const. Значение данному члену присваивается при создании объекта, и после этого не может быть изменено.
«Программисты часто используют синтаксис с двоеточием для инициализации не только константных, но и других членов-данных. Это не обязательно, но часто используется на практике.»
[Советы]
При наличии нескольких объектов, в которых определены конструкторы, программист обычно не заботится о том, в каком порядке будут конструироваться эти объекты. Однако, если один или несколько конструкторов имеют побочное действие, различная последовательность конструирования может привести к разным результатам.
_________________
208 стр. Часть 3. Введение в классы
Порядок создания объектов подчиняется перечисленным ниже правилам.
■■■
■ Локальные и статические объекты создаются в том порядке, в котором они объявлены в программе.
■ Статические объекты создаются только один раз.
■ Все глобальные объекты создаются до вызова функции main( ).
■ Нет какого-либо определённого порядка создания глобальных объектов.
■ Члены создаются в том порядке, в котором они объявлены внутри класса.
■ Деструкторы вызываются в порядке, обратном порядку вызова конструкторов.
■■■
«Статическая переменная — это переменная, которая является локальной по отношению к функции, но при этом сохраняет своё значение между вызовами функции. Глобальная переменная — это переменная, объявленная вне какой-либо функции.»
[Помни!]
Рассмотрим каждое из приведённых выше правил.
Локальные объекты создаются в том порядке, в котором в программе встречаются их объявления. Обычно это порядок появления кода объявлений в функции. ( Если, конечно, в функции нет безусловных переходов, "перепрыгивающих" через объявления. Кстати говоря, безусловные переходы между объявлениями лучше не использовать — это затрудняет чтение и компиляцию программы. )
Статические переменные подобны обычным локальным переменным с тем отличием, что они создаются только один раз. Это очевидно, поскольку статические переменные сохраняют своё значение от вызова к вызову функции. В отличие от С, который может инициализировать статическую переменную в начале программы, С++ дождётся, когда управление перейдёт к строке с объявлением статической переменной, и только тогда начнёт её создание. Разберёмся в приведённой ниже простой программе.
/* ConstructStatic — демонстрация однократного */
/* создания статических объектов */
#include
#include
#include
using namespace std ;
class DoNothing
{
public :
DoNothing( int initial )
{
cout << "DoNothing сконструирован со значением "
<< initial
<< endl ;
}
} ;
_________________
209 стр. Глава 17. Аргументация конструирования
void fn( int i )
{
cout << "Функции fn передано значение " << i << endl ;
static DoNothing dn( i ) ;
}
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
fn( 10 ) ;
fn( 20 ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
После запуска этой программы на экране появится следующее:
Функции fn передано значение 10
DoNothing сконструирован со значением 10
Функции fn передано значение 20
Press any key to continue...
Обратите внимание, что сообщение от функции fn( ) появилось дважды, а сообщение от конструктора DoNothing — только при первом вызове fn( ).
Все глобальные объекты входят в область видимости программы. Таким образом, все они конструируются до того, как управление передаётся функции main( ).
«При отладке такой порядок может привести к неприятностям. Некоторые отладчики пытаются выполнить весь код, который находится до main( ), и только потом передать управление пользователю. Это прекрасно подходит для С, поскольку до входа в функцию main( ) там не может быть никакого кода, написанного пользователем. Однако в С++ это может стать причиной большой головной боли, поскольку тела конструкторов для всех глобальных объектов к моменту передачи управления main( ) уже выполнены. Если хоть один из этих конструкторов содержит серьёзный "жучок", программа погибнет до того, как начнёт выполняться!»
[Атас!]
Существует несколько подходов к решению этой проблемы. Первый заключается в том, чтобы проверять каждый конструктор на локальных объектах перед тем, как использовать его для глобальных. Если это не поможет решить проблему, можно попытаться добавить команды вывода сообщений в начало всех конструкторов, которые, по вашему предположению, могут иметь ошибки. Последнее сообщение, которое вы увидите, вероятно, будет сообщением конструктора с ошибкой.
Локальные объекты создаются в порядке выполнения программы. Для глобальных же объектов порядок создания не определён. Как вы помните, глобальные объекты входят в область видимости программы одновременно. Возникает вопрос: почему бы тогда компилятору не начать с начала файла с исходной программой и не создавать глобальные объекты в порядке их объявления? ( Честно говоря, я подозреваю, что на самом деле большинство компиляторов так и поступают. ) Увы, такой подход отлично работал бы, но только в том случае, если бы программа всегда состояла из одного файла.
_________________
210 стр. Часть 3. Введение в классы
Однако большинство программ в реальном мире состоят из нескольких файлов, которые компилируются каждый в отдельности, а уже затем связываются в единое целое. Поскольку компилятор не управляет порядком связывания, он не может влиять на порядок вызова конструкторов глобальных объектов в разных файлах.
В принципе в большинстве случаев порядок создания глобальных объектов не так уж и важен. Тем не менее иногда это может привести к ошибкам, которые потом очень сложно отследить ( такое случается довольно часто, чтобы обратить на это внимание в книге ). Разберём приведённый ниже пример.
class Student
{
public :
Student ( unsigned id ) : studentId( id ) { }
const int StudentId ;
} ;
class Tutor
{
public :
Tutor ( Student& s ) : tutoredId( s.studentId ) { }
int tutoredId ;
} ;
/* Создаём студента */
Student randy( 1234 ) ;
/* Назначаем студенту учителя */
Tutor jenny( randy ) ;
В этом примере конструктор Student присваивает студенту идентификатор, а конструктор класса Tutor записывает этот идентификатор студента, которому нужен учитель. Программа объявляет студента randy, а затем назначает ему учителя jenny.
При этом подразумевается, что randy создаётся раньше, чем jenny; в этом-то и состоит проблема. Представьте себе, что порядок создания этих объектов будет другим. Тогда объект jenny будет построен с использованием блока памяти, который пока что не является объектом типа Student, а значит, вместо идентификатора студента в randy будет находиться непредсказуемое значение.
«Приведённый выше пример несложен и несколько надуман. Однако проблемы, создаваемые глобальными объектами, могут оказаться гораздо коварнее. Во избежание этого не допускайте, чтобы конструктор глобального объекта обращался к другому глобальному объекту.»
[Советы]
Члены класса создаются в соответствии с порядком, в котором они объявлены внутри класса. Это не так просто и очевидно, как может показаться на первый взгляд. Рассмотрим пример.
class Student
{
public :
Student ( int id , int age ) : sAge( age ) , sId( id ) { }
const int sId ;
const int sAge ;
} ;
_________________
211 стр. Глава 17. Аргументация конструирования
В этом примере sId создаётся до sAge, несмотря на то что он стоит вторым в инициализирующем списке конструктора. Впрочем, единственный случай, когда можно заметить какую-то разницу в порядке конструирования, — это когда оба члена класса имеют конструкторы, которым присуще какое-либо общее побочное действие.
В каком бы порядке ни вызывались конструкторы объектов, вы можете быть уверены, что их деструкторы будут вызваны в обратном порядке. ( Приятно сознавать, что хоть одно правило в С++ не имеет никаких "если", "и" или "но". )
_________________
212 стр. Часть 3. Введение в классы
В этой главе...
►Автоматический конструктор копирования 215
►"Мелкие" и "глубокие" копии 217
Конструктор — это специальная функция, которая автоматически вызывается С++ при создании объекта с тем, чтобы предоставить ему возможность проинициализировать самого себя. В главе 16, "Создание и удаление объектов", описаны основные концепции применения конструкторов, в главе 17, "Аргументация конструирования", вы познакомились с разными типами конструкторов. А в настоящей главе рассматривается частный случай, известный под названием копирующего конструктора ( или конструктора копирования ).
Конструктор, который используется С++ для создания копий объекта, называется копирующим конструктором, или конструктором копирования. Он имеет вид X::Х( Х& ) ( или X::X( const Х& ) ), где X — имя класса. Да, это не ошибка — это действительно конструктор класса X, который требует в качестве аргумента ссылку на объект класса X. Это звучит несколько бессмысленно, но не торопитесь с выводами и позвольте объяснить, зачем такое "чудо" в С++.
Подумайте о том, что будет происходить в программе, если вы вызовете следующую функцию:
void fn( Student fs )
{
/* Некоторые действия */
}
int main( int argcs , char* pArgs[ ] )
{
Student ms ;
fn( ms ) ;
return 0 ;
}
При вызове описанной функции fn( ) ей будет передан в качестве аргумента не сам объект, а его копия.
_________________
213 стр. Глава 18. Копирующий конструктор
«В С++ аргументы функции передаются по значению.»
[Помни!]
Теперь попробуем понять, что же значит — создать копию объекта. Для этого требуется конструктор, который будет создавать объект ( даже если копируется уже существующий объект ). С++ мог бы побайтово скопировать существующий объект в новый, но как быть, если побайтовая копия не совсем то, что нам нужно? Что, если мы хотим нечто иное? ( Не спрашивайте у меня пока, что такое это "иное" и зачем оно нужно. Немного терпения! ) У нас должна быть возможность самим определять, как будет создаваться копия объекта.
Таким образом, в приведённом выше примере необходим копирующий конструктор, который будет выполнять копирование объекта ms при вызове функции fn( ). Этот частный копирующий конструктор и есть Student::Student( students ) ( попробуйте-ка произнести эту скороговорку... ).
Лучший путь понять, как работает конструктор копирования, — это увидеть его в действии. Рассмотрим приведённый ниже пример с классом Student.
//
/* CopyConstructor — работа конструктора копирования */
//
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
class Student
{
public :
/* conventional constructor — обычный конструктор */
Student( char *pName = "no name" , int ssId = 0 )
{
strcpy( name , pName ) ;
id = ssId ;
cout << "Конструируем " << name << endl ;
}
/* Копирующий конструктор */
Student( Student& s )
{
strcpy( name , "Копия " ) ;
strcat( name , s.name ) ;
id = s.id ;
cout << "Сконструирована " << name << endl ;
}
~Student( )
{
cout << "Деструкция " << name << endl ;
}
protected :
_________________
214 стр. Часть 3. Введение в классы
char name[ MAXNAMESIZE ] ;
int id ;
} ;
/* fn — передача параметра по значению */
void fn( Student copy )
{
cout << "В функции fn( )" << endl ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student Chester( "Chester" , 1234 ) ;
cout << "Вызов fn( )" << endl ;
fn( Chester ) ;
cout << "Возврат из fn( )" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
После запуска этой программы на экран будут выведены следующие строки:
Конструируем Chester
Вызов fn( )
Сконструирована Копия Chester
В функции fn( )
Деструкция Копия Chester
Возврат из fn( )
Press any key to continue...
Деструкция Chester
Давайте внимательно рассмотрим, как же работает эта программа. Обычный конструктор выводит первую строку; затем main( ) выводит строку "Вызов...". После этого С++ вызывает копирующий конструктор для создания копии объекта Chester ( которая и передаётся функции fn( ) в качестве аргумента ). Эта копия будет ликвидирована при возврате из функции fn( ) ; исходный же объект Chester ликвидируется при выходе из main( ).
Копирующий конструктор выглядит как обычный, но обладает особенностью получать в качестве аргумента ссылку на другой объект того же класса. ( Обратите внимание, что использованный в примере копирующий конструктор, помимо простого копирования объекта, делает кое-что ещё, например выводит строку "Конструируем копию...". Эту возможность выполнять кроме собственно копирования и другие действия можно будет с успехом применить для решения разных задач. Конечно, копирующие конструкторы обычно ограничиваются созданием копий уже существующих объектов, но на самом деле они могут делать всё, что угодно программисту. )
Копирующий конструктор так же важен, как и конструктор по умолчанию. Важен настолько, что С++ считает невозможным существование класса без копирующего конструктора. Если вы не создадите свою версию такого конструктора, С++ создаст её за вас. ( Это несколько отличается от конструктора по умолчанию, который создаётся только в том случае, если в вашем классе не определено вообще никаких конструкторов. )
_________________
215 стр. Глава 18. Копирующий конструктор
«Копирующий конструктор, создаваемый С++, выполняет поэлементное копирование всех членов-данных. Ранее копирующий конструктор, создаваемый С++, выполнял побитовое копирование. Отличие между этими методами заключается в том, что при поэлементном копировании для каждого члена класса вызываются соответствующие копирующие конструкторы ( если они существуют ), тогда как при побитовом копировании конструкторы не вызывались. Разницу в результатах можно увидеть, выполнив приведённый пример.»
[Диск]
/* DefaultCopyConstructor — демонстрация вызова */
/* конструктором копирования по */
/* умолчанию конструкторов */
/* копирования членов */
#include
#include
#include
#include
using namespace std ;
const int MAXNAMESIZE = 40 ;
class Student
{
public :
Student( char *pName = "no name" )
{
strcpy( name , pName ) ;
cout << "Конструируем " << name << endl ;
}
Student( Student& s )
{
strcpy( name , "Копия " ) ;
strcat( name , s.name ) ;
cout << "Сконструирована " << name << endl ;
}
~Student( )
{
cout << "Деструкция " << name << endl ;
}
protected :
char name[ MAXNAMESIZE ] ;
} ;
class Tutor
{
public :
/* Вызов конструктора копирования Student */
Tutor( Student& s ) : student( s )
{
cout << "Конструирование объекта Tutor" << endl ;
id = 0 ;
}
protected :
_________________
216 стр. Часть 3. Введение в классы
Student student ;
int id ;
} ;
void fn( Tutor tutor )
{
cout << "В функции fn( )" << endl ;
}
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
Student Chester( "Chester" ) ;
Tutor tutor( Chester ) ;
cout << "Вызов fn ( )" << endl ;
fn( tutor ) ;
cout << "Возврат из fn( )" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Запуск этой программы приведёт к выводу таких сообщений:
Конструируем Chester
Сконструирована Копия Chester
Конструирование объекта Tutor
Вызов fn( )
Сконструирована Копия Копия Chester
В функции fn( )
Деструкция Копия Копия Chester
Возврат из fn( )
Press any key to continue . . .
Деструкция Копия Chester
Деструкция Chester
Конструирование объекта Chester приводит к вызову конструктора Student, который выводит первое сообщение. Конструктор объекта tutor вызывает копирующий конструктор Student для генерации собственного члена student, и приводит к выводу следующих двух строк.
Затем программа передаёт копию объекта Tutor в функцию fn( ). Поскольку класс Tutor не определяет копирующий конструктор, программа вызывает конструктор копирования по умолчанию.
Конструктор копирования по умолчанию Tutor вызывает конструкторы копирования для членов-данных. Копирующий конструктор для int просто копирует значение, но конструктор копирования Student генерирует вывод на экран строки Сконструирована Копия Копия Chester. Деструктор копии вызывается как часть возврата из функции fn( ).
Выполнение поэлементного копирования — естественная задача конструктора копирования. Но что ещё можно сделать с помощью такого конструктора? Когда наконец можно будет попытаться сделать что-то поинтереснее, чем программирование поэлементного копирования и объединения каких-то строк с именем несуществующего студента?
_________________
217 стр. Глава 18. Копирующий конструктор
Представим ситуацию, когда конструктор распределяет для объекта некоторые системные ресурсы, например память из кучи. Если копирующий конструктор будет выполнять простое копирование без выделения памяти из кучи для копируемого объекта, может возникнуть ситуация, когда два объекта будут считать, что именно они являются владельцами одного блока памяти. Ситуация ещё более усугубится при вызове деструкторов обоих объектов, которые попытаются освободить одну и ту же память. Взгляните на приведённый ниже пример.
/* ShallowCopy — мелкое копирование */
/* неприменимо при захвате */
/* ресурсов */
//
#include
#include
#include
#include
using namespace std ;
class Person
{
public :
Person( char *pN )
{
cout << "Конструирование \" " << pN << " \" " << endl ;
pName = new char[ strlen( pN ) + 1 ] ;
if ( pName != 0 )
{
strcpy( pName , pN ) ;
}
}
~Person( )
{
cout << "Деструкция \" " << pName << " \" " << endl ;
strcpy( pName , "Уже освобождённая память" ) ;
/* delete pName ; */
}
protected :
char *pName ;
} ;
void fn( )
{
/* Создание нового объекта */
Person p1( "Достаточно длинное имя" ) ;
/* Копирование p1 в р2 */
Person p2(p1);
}
int main( int argcs , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
cout << "Вызов fn( )" << endl ;
fn( ) ;
cout << "Возврат из fn( )" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
218 стр. Часть 3. Введение в классы
Эта программа порождает следующий вывод:
Вызов fn( )
Конструирование "Достаточно длинное имя"
Деструкция "Достаточно длинное имя"
Деструкция "Уже освобождённая память"
Возврат из fn( )
Press any key to continue...
В этом примере конструктор для Person выделяет память из кучи для хранения в ней имени произвольной длины, что невозможно при использовании массивов. Деструктор возвращает эту память в кучу. Основная программа вызывает функцию fn( ), которая создаёт объект p1, описывающий человека, после чего создаётся копия этого объекта — р2. Оба объекта автоматически уничтожаются при выходе из функции fn( ).
После запуска этой программы вы получите сообщение только от одного конструктора. Это неудивительно, поскольку копия р2 создаётся с помощью предоставляемого С++ конструктора копирования по умолчанию, а он не выводит никаких сообщений. Однако, после того как p1 и р2 выходят из области видимости, вы не получите двух сообщений о ликвидации объектов, как можно было ожидать. Первый конструктор выводит ожидаемое сообщение о деструкции объекта, но второй деструктор сообщает, что память уже была освобождена.
«Если бы мы действительно освобождали память в программе, то программа после попытки освободить уже освобождённую память оказалась бы в нестабильном состоянии и могла аварийно завершиться.»
[Атас!]
Конструктор вызывается один раз и выделяет блок памяти из кучи для хранения в нём имени человека. Копирующий конструктор, создаваемый С++, просто копирует этот адрес в новый объект, без выделения нового блока памяти.
Когда объекты ликвидируются, деструктор для р2 первым получает доступ к этому блоку памяти. Этот деструктор стирает имя и освобождает блок памяти. К тому времени как деструктор p1 получает доступ к этому блоку, память уже очищена, а имя стёрто. Теперь понятно, откуда взялось сообщение об ошибке. Суть проблемы проиллюстрирована на рис. 18.1. Объект p1 копируется в новый объект р2, но не копируются используемые им ресурсы. Таким образом, р1 и р2 указывают на один и тот же ресурс ( в данном случае это блок памяти ). Такое явление называется "мелким" ( shallow ) копированием, поскольку при этом копируются только члены класса как таковые.
«Решение этой проблемы визуально показано на рис. 18.2. В данном случае нужен такой копирующий конструктор, который будет выделять ресурсы для нового объекта. Давайте добавим такой конструктор к классу и посмотрим, как он работает ( здесь приведён только фрагмент программы, полностью находящейся на прилагаемом компакт-диске ).»
[Диск]
class Person
{
public :
Person( char *pN )
{
cout << "Конструирование " << pN << endl ;
pName = new char[ strlen( pN ) + 1 ] ;
if ( pName != 0 )
{
strcpy( pName , pN ) ;
}
}
_________________
219 стр. Глава 18. Копирующий конструктор
/* Копирующий конструктор выделяет новый блок памяти из кучи */
Person( Person& p )
{
cout << "Копирование " << p.pName
<< " в собственный блок" << endl ;
pName = new char[ strlen( p.pName ) + 1 ] ;
if ( pName != 0 )
{
strcpy( pName , p.pName ) ;
}
}
~Person( )
{
cout << "Деструкция " << pName << endl ;
strcpy( pName , "Уже освобождённая память" ) ;
/* delete pName ; */
}
protected :
char *pName ;
} ;
Рис. 18.1. Мелкое копирование объекта p1 в р2
Здесь копирующий конструктор выделяет новый блок памяти для имени, а затем копирует содержимое блока памяти исходного объекта в этот новый блок ( рис. 18.2 ). Такое копирование называется "глубоким" ( deep ), поскольку копирует не только элементы, но и занятые ими ресурсы ( конечно, аналогия, как говорится, притянута за уши, но ничего не поделаешь — не я придумал эти термины ).
Запуск программы с новым копирующим конструктором приведёт к выводу на экран следующих строк:
Вызов fn( )
Конструирование Достаточно_длинное_имя
Копирование Достаточно_длинное_имя в собственный блок
Деструкция Достаточно_длинное_имя
Деструкция Достаточно_длинное_имя
Возврат из fn( )
Press any key to continue...
_________________
220 стр. Часть 3. Введение в классы
Как видите, теперь указатели на строки в р1 и р2 указывают на разные данные.
Рис. 18.2. Глубокое копирование объекта p1 в р2
Копии создаются не только тогда, когда объекты передаются в функции по значению. Копии объектов могут создаваться и по другим причинам, например при возврате объекта по значению. Рассмотрим пример.
Student fn( ) ; /* Возвращает объект по значению */
int main ( int argcs , char* pArgs[ ] )
{
Student s ;
s = fn( ) ; /* В результате вызова fn( ) будет создан временный объект */
return 0 ;
}
Функция fn( ) возвращает объект по значению. В конечном счёте этот объект будет скопирован в s, но где он находится до этого?
Для хранения таких объектов С++ создаёт временные объекты ( такие объекты создаются и в некоторых других случаях ). "Хорошо, — скажете вы, — С++ создаёт временные объекты, но откуда он знает, когда их надо уничтожать?" ( Спасибо за хороший вопрос! ) В нашем примере это не имеет особого значения, поскольку временный объект выйдет из области видимости, как только копирующий конструктор скопирует его в s. Но что, если s будет определено как ссылка?
_________________
221 стр. Глава 18. Копирующий конструктор
int main ( int argcs , char* pArgs[ ] )
{
Student& refS = fn( ) ;
/* ...Что теперь?... */
return 0 ;
}
Теперь период жизни временного объекта имеет большое значение, поскольку ссылка refS продолжает своё существование независимо от существования объекта! В приведённом ниже примере я отметил место, начиная с которого временный объект становится недоступен.
Student fn1( ) ;
int fn2( Student& ) ;
int main ( int argcs , char* pArgs[ ] )
{
int x ;
/* Создаём объект Student, вызывая fn1( ), а затем передаём этот объект функции fn2( ) . fn2( ) возвращает целочисленное значение, которое используется для выполнения некоторых вычислений. Весь этот период временный объект, возвращённый функцией fn1( ), доступен */
х = 3*fn2( fn1( ) ) + 10 ;
/* Временный объект, который вернула функция fn1( ), становится недоступен */
/* ...Остальной код... */
return 0 ;
}
Таким образом, пример с использованием ссылки неверен, поскольку объект выйдет из области видимости, a refS будет продолжать существовать, и в результате ссылка будет указывать на несуществующий объект.
Вы можете подумать, что изучение всего этого копирования объектов туда и обратно — пустая трата времени. Что, если вы не хотите делать все эти копии? Самое простое решение заключается в передаче и приёме объектов функции по ссылке. Это исключает все описанные неприятности.
Но как убедиться, что С++ не создаёт временных объектов незаметно для вас? Допустим, ваш класс использует ресурсы, которые вы не хотите копировать. Что же вам делать?
Можно просто использовать вывод сообщения в копирующем конструкторе, которое предупредит вас о том, что была сделана копия. А можно объявить копирующий конструктор защищённой функцией, как показано в приведённом ниже примере.
class Student
{
protected :
Student( Student& s ){ }
public :
/* ...Всё остальное как обычно... */
} ;
Такой подход исключит использование копирующего конструктора любыми внешними функциями, включая сам С++, а значит, запретит создание копий ваших объектов Student ( позволяя при этом создавать копии функциям-членам ).
_________________
222 стр. Часть 3. Введение в классы
Использование копирующего конструктора для создания временных объектов и копий объектов вызывает один интересный вопрос. Рассмотрим очередной пример.
class Student
{
public :
Student( Student s )
{
/* ...всё, что угодно... */
}
} ;
void fn( Student fs )
{
}
int main ( int argcs , char* pArgs[ ] )
{
Student ms ;
fn( ms ) ;
return 0 ;
}
И в самом деле, почему бы не объявить копирующий конструктор класса Student как Student::Student( Student ) ? Однако такое объявление попросту невозможно! При попытке скомпилировать программу с таким объявлением вы получите сообщение об ошибке; Dev-C++ сообщит примерно следующее:
invalid constructor; you probably meant ' Student ( const Student& )'
Давайте подумаем, почему аргумент конструктора обязательно должен быть ссылкой? Представим, что ограничений на тип аргумента копирующего конструктора нет. В этом случае, когда main( ) вызовет функцию fn( ), компилятор С++ использует копирующий конструктор для создания копии объекта класса Student. При этом копирующий конструктор, получая объект по значению, требует вызова копирующего конструктора для создания копии объекта класса Student. И так до полного исчерпания памяти и аварийного останова...
_________________
223 стр. Глава 18. Копирующий конструктор
В этой главе...
►Определение статических членов 224
►Объявление статических функций-членов 228
►Что такое this 230
По умолчанию данные-члены создаются отдельно для каждого объекта. Например, каждый студент имеет своё собственное имя.
Однако, кроме того, вы можете создавать данные-члены, используемые всеми объектами класса совместно, объявив их статическими. Несмотря на то что термин статический применим как к данным-членам, так и к функциям-членам, для данных и для функций его значение несколько различно. В этой главе рассматриваются оба типа и их отличия.
Данные-члены можно сделать общими для всех объектов класса, объявив их статическими ( static ). Такие члены называются статическими данными-членами ( я бы удивился, если бы они назывались по-другому... ).
Большинство свойств класса являются свойствами отдельных объектов. Если использовать избитый ( точнее, очень избитый ) пример со студентами, можно сказать, что такие свойства, как имя, идентификационный номер и пройденные курсы, специфичны для каждого отдельного студента. Однако есть свойства, которые распространяются на всех студентов, например количество зачисленных студентов, самый высокий балл среди всех студентов или указатель на первого студента в связанном списке.
Такую информацию можно хранить в общей ( и ставшей привычной ) глобальной переменной. Например, можно использовать простую целочисленную переменную для отслеживания количества объектов Student. Однако при таком подходе возникает проблема, связанная с тем, что эти переменные находятся "снаружи" класса. Это подобно, например, установке регулятора напряжения моей микроволновой печи где-нибудь в спальне. Конечно, так можно сделать, и печь, вероятно, даже будет нормально работать, но моя собака во время очередной пробежки по квартире может наступить на провода и её придётся соскребать с потолка, что вряд ли доставит мне удовольствие ( собаке, я думаю, это тоже не очень-то понравится ).
Если уж создавать класс, полностью отвечающий за своё состояние, то такие глобальные переменные, как регулятор напряжения, нужно хранить внутри класса, подальше от неаккуратных собачьих лап. Это и объясняет необходимость статических членов.
_________________
224 стр. Часть 3. Введение в классы
«Вы могли слышать, что статические члены также называют членами класса, поскольку они доступны для всех объектов класса. Чтобы подчеркнуть отличие от статических членов, обычные члены называют компонентами экземпляра или членами объекта, поскольку каждый объект имеет собственный набор таких членов.»
[Советы]
Статические данные-члены объявляются в классе с помощью ключевого слова static, как показано в приведённом ниже примере.
class Student
{
public :
Student( char *pName = "no name" ) : name( pName )
{
noOfStudents++ ;
}
~Student( )
{
noOfStudents-- ;
}
protected :
static int noOfStudents ;
string name ;
} ;
Student s1 ;
Student s2 ;
Член noOfStudents входит в состав класса Student, но не входит в состав объектов s1 и s2. Таким образом, для любого объекта класса Student существуют отдельные члены name и только один noOfStudents, который доступен для всех объектов класса Student.
"Хорошо,— спросите вы,— если место под noOfStudents не выделено ни в каком объекте класса Student, то где же он находится?" Ответ прост: это место не выделяется. Вы должны сами выделить для него место так, как показано ниже, int Student::noOfStudents = 0 ;
Этот своеобразный синтаксис выделяет место для статического члена класса и инициализирует его нулём. Статические данные-члены должны быть глобальными ( как статические переменные не могут быть локальными по отношению к некоторой функции ).
«Для любого члена, имя которого встречается вне класса, требуется указание класса, к которому он принадлежит.»
[Помни!]
«Такое выделение памяти вручную удивляет, но только до тех пор, пока вы не столкнётесь с проектами, в которых используется несколько модулей с исходным кодом. С++ должен знать, в каком именно .срр-файле надо выделить пространство для статической переменной. В случае нестатических членов это не составляет проблемы, так как память выделяется там и тогда, где и когда создаётся объект класса.»
[Советы]
_________________
225 стр. Глава 19. Статические члены
Правила обращения к статическим данным-членам те же, что и к обычным членам. Из класса к статическим членам обращаются так же, как и к другим членам класса. К открытым статическим членам можно обращаться извне класса, а к защищённым — нельзя, как и к обычным защищённым членам.
class Student
{
public :
Student( )
{
noOfStudents++ ; /* Обращение из класса */
/* ...остальная программа... */
}
static int noOfStudents ;
/* ...то же, что и раньше... */
} ;
void fn( Student& s1 , Student s2 )
{
/* Обращение к открытому статическому члену */
cout << "Количество студентов - "
<< s1.noOfStudents /* Обращение извне Класса */
<< "\n" ;
}
В функции fn( ) происходит обращение к noOfStudents с использованием объекта s1. Однако, поскольку s1 и s2 имеют одинаковый доступ к члену noOfStudents, возникает вопрос: почему я выбрал именно s1? Почему я не использовал s2? На самом деле это не имеет значения. Вы можете обращаться к статическим членам, используя любой объект класса, например, так:
/* ...Класс определяется так же, как и раньше... */
void fn( Student& s1 , Student s2 )
{
/* Представленные команды приведут к идентичному результату */
cout << "Количество студентов - "
<< s1.noOfStudents <<"\n" ;
cout << "Количество студентов - "
<< s2.noOfStudents << "\n" ;
}
На самом деле нам вообще не нужен объект! Можно использовать просто имя класса, как показано в следующем примере:
/* ...Класс определяется так же, как и раньше... */
void fn( Student& s1 , Student s2 )
{
/* Результат остаётся неизменным */
cout << "Количество студентов - "
<< Student::noOfStudents
<< " \ n" ;
}
Независимо от того, будете ли вы использовать имя объекта или нет, С++ всё равно будет использовать имя класса.
_________________
226 стр. Часть 3. Введение в классы
«Объект, используемый для обращения к статическому члену, никак не обрабатывается, даже если это обращение явным образом указано в выражении. Для того чтобы понять, что я имею в виду, рассмотрим приведённый ниже пример.»
class Student
{
static int noOfStudents ;
Student& nextStudent( ) ;
/* ...To же, что и раньше... */
} ;
void fn( Student& s )
{
cout << s.nextStudent( ).noOfStudents
<< "\n" ;
}
Функция-член nextStudent( ) в этом примере не должна вызываться. Всё, что нужно знать С++ для обращения к noOfStudents, — тип возвращаемого значения, а он может это выяснить и не выполняя данную функцию[ 15 ].
Существует бесчисленное множество областей применения статических данных-членов, но здесь мы остановимся лишь на нескольких из них. Во-первых, можно использовать статические члены для хранения количества объектов, задействованных в программе. Например, в классе Student такой счётчик можно проинициализировать нулём, а затем увеличивать его на единицу внутри конструктора и уменьшать внутри деструктора. Тогда в любой момент этот статический член будет содержать количество существующих в данный момент объектов класса Student. Однако следует помнить, что этот счётчик будет содержать количество объектов, существующих в данный момент ( включая временные объекты ), а не количество студентов[ 16 ].
Ещё один способ использования статических членов заключается в индицировании выполнения определённого действия. Например, классу Radio может понадобиться инициализировать некие аппаратные средства при первом выполнении команды tune, но не перед последующими вызовами. С помощью статического члена можно указать, что первый вызов tune уже выполнил инициализацию. Кроме всего прочего, статический член может служить указателем безошибочности инициализации аппаратных средств.
И наконец, в статических членах можно хранить указатель на первый элемент связанного списка. Таким образом, статические члены могут содержать любую информацию "общего использования", которая будет доступна для всех объектов и во всех функциях ( не стоит забывать, однако, что чрезмерное использование статических переменных усложняет поиск ошибок в программе ).
___________________
15Вообще говоря, это зависит от используемого компилятора. Так, тот же Dev-C++ вызовет данную функцию, в чём легко убедиться, скомпилировав и выполнив приведённый пример ( дополнив его, естественно, функцией main( ), в которой вызывается fn( ) ). — Прим. ред.
16Ещё одно замечание: в этом случае вы должны позаботиться о том, чтобы счётчик увеличивался во всех конструкторах, включая конструктор копирования. — Прим. ред.
_________________
227 стр. Глава 19. Статические члены
Функции-члены также могут быть объявлены статическими. Подобно статическим данным-членам, они связаны с классом, а не с каким-либо отдельным объектом класса. Это означает, что обращение к статическим функциям-членам, как и к статическим данным-членам, не требует наличия объекта. Если объект и присутствует, то используется только его тип.
«Таким образом, оба вызова статической функции-члена number( ) в приведённой ниже программе корректны.»
[Диск]
//
/* CallStaticMember — два способа вызова */
/* статической функции-члена */
//
#include
#include
#include
#include
using namespace std ;
class Student
{
public :
Student( char* pN = "no name" )
{
pName = new char[ strlen( pN ) + 1 ] ;
if ( pName )
{
strcpy( pName , pN ) ;
}
noOfStudents++ ;
}
~Student ( ) { noOfStudents-- ; }
static int number( ) { return noOfStudents ; }
/* ...Всё прочее — как и раньше... */
protected :
char* pName ;
static int noOfStudents ;
} ;
int Student::noOfStudents = 0 ;
int main( int argcs , char* pArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
Student s1( "Chester" ) ;
Student s2 ( "Scooter" ) ;
cout << "Количество студентов — "
<< s1.number( ) << endl ;
cout << " Количество студентов — "
<< Student::number( ) << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ;
return 0 ;
}
Количество студентов — 2
Количество студентов — 2
Press any key to continue...
_________________
228 стр. Часть 3. Введение в классы
Обратите внимание на то, как статическая функция-член обращается к статическим данным-членам. Поскольку статическая функция-член не связана с каким-либо объектом, она не может неявно обращаться к нестатическому члену. Таким образом, приведённый ниже пример неправилен.
class Student
{
public :
/* Приведённый ниже код неверен */
static char *sName( )
{
return pName ; /* Какое именно имя? */
}
/* ...всё остальное то же, что и ранее... */
protected :
char * pName ;
static int noOfStudents ;
} ;
Это не означает, что статические функции-члены не имеют доступа к нестатическим данным-членам. Рассмотрим следующий пример, в котором функция findName( ) используется для поиска объекта в связанном списке ( о том, как работают связанные списки, рассказывается в главе 14, "Указатели на объекты" ). Здесь приводится только относящаяся к нашему рассмотрению часть кода; всё остальное вы можете дописать самостоятельно, в качестве небольшого домашнего задания.
class Student
{
public :
Student ( char *pName )
{
/* Создаёт объект и добавляет его в список */
}
static Student* findName( char *pName )
{
/* Ищет объект в списке, на который указывает указатель pHead */
}
protected :
static Student *pHead ;
Student *pNext ;
char* pName ;
} ;
Student* Student::pHead = 0
Функция findName( ) имеет доступ к pHead, поскольку этот указатель доступен для всех объектов. Так как findName является членом класса Student, он имеет доступ к членам pNext объектов. Этот доступ позволяет функции проходить по списку в поисках требуемого объекта. Вот как используется такая функция.
int main( int argcs , char* pArgs[ ] )
{
Student s1( "Randy" ) ;
Student s2( "Jenny" ) ;
Student s3( "Kinsey" ) ;
Student *pS = s1.findName( "Jenny" ) ;
return 0 ;
}
_________________
229 стр. Глава 19. Статические члены
Я уже упоминал несколько раз о том, что такое this, но тем не менее давайте ещё раз разберёмся в этом вопросе, this — это указатель на текущий объект внутри функции-члена. Он используется, когда не указано другое имя объекта. В обычной функции-члене this — скрытый первый аргумент, передаваемый функции.
class SC
{
public :
void nFn( int a ) ;
/* To же, что и SC::nFn( SC *this , int a ) */
static void sFn( int a ) ;
/* To же, что и SC::sFn( int a ) */
} ;
void fn( SC& s )
{
s.nFn( 10 ) ; /* Преобразуется в SC::nFn( &s , 10 ) ; */
s.sFn( 10 ) ; /* Преобразуется в SC::sFn( 10 ) ; */
}
Таким образом, функция nFn( ) интерпретируется так же, как если бы мы объявили её void SC::nFn( SC *this , int a ). При вызове nFn( ) неявным первым аргументом ей передаётся адрес s ( вы не можете записать вызов таким образом, поскольку передача адреса объекта — дело компилятора ).
Обращения к другим, нестатическим членам из функции SC::sFn автоматически используют аргумент this как указатель на текущий объект. Однако при вызове статической функции SC::sFn( ) адрес объекта ей не передаётся и указателя this, который можно использовать при обращении к нестатическим членам, не существует. Поэтому мы и говорим, что статическая функция-член не связана с каким-либо текущим объектом.
_________________
230 стр. Часть 3. Введение в классы