目录
1.什么是继承?
继承方式有三种:public,protected,private
基类的成员访问限定符也有三种,与其组合,继承下来的成员在派生类的访问权限就有九种情况:
情况较多,简便的记忆方式是基类的private成员在派生类中不可见,其他继承下的成员在派生类的访问权限取继承方式和基类成员访问限定符的较小权限。(public>protected>private)
上述的不可见是指在派生类中也不可访问,但是基类的成员是继承下来的。基类的protected成员只有在基类和派生类中可以访问,可见是继承中特殊的访问限定符,可以说是为继承而生的限定符。
因为protected和private继承都改变了基类成员在派生类成员的访问权限,较为复杂,故实际中基本使用public继承。
注:倘若不写继承方式,class默认的继承方式是private,struct则是public。
2.切片
int main()
{
student s;
person* p = &s;
return 0;
}
3.隐藏(重定义)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
class person
{
public:
size_t _age=20;
string _name;
};
class student:public person
{
public:
void setName()
{
_name = "zhangsan";
person::_name = "lisi";
}
public:
std::string _id="00000";
string _name;
};
int main()
{
student s1;
s1.setName();
cout << s1._name << endl;
cout << s1.person::_name << endl;
return 0;
}
输出:
注意:如果是成员函数,当函数名相同时就构成隐藏。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
class person
{
public:
void setAge()
{
_age = 20;
}
size_t _age=20;
};
class student:public person
{
public:
void setAge(int age)
{
_age = age;
}
public:
std::string _id="00000";
};
int main()
{
student s1;
s1.setAge(15);//不显示写基类的类域则无法直接调用到基类的同名函数
cout << s1._age << endl;
s1.person::setAge();
cout << s1._age << endl;
return 0;
}
输出:
隐藏与函数重载的区别是,函数重载是在同一个域中,而隐藏是在派生类域和基类域中。
因为成员同名会带来隐藏的问题,容易让人混淆,故最好不定义同名的成员。
4.派生类的默认成员函数
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
class person
{
public:
person(size_t age = 20)
:_age(age)
{
cout << "person()" << endl;
}
size_t _age;
};
class student:public person
{
public:
student(string id="0000")
:_id(id)
{
cout << "student()" << endl;
}
std::string _id;
};
int main()
{
student s("28937");
return 0;
}
输出:可见,派生类的默认构造函数先调用了基类的默认构造函数完成继承下的基类成员初始化
student& operator=(student& s)
{
person::operator=(s);//调用方法
return *this;
}
class person
{
public:
person(size_t age = 20)
:_age(age)
{
cout << "person()" << endl;
}
~person()
{
cout << "~person()" << endl;
}
size_t _age;
};
class student:public person
{
public:
student(string id="0000")
:_id(id)
{
cout << "student()" << endl;
}
~student()
{
cout << "~student()" << endl;
}
std::string _id;
};
int main()
{
student s("28937");
return 0;
}
输出:
在汇编代码下,派生类的代码可以验证:
注意:因为后续在多态的场景下析构函数需要构成重写,重写的条件之一是函数名相同,所以编译器会对析构函数名进行特殊处理,将基类和派生类的函数名都处理成destructor,所以基类析构函数在不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。
5.继承与友元
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
class student;
class person
{
public:
friend void printIdPassword(person& p,student& s);
private:
string secret = "password";
public:
size_t _age=20;
};
class student:public person
{
private:
std::string _id="00000";
};
void printIdPassword(person& p,student&s)
{
cout << p.secret << endl;
cout << s._id << endl;
}
int main()
{
person p;
student s;
printIdPassword(p,s);
return 0;
}
6.继承与静态成员
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
class person
{
public:
person()
{
++countPerson;
}
static int countPerson;
size_t _age=20;
};
int person::countPerson = 0;
class student:public person
{
private:
std::string _id="00000";
};
int main()
{
person p;
student s;
printf("%p\n", &p.countPerson);
printf("%p\n", &s.countPerson);
cout << p.countPerson << endl;
cout << s.countPerson << endl;
person p1;
person p2;
person p3;
person p4;
student s1;
student s2;
cout << p.countPerson << endl;
cout << s.countPerson << endl;
return 0;
}
输出:从输出可看出基类的静态成员在继承体系中只有一份实例。
7.菱形继承和菱形虚拟继承
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class A { public: int m_a = 1; };
class B:public A { public: int m_b = 2; };
class C:public B { public: int m_c = 3; };
class D:public B,public C { public: int m_d = 4; };
int main()
{
D d;
return 0;
}
从内存中亦可验证之:
要解决数据的二义性和数据冗余问题可以采用虚继承,即让B和C虚继承A即可。
即:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class A { public: int m_a = 1; };
class B:virtual public A { public: int m_b = 2; };
class C:virtual public A { public: int m_c = 3; };
class D:public B,public C { public: int m_d = 4; };
int main()
{
D d;
return 0;
}
B和C虚继承A时,将m_a存放在高地址。
B的对象模型是一个虚基表指针和m_b,m_a,虚基表存储了m_a相对B对象起始地址的偏移量。
C的对象模型是一个虚基表指针和m_c,m_a,虚基表存储了m_a相对C对象起始地址的偏移量。
而D继承了B和C,D的对象模型是B的虚基表指针,m_b,C的虚基表指针,m_c,最后是m_a。
从内存中可以验证:
这样一来D的对象模型中就只有一份m_a解决了数据二义性和数据冗余问题,通过虚基表指针来查找虚基表中的偏移量从而确定了m_a的存储位置,这样一来D对象模型中仅多存储了指针,当A对象很大时能节省空间。
8.总结
C++中的多继承可能会导致菱形继承的问题,需用菱形虚拟继承解决,底层实现较复杂,且多继承的适用场景不多,在C++后续出现的面向对象语言都舍弃了多继承,采用了单继承,多继承可认为是C++的不足。
继承与组合:继承即是上述所谈及的继承,组合是指对象中存储了另一个对象。
继承可以认为是is-a的关系,B继承了A可以认为B是A,而组合是has-a的关系,B组合了A是指B对象中有A对象。
(白箱复用)通常使用的继承(public),基类的的实现细节对派生类可见,继承一定程度破坏了基类的封装性,基类的改变对派生类影响大,派生类和基类的关系紧密,耦合度高。
(黑箱复用)组合是类复用的另一种手段,类的内部实现细节不可见,类与类之间的依赖关系弱,耦合度低,故组合保持了类的封装性。
故:优先使用组合而不是继承,组合的耦合度更低,代码维护性更强,但当适合继承的场景也得使用,实现多态时也必须使用继承。