什么是继承??
继承是面向对象复用的重要手段,通过继承定义一个类,继承是类型之间的关系建模,共享共有的东西,实现各自本质不同的东西。
三种继承关系:
public–公有继承
protected–保护继承
private–私有继承
继承方式和访问限定符的关系如下:
总结:
1.public继承:基类的非私有成员在子类的访问属性都不变
2.protected继承:基类的非私有成员都成为子类的保护成员
3.private继承:基类中的非私有成员都成为子类的私有成员
4.不可见:成员是存在的,但是不能去访问
5.public继承是一个is-a的关系,每个子类对象也是一个父类对象
6.protected/private继承是has-a的关系
继承与转换(赋值兼容规则,public继承)
1.子类对象可以赋值给父类对象(切片)–天然的,没有类型转换的发生
2.父类对象不能赋值给子类对象
3.父类的指针/引用可以指向子类对象
4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
#include <iostream>
#include<string>
using namespace std;
class Person
{
public:
void Display()
{
std::cout<<_name<<std::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;
r2._num = 20;
std::cout<<p2->_num<<std::endl;
}
int main()
{
Test();
return 0;
}
当父类对象赋值给子类对象时会出现如下错误:
p2->_num = 10;
r2._num = 20;
执行上面两句代码后的结果:
总结:
只有父类的指针/引用本来就是指向子类时,父类的指针/引用才能强转为子类的指针/引用
继承体系中的作用域
- 在继承体系中基类和派生类都有独立的作用域
- 子类和父类有同名成员,子类成员将屏蔽父类对成员的直接访问(在子类成员函数中,可以使用 基类::基类成员 访问)
- 隐藏的特征:
1.在不同作用域(分别在基类和派生类)
2.函数名相同
3.在基类中只要不构成重写就是重定义
#include <iostream>
#include<string>
using namespace std;
class Person
{
public:
Person(const char* name = "",int id = 0)
:_name(name)
,_num(id)
{}
protected:
string _name;
int _num;
};
class Student:public Person
{
public:
Student(const char* name,int id,int stuNum)
:Person(name,id)
,_num(stuNum)
{}
void DisplayNum()
{
//Person::_num重定义
std::cout<<"身份证号:"<<Person::_num<<std::endl;
std::cout<<"学号:"<<_num<<std::endl;
}
protected:
int _num;
};
void Test(){
Student s1("paul",110,1);
s1.DisplayNum();
}
int main()
{
Test();
return 0;
}
派生类的默认成员函数
#include <iostream>
#include<string>
using namespace std;
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& opreator(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& s)"<<endl;
if(this != &s)
{
Person::operator=(s);//如果不加Person::就会就近调用,造成循化递归,栈溢出
_num = s._num;
}
return *this;
}
~Student()
{
cout<<"~Student()"<<endl;
}
private:
int _num;
};
void Test()
{
Student s1("Alic",18);
Student s2(s1);
Student s3("Tom",18);
s1 = s3;
}
int main()
{
Test();
return 0;
}
单继承&多继承&菱形继承
菱形继承中的Assistant的对象中有两份Person成员,菱形继承存在二义性和数据冗余的问题,所以引入了虚继承,来解决菱形继承的问题
虚继承
1.虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题2.虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗
- 虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间),和虚基类表(不占用类对象的存储空间)(虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承
- 实际上,vbptr指的是虚基类表指针,该指针指向了一个虚基类表,虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间
- 虚函数与虚继承
1.他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
2.虚基类依旧存在继承类中,只占用存储空间,虚函数不占用存储空间
3.虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表的存储的是虚函数地址