C++ 继承

一、继承方式

1、类的继承方式

继承方式有三种:
1、公有继承;
2、私有继承;
3、保护继承
默认的继承方式为私有继承

2、is-a关系

is-a(is-a-kind-of)关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行
公有继承是is-a关系

class Base
{
	//TODO
};

class Derive : public Base
{
	//TODO
};

3、has-a关系

has-a关系是一种组合关系,通过在一个类中包含其他类的对象和私有继承实现

class A
{
	//TODO
};

class B
{
	//TOD
	A a;
};

4、术语基类和派生类

基类和派生类也称为父类(对应基类)和子类(对应派生类)

二、公有继承

1、语法

class 派生类名: public 基类名
{
    派生类新增加的成员
};

2、基类成员在派生类中的访问权限

基类中所有 public 成员在派生类中为 public 属性;
基类中所有 protected 成员在派生类中为 protected 属性;
基类中所有 private 成员在派生类中不能使用。

三、保护继承

1、语法

class 派生类名: protected 基类名
{
    派生类新增加的成员
};

2、基类成员在派生类中的访问权限

基类中的所有 public 成员在派生类中为 protected 属性;
基类中的所有 protected 成员在派生类中为 protected 属性;
基类中的所有 private 成员在派生类中不能使用。

四、私有继承

1、语法

class 派生类名: private 基类名
{
    派生类新增加的成员
};

2、基类成员在派生类中的访问权限

基类中的所有 public 成员在派生类中均为 private 属性;
基类中的所有 protected 成员在派生类中均为 private 属性;
基类中的所有 private 成员在派生类中不能使用。

#include <iostream>

using namespace std;

class Base
{
protected:
    int m_a;
    char* m_str;

public:
    void show()
    {
        cout << "m_a = " << m_a << " m_str = " << m_str << endl;
    }
};

class Derive : private Base
{
private:
    int m_b;

public:
    Derive(int a, char* s, int b)
    {
        m_a = a;
        m_str = s;
        m_b = b;
    }

    void show()
    {
        cout << "m_b = " << m_b << endl;
        Base::show();
    }
};

int main()
{
    Derive d(1, "Hello", 2);
    d.show();

    return 0;
}

五、继承时的注意事项

1、派生类不能继承的函数

派生类不能继承基类的以下函数:
1、基类的构造函数
2、基类的析构函数
3、基类的拷贝构造函数
4、基类的重载运算符
5、基类的友元函数

2、如何确定基类成员的访问权限

1、 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
2、如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

3、继承时的名称隐藏问题

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的

派生类中和基类中重名的成员函数不构成重载

#include <iostream>

using namespace std;

class Base
{
protected:
    int m_a;
    char* m_str;

public:
    void show()
    {
        cout << "m_a = " << m_a << " m_str = " << m_str << endl;
    }
};

class Derive : public Base
{
private:
    int m_b;

public:
    Derive(int a, char* s, int b)
    {
        m_a = a;
        m_str = s;
        m_b = b;
    }

    void show()
    {
        cout << "m_b = " << m_b << endl;
    }
};

int main()
{
    Derive d(1, "Hello", 2);
    d.show();
    d.Base::show();

    return 0;
}

4、继承时对象的创建顺序问题

创建派生类对象时,程序先创建基类对象

5、继承时对象的析构顺序问题

创建对象时先创建基类对象再创建派生类对象,程序运行结束时先析构派生类对象再析构基类对象

#include <iostream>

using namespace std;

class Base
{
public:
    Base()
    {
        cout << "创建基类对象" << endl;
    }

    ~Base()
    {
        cout << "析构基类对象" << endl;
    }
};

class Derive : public Base
{
public:
    Derive()
    {
        cout << "创建派生类对象" << endl;
    }

    ~Derive()
    {
        cout << "析构派生类对象" << endl;
    }
};

int main()
{
    {
        Derive d;
    }

    return 0;
}

