1.继承的概念及定义
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
如下例:
class fruit
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "price:" << _price << endl;
}
protected:
string _name = "fruit"; // 名称
int _price = 5; // 价格
private:
int _aa = 0;
};
class apple : public fruit
{
protected:
string _nutrition = "维生素C";
public:
int _bb = 0;
};
int main()
{
apple a1;
a1.Print();
return 0;
}
通过监视窗口我们可以看到apple复用了fruit中的成员函数和成员变量。
继承的定义
(1) 定义格式
例:下面我们看到fruit是父类,也称作基类。apple是子类,也称作派生类
(2)继承关系和访问限定符
(3) 继承基类成员访问方式的变化
注意:
1> 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2> 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
3> 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承
2.基类和派生类对象赋值转换
1>派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切
割。寓意把派生类中父类那部分切来赋值过去。
2> 基类对象不能赋值给派生类对象
3> 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
如下程序也可说上述几点:
void test()
{
apple a;
fruit f;
//子类 = 父类 ,怎样都不行
//a = f;
//a = (apple)f; -> 强转也不行
//强制类型转换赋值给派生类的指针
apple* pap = (apple*)&f;
apple& ap = (apple&)f;
//可以是可以不过很危险,存在越界访问的风险,如下
pap->_bb = 20; //已经强制转化为父类类型指针,但是仍然可以访问子类对象中的成员
//父类 = 子类 ,可以 --> 切割, 切片
f = a;
//不存在类型转换,是语法天然支持行为
fruit* pf = &a;
pf->Print();
fruit& pfr = a;
pfr.Print();
//总结:大的可以直接赋值给小的,小的不能直接赋值给大的
}
3.继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏(跟参数没关系)。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
那下例程序运行结果是什么:
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
void test2()
{
B b;
b.fun(1);
//b.fun(); // 被隐藏了,所以调不动 -->编译报错
b.A::fun(); //指定类域就可以
};
首先:B中的fun和A中的fun不是构成重载,因为不是在同一作用域。B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。故执行b.fun(1)时没问题,而执行b.fun()则会报错(父类fun被隐藏了)。
4.派生类的默认成员函数
- 我们不写默认生成的构造函数和析构函数?
a、父类继承下来得 (调用父类默认构造和析构处理)
b、自己的(内置类型和自定义类型成员)(跟普通类一样) - 我们不写默认生成的拷贝构造和operator=?
a、父类继承下来得 (调用父类拷贝构造和operator=)
b、自己的(内置类型和自定义类型成员)(跟普通类一样)
总结:继承下来调用父类处理,自己的按普通类基本规则。
- 什么情况下必须自己写?
1、父类没有默认构造,需要我们自己显示写构造
2、如果子类有资源需要释放,就需要自己显示写析构
3、如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值解决浅拷贝问题
注意:
子类析构函数结束时,会自动调用父类的析构函数不用显示调用,所以我们自己实现子类析构函数时,不需要显示调用父类析构函数这样才能保证先析构子类成员,再析构父类成员。
5.多继承和菱形继承
1.多继承
2.菱形继承
菱形继承的问题,及解决方式。
class fruit
{
public:
string _name = "fruit"; // 名称
int _price = 5; // 价格
};
//class apple : public fruit
class apple : virtual public fruit
{
public:
string _colour; //颜色
};
//class orange : public fruit
class orange : virtual public fruit
{
public:
string _nutrition; // 营养
};
class juice : public apple, public orange
{
protected:
string _capacity; // 容量
};
void test5()
{
juice j1;
//j1._name = "juice";--> 二义性无法明确知道访问的是哪一个(需指定区域)
j1._colour = "red";
cout << sizeof(j1) << endl; //数据冗余 -->数据量多了一倍
//解决->在直接继承父类处虚继承
}
二义性:apple 和 orange分别继承了父类fruit中的数据,而juice继承了apple和orange,则juice定义的对象,会有两份重复的父类数据,在调用的时候,编译器不知道该调哪一个就会产生二义性。
数据冗余:因为存在重复虚表占的内存自然也要大一些,运行程序打印一下大小。
二义性和数据冗余解决办法就是在直接继承父类时进行虚继承(virtual),我们加虚继承后在来打印一下大小。
很明显数据量比之前小了,而此时j1._name也可以正常访问。