0
点赞
收藏
分享

微信扫一扫

深剖 C++ 继承——十分钟手撕C++


目录

  • ​​传统艺能😎​​
  • ​​概念🤔​​
  • ​​定义🤔​​
  • ​​区别🤔​​
  • ​​继承关系🤔​​
  • ​​对象赋值转换🤔​​
  • ​​重定义(隐藏)🤔​​
  • ​​派生类默认构造函数🤔​​
  • ​​友元与静态成员🤔​​
  • ​​菱形继承🤔​​
  • ​​虚拟继承🤔​​
  • ​​原理🤔​​

传统艺能😎


🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,打码路上一路向北,背后烟火,彼岸之前皆是疾苦
一个人的单打独斗不如一群人的砥砺前行
这是我和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)

🎉🎉🎉倾力打造转码社区微信公众号🎉🎉🎉

深剖 C++ 继承——十分钟手撕C++_父类

深剖 C++ 继承——十分钟手撕C++_派生类_02

概念🤔

继承允许我们依据另一个类来定义一个类,它是面向对象程序中最重要的一个概念,当创建了一个类时,不需要重新编写新的成员和成员函数,只需指定新建的类继承一个已有类的成员即可,这样也有功能复用和提高效率的效果。这个已有的类称为基类或者父类,新建的类称为派生类或者子类

定义🤔

继承定义的格式为:

class B:public A
{
……
};

这里的 B 就是子类对象,A 就是父类对象,比如:

class student
{
public:
void Print()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
protected:
string name = "xx";
int age = 20;
};

class me :public student
{
protected:
int score;//学分
};
int main()
{
me m;
m.Print();
}

子类 me 复用父类 student 的成员,继承后 student 的成员或者成员函数会成为子类的一部分,基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected ,可以看出保护成员限定符是因继承才出现的。

区别🤔

继承和组合不同,继承代表了 is-a 关系,比如 she is a girl ,这里 she 派生类就和 girl 基类构成了继承关系,而组合代表了 has-a 关系,比如 she has a boyfriend ,这里 boyfriend 和 she 就是组合关系。

深剖 C++ 继承——十分钟手撕C++_c++_03

继承关系🤔

不同的访问限定符支持不同的继承方式,因此继承方式就分为了三种:public 继承,protected 继承和 private 继承,因此继承成员也会因为继承方式而有所决策:

深剖 C++ 继承——十分钟手撕C++_子类_04

其实很好记,只需要记住私有类都不可见,而依据访问限定符的权限划分, public > protected > private,两两结合时会遵循“ 弱者优先原则 ”,即以权限低的为标准执行。

假设我们把基类成员的访问限定修饰符改为private,然后在子类内或者在子类外去访问这些 private 成员,再编译代码结果必然报错,但是实际上 private 类中的成员是实打实的被继承下来了,但是由于权限原因不能被访问。基类private成员在派生类中无论以什么方式继承都是不可见的,这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

我们在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用后者,因为他们的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

在具体情况中还要注意下面这些点(敲黑板~):

  1. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
  2. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类后清理基类的先后顺序。
  3. 派生类对象初始化先调用基类构造再调派生类构造
  4. 派生类对象析构清理先调用派生类析构再调基类的析构 (其实构造和析构的调用特点可看作是一个栈的操作)

对象赋值转换🤔

派生类对象可以进行赋值操作,赋值给基类的对象 ,指针或者引用;一般叫他切片或者切割,顾名思义就是将派生类中对象切来赋值。

= p;//派生类直接赋值
Student* ptr = &p;//派生类的地址赋给基类指针
Student& quote = p;//派生类引用赋给基类引用

深剖 C++ 继承——十分钟手撕C++_c++_05

注意,我们说的将派生类引用赋给基类引用并不是类型转换,是语法支持的行为,为什么呢?

以例子来说:

int main()
{
int a = 1;
double b = 1.11;
a = b;
int& ret = b;
return 0;
}

深剖 C++ 继承——十分钟手撕C++_c++_06

当执行这个代码时就会报以上错误,我们类型转换的中间过程实际上是生成了一个中间的临时变量,临时变量属于应该被 const 修饰的常属性变量,但是我们在继承中赋值时并没有使用 const 关键字,所以其本质上就不属于类型转换,属于语法的支持,当然值得注意的是只有 public 才能切割,而 private 和 protected 不能被切割!!

