继承
继承是面向对象复用的重要手段。通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同的东西。函数复用是为了调用它,而类复用是继承它。
继承的三种关系:public(共有),protected(保护),private(私有)。
一个简单的继承关系:
class Person
{
public:
Person(const string& name = " ")
:_name(name)
{}
void Display()
{
cout << _name << endl;
}
protected:
string _name;
};
class Student:public Person
{
protected:
int _num;
};
继承关系下基类成员在派生类的访问关系变化图:
1. 基类的私有成员在派生类成员中是不能被访问的,如果一些基类成员不想被基类对象直接访问,但需要在派生类中能访问,就定义成保护成员。可以看出保护成员限定符是因继承才出现的。
2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
3. protected/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a原则关系,所以非特殊情况下不会用这两种关系,在绝大部分的场景下用的是public继承。
4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能访问)。
5. 使用关键字class时默认的继承方式是public,不过最好显示的写出继承方式。
6. 在实际运用中一般使用都是public继承,极少场景下才会使用protected/private继承。
继承与转换
- 子类对象可以赋值给父类对象(切片/切割)。
- 父类对象不能赋值给子类对象。
- 父类的指针/引用可以指向子类对象。
- 子类的指针/引用不能指向父类对象(可以通过强制转换完成)。
- 子类对象切片给父类时,不会将虚表指针给父类。
class Person
{
public:
void Display()
{
cout<<_name<<endl;
}
protected:
string _name;
};
class Student:public Person
{
public:
int _num;
};
void Test()
{
Person p;
Student s;
//子类对象可以赋值给父类对象
p = s;
//父类对象不能赋值给子类对象
//s = p;
//父类指针/引用可以指向子类对象
Person* p1 = &s;
Person& r1 = s;
//子类指针不能指向父类对象(可以通过强转完成)
Student* p2 = (Student*)&p;
//Student& r2 = (Student&)p;
//会越界
//p2->_num = 10;
}
继承中的作用域:
1.在继承体系中基类和派生类都有独立的作用域。
2.子类和父类中有同名成员,子类成员将屏蔽父类成员的直接访问。又叫隐藏或重定义。
因此最好不要定义重名的成员变量或成员函数。
3.如果定义了重名的函数。子类一般先访问子类的,例如:s._num = 10;若想访问父类的也可以,显示的去调。例如:s.person::_num = 20;
派生类的默认成员函数
在继承关系里,在派生类中如果没有显示定义这六个成员函数(构造函数,拷贝构造函数,析构函数,赋值操作符重载,取地址操作符重载,const修饰的取地址操作符重载),编译系统则会默认合成这六个默认成员函数。
class Person
{
public:
Person(const char* name = " ")
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator = (const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person" << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char* name, int num)
:Person(name)
,_num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student operator=(const Student& p)" << endl;
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student" << endl;
}
protected:
int _num;
};
void Test()
{
Student s1("xiaoming", 12);
Student s2(s1);
Student s3("lisa", 18);
s1 = s3;
}
- 针对子类来说,子类管自己的构造函数。父类初始化父类,子类初始化子类。
- 子类和父类的析构函数都变名为destruct,因此构成隐藏。子类优先调自己,再调父类的析构。
单继承和多继承
1. 单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
2. 多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承。
菱形继承
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B,public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
cout << sizeof(D) << endl;
return 0;
}
Assistant的对象中有两份Person成员,因此菱形继承存在二义性和数据冗余的问题。
虚继承(解决数据冗余和二义性)
- 虚继承解决了在菱形继承中子类对象包含多份父类对象的数据冗余(浪费空间)的问题。
在实际应用中我们通常不会定义如此复杂的继承体系。一般情况不用菱形继承的虚继承体系,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。
虚函数
虚函数:在类的成员函数前加virtual关键字,则这个成员函数成为虚函数。
虚函数重写:当在子类定义了一个父类完全相同的虚函数时,则称子类的这个函数重写(覆盖)了父类这个虚函数。
多态
当使用基类的指针或引用调用重写的虚函数时,当指向父类调用的就是父类的虚函数,指向子类调用的就是子类的虚函数。
多态条件:
- 虚函数的重写
父类的指针或引用
总结:
1.派生类重写基类的虚函数实现多态,要求函数名,参数列表,返回值完全相同。(协变除外)
2.基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3.四个默认成员函数,除了析构,都不要定义成虚函数。
4.基类必须有virtual关键字。
继承体系同名成员函数的关系:
重载:在同一作用域。函数名相同/参数不同。返回值可以不同。
重写/覆盖:不再同一作用域(分别在基类和派生类)。函数名相同/参数相同/返回值相同(协变除外),基类函数必须由virtual关键字,访问修饰符可以不同。
重定义/隐藏:在不同作用域(分别在基类和派生类),函数名相同。在基类和派生类中只要不构成重写就是重定义。
纯虚函数
在成员函数的后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义后,派生类才能实例化出对象。
友元与继承
友元关系不能继承,也就是说基类友元不能访问子类私有成员和保护成员。