Главная страница
Навигация по странице:

  • Примечание [O.A.4]

  • 14.2.1. Конструктор по умолчанию

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница63 из 93
    1   ...   59   60   61   62   63   64   65   66   ...   93
    656
    }
    Имена в теле локального класса разрешаются лексически путем поиска в объемлющих областях видимости объявлений, предшествующих определению такого класса. При разрешении имен, встречающихся в телах его функций-членов, сначала просматривается область видимости класса, а только потом – объемлющие области,
    Как всегда, если первое найденное объявление таково, что употребление имени оказывается некорректным, поиск других объявлений не производится. Несмотря на то что использование val в fooBar() выше является ошибкой, глобальная переменная val не будет найдена, если только ее имени не предшествует оператор разрешения глобальной области видимости. int a, val; void foo( int val )
    { static int si; enum Loc { a = 1024, b }; class Bar { public:
    Loc locVal; // правильно int barVal; void fooBar ( Loc l = a ) { // правильно: Loc::a barVal = val; // ошибка: локальный объект barVal = ::val; // правильно: глобальный объект barVal = si; // правильно: статический локальный объект locVal = b; // правильно: элемент перечисления
    }
    };
    // ...

    С++ для начинающих
    657
    14.
    Инициализация, присваивание и
    уничтожение класса
    В этой главе мы детально изучим автоматическую инициализацию, присваивание и уничтожение объектов классов в программе. Для поддержки инициализации служит конструктор – определенная проектировщиком функция (возможно, перегруженная), которая автоматически применяется к каждому объекту класса перед его первым использованием. Парная по отношению к конструктору функция, деструктор, автоматически применяется к каждому объекту класса по окончании его использования и предназначена для освобождения ресурсов, захваченных либо в конструкторе класса, либо на протяжении его жизни.
    По умолчанию как инициализация, так и присваивание одного объекта класса другому выполняются почленно, т.е. путем последовательного копирования всех членов. Хотя этого обычно достаточно, при некоторых обстоятельствах такая семантика оказывается неадекватной. Тогда проектировщик класса должен предоставить специальный копирующий конструктор и копирующий оператор присваивания. Самое сложное в поддержке этих функций-членов – понять, что они должны быть написаны.
    14.1.
    Инициализация класса
    Рассмотрим следующее определение класса:
    };
    Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль?
    Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival
    – его уникальный номер. Тогда отрицательное или нулевое значения ошибочны.
    Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком
    ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr.
    При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.
    Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной class Data { public: int ival; char *ptr;

    С++ для начинающих
    658
    информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации –
    конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса
    Data
    :
    Data dat04;
    Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.
    Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты.
    Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:
    }
    Значения присваиваются позиционно, на основе порядка, в котором объявляются данные- члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед ptr
    :
    Data.local2 - { "Anna Livia Plurabelle", 1024 };
    Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов – их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует
    Data dat01( "Venus and the Graces", 107925 );
    Data dat02( "about" );
    Data dat03( 107925 ); int main()
    {
    // local1.ival = 0; local1.ptr = 0
    Data local1 = { 0, 0 };
    // local2.ival = 1024;
    // local3.ptr = "Anna Livia Plurabelle"
    Data.local2 - { 1024, "Anna Livia Plurabelle" };
    // ...
    // ошибка: ival = "Anna Livia Plurabelle";
    // ptr = 1024

    С++ для начинающих
    659
    вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).
    Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами
    1
    Однако в общем случае предпочтительным методом инициализации является конструктор, который гарантированно будет вызван компилятором для каждого объекта до его первого использования. В следующем разделе мы познакомимся с конструкторами детально.
    14.2.
    Конструктор класса
    Среди других функций-членов конструктор выделяется тем, что его имя совпадает с именем класса. Для объявления конструктора по умолчанию мы пишем
    2
    :
    };
    Единственное синтаксическое ограничение, налагаемое на конструктор, состоит в том, что он не должен иметь тип возвращаемого значения, даже void. Поэтому следующие объявления ошибочны:
    Account* Account::Account( const char *pc ) { ... }
    1
    Более подробное обсуждение этой темы с примерами и приблизительными оценками производительности см. в [LIPPMAN96a].
    2
    В реальной программе мы объявили бы член _name как имеющий тип string.
    Здесь он объявлен как C-строка, чтобы отложить рассмотрение вопроса об инициализации членов класса до раздела 14.4. class Account { public:
    // конструктор по умолчанию ...
    Account();
    // ... private: char *_name; unsigned int _acct_nmbr; double _balance;
    // ошибки: у конструктора не может быть типа возвращаемого значения void Account::Account() { ... }
    Примечание [O.A.4]: Нумера ция сносок сбита.

    С++ для начинающих
    660
    Количество конструкторов у одного класса может быть любым, лишь бы все они имели разные списки формальных параметров.
    Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически.
    Тогда мы должны разрешить инициализировать оставшиеся два члена _name и _balance:
    Account( const char *name, double open_balance );
    Объект класса Account, инициализируемый конструктором, можно объявить следующим образом:
    Account newAcct( "Mikey Matz", 0 );
    Если же есть много счетов, для которых начальный баланс равен 0, то полезно иметь конструктор, задающий только имя владельца и автоматически инициализирующий
    _balance нулем. Один из способов сделать это – предоставить конструктор вида:
    Account( const char *name );
    Другой способ – включить в конструктор с двумя параметрами значение по умолчанию, равное нулю:
    Account( const char *name, double open_balance = 0.0 );
    Оба конструктора обладают необходимой пользователю функциональностью, поэтому оба решения приемлемы. Мы предпочитаем использовать аргумент по умолчанию, поскольку в такой ситуации общее число конструкторов класса сокращается.
    Нужно ли поддерживать также задание одного лишь начального баланса без указания имени клиента? В данном случае спецификация класса явно запрещает это. Наш конструктор с двумя параметрами, из которых второй имеет значение по умолчанию, предоставляет полный интерфейс для указания начальных значений тех членов класса
    Account
    , которые могут быть инициализированы пользователем:
    };
    Ниже приведены два примера правильного определения объекта класса Account, где конструктору передается один или два аргумента: class Account { public:
    // конструктор по умолчанию ...
    Account();
    // имена параметров в объявлении указывать необязательно
    Account( const char*, double=0.0 ); const char* name() { return name; }
    // ... private:
    // ...

    С++ для начинающих
    661
    }
    C++ требует, чтобы конструктор применялся к определенному объекту до его первого использования. Это означает, что как для acct, так и для объекта, на который указывает pact
    , конструктор будет вызван перед проверкой в инструкции if.
    Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по всей вероятности, будет модифицировано определение acct внутри main():
    }
    Конечно, если конструктор определен как встроенный, то он подставляется в точке вызова.
    Обработка оператора new несколько сложнее. Конструктор вызывается только тогда, когда он успешно выделил память. Модификация определения pact в несколько упрощенном виде выглядит так:
    } int main()
    {
    // правильно: в обоих случаях вызывается конструктор
    // с двумя параметрами
    Account acct( "Ethan Stern" );
    Account *pact = new Account( "Michael Lieberman", 5000 ); if ( strcmp( acct.name(), pact->name() ))
    // ...
    // псевдокод на C++,
    // иллюстрирующий внутреннюю вставку конструктора int main()
    {
    Account acct; acct.Account::Account("Ethan Stern", 0.0);
    // ...
    // псевдокод на C++,
    // иллюстрирующий внутреннюю вставку конструктора при обработке new int main()
    {
    // ...
    Account *pact; try { pact = _new( sizeof( Account )); pact->Acct.Account::Account(
    "Michael Liebarman", 5000.0);
    } catch( std::bad_alloc ) {
    // оператор new закончился неудачей:
    // конструктор не вызывается
    }
    // ...

    С++ для начинающих
    662
    Существует три в общем случае эквивалентных формы задания аргументов конструктора:
    Account acct3 = "Anna Press";
    Форма acct3 может использоваться только при задании единственного аргумента. Если аргументов два или более, мы рекомендуем пользоваться формой acct1, хотя допустима и acct2.
    Account acct1( "Anna Press" );
    Новички часто допускают ошибку при объявлении объекта, инициализированного конструктором по умолчанию:
    Account newAccount();
    Эта инструкция компилируется без ошибок. Однако при попытке использовать объект в таком контексте: if ( ! newAccount.name() ) ... компилятор не сможет применить к функции нотацию доступа к членам класса.
    Определение
    Account newAccount(); интерпретируется компилятором как определение функции без параметров, которая возвращает объект типа Account. Правильное объявление объекта класса, инициализируемого конструктором по умолчанию, не содержит пустых скобок:
    Account newAccount;
    Определять объект класса, не указывая списка фактических аргументов, можно в том случае, если в нем либо объявлен конструктор по умолчанию, либо вообще нет
    // в общем случае эти формы эквивалентны
    Account acct1( "Anna Press" );
    Account acct2 = Account( "Anna Press" );
    // рекомендуемая форма вызова конструктора
    // увы! работает не так, как ожидалось
    // ошибка компиляции ...
    // определяет функцию newAccount,
    // а не объект класса
    // правильно: определяется объект класса ...

    С++ для начинающих
    663
    объявлений конструкторов. Если в классе объявлен хотя бы один конструктор, то не разрешается определять объект класса, не вызывая ни одного из них. В частности, если в классе определен конструктор, принимающий один или более параметров, но не определен конструктор по умолчанию, то в каждом определении объекта такого класса должны присутствовать необходимые аргументы. Можно возразить, что не имеет смысла определять конструктор по умолчанию для класса Account, поскольку не бывает счетов без имени владельца. В пересмотренной версии класса Account такой конструктор исключен:
    };
    Теперь при объявлении каждого объекта Account в конструкторе обязательно надо указать как минимум аргумент типа C-строки, но это скорее всего бессмысленно.
    Почему? Контейнерные классы (например, vector) требуют, чтобы для класса помещаемых в них элементов был либо задан конструктор по умолчанию, либо вообще никаких конструкторов. Аналогичная ситуация имеет место при выделении динамического массива объектов класса. Так, следующая инструкция вызвала бы ошибку компиляции для новой версии Account:
    Account *pact = new Account[ new_client_cnt ];
    На практике часто требуется задавать конструктор по умолчанию, если имеются какие- либо другие конструкторы.
    А если для класса нет разумных значений по умолчанию? Например, класс Account требует задавать для любого объекта фамилию владельца счета. В таком случае лучше всего установить состояние объекта так, чтобы было видно, что он еще не инициализирован корректными значениями:
    }
    Однако в функции-члены класса Account придется включить проверку целостности объекта перед его использованием. class Account { public:
    // имена параметров в объявлении указывать необязательно
    Account( const char*, double=0.0 ); const char* name() { return name; }
    // ... private:
    // ...
    // ошибка: требуется конструктор по умолчанию для класса Account
    // конструктор по умолчанию для класса Account inline Account::
    Account() {
    _name = 0;
    _balance = 0.0;
    _acct_nmbr = 0;

    С++ для начинающих
    664
    Существует и альтернативный синтаксис: список инициализации членов, в котором через запятую указываются имена и начальные значения. Например, конструктор по умолчанию можно переписать следующим образом:
    {}
    Такой список допустим только в определении, но не в объявлении конструктора. Он помещается между списком параметров и телом конструктора и отделяется двоеточием.
    Вот как выглядит наш конструктор с двумя параметрами при частичном использовании списка инициализации членов:
    } get_unique_acct_nmbr()
    – это не являющаяся открытой функция-член, которая возвращает гарантированно не использованный ранее номер счета.
    Конструктор нельзя объявлять с ключевыми словами const или volatile (см. раздел
    13.3.5), поэтому приведенные записи неверны:
    };
    Это не означает, что объекты класса с такими спецификаторами запрещено инициализировать конструктором. Просто к объекту применяется подходящий конструктор, причем без учета спецификаторов в объявлении объекта. Константность объекта класса устанавливается после того, как работа по его инициализации завершена, и пропадает в момент вызова деструктора. Таким образом, объект класса со спецификатором const считается константным с момента завершения работы конструктора до момента запуска деструктора. То же самое относится и к спецификатору volatile
    Рассмотрим следующий фрагмент программы:
    // конструктор по умолчанию класса Account с использованием
    // списка инициализации членов inline Account::
    Account()
    : _name(0),
    _balance( 0.0 ), _acct_nmbr( 0 ) inline Account::
    Account( const char* name, double opening_bal )
    : _balance( opening_bal )
    {
    _name = new char[ strlen(name)+1 ]; strcpy( _name, name );
    _acct_nmbr = get_unique_acct_nmbr(); class Account { public:
    Account() const; // ошибка
    Account() volatile; // ошибка
    // ...

    С++ для начинающих
    665
    }
    По умолчанию конструктор с одним параметром (или с несколькими – при условии, что все параметры, кроме первого, имеют значения по умолчанию) играет роль оператора преобразования. В этом фрагменте программы конструктор Account неявно применяется компилятором для трансформации литеральной строки в объект класса Account при вызове print(), хотя в данной ситуации такое преобразование не нужно.
    Непреднамеренные неявные преобразования классов, например трансформация "oops" в объект класса Account, оказались источником трудно обнаруживаемых ошибок.
    Поэтому в стандарт C++ было добавлено ключевое слово explicit, говорящее компилятору, что такие преобразования не нужны:
    };
    Данный модификатор применим только к конструктору. (Операторы преобразования и слово explicit обсуждаются в разделе 15.9.2.)
    14.2.1.
    Конструктор по умолчанию
    Конструктором по умолчанию называется конструктор, который можно вызывать, не задавая аргументов. Это не значит, что такой конструктор не может принимать аргументов; просто с каждым его формальным параметром ассоциировано значение по умолчанию:
    Complex::Complex(double re=0.0, double im=0.0) { ... }
    Когда мы пишем:
    // в каком-то заголовочном файле extern void print( const Account &acct );
    // ... int main()
    {
    // преобразует строку "oops" в объект класса Account
    // с помощью конструктора Account::Account( "oops", 0.0 ) print( "oops" );
    // ... class Account { public: explicit Account( const char*, double=0.0 );
    // все это конструкторы по умолчанию
    Account::Account() { ... } iStack::iStack( int size = 0 ) { ... }

    С++ для начинающих
    1   ...   59   60   61   62   63   64   65   66   ...   93


    написать администратору сайта