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

  • 1.3. Перегрузка идентификаторов В С++ можно определить несколько функций с одним и тем же именем. Это явление называется перегрузкой имени — o verloading

  • 1.4. Переопределенные элементы подпрограмм

  • Глава 2 2.1. Классы в С++ Классы в С++ определяются с помощью одного из ключевых слов: class

  • 2.3. Конструкторы и деструкторы

  • 2.4. Стандартные конструкторы

  • 2.6. Порядок конструирования и разрушения объектов

  • 2.7. Агрегированные объекты

  • 2.8. Вложенные определения класса

  • 2.9. Друзья класса Для того, чтобы объекты некоторого класса могли получить доступ в private и protected полям другого класса, используется оператор friend

  • 2.10. Статические члены класса Поля и методы класса могут быть объявлены при помощи слова static

  • 2.12. Проблема повторяющихся базовых классов

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


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

    Глава 1

    1.1. Принципы модульного программирования на языке С++

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

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

    Очевидно, что программисту при включении h-файла другого модуля предоставляется выбор: подключить его в h-файле модуля или в cpp-файле. В данном случае предпочтение следует отдавать части реализации модуля (cpp-файл).

    При подключении h-файла следует придерживаться следующей схемы: предположим, что наш модуль называется SysModuleи состоит из двух частей: SysModule.h и SysModule.cpp. Рекомендуется следующая схема подключения:

    SysModule.h:

    #include "Config.h" // наш файл конфигурации

    // подключается первым во всех h-файлах

    // всех наших проектов

    #include "Другой стандартный модуль"

    #include "Другой наш модуль"

    SysModule.cpp:

    #include "Файл предкомпилированных заголовков"

    #include "Еще один наш модуль"

    #include "Другой стандартный модуль"

    #include "SysModule.h" // подключается последним

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

    #ifndef __SysModule_h__

    #define __SysModule_h__

    ...

    #endif //__SysModule_h__

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

    Внимание! Согласно стандарту ISO, любой h- и cpp-файл в С++ должен заканчиваться символом перевода строки.

    1.2. Пространства имен

    В больших проектах наблюдается серьезная проблема — конфликт идентификаторов. Она решается с помощью пространства имен.

    namespace Sys

    {

    int var;

    void Proc();

    }

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

    var = 10;

    Proc();

    за пределами – надо использовать полную форму записи:

    Sys::var = 10;

    Sys::Proc();

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

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

    using namespace Sys;

    Но следует отметить, что данная конструкция является причиной многих ошибок, поэтому так писать не стоит.

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

    using Sys::Proc();

    ...

    Proc();

    ...

    Но рекомендуется использовать Sys::Proc();

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

    ::Funk();

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

    namespace

    {

    ...

    }

    Пространства имен могут быть вложенными:

    namespace Sys

    {

    namespace Local

    {

    int var;

    ...

    }

    ...

    }

    Sys::Local::var = 10;

    Замечание! Когда возникает желание объявить переменный тип данных или подпрограмму внутри пространства имен, а реализовать за пределами (или наоборот), следует поступать так:

    SysModul.h: SysModul.cpp:
    namespace Sys namespace Sys

    { {

    int Proc(); int Proc();

    } {

    ...

    };

    }

    1.3. Перегрузка идентификаторов

    В С++ можно определить несколько функций с одним и тем же именем. Это явление называется перегрузкой имени — overloading.

    Эти функции должны отличаться по количеству и типу параметров:

    void print(int);

    void print(const char *);

    void print(double);

    void print(long);

    void print(char);

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

    • полное соответствие типов;

    • соответствие, достигнутое продвижением скалярных типов данных:

    bool – int

    char – int

    short – int

    float – double

    double – long double

    • соответствия, достигнутые путем стандартных преобразований:

    int – double

    double – int

    int – unsigned int;

    • соответствия, достигнутые за счет преобразований, определенных пользователем (перегрузка операторов в преобразовании типов);

    • соответствия за счет многоточия в объявлении функции.

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

    Пример:

    void TestPrint(char c, int i, short s, float f)

    {

    print(c); // char

    print(i); // int

    print(s); // int

    print(f); // double

    print(‘a’); // char

    print(49); // int

    print(0); // int

    print("a"); // const char*

    }

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

    1.4. Переопределенные элементы подпрограмм

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

    void print(int value, int base = 10); // base – система счисления

    void print(..., int base)

    {

    ...

    }

    При этом функция может вызываться либо print(100, 10) либо print(100).

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

    Глава 2

    2.1. Классы в С++

    Классы в С++ определяются с помощью одного из ключевых слов: class или struct.

    class TTextReader struct TTextReader

    { {

    ... // private ... // public

    }; };

    В С++ доступны атрибуты доступа в классах:

    • public

    • protected

    • private

    В Delphi данные секции принято употреблять в порядке: private...protected...public. В С++ их можно чередовать.

    В работе секций protected и private в Delphi и C++ есть различия:

    • В Delphi классы внутри одного модуля могут обращаться к данным и подпрограммам друг друга без ограничений. А действие секций protected и private распространяется только за пределами данного модуля.

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

    class TTextReader

    {

    friend class TList;

    };

    После этого объект класса TList может обращаться к полям из секций private и protected класса TTextReader.

    2.2. Наследование

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

    class TDelimitedReader: public TTextReader

    {

    ...

    };

    При наследовании указываются атрибуты доступа к элементам базового класса (public, protected, private). Для того чтобы понять смысл атрибута доступа к базовому классу, базовый класс следует рассматривать, как поле производного класса.

    2.3. Конструкторы и деструкторы

    Конструктор создает объект и инициализирует память для него (деструктор — наоборот).

    class TTextReader

    {

    public:

    TTextReader();

    TTextReader();

    };

    В Delphi стандартный деструктор является виртуальным. В С++ это определяет программист, если планируется создавать объекты в динамической памяти (по ссылке), деструктор необходимо делать виртуальным:

    virtual TTextReader();

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

    • по значению (на стеке):

    TTextReader reader;

    • по ссылке (в динамической памяти):

    TTextReader*reader = new TTextReader(); //оператор new служит для размещения объекта в динамической памяти

    • с помощью оператора new:

    TTextReader*reader = new (адрес ) TTextReader;

    Таким способом объект создается по ссылке по указанному адресу памяти.

    Разрушение объектов:

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

    {

    TTextReader reader;

    ...

    } // здесь происходит разрушение объекта reader при автоматическом вызове деструктора


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

    delete reader;

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

    Так выглядит динамическое создание и разрушение объектов:

    new:

    malloc();

    TTextReader();
    delete:

    TTextReader();

    free();
    2.4. Стандартные конструкторы

    Если программист не определяет в классе конструкторы, то компилятор создает автоматически два конструктора:

    • конструктор без параметров

    • конструктор копирования

    Пример:

    class TTextReader

    {

    public:

    TTextReader(); // конструктор без параметров

    TTextReader(const TTextReader&R); // конструктор копирования

    }

    Внимание! Если программист определил хотя бы один конструктор в класс — компилятор не создаст никаких стандартных конструкторов.

    Конструктор без параметров создается для того, чтобы можно было написать:

    TTextReader R;

    Конструктор копирования нужен для следующей записи:

    TTextReader R1 = R2; // означает TTextReader R1(R2);

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

    Следует отметить, что запись:

    TTextReader R1 = R2;

    и два оператора:

    TTextReader R1;

    R1 = R2;

    имеют схожий синтаксис с вызовом конструктора копирования, но разную семантику: в первом случае объект создается конструктором копирования, во втором — конструктором без параметров, а затем с помощью оператора ‘=’ выполняется присваивание одного объекта другому (данный вариант требует перегрузки оператора ‘=’ для класса TTextReader)

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

    2.5. Реализация методов класса

    Метод класса может быть реализован по месту или отдельно от класса:

    class TTextReader // по месту

    {

    public:

    TTextReader() { ... }

    TTextReader() { ... }

    };
    TTextReader::TTextReader() // отдельно от класса

    {

    ...

    }
    TTextReader::TTextReader()

    {

    ...

    }

    Если класс описан в интерфейсной части модуля, его методы рекомендуется реализовывать отдельно от класса в cpp-файле. В том случае, когда некоторый класс надо сделать inline-методом, следует писать так:

    class TTextReader

    {

    public:

    TTextReader();

    TTextReader();

    int ItemCount();

    };
    inline int TTextReader::ItemCount()

    {

    ...

    }

    2.6. Порядок конструирования и разрушения объектов

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

    class TTextReader

    {

    public:

    TTextReader();

    TTextReader();

    }
    class TDelimitedReader: public TTextReader

    {

    public:

    TDelimitedReader();

    TDelimitedReader();

    }
    TDelimitedReader::TDelimitedReader()

    {

    ...

    }
    TDelimitedReader::TDelimitedReader()

    {

    ...

    }

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

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

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

    TDelimitedReader::TDelimitedReader() : TTextReader(...)

    // в скобках записываются параметры для вызова конструктора

    {

    ...

    }

    После двоеточия разрешена запись списка операторов, разделенных запятыми. Эти операторы называются списком инициализации.

    В С++ поддерживается множественное наследование. В этом случае конструктор базовых классов вызывается автоматически в порядке их упоминания в описании класса. Деструктор же базовых классов вызывается строго в обратном порядке.

    Каждый конструктор перед началом своей работы инициализирует указатель vtable (в Delphi он называется VMT). Конструктор базового класса тоже инициализирует этот указатель. В результате этого объект как бы "рождается", сначала становясь экземпляром базового класса, а затем производного. Деструкторы выполняют противоположную операцию.

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

    2.7. Агрегированные объекты

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

    Агрегирование по значению:

    class TDelimitedReader

    {

    public:

    ...

    private:

    std::string m_FileName; // std::string – стандартный класс

    // для представления строк

    };

    Агрегированные по значению объекты конструируются автоматически в порядке объявления после вызова конструктора базового класса (если не указан другой способ инициализации).

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

    TTextReader::TDelimitedReader() : TTextReader(), m_FileName("c:/myfile.txt")

    {

    ...

    }

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

    TDelimitedReader::TDelimitedReader() : TTextReader()

    {

    m_FileName = "c:/myfile.txt";

    }

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

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

    class TDelimitedReader : public TTextReader

    {

    ...

    private:

    std::string m_FileName;

    TItems *m_Items;

    };
    TDelimitedReader::TDelimitedReader() : TTextReader(), m_FileName("c:/myfile.txt")

    {

    m_Items = new TItems;

    }
    TDelimitedReader::TDelimitedReader()

    {

    delete m_Items;

    }

    Правило конструирования агрегированных объектов:

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

    • объекты, агрегированные по ссылке, инициализируются в теле конструктора.

    2.8. Вложенные определения класса

    В С++ внутри класса можно определить другой класс:

    class TTextReader

    {

    public:

    class TItems

    {

    ...

    };

    ...

    };

    Эта запись по смыслу соответствует следующей записи:

    class TTextReader::TItems

    {

    ...

    };
    class TTextReader

    {

    friend class TTextReader::TItems; // см. ниже "Друзья класса"

    };

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

    2.9. Друзья класса

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

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

    Поля и методы класса могут быть объявлены при помощи слова static:

    class TTextReader

    {

    public:

    ...

    static char*ClassName();

    ...

    private:

    static int m_ObjectCount;

    ...

    };

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

    class TTextReader

    {

    friend char*TTextReader::ClassName();

    ...

    };
    class TTextReader::ClassName()

    {

    ...

    };
    int TTextReader::m_ObjectCount;

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

    Если метод объявляется с этим словом, то это — обычная глобальная функция, которая является другом класса. Такая функция не имеет псевдо-параметра this.

    2.11. Множественное наследование

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

    class TDelimitedReader : public TTextReader, public TStringList

    {

    ...

    };

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

    Множественное наследование имеет ряд проблем:

    • отсутствие эффективной реализации (неэффективность скрыта от программиста);

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

    • повторяющийся базовый класс в иерархии классов.

    Неоднозначность при множественном наследовании:

    class TTextReader

    {

    virtual void NextLine();

    ...

    };
    class TStringList

    {

    public:

    virtual void NextLine();

    ...

    };
    class TDelimitedReader: public TTextReader, public TStringList

    {

    ...

    };
    TDelimitedReader*Reader;

    Reader->NextLine(); // Ошибка. Неоднозначность.

    Неоднозначность возникает потому, что в классе TDelimitedReader существуют две таблицы виртуальных методов и неизвестно, к какой из них надо обращаться за методом NextLine(). Поэтому последний оператор должен быть скорректирован на следующий:

    Reader->TTextReader::NextLine();

    или:

    Reader->TStringList::NextLine();

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

    Перегрузка функций по типам аргументов не приводит к разрешению неоднозначности.

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

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

    class TTextReader

    {

    public:

    virtual void NextLine();

    ...

    };
    class TStringList

    {

    public:

    virtual void NextLine(int);

    ...

    };
    class TDelimitedReader : public TTextReader, public TStringList

    {

    public:

    using TStringList::NextLine;

    virtual void NextLine(int);

    ...

    };

    2.12. Проблема повторяющихся базовых классов

    Классы TStringList и TTextReader в нашем примере могут иметь одинаковый базовый класс, например TObject. Это приводит к следующей иерархии классов:



    В этом случае объект класса TDelimitedReader имеет две копии полей класса TObject.



    Из-за дублирования полей возникает неоднозначность при обращении к полю класса TObject из метода класса TDelimitedReader. Проблема решается с помощью уточненного имени:

    TTextReader::m_Field;

    TStringList::m_Field;

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

    На практике обычно требуется получать следующий результат:



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

    class TDelimitedReader: public TTextReader, public TStringList

    {

    ...

    };
    class TTextReader: public TObject

    {

    ...

    };
    class TStringList: virtual public TObject

    {

    ...

    };

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

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



    Если же при объявлении класса TTextReader мы запишем следующее:

    class TTextReader: virtual public TObject

    {

    ...

    };

    то структура полей будет такой:



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

    Вывод: одинарное наследование в стиле Java, C++, Delphi допустимо только от классов, множественное — от интерфейсов. Иначе можно осуществлять множественное наследование лишь от классов, в которых отсутствуют поля.

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


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