c++继承

一、继承方式介绍

在C++中,继承是面向对象编程的重要概念,通过继承可以创建一个新类,该类继承了另一个已存在的类的特性和行为。C++中的继承方式主要有三种:public(公有的)、protected(受保护的)和private(私有的)。这些继承方式决定了派生类中如何访问基类的成员。

1.1 访问权限说明

C++中类成员的访问权限从高到低依次为:

  1. public:成员在类内外都可以访问。
  2. protected:成员只能在类的成员函数中访问,且在派生类中可以访问基类的protected成员。
  3. private:成员只能在类的成员函数中访问,不能被类外或派生类访问。

1.2 继承方式对访问权限的影响

在这里插入图片描述

  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。
    • 当继承方式为protected时,基类成员在派生类中的访问权限最高也为protected。
    • 当继承方式为public时,基类成员在派生类中的访问权限将保持不变。
  2. 基类中的private成员在派生类中始终不能使用。
  3. 合理选择基类成员的访问权限。
    • 希望基类成员被派生类继承并使用时,声明为public或protected。
    • 不希望在派生类中使用的成员可以声明为private。
  4. private和protected继承方式的影响较复杂,一般使用public继承。
    • private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂。
  5. 在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。
// 基类定义
class Base {
public:
    int publicMember;
    Base() : publicMember(0), protectedMember(0), privateMember(0) {}

    void PublicFunction() {
        // 可以访问所有成员
        publicMember = 1;
        protectedMember = 2;
        privateMember = 3;
    }

protected:
    int protectedMember;

private:
    int privateMember;
};

// 公有继承
class PublicDerived : public Base {
public:
    void AccessBaseMembers() {
        publicMember = 10;      // 可以访问public成员
        protectedMember = 20;   // 可以访问protected成员
        // privateMember = 30;   // 不能访问private成员
    }
};

// 受保护继承
class ProtectedDerived : protected Base {
public:
    void AccessBaseMembers() {
        publicMember = 10;      // 可以访问public成员
        protectedMember = 20;   // 可以访问protected成员
        // privateMember = 30;   // 不能访问private成员
    }
};

// 私有继承
class PrivateDerived : private Base {
public:
    void AccessBaseMembers() {
        publicMember = 10;      // 可以访问public成员
        protectedMember = 20;   // 可以访问protected成员
        // privateMember = 30;   // 不能访问private成员
    }
};

int main() {
    PublicDerived pubDerived;
    pubDerived.publicMember = 100;     // 可以访问public成员
    // pubDerived.protectedMember = 200; // 不能访问protected成员

    ProtectedDerived protDerived;
    // protDerived.publicMember = 100; // 不能访问public成员
    // protDerived.protectedMember = 200; // 不能访问protected成员

    PrivateDerived privDerived;
    // privDerived.publicMember = 100; // 不能访问public成员
    // privDerived.protectedMember = 200; // 不能访问protected成员

    return 0;
}
  1. 使用using关键字改变访问权限
    在派生类中,可以使用using关键字来改变基类成员的访问权限。注意,using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为private成员在派生类中是不可见的。
    class Derived : public Base {
    public:
        using Base::publicMember; // 改变public成员的访问权限为派生类中的public
    
        void AccessPublicMember() {
            publicMember = 10; // 现在可以访问基类的public成员
        }
    };
    

二、继承的对象模型解释

2.1 创建派生类对象时的构造函数调用顺序

在创建派生类的对象时,首先会调用基类的构造函数,然后再调用派生类的构造函数。这是因为派生类需要继承基类的成员,并且在初始化时确保基类的构造过程完成。

2.2 销毁派生类对象时的析构函数调用顺序

在销毁派生类的对象时,先会调用派生类的析构函数,然后再调用基类的析构函数。这样确保对象从派生类向基类的方向进行清理操作。

class Base {
public:
    Base(){
        cout<<"基类构造函数"<<endl;
    }
    ~Base(){
        cout<<"基类析构函数"<<endl;
    }
};

class Derived:public Base {
public:
    Derived(){
        cout<<"派生类构造函数"<<endl;
    }
    ~Derived(){
        cout<<"派生类析构函数"<<endl;
    }
};


int main() {
    Derived derived;
}

输出:

基类构造函数
派生类构造函数
派生类析构函数
基类析构函数

2.3 派生类对象的内存分配和this指针

创建派生类对象时,只会申请一次内存,派生类对象会包含基类对象的内存空间,并且派生类对象和基类对象共享相同的this指针。

