С++ для "чайников" .

Дэвис Стефан Рэнди

 

 

  

Глава 11. ЗНАКОМСТВО С ОБЪЕКТНО-ОРИЕНТИРОВАННЫМ ПРОГРАММИРОВАНИЕМ...145

Глава 12. КЛАССЫ В С++...149

Глава 13. РАБОТА С КЛАССАМИ...154

Глава 14. УКАЗАТЕЛИ НА ОБЪЕКТЫ...167

Глава 15. ЗАЩИЩЁННЫЕ ЧЛЕНЫ КЛАССА: НЕ БЕСПОКОИТЬ!...181

Глава 16. СОЗДАНИЕ И УДАЛЕНИЕ ОБЪЕКТОВ...188

Глава 17. АРГУМЕНТАЦИЯ КОНСТРУИРОВАНИЯ...198

Глава 18. КОПИРУЮЩИЙ КОНСТРУКТОР...213

Глава 19. СТАТИЧЕСКИЕ ЧЛЕНЫ...224

        В этой части...

Основным отличием С++ от других языков является возможность объектно-ориентированного программирования. Термин объектно-ориентарованный — один из самых популярных в современном компьютерном мире. Языки программирования, редакторы и базы данных — буквально все претендуют на звание объектно-ориентированных. Иногда так оно и есть, но часто такое определение даётся исключительно в рекламных целях.

На прилагаемом компакт-диске имеется программа BUDGET2, которая поможет вам разобраться в этих объектно-ориентированных концепциях.

 

 

В этой главе...

►Микроволновые печи и уровни абстракции  145

►Классификация микроволновых печей  146

►Зачем нужна классификация  147

Что такое объектно-ориентированное программирование вообще? Объектно-ориентированное программирование, или ООП, базируется на двух принципах, которые вам известны ещё с младенческого возраста: абстракция и классификация. Чтобы пояснить, что имеется в виду, я расскажу вам одну историю.

 

►Микроволновые печи и уровни абстракции...145

 

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

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

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

■■■

■ Не перепрограммировал процессор внутри печи, даже если прошлый раз готовилось абсолютно другое блюдо.

■ Не смотрел внутрь печи.

■ Не задумывался бы над внутренним устройством печи во время приготовления блюд даже в том случае, если бы был главным инженером по производству печей и знал о них всё, включая каждую программу.

■■■

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

_________________

145 стр. . Знакомство с объектно-ориентированным программированием

Во время приготовления блюда я смотрел на микроволновую печь просто как на железный ящик. И пока я управляю печью с помощью интерфейса, я не могу её сломать, "подвесить" или, что ещё хуже, превратить своё блюдо в угли.

 

Приготовление блюд с помощью функций...146

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

Этот рецепт прост и верен. Но с помощью такого алгоритма "функциональный" программист не сможет написать программу приготовления закусок. Программисты, работающие с функциями, живут в мире, лишённом таких объектов, как микроволновая печь и прочие удобства. Они заботятся о последовательности операций в функциях. В "функциональном" решении проблемы закусок управление будет передано от моих пальцев кнопкам передней панели, а затем внутрь печи. После этого программе придётся решать, на какое время включать печь и когда следует включить звуковой сигнал готовности.

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

 

Приготовление "объектно-ориентированных" блюд...146

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

При этом я работаю ( и думаю ) на уровне базовых объектов. Я должен думать о том, как приготовить блюдо, не волнуясь о деталях работы микроволновой печи — над этим уже подумали её создатели ( которым нет дела до моих любимых блюд ).

После создания и проверки всех необходимых объектов можно переключиться на следующий уровень абстракции. Теперь я начинаю думать на уровне процесса приготовления закуски, не отвлекаясь на отдельные куски сыра или банку бобов. При таком подходе я легко переведу рецепт моего сына на язык С++. 

 

►Классификация, микроволновых печей...146

В концепции уровней абстракции очень важной частью является классификация. Если бы я спросил моего сына: "Что такое микроволновая печь?" — он бы наверняка ответил: "Это печь, которая...". Если бы затем я спросил: "А что такое печь?" — он бы ответил что-то вроде: "Ну, это кухонный прибор, который...". ( Если бы я попытался выяснить, что такое кухонный прибор, он наверняка бы спросил, почему я задаю так много дурацких вопросов. )

Из ответов моего сына становится ясно, что он видит нашу печь как один из экземпляров вещей, которые называются микроволновыми печами. Кроме того, печь является подразделом духовок, а духовки относятся к типу кухонных приборов.

_________________

146 стр. . Введение в классы

«В ООП моя микроволновая печь является экземпляром класса микроволновых печей. Класс микроволновых печей является подклассом печей, который, в свою очередь, является подклассом кухонных приборов.»

[]

Люди склонны заниматься классификацией. Всё вокруг увешано ярлыками. Мы делаем всё, для того чтобы уменьшить количество вещей, которые надо запомнить. Вспомните, например, когда вы первый раз увидели "Пежо" или "Рено". Возможно, в рекламе и говорилось, что это суперавтомобиль, но мы-то с вами знаем, что это не так. Это ведь просто машина. Она имеет все свойства, которыми обладает автомобиль. У неё есть руль, колёса, сиденья, мотор, тормоза и т.д. Могу поспорить, что я смог бы даже водить такую штуку без инструкции.

Я не буду тратить место в книге на описание того, чем этот автомобиль похож на другие. Мне нужно знать лишь то, что это "машина, которая...", и то, чем она отличается от других машин ( например, ценой ). Теперь можно двигаться дальше. Легковые машины являются таким же подклассом колёсных транспортных средств, как грузовики и пикапы. При этом колёсные транспортные средства входят в состав транспортных средств наравне с кораблями и самолётами.

 

►Зачем нужна классификация...147

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

Иногда может показаться, что легче разработать и создать микроволновую печь специально для некоторого блюда и не строить универсальный прибор на все случаи жизни. Тогда на лицевую панель не надо будет помещать никаких кнопок, кроме кнопки СТАРТ. Блюдо всегда готовилось бы одинаковое время, и можно было бы избавиться от всех этих бесполезных кнопок типа РАЗМОРОЗКА или ТЕМПЕРАТУРА ПРИГОТОВЛЕНИЯ. Всё, что требовалось бы от такой печи, — это чтобы в неё помещалась одна тарелка с полуфабрикатом. Да, но что же тогда получится? Ведь при этом один кубический метр пространства использовался бы для приготовления всего одной тарелки закуски!

Чтобы сэкономить место, можно освободиться от этой глупой концепции — "микроволновая печь". Для приготовления закуски хватит и внутренностей печи. Тогда в инструкции достаточно написать примерно следующее: "Поместите полуфабрикат в ящик. Соедините красный и чёрный провод. Установите на трубе излучателя напряжение в 3000 вольт. Должен появиться негромкий гул. Постарайтесь не стоять близко к установке, если вы хотите иметь детей". Простая и понятная инструкция!

Но такой функциональный подход создаёт некоторые проблемы.

■■■

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

■ Не гибко. Когда-нибудь мне потребуется поменять свою микроволновую печь на печь другого типа. Я смогу это сделать без проблем, если интерфейс печи можно будет оставить старым. Без чётко очерченных областей действия, а также без разделения интерфейса и внутреннего содержимого становится крайне трудно убрать старый объект и поставить на его место новый.

_________________

147 стр. . Знакомство с объектно-ориентированным программированием

■ Невозможно использовать повторно. Печи делаются для приготовления разных блюд. Мне, например, не хочется создавать новую печь всякий раз, когда требуется приготовить новое блюдо. Если задача уже решена, неплохо использовать её решение и в других программах.

■■■

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

_________________

148 стр. . Введение в классы

 

 

        В этой главе...

►Введение в классы  149

►Формат класса  149

►Обращение к членам класса  150

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

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

 

►Введение в классы...149

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

В С++ структура, которая может объединить несколько разнотипных переменных в одном объекте, называется классом. 

 

►Формат класса...149

