深度解读《深度探索C++对象模型》之拷贝构造函数(二)

news2024/9/28 19:26:22

目录

 含有虚函数的情形

继承链上有virtual base class的情形

抑制合成拷贝构造函数的情况

总结


接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎左下角点击关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。

        上一篇请从这里阅读:深度解读《深度探索C++对象模型》之拷贝构造函数(一) 

含有虚函数的情形

        从前面的文章中我们知道,当一个类定义了一个或以上的虚函数时,或者继承链上的父类中有定义了虚函数的话,那么编译器就会为他们生成虚函数表,并会扩充类对象的内存布局,在类对象的起始位置插入虚函数表指针,以指向虚函数表。这个虚函数表指针很重要,如果没有设置好正确的值,那么将引起调用虚函数的混乱甚至引起程序的崩溃。编译器往类对象插入虚函数表指针将导致这个类不再具有逐成员拷贝的语意,当程序中没有显式定义拷贝构造函数时,编译器需要为它自动生成一个拷贝构造函数,以便在适当的时机设置好这个虚函数表指针的值。我们以下面的例子来分析一下:

#include <stdio.h>

class Base {
public:
    virtual void virtual_func() {
        printf("virtual function in Base class\n");
    }
private:
    int b;
};

class Object: public Base {
public:
    virtual void virtual_func() {
         printf("virtual function in Object class\n");
    }
private:
    int num;
};

void Foo(Base& obj) {
    obj.virtual_func();
}

int main() {
    Object a;
    Object a1 = a;
    Base b = a;
    Foo(a);
    Foo(b);
    
    return 0;
}

        看下生成的汇编代码,节选main函数部分:

main:									# @main
    push    rbp
    mov     rbp, rsp
    sub     rsp, 64
    mov     dword ptr [rbp - 4], 0
    lea     rdi, [rbp - 24]
    call    Object::Object() [base object constructor]
    lea     rdi, [rbp - 40]
    lea     rsi, [rbp - 24]
    call    Object::Object(Object const&) [base object constructor]
    lea     rdi, [rbp - 56]
    lea     rsi, [rbp - 24]
    call    Base::Base(Base const&) [base object constructor]
    lea     rdi, [rbp - 24]
    call    Foo(Base&)
    lea     rdi, [rbp - 56]
    call    Foo(Base&)
    xor     eax, eax
    add     rsp, 64
    pop     rbp
    ret

        上面汇编代码中的第10行对应C++代码中的第27行,这里调用的是Object类的拷贝构造函数,汇编代码中的第13行对应C++代码中的第28行,这里调用的是Base类的拷贝构造函数,这说明了编译器为Object类和Base类都生成了拷贝构造函数。继续分析这两个类的拷贝构造函数的汇编代码:

Object::Object(Object const&) [base object constructor]:	# @Object::Object(Object const&) [base object constructor]
    push    rbp
    mov     rbp, rsp
    sub     rsp, 32
    mov     qword ptr [rbp - 8], rdi
    mov     qword ptr [rbp - 16], rsi
    mov     rdi, qword ptr [rbp - 8]
    mov     qword ptr [rbp - 24], rdi       # 8-byte Spill
    mov     rsi, qword ptr [rbp - 16]
    call    Base::Base(Base const&) [base object constructor]
    mov     rax, qword ptr [rbp - 24]       # 8-byte Reload
    lea     rcx, [rip + vtable for Object]
    add     rcx, 16
    mov     qword ptr [rax], rcx
    mov     rcx, qword ptr [rbp - 16]
    mov     ecx, dword ptr [rcx + 12]
    mov     dword ptr [rax + 12], ecx
    add     rsp, 32
    pop     rbp
    ret