2.4 派生类对象的初始化顺序

在创建派生类对象时,首先会初始化基类对象,然后再初始化派生类对象。

  • 首先,会先初始化基类对象。这意味着基类的构造函数会被调用,基类的成员变量会被初始化。
  • 然后,在基类对象初始化完成后,会初始化派生类对象。这时派生类的构造函数会被调用,派生类的成员变量会被初始化。

2.5 使用sizeof获取派生类对象的大小

对于派生类对象,使用sizeof运算符得到的大小包括了基类的所有成员(包括私有成员)以及派生类对象自己的成员。派生类只是无法看到和操作基类的private成员,但是还是包含在内存中。

2.6 继承方式的访问权限

在C++中,不同的继承方式(public、protected、private)在语法上处理访问权限,但在内存布局和对象模型方面没有区别。
在这里插入图片描述

void *operator new(size_t size) {

    void *p = malloc(size);
    cout << "申请到的内存地址为:" << p << ",大小为:" << size << endl;
    return p;
}

void operator delete(void *p) {
    if(p != nullptr){
        cout << "释放内存" << endl;
        free(p);
    }
}
class Base {
public:
    int a = 1;
protected:
    int b = 2;
private:
    int c = 3;
public:
    Base() {
        cout << "Base中this的地址是:" << this << endl;
        cout << "Base中a的地址是:" << &a << endl;
        cout << "Base中b的地址是:" << &b << endl;
        cout << "Base中c的地址是:" << &c << endl;
    }

    void fun1() {
        cout << "a=" << a << endl;
        cout << "b=" << b << endl;
        cout << "c=" << c << endl;
    }

};

class Derived : public Base {
public:
    int d = 4;

    Derived() {
        cout << "Derived中this的地址是:" << this << endl;
        cout << "Derived中a的地址是:" << &a << endl;
        cout << "Derived中b的地址是:" << &b << endl;
        cout << "Derived中d的地址是:" << &d << endl;
    }

    void fun2() {
        cout << "d=" << d << endl;
    }
};


int main() {
    cout << "Base占用内存大小是:" << sizeof(Base) << endl;
    cout << "Derived占用内存大小是:" << sizeof(Derived) << endl;

    Derived *derived = new Derived;
    derived->fun1();
    derived->fun2();
}

输出:

Base占用内存大小是:12
Derived占用内存大小是:16
申请到的内存地址为:0x600003ca8040,大小为:16
Base中this的地址是:0x600003ca8040
Base中a的地址是:0x600003ca8040
Base中b的地址是:0x600003ca8044
Base中c的地址是:0x600003ca8048
Derived中this的地址是:0x600003ca8040
Derived中a的地址是:0x600003ca8040
Derived中b的地址是:0x600003ca8044
Derived中d的地址是:0x600003ca804c
a=1
b=2
c=3
d=4

2.7 奇巧淫技

可以通过指针操作private变量,不推荐使用。

class Base {
private:
    int c = 3;
public:
    void show() {
        cout << c << endl;
    }
};


int main() {
    Base *base = new Base;
    base->show();//3
    (*(int *)(base))= 4;
    base->show();//4
}

三、派生类如何构造基类

  1. 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
  2. 如果没有指定基类构造函数,将使用基类的默认构造函数。
  3. 可以使用初始化列表指明要使用的基类构造函数。
  4. 基类构造函数负责初始化被继承的数据成员,派生类构造函数主要用于初始化新增的数据成员。
  5. 派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
class Base {
private:
    int a;
public:
    int b;
    Base():a(0),b(0) {
        cout<<"调用了Base的默认构造函数Base()"<<endl;
    }
    Base(int a, int b):a(a),b(b) {
        cout<<"调用了Base的构造函数Base(int a, int b)"<<endl;
    }
    Base(const Base& base):a(base.a),b(base.b) {
        cout<<"调用了Base的拷贝构造函数Base(const Base& base)"<<endl;
    }
};

class Derived: public Base {
public:
    int c;
    Derived():c(0) {
        cout<<"调用了Derived的默认构造函数Derived()"<<endl;
    }
    Derived(int a, int b, int c):Base(a,b),c(c) {
        cout<<"调用了Derived的构造函数Derived(int a, int b, int c)"<<endl;
    }
    //尽管 Derived 和 Base 是不同的类型,但是由于派生类对象包含了基类对象的部分,所以可以在这种情况下进行对象之间的初始化和拷贝。
    Derived(const Derived& derived):Base(derived),c(derived.c) {
        cout<<"调用了Derived的拷贝构造函数Derived(const Derived& derived)"<<endl;
    }
};
int main() {
    Derived d1;

    Derived d2(1,2,3);

    Derived d3(d2);
}

