总览
一提到面向对象,就会提到面向对象的三个特征,封装,继承和多态,虽然这种概括可能显得比较笼统,但是都这里还是从这三个角度来进行cpp面向对象的分析。
封装
所谓的封装一般指的就是,隐藏自身的一些属性和方法,对外暴露出一些抽象的有具体的使用目的的方法,这样做的一个显著的好处就是在类的属性需要修改的时候,使用方不需要做相应的改动。使用方不需要关注类的内部具体的实现细节,只需要关注特定的功能,在一定程度上实现了解耦,同时提供了很多灵活性。
多态
提到多态呢,顾名思义,就是说有一样东西他会有几种不同的形态,具体从c++的角度来说,可以分为编译时多态和运行时多.
编译时多态一般就指的是函数的重载;运行时多态一般特指父类的指针去指向子类的对象。关于这部分内容可以看看另一篇随笔:https://www.jianshu.com/p/b2fb9943aaf0
继承
一般说到继承,离不开以下几点,继承方式:公有(public), 私有(private),保护(protected),友元函数。
- 1 公有继承时,基类的公用成员和保护成员在派生类中保持原有的访问属性,其私有成员仍为基类私有,即在派生类中不能访问,在类外也不能访问。私有成员体现了数据的封装性,如果基类的私有成员可以被派生类所访问,即破坏了基类的封装性,这就会失去C++的一个重要特性。
- 2 保护继承(protected)的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。对派生类而言,保护成员类似于公有成员,但对于外部而言,保护成员与私有成员类似。
- 3 私有继承即所有基类成员均变成派生类的私有成员,基类的私有成员仍然不能在派生类中访问。(基类的保护成员在子类中也会变成私有成员)
小结:公有和保护可以被子类继承,私有不可以。父类成员变量的权限和子类继承父类的方式会以一种两者比较取较低的方式存在于此类中。
对象
从下面的代码我们演示了一个先有父亲的基类,然后三个子类分别以公有,私有,保护的方式继承这个父类,发生的现象。
首先讲一下继承类的构造方式:首先会调用父类的构造函数,然后去调用自身的构造函数;析构的时候是相反的,首先析构自身,然后再去析构父类对象。
#include <string>
#include <iostream>
using namespace std;
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
public:
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : public Father {
};
class SonB : protected Father {
};
class SonC : private Father {
};
cout << "sizeof father is " << sizeof(Father) << endl;
cout << "sizeof sonA is " << sizeof(SonA) << endl;
cout << "sizeof sonB is " << sizeof(SonB) << endl;
cout << "sizeof sonC is " << sizeof(SonC) << endl;
结果:
sizeof father is 104
sizeof sonA is 104
sizeof sonB is 104
sizeof sonC is 104
我们首先去看一下,以不同方式继承父类的子类的size会不会不同,实验发size都是一样的,说明不同的继承方式不影响父类在子类内部的存储。
下面如果在子类A中重写父类的一个成员变量,看看会发生什么?
class SonA : public Father {
public:
string gender = "male";
};
sizeof(SonA) = 128
这说明子类新的成员变量并没有把父类的成员变量覆盖掉。同样,要遵守内存对齐。
友元
如果从现实的角度去分析父类,子类,友元之间的关系,友元是你家的生死之交,虽然跟你家没有什么血缘关系,但是他有着你家的钥匙,跟你爹的关系可能比你跟你爹关系还要亲。下面给出一个正式一点的说法:
类对数据进行了隐藏和封装后,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。 友元函数是类外的函数,所以它的声明可以放在类的私有段或公有段且没有区别。友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
下面来看一个例子:友元函数getheight
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
如果在栈上定义
image.png
会提示成员私有,无法获取。但是友元可以获取到类的私有信息:
int getHeight(Father father) {
return father.height;
}
int main()
{
Father father;
cout << getHeight(father);
return 0;
}
结果:
175
菱形继承
在多重继承时可能会导致菱形继承(钻石继承的问题)。
- 多重继承的情况下,严格按照派生类定义时从左到右的顺序来调用构造函数,析构函数与之相反。但是如果基类(基类,父类,超类是指被继承的类,派生类,子类是指继承于基类的类.)中有虚基类的话则构造函数的调用顺序如下:
(1) 虚基类的构造函数在非虚基类的构造函数之前调用;
(2) 若同一层次中包含多个虚基类,这些虚基类的构造函数按照他们的说明顺序调用;
(3) 若虚基类由非虚基类派生而来,则任然先调用基类构造函数,再调用派生诶,在调用派生类的构造函数。 - 钻石继承:B继承A,C继承A,D多重继承B,C,则A会在D中存在两份拷贝,而且调用的时候不知道调用谁,不过可以通过D.B::f来指定调用。
解决这个问题的方式是使用虚继承,虚继承的原理是通过虚基类表指针指向虚基类表,虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
可以通过下面的代码看一下:
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
Father() {
cout << "construct Father" << endl;
}
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : public Father {
public:
SonA() {
cout << "contruct SonA" << endl;
}
string gender = "male";
};
class SonD : public Father {
public:
SonD() {
cout << "contruct SonB" << endl;
}
};
class Grandson : public SonA , public SonD {
public:
Grandson() {
cout << "construct GrandSon" << endl;
}
};
在构造GrandSon时会调用分别调用两次Father的构造函数:
int main(void) {
Grandson* grandson = new Grandson;
//grandson->getName();
return 0;
}
结果:
construct Father
contruct SonA
construct Father
contruct SonB
construct GrandSon
在调用getName方式时,编译器会提示:
image.png
这就是所谓的钻石继承。也可以显式的指明调用的是哪个函数:
int main(void) {
Grandson* grandson = new Grandson;
grandson->SonA::getName();
return 0;
}
结果:
construct Father
contruct SonA
construct Father
contruct SonB
construct GrandSon
father's name is Tom
但这样其实还有一个问题,我们只需要一个Father,但是现在却有了两个:
sizeof(*grandson) = 232
这种问题可以通过虚继承来避免:
using namespace std;
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
Father() {
cout << "construct Father" << endl;
}
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : virtual public Father {
public:
SonA() {
cout << "contruct SonA" << endl;
}
string gender = "male";
};
class SonD : virtual public Father {
public:
SonD() {
cout << "contruct SonB" << endl;
}
};
class Grandson : public SonA , public SonD {
public:
Grandson() {
cout << "construct GrandSon" << endl;
}
};
看一下效果:
using namespace std;
int main(void) {
Grandson* grandson = new Grandson;
cout << sizeof(* grandson) << endl;
grandson->getName();
return 0;
}
结果:
construct Father
contruct SonA
contruct SonB
construct GrandSon
144
father's name is Tom
编译器不再困惑,对象占用的空间也少了。减到了 144.
注意一点,虚继承要在继承Father时添加,如果另GrandSon虚继承SonA,SonD的话就没有效果了,反而会因为引入虚指针增加对象大小。
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
Father() {
cout << "construct Father" << endl;
}
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : public Father {
public:
SonA() {
cout << "contruct SonA" << endl;
}
string gender = "male";
};
class SonD : public Father {
public:
SonD() {
cout << "contruct SonB" << endl;
}
};
class Grandson : virtual public SonA , virtual public SonD {
public:
Grandson() {
cout << "construct GrandSon" << endl;
}
};
看一下效果:
int main(void) {
Grandson* grandson = new Grandson;
cout << "sizeof grandson is " << sizeof(* grandson) << endl;
return 0;
}
结果:
construct Father
contruct SonA
construct Father
contruct SonB
construct GrandSon
sizeof grandson is 240
可以看到size 反而增加了,分析一下:
从结果上看:被虚继承的类,在发生多重继承时,只会被实例化一次。
在发生虚继承时,子类实例中会有一个虚指针指向虚表,虚表中存着一个地址或者是一个偏移量,发生了多重继承时,不同的对象指向的父类地址是一个。
虚表的内存布局
上面说过,对象的第一个位置有一个虚指针指向该类的虚表,在发生多重继承时,其实是有多个指针的:
class A{
virtual void play() {
}
};
class B{
virtual void plays() {
}
};
class C: public A, public B {
};
sizeof(C) = 16;
说明有两个虚指针指向两个虚表,虚指针并非指向表头,而是直接指到了虚函数的位置。虚表的内存布局是什么样的呢:
- 首先是top_offset:非多重继承时都为0,在多重继承时为第二个被继承的偏移值。
- 其次是子类的type_info。
- 之后是虚函数。
网友评论