Класс, описывающий объект, который содержит имя и номер кредитной карты, может быть создан так:

    /* Класс dataset */

    class NameDataSet

    {

        public :

              char firstName[ 128 ] ;

              char lastName [ 128 ] ;

              int creditCard ;

    } ;

    /* Экземпляр класса dataset */

    NameDataSet nds ;

_________________

149 стр. . Классы в С++

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

После открывающей скобки находится ключевое слово public. ( Не спрашивайте меня сейчас, что оно значит, — я объясню его значение немного позже. В следующих главах поясняются разные ключевые слова, такие как public или private. А до тех пор пока я не сделаю private публичным, значение public останется приватным :-). )

«Можно использовать альтернативное ключевое слово struct , которое полностью идентично class , с предполагаемым использованием объявлений public .»

[]

После ключевого слова public идёт описание полей класса. Как видно из листинга, класс NameDataSet содержит поля имени, фамилии и номера кредитной карты. Первые два поля являются символьными массивами, а третье имеет тип int ( будем считать, что это и есть номер кредитной карты ).

«Объявление класса содержит поля данных, необходимые для описания единого объекта.»

[]

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

Говорят, что nds является экземпляром класса NameDataSet и что мы создали этот экземпляр, реализовав класс NameDataSet. Поля firstName и остальные являются членами, или свойствами класса. 

 

►Обращение к членам класса...150

Обратиться к членам класса можно так:

    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 стр. . Введение в классы

Теперь программа может работать с объектом 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 стр. . Классы в С++

    {

        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. Пока что не обращайте внимания на символ "&" — о нём речь пойдёт в.

Внутри функции getData( ) происходит считывание строки из устройства стандартного ввода с последующей его записью в член firstName. Если stricmp( ) находит, что введённая строка — "exit", функция getData( ) возвращает false функции main( ), сигнализируя, что пора выходить из цикла ввода информации. ( Функция stricmp( ) сравнивает строки, не обращая внимания на регистр. Строки "EXIT", "exit" и другие считаются идентичными. ) Если введена строка, отличная от "exit", функция считывает из стандартного ввода фамилию и номер кредитной карты и записывает их в объект nds. Функция displayData( ) выводит на экран все члены объекта nds. Результат работы этой программы выглядит следующим образом.

    Считываем информацию о пользователе

    Введите 'exit' для выхода из программы

    Введите имя: Stephen

    Введите фамилию: Davis

    Введите номер кредитной карты: 123456

_________________

152 стр. . Введение в классы

    Введите имя: Marshall

    Введите фамилию: Smith

    Введите номер кредитной карты: 567890

    Введите имя: exit

    Записи:

    Stephen Davis/123456

    Marshall Smith/567890

    Для продолжения нажмите любую клавишу...

Вывод программы начинается с пояснения, как с ней работать. В первой строке я ввёл своё имя ( видите, какой я скромный! ). Поскольку меня не зовут exit, программа продолжает выполнение. Далее я ввёл свою фамилию и номер кредитной карты. Следующим элементом массива я ввёл имя Marshall Smith и номер его кредитной карты. Затем я ввёл строку exit и таким образом прервал цикл заполнения объектов. Как видите, эта программа не делает ничего, кроме вывода только что введённой информации.

_________________

153 стр. . Классы в С++

 

 

 В этой главе...

►Активизация объектов  154

►Добавление функции-члена  156

►Вызов функций-членов  157

►Разрешение области видимости  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. Эти переменные различаются между собой так же, как баланс моего банковского счёта отличается от вашего ( хотя они оба называются балансами ). 

 

►Активизация объектов...154

 

Классы используются для моделирования реально существующих объектов. Чем ближе объекты С++ к реальному миру, тем проще с ними работать в программах. На словах это звучит довольно просто, однако существующий сейчас класс Savings не предпринимает ничего, чтобы хоть в чём-то походить на настоящий банковский счёт.

_________________

154 стр. . Введение в классы

 

Моделирование реальных объектов...155

Реальные объекты имеют свойства-данные, например номера счетов и балансы. Но кроме этого, реальные объекты могут выполнять действия: микроволновые печи готовят, сберегательный счёт начисляет проценты, полицейский выписывает штраф и т.д.

Функционально ориентированные программы выполняют все необходимые действия с помощью функций. Программа на С++ может вызвать функцию strcmp( ) для сравнения двух строк или функцию getLine( ) для ввода строки. В , будет показано, что даже операторы работы с потоками ввода-вывода ( 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( ). Функции, определённые в классе, называются функциями-членами. 

 

Зачем нужны функции-члены...155

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

    class Savings

    {

    public :

        unsigned accountNumber ;

        float balance ;

    } ;

    unsigned deposit( Savings& s , unsigned amount )

    {

        s.balance += amount ;

        return s.balance ;

    }

Ещё раз напомню: пока что не обращайте внимания на символ "&" — его смысл станет понятен позже.

В этом фрагменте deposit( ) является функцией "вклада на счёт". Эта функция поддержки реализована в виде внешней функции, которая выполняет необходимые действия с экземпляром класса Savings. Конечно, такой подход имеет право на существование, но он нарушает наши правила объектно-ориентированного программирования.

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

_________________

155 стр. . Работа с классами

Когда я делаю закуску, я не должен начинать приготовление с подключения внутренних элементов микроволновой печи. И я хочу, чтобы мои классы работали так же, т.е. чтобы они без всякого внешнего вмешательства знали, как управлять своими "внутренними органами". Конечно, такие функции-члены класса Savings, как deposit( ), могут быть реализованы и в виде внешних функций. Можно даже расположить все функции, необходимые для работы со счетами, в одном месте файла. Микроволновую печь можно заставить работать, соединив необходимые провода внутри неё, но я не хочу, чтобы мои классы ( или моя микроволновая печь ) работали таким образом. Я хочу иметь класс Savings, который буду использовать в своей банковской программе, не задумываясь над тем, какова его рабочая "кухня".

 

►Добавление функции-члена...156

 

Эта процедура включает два аспекта: создание функции-члена и её именование ( звучит довольно глупо, не правда ли? ).

 

Создание функции-члена...156

Чтобы продемонстрировать работу с функциями-членами, начнём с определения класса 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 стр. . Введение в классы

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

[]

 

Именование членов класса...157

Функция-член во многом похожа на члена семьи. Полное имя нашей функции 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 ) .»

[]

 

►Вызов функций-членов...157

 

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

    class Student

    {

    public :

        int semesterHours ;

        float gpa ;

    } ;

    Student s ;

    void fn( void )

    {

        /* Обращение к данным-членам объекта s */

        s.semesterHours = 10 ;

        s.gpa = 3.0 ;

    }

_________________

157 стр. . Работа с классами

Обратите внимание, что наряду с именем переменной необходимо указать имя объекта. Другими словами, приведённый ниже фрагмент программы не имеет смысла.

    Student s ;

    void fn( void )

    {

        /* Этот пример ошибочен */

        semesterHours = 10 ;

            /* Член какого объекта и какого класса? */

        Student::semesterHours = 10 ;

            /* Теперь ясно, какого класса, однако до сих пор не ясно, какого объекта */

    }

 

Обращение к функциям-членам...158

Формально между данными-членами и функциями-членами нет никакого различия. Следующая программа показывает, как можно использовать функцию-член 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 стр. . Введение в классы

    {

        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 представляет собой студента, к записи которого добавляется новый курс. Вы не можете получить информацию о студенте или изменить её, не указав, о каком конкретно студенте идёт речь.

Вызов функции-члена без указания имени объекта имеет не больше смысла, чем обращение к данным-членам без указания объекта.

 

Доступ к членам из функции-члена...159

Я так и слышу, как вы повторяете про себя: "Нельзя обратиться к функции-члену без указания имени объекта! Нельзя обратиться к функции-члену без указания имени объекта! Нельзя..." Запомнив это, вы смотрите на тело функции-члена 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 стр. . Работа с классами

Когда addCourse( ) вызывается для объекта s, все сокращённые имена в теле этой функции считаются членами объекта s. Таким образом, обращение к переменной semesterHours внутри функции s.addCourse( ) в действительности является обращением к переменной s.semesterHours, а обращение к gpa — обращением к s.gpa. В следующей строке функции main( ), когда addCourse( ) вызывается для объекта t того же класса Student, происходит обращение к членам класса t.semesterHours и t.gpa.

«Объект, для которого вызывается функция-член, называется "текущим", и все имена членов, записанные в сокращённом виде внутри функции-члена, считаются членами текущего объекта. Другими словами, сокращённое обращение к членам класса интерпретируется как обращение к членам текущего объекта.»

[]

 

Именование текущего объекта...160

«Как функция-член определяет, какой объект является текущим? Это не магия и не шаманство — просто адрес этого объекта всегда передаётся функции-члену как скрытый первый аргумент. Другими словами, при вызове функции-члена происходит преобразование такого вида:

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 стр. . Введение в классы

 

►Разрешение области видимости...161

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

С помощью оператора :: можно также описать функцию — не член, использовав для этого пустое имя класса. В этом случае функция 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 стр. . Работа с классами

    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 ;

        }

    } ;