深剖 C++ 继承——十分钟手撕C++_c++_07

派生类可以赋值给基类,但是该过程并不可逆,也就是说基类不可以赋值给派生类,原因很简单,派生类完全继承自基类,包含了基类的成员和成员函数以及自己的成员和成员函数,==此时基类类型对于子类类型来说是不完全的!==正所谓强扭的瓜不甜所以强转也是不通过的。

虽然不能强转赋值,但是可以间接强转,我们对指针进行强转可达到目的,基类的指针可以通过强制类型转换赋值给派生类指针,但是为了保证安全必须要基类指针指向派生类指针。此方法存在越界访问的问题,所以谨慎使用。

int main()
{
student s;
me m;
me* ptr = (student*)&p;
}

( 没有强转是会报错哒!)

重定义(隐藏)🤔

一个类就是一个域,所以继承中基类和派生类拥有独立的作用域,当父类和子类中有同名的成员时,就会发生重定义机制,我们也称之为隐藏,即子类成员将屏蔽父类对该成员的直接访问,默认就近原则会直接访问子类的该成员,若需要父类访问,需要指定域名基类:基类对象的显示访问。

如果是成员函数的重定义,只需要函数名相同即可构成重定义,所以在继承体系中建议最好避免同名定义。

总的来说找变量或者函数遵循就近原则,即先局部,再全局,对于如下代码:

int a = 2;
int main()
{
int a = 1;
cout << a << endl;//打印1
cout << ::a << endl;//打印2
return 0;
}

就从全局作用域和局部作用域的两次定义说明了作用域的机制。但是注意,隐藏机制确实存在于子类和父类之间,但是如果父类对象调用成员函数是不会发生隐藏机制的,也就是说父类就是老大哥,隐藏只能限制子类!

派生类默认构造函数🤔

子类会自动调用父类的构造函数去初始化父类继承下来的成员,自动调用父类的析构函数去释放,但对于子类自身的成员呢?

该成员无非就是内置类型和自定义类型两种,对于子类成员如果是内置类型,则不做处理,如果是自义定类型就要调用自义定类型的构造函数,不仅构造和析构是如此,拷贝和重载也是一样的。

一句话就是继承下来调用父类去处理,自己的按照普通类基本规则去处理。但是什么情况下又需要自己写默认成员函数呢?先看下面这个例子:

class Person
{
public:
Person(const char* name)//这里不给默认构造函数
:_name(name)
{
cout << "Person" << endl;
}
~Person()
{
cout << "~Person" << endl;
}
protected:
string name = "吉良吉影";
int age = 33;

};

class Student :public Person
{
protected:
int score;//学分
string s;
};

int main()
{
Student stu;
}

就这段代码来讲是会报错的

深剖 C++ 继承——十分钟手撕C++_c++_08


原因很简单,父类没有默认构造函数,而继承父类的子类 student 中并没有构造函数,编译器会自动生成一个,然后调用父类的默认构造函数,而此时父类并没有默认构造函数,子类就无法进行初始化报错。

注意三种情况需要我们自己写构造函数:

  1. 父类没有默认构造
  2. 子类需要及时释放,需要自己写析构函数
  3. 子类存在浅拷贝问题时,就需要写构造函数和赋值来进行深拷贝

友元与静态成员🤔

友元类不能被继承,也就是说基类友元不能访问子类私有和保护成员。也就相当于你兄弟的爹不是你的爹。

静态成员不参与继承不被包括在子类中,基类定义的 static 成员,会贯穿整个代码在整个继承体系中只有一份,他作为全局变量在继承时都可以访问,相同的值相同的空间相同的地址,看似每次都被继承了但是实际上并没有参与其中,因为静态变量属于整个类而不属于任何对象。

菱形继承🤔

我们一直谈论的是单继承,即一个子类只有一个直接父类的继承关系,多继承就是一个子类有两个或两个以上的父类

深剖 C++ 继承——十分钟手撕C++_派生类_09

菱形继承是多继承下的复杂模型,因为 C++ 语法下继承语法本身的不完美性,在复杂化语法时带来了不可避免的缺陷:数据冗余和二义性

