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

  • 3.5. Размещающий оператор new Обычно оператор new

  • 3.7. Обработка исключительных ситуаций В С++ отсутствует аналог блока try…finally…end

  • 3.8. Защита ресурсов от утечки Поскольку в С+ отсутствует блок try…finally

  • 3.9. Перегрузка операторов

  • Учебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1


    Скачать 2.61 Mb.
    НазваниеУчебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1
    Дата15.01.2023
    Размер2.61 Mb.
    Формат файлаdoc
    Имя файлаlab_OOTP.doc
    ТипУчебное пособие
    #886736
    страница32 из 33
    1   ...   25   26   27   28   29   30   31   32   33

    Глава 3

    3.1. Виртуальные методы

    В С++ виртуальные методы определяются при помощи ключевого слова virtual:

    class TTextReader: virtual public TObject

    {

    ...

    };

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

    Если метод виртуальный следует всегда писать ключевое слово virtual.

    3.2. Абстрактные классы

    В С++ абстрактный класс объявляется следующим образом:

    class TTextReader

    {

    protected:

    virtual void NextLine() = 0;

    ...

    };

    Такой метод называется абстрактным и класс, содержащий данный метод, тоже называется абстрактным.

    Виртуальные методы следует объявлять в секции protected.

    3.3. Подстановочные функции

    В С существует способ оптимизировать вызов функций с помощью макросов (#define).

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

    Вместо макросов в С++ используются подстановочные функции. Они определяются с помощью ключевого слова inline:

    inline int Min(int x, int y)

    {

    if (x < y)

    return x;

    else

    return y;

    };

    Тело подстановочной функции в большинстве случаев подставляется в код программы вместо ее вызова. Она должна быть целиком определена в h-файле. Типичной ошибкой программиста является размещение этой функции в cpp-файле и вынос ее прототипа в h-файл.

    Если записана директива inline, это еще не означает, что компилятор подставляет тело функции в место ее вызова — он сам решает, что будет более удобным в данном случае.

    3.4. Операторы преобразования типа

    Существует четыре оператора преобразования типа в С++:

    reinterpret_cast<тип>(переменная)

    static_cast<тип>(переменная)

    const_cast<тип>(переменная)

    dynamic_cast<тип>(переменная)

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

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

    int i;

    char *p;

    p = reinterpret_cast(&i);

    Второй оператор (static_cast) используется вместо преобразования тип(переменная), (тип)переменная и (тип)(переменная) при работе с классами, структурами и указателями на них.

    Оператор static_cast был задуман по причине того, что в С++ выражение тип(переменная) может оказаться вызовом конструктора. Если в программе требуется преобразовать тип, а не вызвать конструктор типа, используется данный оператор. Кроме того, оператор (тип)переменная или (тип)(переменная) может в некоторых случаях оказаться преобразованием reinterpret_cast<тип>(переменная), а при разработке кросс-платформенных программ оператор reinterpret_cast всегда содержит потенциальную опасность неправильной работы программы на другой платформе. Поэтому вместо операторов тип(переменная), (тип)переменная и (тип)(переменная) следует использовать операторы reinterpret_cast и static_cast, которые убирают не явность из преобразования.

    Так как оператор static_cast является громоздким, то для простых типов данных допустимо использование форм: (тип)переменная и (тип)(переменная). Форма тип(переменная) не должна использоваться для преобразования типа.

    Третий оператор (const_cast) используется для приведения не константных указателей к константным и наоборот:

    void f2(char *s);

    void f1(const char *s)

    {

    ...

    f1(const char *s);

    ...

    f2(const_cast(s))

    ...

    };

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

    Объявление f(const char *s); означает, что символы, адресуемые указателем s, изменять нельзя.

    Объявление f(char const *s) означает, что указатель s изменять нельзя.

    Так же можно сделать объявление: f(const char const *s), которое будет означать, что ни указатель, ни переменную изменять нельзя.

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

    class TTextReader

    {

    public:

    int ItemCount() const;

    ...

    };

    Наличие константных объектов порождает проблему — огромная избыточность программного кода. Заранее программист не знает, будет ли пользователь (другой программист) его класса создавать константные объекты. Вследствие того, что это не исключено, программист начинает записывать слово const в объявление всех методов, в которых его можно записать. Многие методы являются виртуальными или вызывают виртуальные методы. Случается так, что в производных классах виртуальные методы, вызванные константными методами, модифицируют поля объектов (это требуется по условию задачи). Это приводит к логической проблеме, которая решается либо за счет применения оператора const_cast к указателю this в производных классах, либо за счет объявления полей в производных классах с модификатором mutable (записывается при описании полей класса в том случае, если они должны модифицироваться константными методами). Пример:

    mutable int m_RefCount;

    Так же решить проблему можно при помощи перегрузки метода класса без модификатора const:

    class TTextReader

    {

    public:

    int ItemCount() const;

    int ItemCount();

    int ItemCount() const volatile;

    int ItemCount() volatile;

    ...

    };

    Варианты объявления:

    volatile TTextReader r;

    const volatile TTextReader r;

    const TTextReader r;

    TTextReader r;

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

    Если ключевое слово volatile не указано, то оптимизатор C++ имеет право выполнять регистровые операции (оптимизации) при чтении и записи переменных, а так же размещать их в регистрах.

    Четвертый оператор (dynamic_cast) соответствует оператору as в Delphi. Для работы этого оператора нужно в опциях компилятора включить опцию RTTI. Если это выполнено, то оператор dynamic_cast работает, как static_cast.

    Оператор dynamic_cast работает по-разному в зависимости от того, применяется он к ссылке на объект (&) или указателю на объект (*). Если оператор применяется к ссылке на объект, то преобразование не может быть выполнено и возникает исключительная ситуация. Если он применяется к указателю на объект и преобразование не может быть выполнено, оператор возвращает NULL.

    3.5. Размещающий оператор new

    Обычно оператор new размещает объекты в динамической памяти (heap).

    TDelimitedReader *R = new TDelimitedReader();

    В данном случае оператор new имеет следующий вид:

    void *operator new(size_t size);

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

    TDelimitedReader *R = new (Buffer)TDelimitedReader();

    В этом же случае оператор new имеет следующий вид:

    void *operator new(size_t size, void *p)

    {

    return p;

    }

    Внимание! Комбинировать различные способы выделения и освобождения памяти не рекомендуется.

    3.6. Ссылки

    Ссылка является альтернативным именем объекта и объявляется следующим образом:

    int i;

    int &r = i;

    Использование ссылки r эквивалентно использованию переменной i.

    Основное применение ссылок — передача параметров в функцию и возврат значения.

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

    void f(int &i);

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

    Если ссылка является полем класса, она должна инициализироваться в конструкторе класса в списке инициализации до тела конструктора.

    При использовании в качестве параметров функций ссылки соответствуют var-параметрам в языке Delphi:

    procedure P(var I: Integer)

    begin

    ...

    end;

    Константные ссылки соответствуют const-параметрам в языке Delphi:

    procedure P(const I: Integer)

    begin

    ...

    end;

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

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

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

    При работе со ссылками существует типовая ошибка — возврат через ссылку переменной, созданной на стеке. Пример ошибочной записи:

    void f(int *p)

    {

    int i;

    p = &i;

    }

    Следующая запись тоже будет ошибочной:

    void f(int &r)

    {

    int i;

    r = i;

    }

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

    std::string& GetName(Object* Obj)

    {

    const char* str = Obj->GetName();

    return std::string(str);

    }

    3.7. Обработка исключительных ситуаций

    В С++ отсутствует аналог блока try…finally…end.

    На платформе Windows благодаря структурной обработке ОС существуют следующий блок:

    __try

    {

    ...

    }

    __finally

    {

    ...

    }

    Но следует отметить, что для переносимых программ он не подходит.

    В С++ существует аналог блока try…except…end:

    try

    {

    ...

    }

    catch(std::ios_base::failure)

    {

    ...

    }

    catch(std::exception)

    {

    ...

    }

    catch(...)

    {

    ...

    }

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

    Последний блок catch в примере выше ловит любую исключительную ситуацию.

    Создание исключительных ситуаций выполняется с помощью оператора throw (аналог raise в Delphi):

    throw std::exception("Ошибка");

    Внутри блока catch оператор throw возобновляет исключительную ситуацию, как и raise в Delphi.

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

    throw new std::exception("Ошибка");

    Если применяется такой способ создания исключительной ситуации, ее уничтожение должно происходить следующим образом:

    try

    {

    ...

    throw new std::exception("Ошибка");

    }

    catch(std::exception *e)

    {

    delete e;

    }

    catch(...)

    {

    ...

    }

    Если же записать так:

    try

    {

    ...

    throw new std::exception("Ошибка");

    }

    catch(...)

    {

    ...

    }

    то возникнет утечка ресурсов из-за того, что объект std::exception, созданный в динамической памяти, не будет освобожден.

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

    Program



    Program



    3.8. Защита ресурсов от утечки

    Поскольку в С+ отсутствует блок try…finally, его приходится эмулировать.

    Object *p = new Object();

    try

    {

    ...

    }

    catch(...)

    {

    delete p;

    throw;

    }

    delete p;

    Данный код эквивалентен следующему:

    Object *p = new Object();

    __try

    {

    ...

    }

    __finally

    {

    delete p;

    }

    за исключение того, что второй пример не является переносимым.

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

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

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

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

    Такие оболочечные объекты в библиотеках программирования называются AutoPtr, SafePtr и т.д.

    class AutoPtr

    {

    public:

    AutoPtr(int *arr);

    AutoPtr();

    private:

    int *m_arr;

    };
    AutoPtr::AutoPtr(int *arr)

    {

    m_arr = arr;

    }
    AutoPtr::AutoPtr()

    {

    delete[] m_arr;

    }
    void Proc()

    {

    int *arr = new int[100];

    AutoPtr autoc(arr);

    ...

    }

    3.9. Перегрузка операторов

    Перегрузка операторов позволяет заменить смысл стандартных операторов (+, –, = и др.) для пользовательских типов данных.

    В С++ разрешена перегрузка операторов, выраженных в виде символов, а также операторов:

    new delete

    new[] delete[]

    Запрещена перегрузка следующих операторов:

    :: . .* ?:

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

    Бинарные операторы

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

    Для любого бинарного оператора @ выражение aa@bb интерпретируется как aa.operator@(bb) или operator@(aa, bb). Если определены оба варианта, то применяется механизм разрешения перегрузки функций.

    Пример:

    class X

    {

    public:

    void operator +(int);

    X(int);

    };
    void operator +(X, X);

    void operator +(X, double);
    void Proc(X, a)

    {

    a + 1; // a.operator +(1)

    1 + a; // ::operator +(X(1),a)

    a + 1.0; // ::operator +(a, 1.0)

    }

    Унарные операторы

    Унарные операторы бывают префиксными и постфиксными.

    Унарный оператор можно определить в виде метода класса без аргументов и в виде функции с одним аргументом. Аргумент функции — объект некоторого класса.

    Для любого префиксного унарного оператора выражение @aa интерпретируется как:

    aa.operator @();

    operator @(aa);

    Для любого постфиксного унарного оператора выражение aa@ интерпретируется, как:

    aa.operator @(int);

    operator @(aa, int);

    Запрещено перегружать операторы, которые нарушают грамматику языка.

    Существует три оператора, которые следует определить внутри класса в виде методов:

    operator =

    operator []

    operator ->

    Это гарантирует, что в левой части оператора будет записан lvalue (присваиваемое значение).

    Операторы преобразования

    В С++ существуют операторы преобразования типов. Это является хорошим способом использования конструктора для преобразования типа. Конструктор не может выполнять следующие преобразования:

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

    • преобразование из нового класса в ранее определенный класс, не модифицируя объявление ранее определенного класса.

    Оператор преобразования типа:

    X::operator T() // определяет преобразования класса X в тип данных T (T — класс или базовый тип)

    Пример:

    class Number

    {

    public:

    operator int();

    operator Complex();

    };
    Number::operator int()

    {

    ...

    }
    Number::operator Complex()

    {

    ...

    }

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

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

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

    С помощью ключевого слова explicit можно запретить неявное преобразования типа к конструкторам.

    Пример:

    class File

    {

    public:

    explicit File(const char *name); // одновременно не могут быть определены, надо выбирать один из них

    explicit File(const char *name, int mode = FILE_READ);

    };

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

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

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

    X operator; // по значению

    X &operator; // по ссылке

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

    Замечание по поводу преобразования типа в тернарном операторе (c ? x : y).

    class A { ... };

    class B: public A { ... };

    class C: public A { ... };

    Запись

    A* p = cond ? new B : new C;

    вызовет ошибку компилятора, поскольку между типами выражений "new B" и "new C" выбирается общий тип, а такого нет. Ошибку следует устранить, выполнив преобразование "new B" или "new C" к общему типу, например:

    A* p = cond ? (A*)new B : new C;

    или

    A* p = cond ? new B : (A*)new C;

    или

    A* p = cond ? (A*)new B : (A*)new C; // самый лучший вариант

    1   ...   25   26   27   28   29   30   31   32   33


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