11.2. Иерархия объектов. Наследование

    11.2. Иерархия объектов. Наследование Иерархия объектов До сих пор мы рассматривали в качестве содержимого объектов только переменные. При использовании на их месте объектов возникает иерархия вложенности. В ней самой нет ничего необычного, вложенные объекты можно применять как обычные данные, вызывать для них собственные методы и т.д..

    Однако технология ООП требует «распространения» по иерархии объектов сверху вниз ряда специфичных для нее действий над объектами, таких как конструирование (в т.ч. конструктор копирования), присваивание, ввод и вывод в стандартные потоки. За это отвечает внешний класс, включающий в себя объекты: при выполнении в нем перечисленных действий он должен инициировать аналогичные действия в объектах, определенных внутри него. Рассмотрим эту проблему с общих позиций.

    Класс user в качестве элементов данных включает в себя объекты классов string и date. Если объект не содержит других объектов и динамических данных (класс date), то переопределение присваивания и конструктора копирования не требуется. Для класса string, содержащего внешний динамический массив символов, требуется корректное определение конструкторов (пустого и копирования), деструктора и присваивания.

    //-------------------------------------------------112-01.cpp

    class string{ char *str;

    public: string(){ str=strdup("");}

    TEXT
            string(char *s){ str=strdup(s); }
    
            string(string &T){ str=strdup(T.str); }         // КК
    
            ~string(){ delete []str; }
    
            string &operator=(string &T){                   // Присваивание
    
                        delete []str;                                // разрушить старое
    
                        str=strdup(T.str);                        // копия ДМ источника
    
                        return *this; }                             // возврат ссылки на текущий
    
            friend ostream&operator<<(ostream &O, string &D){
    
                        O << D.str << endl; return O; }    // вывод в поток

    };

    class date{ int dd,mm,yy;

    public: date(int d=1,int m=1,int y=2000) { dd=d; mm=m; yy=y; }

    TEXT
            // КК и = переопределять не требуется
    
            friend ostream&operator<<(ostream &O, date &D){
    
                        O << D.dd << " " << D.mm << " " << D.yy << endl; return O; }

    };

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

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

    class user {

    TEXT
            string name,addr;
    
            date birth;

    public: user(){} // Конструктор без параметров

    TEXT
            // Явный вызов конструкторов для вложенных объектов
    
            // с передачей параметров от конструктора основного класса
    
            user(char nm[], char a[],int d0, int m0, int y0):
    
                        name(nm), addr(a), birth(d0,m0,y0) { }

    Далее внешний класс должен «переадресовывать» операции присваивания, ввода-вывода и конструктор копирования для всех своих внутренних компонент, в надежде на то, что они должным образом в них переопределены.

    class user{

    TEXT
            string name,addr;
    
            date birth;

    public: …

    TEXT
            user(user &T){                // Конструктор копирования - копии всех
    
                        name=T.name;  // компонент через переопределенное =
    
                        addr=T.addr;
    
                        birth=T.birth; }
    
            user &operator=(user &T){
    
                        name=T.name;  // Переопределение присваивания
    
                        addr=T.addr;      // Копирование всех компонент
    
                        birth=T.birth;      // через переопределенное =
    
                        return *this;}      // Переопределение вывода в поток
    
            friend ostream&operator<<(ostream &O, user &D){
    
                        O << D.name << D.addr << D.birth;
    
                        return O; }};

    Замечание: в 10.1 мы уже обсуждали границы ответственности транслятора и программы в отношении класса. Здесь ситуация аналогичная: транслятор отвечает только за копирование объекта основного класса «в целом», т.е. компонента в компоненту, а также конструирует вложенные объекты, используя конструктор без параметров. Все остальное должна делать программа.

    Принципиальное различие между технологическим и синтаксическим объектами распространяется на более сложные формы представления внутренних компонент. Например, если речь идет о статических массивах, содержащих объекты, то транслятор только вызывает для их элементов конструкторы без параметров и деструкторы, все остальное, в том числе и поэлементное копирование (присваивание) должна делать программа. Если речь идет о динамических структурах данных, доступных через указатели, то «ответственность» за их копирование ложится целиком на программу.

    //-------------------------------------------------112-01.cpp

    // Иерархия данных - конструирование, присваивание, копирование

    // Статический и динамический массивы внутренних объектов типа string

    #define N 10

    class user{

    TEXT
            string name,addr;
    
            date birth;
    
            int sz,n,n2;                    // Размерность и текущее кол-во элементов
    
            string *P;                       // Динамический массив string[]
    
            string Q[N];                   // Статический массив string[]

    public: user(){ sz=5; n=0; n2=0; P=new string[sz]; }

    TEXT
            user(char nm[], char a[],int d0, int m0, int y0):
    
                        name(nm), addr(a), birth(d0,m0,y0)
    
                        { sz=5; n=0; n2=0; P=new string[sz]; }
    
            user(user &T){                // Конструктор копирования - копии всех
    
                        name=T.name;  // компонент через переопределенное =
    
                        addr=T.addr;
    
                        birth=T.birth;
    
                        sz=T.sz; n=T.n; n2=T.n2;
    
                        P=new string[sz];         // Копия динамического и статического массивов
    
                        for (int i=0;i<n;i++) P[i]=T.P[i];
    
                        for (i=0;i<N;i++) Q[i]=T.Q[i];}
    
            user &operator=(user &T){
    
                        name=T.name;              // Переопределение присваивания
    
                        addr=T.addr;                  // Копирование всех компонент
    
                        birth=T.birth;                  // через переопределенное =
    
                        delete []P;                    // Копирование динамического
    
                        sz=T.sz; n=T.n; n2=T.n2;
    
                        P=new string[sz];         // и статического массивов
    
                        for (int i=0;i<n;i++) P[i]=T.P[i];
    
                        for (i=0;i<T.n2;i++) Q[i]=T.Q[i];
    
                        return *this;
    
                        }                                               // Переопределение вывода в поток
    
            friend ostream&operator<<(ostream &O, user &D){
    
                        O << D.name << D.addr << D.birth;
    
                        for (int i=0;i<D.n; i++) O << D.P[i];
    
                        for (i=0;i<D.n2; i++) O << D.Q[i];
    
                        return O; }
    
            void add(string &D){ P[n++]=D; }              // Добавление в динамический
    
            void add2(string &D){ Q[n2++]=D; }};        // и статический массивы

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

    • TEXT
         в объект производного класса включается неименованный объект базового. Это значит, что он не может быть использован в явном виде как обычный элемент данных, но с другой стороны, с его данные можно работать в контексте текущего объекта как с собственными;
    • TEXT
         данные базового класса включаются в объект производного класса (как правило, транслятор размещает их в начале объекта производного класса). Однако личная часть базового класса закрыта для прямого использования в производном классе. Это называется наследованием данных;
    • TEXT
         наследование методов заключается в том, что методы базового класса  можно вызывать и для объекта производного класса, при этом они применяются опять-таки к включенному в него объекту базового класса.

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

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

    //------- наследование как «экономия синтаксиса»

    class A {

    public: int a;

    TEXT
            void F(){ a++; }};

    class B : A {

    public: int b;

    TEXT
            void G(){ a++; b++; F(); }};

    void main(){

    TEXT
            B bb;    bb.a++; bb.b++; bb.F(); bb.G(); }

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

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

    При наследовании активно используется перекрытие методов, которое получает здесь специфический смысл перепрограммирования базового класса. Если программист определяет в производном классе метод с тем же именем, что и базовый, то последний «закрывается» для доступа через производный класс. В новом методе он может записать любые действия, в том числе вызвать метод базового класса, но уже по полному имени в виде A::F().

    //------- наследование как «перепрограммирование класса»

    class A {

    public: int a;

    TEXT
            void F(){ a++; }};

    class B {

    public: int b; // Перекрытие метода

    TEXT
            void F(){ A::F(); b++; }}; // Явный вызов перекрытого метода

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

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

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

    Аналогичные обязательства производный класс имеет и при переопределении присваивания, ввода-вывода в потоки и копирования. Он должен выполнить аналогичные действия в базовом классе. Как это сделать, будет сказано ниже (см. «Переход между базовым и производными классами»).

    //-------------------------------------------------112-02.cpp

    // Конструирование, присваивание, копирование при наследовании

    class string{ // Обычный класс строк

    TEXT
            char *str;

    public: string(){ str=strdup("");}

    TEXT
            string(char *s){ str=strdup(s); }
    
            string(string &T){ str=strdup(T.str); }        
    
            ~string(){ delete []str; }                            // КК
    
            string &operator=(string &T){                   // Присваивание
    
                        delete []str;
    
                        str=strdup(T.str);
    
                        return *this;}
    
            friend ostream&operator<<(ostream &O, string &D){
    
                        O << D.str << endl; return O; }};

    class string2 : string{ // Строка, наследующая строку

    TEXT
            char *str;          

    public: string2(){ str=strdup("");} // Вызов конструктора БК

    TEXT
            string2(char c[], char c2[]): string(c2)
    
                        { str=strdup(c); }
    
            string2(string2 &T){
    
                        *(string*)this=*(string*)&T;         // Присваивание объекта БК     
    
                        str=strdup(T.str); }
    
            string2 &operator=(string2 &T){
    
                        *(string*)this=*(string*)&T;         // Присваивание объекта БК
    
                        delete []str;
    
                        str=strdup(T.str);
    
                        return *this;}                              // Переопределение вывода в поток
    
            friend ostream&operator<<(ostream &O, string2 &D){
    
                        O << *(string*)&D << D.str << endl; return O; }
    
            };                                                          // Вызов операции в БК

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

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

    рис. 112-1. Варианты доступа при наследовании.

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

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

    • TEXT
         производный класс может «закрыть» интерфейс базового класса, т.е. сделать его невидимым. В этом случае он берет на себя полное перепрограммирование базового класса. Такое наследование называется обычным, в заголовке при этом слово public перед именем базового класса не пишется. Фактически в этом случае все три зоны доступа базового класса переносятся в личную часть производного (при этом личная часть БК становится недоступной и в БК);
    • TEXT
         производный класс может полностью сохранить интерфейс базового класса (публичное наследование). В этом случае базовый и производный класс работают совместно, причем считается, что вызов методов базового класса не может привести к некорректности части того же объекта, относящегося к производному классу. Зоны доступа при этом переносятся «один в один».

    рис. 112-2. Перенос зон доступа при обычном и публичном наследовании

    Следующий пример иллюстрирует правила переноса зон доступа (Обратите внимание, это не программа, а иллюстрация на Си++).

    // Обычное и публичное наследование

    class A { int n; // Базовый класс

    void f(); // Личная часть

    protected: int m; // Защищенная часть

    TEXT
            void q();

    public: int k; // Общая часть

    TEXT
                        void t(); };

    //------ Обычное наследование

    class B : A { // Заголовок класса

    TEXT

    public A::fun; // Явное объявление элемента общей части

    };

    //------ Содержимое производного класса

    class B { (-)int n; // Личная часть класса A недоступна

    (-)void f();

    privat: int m; // Защищенная часть перенесена в личную

    TEXT
            void q();
    
            int k;     // Общая часть перенесена в личную

    public: … void t(); // Явно перенесенный элемент общей части

    }; // в общую часть

    //------ Публичное наследование

    class B : public A { // Заголовок класса

    TEXT
            };

    //------ Содержимое производного класса

    class B { (-)int n; // Личная часть класса A недоступна

    (-)void f();

    protected: int m; // Защищенная часть перенесена в защищенную

    TEXT
            void q();

    public: int k; // Общая часть перенесена в общую

    TEXT
            void t(); };

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

    1. «Новое свойство». Имя определяемого в производном классе метода не совпадает ни с одним из известных в базовом классе. В этом случае это - «новое свойство» объекта, которое объект приобретает в производном классе.

    class a {

    public: void f() {}};

    class b : public a {

    public: void newb() {…}}; // newb() - новое свойство (метод)

    1. «Полное неявное наследование». Если в производном классе метод не переопределяется, то по умолчанию он наследуется из базового класса. Это значит, что будучи применен к объекту производного класса, он будет вызван в базовом. Определенное в базовом классе свойство не меняется.

    class a {

    public: void f() {}};

    class b : public a{

    public: // f() - унаследованное свойство

    TEXT
                                    };          //  эквивалентно void f() { a::f(); }
    1. «Полное перекрытие». Если в производном классе определяется метод, совпадающий с именем с методом базового класса, причем в теле метода отсутствует вызов одноименного метода в базовом классе, то мы имеем дело с полностью переопределенным свойством. В этом случае свойство объекта базового класса в производном классе отвергается и перепрограммируется заново.

    class a {

    public: void f() {} };

    class b : public a{

    public: void f() {...} // полностью переопределенное свойство

    TEXT
                        };
    1. «Условное наследование». Наиболее точно отражает сущность наследования. Но в перекрывающем методе производного класса обязательно имеется вызов одноименного метода базового класса - условный или безусловный. Этот прием наиболее полно соответствует принципу развития свойств объекта, поскольку свойство в производном классе является усложненным вариантом аналогичного свойства объекта базового класса.

    class a {

    public: void f() {} };

    TEXT
            class b : public a{
    
            public:   void f() {... a::f(); .... }};

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

    Представление производного класса как «развития» базового позволяет нам дать основную технологическую интерпретацию наследования в технологии ООП:

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

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

    Рассмотрим пример наследования – как простого развития сущности базового класса в производный. Базовый класс – целое без знака. Производный класс использует эту сущность (данные и методы), добавляя еще один компонент – знак числа. Все операции базового класса переопределяются с учетом этого нового представления. Производный класс не работает напрямую с данными базового, а использует только определенные в базовом классе методы.

    //------------------------------------------------------------------------------112-03.cpp

    // Класс целого без знака

    class UNS{

    protected: unsigned val;

    public:

    TEXT
            UNS(unsigned v0){ val=v0; }
    
            void add(UNS &T){ val+=T.val; }
    
            int  sub(UNS &T){                       // Вычитание без знака не всегда корректно
    
                        if (val<T.val) return 0;
    
                        val-=T.val;
    
                        return 1; }
    
            void mul(UNS &T){ val*=T.val; }
    
            void div(UNS &T){ val/=T.val; }
    
            UNS operator+(UNS &T){ UNS X=*this; X.add(T); return X; }
    
            UNS operator-(UNS &T){ UNS X=*this; X.sub(T); return X; }
    
            UNS operator*(UNS &T){ UNS X=*this; X.mul(T); return X; }
    
            UNS operator/(UNS &T){ UNS X=*this; X.div(T); return X; }
    
            friend ostream &operator<<(ostream &O, UNS &D){
    
                        O << D.val;
    
                        return O;}};

    // Класс целого со знаком

    class INT : UNS{

    TEXT
            int sign;                         // Знак числа - отдельно

    public: // Конструктор - базовый класс = абс.величина

    TEXT
            INT(int v0):UNS(v0<0 ? -v0 : v0){ sign=(v0<0); }
    
            void add(INT &T){            // Знаки одинаковые - сложение абс.величин
    
                        if (sign==T.sign) UNS::add(T);
    
                        else if (!UNS::sub(T)){     // Иначе - вычитание, если получается <0
    
                                    UNS X=T;          // надо выполнить наоборот и поменять знак
    
                                    X.sub(*this);       // (с использованием дополнительного объекта)
    
                                    *(UNS*)this=X;   // Копировать объект базового класса обратно
    
                                    sign=!sign;        // Поменять знак
    
                                    }}
    
            void sub(INT &T){                        // Вычитание - сложение с обратным знаком
    
                        T.sign=!T.sign;               // Поменять знак
    
                        add(T);
    
                        T.sign=!T.sign;}              // Поменять знак обратно
    
            void mul(INT &T){ sign=(sign!=T.sign);
    
                        UNS::mul(T);                 // Знак '-' при несовпадении знаков операндов  
    
                        }                                   // Умножение абс. значений
    
            void div(INT &T){ sign=(sign!=T.sign);
    
                        UNS::div(T);                   // Знак '-' при несовпадении знаков операндов  
    
                        }                                   // Деление абс. значений
    
            INT operator+(INT &T){ INT X=*this; X.add(T); return X; }
    
            INT operator-(INT &T){ INT X=*this; X.sub(T); return X; }
    
            INT operator*(INT &T){ INT X=*this; X.mul(T); return X; }
    
            INT operator/(INT &T){ INT X=*this; X.div(T); return X; }
    
            friend ostream &operator<<(ostream &O, INT &D){
    
                        O << (D.sign==0 ? '+' : '-') << D.val;
    
                        return O;}};

    Переходы между базовым и производным классами «Каждая селедка – рыба,

    но не каждая рыба – селедка»

    А.С. Некрасов «Приключения капитана Врунгеля».

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

    Обратное преобразование от указателя на базовый класс к указателю на производный класс может быть сделано только явно, с использованием операции преобразования типа. В Java оно носит название расширения. Преобразование будет корректным, если данный объект базового класса действительно заключен в объект того производного класса, к типу указателя которого оно выполняется: иначе возникает динамическая ошибка преобразования типов, которая не обнаруживается транслятором (о контроле за преобразованием типов см. также 11.3).

    class A {

    public: void f1();};

    class B : A {

    public: void f1(); // Переопределена в классe B

    void f2();

    };

    class С: A {

    public: void f1(); // Переопределена в классe B

    void f3();

    };

    void main(){

    B x, *pb;

    C *pc;

    A *pa = &x; // Прямое преобразование - неявное

    pa->f1(); // Вызов A::f1(), хотя внутри объекта класса B

    pb = (B*) pa; // Обратное преобразование - явное

    pb ->f2(); // Корректно, под pa был объект класса B

    pc = (C*) pa; // Некорректно, под pa был объект класса B

    pc->f3(); // Ошибка времени выполнения

    // Метод f3 будет вызван для объекта класса B

    После преобразования указателя на объект класса B в указатель на объект класса A происходит вызов функции из вложенного объекта базового класса A::f1(), хотя реально под указателем находится объект класса B. Обратно приведение pa к pb будет корректно. А вот с приведением к pc имеют место тонкости. То, что оно некорректно, ясно сразу. Но эта некорректность «вылезет» только при вызове метода f3, который определен только в классе C, а в B отсутствует. Тем не менее, он будет вызван, но программы «подсунет» ему в качестве текущего объекта объект класса B.

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

    Если «рыба» - это базовый класс, а «селедка» и «камбала» - производные, то расширение – это получение указателем на объект класса «рыба» адреса объекта класса «селедка», что всегда является корректным. Обратное приведение (сужение) именно этого указателя к классу «селедка» будет корректным, а к классу «камбала» - ошибочным. Как говорится: «Каждая селедка – рыба, но не каждая рыба - селедка».

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

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

    //---------------------------------------------------112-05.cpp

    //---------------------------------------------------1

    class A {

    protected: int a1;

    public: int a2;

    TEXT
                        void f();};

    class B : public A {

    TEXT
                        int b;

    public: void g();};

    class C : B {

    public: void h();};

    void main(){ B x; x.b++; } // 1

    void B::g(){ a1++; } // 2

    void B::g(){ a2++; } // 3

    void C::h(){ a1++; } // 4

    void C::h(){ a2++; } // 5

    //----------------------------------------------------2

    class A { void f();

    protected: int a1;

    public: int a2;};

    class B : public A {

    public: int b;

    TEXT
                        void g();};

    class C : public B {

    public: void h();};

    void main(){ B x; x.a2++; } // 1

    void main(){ C x; x.a2++; } // 2

    void main(){ B x; x.a1++; } // 3

    void main(){ C x; x.a1++; } // 4

    void main(){ C x; x.b++; } // 5

    //---------------------------------------------------3

    class A { void f1();

    protected: void f2();

    public: void f3();};

    class B : public A {

    public: void g();};

    class C : public B {

    public: void h();};

    void A::f2(){ f1(); } // 1

    void B::g(){ f1(); } // 2

    void B::g(){ f2(); } // 3

    void C::h(){ f1(); } // 4

    void C::h(){ f2(); } // 5

    //---------------------------------------------------4

    class A { void f1();

    protected: void f2();

    public: void f3();};

    class B : A {

    public: void g();};

    class C : B {

    public: void h();};

    void B::g(){ f2(); } // 1

    void C::h(){ f2(); } // 2

    void B::g(){ f3(); } // 3

    void C::h(){ f3(); } // 4

    void C::h(){ B::g(); } // 5

    //---------------------------------------------------5

    class A { int a1;

    protected: int a2;

    public: int a3;};

    class B : public A {

    protected: int b;

    public: void g();};

    class C : B {

    public: void h();};

    void B::g(){ a2++; } // 1

    void C::h(){ a1++; } // 2

    void C::h(){ a2++; } // 3

    void C::h(){ a3++; } // 4

    void C::h(){ b++; } // 5

    Сформулируйте сущности базового и производного класса, исходя из этого, объясните форматы данных и особенности переопределения операций. Определите значения объектов и переменных после выполнения main.

    //---------------------------------------------------112-06.cpp

    //---------------------------------------------------1

    class A{ int val;

    public: A(int n=0) { val = n; }

    TEXT
            int get() { return val; }
    
            A operator++(){ A t=*this; val++; return t; }};

    class INC : public A {

    public: INC(int n) : A(n+1) {}

    TEXT
            int get() { return A::get()+1; }};

    void main(){ A a(10); INC b(12);

    A x=a++; A y=b++;

    int z=(b++).get(); }

    //---------------------------------------------------2

    class A{ int val;

    public: A(int n=0) { val = n; }

    TEXT
            A operator++(){ A t=*this; val++; return t; }};

    class INC : public A {

    public: INC(int n) : A(n+1) {}

    TEXT
            INC operator++(){ A::operator++(); return *this; }};

    void main(){ A a(10); INC b(12);

    A x=a++; INC y=b++;

    A z=((A)&b)++;}

    //---- Общий код для 3-5,7,8,10

    class A{

    protected: int val;

    public: A(int n=0) { val = n; }

    TEXT
            A operator+(A &two){ A t=*this; t.val+=two.val; return t; }
    
            A operator-(A &two){ A t=*this; t.val-=two.val; return t; }
    
            A &operator*(A &two){ val*=two.val; return *this; }
    
            A &operator/(A &two){ val/=two.val; return *this; }};

    //---------------------------------------------------3

    class ABS : public A {

    public: ABS(int n=0) : A(n<0 ? -n : n) {}

    TEXT
            ABS operator-(ABS &two){
    
                        ABS t=*this; t.val-=two.val;
    
                        if (t.val<0) t.val=-t.val;
    
                        return t; }};

    void main(){ A a1(10),a2(-5),a3,a4; ABS b1(-6),b2(12),b3,b4;

    a3=a1+a2; a4=a1-a2;

    b3=(ABS)&(b1+b2);

    b4=b1-b2;

    }

    //---------------------------------------------------4

    class ABS : public A {

    public: ABS(int n=0) : A(n<0 ? -n : n) {}

    TEXT
            ABS &operator-(ABS &two){
    
                        val-=two.val;
    
                        if (val<0) val=-val;
    
                        return *this; }};

    void main(){

    A a1(10),a2(-5),a3,a4,a5; ABS b1(-6),b2(12),b3,b4;

    a1+a2; a2-a1; b1+b2; b2-b1; }

    //---------------------------------------------------5

    class ABS : public A {

    public: ABS(int n=0) : A(n<0 ? -n : n) {}

    TEXT
            ABS &operator+(ABS &two){
    
                        A::operator+(two);
    
                        return *this; }
    
            ABS &operator-(ABS &two){
    
                        A::operator-(two);
    
                        if (val<0) val=-val;
    
                        return *this; }};

    void main(){ A a1(10),a2(-5),a3,a4; ABS b1(-6),b2(12),b3,b4;

    a1+a2; a2-a1; b1+b2; b2-b1; }

    //---- Общий код для 6,9

    class A{

    protected: int val;

    public: A(int n=0) { val = n; }

    TEXT
            A &operator+(A &two){ val+=two.val; return *this; }
    
            A &operator-(A &two){ val-=two.val; return *this; }};

    //---------------------------------------------------6

    class SIGN : public A {

    TEXT
            int s;

    public: SIGN(int n=0) : A(n<0 ? -n : n) { s=(n<0); }

    TEXT
            SIGN &operator+(SIGN &two){

    if (s==two.s) A::operator+(two);

    TEXT
                        else {
    
                                    A::operator-(two);
    
                                    if (val<0) { val=-val; s=!s; }}
    
                        return *this; }
    
            SIGN &operator-(SIGN &two){
    
                        two.s=!two.s;
    
                        operator-(two);
    
                        two.s=!two.s;
    
                        return *this; }};

    void main(){ A a1(10),a2(5),a3,a4; SIGN b1(-6),b2(12),b3,b4;

    a1+a2; a2-a1; b1+b2; b2-b1; }

    //---------------------------------------------------7

    class SIGN : public A {

    TEXT
            int s;

    public: SIGN(int n=0) : A(n<0 ? -n : n) { s=(n<0); }

    TEXT
            SIGN &operator*(SIGN &two){
    
                        s=(s!=two.s);
    
            A::operator*(two);
    
                        return *this; }
    
            SIGN &operator/(SIGN &two){
    
                        s=(s!=two.s);
    
                        A::operator/(two);
    
                        return *this; }};

    void main(){ A a1(10),a2(5),a3,a4; SIGN b1(-6),b2(12),b3,b4;

    a1a2; a2/a1; b1b2; b2/b1; }

    //---------------------------------------------------8

    class SIGN : public A {

    TEXT
            int s;

    public: SIGN(int n=0) : A(n<0 ? -n : n) { s=(n<0); }

    TEXT
            SIGN &operator*(SIGN &two){
    
                        s=(s!=two.s);
    
                        A::operator*(two);
    
                        return *this; }
    
            SIGN &operator/(SIGN &two){
    
                        s=(s!=two.s);
    
                        A::operator/(two);
    
                        return *this; }};

    void main(){ A a1(10),a2(15),a3,a4; SIGN b1(-6),b2(12),b3,b4;

    a1A(2); a2/A(-3); b1SIGN(7); b2/SIGN(-2); }

    //---------------------------------------------------9

    class MOD : public A {

    TEXT
            int s;

    public: MOD(int n=0, int m=10) : A(n)

    TEXT
                        { s=m; if (val>=s) val%=s; else while(val<0) val+=s; }
    
            MOD &operator+(MOD &two){
    
                        A::operator+(two);
    
                        if (val>=s) val%=s; else while(val<0) val+=s;
    
                        return *this; }
    
            MOD &operator-(MOD &two){
    
                        A::operator-(two);
    
                        if (val>=s) val%=s; else while(val<0) val+=s;
    
                        return *this; }};

    void main(){

    A a1(10),a2(15),a3,a4; MOD b1(6,10),b2(12,20),b3(-6,10),b4;

    a1+A(2); a2-A(-3); b1+MOD(7,10); b2-MOD(15,20); b3-MOD(6,10); }

    //---------------------------------------------------10

    class MOD : public A {

    TEXT
            int s;

    public: MOD(int n=0, int m=10) : A(n)

    TEXT
                        { s=m; if (val>=s) val%=s; else while(val<0) val+=s; }
    
            MOD &operator*(MOD &two){
    
                        A::operator*(two);
    
                        while(val>=s) val-=s;
    
                        return *this; }};

    void main(){

    A a1(10),a2(15),a3,a4; MOD b1(6,10),b2(15,20),b3(-6,10),b4;

    a1A(2); a2/A(-3); b1MOD(7,10) b2/MOD(25,20); b3/MOD(2,10); }