深剖 C++ 继承——十分钟手撕C++_开发语言_10


二义性:

当 A,B 两个父类都被继承到类 C 中,当访问类中的成员时,这样就会引发二义性无法明确知道访问的是哪一个的成员,要解决二义性问题需要显示指定访问哪一个父亲的成员,但是这样依然无法解决数据冗余问题。

C information;
information.name = “xxx”;
information.A::name = "dio";
information.B::name = "jojo";

数据冗余:

class test1
{
public:
int a;
};
class test2:public test1
{};
class test3 :public test1
{};
class test4 :public test2,public test3
{};
}

上面菱形继承模型中如果 test1 中成员不是 int ,而是 int [1000000] , 我们跳出这个模型其实 test4 的根本目的就是继承 test1 的 1000000 个数据,但是走完菱形继承会继承两份也就是 2000000 个数据,从而造成数据冗余问题,这样极大造成了空间上的浪费。

虚拟继承🤔

为了解决数据冗余和二义性,C++ 采用虚拟继承来解决它们。虚拟继承的原理是使用 virtual 关键字,只需要在菱形继承的中间层,也就是上图的 B,C 类,在它们继承方式前面加上 virtual 即可:

using  namespace std;
class test1
{
public:
string _s;
};
class test2 :virtual public test1
{
protected:
int num;
};
class test3 :virtual public test1
{
protected:
int score;
};
class test4 :public test2, public test3
{
protected:
string information;
};


int main()
{
test4 Ex;
Ex._s = "jojo";
cout << Ex._s << endl;
cout << sizeof(Ex) << endl;
}

深剖 C++ 继承——十分钟手撕C++_父类_11

如图访问 _s 时就不需要指定作用域了,test4 创建的对象 Ex 所占空间的大小也减少了一半,但是注虚拟继承不要在其他地方去使用

原理🤔

要探究底层原理不妨调用内存窗口看看是怎样的结构,我们以简化的模型来探究一下:

class A {
public:
int _a;
};

class B : public A {
public:
int _b;
};

class C : public A {
public:
int _c;
};

class D : public B, public C{
public:
int _d;
};

int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}

class D:public B,public C:先继承B再继承C

深剖 C++ 继承——十分钟手撕C++_开发语言_12

class D:public C,public B:先继承C再继承B

深剖 C++ 继承——十分钟手撕C++_开发语言_13

如上在创建子类时,也会遵循先后关系进行继承的空间开辟,先继承的会先在内存的低地址处开辟空间,后继承的会在内存的高地址处开辟空间

==那虚拟继承呢?==我们稍作修改:

:virtual public A {
public:
int _b;
};

class C :virtual public A {
public:
int _c;
};

依然采用内存窗口观察:

深剖 C++ 继承——十分钟手撕C++_c++_14


我们看到这个多出来的东东其实是存的偏移量,这个偏移量什么意思?难道是一个地址吗?这个很好验证我们再次代入内存中查找:

深剖 C++ 继承——十分钟手撕C++_子类_15


得到了一个 14 ,这个 14 又是个什么鬼?这就要牵扯到一个东西叫虚基表,我们 A 一般叫虚基类(额外的空间,不占用实例对象的空间),它是通过 B 和 C 的两个指针指向的一张表,这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量,通过偏移量可以找到下面的 A。

就拿这里为例,0x006FF7C0 到 0x006FF7D4 的绝对值之差是 20 字节,以内存的 16 进制转换过来就是 14,所以这个偏移量表达的内容就是相对于 _a 的位置!那么问题来了,为什么不直接访问 _a 而是去搞一个麻烦的偏移量呢?

其实这是为了方便基类赋值给派生类时准确的找到 _a 的位置,因为发生切片时我们是无从得知此时的 _a 在表中什么位置,因此引入偏移量是为了方便的获取准确的位置。

可参考偏移量的原理图:

深剖 C++ 继承——十分钟手撕C++_开发语言_16


到此为止就总结完毕,很多人说继承和多态很难,其实他并不难,只是他的体系比较抽象,概念性的东西多看几遍其义自见~ aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们。


举报

相关推荐

0 条评论