Это похоже на то, как если бы я звал Стефана в собственном доме. Все решили бы, что я зову самого себя: ведь в моём доме, естественно, подразумевается фамилия Дэвис. Если же я имею в виду какого-то другого Стефана, то должен сказать "Стефан Спупендайк" или "Стефан Мак-Суини" либо использовать какую-нибудь другую фамилию. Так же действует и оператор разрешения области видимости.

«Расширенное имя функции включает в себя её аргументы. Теперь же мы добавляем к полному имени ещё и имя класса, к которому принадлежит функция.»

 []

 

►Определение функции-члена...162

Функция-член может быть определена как внутри класса, так и отдельно от него. Когда функция определяется внутри класса, это выглядит так, как в приведённом далее файле Savings.h:

    /* Savings — определение класса с возможностью делать вклады */

    class Savings

    {

        public :

            /* Объявляем и определяем функции-члены */

            float deposit( float amount )

            {

                balance += amount ;

                return balance ;

            }

            unsigned int accountNumber ;

            float balance ;

    } ;

Использование такого заголовочного файла проще простого — его надо включить в программу и пользоваться определённым в нём классом, как вам заблагорассудится, например, как в приведённой далее программе SavingsClassInline.

_________________

162 стр. . Введение в классы

    /* 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 стр. . Работа с классами

 

►Определение функций-членов вне класса...164

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

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

    /* 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 стр. . Введение в классы

         /* Добавляем немного на счёт... */

        cout << "Вкладываем на счёт 10 монет"

                << s.accountNumber << endl ;

        s.deposit( 10 ) ;

        cout << "Состояние счёта "

                << s.balance << endl ;

        /* Пауза для того, чтобы посмотреть на результат работы программы */

        system( "PAUSE" ) ; return 0 ;

    }

Определение класса содержит только прототип функции deposit( ), а её тело определено в другом месте. Такое объявление аналогично объявлению любого другого прототипа.

Обратите внимание, что при определении функции-члена deposit( ) потребовалось указание её полного имени

    float Savings::deposit( float amount )

; сокращённого имени при определении вне класса недостаточно.

 

►Перегрузка функций-членов...165

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

    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 стр. . Работа с классами

Полные имена вызываемых из main( ) функций указаны в комментариях.

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

В приведённом примере первые два вызова обращаются к функциям-членам Student::grade( float ) и Student::grade( ) соответственно. Эти функции отличаются списками аргументов. Вызов функции s.grade( ) обращается к Student::grade( ), поскольку тип объекта s — Student.

Третья вызываемая функция в данном примере — функция ::grade( float ), не имеющая вызывающего объекта. Последний вызов осуществляется объектом типа Slope, и соответственно вызывается функция-член Slope::grade( float ).

_________________

166 стр. . Введение в классы

 

 

ОГЛАВЛЕНИЕ

        В этой главе...

►Определение массивов и указателей  167

►Объявление массивов объектов  168

►Объявление указателей на объекты  169

►Передача объектов функциям  171

►Зачем использовать указатели и ссылки  174

►Возврат к куче  175

►Сравнение указателей и ссылок  175

►Почему ссылки не используются вместо указателей  175

►Использование связанных списков  176

►Списки в стандартной библиотеке  180

Программисты на С++ всё время создают массивы чего-либо. Формируются массивы целочисленных значений, массивы действительных значений; так почему бы не создать массив студентов? Студенты всё время находятся в списках ( причём гораздо чаще, чем им хотелось бы ). Концепция объектов Student, стройными рядами ожидающих своей очереди, слишком привлекательна, чтобы можно было пройти мимо неё.

 

►Определение массивов и указателей...167

Массив является последовательностью идентичных объектов и очень похож на улицу с одинаковыми домами. Каждый элемент массива имеет индекс, который соответствует порядковому номеру элемента от начала массива. При этом первый элемент имеет нулевое смещение от начала массива, а значит, имеет индекс 0.

Массивы в С++ объявляются с помощью квадратных скобок, в которых указывается количество элементов в массиве.

    int array[ 10 ] ; /* Объявление массива из 10 элементов */

К отдельному элементу массива можно обратиться, подсчитав смещение от начала массива:

    array[ 0 ] = 10 ; /* Присвоить 10 первому элементу */

    array[ 9 ] = 20 ; /* Присвоить 20 последнему элементу */

В этом фрагменте первому элементу массива ( элементу под номером 0 ) присваивается значение 10 , а последнему — 20.

«Не забывайте, что в С++ массив начинается элементом с индексом 0 и заканчивается элементом, имеющим индекс, равный длине массива минус 1.»

[]

_________________

167 стр. . Указатели на объекты

Если продолжить аналогию с домами, получится, что имя массива — это название улицы, а номер дома равнозначен номеру элемента в массиве. Таким же образом можно отождествить переменные с их адресом в памяти компьютера. Эти адреса могут быть определены и сохранены для последующего использования.

    /* Объявление целочисленной переменной */

    int variable ;

    /* Сохранить её адрес в pVariable */

    int* pVariable = &variable

    /* Присвоить 10  целочисленной переменной,  на которую указывает pVariable */

    *pVariable = 10 ;

Указатель pVariable был объявлен для того, чтобы хранить в нём адрес переменной variable. После этого целочисленной переменной, находящейся по адресу pVariable, присваивается значение 10.

Использовав аналогию с домами в последний раз ( честное слово, в последний! ), мы получим:

■■■

■ variable — это дом;

■ pVariable — это листок с адресом дома;

■ в последней строке примера отправляется сообщение, содержащее 10 , по адресу, который находится на листке бумаги. Всё почти так же, как на почте ( единственное отличие состоит в том, что компьютер не ошибается адресом ).

■■■

В , описаны основы работы с массивами простых ( встроенных ) типов, а в , и , подробно рассматриваются указатели.

 

