一、三种继承方式
1、类成员的public,protected,private三种属性
类成员的访问权限由高到低依次为 public --> protected --> private,public成员可以通过对象直接访问,protected和private成员不能通过对象直接访问,并且protected可以在派生类中使用,而private成员在派生类中不能使用。
2、public、protect、private指定继承方式
不同的继承方式会影响基类成员在派生类中的访问权限。
1) public继承方式
- 基类中所有 public 成员在派生类中为 public 属性;
- 基类中所有 protected 成员在派生类中为 protected 属性;
- 基类中所有 private 成员在派生类中不能使用。
2) protected继承方式
- 基类中的所有 public 成员在派生类中为 protected 属性;
- 基类中的所有 protected 成员在派生类中为 protected 属性;
- 基类中的所有 private 成员在派生类中不能使用。
3) private继承方式
- 基类中的所有 public 成员在派生类中均为 private 属性;
- 基类中的所有 protected 成员在派生类中均为 private 属性;
- 基类中的所有 private 成员在派生类中不能使用。
基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。
3、改变访问权限
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限。
例(没写全,如果不懂,可自行百度)
//派生类Student
class Student : public People {
public:
void learning();
public:
using People::m_name; //将protected改为public
using People::m_age; //将protected改为public
float m_score;
private:
using People::show; //将public改为private
};
二、派生类对象的内存布局以及对基类成员的使用
参考博客
https://blog.youkuaiyun.com/codercong/article/details/52065106
1、单一继承
(1)派生类完全拥有基类的内存布局,并保证其完整性。
派生类可以看作是完整的基类的Object再加上派生类自己的Object。如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异。另外,一定要保证基类的完整性。实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object。举个栗子:
class A
{
int b;
char c;
};
class A1 :public A
{
char a;
};
int main()
{
cout << sizeof(A) << " " << sizeof(A1) << endl;
return 0;
}
输出是什么?
答案:8 12
A类的话,一个int,一个char,5B,内存对齐一下,8B。A1的话,一个int,两个char,内存对齐一下,也是8B。不对吗?
要保证基类对象的完整性。那么一定要保证A1类前面的几个字节一定要与A类完全一样。也就是说,A类作为内存补齐的3个字节也是要出现在A1里面的。也就是说,A类是这样的:int(4B)+char(1B)+padding(3B)=8B,
A1类:int(4B)+char(1B)+padding(3B)+char(1B)+padding(3B)=12B。
(2)数据成员继承与同名函数
在C++中,派生类在定义构造函数时,会调用基类构造函数首先完成基类部分的构造。
在定义派生类的时候,C++语言允许在派生类中说明的成员与基类中的成员名字相同,也就是说,派生类可以重新说明与基类成员同名的成员。如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中重新说明的成员。为了在派生类中使用基类的同名成员,必须在该成员名之前加上基类名和作用域标识符“::”,即必须使用下列格式才能访问到基类的同名函数。
基类名::成员名
当派生类存在与基类同名的成员变量时候,派生类的成员会隐藏基类成员,但派生类中存在基类成员的拷贝
(3)派生类重写基类重载函数时需要注意的问题:派生类函数屏蔽基类中同名函数
转载博客
https://blog.youkuaiyun.com/iicy266/article/details/11906697
派生类可以继承基类中的非私有函数成员,当然也就可以继承其中非私有的被重载的函数。如下:
class Base {
public:
void print() {
cout << "print() in Base." << endl;
}
void print(int a) {
cout << "print(int a) in Base." << endl;
}
void print(string s) {
cout << "print(string s) in Base." << endl;
}
};
class Derived : public Base { };
int main() {
Derived d;
d.print();
d.print(10);
d.print("");
return 0;
}
运行结果
print() in Base.
print(int a) in Base.
print(string s) in Base.
现在,我们想要在派生类中重写其中的一个重载函数:
class Derived : public Base {
public:
void print() {
cout << "Rewrite print() in Derived." << endl;
}
};
运行结果
reload_test.cc: In function ‘int main()’:
reload_test.cc:39: error: no matching function for call to ‘Derived::print(int)’
reload_test.cc:21: note: candidates are: void Derived::print()
reload_test.cc:40: error: no matching function for call to ‘Derived::print(const char [1])’
reload_test.cc:21: note: candidates are: void Derived::print()
结果出错了,显示说匹配不到后两种情况,这是为什么呢?
下面一段内容来自 C++ Primer:
理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1、首先确定进行函数调用的对象、引用或指针的静态类型。
一个对象(变量)的静态类型就是其声明类型,而一个对象(变量)的动态类型就是指程序执行过程中对象(指针或引用)实际所指对象的类型。
2、 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3、 一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4、假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
当我们在派生类中没有重写重载函数之一的时候,在派生类中调用的重载函数是在其基类中查到的,因此,调用可以成功;然而,当我们仅重写了其中的一个重载函数时,在做函数名匹配时,在本类中就可以匹配到了,就不会向其父类查找了。而在派生类中,仅记录了这个被重写的函数的信息,当然也就没有另外两个重载函数的一些了,因此就导致了上述错误的出现了。
派生类中的函数会将其父类中的同名函数屏蔽掉。 因此,如果派生类想通过自身类型使用基类中重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。
仅重定义一个想要改变的函数的同时能使用其他重载函数的方法
a、通过using在派生类中为父类函数成员提供声明:
前面知道,因为派生类重写的函数名屏蔽了父类中的同名函数,那么我们可以通过using来为父类函数提供声明;这样,派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供 using声明。一个 using 声明只能指定一个名字,不能指定形参表,因此,为基类成员函数名称而作的 using 声明将该函数的所有重载实例加到派生类的作用域。将所有名字加入作用域之后,派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。(使用方法在上面已经介绍)
b、通过基类指针调用
在调用被屏蔽的重载函数时,可以不直接通过派生类对象调用,而是通过基类指针指向派生类对象,通过基类指针进行调用,这样就会直接在基类中进行查找函数名,从而可以匹配并进行类型匹配。
int main() {
Derived d;
Base* bp = &d;
d.print();
bp->print(10);
bp->print("");
return 0;
}
但是这样就有两种调用方式,看起来很不舒服,而且容易弄错。那么把在派生类中需要重载的那个版本相应地在基类中声明为vitual,从而可以实现动态绑定,就能统一的使用基类指针来调用了:(后面介绍)
2、虚函数
参考
http://c.biancheng.net/cpp/biancheng/view/244.html
1、基类的指针指向派生类的对象,指向的是派生类中基类的部分。所以只能操作派生类中从基类中继承过来的数据和基类自身的数据。
2,C++的多态性可以解决基类指针不能操作派生类的数据成员的问题。而虚函数是c++实现多态的机制之一
多态性:对同一消息,不同对象有不同的响应方式。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数是通过虚函数表实现的,下面这篇博客讲述的很清楚,强烈推荐
https://blog.youkuaiyun.com/haoel/article/details/1948051
3、多重继承
多重继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。
多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的Java,C# 等干脆取消了多继承。
a、基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。
b、多继承形式下析构函数的执行顺序和构造函数的执行顺序相反。
c、当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::
,以显式地指明到底使用哪个类的成员,消除二义性。
多重继承的内存布局是按照声明顺序排列内存。
(这里引用了最上面的那个引用博客里的代码)
class point2d
{
public:
virtual ~point2d(){};
float x;
float y;
};
class point3d :public point2d
{
~point3d(){};
float z;
};
class vertex
{
public:
virtual ~vertex(){};
vertex* next;
};
class vertex3d :public point3d, public vertex
{
float bulabula;
};
int _tmain(int argc, _TCHAR* argv[])
{
cout << sizeof(point2d) << " " << sizeof(point3d) << " " << sizeof(vertex) << " " << sizeof(vertex3d) << endl;
return 0;
}
输出: 12 16 8 24。
内存布局:
point2d: vptr(4)+x(4)+y(4)=12B
point3d: vptr+x+y+z=16B
vertex: vptr+next=8B
vertex3d: vptr+x+y+z+vptr+next+bulabula=28B
4、虚拟继承
可以看陈皓大佬的博客
https://blog.youkuaiyun.com/haoel/article/details/3081328
https://blog.youkuaiyun.com/haoel/article/details/3081385