文章目录
- 继承的概念
- 继承的定义方式
- 继承关系和访问限定符
- 基类和派生类对象的赋值转换
- 继承中的作用域
- 派生类中的默认成员函数
- 构造函数
- 拷贝构造函数
- 赋值拷贝函数
- 析构函数
- 总结
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用。
继承中的一些概念:
基类:被一个类继承的类叫做基类。
派生类:继承另一个类的类叫做派生类。
继承的使用场景,当我们有很多个类时,很多个类都有一些共同的特性,有共同的成员变量或者成员函数时,我们就可以把这些特性封装成一个公共的基类,再通过这些类继承这个公共的基类,如果个别类有独特的特性的话,可以单独写在成员变量或者成员函数当中。
继承的定义方式
class A
{
private:
public:
int _a;
};
class B :public A
{
public:
private:
int _b;
};
上面的代码是B类继承A类,A类作为基类,B类作为派生类
继承关系和访问限定符
继承方式有三种,public继承,private继承,protected继承,继承方式可以显示写,也可以不写,如果不写的话,class的默认继承方式是private,struct默认继承方式是public。
访问限定符有三种:public,private,protected
这里有一个规则 : 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 = Min(成员在基类的访问限定符,继承方式)public > protected>private。大概规则就是在继承方式和访问限定符中取最小的一个,如果访问限定符是public,但是继承方式是private,那么最后就取private,就不能访问基类成员,只要有private就不能访问基类的成员。
注意:这里这是不能访问,但是还是继承了
可以看到这里是继承了,但是不能访问
如果对访问方式还是不清楚的,可以看看上面的表格,再看看上面的规则。
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。 - 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。 - 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected> private。 - 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。 - 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强
基类和派生类对象的赋值转换
class A
{
private:
public:
int _a;
};
class B :public A
{
public:
B()
{
}
private:
int _b;
};
对于上面这个类来说,我们做以下事情。
int main()
{
B b;
A a;
a = b;
A& ref = b;
A* Ptr = &b;
return 0;
}
这是不是类型转换呢?很显然a和b不是一个类型,如果我们只看赋值操作的话,可能还会认为可能是类型转换,但是如果我们看看下面的引用ref等于b的话就会打消这个念头了,我们知道类型转换会产生临时变量,临时变量具有常性,所以这里引用不能引用一个具有常性的变量,所以这里应该加const,但是实践过程当中这里并没有报错,所以这里肯定不是类型转换,这里是一个特殊规则:赋值兼容转换。
什么事赋值兼容转换呢?
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
我们可以这样理解:
赋值兼容转换可以理解为,我们将一个派生类拷贝给一个基类实际上是把派生类中的基类部分拿出来单独给基类,这里就像切片一样,把派生类切开,把基类的部分给基类,所以这里才取名为切片或者切割。
int main()
{
B b;
A a;
a = b;
A& ref = b;
A* Ptr = &b;
return 0;
}
这里如果我们 改变ref对应的成员变量相应的b中的对应的继承下来的成员变量也会变,指针也相同,这里我们讨论的是public情况下,如果是private就不能进行赋值兼容转换。
继承中的作用域
首先我们讨论一下,基类和派生类中是否可以存在相同的函数或者变量,答案是肯定的,首先作用域不同,可以存在相同的函数或者变量。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
对于上面这两个类来说,下面这个函数调用的是哪个fun呢?
void Test()
B b;
b.fun(10);
};
这里很显然调用的是B中的fun(),因为有参数,但是下面这个呢?
void Test()
B b;
b.fun();
};
上面这个调用的哪个fun呢?这里就不卖关子了,这里会报错。
这里直接会报编译错误,如果我们想调用A类中的函数怎么办呢?
我们可以像下面一样调用。
void Test()
{
B b;
b.A::fun();
};
像上面这样调用就可以直接调用A类中的相同函数。
这里我们提一下,这两个同名函数的关系,这两个同名函数构成隐藏。
派生类中的默认成员函数
构造函数
由于我们是先继承的基类的成员,所以我们实例化的时候,也是先定义的基类的成员,所以在初始化的时候我们也是先初始化基类的成员变量
可以看到这里先初始化的是基类的成员,即便我将基类的构造函数写在初始化列表的后面,根据这个也可以断定基类的成员变量是在派生类原本的成员变量之前定义的,因为初始化列表的初始化顺序是和声明的顺序一致的,这说明在派生类中继承的基类的成员是先声明的,这也说明应该先初始化基类成员再初始化派生类成员。
拷贝构造函数
class A
{
public:
A(int a = 0)
:_a(a)
{}
A(const A& a)
:_a(a._a)
{}
private:
int _a;
};
class B : public A
{
public:
B(int a = 0, int b = 0)
:_b(b)
,A(a)
{}
B(const B& b)
:_b(b._b)
,A(b)
{}
private:
int _b;
};
对于拷贝构造来说这里就要用到刚刚我们刚刚讲到的切片,也就是赋值兼容转换,如果是派生类的成员变量的话,就直接走初始化列表直接进行初始化,但是对于基类的成员这里我们直接调用基类的默认的拷贝构造函数,这里我们直接传的变量是类型为B的,这里会做一个切片,直接将属于传递过去用的是A类接收,所以这里会做一个切片,传递过去只接受了A类的部分。
赋值拷贝函数
//A类的赋值拷贝函数
A& operator=(const A& a)
{
_a = a._a;
}
//B类的赋值拷贝函数
B& operator=(const B& b)
{
_b = b._b;
A::operator=(b);
}
需要注意的是,我们在复用基类的赋值拷贝函数的时候,需要说明一下作用域,因为两个赋值拷贝函数构成隐藏,所以调用的时候会优先调用派生类的赋值拷贝函数,所以这里如果不指定作用域的话,会出现无穷递归,很明显下面就出现了无穷递归,所以这里要指定作用域。
析构函数
对于析构函数来说,首先我们要知道的是,在我们调用构造函数的时候,先初始化的是基类的成员,所以应该是基类成员先初始化,然后再初始化派生类自身的成员,所以这里应该是基类的成员先入栈,派生类再入栈,所以这里我们应该先保证先析构派生类的成员变量,再析构基类的成员变量,我们需要保证这一点,由于C++的机制,C++在析构完派生类的成员变量的时候,会直接自动调用基类的析构函数,所以我们并不用显示调用析构函数,但是如果我们也可以显示调用。
这里是因为多态的某些原因,后面细讲。
这里应该指定作用域,所以应该像下面一样调用。
但是我们可以看见,一个对象调用了两次析构,而且还是先析构的是基类成员,所以这里我们就不需要显示写出来,直接等编译器自动调用即可。
总结
继承是面向对象编程中的一个核心概念,通过它我们能够实现代码的重用和扩展。在本篇博客中,我们从继承的基本概念开始,逐步深入探讨了继承的定义方式、继承关系中的访问限定符以及基类和派生类对象之间的赋值转换。我们还讨论了继承中的作用域问题以及派生类中的默认成员函数,包括构造函数、拷贝构造函数、赋值拷贝函数和析构函数。
特别地,我们重点分析了析构函数在多态性中的作用。通过将基类的析构函数声明为虚函数,确保在基类指针指向派生类对象时,能够正确调用派生类的析构函数,从而避免资源泄露和内存管理问题。
通过这些内容的学习,我们不仅理解了继承的基本原理和实现方法,还掌握了如何在实际编程中应用这些知识,提高代码的可维护性和扩展性。希望这些内容能为大家提供有价值的参考,帮助大家更好地理解和应用继承这一重要的编程概念。