面向对象的三大特性:封装,继承,多态。
单继承
继承的概念:面向对象程序设计使得代码可以复用的重要手段,允许用户在维持原有类的特性的基础上进行扩展。
继承的格式:
class Person{...};
class Student : public Person{...};
总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
静态成员的继承:对于静态成员而言,因为静态成员并不存在于类对象的所在的栈区或者堆区,而是存在于初始化区,所以静态成员也会被继承下去,被所有的继承类所共享,并且只有一份这样的静态成员数据。
1.1 基类和派生类的赋值转换
继承中最重要的一个特点就是:基类的对象或指针或引用可以和普通的派生类对象进行赋值转换。
在内置类型的对象在进行赋值转化的时候,会发生类型转化(隐式类型转化或显示类型转换)
int a = 10;
double d = a; //int --> double,中间就会产生临时变量存储a的值后,在给d对象
const double& rd = a; //报错,因为不能对临时变量(常量)使用引用
而对于继承的父类和子类而言,派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。
Student s1;
person p = s1; //此时这里就完成一次切片操作
person& rp = s1; //引用方式
person* pp = &s1; //指针方式
//当然对于父类指向子类的指针,可以通过强制类型转换的方式让它赋值给子类的指针
pp = &s1;
Student* ps1 = (Student*)pp;
1.2 继承中的作用域
在继承体系中基类和派生类都有独立的作用域,如果子类和父类中有同名成员,子类成员将屏蔽父类中同名的成员,这种情况叫做隐藏,也叫重定义(在子类成员函数中,可以使用 基类::基类成员 显示访问)。 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。注意在实际中在继承体系里面最好不要定义同名的成员。
友元关系是不能够继承的,也就是说基类友元不能访问派生类的私有和保护成员。
1.3 派生类的默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。(①调用自己的析构,②调用父类的析构)。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 派生类的析构函数和基类的析构函数构成隐藏关系,这是因为编译器会对函数名进行处理,变成同名函数。
#include <iostream>
using namespace std;
class Person {
public:
Person(int num =10)
:_num(num)
{
cout << "Person()构造函数" << endl;
}
//拷贝构造函数
Person(const Person& p)
:_num(p._num)
{
cout << " Person(const Person& p)" << endl;
}
//赋值构造函数
Person operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
{
_num = p._num;
}
return *this;
}
~Person()
{
cout << "Person的析构函数" << endl;
delete[] a;
}
protected:
int _num; //身份证号
int* a = new int[10];
};
class Student :public Person {
public:
Student() //无参的构造函数
{
}
Student(int num, int age)
:Person(num)
, _age(age)
{
cout << "Student(int num, int age)" << endl;
}
//拷贝构造
Student(const Student& s)
:Person(s) //_num(s._num) 这里用到切片
, _age(s._age)
{
cout << "Student()构造函数" << endl;
}
//赋值构造
Student operator=(const Student& s)
{
cout << "Student operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s); //父类的部分显示的调用父类的赋值构造
_age = s._age;
}
return *this;
}
~Student()
{
//这就是编译器处理的问题,说到隐藏很类似,---编译器在处理之后,所有的类的析构函数她都会被重新命名称为deStructor()
// 析构函数在对象的生命周期结束之后,他会自己调用的
//1 、跟父类的析构函数构成隐藏关系
//2、对象的生命周期结束之后,他会自己调用自己的析构函数
//Person::~Person(); //不建议显示的调用父类的析构函数
cout << "Student的析构函数" << endl;
}
protected:
int _age; //年龄
};
void Test()
{
//Student s1; //这里是age类型,不做处理,自定义类型调用自己的构造函数,父类调用父类的构造函数
//Student s2(s1);
Student s1(24, 30); //调用子类的有参构造
Student s2(s1); //拷贝构造
Student s3 = s1; //赋值构造
}
int main()
{
Test();
return 0;
}
多继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承:是多继承的一种特殊情况
多继承的对象模型如下,可以清楚的看出存在数据冗余的问题,在访问的时候出现数据二义性。
需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。
虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在student和 teacher的继承person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
虚拟继承的对象模型
菱形虚拟继承之后,会将A类的对象放置在整个类的地址最高处,原本B中存放A的位置处,存放了虚基表的地址,该虚继表中存放了该位置到重新存储A位置的偏移量。