输出:

调用了Base的默认构造函数Base()
调用了Derived的默认构造函数Derived()
调用了Base的构造函数Base(int a, int b)
调用了Derived的构造函数Derived(int a, int b, int c)
调用了Base的拷贝构造函数Base(const Base& base)
调用了Derived的拷贝构造函数Derived(const Derived& derived)

四、名字遮蔽与类作用域

4.1名字遮蔽和作用域

如果在派生类中的成员(变量或函数)与基类中的成员重名,派生类的成员将遮蔽(隐藏)基类的成员。这意味着当通过派生类对象或在派生类的成员函数中使用该成员时,将使用派生类的版本而不是基类的。

class Base {
public:
    int a=1;
    void show(){
        cout<<"Base的show"<<endl;
    }
};

class Derived: public Base {
public:
    int a=2;
    void show(){
        cout<<"Derived的show"<<endl;
    }
};

int main() {
    Derived d;
    cout<<d.a<<endl;
    d.show();
}

输出:

2
Derived的show

4.2 遮蔽同名函数

注意,基类的成员函数和派生类的成员函数不会构成重载。如果派生类有与基类同名的函数,那么派生类中的函数将遮蔽基类中的所有同名函数。

class Base {
public:
    int a=1;
    void fun(){
        cout<<"无参fun"<<endl;
    }
    void fun(int b){
        cout<<"有参fun"<<endl;
    }
};

class Derived: public Base {
public:
    int a=2;
    void fun(int b){
        cout<<"派生类fun"<<endl;
    }
};

int main() {
    Derived d;
    d.fun();//报错,因为派生类已经重写了fun,遮蔽了基类中所有的同名函数
    d.fun(1);
}

4.3 类作用域和成员访问

每个类都有自己的作用域,在这个作用域内定义成员。在类的作用域之外,普通的成员只能通过对象(对象本身、对象指针或对象引用)来访问,而静态成员可以通过对象访问,也可以通过类名访问。

4.4 使用域解析符

通过在成员名前面加上类名和域解析符(::),可以访问特定作用域的成员。在没有继承关系的情况下,类名和域解析符可以省略。

4.5 继承关系中的作用域

在存在继承关系时,派生类的作用域会嵌套在基类的作用域中。如果成员在派生类的作用域中找到,就不会在基类的作用域中继续查找;如果在派生类作用域中未找到,则会在基类作用域中继续查找。

4.6 直接使用作用域成员

通过在成员名前面加上类名和域解析符,可以直接使用特定作用域的成员。

class Base {
public:
    int a=1;
    void show(){
        cout<<"Base的show"<<endl;
    }
};

class Derived: public Base {
public:
    int a=2;
    void show(){
        cout<<"Derived的show"<<endl;
    }
};

int main() {
    Derived d;

    cout<<d.Base::a<<endl;
    d.Base::show();

    cout<<d.Derived::a<<endl;
    d.Derived::show();
}

输出:

1
Base的show
2
Derived的show