►Объявление массивов объектов...168

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

      /* 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 стр. . Введение в классы

В данном фрагменте s является массивом объектов типа Student. Запись s[ 4 ] означает пятый элемент массива, а значит, s[ 4 ].gpa является усреднённой оценкой пятого студента. В следующей строке с помощью функции s[ 4 ].addCourse( ) пятому студенту добавляется ещё один прослушанный и несданный курс. 

 

►Объявление указателей на объекты...169 

 

Указатели на объекты работают так же, как и указатели на простые типы.

    /* 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 я объясню немного позже в этой главе.

 

Разыменование указателей на объекты...169

По аналогии с указателями на простые переменные можно решить, что в приведённом ниже примере происходит обращение к усреднённой оценке студента s.

_________________

169 стр. . Указатели на объекты

    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 обращается к члену этого объекта. 

 

Использование стрелок...170

Использование для разыменования указателей на объекты оператора * со скобками будет прекрасно работать. Однако даже самые твёрдолобые программисты скажут вам, что такой синтаксис разыменования очень неудобен.

Для доступа к членам объекта С++ предоставляет более удобный оператор  -> , позволяющий избежать неуклюжей конструкции со скобками и оператором * ; таким образом, 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 стр. . Введение в классы

 

►Передача объектов функциям...171

 

Передача указателей функциям — один из способов выразить себя в области указателей. 

 

Вызов функции с передачей объекта по значению...171

Как вы знаете, С++ передаёт аргументы в функцию по ссылке при использовании в описании символа & ( см. ). Однако по умолчанию С++ передаёт функции только значения аргументов. ( Обратитесь к , если вы этого не знали. ) То же касается и составных, определённых пользователем объектов: они также передаются по значению.

    /* 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 стр. . Указатели на объекты

В этом примере функция 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...

 

Вызов функции с передачей указателя...172

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

    /* 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 стр. . Введение в классы

В этом примере аргумент, передаваемый в 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...

 

Передача объекта no ссылке...173

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

    /* 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 стр. . Указатели на объекты

            /* Вывод значения 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 . С++ самостоятельно отслеживает адрес ссылки, в то время как при передаче указателя вы должны заниматься этим сами.»  

[]

 

►Зачем, использовать указатели и ссылки...174

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

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

А вот и вторая причина — некоторые объекты могут оказаться действительно очень большими. Передача такого объекта по значению приводит к копированию большого объёма информации в память функции.

«Область, используемая для передачи аргументов функции, называется стеком вызова.»

[]

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

«Проблема на самом деле ещё сложнее, чем описано здесь. В главе 18, "Копирующий конструктор" , вы убедитесь, что создание копии объекта представляет собой существенно более сложную задачу, чем простое копирование участка памяти из одного места в другое.»

[]

_________________

174 стр. . Введение в классы

 

►Возврат к куче...175

Проблемы, возникающие при работе с указателями на простые переменные, распространяются и на указатели на объекты. В частности, необходимо гарантировать, что указатель ссылается на существующий корректный объект. Так, нельзя возвращать указатель на локально определённый объект, как это сделано в данном примере:

    MyClass* myFunc( )

    {

        /* Эта функция не будет работать правильно */

        MyClass mc ;

        MyClass* рМС = &mc ;

        return рМС ;

    }

После возврата из myFunc( ) объект mc выходит из области видимости, а значит, указатель, который возвращает myFunc( ), указывает на несуществующий объект.

«Проблемы, связанные с возвратом памяти, которая выходит из области видимости, рассматривались в главе 9, "Второе знакомство с указателями" .»

[]

Использование кучи позволяет решить эту проблему:

    MyClass* myFunc( )

    {

        MyClass* рМС = new MyClass ;

        return рМС ;

    }

«С помощью кучи можно выделять память для объектов в самых разнообразных ситуациях.»

[] 

 

►Сравнение указателей и ссылок...175

Очень часто новички в программировании спрашивают, зачем нужны и указатели, и ссылки, и нельзя ли обойтись чем-то одним?

«В принципе, можно обойтись чем-то одним. Тот же С#, да и многие другие языки обходятся без указателей. Однако С++ — язык крайне широкого применения, и имеется множество задач, решение которых существенно упрощается при использовании указателей. Указатели — неотъемлемая часть стандартного, не ограниченного узкими рамками Visual Studio .NET языка программирования С++.»

[] 

 

►Почему ссылки не используются вместо указателей...175

Синтаксис работы со ссылками аналогичен синтаксису, используемому при работе с обычными объектами. Так почему бы не перейти на использование только ссылок и никогда не использовать указатели?

_________________

175 стр. . Указатели на объекты

Объекты и их адреса — это "две большие разницы", и зачастую синтаксис для ссылок оказывается более сложным, чем синтаксис при работе с указателями. Рассмотрим следующий пример.

    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 так, чтобы он мог указать своего лучшего друга. Для этого я пытаюсь воспользоваться ссылочной переменной. В функции main( ) я создаю двух студентов и пытаюсь сделать одного из них другом другого.

Первое присвоение копирует объект в тело другого объекта, так что принимающий объект просто содержит копию. Второе присвоение не будет работать, так как С++ не в состоянии отличить присвоение ссылке от присвоения самому объекту, так что корректно работать будет только третье присвоение, приводя к желаемому результату. 

 

►Использование связанных списков...176

 

Связанный список является второй по распространённости структурой после массива. Каждый объект в связанном списке указывает на следующий, образуя цепочку в памяти.

___________

14Это сделано некорректно; как минимум член valFriend не может быть определён в классе того же типа, не считая массы других ошибок. Поэтому к данному примеру следует относиться как к не более чем поясняющей сугубо теоретической модели, которая никогда не будет даже скомпилирована. — Прим. ред.

_________________

176 стр. . Введение в классы

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

Не всякий класс может быть использован для создания связанного списка. Связываемый класс объявляется так, как показано в приведённом ниже фрагменте.

    class LinkableClass

    {

        public :

            LinkableClass* pNext ;

            /* Прочие члены класса */

    } ;

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

Указатель pNext и есть тот элемент, с помощью которого дети объединяются в цепочки. Фигурально выражаясь, можно сказать, что список детей состоит из некоторого количества объектов, каждый из которых имеет тип "ребёнок". Каждый ребёнок указывает на следующего ребенка.

Головной указатель является указателем типа LinkableClass*, и если использовать аналогию с цепочкой детей, держащихся за руки, то можно сказать, что учитель указывает на объект класса "ребёнок" ( любопытно отметить, что сам учитель не является ребёнком — головной указатель не обязательно должен иметь тип LinkableClass ).

«Не забывайте инициализировать указатели значением 0. Указатель, содержащий нуль, так и называется — нулевым. Обычно попытка обращения по адресу 0 вызывает аварийную остановку программы.»

[]

«Преобразование целочисленного нуля в тип LinkableClass* не обязательно. С++ воспринимает 0 как значение любого типа ( в частности, как "универсальный указатель" ).»

[]

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

    void addHead( LinkableClass* pLC )

    {

        pLC -> pNext = pHead

        pHead = pLC ;

    }

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

 

Другие операции над связанным списком...177

Добавление объекта в начало списка — самая простая операция со связанным списком. Хорошее представление о работе связанного списка даёт процедура прохода по нему до конца списка.

_________________

177 стр. . Указатели на объекты

    /* Проход по связанному списку */

    LinkableClass* pL = pHead ;

    while ( pL )

    {

        /* Выполнение некоторых операций */

        /* Переход к следующему элементу */

        pL = pL -> pNext ;

    }

Сначала указатель pL инициализируется адресом первого объекта в списке ( который хранится в переменной pHead ). Затем программа входит в цикл while. Если указатель pL не нулевой, он указывает на некоторый объект LinkableClass. В этом цикле программа может выполнить те или иные действия над объектом, после чего присвоение pL = pL -> pNext "перемещает" указатель к следующему объекту списка. Если указатель становится нулевым, список исчерпан.

 

Программа LinkedListData...178

Программа 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 стр. . Введение в классы

        // Читаем имя

        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 стр. . Указатели на объекты

Если от пользователя больше не поступает элементов NameDataSet, функция main( ) выводит на экран все элементы списка, используя функцию displayData( ).

Функция getData( ) выделяет из кучи пустой объект класса NameDataSet. После этого getData( ) ожидает ввода имени для записи его в соответствующее поле нового объекта. Если пользователь вводит в поле имени строку "exit", функция уничтожает последний созданный объект и возвращает 0. В противном случае getData( ) считывает фамилию и номер социального страхования, после чего обнуляет указатель pNext и передаёт управление вызывающей функции.

«Никогда не оставляйте связывающие указатели не проинициализированными! Старая поговорка программистов гласит: "Не уверен — обнули".»

[]

Функция getData( ) возвращает адрес объекта.

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

«Вывод программы представляет собой введённые имена в обратном порядке. Это происходит потому, что добавление элементов выполняется в начало списка. Возможна вставка элементов в конец списка, однако эта задача посложнее.»  

[]

 

►Списки в стандартной библиотеке...180