运行结果:

创建基类对象
创建派生类对象
析构派生类对象
析构基类对象

6、继承时基类成员的初始化问题

C++通过调用基类构造函数使用成员初始化列表初始化基类成员

1)、显式调用基类构造函数

class Base
{
protected:
    int m_a;
    char* m_str;

public:
    Base(int a, char* s)
    {
        m_a = a;
        m_str = s;
    }
};

class Derive : public Base
{
private:
    int m_b;

public:
    Derive(int a, char* s, int b) : Base(a, s) //调用基类构造函数
    {
        m_b = b;
    }
}

2)、调用默认基类构造函数

如果不显式调用基类构造函数,则程序将使用基类默认构造函数,此时基类数据成员的值不确定

class Base
{
protected:
    int m_a;
    char* m_str;
    
public:
};

class Derive : public Base
{
private:
    int m_b;

public:
    Derive(int a, char* s, int b)  //调用基类默认构造函数Base()
    {
        m_b = b;
    }
}

7、虚函数

基类成员函数应该分成两类:
1)、希望派生类直接继承而不需要改变的:在基类不需要做任何改变
2)、希望派生类进行覆盖重写的:在基类中声明为虚函数

8、继承和动态内存分配

当基类使用动态内存分配,重新定义赋值运算符和拷贝构造函数时,派生类是否要重新定义赋值运算符和拷贝构造函数?
1)、若派生类不使用new,则不需要重新定义析构函数、赋值运算符和拷贝构造函数
2)、若派生类使用new,此时必须要重新定义析构函数、赋值运算符和拷贝构造函数

六、多重继承

1、语法

多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

class D: public A, private B, protected C
{
    //类D新增加的成员
}

2、多继承下的构造函数

D(形参列表): A(实参列表), B(实参列表), C(实参列表)
{
    //TODO
}

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同

#include <iostream>
using namespace std;

//基类
class BaseA
{
public:
    BaseA(int a, int b);
    ~BaseA();

protected:
    int m_a;
    int m_b;
};

BaseA::BaseA(int a, int b): m_a(a), m_b(b)
{
    cout<<"BaseA constructor"<<endl;
}

BaseA::~BaseA()
{
    cout<<"BaseA destructor"<<endl;
}

//基类
class BaseB
{
public:
    BaseB(int c, int d);
    ~BaseB();

protected:
    int m_c;
    int m_d;
};

BaseB::BaseB(int c, int d): m_c(c), m_d(d)
{
    cout<<"BaseB constructor"<<endl;
}

BaseB::~BaseB()
{
    cout<<"BaseB destructor"<<endl;
}

//派生类
class Derived: public BaseA, public BaseB
{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();

public:
    void show();

private:
    int m_e;
};

Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e)
{
    cout << "Derived constructor" << endl;
}

Derived::~Derived()
{
    cout<<"Derived destructor"<<endl;
}

void Derived::show()
{
    cout << m_a << ", " 
         << m_b << ", " 
         << m_c << ", " 
         << m_d << ", " 
         << m_e << endl;
}

int main()
{
    Derived obj(1, 2, 3, 4, 5);
    obj.show();
    return 0;
}

运行结果:

BaseA constructor
BaseB constructor
Derived constructor
1, 2, 3, 4, 5
Derived destructor
BaseB destructor
BaseA destructor

3、命名冲突

当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。

4、解决命名冲突的方法

1)、在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性
2)、使各基类的成员名各不相同
3)、将基类的成员数据的访问权限说明为private,并在相应的基类中提供成员函数访问这些成员数据。这种方法并不是很实用,因为通常将基类成员数据的访问权限定义为protected,以保障类的封装性便于派生类访问

#include <iostream>
using namespace std;