Base::Base(Base const&) [base object constructor]:	# @Base::Base(Base const&) [base object constructor]
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     qword ptr [rbp - 16], rsi
    mov     rax, qword ptr [rbp - 8]
    lea     rcx, [rip + vtable for Base]
    add     rcx, 16
    mov     qword ptr [rax], rcx
    mov     rcx, qword ptr [rbp - 16]
    mov     ecx, dword ptr [rcx + 8]
    mov     dword ptr [rax + 8], ecx
    pop     rbp
    ret

        在Object类的拷贝构造函数里,上面汇编代码的第10行,调用了Base类的拷贝构造函数,这里的意思是先构造Base子类部分,在Base类的拷贝构造函数里,上面汇编代码的第27行到29行,在这里设置了Base类的虚函数表指针,因为这里构造的是Base子类的对象,所以这里设置的是Base类的虚函数表指针。然后返回到Object类的拷贝构造函数,在上面汇编代码的第12行到第14行,这里又重新设置回Object类的虚函数表指针,因为构造完Base子类之后继续构造Object类,需要重设回Object类的虚函数表指针,Base类和Object类的虚函数表是不同的两个表,所以需要为它们对应的对象设置对应的虚函数表指针。

        其实同一类型的对象的赋值是可以采用逐成员拷贝的方式来完成的,比如像Object a1 = a;这行代码,因为它们的虚函数表是同一个,直接拷贝对象a的虚函数表指针给a1对象没有任何问题。但是问题出在于使用派生类的对象给父类的对象赋值时,这里会发生切割,把派生类对象中的父类子对象部分拷贝给父类对象,如果没有编译器扩充的部分(这里是虚函数表指针),只是拷贝数据部分是没有问题的,但是如果把派生类的虚函数表指针赋值给父类子对象,这将导致虚函数调用的混乱,本该调用父类的虚函数的,却调用了派生类的虚函数。所以编译器需要重设这个虚函数表指针的值,也就是说这里不能采用逐成员拷贝的手法了,当程序中没有显式地定义拷贝构造函数时编译器就会生成一个,或者在已有的拷贝构造函数中插入代码,来完成重设虚函数表指针这个工作。

        再看下C++代码中的这三行代码:

Base b = a;
Foo(a);
Foo(b);

        第一行的赋值语句,虽然是使用派生类Object的对象a作为初值,但是调用的却是Base类的拷贝构造函数(见main函数的汇编代码第13行),因为b的类型是Base类。这就保证了只使用了对象a中的Base子对象部分的内容,以及确保设置的虚函数表指针是指向Base类的虚函数表,这样在调用Foo函数时,分别使用对象a和b作为参数,尽管Foo函数的形参使用的是“Base&”,是使用基类的引用类型,但却不会引起调用上的混乱。第二个调用使用b作为参数,它是Base类的对象,调用的是Base类的虚函数,这两行的输出结果是:

virtual function in Object class
virtual function in Base class

继承链上有virtual base class的情形

        当一个类的继承链上有一个virtual base class时,virtual base class子对象的布局会重排,内存布局的分析可以参考另一篇文章“深度解读《深度探索C++对象模型》之C++对象的内存布局(一)”、“深度解读《深度探索C++对象模型》之C++对象的内存布局(二)”。为使得能支持虚继承的机制,编译器运行时需要知道虚基类的成员位置,所以编译器会在编译时生成一个虚表,这个表格里会记录成员的相对位置,在构造对象时会插入一个指针指向这个表。这使得类失去了逐成员拷贝的语意,如果一个类对象的初始化是以另一个相同类型的对象为初值,那么逐成员拷贝是没有问题的,问题在于如果是以派生类的对象赋值给基类的对象,这时候会发生切割,编译器需要计算好成员的相对位置,以避免访问出现错误,所以编译器需要生成拷贝构造函数来做这样的事情。以下面的代码为例:

#include <stdio.h>

class Grand {
public:
    int g = 1;
};

class Base1: virtual public Grand {
    int b1 = 2;
};

class Base2: virtual public Grand {
    int b2 = 3;
};

class Derived: public Base1, public Base2 {
    int d = 4;
};

int main() {
    Derived d;
    Base2* pb2 = &d;
    d.g = 11;
    pb2->g = 10;
    Base2 b2 = *pb2;
    
    return 0;
}

        第25行的代码是将派生类Derived类的对象赋值给Base2父类对象,这将会发生切割,将Derived类中的Base2子对象部分拷贝过去,看下对应的汇编代码:

# 节选部分main函数汇编
mov     rsi, qword ptr [rbp - 56]
lea     rdi, [rbp - 72]
call    Base2::Base2(Base2 const&) [complete object constructor]

        [rbp - 56]存放的是C++代码里的pb2的值,也就是对象d的地址,存放在rsi寄存器中,[rbp - 72]是对象b2的地址,存放到rdi寄存器中,然后将rsi和rdi寄存器作为参数传递给Base2的拷贝构造函数,然后调用它。继续看下Base2的拷贝构造函数的汇编代码:

Base2::Base2(Base2 const&) [complete object constructor]:	# @Base2::Base2(Base2 const&) [complete object constructor]
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     qword ptr [rbp - 16], rsi
    mov     rax, qword ptr [rbp - 8]
    mov     rcx, qword ptr [rbp - 16]
    mov     rdx, qword ptr [rcx]
    mov     rdx, qword ptr [rdx - 24]
    mov     ecx, dword ptr [rcx + rdx]
    mov     dword ptr [rax + 12], ecx
    lea     rcx, [rip + vtable for Base2]
    add     rcx, 24
    mov     qword ptr [rax], rcx
    mov     rcx, qword ptr [rbp - 16]
    mov     ecx, dword ptr [rcx + 8]
    mov     dword ptr [rax + 8], ecx
    pop     rbp
    ret

        首先将两个参数(分别存放在rdi和rsi寄存器)拷贝到栈空间[rbp - 8]和[rbp - 16]中,第8到11行代码就是将对象d中的Grand子对象的成员拷贝到b2对象中,对象的前8个字节在构造对象的时候已经设置好了虚表的指针,这里将指针指向的内容存放到rdx寄存器中,第9行取得虚基类成员的偏移地址然后存放在rdx寄存器,第10行将对象的首地址加上偏移地址,取得虚基类的成员然后拷贝到ecx寄存器,在第11行代码里拷贝给[rax + 12],即b2对象的起始地址加上12字节的偏移量(8字节的虚表指针加上成员变量b2占4字节),即完成对Grand类中的成员变量g的拷贝。

        所以对于有虚基类的情况,将一个派生类的对象赋值给基类对象时,不能采取逐成员拷贝的手法,需要借助虚表来计算出虚基类的成员的相对位置,以获得正确的成员地址,需要生成拷贝构造函数来完成。

抑制合成拷贝构造函数的情况

        C++11标准之后新增了delete关键字,它可以指定不允许编译器生成哪些函数,比如我们不允许拷贝一个类对象,那么可以将此类的拷贝构造函数声明为=delete的。例如标准库中的iostream类,它不允许拷贝,防止两个对象同时指向同一块缓存。如果一个类的定义中有一个类类型成员,而此成员的拷贝构造函数声明为=delete的,或者类的父类中声明了拷贝构造函数为=delete的,那么这个类的拷贝构造函数也会被编译器声明为delete的,这个类的对象将不允许被拷贝,如以下的代码:

class Base {
public:
    Base() = default;
    Base(const Base& rhs) = delete;
};

class Object {
    Base b;
};

int main() {
    Object d;
    Object d1 = d;	// 此行编译错误
    
    return 0;
}

        上面代码的第13行会引起编译错误,原因就是Object类没有拷贝构造函数,不允许赋值的操作,同样地,拷贝赋值运算符也将被声明为delete的。

总结

  • 拷贝赋值运算符的情况和拷贝构造函数的情况类似,可以采用上述的方法来分析。
  • 当不需要涉及到资源的分配和释放时,不需要显示地定义拷贝构造函数,编译器会为我们做好逐成员拷贝的工作,效率比去调用一个拷贝构造函数要更高效一些。
  • 当你需要为程序定义一个析构函数时,那么肯定也需要定义拷贝构造函数和拷贝赋值运算符,因为当你需要在析构函数中去释放资源的时候,说明在拷贝对象的时候需要为新对象申请新的资源,以避免两个对象同时指向同一块资源。
  • 当你需要为程序定义拷贝构造函数时,那么也同时需要定义拷贝赋值运算符,反之亦然,但是却并不一定需要定义析构函数,比如在构造对象时为此对象生成一个UUID,这时在析构对象时并不需要释放资源。

