一、继承
1、继承
通过一种机制,表达出类型之间的共性和特性的方式,利用已有的数据类型定义新的数据类型,这种机制称为继承。这个过程也叫做派生,所以子类也叫派生类。
继承语法:
class 子类:继承方式 基类{....}
继承方式分为:公有继承(pubic)、保护继承(protected)、私有继承(private)
protected:只能在类的内部和子类中被访问,注意不能被子类的对象所访问
继承后,基类的成员不需要重复定义,可以直接使用
class Hman{ public: Human(const string& name,int age):m_name(name),m_age(age){} void eat(const string& food){} void sleep(int time){} protected: string m_name; int age; }; class Student:public Human{ public: Student(const string& name,int age,int no) :Human(name,age),m_no(no){}//注意基类成员初始化的方式 void who(void){ ... } private: int m_no } class Teacher:public Human{ public: Teacher(const string& name,int age,double salary):Human(name,age),m_salary(salary){} private: double m_salary; };
2、公有继承的特性:
(1)子类对象会继承基类的属性和行为,通过子类对象访问基类中的成员,如果基类对象在访问他们也一样。假如一个函数需要几个基类类型的参数,传子类的对象也是合法的,
子类对象的中包含的基类部分称为“基类子对象”。注意区别成员子对象
(2)向上造型(重要,在多态和函数传参中经常出现)
将子类的指针或引用转换为基类类型的指针或引用。向上造型可以隐式完成
Student s(...); //Student-->Human Human* people=&s;//通过向上造型的类型转换,缩小了可操作内存范围,所以编译器认为安全,people指针只能访问Human的成员,而不能访问Student的成员
可参考day3类型转换部分
(3)向下造型
将基类的类型的指针或引用转换子类类型的指针或引用。 向下转换编译器不允许,必须使用显式转换。编译器并没有规定不允许向下转换,只是不允许指针可操作范围扩大的操作。
Studen* ps=people;//编译器报错, 必须做显式的转换 Student* ps=static_cast<Human*>people;
向下造型是否安全,应该有程序员自己来判断,如果向下造型时,多出来得操作内存是确定的,有效的内存,则认为安全。如果多出来的操作内存是不确定操作内容的,那么就认为不安全。例如多出来的内存没有初始化,可能出现不确定的值。
(4)子类继承基类的成员
在子类的中,可以访问基类中的公有成员和保护成员,基类的私有成员在子类中存在,但是在基类中不可见,无法直接访问,即子类的继承父类私有成员也占内存大小。可以通过公有的接口函数访问基类的私有成员。
(5)基类的构造函数和析构函数无法被子类继承,但是可以在子类的构造函数中,通过初始化表显式的说明基类子对象的初始化方式。如果不指定,那么将使用无参的方式进行初始化
(6)子类隐藏基类的成员
当基类和子类同名时,会隐藏基类的成员,优先使用子类的成员
注意:函数重载三个条件
(1)相同的作用域,子类和父类中的函数不能构成重载关系,会被认为是同名函数,父类函数会被隐藏。如果要访问父类的函数,必须加作用域限定。例如,假如父类A和子类B中都存在foo()函数,调用父类的foo()函数如下:
B b; b.A::foo();
也可以把子类和父类认为是不同的命名空间,通过using关键字可以在一个作用域使用另外一个作用域的成员,比如在B中引入A的foo()构成重载。
//在B的定义中
using A::foo;//引入到B中,可构成重载关系一般使用前一种方式较多,因为不能保证子类和父类中的同名函数参数是否不同。
(2)相同的函数名
(3)不同的而参数个数或类型
3、继承方式和访问控制属性
(1)访问控制限定符以及成员可见范围
访问控制限定符 | 访问控制属性 | 内部 | 外部 | 外部 | 友元 |
public | 公有成员 | ok | ok | ok | ok |
protected | 保护成员 | ok | ok | no | ok |
private | 私有成员 | ok | no | no | ok |
(2)不同继承方式对基类成员的可见性
基类中的 | 公有继承的子类 | 保护继承的子类 | 私有继承的子类 |
公有成员 | 公有 | 保护 | 私有 |
保护成员 | 保护 | 保护 | 私有 |
私有成员 | 私有 | 私有 | 私有 |
注意:
(1)假如,一个基类的成员是公有的或者保护的,那么在基类私有继承之后,基类的公有或者保护成员对子类来说是子类的私有成员。而父类的私有成员,子类不可访问,公有继承和保护继承也是相同的道理。所以继承方式对于子类来说,对父类的访问影响并不大,但是子类的子类会有一定的影响。
(2)私有继承和保护继承不能向上造型
因为继承的时候已经把基类公有部分继承为私有的,或者保护的了,如果在向上造型回去称为公有的,存在安全问题。
class Base{ public: int m_data; }; class Derived:private Base{}; int main(){ Derived d; Base* pb=&d;//不支持向上造型 Base* pb=static_cast<Base*>&d;//也不支持显式静态类型转换,它会做安全检查 Base* pb=(Base*)&d;//支持强制转换,但是成功后不安全。强制转换不会做安全检查 return 0 }
二、子类的构造函数与析构函数
1、基类子对象的构造
如果子类的构造函数没有显式的指明基类子对象的初始化方式,那么编译器会调用基类的无参构造函数来初始化基类自对象。
class Base{ public: Base(void):m_i(0){//无参构造 } Base(int i):m_i(i){//有参构造 } ~Base(void){} int m_i; }; class Derived:private Base{ public: Derived(void){} Derived(int i):Base(i){} ~Derived(void){} }; int main(){ Derived d;//调用基类的无参构造函数 Derived d2(100);//基类和子类都调用有参构造函数 return 0 }
构造顺序:
分配内存->构造基类子对象(多个基类按继承表顺序构造)->构造成员子对象(按声明顺序构造)->执行子类构造函数
先基类后子类构造,如果基类子对象以有参的方式初始化,必须在子类的构造函数初始化表中显式的指定初始化方式
关于基类子对象和成员子对象
先构造基类子对象,再构造成员子对象
2、子类的析构函数
子类的析构函数,无论是自定义的还是编译器缺省提供的,都会调用基类的析构函数,析构基类子对象。
析构顺序:
执行子类的析构函数->析构成员子对象(按声明逆序)->执行基类子对象析构(多个基类按继承表逆序执行)->释放内存
子类可以调用基类的析构函数,但是基类不会调用子类的析构函数,对于上述代码,
Base* pb =new Derived; //... //delete pb;//只能调用基类的析构函数,存在内存泄漏
//解决方法是将pb向下造型转换为子类的指针,通过多态方式也可以解决