//基类
class BaseA
{
public:
    BaseA(int a, int b);
    ~BaseA();

public:
    void show();

protected:
    int m_a;
    int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b)
{
    cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA()
{
    cout<<"BaseA destructor"<<endl;
}
void BaseA::show()
{
    cout<<"m_a = "<<m_a<<endl;
    cout<<"m_b = "<<m_b<<endl;
}

//基类
class BaseB
{
public:
    BaseB(int c, int d);
    ~BaseB();
    void show();

protected:
    int m_c;
    int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d)
{
    cout<<"BaseB constructor"<<endl;
}

BaseB::~BaseB()
{
    cout<<"BaseB destructor"<<endl;
}

void BaseB::show()
{
    cout<<"m_c = "<<m_c<<endl;
    cout<<"m_d = "<<m_d<<endl;
}

//派生类
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();

public:
    void display();

private:
    int m_e;
};

Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e)
{
    cout<<"Derived constructor"<<endl;
}

Derived::~Derived()
{
    cout<<"Derived destructor"<<endl;
}

void Derived::display()
{
    BaseA::show();  //调用BaseA类的show()函数
    BaseB::show();  //调用BaseB类的show()函数
    cout<<"m_e = "<<m_e<<endl;
}

int main()
{
    Derived obj(1, 2, 3, 4, 5);
    obj.display();
    return 0;
}

运行结果:

BaseA constructor
BaseB constructor
Derived constructor
m_a = 1
m_b = 2
m_c = 3
m_d = 4
m_e = 5
Derived destructor
BaseB destructor
BaseA destructor

七、虚继承

1、菱形继承

如下图所示:
在这里插入图片描述

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径

//间接基类A
class A
{
protected:
    int m_a;
};

//直接基类B
class B: public A
{
protected:
    int m_b;
};

//直接基类C
class C: public A
{
protected:
    int m_c;
};

//派生类D
class D: public B, public C
{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
    
private:
    int m_d;
};

int main()
{
    D d;
    return 0;
}

下面这条语句之所以出错,是因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

void seta(int a){ m_a = a; }  //命名冲突  

可以使用以下方法消除歧义

void seta(int a){ B::m_a = a; } //方式1
void seta(int a){ C::m_a = a; } //方式2

2、虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员

在这里插入图片描述

上图中类A称之为虚基类

//间接基类A
class A
{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A
{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A
{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C
{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
    
private:
    int m_d;
};

int main()
{
    D d;
    return 0;
}

3、虚继承时的构造函数

在虚继承中,虚基类是由最终的派生类初始化的,即类A由类D初始化。对类D来说,类A是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

#include <iostream>
using namespace std;

//虚基类A
class A
{
public:
    A(int a);

protected:
    int m_a;
};

A::A(int a): m_a(a){ }

//直接派生类B
class B: virtual public A
{
public:
    B(int a, int b);

public:

    void display();

protected:
    int m_b;
};

B::B(int a, int b): A(a), m_b(b){ }

void B::display(){
    cout << "m_a=" << m_a << ", m_b=" << m_b << endl;
}

//直接派生类C
class C: virtual public A
{
public:
    C(int a, int c);

public:
    void display();

protected:
    int m_c;
};

C::C(int a, int c): A(a), m_c(c){ }

void C::display()
{
    cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}

//间接派生类D
class D: public B, public C
{
public:
    D(int a, int b, int c, int d);

public:
    void display();

private:
    int m_d;
};

D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }

void D::display()
{
    cout << "m_a=" << m_a
         << ", m_b=" << m_b
         << ", m_c=" << m_c
         << ", m_d=" << m_d
         << endl;
}

int main()
{
    B b(10, 20);
    b.display();

    C c(30, 40);
    c.display();

    D d(50, 60, 70, 80);
    d.display();
    
    return 0;
}

运行结果:

m_a=10, m_b=20
m_a=30, m_c=40
m_a=50, m_b=60, m_c=70, m_d=80

参考:
1、《C++ Primer Plus》
2、C语言中文网

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值