本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1663068.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

android进阶-Binder

参考&#xff1a;Android——Binder机制-CSDN博客 机制&#xff1a;Binder是一种进程间通信的机制 驱动&#xff1a;Binder是一个虚拟物理设备驱动 应用层&#xff1a;Binder是一个能发起进程间通信的JAVA类 Binder相对于传统的Socket方式&#xff0c;更加高效Binder数据拷贝…

销售第一天拿下7400万 《我独自升级》在全球范围炸响

易采游戏网5月11日消息&#xff0c;近日一款名为《我独自升级》的韩式二次元游戏在全球范围内引发了热烈的反响。据悉&#xff0c;该游戏在上线首日便实现了惊人的收入&#xff0c;达到了7400万人民币&#xff0c;这一数字不仅远超预期&#xff0c;更是有史以来同类型游戏中最高…

【C++阅览室】C++之Vector(容器)

目录 vector的介绍 vector的使用 vector的定义 vector iterator 的使用 vector 空间增长问题 vector 增删查改 vector 迭代器失效问题。&#xff08;重点&#xff09; vector的介绍 1、 vector 是表示可变大小数组的序列容器&#xff0c;可以使用连…

C——单链表

一.前言 我们在前面已经了解了链表中的双向链表&#xff0c;而我们在介绍链表分类的时候就说过常用的链表只有两种——双向带头循环链表和单向不带头不循环链表。下来我来介绍另一种常用的链表——单向不带头不循环链表也叫做单链表。不清楚链表分类的以及不了解双向链表的可以…

数仓开发流程规范

一、目的 数据研发规范化旨在为数据开发提供规范化的研发流程指导方法&#xff0c;目的是简化、规范化日常工作流程&#xff0c;提高工作效率&#xff0c;较少无效与冗余工作&#xff0c;赋能企业更强大的数据掌控力来应对海量增长的业务数据&#xff0c;从而释放更多的人力与…

keil的jlink重新选择芯片识别

keil选择jlink要选择对应芯片&#xff0c;一旦选择成功会出现以下文件 如果选择错了芯片类型&#xff0c;就需要删除这两个文件&#xff0c;然后重新进入选择&#xff0c;就可以了

神经网络与空间变换关系

神经网络的隐藏层实际上就是在进行一次空间变换&#xff0c;隐藏层中神经元的个数就是变换后空间的维度&#xff0c;代表可以升维也可以降维。 不同是 神经网络的一层运算不只有矩阵乘法&#xff0c;还会有一个加法。以及 进行完线性计算后&#xff0c;还要经过非线性的激活函…

泰迪智能科技企业数据挖掘流程分析及特色服务优势

企业发展会沉淀大量的数据&#xff0c;数据中囊括了企业业务各种维度指标&#xff0c;通过数据挖掘和数据分析 &#xff0c;让企业业务了解过去、现在和未来将要发生什么&#xff0c;从而更好的调整企业发展方向。泰迪智能科技企业数据挖掘平台是面向企业级用户快速处理数据构建…

微信小程序之简单的发送弹幕操作

大家看视频的时候是不是时不时会有弹幕飘过~ 在我们微信小程序当中&#xff0c;我们可以十分简单的实现&#xff0c;接下来为大家介绍一下吧&#xff01; 我们使用微信官方给我们的一个视频链接 "http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey302802…

语言基础 /CC++ 可变参函数设计与实践,va_ 系列实战详解(强制参数和变参数的参数类型陷阱)

文章目录 概述va_ 系列定义va_list 类型va_start 宏从变参函数的强制参数谈起宏 va_start 对 char 和 short 类型编译告警宏 va_start 源码分析猜测 __va_start 函数实现 va_arg 宏宏 va_arg 无法接受 char 和 short为啥va_arg可解析int却不能解析float类型&#xff1f;宏 va_a…

