继承作为面向对象的三大特性之一,是面向对象复用的重要手段,通过继承定义一个类,继承是类型之间的关系建模,共享共有的东西,实现各自本质不同的东西。
继承是一种复用手段,在继承关系里,基类继承派生类的成员,以此来达到复用的效果。
先看一段简单的继承:
简单认识下继承实现复用,
成员访问限定符&继承关系
三种继承关系下基类成员的在派⽣类的访问关系变化
简单的总结下:
三种继承关系:public,protected,private其实是权限放缩的过程,public继承不会改变父类成员(成员变量&成员函数)的性质,proteced继,父类的public成员访问权限成为了protected(权限放缩),private继承,成员权限放缩的最小。均成为私有。
备注:protected与private成员对类外均为不可见,两者间的区别主要体现在继承过程中,private成员子类继承后子类仍然不可见,protected成员可见。
总结:
1.基类的私有成员在派⽣类中是不能被访问的,如果⼀些基类成员不想被基类对象直接访问,但需要在派⽣类中能访问,就定义为保护成员。可以看出保护成员限定符是因继承才出现的。
2.public继承是⼀个接口继承,保持is-a原则,每个⽗类可⽤的成员对⼦类也可⽤,因为每个⼦类对象也都是⼀个⽗类对象。
3.protetced/private继承是⼀个实现继承,基类的部分成员并未完全成为⼦类接口的⼀部分,是 has-a 的关系原则,所以⾮特殊情况下不会使⽤这两种继承关系,在绝⼤多数的场景下使⽤的都是公有继承。
4.不管是哪种继承⽅式,在派⽣类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在⼦类中不可见(不能访问)。
5.使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
6.在实际运⽤中⼀般使⽤都是public继承,极少场景下才会使⽤protetced/privat继承.。
备注:struct与class的区别
a.struct的默认成员是共有的,class是siyoude;
b.struct的默认继承关系是共有的,class是私有的。
继承与转换–赋值兼容规则–public继承
1. ⼦类对象可以赋值给⽗类对象(切割/切⽚);
2. ⽗类对象不能赋值给⼦类对象;
3. ⽗类的指针/引⽤可以指向⼦类对象;
4. ⼦类的指针/引⽤不能指向⽗类对象(可以通过强制类型转换完成)。但这样做会留下隐患,父类中没有子类的成员,子类通过强转赋给父类,父类访问子类定义的成员时,因为自身没有导致访问越界,造成程序崩溃。
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void Display()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num; // 学号
};
void Test()
{
Person p;
Student s;
// 1.⼦类对象可以赋值给⽗类对象(切割 /切⽚)
p = s;
// 2.⽗类对象不能赋值给⼦类对象
//s = p; 编译器不会允许这样的操作发生
// 3.⽗类的指针/引⽤可以指向⼦类对象
Person* p1 = &s;
Person& r1 = s;
// 4.⼦类的指针/引⽤不能指向⽗类对象(可以通过强制类型转换完成)
Student* p2 = (Student*)& p;
Student& r2 = (Student&)p;
//p2->_num = 10; 赋值过来后,但访问子类定义的成员会造成崩溃
}
int main()
{
Test();
system("pause");
return 0;
}
从图片中也可以看出,如果子类强转赋给父类(引用和指针,对象赋值即便强转也无法赋过去),父类在指向_num时,因为后续空间不是自己的,会非法访问,造成崩溃。
继承体系中的作用域:
1. 在继承体系中基类和派⽣类都有独⽴的作⽤域;
2. ⼦类和⽗类中有同名成员,⼦类成员将屏蔽⽗类对成员的直接访问。(在⼦类成员函数中,可以使⽤基类::基类成员 访问)–隐藏 –重定义;
3. 但在继承体系中最好不要定义同名的函数。
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
void Display()
{
cout << "A::Display()" << endl;
}
protected:
int _a;
};
class B :public A
{
public:
void Display()
{
cout << "B::Dispiay()" << endl;
}
protected:
int _b;
};
int main()
{
B b;
b.Display();
b.A::Display();
system("pause");
return 0;
}
输出结果是:
思考下重载与重定义的区别?
重载:必须在同一作用域内,函数名相同,参数类型/个数不同构成重载。
重定义:在继承中,子类与父类有同名成员,子类成员将隐藏父类的成员的访问。
扩展:
看下面的代码
#include <iostream>
using namespace std;
int x = 10;
int main()
{
int x = 20;
//就近原则
cout << x << endl;
//'::'默认为全局域
cout << ::x << endl;
system("pause");
return 0;
}
派⽣类的默认成员函数:
在继承关系⾥⾯,在派⽣类中如果没有显⽰定义这六个成员函数,编译系统则会默
认合成六个默认的成员函数。(主要为前四个)
#include <iostream>
#include <string>
using namespace std;
class person
{
public:
person(const char* name)
:_name(name)
{
cout << "person(const char* name)" << 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(const char*name,int num)" << 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);
_num = s._num;
}
return *this;
}
~student()
{
cout << "~student()" << endl;
}
private:
int _num;
};
void test()
{
student stu1("hehe", 18);
student stu2(stu1);
student stu3("lala", 20);
stu3 = stu2;
}
int main()
{
test();
system("pause");
return 0;
}
分析运行结果可以看出,子类在实现自身的构造函数时,需要使用匿名构造,直接构造父类的成员变量是不被允许的,在赋值运算符的重载时,同样赋值父类成员时需要调用父类的赋值运算符重载(此时父类的赋值是重定义的,需要person::operator=(s)这样去调用),在析构时遵循先构造的后析构,所以先析构子类,父类的析构会自动调用。
不难想到:我们如果在创建一个类时,让其构造函数私有,那么这个类将无法被继承。
单继承&多继承
1.单继承–⼀个⼦类只有⼀个直接⽗类时称这个继承关系为单继承
2. 多继承–⼀个⼦类有两个或以上直接⽗类时称这个继承关系为多继承
3.
c++支持多继承会造成菱形继承的产生
下面是一个简单的菱形继承
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职⼯编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
上述菱形继承的图解:
菱形继承对象的模型
菱形继承的问题经过上述两图的简单分析会明显的暴露出来:
1.代码的二义性(Assistant的对象访问 _name 时访问 Student类的还是Teacher类的)
2.数据冗余的问题,显然 Assitant的name存在了两个。
代码的二义性c++不难想出,可以使用 类名+’::’+成员名 这样的方式去解决,
数据的冗余c++使用了虚继承的方式去进行解决。下面就探讨下虚继承:
虚继承就是在继承关系前加 virtual:
class Person
{}
class Student :virtual public Person
{}
class Teacher :virtual public Person
{}
class Assistant : public Student, public Teacher
虚继承是为了解决菱形继承数据冗余的问题产生的,所以很显然Assistant类里面关于Person类的成员只有一份,指向共有的地方。那么c++是如何去实现虚继承的呢?
为方便演示改用下面的代码:
#include <iostream>
#include <string>
using namespace std;
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.A::_a = 1;
d.B::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
首先先看下对象d的内存监视:
上图截取了普通继承与虚继承的内存管理,可以看出,虚继承在存储公共区域时,存储了可以指向虚基表的一个指针,虚基表内存储了到公共区域的偏移数。
在实现虚继承时引入虚基表用来存储到公共区域的偏移数,解决数据冗余的问题,当公共区域所占字节数越大,虚继承的优势便越明显。
以上呢就是我对继承的简单认识,希望大家多多交流。