Точно так, как ребёнок должен научиться ходить перед тем как ездить на автомобиле, считать перед тем как использовать калькулятор, программисту необходимо научиться писать программы, работающие со связанными списками, перед тем как использовать классы списков, написанные другими. В , будут описаны классы-контейнеры, предоставляемые в распоряжение программиста средами С++, среди которых есть и класс, представляющий собой связанный список.

_________________

180 стр. . Введение в классы

 

 

        В этой главе...

►Защищённые члены  181

►Чем хороши защищённые члены  183

►Обращение к защищённым членам  184

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

 

►Защищённые члены...181

 

Члены класса могут быть помечены как защищённые, что делает их недоступными извне класса. В отличие от защищённых, открытые ( public ) члены класса доступны для всех.

«Термин "недоступные" не следует понимать буквально. Любой программист может немного повозиться с исходным текстом и убрать ключевое слово protected . Возможность сделать член защищённым разработана для того, чтобы защитить программиста от обращения к члену просто по невнимательности.»

[]

 

Зачем нужны защищённые члены...181

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

■■■

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

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

_________________

181 стр. . Защищённые члены класса: не беспокоить!

■ Сокращение до минимума внешнего интерфейса класса. Гораздо проще изучать и использовать класс, который имеет ограниченный интерфейс ( а интерфейсом класса являются его открытые члены ). Защищённые члены скрыты от пользователя, и их не надо помнить ( в противном случае интерфейсом становится весь класс ). Такой подход называется абстракцией, которая описана в главе 11, "Знакомство с объектно-ориентированным программированием" .

■ Уменьшение уровня взаимосвязи между классом и внешней программой. Ограничив взаимосвязь класса с внешним кодом, при необходимости гораздо проще заменить класс каким-либо другим.

■■■

Я так и слышу, как поклонники функционального подхода говорят: "Не нужно делать ничего противоестественного! Достаточно потребовать от программиста, чтобы он попросту не трогал некоторые члены класса". 

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

 

Как устроены защищённые члены...182

Добавление в класс ключевого слова 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 стр. . Введение в классы

Теперь члены 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 станет ясной при изучении наследования.»

[]

 

►Чем хороши защищённые члены...183

 

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

 

Защита внутреннего устройства класса...183 

Ключевое слово 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 стр. . Защищённые члены класса: не беспокоить!

Добавление новой функции grade( float ) позволяет внешним приложениям изменять содержимое gpa. Заметьте, что класс всё равно не позволяет внешним функциям полностью контролировать содержимое своих защищённых членов. Внешнее приложение не может присвоить gpa любое значение, а только то, которое лежит в диапазоне между 0 и 4.0.

Теперь класс Student обеспечивает внешний доступ к своим внутренним членам, одновременно не позволяя присвоить им недопустимое значение.

 

Классы с ограниченным интерфейсом...184

Теперь наш класс предоставляет ограниченный интерфейс. Чтобы использовать класс, достаточно знать, каковы его открытые члены, что они делают и какие аргументы принимают. Это значительно уменьшает объём информации, которую необходимо помнить для работы с классом.

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

Ещё одна причина — едва ли не самая важная — в ограниченности человеческих возможностей удержать в голове большое количество объектов и связей между ними. Использование строго ограниченного интерфейса класса позволяет программисту отвлечься от деталей реализации, скрытых за этим интерфейсом. Соответственно, разработчик класса может не думать о том, как именно будет использоваться интерфейс разрабатываемого им класса.

 

►Обращение к защищённым члена...184

 

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

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

Это похоже на то, как вы порой оставляете соседям ключ от своего дома на время отпуска, чтобы они иногда проверяли его. Давать ключи не членам семьи не совсем хорошо, однако это куда лучше, чем оставлять дом открытым.

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

_________________

184 стр. . Введение в классы

    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 стр. . Защищённые члены класса: не беспокоить!

В данном примере функция 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 стр. . Введение в классы

    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 стр. . Защищённые члены класса: не беспокоить!

 

 

        В этой главе...

►Создание объектов 188

►Использование конструкторов 189

►Что такое деструктор  194

Объекты в программе создаются и уничтожаются так же, как и объекты реального мира. Если класс сам отвечает за своё существование, он должен обладать возможностью управления процессом уничтожения и создания объектов. Программистам на С++ повезло, поскольку С++ предоставляет необходимый для этого механизм ( хотя, скорее всего, это не удача, а результат разумного планирования языка ). Прежде чем начинать создавать и уничтожать объекты в программе, обсудим, что значит "создавать объекты".

 

►Создание объектов...188

Некоторые подчас теряются в терминах класс и объект. В чём разница между этими терминами? Как они связаны?

Я могу создать класс Dog, который будет описывать соответствующие свойства лучшего друга человека. К примеру, у меня есть две собаки. Это значит, что мой класс Dog содержит два экземпляра — Труди и Скутер ( надеюсь, что два: Скутера я не видел уже несколько дней... ).

«Класс описывает тип предмета, а объект — это экземпляр класса. Dog является классом, а Труди и Скутер — объектами. Каждая собака представляет собой отдельный объект, но существует только один класс Dog , при этом не имеет значения, сколько у меня собак.»

[]

Объекты могут создаваться и уничтожаться, а классы попросту существуют. Мои собаки Труди и Скутер приходят и уходят, а класс Dog ( оставим эволюцию в стороне ) вечен.

Различные типы объектов создаются в разное время. Когда программа начинает выполняться, создаются глобальные объекты. Локальные объекты создаются, когда программа сталкивается с их объявлением.

«Глобальный объект является объектом, объявленным вне каких-либо функций. Локальный объект объявляется внутри функции, а следовательно, является локальным для функции. В приведённом ниже примере переменная me является глобальной, а переменная noМе — локальной по отношению к pickOne( ) .»

 []

    int me = 0 ;

    void pickOne( )

    {

        int noMe ;

     }

_________________

188 стр. . Введение в классы

«Согласно правилам языка глобальные объекты по умолчанию инициализируются нулевыми значениями. Локальные объекты, т.е. объекты, объявленные внутри функций, не имеют инициализирующих значений. Такой подход, вообще говоря, для классов неприемлем.»

[]

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

 

►Использование конструкторов...189

 

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

 

Зачем нужны конструкторы...189

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

    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 стр. . Создание и удаление объектов

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

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

 

Работа с конструкторами...190

 

Конструктор — это специальная функция-член, которая автоматически вызывается во время создания объекта. Конструктор должен иметь то же имя, что и класс. Таким образом компилятор сможет определить, что именно эта функция-член является конструктором. Конечно, создатели С++ могли сформулировать это правило как угодно, например, так: "Конструктором является функция с именем init( )". Как именно определено правило, не имеет значения; главное — чтобы конструктор мог быть распознан компилятором. Ещё одним свойством конструктора является то, что он не возвращает никакого значения, поскольку вызывается автоматически ( если бы конструктор и возвращал значение, его всё равно некуда было бы записать ).

 

Конструирование одного объекта...190

Класс с использованием конструктора продемонстрирован в следующем примере.

    //

    /* 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 стр. . Введение в классы

В этом примере компилятор сам вызывает конструктор 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 стр. . Создание и удаление объектов

 

Конструирование нескольких объектов...192

Каждый элемент массива конструируется отдельно. Внесём в программу 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

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

_________________

192 стр. . Введение в классы

    //

    /* 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 стр. . Создание и удаление объектов

В результате работы этой программы на экран будут выведены следующие сообщения:

 

    Создаём объект 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

 

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

 

Зачем нужен деструктор...194

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

 

Работа с деструкторами...194

Деструктор имеет то же имя, что и класс, но только с предшествующим ему символом тильды ( ~ ) ( С++ последователен и здесь: ведь символ тильды не что иное, как символ оператора "нет", т.е. деструктор — это отрицание конструктора ).

_________________

194 стр. . Введение в классы

 Как и конструктор, деструктор не имеет типа возвращаемого значения. С учётом сказанного деструктор класса 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 стр. . Создание и удаление объектов

            }

            ~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 стр. . Введение в классы

Функция 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 стр. . Создание и удаление объектов

 

 

        В этой главе...

►Как снабдить конструктор аргументами  198

►Перегрузка конструктора  200

►Определение конструкторов по умолчанию  203

►Конструирование членов класса  204

►Управление последовательностью конструирования  208

Класс представляет тип объекта в реальном мире. Например, мы использовали класс Student для представления студента и его свойств. Точно так же, как и студенты, классы считают себя абсолютно самостоятельными. Однако, в отличие от студентов, класс действительно сам "ухаживает" за собой — он должен всё время поддерживать себя в приемлемом состоянии.

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

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

 

►Как снабдить конструктор аргументами...198

 

С++ позволяет программисту определить конструктор с аргументами, например:

    class Student

    {

    public :

        Student( char *pName ) ;

        /* Продолжение класса Student */

    } ;

 

