继承
继承的基本概念和定义
基本概念
继承是面向对象的三大特性之一(封装、继承、多态),继承体现的是类设计层次的复用。
class Person
{
public:
void ShowInfo() const
{
cout<<"name:"<<name<<",age:"<<age<<endl;
}
protected:
string name;
int age;
};
class Student:public Person
{
protected:
int stu_num;//学号
};
Person是基类,每一个Person对象都有name和age,Student是一个(is a)Person,Student有Person的所有属性(name,age),除此之外,Student也有自己特有的属性(stu_num)。Student通过继承Person,避免重复的代码,体现了复用。
继承方式
继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
-
无论子类以哪一种方式去继承基类,基类的private成员在子类中总是不可见的。不可见的含义:子类将父类的private成员的确继承下去了,但是在子类的类内和类外均不可访问。
class A { public: int a1; private: void Print() { cout<<"hello"<<endl; } int a2; }; class B:public A { public: void Show() { Print();//父类的private成员不可访问 } void Set() { a2=1;//父类的private成员不可访问 } private: int b; }; int main() { B b; b.a1=10;//父类的public成员可以访问 b.a2=1;//父类的private成员不可访问 return 0; }
-
子类以public的方式继承父类,父类中所有成员的访问权限到了子类中不变
-
子类以protected的方式继承父类,父类的public成员和protected成员到了子类中均是protected
-
子类以private的方式继承父类,父类的public成员和protected成员到了子类中均是private
-
protected访问权限与private访问权限的区别在于被继承时的行为不同
-
继承时可以不指定继承方式,默认class是private继承,struct是public继承
class A { protected: int a; }; class B:A//等价于class B:private A { protected: int b; }; struct C:A//等价于struct C:public A { protected: int c; };
继承体系中父类和子类的行为
切片
-
在继承体系中,子类可以赋值给父类,父类的指针可以指向子类对象,父类的引用可以直接引用子类对象,把这种行为称为切片或者切割,又称子类与父类的赋值兼容转换。这种赋值兼容转换是c++语法天然支持的,只要是有继承关系,都可以实现切片。
class Person { protected: string name; int age; }; class Teacher:public Person { protected: int t_num; }; int main() { Teacher t; Person p=t;//切片 Person* ptr=&t;//父类的指针指向子类 Person& ref=t;//父类引用子类 return 0; }
切片的行为是看待子类的内存布局的一种方式,可以理解为将子类中父类的部分提取出来单独使用。
-
切片行为的顺序不能颠倒,不能把父类对象赋值给子类。
class A { protected: int a; }; class B:public A { protected: int b; }; int main() { A father; B son=father;//不能把父类赋值给子类。 return 0; }
-
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是可能有安全问题。
class A { protected: int a; }; class B:public A { protected: int b; }; int main() { A father; A* ptra=&father;//ptra是基类的指针,指向基类对象 B son; B* ptrb=&son;//ptrb是派生类指针,指向派生类对象 ptrb=(B*)ptra;//基类指针强制类型转换赋值给派生类指针,此时派生类指针指向基类对象,不安全 return 0; }
因为派生类指针进行+1或者-1跳过8字节,进行解引用一次访问8字节的空间,而一个基类对象只有4字节大小,所以派生类指针指向基类对象,如果进行解引用操作,会有越界访问问题。
int main() { B son; A* ptra=&son;//ptra是基类的指针,指向派生类对象 B* ptrb=&son;//ptrb是派生类指针,指向派生类对象 ptrb=(B*)ptra;//基类指针强制类型转换赋值给派生类指针,此时派生类指针依然指向派生类对象,是安全的 return 0; }
如果基类是多态的类型,那么把基类指针赋值给派生类指针的行为可以使用RTTI(Run Time Type Information)的dynamic_cast来进行识别是否安全。
总结:切片行为关键是看指向的对象,如果指向的对象比自己类型大小大,就能安全实现切片,否则就是非法的
隐藏(重定义)
-
在继承体系中,父类和子类有各自独立的作用域。
-
在继承体系中,如果子类和父类有同名成员,那么子类会屏蔽掉队父类同名成员的直接访问,这种行为叫做隐藏,或重定义。
class A { public: void Print() { cout<<"hello A"<<endl; } int a; int age; }; class B:public A { //B继承A,A中a,age,Print()是属于A的作用域的 public: void Print(int i) { cout<<"hello B"<<endl; } int age; int b; //B中的age,b,Print(int i)是属于B的作用域的 }; int main() { B tmp; tmp.a=10;//访问的是A作用域中的a,因为在B的作用域中没有a,相当于tmp.A::a=10 tmp.age=18;//访问的是B作用域中的age,因为A和B中都有age,B会把A中的age隐藏,相当于tmp.B::age=18; tmp.A::age=18;//必须显示指定才能访问A作用域中的age tmp.Print();//编译错误,B作用域中的Print(int i)把A作用域中的Print()隐藏了 tmp.Print(1);//调用B作用域中的Print(int i),相当于tmp.B::Print(1) tmp.A::Print();//显示调用A作用域中的Print() return 0; }
在继承体系中,子类的成员函数只要和父类的成员函数的函数名相同,就构成隐藏。构成隐藏关系的父类成员,子类必须指定作用域才能访问。
子类的6个默认成员函数
-
子类的默认构造函数:子类的默认构造函数对于子类作用域下的内置类型成员不处理,对于子类作用域下的自定义类型成员会去调用它的默认构造函数。对于父类作用域下的成员,子类的默认构造函数会调用父类的默认构造函数,如果父类没有默认构造函数,则需要子类在初始化列表显示调用。
class A { public: A(){cout<<"A()"<<endl;} protected: int a; }; class B:public A { protected: int b; }; int main() { B b;//B的默认构造函数会在初始化列表调用A的默认构造函数 return 0; }
A没有默认构造函数:
class A { public: A(int val):a(val){cout<<"A()"<<endl;} protected: int a; }; class B:public A { public: //如果A没有默认构造函数,那么B必须显示调用A的其它构造函数。 B(int x=1,int y=1):A(x),b(y){} protected: int b; }; int main() { B b; return 0; }
如果显示写B的构造函数,且不在初始化列表做任何工作,那么默认的初始化列表会调用A的默认构造函数。
-
子类的拷贝构造函数:子类的拷贝构造函数对于子类作用域中的成员,实现浅拷贝(自定义类型调用它的拷贝构造函数),对于父类作用域中的成员,调用父类的拷贝构造函数。
class A { protected: int a; }; class B:public A { public: B()=default; B(const B& tmp):b(tmp.b),A(tmp){} protected: int b; };
-
子类的赋值运算符重载:子类的赋值运算符重载对于子类作用域中的成员进行简单的值赋值(自定义类型调用它的赋值运算符重载),对于父类作用域中的成员调用父类的赋值运算符重载。
class A { protected: int a; }; class B:public A { public: B()=default; B& operator=(const B& tmp) { b=tmp.b; A::operator=(tmp);//发生切片 return *this; } protected: int b; };
如果需要显示写子类的赋值运算符重载,应该在函数体内部显示调用父类的operator=.
-
子类的析构函数:子类的析构函数会在它执行完毕之后自动调用父类的析构函数,因此,不需要在子类的析构函数中显示调用父类的析构函数,另外,由于多态的需要,子类的析构函数和父类的析构函数函数名被编译器统一处理为destructor,子类的析构函数和父类的析构函数构成隐藏。
-
子类的取地址重载:子类的取地址重载需要拿到子类的地址,与父类无关。
class A { protected: int a; }; class B:public A { public: B* operator&() { return this; } const B* operator&() const { return this; } protected: int b; };
继承与友元
在继承体系中友元关系不能被继承。
class B;
class A
{
friend void Print(const A& left,const B& right);
protected:
int a=1;
};
class B:public A
{
protected:
int b=2;
};
void Print(const A& left,const B& right)
{
cout<<left.a;//Print是A的友元,不是B的友元
cout<<right.b<<endl;//无法访问b
}
int main()
{
Print(A(),B());
return 0;
}
继承与静态成员
在整个继承体系中,静态成员只有一个实例。可以通过这一特点计算在一个继承体系中创建了多少个对象。
class A
{
public:
A(){count++;}
static int count;
int a;
};
int A::count=0;
class B:public A
{
public:
int b;
};
class C:public B
{
public:
int c;
};
int main()
{
A a;
B b;
C c;
a.count=10;
cout<<b.count<<endl;
cout<<c.count<<endl;
return 0;
}
静态成员也可以构成隐藏
class A
{
public:
A(){count++;}
static int count;
int a;
};
int A::count=0;
class B:public A
{
public:
static int count;
int b;
};
int B::count=0;
class C:public B
{
public:
int c;
};
int main()
{
A a;
B b;
C c;
a.count=10;
cout<<b.count<<endl;//B::count==0
cout<<c.count<<endl;//B隐藏了A中的count,所以C继承B,c访问count默认是B::count
return 0;
}
菱形继承
class A
{
public:
int a=1;
};
class B:public A
{
public:
int b=2;
};
class C:public A
{
public:
int c=3;
};
class D:public B,public C
{
public:
int d=4;
};
D的对象模型
菱形继承有数据冗余和二义性的问题,需要使用虚拟继承来解决这个问题。
class A
{
public:
int a=1;
};
class B:virtual public A
{
public:
int b=2;
};
class C:virtual public A
{
public:
int c=3;
};
class D:public B,public C
{
public:
int d=4;
};
虚拟继承之后的对象模型
此时D实例化对象,对象访问int a,可以直接访问。这种菱形虚拟继承的方法解决了数据冗余和二义性的问题,不过同时使对象模型变得复杂。例如D在发生切片的时候:
int main()
{
D tmp;
B tmpb=tmp;
C tmpc=tmp;
return 0;
};
需要在对象模型中找到B::ptrb和C::ptrc,通过这两个指针+偏移量找到A,然后在拼接上去,比较麻烦。
虚拟继承
虚拟继承指的是在继承的时候加一个virtual关键字。
class A
{
public:
int a=1;
};
class B :virtual public A
{
public:
int b=2;
};
int main()
{
cout << sizeof(B) << endl;//32位平台下是12
B tmp;
tmp.a = 10;
return 0;
}
虚拟继承的基类被称为虚基类,上面的A就是虚基类,虚继承以后,B中除了继承A,还会多一个指针,这个指针称为虚基表指针,该指针指向一张表,称为虚基表,在虚基表中存放偏移量,B虚继承A,此时tmp要访问A作用域下的a,不能直接访问,需要通过虚基指针+偏移量的方式找到a.
当一个继承体系中存在多继承,并且大量使用虚拟继承,那么最终的对象模型会变得非常复杂,效率也有所下降,因为虚拟继承以后访问基类成员是通过虚基指针+偏移量访问的,所以一般不推荐使用菱形继承和虚拟继承。
继承和组合
组合
class Tire
{
//.......
};
class Car
{
private:
//.....
Tire t;
}
组合是指的一个类中有一个类类型成员(has a),而继承是is a的关系,例如学生是一个人。
继承是一种白箱复用,在一定程度上破坏了子类的封装性,增加了耦合度。而组合是一种黑箱复用,在Car里面无法得知Tire的内部细节,只能使用Tire的接口,一定程度上降低了耦合度。