五、继承的特殊关系

  1. 派生类对象可以使用基类成员。

  2. 派生类对象可以赋值给基类对象,但会丢失非基类的成员。

  3. 基类指针可以指向派生类对象。

  4. 基类引用可以引用派生类对象。

    #include <iostream>
    using namespace std;
    class Base {
    public:
        int Var=1;
        void fun() {
            cout << "父类Function" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        int Var=2;
        void fun() {
            cout << "子类Function" << endl;
        }
    };
    
    int main() {
        Derived derivedObj;
    
        // 1. 使用基类成员
        derivedObj.fun();
        cout<<derivedObj.Var<<endl;
    
        // 2. 派生类对象赋值给基类对象
        Base baseObj = derivedObj; // 非基类成员会被舍弃
        baseObj.fun(); // 父类Function
        cout<<baseObj.Var<<endl;
    
        // 3. 基类指针指向派生类对象
        Base* basePtr = &derivedObj;
        basePtr->fun(); // 父类Function
        cout<<basePtr->Var<<endl;
    
        // 4. 基类引用引用派生类对象
        Base& baseRef = derivedObj;
        baseRef.fun(); // 父类Function
        cout<<baseRef.Var<<endl;
    
        return 0;
    }
    

    输出:

    子类Function
    2
    父类Function
    1
    父类Function
    1
    父类Function
    1
    
  • 注意:
    1. 基类指针或引用只能调用基类的方法,不能调用派生类的方法。

      #include <iostream>
      
      class Base {
      public:
          void method() {
              std::cout << "Base Method" << std::endl;
          }
      };
      
      class Derived : public Base {
      public:
          void method() {
              std::cout << "Derived Method" << std::endl;
          }
      };
      
      int main() {
          Derived derivedObj;
          Base* basePtr = &derivedObj;
      
          basePtr->method();  // 调用基类的方法,而不是派生类的方法
      
          return 0;
      }
      
    2. 可以使用派生类构造基类对象。

      class Base {
      public:
          int value;
      
          Base(int val) : value(val) {}
      };
      
      class Derived : public Base {
      public:
          Derived(int val) : Base(val * 2) {}
      };
      
      int main() {
          Derived derivedObj(5);
          Base baseObj = derivedObj;  // 使用派生类构造基类对象
      
          return 0;
      }
      
    3. 如果函数的形参是基类,可以使用派生类作为实参。

      #include <iostream>
      
      class Base {
      public:
          virtual void print() {
              std::cout << "Base" << std::endl;
          }
      };
      
      class Derived : public Base {
      public:
          void print() override {
              std::cout << "Derived" << std::endl;
          }
      };
      
      void foo(Base base) {
          base.print();
      }
      
      int main() {
          Derived derivedObj;
          foo(derivedObj);  // 使用派生类对象作为基类实参,输出:Base
      
          return 0;
      }
      
    4. C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只 是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。

      class Base {
      public:
          void method() {}
      };
      
      class Derived : public Base {
      public:
          void anotherMethod() {}
      };
      
      int main() {
          Base baseObj;
          Derived derivedObj;
      
          // 不允许将基类对象赋给派生类引用和指针
          // Derived& dRef = baseObj;  // 错误
          // Derived* dPtr = &baseObj;  // 错误
      
          // 允许将派生类对象赋给基类引用和指针
          Base& bRef = derivedObj;
          Base* bPtr = &derivedObj;
      
          return 0;
      }
      

六、多继承和虚继承(不推荐使用)

多继承是面向对象编程中的一个概念,它允许一个派生类从多个基类继承属性和方法。然而,多继承在实际使用中可能会引发一些问题,比如菱形继承问题,即某个派生类通过两个不同的路径继承同一个基类,从而导致二义性和数据冗余。虚继承是解决菱形继承问题的一种方式。

  • 多继承的语法

    class Base1 {
    public:
        int a=1;
    };
    
    class Base2 {
    public:
        int b=2;
    };
    
    class Derived : public Base1, public Base2 {
    public:
        int c=3;
    };
    
    int main() {
        Derived derivedObj;
        std::cout << derivedObj.a << std::endl;
        std::cout << derivedObj.b << std::endl;
        std::cout << derivedObj.c << std::endl;
        return 0;
    }
    
  • 多继承存在问题:菱形继承
    在这里插入图片描述

    1. 数据冗余:类D中包含两个m_a
    2. 名称二义性:直接使用D中的m_a不知道是用的B中的还是C中的
      class A {
      public:
          int a=1;
      };
      
      class B : public A{
      };
      
      class C : public A{
      };
      
      class D:public B, public C{
      
      };
      
      
      int main() {
          D d;//多继承后,会有2个a,所以需要虚继承
          cout <<sizeof(d)<< endl;//8
      //    cout <<d.a<< endl;//报错:a的访问不明确
          //可以使用作用于限定符,可以看出地址不同
          cout <<&(d.B::a)<< endl;//0x16aeeac14
          cout <<&(d.C::a)<< endl;//0x16aeeac18
          return 0;
      }
      

    解决多继承存在的访问不明确这个问题,可以使用虚继承(虚继承中涉及到虚基类指针等东西,这里暂不介绍):

    class A {
    public:
        int a=1;
    };
    
    class B : virtual public A {
    };
    
    class C : virtual public A{
    };
    
    class D:public B, public C{
    
    };
    
    
    int main() {
        D d;//虚继承后,只会有1个a
        cout <<d.a<< endl;//不报错
        //可以看出地址相同
        cout <<&(d.B::a)<< endl;//0x16f2eac10
        cout <<&(d.C::a)<< endl;//0x16f2eac10
        return 0;
    }
    

    注意:
    不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问题就不要使用多继承。
    如果继承的层次很多、关系很复杂,程序的编写、调试和维护工作都会变得更加困难,由于这个原因,C++之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值