继承
C++ 继承的一般形式如下:
class DeriveClassName : Inheritlabel BaseClassName
{
//...
};
DeriveClassName:派生类的名称。
Inheritlabel:继承权限(public、protected、private),基类成员在派生类中的可见性。
BaseClassName:基类名称。
派生类继承基类时是全盘继承,基类所有的东西都继承给派生类,除了基类的构造和析构不继承。
如下程序:
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "Construct A" << endl; }
~A() { cout << "Destruction A" << endl; }
private:
int m_id;
int m_score;
};
class B : private A
{
public:
B() { cout << "Construct B" << endl; }
~B() { cout << "Destruction B" << endl; }
private:
int m_age;
};
int main(int argc, char** agrv)
{
B b;
cout << "sizeof b:" << sizeof b << endl;
// cout << b.m_id << endl; //基类私有成员在派生类中不可访问
return 0;
}
运行结果:
$ ./test
Construct A
Construct B
sizeof b:12
Destruction B
Destruction A
可以看到,b 对象的大小是 12,包含了基类私有的成员,表示基类的private成员也被继承了,但是在派生类中却并不能访问,这个权限的设计是由意义的,用于保护基类的私有数据,并不是所有的基类数据都希望派生类可以访问,但是通过基类的公有成员函数可以间接访问,如果你想在派生类型红访问,就应该将其定义为 protected 或 public 的。
继承的访问权限从如下两方面看:
- 对象:对象只能直接访问类中公有方法和成员。
- 继承的派生类:不同继承方式,派生类中包含基类的成员的访问权想也可能不一样。具体如下:
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类private成员 | 派生类不可见 | 派生类不可见 | 派生类不可见 |
从中可以看到,私有继承直接终止了基类再向下继承的能力。注意:在 C++ 中默认继承为私有继承。如下程序:
class D : public B1, public B2, public B3
{
//公有继承B1,B2,B3
};
class D : public B1, B2, B3
{
//公有继承B1,私有继承B2、B3
};
多继承的构造中,是按照继承的顺序进行构造,和构造函数的初始化顺序无关,如下程序:
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "Construct A" << endl; }
~A() { cout << "Destruction A" << endl; }
};
class B
{
public:
B() { cout << "Construct B" << endl; }
~B() { cout << "Destruction B" << endl; }
};
class C : public A, public B
{
public:
C() : B(), A()
{
cout << "Construct C" << endl;
}
~C() { cout << "Destruction C" << endl; }
};
int main(int argc, char** agrv)
{
C cc;
return 0;
}
运行结果:
$ ./test
Construct A
Construct B
Construct C
Destruction C
Destruction B
Destruction A
可以看到即使构造函数初始化顺序和继承顺序不同,但是还是按继承的顺序构造。
如果派生类中有对象成员,构造函数的顺序依次是:1. 基类;2.对象成员;3.自己。看如下程序:
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "Construct A" << endl; }
~A() { cout << "Destruction A" << endl; }
};
class B
{
public:
B() { cout << "Construct B" << endl; }
~B() { cout << "Destruction B" << endl; }
};
class B1
{
public:
B1() { cout << "Construct B1" << endl; }
~B1() { cout << "Destruction B1" << endl; }
};
class B2
{
public:
B2() { cout << "Construct B2" << endl; }
~B2() { cout << "Destruction B2" << endl; }
};
class C : public A, public B
{
public:
C() : B(), A()
{
cout << "Construct C" << endl;
}
~C() { cout << "Destruction C" << endl; }
private:
B2 b2;
B1 b1;
};
int main(int argc, char** agrv)
{
C cc;
return 0;
}
程序运行结果:
$ ./test
Construct A
Construct B
Construct B2
Construct B1
Construct C
Destruction C
Destruction B1
Destruction B2
Destruction B
Destruction A
如果基类中有虚基类,先构造的是虚基类,关于虚基类将在下文详细描述,先看如下实例:
$ ./test
Construct A
Construct B
Construct B2
Construct B1
Construct C
Destruction C
Destruction B1
Destruction B2
Destruction B
Destruction A
虚继承
虚继承主要解决的是菱形继承问题。先看以下程序:
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "Crate A" << endl; }
~A() { cout << "Free A" << endl; }
public:
int a;
};
class B1 : public A
{
public:
int b1;
};
class B2 : public A
{
public:
int b2;
};
class C : public B1, public B2
{
public:
int c;
};
int main()
{
C cc;
cc.c = 10; //可行
//cc.a = 100; //错误,不可以对a进行
return 0;
}
此时的继承模型为:
其内存模型为:
我们并不能对基类 A 的成员 a 进行赋值,会引发错误,编译器直接报错,因为 C 继承了 B1、B2,而这两个类又继承 A,则在 C 中会出现两个 a 成员,直接访问 cc.a 会出现二义性。
但是我们可以指定访问 A1,还是 A2 中的 a 就不会报错,如下程序:
int main()
{
C cc;
cc.c = 10; //可行
//cc.a = 100; //错误,不可以对a进行
cc.B1::a = 100; //可行
cc.B2::a = 200; //可行
return 0;
}
再来看内存中的 B1 和 B2 的成员 a 已经被赋值了,并且值不同。说明这两个成员 a 并不是同一内存地址。
那么,如果我们只希望基类 A 的成员 a 在派生类中只有 1 份,那么就需要虚继承,虚继承的一般写法如下:
class B1 : virtual public A
{
//虚继承就是在要继承的基类前加关键字virtual
};
对于虚继承来的基类,又叫做虚基类。
现在我们改写程序如下:
class B2 : virtual public A
{
public:
int b2;
};
class C : public B1, public B2
{
public:
int c;
};
int main()
{
C cc;
cc.c = 10; //可行
cc.a = 100; //可行,因为是虚继承,基类的成员a只有一份
return 0;
}
现在就可以正确运行了,我们看一下内存成员 a 的值:
现在 B1 和 B2 中的成员 a 值都是 100 了。 而且我们可以通过打印变量地址看到 B1 中 a 成员的地址和 B2 中 a 成员的地址是同一地址,而如果不使用虚继承,则不是同一地址。所以虚继承让派生类只保持基类的一份成员拷贝,B1 和 B2 的继承的成员的空间是相同的。
下面再来看一下,C++中虚继承时所占用的空间情况,有下面程序:
#include <iostream>
using namespace std;
#pragma pack(1)
class A //大小为4
{
public:
int a;
};
class B : virtual public A //大小为16,变量a,b共8字节,虚基类表指针8
{
public:
int b;
};
class C : virtual public A //与B一样16
{
public:
int c;
};
class D : public B, public C //32,变量a,b,c,d共16,B的虚基类指针8,C的虚基类指针8
{
public:
int d;
};
int main()
{
A a;
B b;
C c;
D d;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
cout << sizeof(d) << endl;
return 0;
}
这个程序在不同的编译器下运行的结果可能不一样,但是必须要说明的是:使用虚继承时,由于要涉及到虚基表指针,会增加一个 vbPtr (virtual base table pointer) 指针指向虚基表,这个虚基表指针所占空间为 8 个字节(gcc9_x64下),但是在多继承中,还要乘上基类的和个数。这里特意指定了字节对齐为 1,否则编译器会额外增加字节。
虚继承下,A 类将变成一个虚基类,从打印的内存布局来看,D 中的 B 和 C 中都不包含虚基类A,而是有一个虚基表指针,而虚基类 A 在 D 中,只有一份,其 B 和 C 的虚基表中记录了 vbptr 与本类的偏移地址和 vbptr 到共有且唯一的虚基类元素之间的偏移量。看一下如下内存布局:
> cl CppTest.cpp /d1reportSingleClassLayoutD
class D size(24):
+---
0 | +--- (base class B)
0 | | {vbptr}
4 | | b
| +---
8 | +--- (base class C)
8 | | {vbptr}
12 | | c
| +---
16 | d
+---
+--- (virtual base A)
20 | a
+---
D::$vbtable@B@:
0 | 0
1 | 20 (Dd(B+0)A)
D::$vbtable@C@:
0 | 0
1 | 12 (Dd(C+0)A)
vbi: class offset o.vbptr o.vbte fVtorDisp
A 20 0 4 0
关于虚基类表详细介绍,请点击这里。
继承中的隐藏、覆盖、重载
https://www.cnblogs.com/Lalafengchui/p/3994340.html
有关的关键名词:重载(overload),覆盖(override),隐藏(hide)。
继承中的隐藏
派生类继承基类,若派生类中有基类的同名方法,优先访问的是派生类的方法,派生类会隐藏基类所有的同名方法,即使基类有一个同名但参数不同的方法也是如此。看如下程序:
#include <iostream>
using namespace std;
class A
{
public:
void fun()
{
cout << "A::fun" << endl;
}
void fun(int)
{
cout << "A::fun(int)" << endl;
}
};
class B : public A
{
public:
void fun()
{
cout << "B::fun" << endl;
}
};
int main()
{
B b;
b.fun(); //正确,默认访问B作用与的成员,且所有A作用域同名的都会被隐藏
// b.fun(1); //错误
b.A::fun(1); //正确,指定访问A作用域的成员
return 0;
}
派生类对象直接访问基类的 fun(int a) 方法,编译会报错,但是通过显示调用基类的作用域的方法是可以的。
继承中的覆盖
https://www.cnblogs.com/Lalafengchui/p/3994340.html
覆盖发生在有虚函数的继承下,基类的方法是虚方法(带 virtual 关键字),派生类重写了基类的虚方法,基类的方法将会被覆盖。基类中的方法和派生类中的方法需要一模一样(函数名,参数,返回值类型),派生类对象调用派生类作用域中的成员函数,基类作用域中的同名成员就像是不存在一样,(可以显示调用)即基类该成员被派生类成员覆盖。这里很多人会感觉疑惑,认为是隐藏,因为基类的成员函数依然存在,依然可以调用,只是优先调用派生类的,也就是“隐藏”了。而“覆盖”两个字的意思,应该是一个将另一个替代了,也就是另一个不存在了。下面解释一下,“覆盖”二字的由来。
首先需明白一点,虚函数的提出,是为了实现多态。也就是说,虚函数的目的是为了,在用基类指针指向不同的派生类对象时,调用虚函数,调用的是对应派生类对象的成员函数,即可以自动识别具体派生类对象。所以,上述例子中,直接用派生类对象调用虚函数是没有意义的,一般情况也不会这样使用。看如下示例:
#include <iostream>
using namespace std;
class Father
{
public:
virtual void ff1() { cout << "father ff1" << endl; }
};
class Childer_1 : public Father
{
public:
void ff1() { cout << "childer_1 ff1 " << endl; }
};
class Childer_2 : public Father
{
public:
void ff1() { cout << "childer_2 ff1" << endl; }
};
int main()
{
Father *fp;
Childer_1 ch1;
fp = &ch1;
fp->ff1();
Childer_2 ch2;
fp = &ch2;
fp->ff1();
return 0;
}
上面的示例,都是基类指针的形式,pf->ff1() 。因为 fp 的指向不同对象,所以调用不同对象的虚函数。但从代码上看,fp 是一个 Father 类的指针,但调用的是派生类成员函数,就好像基类的成员被覆盖了一样。这就是覆盖一词的来源。
覆盖的情况下,派生类虚函数必须与基类虚函数有相同的参数列表,否则认为是一个新的函数,与基类的该同名函数没有关系。但不可以认为两个函数构成重载。因为两个函数在不同的域中。看如下示例:
#include <iostream>
using namespace std;
class Father
{
public:
virtual void ff1() { cout << "father ff1" << endl; }
};
class Childer_1 : public Father
{
public:
void ff1(int a) { cout << "childer_1 ff1 " << endl; }
};
int main()
{
Father *fp;
Childer_1 ch1;
fp = &ch1;
fp->ff1();
//ch1.ff1(); //错误,没有匹配的成员
ch1.ff1(2);
return 0;
}
运行结果:
father ff1
childer_1 ff1
上面的 fp->ff1(); 的运行结果可以看出,fp 虽然指向派生类对象,并且调用的是虚函数。但是该虚函数,在派生类中没有对应的实现,只好使用基类的该成员。即派生类中带参 ff1 并没有覆盖从基类中继承的无参ff1。而是认为是一个新函数。
继承中的重载
重载必须在同一个域中,函数名称相同但是函数参数不同,重载的作用就是同一个函数有不同的行为,因此不是在一个域中的函数是无法构成重载。如果一个在基类域一个在派生类域,是不会存在重载的,属于隐藏的情况。调用时,只会在派生类域中搜索,如果形参不符合,会认为没有该函数,而不会去基类域中搜索。
多态
关于多态,简而言之就是用基类型别的指针指向其派生类的实例,然后通过基类的指针调用实际派生类的成员函数。这种技术可以让基类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
赋值兼容规则
- 可以将派生类对象赋值给基类,其实只是把派生类中的基类的部分,赋值给了基类。(称为对象的切片)
- 派生类的对象地址赋值给基类的指针
- 派生类的对象初始化基类的引用
如下程序:
#include <iostream>
using namespace std;
class A
{
public:
virtual void func() { cout << "A::func" << endl; }
public:
int a;
};
class B : public A
{
public:
void func() { cout << "B::func" << endl; }
public:
int b;
};
int main()
{
A a;
B b;
a = b; //派生类对象赋值给基类
A *pa = &b; //派生类的对象地址赋值给基类的指针
A &ra = b; //派生类的对象初始化基类的引用
pa->func();
ra.func();
return 0;
}
达到多态
想要实现多态,就需要动态绑定,需要基类的指针或基类的引用,并且基类的方法为虚方法,派生类覆盖基类的虚方法,才能达到多态,上述程序中,因为 func 是基类的虚函数,所以达成了多态,在使用基类的指针或引用调用 func 函数,其调用到派生类的 func 函数,运行结果如下:
$ ./test
B::func
B::func
但是派生类中基类没有的方法基类指针不能访问到,基类指针只能访问到基类自己的作用域范围。
注意:
virtual 关键字与函数重载无关,基类没有 virtual,派生类的同名方法就是隐藏而不是覆盖。
只要没有指针或引用,基类调用派生类的,全部是调用基类作用范围内的,就算派生类覆盖了基类的 virtual 方法也是如此。
派生类覆盖了基类的虚方法,派生类的方法也是虚方法,即使前面没有 virtual 关键字也是如此。
派生类要覆盖基类的方法,就是要函数名参数都必须一样才叫覆盖。
多态的实现原理:
首先看下面的程序:
#include <iostream>
using namespace std;
class Base
{
public:
Base() : y(0)
{
cout << "Create Base Object." << endl;
}
public:
virtual void fun()
{
cout << "This is Base::fun()" << endl;
}
virtual void list()
{
cout << "This is Base::list()" << endl;
}
void print()
{
cout << "This is Base::print()" << endl;
}
public:
int y;
};
class D : public Base
{
public:
D() : x(0)
{
cout << "Create D Object." << endl;
}
public:
void fun()
{
cout << "This is D::fun()" << endl;
}
void list()
{
cout << "This is D::list()" << endl;
}
void print()
{
cout << "This is D::print()" << endl;
}
private:
int x;
};
void main()
{
D d;
Base* pb = &d;
pb->fun();
pb->list();
pb->print();
}
某个类只要有虚函数,创建的实例对象就会自动建立一个虚表指针指向一个虚表,虚表指针一般在对象空间的开始位置,虚表的最后一个位置为 NULL(VC 下)。
注意:当派生类没有虚函数,但是基类存在虚函数的时候,创建的派生类对象依旧会存在一个虚表指针。如下程序:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "This is Base::fun()" << endl;
}
};
class D : public Base
{
public:
void fun()
{
cout << "This is D::fun()" << endl;
}
};
int main()
{
Base base;
printf("Base 虚表指针地址:%p\n", *(int *)&base);
D d;
printf("D 虚表指针地址:%p\n", *(int *)&d);
return 0;
}
运行结果:
Base 虚表指针地址:00000000704BB030
D 虚表指针地址:00000000704BB060
用派生类对象赋值给基类的对象指针,基类对象的指针就指向了派生类的对象空间,基类操作派生类的范围是有限制的,只能操作到派生类中基类的范围。在构造派生类对象前,先构造一个基类对象,基类构造完后,虚表中的虚函数都是基类的,当派生类构造完成后,若派生类重写了基类的虚函数,或派生类有了自己的虚函数,虚表中的函数已经被更改为派生类的了。我们在基类中加一个虚的 show() 方法,而派生类中不去覆盖基类的 show 方法,可以看到派生类中虚表的成员 show() 还是基类的函数指针。
用基类的指针调用虚函数,将会在虚表中查找,派生类重写了基类的虚函数,派生类实例对象的虚表中的虚函数地址已经变成派生类的虚函数的地址,所以会调用派生类的函数。这就是多态的实现基本原理。
但还有很多复杂一些的情况,查看请点击这里。
抽象类
如果基类是一个抽象的概念,就可以将其定义纯虚函数构成一个抽象类。
如下的抽象类就定义了三个纯虚函数:
#include <iostream>
using namespace std;
class A
{
public:
virtual void eat() = 0;
virtual void sleep() = 0;
virtual void foot() = 0;
};
int main()
{
//A a; //错误,包含纯虚函数的类不能定义对象实例
return 0;
}
注意:
如果一个类具有一个或者多个纯虚函数,那么它就是一个抽象类,必须让其他类继承它。
抽象类位于类层次的上层,不能定义抽象类的对象。
基类中的纯虚函数没有代码,而且必须在派生类中覆盖基类中的纯虚函数。
派生类中如果没有实现其抽象基类的所有纯虚函数,其也是一个抽象类,也不能实例化对象。
抽象类和虚函数的类有区别:
抽象类中的纯虚函数不实现,并且继承他的派生类必须实现它。
虚函数基类中可以有实现,并且派生类中如果重新覆盖了它,还可以实现多态机制。