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

  • 17.4.5. Деструкторы

  • 17.5.1. Виртуальный ввод/вывод

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


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница82 из 93
    1   ...   78   79   80   81   82   83   84   85   ...   93

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

    если ошибка обнаруживается в точке объявления, то мы не можем продолжать компиляцию приложения, пока не исправим ее. Если же конфликтующее объявление – это часть библиотеки, для которой у нас нет исходного текста, то разрешение конфликта может оказаться нетривиальной задачей. Более того, возможно, в нашем коде никогда и не возникнет ситуации, когда эта ошибка проявляется, так что для нас она останется лишь потенциальной угрозой;
    // правильно: эти определения классов корректны class OrQuery : public BinaryQuery { public:
    OrQuery( Query *lop, Query *rop )
    : BinaryQuery( lop, rop ) {} virtual void eval();
    }; class AndQuery : public BinaryQuery { public:
    AndQuery( Query *lop, Query *rop )
    : BinaryQuery( lop, rop ) {} virtual void eval();

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

    с другой стороны, если ошибка не найдена вплоть до момента использования, то код может оказаться замусоренным ошибками, проявляющимися в самый неподходящий момент к удивлению программиста. При такой стратегии успешная компиляция говорит не об отсутствии семантических ошибок, а лишь о том, что программа не исполняет код, нарушающий семантические правила языка.
    Выдача сообщения об ошибке в точке использования – это одна из форм отложенного
    вычисления, распространенного метода повышения производительности программ. Он часто применяется для того, чтобы отложить потенциально дорогую операцию выделения или инициализации ресурса до момента, когда в нем возникнет реальная необходимость.
    Если ресурс так и не понадобится, мы сэкономим на ненужных подготовительных операциях. Если же он потребуется, но не сразу, мы растянем инициализацию программы на более длительный период.
    В C++ потенциальные ошибки “комбинирования”, связанные с перегруженными функциями, шаблонами и наследованием классов, обнаруживаются в точке использования, а не в точке объявления. (Мы полагаем, что это правильно, поскольку необходимость выявлять все возможные ошибки, которые можно допустить в результате комбинирования многочисленных компонентов, – пустая трата времени). Следовательно, для обнаружения и устранения латентных ошибок необходимо тщательно тестировать код. Подобные ошибки, возникающие при комбинировании двух или более больших компонентов, допустимы; однако в пределах одного компонента, такого, как иерархия классов Query, их быть не должно.
    17.4.5.
    Деструкторы
    Когда заканчивается время жизни объекта производного класса, автоматически вызываются деструкторы производного и базового классов (если они определены), а также деструкторы всех объектов-членов. Например, если имеется объект класса
    NameQuery
    :
    NameQuery nq( "hyperion" ); то порядок вызова деструкторов следующий: сначала деструктор NameQuery, затем деструктор string для члена _name и наконец деструктор базового класса. В общем случае эта последовательность противоположна порядку вызова конструкторов.
    Вот деструкторы нашего базового Query и производных от него (все они объявлены открытыми членами соответствующих классов):

    AndQuery(){ delete _lop; delete _rop; }
    Отметим два аспекта: inline Query::
    Query(){ delete _solution; } inline NotQuery::
    NotQuery(){ delete _op; } inline OrQuery::
    OrQuery(){ delete _lop; delete _rop; } inline AndQuery::

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

    мы не предоставляем явного деструктора NameQuery, потому что никаких специальных действий по очистке его объекта предпринимать не нужно.
    Деструкторы базового класса и класса string для члена _name вызываются автоматически;

    в деструкторах производных классов оператор delete применяется к указателю типа Query*. Чтобы вызвать не деструктор Query, а деструктор класса того объекта, который фактически адресуется этим указателем, мы должны объявить деструктор базового Query виртуальным. (Более подробно о виртуальных функциях вообще и о виртуальных деструкторах в частности мы поговорим в следующем разделе.)
    В нашей реализации неявно подразумевалось, что память для операндов, указатели на которые имеются в объектах классов NotQuery, OrQuery и AndQuery, выделена из хипа.
    Именно поэтому в деструкторах мы применяли к этим указателям оператор delete. Но язык не позволяет обеспечить истинность такого предположения, так как в нем нет различий между адресами в хипе и вне его. С этой точки зрения наша реализация не застрахована от ошибок.
    В разделе 17.7 мы инкапсулируем выделение памяти и конструирование объектов иерархии Query в управляющий класс UserQuery. Это гарантирует выполнение нашего предположения. На уровне программы в целом следует перегрузить операторы new и delete для классов иерархии. Например, можно поступить следующим образом.
    Оператор new устанавливает в объекте флажок, говорящий, что память для него выделена из хипа. Перегруженный оператор delete проверяет этот флажок: если он есть, то память освобождается с помощью стандартного оператора delete.
    Упражнение 17.7
    Идентифицируйте конструкторы и деструкторы базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1).
    Упражнение 17.8
    Измените реализацию класса OrQuery так, чтобы он был производным от BinaryQuery.
    Упражнение 17.9
    Найдите ошибку в следующем определении класса:
    };
    Упражнение 17.10
    Дано определение базового класса: class Object { public: virtual Object(); virtual string isA(); protected: string _isA; private:
    Object( string s ) : _isA( s ) {}

    С++ для начинающих
    880
    };
    Что неправильно в следующих фрагментах:
    };
    };
    };
    };
    Упражнение 17.11
    В первоначальном определении языка C++ порядок следования инициализаторов в списке инициализации членов определял порядок вызова конструкторов. Принцип, class ConcreteBase { public: explicit ConcreteBase( int ); virtual ostream& print( ostream& ); virtual Base(); static int object_count(); protected: int _id; static int _object_count;
    (a) class C1 : public ConcreteBase { public:
    C1( int val )
    : _id( _object_count++ ) {}
    // ...
    (b) class C2 : public C1 { public:
    C2( int val )
    : ConcreteBase( val ), C1( val ) {}
    // ...
    (c) class C3 : public C2 { public:
    C3( int val )
    : C2( val ), _object_count( val ) {}
    // ...
    (d) class C4 : public ConcreteBase { public:
    C4( int val )
    : ConcreteBase ( _id+val ){}
    // ...

    С++ для начинающих
    881
    который действует сейчас, был принят в 1986 году. Как вы думаете, почему была изменена исходная спецификация?
    17.5.
    Виртуальные функции в базовом и производном классах
    По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса
    (или указателя, или ссылки на объект), для которого она вызвана:
    }
    Статический тип pb – это Query*. При обращении к невиртуальному члену solutions() вызывается функция-член класса Query. Невиртуальная функция display() вызывается через неявный указатель this. Статическим типом указателя this также является
    Query*
    , поэтому вызвана будет функция-член класса Query.
    Чтобы объявить функцию виртуальной, нужно добавить ключевое слово virtual:
    };
    Если функция-член виртуальна, то при обращении к ней вызывается функция, определенная в динамическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана. Однако для самих объектов класса статический и динамический тип – это одно и то же. Механизм виртуальных функций правильно работает только для указателей и ссылок на объекты.
    Таким образом, полиморфизм проявляется только тогда, когда объект производного класса адресуется косвенно, через указатель или ссылку на базовый. Использование самого объекта базового класса не сохраняет идентификацию типа производного.
    Рассмотрим следующий фрагмент кода:
    Query qobject = nq;
    Инициализация qobject переменной nq абсолютно законна: теперь qobject равняется подобъекту nq, который соответствует базовому классу Query, однако qobject не void Query::display( Query *pb )
    { set *ps = pb->solutions();
    // ... display(); class Query { public: virtual ostream& print( ostream* = cout ) const;
    // ...
    NameQuery nq( "lilacs" );
    // правильно: но nq "усечено" до подобъекта Query

    С++ для начинающих
    882
    является объектом NameQuery. Часть nq, принадлежащая NameQuery, “усечена” перед инициализацией qobject, поскольку она не помещается в область памяти, отведенную под объект Query. Для поддержки этой парадигмы приходится использовать указатели и ссылки, но не сами объекты:
    }
    В данном примере оба обращения через указатель pointer и ссылку reference разрешаются своим динамическим типом; в обоих случаях вызывается
    NameQuery::print()
    . Обращение же через объект object всегда приводит к вызову
    Query::print()
    . (Пример программы, в которой используется эффект “усечения”, приведен в разделе 18.6.2.)
    В следующих подразделах мы продемонстрируем определение и использование виртуальных функций в разных обстоятельствах. Каждая такая функция-член будет иллюстрировать один из аспектов объектно-ориентированного проектирования.
    17.5.1.
    Виртуальный ввод/вывод
    Первая виртуальная операция, которую мы хотели реализовать, – это печать запроса на стандартный вывод либо в файл: ostream& print( ostream &os = cout ) const;
    Функцию print() следует объявить виртуальной, поскольку ее реализации зависят от типа, но нам нужно вызывать ее через указатель типа Query*. Например, для класса
    AndQuery эта функция могла бы выглядеть так:
    } void print ( Query object, const Query *pointer, const Query &reference )
    {
    // до момента выполнения невозможно определить,
    // какой экземпляр print() вызывается pointer->print(); reference.print();
    // всегда вызывается Query::print() object.print();
    } int main()
    {
    NameQuery firebird( "firebird" ); print( firebird, &firebird, firebird ); ostream&
    AndQuery::print( ostream &os ) const
    {
    _lop->print( os ); os << " && ";
    _rop->print( os );

    С++ для начинающих
    883
    Необходимо объявить print() виртуальной функцией в абстрактном базовом Query, иначе мы не сможем вызвать ее для членов классов AndQury, OrQuery и NotQuery, являющихся указателями на операнды соответствующих запросов типа Query*. Однако для самого Query разумной реализации print() не существует. Поэтому мы определим ее как пустую функцию, а потом сделаем чисто виртуальной:
    };
    В базовом классе, где виртуальная функция появляется в первый раз, ее объявлению должно предшествовать ключевое слово virtual. Если же ее определение находится вне этого класса, повторно употреблять virtual не следует. Так, данное определение print()
    приведет к ошибке компиляции: virtual ostream& Query::print( ostream& ) const { ... }
    Правильный вариант не должен включать слово virtual.
    Класс, в котором впервые появляется виртуальная функция, должен определить ее или объявить чисто виртуальной (напомним, что пока мы определили ее как пустую). В производном классе может быть либо определена собственная реализация той же функции, которая в таком случае становится активной для всех объектов этого класса, либо унаследована реализация из базового класса. Если в производном классе определена собственная реализация, то говорят, что она замещает реализацию из базового.
    Прежде чем приступать к рассмотрению реализаций print() для наших четырех производных классов, обратим внимание на употребление скобок в запросе. Например, с помощью fiery && bird || shyly пользователь ищет вхождения пары слов fiery bird или одного слова shyly
    С другой стороны, запрос class Query { public: virtual ostream& print( ostream &os=cout ) const {}
    // ...
    // ошибка: ключевое слово virtual может появляться
    // только в определении класса

    С++ для начинающих
    884
    fiery && ( bird || hair ) найдет все вхождения любой из пар fiery bird или fiery hair
    Если наши реализации print() не будут показывать скобки в исходном запросе, то для пользователя они окажутся почти бесполезными. Чтобы сохранить эту информацию, введем в наш абстрактный базовый класс Query два нестатических члена, а также функции доступа к ним (подобное расширение класса – естественная часть эволюции иерархии):
    };
    _lparen
    – это количество левых, а _rparen – правых скобок, которое должно быть выведено при распечатке объекта. (В разделе 17.7 мы покажем, как вычисляются такие величины и как происходит присваивание обоим членам.) Вот пример обработки запроса с большим числом скобок:
    ==> ( untamed || ( fiery || ( shyly ) ) ) evaluate word: untamed
    _lparen: 1
    _rparen: 0 evaluate Or
    _lparen: 0
    _rparen: 0 class Query { public:
    // ...
    // установить _lparen и _rparen void lparen( short lp ) { _lparen = lp; } void rparen( short rp ) { _rparen = rp; }
    // получить значения_lparen и _rparen short lparen() { return _lparen; } short rparen() { return _rparen; }
    // напечатать левую и правую скобки void print_lparen( short cnt, ostream& os ) const; void print_rparen( short cnt, ostream& os ) const; protected:
    // счетчики левых и правых скобок short _lparen; short _rparen;
    // ...

    С++ для начинающих
    885
    evaluate word: fiery
    _lparen: 1
    _rparen: 0 evaluate 0r
    _lparen: 0
    _rparen: 0 evaluate word: shyly
    _lparen: 1
    _rparen: 0 evaluate right parens:
    _rparen: 3
    ( untamed ( 1 ) lines match
    ( fiery ( 1 ) lines match
    ( shyly ( 1 ) lines match
    ( fiery || (shyly ( 2 ) lines match3
    ( untamed || ( fiery || ( shyly ))) ( 3 ) lines match
    Requested query: ( untamed || ( fiery || ( shyly ) ) )
    ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,
    ( 4 ) magical but untamed. "Daddy, shush, there is no such thing,"
    ( 6 ) Shyly, she asks, "I mean, Daddy, is there?"
    Реализация print() для класса NameQuery:
    }
    А так выглядит объявление:
    };
    Чтобы реализация виртуальной функции в производном классе замещала реализацию из базового, прототипы функций обязаны совпадать. Например, если бы мы опустили слово const или объявили еще один параметр, то реализация print() в NameQuery не заместила бы реализацию из базового класса. Возвращаемые значения также должны
    3
    Увы! Правые скобки не распознаются, пока OrQuery не выведет все ассоциированное с ним частичное решение. ostream&
    NameQuery:: print( ostream &os ) const
    { if ( _lparen ) print_lparen( _lparen, os ); os << _name; if ( _rparen ) print_rparen( _rparen, os ); return os; class NameQuery : public Query { public: virtual ostream& print( ostream &os ) const;
    // ...

    С++ для начинающих
    886
    быть одинаковыми за одним исключением: значение, возвращенное реализацией в производном классе, может принадлежать к типу класса, который открыто наследует классу значения, возвращаемого реализацией в базовом классе. Если бы реализация из базового класса возвращала значение типа Query*, то реализация из производного могла бы возвращать NameQuery*. (Позже при работе с функцией clone() мы покажем, зачем это нужно.) Вот объявление и реализация print() в NotQuery:
    };
    }
    Разумеется, вызов print() через _op – виртуальный.
    Объявления и реализации этой функции в классах AndQuery и OrQuery практически дублируют друг друга. Поэтому приведем их только для AndQuery:
    }; class NotQuery : public Query { public: virtual ostream& print( ostream &os ) const;
    // ... ostream&
    NotQuery:: print( ostream &os ) const
    { os << " ! "; if ( _lparen ) print_lparen( _lparen, os );
    _op->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; class AndQuery : public Query { public: virtual ostream& print( ostream &os ) const;
    // ...

    С++ для начинающих
    887
    }
    Такая реализация виртуальной функции print() позволяет вывести любой подтип Query в поток класса ostream или любого другого, производного от него: pq->print( cout );
    Однако такой возможности недостаточно. Еще нужно уметь распечатывать любой производный от Query тип, который уже есть или может появиться в будущем, с помощью оператора вывода из библиотеки iostream:
    << " получены следующие результаты:\n";
    Мы не можем непосредственно предоставить виртуальный оператор вывода, поскольку они являются членами класса ostream. Вместо этого мы должны написать косвенную виртуальную функцию:
    }
    Строки ostream&
    AndQuery:: print( ostream &os ) const
    { if ( _lparen ) print_lparen( _lparen, os );
    _lop->print( os ); os << " && ";
    _rop->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; cout << "
    Был сформулирован запрос ";
    Query *pq = retrieveQuery();
    Query *pq = retrieveQuery(); cout << "
    В ответ на запрос "
    << *pq inline ostream& operator<<( ostream &os, const Query &q )
    {
    // виртуальный вызов print() return q.print( os );
    AndQuery query;
    // сформулировать запрос ...

    С++ для начинающих
    888
    cout << query << endl; вызывают наш оператор вывода в ostream, который в свою очередь вызывает q.print( os ) где q привязано к объекту query класса AndQuery, а os – к cout. Если бы вместо этого мы написали: cout << query2 << endl; то была бы вызвана реализация print() из класса NameQuery. Обращение cout << *pquery << endl; приводит к вызову той функции print(), которая ассоциирована с объектом, адресуемым указателем pquery в данной точке выполнения программы.
    1   ...   78   79   80   81   82   83   84   85   ...   93


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