一、继承之子类的构造、析构、拷贝(部分见day8)
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,int j):Base(i),m_i(j){} //Derived(const Derived& that):m_i(that.m_i){}//子类调用这个拷贝赋值,但是由于没有写基类的初始化方式,基类将以无参构造的形式初始化,这样就不符合拷贝的语义 Derived(const Derived& that):m_i(that.m_i),Base(that){}//虽然可以使用Base::m_i进行初始化,但是如果m_i是私有成员,就无法访问 //Base(that)会调用基类子对象的拷贝构造函数来进行初始化,that会被隐式转化为Base对象,即向上造型 ~Derived(void){} int m_i};//主函数中Derived d1(100,200);Deribved d2(d1);//拷贝构造
如上,有以下几点注意:
1)子类没有定义拷贝构造函数,编译器会自动为子类提供缺省的拷贝构造函数,该函数也会自动调用基类的拷贝构造函数,初始化基类子对象。
2)子对象定义了拷贝构造函数,那么需要在子类的初始化表中显式地说明基类子对象也做拷贝构造,否则基类子对象将以无参的方式进行初始化。
2、子类的拷贝赋值
对于上述代码:
Derived d3;d3=d1;//拷贝赋值
对于不涉及深拷贝和成员子对象的情况,可以使用默认的拷贝赋值函数,如果要自定义拷贝赋值函数:
Derived& operator=(const Derived& that){ //只考虑了子对象的拷贝赋值,而基类子对象调用的是无参构造 if(&that!=this){ m_i=that.m_i; //Base::m_i=that.Base::m_i;不完善 Base::operator=(that);//向上造型 } return *this;}
对于上述的代码,有以下几点:
1)子类没有定义拷贝赋值运算符函数,编译器会提供缺省的拷贝赋值运算函数,并且子类会自动调用基类的拷贝赋值运算函数,用于复制基类子对象。
2)子类定义拷贝赋值函数时,需要显式的调用基类的拷贝赋值函数,来赋值基类子对象。否则可能不能正常拷贝赋值。
二、多重继承
1、概念与语法
一个子类同时继承多个基类时,这样的继承方式称为多重继承。多重继承的初始化列表的写法见day8继承部分。
向上造型
多重继承的向上造型和单继规则和单继承相同,但是各个基类子对象的起始地址会按继承顺序和基类子对象的大小做相应的地址偏移。
2、名字冲突
子类中存在相同名字的成员时,会形成冲突。
例:
class A{public: void foo(void){ }};class B{public: void foo(int i){ }};class C:public A,public B{};
int main(void){ C c; //由于A类和B类的foo函数作用域不同,无法构成重载,所以会发生歧义冲突 c.foo(); c.foo(10); return 0;}
可按如下方式调用
int main(void){ C c; //由于A类和B类的foo函数作用域不同,无法构成重载,所以会发生歧义冲突,也可使用using关键字引入到C内部构成重载,但是这两个函数必须符合重载条件 c.A::foo(); c.B::foo(10); return 0;}
例:
class A{public: void foo(void){ } typedef int m_data;}; class B{ public: void foo(int i){ } int m_data;};class C:public A,public B{ };//如果不加作用域限定符,两个m_data虽然不想关,但是直接使用也会报冲突错误,正确使用方式如下: C c; c.B::M_data=10; c.A::m_data num=10;
综上,解决名字冲突的一般方法,一般是使用“类名::”方式,如果的冲突的是成员函数,并且满足重载条件,使用using关键字引入子类解决同名冲突。
3、砖石继承
像上面这样的一个子类的继承了多个基类,而这多个基类又继承自同一个祖先基类,这样形成了闭环的继承就称为砖石继承。
class A{public: A(int data):m_data(data){}protected: int m_data;};class B:public A{public: B(int data):A(data){} void set(int data){ m_data=data; }};class C:public A{public: C(int data):A(data){} int get(void){ return m_data; }};class D:public B,public C{public: D(int data):B(data),C(data){}}; int main(void){ D d(100); d.set(200); d.get(); //m_data获取的m_data的值并不是200,但是并不是没有改变,而是存在两份m_data,一份存在于B的基类子对象,一份存在与C的基类子对象。这样是的代码不易维护。}
像上面这样,派生多个中间子类的公共基类(A)子对象的成员(m_data),会在继承自多个中间子类的汇聚子类(D)对象中,存在多个实例。通过汇聚子类(D)对象,访问公共基类的成员,会因为继承路径不同导致不一致。
4、虚继承
由于上述相同的数据存在两份,代码维护相对复杂,可以采用虚继承来解决。通过虚继承可以让公共基类子对象在汇聚子类对象中实例唯一,并为所有的中间子类共享。这样即使沿着不同的继承路径,所访问到的公共基类中的成员一定是一致的。
class A{public: A(int data):m_data(data){}protected: int m_data;};class B:virtual public A{ //虚继承public: B(int data):A(data){} void set(int data){ m_data=data; }};class C:virtual public A{ //虚继承public: C(int data):A(data){} int get(void){ return m_data; }};class D:public B,public C{public: D(int data):B(data),C(data),A(data){}//由D来指定A的初始化方式}; int main(void){ D d(100); d.set(200); d.get(); //m_data获取的m_data的值并不是200,但是并不是没有改变,而是存在两份m_data,一份存在于B的基类子对象,一份存在与C的基类子对象。这样是的代码不易维护。}
虚继承语法:
(1)虚继承中,使用关键字virtual
(2)位于继承链最末端的汇聚子类构造函数负责构造虚基类(A)子对象
(3)但是虚基类的所有子类(B、C、D)都必须在其构造函数中显式的指明该虚基类子对象的初始化方式,否则编译器将会以无参的构造方式初始化基类子对象。
虚继承原理:
如果在A、B、C、D的构造函数中输出构造对象时的起始地址,会发现,如上的结构,D构造的对象与B起始地址相同。实际只存在一份m_data,在访问时通过B,C内部的指针(虚继承产生的虚指针)加上偏移量,完成对一份数据的访问
三、多态(重要)
1、引入
如果将基类中的某个成员函数声明为虚函数,那么子类中的与基类中具有相同原型的成员函数也是虚函数,并且对基类中的版本形成覆盖。这是通过指向子类对象的基类指针,或者通过引用子类对象的的基类引用,调用虚函数,实际被调用的将是子类中的覆盖版本,而不是基类中原始版本,这种语法现象称为多态。
子类与父类中相同原型的虚函数将形成覆盖关系,即子类覆盖父类的虚函数,并且子类的这个函数无论是否加virtual关键字都是虚函数
class Shape{public: Shape(int x,int y):m_x(x),m_y(y){ } virtual void draw(void){}protected: int m_x; int m_y;};class recr;public Shape{public: Rect(int x,int y,int w,int h): Shape(x,y),m_w(w),m_h(h){} void draw(void){ //需要只要矩形的信息 }private: int m_w; int m_h;};class Circle:public Shape{public: Circle(int x,int y,int r): Shape(x,y),m_r(r){} void draw(void){ //需要知道圆形的信息 }private: int m_r; };void render(Shape* shapes[]){ for(int i=0;shapes[i];i++){ shape[i]->draw();//如果Shape中draw()不加virtual关键字,就只能调用基类Shape中的draw()函数,修改后体现了多态 //调用的函数不再有指针的类型决定,而是有实际的目标对象类型决定 }}int main(void){ Shape* shapes[1024]={NULL};//隐式向上造型 shapes[0]=new Rect(1,2,3,4); shapes[1]=new Circle(5,6,7); //。。。 render(shapes); return 0;}
2、虚函数构成覆盖的条件
1)只有类的普通成员函数才能声明为虚函数(析构函数可以声明为虚函数),全局函数,静态函数,静态成员函数,构造函数不能声明为虚函数
2)只有基类中以virtual修饰的成员函数才能作为虚函数被子类覆盖,而与子类的virtual关键字无关
3)虚函数在子类和基类中必须具有相同的函数原型,即函数名,形参表,常属性等。注意这与重载不同,这里强调的是覆盖。同时,一般返回值也要求相同,但是如果子类的函数和基类中的虚函数返回值具有继承关系(引用或者指针或者类类型),并且满足上述条件,也可以完成覆盖(类型协变)。