6-继承
- 1、基本语法和方式
- 2、继承的基本特点
- 2.1 三种继承方式相同的基本点
- 2.2 三种继承方式的差别
- 2.3 公有继承的独有特点
- 3、子类的构造、析构
- 3.1 子类的构造
- 3.2 子类的析构
- 3.3 子类的拷贝构造函数
- 3.4 子类的拷贝赋值
- 4、多重继承
- 4.1 内存布局
- 4.2 类型转换
- 4.3 名字冲突问题
- 5、砖石继承
- 6、虚继承
1、基本语法和方式
class 子类 : 继承方式1 基类1, 继承方式2 基类2, ... {...
};
- 继承方式
公有继承:public
保护继承:protected
私有继承:private
2、继承的基本特点
2.1 三种继承方式相同的基本点
- 继承所要达到的目的:
- 子类对象包含基类子对象
- 子类内部可以直接访问基类的所有非私有成员
- 继承的本质:
- 基类的非私有成员在子类中仅仅为可见,而非拥有
注意:
对于继承切忌不要理解为基类的成员变为子类的成员,继承不会改变类成员的作用域,基类的成员永远都是基类的成员,并不会因为继承而变成子类的成员
- 基类的非私有成员在子类中仅仅为可见,而非拥有
- 尽管基类的公有和保护成员在子类中直接可见,但仍然可以在子类中重新定义这些名字,子类中的名字会隐藏所有基类中的同名定义
- 如果需要在子类内部访问 一个在基类中定义却被子类标识符所隐藏的名字,可以借助作用域限定操作符“::”实现
- 因为作用域的不同,分别在子类和基类中定义的同名成员函数(包括静态成员函数),并不构成重载关系,相反是一种隐藏关系
- 任何时候,在子类的内部,总可以通过作用域限定操作符“::”,显式地调用那些在基类中定义却被子类所隐藏的成员
// 继承最基本的特点
// (1) 子类对象的内部 包含 基类子对象
// (2) 子类内部可以直接访问 基类的 非私有(共有/保护)成员(变量/函数)
// (3) 一旦被子类同名定义隐藏 可以借助::指明访问基类
class Base{
public:int m_a;void foo(){ cout << "Base::foo" << endl; }
protected:int m_b;void bar(){ cout << "Base::Bar" << endl; }
private:int m_c;void hum(){ cout << "Base::hum" << endl; }
};
class Derived :public Base{
public:void fun(){m_a = 100;Base::foo();// 子类的foo函数将基类的foo函数隐藏,但可以通过作用域限定符访问基类的foom_b = 100;bar(); //子类内部可以直接访问 基类的 非私有(共有/保护)成员(变量/函数)//m_c = 100;//hum();}
private:int m_d;void foo(){ cout << "Derived::foo" << endl; }void bar(){ cout << "Derived::Bar" << endl; }
};
int main(){Base b;// 基类对象cout << "基类对象b的大小" << sizeof(b) << endl; // 12Derived d;// 子类对象cout << "子类对象d的大小" << sizeof(d) << endl; // 16 子类对象的内部 包含 基类子对象d.fun();return 0;
}
2.2 三种继承方式的差别
- 基类中的公有、保护和私有成员,在子类中将对这些基类成员的访问控制限定进行重新标记
// public继承的独特点
class publicBase :public Base{// 子类将对基类的成员重新标记访问限定符// 子类将对基类的成员重新标记访控限定 m_a/foo是public m_b/bar是protected m_c/hum是private
public:void fun(){ // //子类内部访问基类成员时,编译器需要查看这些成员在 基类中的原始标记m_a = 100;Base::foo();// 子类的foo函数将基类的foo函数隐藏,但可以通过作用域限定符访问基类的foom_b = 100;bar(); //子类内部可以直接访问 基类的 非私有(共有/保护)成员(变量/函数)}
private:int m_d;
};
// protected继承的独特点
class protectedBase :protected Base{// 子类将对基类的成员重新标记访问限定符// 子类将对基类的成员重新标记访控限定 m_a/foo是protected m_b/bar是protected m_c/hum是private
public:void fun(){ //子类内部访问基类成员时,编译器需要查看这些成员在 基类中的原始标记m_a = 100;Base::foo();// 子类的foo函数将基类的foo函数隐藏,但可以通过作用域限定符访问基类的foom_b = 100;bar(); //子类内部可以直接访问 基类的 非私有(共有/保护)成员(变量/函数)}
private:int m_d;
};
// private继承的独特点
class privateBase :private Base{// 子类将对基类的成员重新标记访问限定符// 子类将对基类的成员重新标记访控限定 m_a/foo是private m_b/bar是private m_c/hum是private
public:void fun(){ //子类内部访问基类成员时,编译器需要查看这些成员在 基类中的原始标记m_a = 100;Base::foo();// 子类的foo函数将基类的foo函数隐藏,但可以通过作用域限定符访问基类的foom_b = 100;bar(); //子类内部可以直接访问 基类的 非私有(共有/保护)成员(变量/函数)}
private:int m_d;
};
int main(){publicBase d;// 利用子类对象在类外访问 基类成员时,编译器需要查看这些成员在 子类中的重新标记d.m_a = 10;d.foo();//d.m_b = 10; // err//d.bar();// err//d.m_c = 30;// err//d.hum();// errprotectedBase b;// 利用子类对象在类外访问 基类成员时,编译器需要查看这些成员在 子类中的重新标记//b.m_a = 10;// err//b.foo();// err//b.m_b = 10; // err//b.bar();// err//b.m_c = 30;// err//b.hum();// errprivateBase cd;// 利用子类对象在类外访问 基类成员时,编译器需要查看这些成员在 子类中的重新标记//cd.m_a = 10;// err//cd.foo();// err//cd.m_b = 10; // err//cd.bar();// err//cd.m_c = 30;// err//cd.hum();// errreturn 0;
}
基类中的 | 在公有子类中标记为 | 在保护子类中标记为 | 在私有子类中标记为 |
---|---|---|---|
公有成员 | 公有成员 | 保护成员 | 私有成员 |
保护成员 | 保护成员 | 保护成员 | 私有成员 |
私有成员 | 私有成员 | 私有成员 | 私有成员 |
- 当“通过”子类访问其所继承的基类的成员时,需要考虑因继承方式对访问控制限定的影响
2.3 公有继承的独有特点
(1) 只有在公有继承下,子类对象在类外可以访问基类的 公有成员(其他继承不可以)
(2) 如果被子类同名标识符隐藏可以借助::指明访问基类的成员
(3) 只有在公有继承下 子类类型指针 和 基类类型指针 之间可以进行转换
子类类型引用 和 基类类型引用 之间可以进行转换
class publicBase :public Base{
public:void foo(){cout << "publicBase::foo" << endl;}
private:int m_d;
};
class Human{
public:int m_d;string m_name;
};
class Student :public Human{
public :int m_no;
};
int main(){publicBase b;b.m_a = 1000;b.foo(); // 调用的是自己的b.Base::foo(); // 调用的是基类的Human h;cout << "基类对象h的大小" << sizeof(h) << endl;Student s;cout << "子类对象s的大小" << sizeof(s) << endl;// 子类类型指针 和 基类类型指针Human* ph = &s; // Student* --> Human*// 子类类型引用 和 基类类型引用Human& rh = s;// 以上两种转换,编译器认为访问范围缩小,是安全的//Student* ps = &h;//Student& rs = h;// 以上两种转换,编译器认为访问范围扩大,是危险的Student* ps = static_cast<Student*> (&h);Student& rs = static_cast<Student&>(h);// 通过静态转换虽然可以成功,但是风险依旧存在,极其不建议这么使用Student* ps = static_cast<Student*>(ph);// ph 指向的就是C6_Student的对象Student& rs = static_cast<Student&>(rh);// rh 就是C6_Student对象的引用// 以上两种转换毫无风险,极其建议大家这么做// 基类指针或引用的实际目标,究竟是不是子类对象,完全由我们自己判断return 0;
}
向上造型:
- 子类类型指针 隐式 转换为基类类型指针
- 子类类型引用 隐式 转换为基类类型引用
3、子类的构造、析构
3.1 子类的构造
- 子类没有定义构造函数
- 编译器为子类提供的默认无参构造函数,定义基类子对象,并调用其基类的无参构造函数,构造该子类对象中的基类子对象。
- 子类定义构造函数但没有在初始化表中指明基类部分构造方式
- 定义基类子对象,并调用其基类的无参构造函数,构造该子类对象中的基类子对象。
- 子类定义构造函数并在初始化表中指明基类部分构造方式
- 定义基类子对象并 调用指明的其基类的构造函数。
- 子类对象的构造过程
- 构造基类子对象->构造成员变量->执行构造代码
3.2 子类的析构
- 子类没有定义析构函数
- 编译器将提供一个默认析构函数,析构完所有的成员变量以后,会自动调用其基类的析构函数.
- 子类定义析构函数
- 子类的析构函数在执行完自身析构代码,并析构完所有的成员变量以后,会自动调用其基类的析构函数.
- 子类对象的析构过程
- 执行析构代码->析构成员变量->析构基类子对象
3.3 子类的拷贝构造函数
- 子类没有定义拷贝构造函数
- 编译器为子类提供的默认拷贝构造函数,定义基类子对象,并调用其基类的拷贝构造函数构造该子类对象中的基类子对象
- 子类定义了拷贝构造函数,但没有在初始化表指明其基类部分的构造方式
- 定义基类子对象,并调用其基类的无参构造函数,构造该子类对象中的基类子对象
- 子类定义了拷贝构造函数,同时初始化表中指明了其基类部分以拷贝方式构造
- 定义基类子类对象,并调用其基类的拷贝构造函数,构造该子类对象中的基类子对象
3.4 子类的拷贝赋值
- 子类没有定义拷贝赋值函数
- 编译器为子类提供的缺省拷贝赋值函数,会自动调用其基类的拷贝赋值函数,复制该子类对象中的基类子对象
- 子类定义了拷贝赋值函数,但没有显式调用其基类的拷贝赋值函数
- 子类对象中的基类子对象将得不到复制
- 子类定义了拷贝赋值函数,同时显式调用了其基类的拷贝赋值函数
- 子类对象中的基类子对象将得到复制
class C6_people{
public:C6_people(int age = 0, const char*name = "无名"):m_age(age), m_name(name){//【int m age=age;】定义mage,初值为age//【string m name(name);】定义m name,利用m name,string(name)cout << "people类的缺省构造函数被调用" << endl;}C6_people(const C6_people&that):m_age(that.m_age),m_name(that.m_name){// 【int m_age = that, mage; 】定义mage, 初值为that.m age//【string m name=that.m name;】定义m name,利用m name.string(that.m name)cout << "people类的拷贝构造函数被调用" << endl;}C6_people&operator=(const C6_people& that){//编译器不会在自定义拷贝赋值函数中塞任何操作cout << "people类的拷贝赋值函数被调用" << endl;this->m_age=that.m_age;this->m_name=that.m_name; // this->m name.operator=(that.m name)return *this;}~C6_people(){cout << "C6_people类的析构函数被调用" << endl;//对于基本类型的成员变量m_age,什么都不做//对于类类型的成员变量m_name,利用m_name,~string()//释放m_age/m_name本身所占内存空间}void getinfo(){cout << "姓名:" << m_name << ",年龄:" << m_age ;}
private:int m_age;// 基本类型成员变量string m_name;// 类类型成员变量
};
class C6_stu : public C6_people{
public://子类没有定义构造函数编译器为子类提供的默认无参构造函数//C6_stu(){// 【Human(); 】定义基类子对象,利用基类子对象.Human()// 【float m score; 】// 【string m remark; 】//}C6_stu(int age = 45, string name = "张三", float score = 0.0, string remark = "优秀") :m_score(score), m_remark(remark), C6_people(age,name.c_str()){cout << "基类的缺省构造被调用了" << endl;}void getinfo(){C6_people::getinfo();cout << "分数:" << m_score << "评语:" << m_remark << endl;}//子类没有定义析构函数编译器将提供一个默认析构函数//~C6_stu(){//对于基本类型的成员变量m_score,什么都不做//对于类类型的成员变量m m_remark,利用m_remark,~string()// 对于基类子对象,利用基类子对象.~C6_people()//释放m_score/m_remark本身所占内存空间//}~C6_stu(){cout << "C6_stu类的析构函数被调用" << endl;}//子类没有定义拷贝构造函数编译器为子类提供的默认拷贝构造函数//C6_stu(const C6_stu& that){// // [C6_people(that)] 定义基类子对象,利用基类子对象.C6_people(that) ->C6_people类拷贝构造函数// // ...//}// 子类没有定义拷贝赋值函数编译器为子类提供的缺省拷贝赋值函数//C6_stu& operator=(const C6_stu& that){// C6_people *p = this;// *p = that;// C6_people类的拷贝赋值函数//}private:float m_score;string m_remark;
};
int main(){cout << "----s1" << endl;C6_stu s1(22,"李四",89.7,"良好");// 构造s1.getinfo();cout << "----s2" << endl;C6_stu s2 = s1; // 拷贝构造s2.getinfo();cout << "----s3" << endl;C6_stu s3;cout << "s3赋值前" << endl;s3.getinfo();s3 = s2; // 赋值拷贝cout << "s3赋值后" << endl;s3.getinfo();return 0;
}
4、多重继承
4.1 内存布局
- 子类对象中的多个基类子对象,按照继承表的顺序依次被构造,析构的顺序则与构造严格相反,各个基类子对象按照从低地址到高地址排列
class C06_A{
public:int m_a;C06_A(){ cout << "C06_A构造" << endl; }~C06_A(){ cout << "C06_A析构" << endl; }
};
class C06_B{
public:int m_b;C06_B(){ cout << "C06_B构造" << endl; }~C06_B(){ cout << "C06_B析构" << endl; }
};
class C06_C{
public:int m_c;C06_C(){ cout << "C06_C构造" << endl; }~C06_C(){ cout << "C06_C析构" << endl; }
};
class C06_D :public C06_A, public C06_B, public C06_C{ // 汇聚子类
public:int m_d;C06_D(){ cout << "C06_D构造" << endl; }~C06_D(){ cout << "C06_D析构" << endl; }
};
int main(){C06_D d;cout << "汇聚子类对象d的大小:" << sizeof(d) << endl;C06_D * pd = &d;cout << "整个汇聚子类对象的首地址D* pd:" << pd << endl;cout<< "A基类子对象的首地址:" << &d.m_a<<endl;cout << "B基类子对象的首地址:" << &d.m_b << endl;cout << "C基类子对象的首地址:"<<&d.m_c<< endl;cout << "D基类子对象的首地址:" << &d.m_d << endl;return 0;
}
4.2 类型转换
- 将多重继承的子类对象的指针,隐式转换为它的基类类型,编译器会根据各个基类子对象在子类对象中的内存位置,进行适当的偏移计算。
- 反之,将任何一个基类类型的指针静态转换为子类类型,编译器同样会进行适当的偏移计算
- 无论在哪个方向上,重解释类型转换(reinterpret_cast)都不进行任何偏移计算–不要使用
int main(){C06_D d;cout << "汇聚子类对象d的大小:" << sizeof(d) << endl;C06_D * pd = &d;cout << "整个汇聚子类对象的首地址D* pd:" << pd << endl;// 004FF730cout << "A基类子对象的首地址:" << &d.m_a << endl; // 004FF730cout << "B基类子对象的首地址:" << &d.m_b << endl; // 004FF734cout << "C基类子对象的首地址:" << &d.m_c << endl; // 004FF738cout << "D基类子对象的首地址:" << &d.m_d << endl; // 004FF73CC06_A* pa = pd;cout << "隐式转换" << endl;cout << "D* pd ---> A* pa:" << pa << endl; // 004FF730C06_B* pb = pd;cout << "D* pb ---> B* pb:" << pb << endl; // 004FF734C06_C* pc = pd;cout << "D* pd ---> C* pc:" << pc << endl; // 004FF738cout << "静态转换" << endl;C06_D* p1 = static_cast<C06_D*>(pa);cout << "A* pd ---> D* p1:" << p1 << endl; // 004FF730C06_D* p2 = static_cast<C06_D*>(pb);cout << "B* pb ---> D* p2:" << p2 << endl; // 004FF730C06_D* p3 = static_cast<C06_D*>(pc);cout << "C* pc ---> D* p3:" << p3 << endl; // 004FF730return 0;
}
- 引用的情况与指针类似,因为引用的本质就是指针
4.3 名字冲突问题
- 如果在子类的多个基类中,存在同名的标识符,那么任何试图通过子类对象,或在子类内部访问该名字的操作,都将引发歧义。
名字冲突问题解决方法 - 子类隐藏该标识符–不建议使用
- 通过作用域限定操作符“::”显式指明所属基类
class C06_E{
public:int m_a;int m_c;
};
class C06_F{
public:int m_b;int m_c;
};
class C06_G :public C06_E, public C06_F{
public:int m_d;void foo(){// m_c = 100;歧义C06_E::m_c = 100;}
};
int main(){C06_G g;cout << "汇聚子类对象g的大小:" << sizeof(g) << endl;// g.m_c=100; 歧义g.C06_E::m_c = 100;return 0;
}
5、砖石继承
- 砖石继承的问题
一个子类继承自多个基类,而这些基类又源自共同的祖先,这样的继承结构称为钻石继承(菱形继承)
class C06_AA{ // 公共基类
public:int m_a;
};
class C06_X :public C06_AA{ // 中间子类
public:int m_x;
};
class C06_Y :public C06_AA{ // 中间子类
public:int m_y;
};
class C06_Z :public C06_X, public C06_Y{ // 汇聚子类
public:int m_z;
};
int main(){C06_Z z; // X中间子类子对象|Y中间子类子对象|m_z|-->// A公共基类子对象 m_x|A公共基类子对象 m_y|m_z|-->// m_a m_x |m_a m_y|m_z|cout << "汇聚子类对象g的大小:" << sizeof(z) << endl;// 20return 0;
}
- 钻石继承问题
在汇聚子类内部,或通过汇聚子类对象,访问公共基类的成员,会因继承路径的不同而导致匹配歧义
6、虚继承
解决砖石继承存在的问题而诞生的
-
在继承表中使用virtual关键字
-
虚继承可以保证
(1)公共虚基类子对象在汇聚子类对象中仅存一份实例
(2)公共虚基类子对象被多个中间子类子对象所共享
虚继承实现原理 -
汇聚子类对象中的每个中间子类子对象都持有一个指针,通过该指针可以获取中间子类子对象的首地址到公共虚基类子对象的首地址的偏移量
class C06_AA{ // 公共基类
public:int m_a;
};
class C06_X :virtual public C06_AA{ // 中间子类
public:int m_x;void setAge(/* X* this */int age){m_a = age; // this -> X 中间子类子对象-》指针1-》偏移量-》this+偏移量-》A公共基类子对象-》m_a}};
class C06_Y :virtual public C06_AA{ // 中间子类
public:int m_y;int getAge(/* Y* this*/){ return m_a;// this->Y 中间子类子对象 - 》指针2 - 》偏移量 - 》this + 偏移量 - 》A公共基类子对象 - 》m_a}
};
class C06_Z :public C06_X, public C06_Y{ // 汇聚子类
public:int m_z;
};
int main(){C06_Z z; // X中间子类子对象|Y中间子类子对象|m_z|A公共基类子对象|-->// 指针1 m_x |指针2 m_y|m_z|m_a|cout << "汇聚子类对象g的大小:" << sizeof(z) << endl;// 32z.setAge(100); // setAge(&z) -->实参类型Z*cout << "age:" << z.getAge() << endl; // getAge(&z) -->实参类型Z*return 0;
}