Linux 第二十七章

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C&#xff0c;linux &#x1f525;座右铭&#xff1a;“不要等到什么都没有了…

答辩PPT不会做?试试这些AI工具,一键生成

在我原本的认知里面&#xff0c;答辩PPT是要包含论文各个章节的&#xff0c;在答辩时需要方方面面都讲到的&#xff0c;什么摘要、文献综述、实证分析、研究结果样样不落。但是&#xff0c;这大错特错&#xff01; 答辩PPT环节时长一般不超过5分钟&#xff0c;老师想要的答辩P…

【JavaSE】/*初识Java*/

目录 一、了解 Java 语言 二、Java 语言的重要性 2.1 使用程度 2.2 工作领域 三、Java 语言的特性 四、Java 的基础语法 五、可能遇到的错误 六、第一个 java 程序代码解析 七、Java 注释 八、Java 标识符 九、Java 关键字 一、了解 Java 语言 Java 是由 Sun Micr…

2023年建筑特种作业人员安全生产知识试题

100分题库提供安全员考试试题、建筑安全员考试预测题、建筑安全员ABC考试真题、安全员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 判断题&#xff08;1-20&#xff09; 1.《建筑工程安全生产管理条例》是我国第一部关于…

mac苹果电脑卡顿反应慢如何解决?2024最新免费方法教程

苹果电脑以其稳定的性能、出色的设计和高效的操作系统&#xff0c;赢得了广大用户的喜爱。然而&#xff0c;随着时间的推移&#xff0c;一些用户会发现自己的苹果电脑开始出现卡顿、反应慢等问题。这不仅影响使用体验&#xff0c;还会影响工作效率。那么&#xff0c;面对这些问…

2024年旅游行业薪酬报告

来源&#xff1a;薪智 近期历史回顾&#xff1a; 2024年中国健康家电消费洞察及趋势研究报告.pdf 2024巴菲特股东大会5万字完整版.pdf 2024年全国大学生新媒体直播大赛.pdf 2024北京市高级别自动驾驶示范区数据安全治理白皮书.pdf 2024年第一季度开发者健康调查报告.pdf 2024年…

计算机毕业设计 | vue+springboot线上考试 在线测试系统(附源码)

1&#xff0c;项目介绍 项目背景 在线考试借助于网络来进行&#xff0c;传统考试所必备的考场和监考对于在线考试来说并不是必要项目&#xff0c;因此可以有效减少组织考试做需要的成本以及设施。同时&#xff0c;由于在线考试系统本身具有智能阅卷的功能&#xff0c;也大大减…

记录文件上传exists方法遇到的坑

1、问题 判断文件是否存在使用exist方法&#xff0c;官方的注释是这样的 百度翻译结果&#xff1a;true&#xff0c;当且仅当由该抽象路径名表示的文件或目录存在时&#xff1b;否则为false 2、实际返回 注意&#xff1a;实际上exsits方法的返回值与其官方注释的返回结果是相…

NSSCTF中的web学习(md5())

目录 MD5的学习 [BJDCTF 2020]easy_md5 [LitCTF 2023]Follow me and hack me [LitCTF 2023]Ping [SWPUCTF 2021 新生赛]easyupload3.0 [NSSCTF 2022 Spring Recruit]babyphp MD5的学习 md5()函数&#xff1a; md5($a)&#xff1a;返回a字符串的散列值 md5($a,TRUE)&…

一套全新的PACS医学存档影像系统源码 RIS和PACS系统分别在哪些方面发挥作用

RIS和PACS系统分别在哪些方面发挥作用 RIS系统的作用 放射信息系统&#xff08;RIS&#xff09;主要用于管理和调度患者的放射检查流程。它的主要功能包括患者管理、检查预约、报告生成等。RIS系统通常作为独立系统运行&#xff0c;侧重于临床流程管理&#xff0c;并优化放射…