【C++进阶】了解继承

                     个人主页:CSDN_小八哥向前冲~

                      所属专栏:CSDN_C++进阶


目录

继承的概念

继承的定义格式

继承基类成员访问方式的变化

有了继承,我们可以用继承写一个简单的栈!

基类和派生类的转换

继承中的作用域

派生类的默认成员函数

实现一个不能被继承的类

继承中的友元

继承与静态函数

多继承及其菱形继承关系

继承模型

虚继承


继承的概念

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有 类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。

继承 呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的 复⽤,继承是类设计层次的复⽤

比如我们设计了两个类——学生和老师!

我们可以看到两个类里面有多个内置类型是相同的,那这其实就是冗余的,所以我们可以利用继承的手法解决这种问题!

继承的定义格式

那么怎么写继承呢?

这时,Student类继承了Person类,Student类被称为子类(派生类),Person类被称为父类(基类)。

继承基类成员访问方式的变化

我们来看个表就知道了:

再来看看几个注意事件:

  1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员 还是被继承到了派⽣类对象中但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问 它
  2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式),public >protected> private。
  4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显 ⽰的写出继承⽅式。
  5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。

我们从第二点可以看出,protected和private的区别,在前面的学习中,我们没有讲过这两者的区别,现在区别在继承中体现了!

总结:

  1. 不管什么方式继承,子类对于父类中的private成员在类外还是类里面都是不可访问的
  2. 对于父类中的public和protected成员,public继承的话,就分别是子类中的public和protected成员,protected继承的话,就都是子类的protected成员,private继承的话,就都是子类的private成员.

有了继承,我们可以用继承写一个简单的栈!

代码:

template<class T>
class stack:public std::vector<T>
{
public:
	void push(const T& x)
	{
		// 基类是类模板时,需要指定⼀下类域,
		// 否则编译报错: error C3861 : “push_back”找不到标识符
		// 因为stack<int>实例化时,也实例化vector<int>了
		// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
		vector<T>::push_back(x);
	}
	void pop()
	{
		vector<T>::pop_back();
	}
	const T& top()
	{
		return vector<T>::back();
	}
	bool empty()
	{
		return vector<T>::empty();
	}
};
int main()
{
	stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

基类和派生类的转换

我们先来了解一下大概!

  • public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
  • 基类对象不能赋值给派⽣类对象。
  • 基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。

我们用一个形象的图来理解切割:

我们写段代码试试看:

继承中的作用域

注意事项:

  1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。
  2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类 : : 基类成员显⽰访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员

如果我们理解了隐藏的概念,我们来试试一道题目!

答案: B     A

解析:1.根据上面的概念,构成隐藏。

            2.我们知道两个函数构成隐藏的话,那么基类的函数就要指定类域才能调用到!

代码中的 b.fun(10) 没错,但是 b.fun( )错了,改成b.A : : fun( );才对!

派生类的默认成员函数

6个默认函数,在派生类中如何如何生成呢?

我们来看看:

  1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
  2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化
  3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。
  4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序
  5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造
  6. . 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构
  7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我多态时会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系

这几点都是十分重要,我们要注意掌握!

我们用图来理解理解:

我们锻炼一下:

写一个十分经典的两个类!

class Person
{
public:
	Person(const char* name = "peter")//基类构造函数,这不是默认构造
		: _name(name)
	{
        cout << "Person()" << endl;
    }
    Person(const Person& p)//基类拷贝构造
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }
    Person& operator=(const Person& p)//基类赋值重载
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;
        return *this;
    }
    ~Person()//基类析构
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};
class Student : public Person
{
public:
    Student(const char* name, int num)
        //因为基类里没有默认构造函数,所以需要在初始化列表里显示调用
        : Person(name)//调用基类的构造函数
        , _num(num)
    {
        cout << "Student()" << endl;
    }
    Student(const Student& s)
        //和上同理
        : Person(s)//调用基类的拷贝构造函数
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }
    Student& operator = (const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            //同名函数,构成隐藏,所以需要显⽰调⽤
            Person::operator =(s);
            _num = s._num;
        }
        return *this;
    }
    ~Student()
    {
        cout << "~Student()" << endl;
    }
protected:
    int _num; // 学号
};

实现一个不能被继承的类

方法一:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以 后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。

方法二:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。

方法一举例:

方法二举例:

继承中的友元

友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。

这个我们自己记住就行!

继承与静态函数

基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例。

static静态成员可以直接突破类域来访问,不需要实例化对象访问

多继承及其菱形继承关系

继承模型

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承

多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题所以实践中我们也是不建议 设计出菱形继承这样的模型的。

使用多继承有时候会有这么一个问题:

我们带图来理解一下:

同时调试理解一下:

我们应该避免出现这种情况!

虚继承

有了多继承,就存在菱形继承,有了菱形继承就有 菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可 以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。

而虚继承就是来解决二义性和冗余的!关键字:virtual

依然是上面那个程序:

使⽤虚继承,可以解决数据冗余和⼆义性!

在进行构造时,person类只能被构造一次,比如:如果是先构造Teacher类,Person也会被构造,当构造Student类时,按理来说,Person类也会被构造,但是只能被构造一次,这一次的Person类构造被忽略了!

好了,我们下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值