Зачем конструкторам нужны аргументы...198

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

_________________ 

198 стр. . Введение в классы

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

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

 

Как использовать конструктор с аргументами...199

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

При этом нельзя забывать, что вы вызываете конструктор не как нормальную функцию и передать ему аргумент можно только в момент создания объекта. Так, приведённая ниже программа создаёт объект 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 стр. . Аргументация конструирования

В этом примере конструктор выглядит почти так же, как и конструктор из , с тем лишь отличием, что он принимает аргумент pName, имеющий тип char*. Этот конструктор инициализирует все данные-члены нулевыми значениями, за исключением члена name, который инициализируется строкой pName.

Объект s создаётся в функции main( ). Аргумент, передаваемый конструктору, находится в строке объявления s сразу же за именем объекта. Благодаря такому объявлению студент s получил имя Danny. Закрывающая фигурная скобка функции main( ) вызывает гром и молнию деструктора на голову несчастного Danny. Аналогично создаётся объект в куче. 

 

«Многие конструкторы в этой главе нарушают правило "функции размером больше трёх строк не должны быть inline -функциями". Я просто решил облегчить вам чтение ( а теперь — ваши аплодисменты! ).» 

[]

 

►Перегрузка конструктора...200

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

«Словосочетание "перегруженная функция" означает, что определено несколько функций с одинаковым именем, но разными типами аргументов. Если вы немного подзабыли этот термин, освежите память, обратившись к главе 6, "Создание функций" .»

[]

