继承
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承不仅继承父类的成员变量,还继承父类的成员函数
继承是类设计层次的复用
简单来说,继承就是让子类使用父类的成员
class Person//父类
{
public:
void print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name="David";
int _age=18;
};
class Student:public Person//子类
{
protected:
int _stuid;//学号
};
class Teacher:public Person//子类
{
protected:
int _tuid;//工号
};
继承方式
继承关系和访问限定符
所以对应的,继承有9种关系
首先来理解不可见,不可见的意思是父类中的私有成员变量还是被继承到了子类当中,但是因为语法的限制,父类的私有成员变量在子类对象的外部还是内部都不可以访问
子类对象外部:
内部:
但是父类的私有成员变量还是被继承到了子类对象
上面就解决了三种关系,其余六种我们只要知道public > protected > private
保护和私有在父类中没有区别;在子类中,private成员不可见,protected成员是可见的
常见继承的使用
父类成员:公有和保护
子类继承方式:公有继承
class默认访问限定符是private;struct 默认访问限定符是public
class继承中默认继承方式是priavte继承;struct继承默认public继承(跟访问限定符一样,最好写出来,不用默认的)
父类和子类对象赋值转换
子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用。这里有个说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去。
这里不存在类型转换,是语法天然支持的行为
int main()
{
Person p;
Student s;
//这里不存在类型转换
//父类=子类;赋值兼容->切割 切片
p = s;
Person* ptr = &s;
Person& ref = s;
//类型转换,中间会产生临时变量
int i = 1;
double d = 1.2;
i = d;
//int& ri = d;//这里会产生临时变量,临时变量具有常性,所以不能赋值给引用
//要想用引用来接收,加个const就可以
const int& ri = d;
return 0;
}
私有继承不支持切割/切片,这里涉及权限。
如果子类是私有继承,那么父类的成员继承给子类,这些成员在子类里就是私有的。
父类对象不能赋值给子类对象
比较bug的是指针和引用可以,但是很危险,很有会有越界的风险
//子类=父类
//s=p;//无论怎样都不行
Student* pptr = (Student*)&p;
Student& rref = (Student&)p;
继承的作用域
c语言中我们知道有就近原则,如果全局变量和局部变量名字相同,优先使用局部变量
如果想使用全局变量就得指明作用域 ::
//全局变量
int a = 10;
int main()
{
int a=1;
cout << ::a << endl;//打印全局变量
return 0;
}
在继承中也有就近原则
子类和父类出现同名成员:隐藏/重定义
常见题目
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B:public A
{
public:
void func(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
B b;
b.func(12);
return 0;
}
子类B虽然会继承A类中的func(),但是他们不构成函数重载,因为函数的重载必须在同一作用域内;
A和B的func构成函数隐藏,成员函数只要函数名相同就构成隐藏,参数可以相同也可以不相同
当调用b.func(12)时
如果b.func();
(不带参数)编译器就会报错,因为继承A的func已经被隐藏了。
如果想调用A中的func()就可以指定类域b.A::func();
子类默认成员函数
- 从父类继承下来的调用父类的构造和析构
自己的,如果是内置类型不处理,自定义类型调用默认的构造和析构(跟普通类一样) - 从父类继承下来的调用父类的拷贝构造和父类的
operator=
自己的,内置类型内置类型完成浅拷贝,自定义类型完成它的拷贝构造和赋值(跟普通类一样)
总结:原则是继承下来的调用父类处理,自己的按普通类基本规则
什么情况下需要我们自己写?
- 当父类没有默认的拷贝构造需要我们自己写构造
- 如果子类有资源需要释放,就需要自己写析构
- 如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值
如何自己写?
父类成员调用父类对应构造,拷贝构造,operator=和析构处理
自己成员按普通类处理
析构函数的名字会被同一处理成destructor()
那么子类的析构函数和父类的析构函数就会构成隐藏
子类析构函数结束时,会自动调用父类的析构函数,所以我们一般不用显示子类中父类的析构函数
子类析构函数结束时,会自动调用父类的析构函数
所以我们自己实现子类析构函数时,不需要显示调用父类的析构函数
子类自己的成员就按以前(普通类)的方式处理,继承的成员,调用父类几个默认成员函数处理.
友元和继承
友元关系不能被继承
继承和静态成员
静态成员继承下来,子类和父类访问的是同一个,整个继承体系里只有这样的成员,无论派生出多少个子类,都只有这一个static成员
菱形继承
单继承:一个子类只有一个父类时叫单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承
class Person
{
public:
string _name; // 姓名
};
class Teacher : public Person
{
public:
int id;
};
class Student : public Person
{
public:
protected:
int _num; //学号
};
class Assistant :public Student, public Teacher
{
protected:
string _majorCourse;
};
int main()
{
Assistant a;
a.Student::_name="小李";
a.Teacher::_name = "李四";
return 0;
}
解决的方法有:1.指定作用域解决了二义性,但还是有数据冗余
Assistant a;
a.Student::_name="小李";
a.Teacher::_name = "李四";
2.虚继承(直接继承父类的地方用)可以解决数据冗余和二义性
class Teacher :virtual public Person
{
public:
int id;
};
class Student :virtual public Person
{
public:
protected:
int _num; //学号
};
int main()
{
Assistant a;
a._name = "张三";
return 0;
}
继承和组合
B可以直接访问A的公有和保护成员
D只有使用C的公有,不能直接访问保护成员
public继承是一种is-a的关系。也就是说每个子类对象都是一个父类对象。(比如学生和人)
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。(比如车子和轮胎)
优先使用对象组合,而不是类继承
继承通常时是白箱复用,继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
组合通常是黑箱复用, 组合类之间没有很强的依赖关系,耦合度低。
类之间,模块之间关系最好是低耦合,高内聚
有些关系组合不合适,而继承很合适,切片和多态是建立在继承基础之上的,所以继承语法不能抛弃