С++ выбирает вызываемый конструктор, исходя из аргументов, передаваемых при объявлении объекта. Например, класс Student может одновременно иметь три конструктора, что продемонстрировано в следующем примере:

    /* OverloadConstructor — несколько способов */

    /*                          создать объект путём */

    /*                    перегрузки конструктора */

    #include

    #include

    #include

    #include

    using namespace std ;

    const int MAXNAMESIZE = 40 ;

    class Student

    {

      public :

        Student( )

_________________

200 стр. . Введение в классы

        {

            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 стр. . Аргументация конструирования

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

    /* 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 стр. . Введение в классы

«В ранних версиях С++ вы не смогли бы создать конструктор по умолчанию, предусмотрев значения по умолчанию для всех аргументов. Конструктор по умолчанию должен был быть определён явно. Так что будьте готовы к тому, что некоторые старые версии компиляторов могут потребовать явного определения конструктора по умолчанию.»

[]

 

►Определение конструкторов по умолчанию...203

Стоит отметить, что в С++ каждый класс должен иметь свой конструктор. Казалось бы, С++ должен генерировать сообщение об ошибке в случае, когда класс не оснащён конструктором, однако этого не происходит. Дело в том, что для обеспечения совместимости с существующим кодом С, который ничего не знает о конструкторах, С++ автоматически создаёт конструктор по умолчанию ( так сказать, умалчиваемый конструктор по умолчанию ), который инициализирует все данные-члены объекта нулями.

Если ваш класс имеет конструктор, С++ не будет автоматически его создавать ( как только С++ убеждается в том, что это не программа на С, он снимает с себя всю ответственность по обеспечению совместимости ).

 

«Вывод: если вы определили конструктор для вашего класса и при этом хотите, чтобы класс имел конструктор по умолчанию, то должны явно определить такой конструктор сами.»

[]

Приведённый ниже фрагмент демонстрирует сказанное. Этот пример вполне корректен.

    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 стр. . Аргументация конструирования

Не попадитесь в ловушку

♦♦♦♦♦

Ещё раз взгляните на объявление объектов класса 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 при объявлении функции, но тогда нарушалось бы условие совместимости с существующими программами на С... ).

♦♦♦♦♦

 

►Конструирование членов класса...204

 

В предыдущих примерах использовались данные-члены простых типов, такие как 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 стр. . Введение в классы

        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 стр. . Аргументация конструирования

Если программист не обеспечит свой класс конструктором, то конструктор по умолчанию, созданный С++, вызовет конструкторы всех данных-членов для их инициализации. То же касается и уничтожения объекта. Деструктор класса автоматически вызывает деструкторы всех данных-членов ( у которых они определены ).

Теперь мы знаем, что будет с конструктором по умолчанию. Но что, если мы захотим вызвать другой конструктор? Куда в этом случае нужно поместить объект? Я имею в виду следующее: представьте себе, что вместо автоматической генерации идентификатора студента, необходимо передать его конструктору 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 стр. . Введение в классы

Объяснить такое странное поведение программы довольно просто. Член 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 стр. . Аргументация конструирования

Обратите особое внимание на первую строку конструктора. В этой строке есть кое-что, с чем вы до этого не встречались. Следующая за двоеточием команда вызывает конструкторы членов данного класса. Компилятор С++ прочтёт эту строку так: "Вызвать конструктор для члена id с аргументом ssId. Все остальные данные-члены, не вызванные явно, строить с использованием конструктора по умолчанию".

Результат работы этой программы свидетельствует, что всё получилось так, как мы и хотели.

 

    Присвоение id 1234

    Конструктор Student( Chester )

    Сообщение из функции main

    Press any key to continue...

    Деструктор id 1234

 

Конструкторы константных членов...208

Ещё одна проблема возникает при инициализации членов, объявленных как const. Вспомним, что переменная, объявленная как const, инициализируется при объявлении и после этого не может быть изменена. Каким же образом конструктор может присвоить значение константному члену? Проблема решается путём использования синтаксиса с двоеточием:

    class Mammal

    {

        public :

            Mammal( int nof ) : numberOfFeet( nof )

            { }

        protected :

            const int numberOfFeet ;

    } ;

Объект класса Mammal ( млекопитающее ) имеет постоянное количество ног ( ампутации не рассматриваются ). Таким образом, это количество следует объявить как const. Значение данному члену присваивается при создании объекта, и после этого не может быть изменено.

«Программисты часто используют синтаксис с двоеточием для инициализации не только константных, но и других членов-данных. Это не обязательно, но часто используется на практике.»

[]

 

►Управление последовательностью конструирования...208

 

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

_________________

208 стр. . Введение в классы

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

■■■

■ Локальные и статические объекты создаются в том порядке, в котором они объявлены в программе.

■ Статические объекты создаются только один раз.

■ Все глобальные объекты создаются до вызова функции main( ) .

■ Нет какого-либо определённого порядка создания глобальных объектов.

■ Члены создаются в том порядке, в котором они объявлены внутри класса.

■ Деструкторы вызываются в порядке, обратном порядку вызова конструкторов.

■■■

«Статическая переменная — это переменная, которая является локальной по отношению к функции, но при этом сохраняет своё значение между вызовами функции. Глобальная переменная — это переменная, объявленная вне какой-либо функции.»

[]

Рассмотрим каждое из приведённых выше правил.

 

Локальные объекты создаются последовательно...209

Локальные объекты создаются в том порядке, в котором в программе встречаются их объявления. Обычно это порядок появления кода объявлений в функции. ( Если, конечно, в функции нет безусловных переходов, "перепрыгивающих" через объявления. Кстати говоря, безусловные переходы между объявлениями лучше не использовать — это затрудняет чтение и компиляцию программы. )

 

Статические объекты создаются один раз...209

Статические переменные подобны обычным локальным переменным с тем отличием, что они создаются только один раз. Это очевидно, поскольку статические переменные сохраняют своё значение от вызова к вызову функции. В отличие от С, который может инициализировать статическую переменную в начале программы, С++ дождётся, когда управление перейдёт к строке с объявлением статической переменной, и только тогда начнёт её создание. Разберёмся в приведённой ниже простой программе.

    /* ConstructStatic — демонстрация однократного */

    /*                создания статических объектов */

    #include

    #include

    #include

    using namespace std ;

    class DoNothing

    {

        public :

            DoNothing( int initial )

            {

                cout << "DoNothing сконструирован со значением "

                     << initial

                     << endl ;

            }

    } ;

_________________

209 стр. . Аргументация конструирования

    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( )...210

Все глобальные объекты входят в область видимости программы. Таким образом, все они конструируются до того, как управление передаётся функции main( ).

«При отладке такой порядок может привести к неприятностям. Некоторые отладчики пытаются выполнить весь код, который находится до main( ) , и только потом передать управление пользователю. Это прекрасно подходит для С, поскольку до входа в функцию main( ) там не может быть никакого кода, написанного пользователем. Однако в С++ это может стать причиной большой головной боли, поскольку тела конструкторов для всех глобальных объектов к моменту передачи управления main( ) уже выполнены. Если хоть один из этих конструкторов содержит серьёзный "жучок", программа погибнет до того, как начнёт выполняться!»

[]

Существует несколько подходов к решению этой проблемы. Первый заключается в том, чтобы проверять каждый конструктор на локальных объектах перед тем, как использовать его для глобальных. Если это не поможет решить проблему, можно попытаться добавить команды вывода сообщений в начало всех конструкторов, которые, по вашему предположению, могут иметь ошибки. Последнее сообщение, которое вы увидите, вероятно, будет сообщением конструктора с ошибкой.

 

Порядок создания глобальных объектов не определён...210

Локальные объекты создаются в порядке выполнения программы. Для глобальных же объектов порядок создания не определён. Как вы помните, глобальные объекты входят в область видимости программы одновременно. Возникает вопрос: почему бы тогда компилятору не начать с начала файла с исходной программой и не создавать глобальные объекты в порядке их объявления? ( Честно говоря, я подозреваю, что на самом деле большинство компиляторов так и поступают. ) Увы, такой подход отлично работал бы, но только в том случае, если бы программа всегда состояла из одного файла.

_________________

210 стр. . Введение в классы

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

В принципе в большинстве случаев порядок создания глобальных объектов не так уж и важен. Тем не менее иногда это может привести к ошибкам, которые потом очень сложно отследить ( такое случается довольно часто, чтобы обратить на это внимание в книге ). Разберём приведённый ниже пример.

    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 будет находиться непредсказуемое значение.

«Приведённый выше пример несложен и несколько надуман. Однако проблемы, создаваемые глобальными объектами, могут оказаться гораздо коварнее. Во избежание этого не допускайте, чтобы конструктор глобального объекта обращался к другому глобальному объекту.»

[]

 

Члены создаются в порядке их объявления...211

Члены класса создаются в соответствии с порядком, в котором они объявлены внутри класса. Это не так просто и очевидно, как может показаться на первый взгляд. Рассмотрим пример.

    class Student

    {

        public :

            Student ( int id , int age ) : sAge( age ) , sId( id ) { }

            const int sId ;

            const int sAge ;

    } ;

_________________

211 стр. . Аргументация конструирования

В этом примере sId создаётся до sAge, несмотря на то что он стоит вторым в инициализирующем списке конструктора. Впрочем, единственный случай, когда можно заметить какую-то разницу в порядке конструирования, — это когда оба члена класса имеют конструкторы, которым присуще какое-либо общее побочное действие.

 

Деструкторы удаляют объекты в порядке, обратном порядку их создания...212

В каком бы порядке ни вызывались конструкторы объектов, вы можете быть уверены, что их деструкторы будут вызваны в обратном порядке. ( Приятно сознавать, что хоть одно правило в С++ не имеет никаких "если", "и" или "но". )

_________________

212 стр. . Введение в классы

 

 

        В этой главе...

►Копирование объекта  213

►Автоматический конструктор копирования  215

►"Мелкие" и "глубокие" копии  217

►Временные объекты  221

Конструктор — это специальная функция, которая автоматически вызывается С++ при создании объекта с тем, чтобы предоставить ему возможность проинициализировать самого себя. В , описаны основные концепции применения конструкторов, в , вы познакомились с разными типами конструкторов. А в настоящей главе рассматривается частный случай, известный под названием копирующего конструктора ( или конструктора копирования ).

 

►Копирование объекта...213

 

Конструктор, который используется С++ для создания копий объекта, называется копирующим конструктором, или конструктором копирования. Он имеет вид X::Х( Х& ) ( или X::X( const Х& ) ), где X — имя класса. Да, это не ошибка — это действительно конструктор класса X, который требует в качестве аргумента ссылку на объект класса X. Это звучит несколько бессмысленно, но не торопитесь с выводами и позвольте объяснить, зачем такое "чудо" в С++. 

 

Зачем нужен копирующий конструктор...213

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

    void fn( Student fs )

    {

        /* Некоторые действия */

    }

    int main( int argcs , char* pArgs[ ] )

    {

        Student ms ;

        fn( ms ) ;

        return 0 ;

    }

При вызове описанной функции fn( ) ей будет передан в качестве аргумента не сам объект, а его копия.

_________________

213 стр. . Копирующий конструктор

«В С++ аргументы функции передаются по значению.»

[]

Теперь попробуем понять, что же значит — создать копию объекта. Для этого требуется конструктор, который будет создавать объект ( даже если копируется уже существующий объект ). С++ мог бы побайтово скопировать существующий объект в новый, но как быть, если побайтовая копия не совсем то, что нам нужно? Что, если мы хотим нечто иное? ( Не спрашивайте у меня пока, что такое это "иное" и зачем оно нужно. Немного терпения! ) У нас должна быть возможность самим определять, как будет создаваться копия объекта.

Таким образом, в приведённом выше примере необходим копирующий конструктор, который будет выполнять копирование объекта ms при вызове функции fn( ). Этот частный копирующий конструктор и есть Student::Student( students ) ( попробуйте-ка произнести эту скороговорку... ).

 

Использование конструктора копирования ...214

Лучший путь понять, как работает конструктор копирования, — это увидеть его в действии. Рассмотрим приведённый ниже пример с классом 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 стр. . Введение в классы

        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

Копирующий конструктор так же важен, как и конструктор по умолчанию. Важен настолько, что С++ считает невозможным существование класса без копирующего конструктора. Если вы не создадите свою версию такого конструктора, С++ создаст её за вас. ( Это несколько отличается от конструктора по умолчанию, который создаётся только в том случае, если в вашем классе не определено вообще никаких конструкторов. )

_________________

215 стр. . Копирующий конструктор

«Копирующий конструктор, создаваемый С++, выполняет поэлементное копирование всех членов-данных. Ранее копирующий конструктор, создаваемый С++, выполнял побитовое копирование. Отличие между этими методами заключается в том, что при поэлементном копировании для каждого члена класса вызываются соответствующие копирующие конструкторы ( если они существуют ), тогда как при побитовом копировании конструкторы не вызывались. Разницу в результатах можно увидеть, выполнив приведённый пример.»

[]

    /* 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 стр. . Введение в классы

        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

Выполнение поэлементного копирования — естественная задача конструктора копирования. Но что ещё можно сделать с помощью такого конструктора? Когда наконец можно будет попытаться сделать что-то поинтереснее, чем программирование поэлементного копирования и объединения каких-то строк с именем несуществующего студента?

_________________

217 стр. . Копирующий конструктор

Представим ситуацию, когда конструктор распределяет для объекта некоторые системные ресурсы, например память из кучи. Если копирующий конструктор будет выполнять простое копирование без выделения памяти из кучи для копируемого объекта, может возникнуть ситуация, когда два объекта будут считать, что именно они являются владельцами одного блока памяти. Ситуация ещё более усугубится при вызове деструкторов обоих объектов, которые попытаются освободить одну и ту же память. Взгляните на приведённый ниже пример.

    /* 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 стр. . Введение в классы

Эта программа порождает следующий вывод:

    Вызов 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 стр. . Копирующий конструктор

        /* Копирующий конструктор выделяет новый блок памяти из кучи */

        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 стр. . Введение в классы

Как видите, теперь указатели на строки в р1 и р2 указывают на разные данные.

Рис. 18.2. Глубокое копирование объекта p1 в р2

 

►Временные объекты...221

 

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

    Student fn( ) ; /* Возвращает объект по значению */

    int main ( int argcs , char* pArgs[ ] )

    {

        Student s ;

        s = fn( ) ; /* В результате вызова fn( ) будет создан временный объект */

        return 0 ;

    }

Функция fn( ) возвращает объект по значению. В конечном счёте этот объект будет скопирован в s, но где он находится до этого?

Для хранения таких объектов С++ создаёт временные объекты ( такие объекты создаются и в некоторых других случаях ). "Хорошо, — скажете вы, — С++ создаёт временные объекты, но откуда он знает, когда их надо уничтожать?" ( Спасибо за хороший вопрос! ) В нашем примере это не имеет особого значения, поскольку временный объект выйдет из области видимости, как только копирующий конструктор скопирует его в s. Но что, если s будет определено как ссылка?

_________________

221 стр. . Копирующий конструктор

    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 будет продолжать существовать, и в результате ссылка будет указывать на несуществующий объект.

 

Как избегать временных объектов...222

Вы можете подумать, что изучение всего этого копирования объектов туда и обратно — пустая трата времени. Что, если вы не хотите делать все эти копии? Самое простое решение заключается в передаче и приёме объектов функции по ссылке. Это исключает все описанные неприятности.

Но как убедиться, что С++ не создаёт временных объектов незаметно для вас? Допустим, ваш класс использует ресурсы, которые вы не хотите копировать. Что же вам делать?

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

    class Student

    {

    protected :

        Student( Student& s ){ }

    public :

        /* ...Всё остальное как обычно... */

    } ;

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

_________________

222 стр. . Введение в классы

 

Аргумент копирующего конструктора...223

Использование копирующего конструктора для создания временных объектов и копий объектов вызывает один интересный вопрос. Рассмотрим очередной пример.

    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 стр. . Копирующий конструктор

 

 

        В этой главе...

►Определение статических членов  224

►Объявление статических функций-членов  228

►Что такое this  230

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

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

 

►Определение статических членов...224

 

Данные-члены можно сделать общими для всех объектов класса, объявив их статическими ( static ). Такие члены называются статическими данными-членами ( я бы удивился, если бы они назывались по-другому... ).

 

Зачем нужны статические члены...224

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

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

Если уж создавать класс, полностью отвечающий за своё состояние, то такие глобальные переменные, как регулятор напряжения, нужно хранить внутри класса, подальше от неаккуратных собачьих лап. Это и объясняет необходимость статических членов.

_________________

224 стр. . Введение в классы

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

[]

 

Использование статических членов...225

Статические данные-члены объявляются в классе с помощью ключевого слова 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 ;

Этот своеобразный синтаксис выделяет место для статического члена класса и инициализирует его нулём. Статические данные-члены должны быть глобальными ( как статические переменные не могут быть локальными по отношению к некоторой функции ).

«Для любого члена, имя которого встречается вне класса, требуется указание класса, к которому он принадлежит.»

[]

#i_253.jpg

«Такое выделение памяти вручную удивляет, но только до тех пор, пока вы не столкнётесь с проектами, в которых используется несколько модулей с исходным кодом. С++ должен знать, в каком именно .срр -файле надо выделить пространство для статической переменной. В случае нестатических членов это не составляет проблемы, так как память выделяется там и тогда, где и когда создаётся объект класса.»

[]

_________________

225 стр. . Статические члены

 

Обращение к статическим данным-членам...226

Правила обращения к статическим данным-членам те же, что и к обычным членам. Из класса к статическим членам обращаются так же, как и к другим членам класса. К открытым статическим членам можно обращаться извне класса, а к защищённым — нельзя, как и к обычным защищённым членам.

    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 стр. . Введение в классы

«Объект, используемый для обращения к статическому члену, никак не обрабатывается, даже если это обращение явным образом указано в выражении. Для того чтобы понять, что я имею в виду, рассмотрим приведённый ниже пример.»

[]

    class Student

    {

        static int noOfStudents ;

        Student& nextStudent( ) ;

        /* ...To же, что и раньше... */

    } ;

    void fn( Student& s )

    {

        cout << s.nextStudent( ).noOfStudents

              << "\n" ;

    }

Функция-член nextStudent( ) в этом примере не должна вызываться. Всё, что нужно знать С++ для обращения к noOfStudents, — тип возвращаемого значения, а он может это выяснить и не выполняя данную функцию[ 15 ] .

 

Применение статических данных-членов...227

Существует бесчисленное множество областей применения статических данных-членов, но здесь мы остановимся лишь на нескольких из них. Во-первых, можно использовать статические члены для хранения количества объектов, задействованных в программе. Например, в классе Student такой счётчик можно проинициализировать нулём, а затем увеличивать его на единицу внутри конструктора и уменьшать внутри деструктора. Тогда в любой момент этот статический член будет содержать количество существующих в данный момент объектов класса Student. Однако следует помнить, что этот счётчик будет содержать количество объектов, существующих в данный момент ( включая временные объекты ), а не количество студентов[ 16 ] .

Ещё один способ использования статических членов заключается в индицировании выполнения определённого действия. Например, классу Radio может понадобиться инициализировать некие аппаратные средства при первом выполнении команды tune, но не перед последующими вызовами. С помощью статического члена можно указать, что первый вызов tune уже выполнил инициализацию. Кроме всего прочего, статический член может служить указателем безошибочности инициализации аппаратных средств.

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

___________________

15Вообще говоря, это зависит от используемого компилятора. Так, тот же Dev-C++ вызовет данную функцию, в чём легко убедиться, скомпилировав и выполнив приведённый пример ( дополнив его, естественно, функцией main( ), в которой вызывается fn( ) ). — Прим. ред.

16Ещё одно замечание: в этом случае вы должны позаботиться о том, чтобы счётчик увеличивался во всех конструкторах, включая конструктор копирования. — Прим. ред.

_________________

227 стр. . Статические члены

 

►Оъявление статических функций-членов...228

Функции-члены также могут быть объявлены статическими. Подобно статическим данным-членам, они связаны с классом, а не с каким-либо отдельным объектом класса. Это означает, что обращение к статическим функциям-членам, как и к статическим данным-членам, не требует наличия объекта. Если объект и присутствует, то используется только его тип.

«Таким образом, оба вызова статической функции-члена 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 стр. . Введение в классы

Обратите внимание на то, как статическая функция-член обращается к статическим данным-членам. Поскольку статическая функция-член не связана с каким-либо объектом, она не может неявно обращаться к нестатическому члену. Таким образом, приведённый ниже пример неправилен.

      class Student

    {

    public :

        /* Приведённый ниже код неверен */

        static char *sName( )

        {

            return pName ; /* Какое именно имя? */

        }

        /* ...всё остальное то же, что и ранее... */

    protected :

        char * pName ;

        static int noOfStudents ;

    } ;

Это не означает, что статические функции-члены не имеют доступа к нестатическим данным-членам. Рассмотрим следующий пример, в котором функция findName( ) используется для поиска объекта в связанном списке ( о том, как работают связанные списки, рассказывается в ). Здесь приводится только относящаяся к нашему рассмотрению часть кода; всё остальное вы можете дописать самостоятельно, в качестве небольшого домашнего задания.

    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 стр. . Статические члены

 

►Что такое this...230

Я уже упоминал несколько раз о том, что такое 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 стр. . Введение в классы