C++常用特性原理解析

在我的早期印象中,C++这门语言是软件工程发展过程中,出于对面向对象语言级支持不可或缺的情况下,一群曾经信誓旦旦想要用C统治宇宙的极客们妥协出来的一个高性能怪咖。

它驳杂万分,但引人入胜,出于多(mian)种(shi)原因,我把它拿出来进行一次重新的学习。
这篇笔记从G++编译出的汇编代码出发,对部分C++的常用面向对象特性进行原理性解释和总结,其中包括 引用类(成员函数,构造函数)多态(编译时,运行时)模板与泛型

Here we go!


引用

这是一个老生常谈的话题了,C++ primer中文译本上说引用是对象的一个别名,别名是什么鬼?
上码:

int invoke(int a) {return ++a;
}int main(int argc, char **argv) {int a = 123;             //       movl   $123,-20(%rbp)int *pa = &a;            //       leaq    -20(%rbp),%rax//       movq    %rax,-16(%rbp)int &ra = a;             //       leaq    -20(%rbp),%rax//       movq    %rax,-8(%rbp)invoke(a);               //       movl    -20(%rbp),%eax//       movl    %eax,%edi//       call    _Z6invokeiinvoke(*pa);             //       movq    -16(%rbp),%rax//       movl    (%rax),%eax//       movl    %eax,%edi//       call    _Z6invokeiinvoke(ra);              //       movq    -8(%rbp),%rax//       movl    (%rax),%eax//       movl    %eax,%edi//       call    _Z6invokei
}

简单明了,pa是一个指向a的指针,ra是一个a的引用,可以看到编译器对pa和ra的的定义以及参数传递做的工作几乎是一模一样,它们都在栈里有自己的空间且都存了一个a的地址,因此可以十分肯定的说引用是用指针实现的。
引用是对指针的一个语言级别的封装,其出现的意义大概是为了提升程序的可读性,通常都是用来进行参数传递。
关于引用的好处和使用技巧,有待进一步学习。//TODO

类(成员函数,构造函数)

贴代码之前,有必要回顾一下标号这个概念,在汇编语言里,每条指令的前面都可以拥有一个标号,以代表和指示该指令地址的汇编地址,因为毕竟由我们自己来计算和跟踪每条指令所在的汇编地址是极其困难的。

在汇编翻译成机器码的过程中,这些标号会被转换成标号所在行的具体偏移地址,多数情况下用来标记指令块入口地址,就是进行所谓函数的跳转。忘记的同学可以先行度娘。

接下来的代码,会在每个函数后的注释中标出该函数编译后的标号名。

int invoke(int a) {                   //        _Z6invokeireturn ++a;
}class Animal {
public:int age;int weight;Animal(): age(0), weight(0.0) {}    //        _ZN6AnimalC2Evvoid run() { }                      //        _ZN6Animal3runEv
};class Human {
public:Human() {}                          //        _ZN5HumanC2Ev
};int main(int argc, char **argv) {Animal cat;                         //        leaq  -16(%rbp), %rax//        movq  %rax, %rdi//        call  _ZN6AnimalC1Evcat.age = 5;                        //        movl  $5, -16(%rbp) cat.weight = 2;                     //        movl  $2, -12(%rbp)cat.run();                          //        leaq  -16(%rbp), %rax//        movq  %rax, %rdi//        call  _ZN6Animal3runEv
}

相比上一个例子,这波代码里,增加了一个Animal类和一个Human类。

我们从main函数开始

  • 对象初始化
    首先语句Animal cat;初始化了一个Animal的对象cat,从右边的汇编代码可以看到,cat作为一个复合类型被存入新扩展的栈帧的第16个字节的偏移处-16(%rbp),然后将cat的地址存入rdi,显而易见,这就是C++在调用类的成员函数时传递的隐式参数this指针,接着跳转到标号名为_ZN6AnimalC1Ev的地方继续执行,在Animal类里可以看到,对应该标号名的函数就是Animal类的构造函数。

  • 类成员赋值
    这没什么好谈的,跟C里结构体成员的赋值一样。

  • 成员函数调用
    对成员函数run()的调用,编译器的处理方式与对构造函数的调用一模一样。

对比G++编译过程中对不同的函数的标号命名:
Animal 类
普通函数: invoke() _Z6invokei
普通成员函数:run() _ZN6Animal3runEv
构造函数: Animal() _ZN6AnimalC2Ev
Human 类:
构造函数: Human() _ZN5HumanC2Ev

在语法层面上,C++规定了不同函数的定义和调用方式,编译器会对不同函数使用不同的处理方式,比如调用成员函数会隐式传递this指针,比如直接调用成员函数会导致编译出错,在成功编译后,所有函数都不外乎是以一个特定标号标志的指令序列。
从标号的命名上可以看出C++确保其唯一的方式。

因此,狭义上讲,所谓类,其实就是一个复合类型,所谓成员函数,其实就是一个默认会传递调用对象本身指针的普通函数,所谓构造函数,其实就是一个在对象初始化的时候会自动调用的普通函数,这些额外的特性都是在编译阶段实现的。

多态(编译时,运行时)

  • 重载
    从汇编的角度看,重载的多个函数也不过是对应多个不同的标号名而已:
class Animal {
public:void run() {}                     //      _ZN6Animal3runEv  void run(int a) {}                //      _ZN6Animal3runEivoid run(char b) {}               //      _ZN6Animal3runEcvoid run(int a, Human p) {}       //      _ZN6Animal3runEi5Human
};

G++正是通过重载的多个函数的不同形参列表来对标号进行唯一的命名,也是所谓的编译时多态。

  • 继承
    简单的继承是很容易实现的,第一点,编译器在分配空间的时候会分配子类自有成员变量和其父类成员变量的总大小,第二点,编译时会在子类构造函数的中调用父类的构造函数。
    这里就不给例子了,主要篇幅放在下面的运行时多态上。

  • 运行时多态

class Animal {
public:virtual void run() {}              //     _ZN6Animal3runEv
};class Cat : public Animal {
public:void run() {}                      //     _ZN3Cat3runEv
};int main(int argc, char **argv) {Animal *tom = new Cat();           //     _ZN3CatC2Ev://         _ZN6AnimalC2Ev://         movq  $_ZTV6Animal+16, (%rax)//     movq  $_ZTV3Cat+16, (%rax)tom.run();                         //     movq  %rbx, -24(%rbp)//     movq  -24(%rbp), %rax //     movq  (%rax), %rax //     movq  (%rax), %rax//     movq  -24(%rbp), %rdx//     movq  %rdx, %rdi//     call  *%rax
}

这里,我们把new Cat()要调用的2个构造函数按照执行顺序进行选择性展开,可以看到两条关键的汇编代码,其中(%rax)表示tom对象在堆中的起始位置,于是,唯一有效的最后一条代码movq $_ZTV3Cat+16, (%rax)将Cat类的_虚函数表_指针存入了cat对象的起始位置。

再看tom.run()的汇编,追踪发现,最后一条代码call *%rax正好调用了Cat类的虚函数表的第一个函数。

这就是所谓的运行时多态的调用逻辑,为什么说是所谓的呢?因为这个逻辑在编译的时候就可以实现了,有些聪明的编译器会在你将tom指针指向Cat对象的时候就确定了tom到底对哪个run进行调用,它会将tom.run()直接优化编译成call _ZN3Cat3runEv

那么,什么样的运行时多态是在编译阶段做不了的呢?看下面代码:

int main(int argc, char **argv) {Animal *tom;if (argc == 0)  tom = new Animal();else tom = new Cat();tom->run();
}

这时,编译tom->run()的时候是不可能知道该调哪个run的,所以,根据上一段代码我们展开的构造函数可以知道,在运行时,哪一个构造函数被调用,tom所指向的对象里就存了哪个类的虚函数表指针,这才是真正意义上的运行时多态。

模板与泛型

class Cat {};
class Mouse {}; template <typename T>
class Cave {
public:void capture(T& a) {}; 
};int main(int argc, char **argv) {Cat tom;Mouse jerry;Cave<Cat> catsCave;                    catsCave.capture(tom);               //       call  _ZN4CaveI3CatE7captureERS0_       Cave<Mouse> miceCave;miceCave.capture(jerry);             //       call  _ZN4CaveI5MouseE7captureERS0_
}

有了之前对函数和标号的认识,理解模板与泛型的实现就是信手拈来了。
编译器会识别一个模板类有几种指定了不同类型的声明,然后会为每一种类型生成对应的唯一的函数标号和不同的函数实现。
就这个简单的例子来说,编译器会为抓猫的笼子和抓老鼠的笼子编译出不同捕捉函数。

传统的实现方式是为不同的笼子声明不同的类和函数,这所产生的汇编代码与使用模板与泛型产生的汇编代码在功能上是一模一样的,甚至在代码细节上都是差不多的,不同的只是标号名罢了。

模板与泛型在语言级别上提供了这种简便且扩展性极佳的编程方式,这种设计思维是C++所推荐的。


希望这写篇笔记能够为C++初学者提供些许指引,同时为我即将开始的求职之路提供一些帮助。

附上《C++程序设计语言》上的一句话:C++是一个可以伴随你成长的语言。

欢迎批评和讨论。

转载于:https://www.cnblogs.com/JaSonS-toy/p/5347759.html

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

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

相关文章

容器created状态_docker容器状态的转换实现

一 docker容器状态转换图二 实战[rootlocalhost ~]# docker infoContainers: 0Running: 0Paused: 0Stopped: 0Images: 3Server Version: 17.09.0-ceStorage Driver: overlayBacking Filesystem: xfsSupports d_type: falseLogging Driver: json-fileCgroup Driver: cgroupfsPlu…

nodejs命令行执行程序_在NodeJS中编写命令行应用程序

nodejs命令行执行程序by Peter Benjamin彼得本杰明(Peter Benjamin) 在NodeJS中编写命令行应用程序 (Writing Command-Line Applications in NodeJS) With the right packages, writing command-line apps in NodeJS is a breeze.有了合适的软件包&#xff0c;用NodeJS编写命令…

python re findall 效率_python re模块findall()详解

今天写代码&#xff0c;在写到郑泽的时候遇到了一个坑&#xff0c;这个坑是re模块下的findall()函数。下面我将结合代码&#xff0c;记录一下importrestring"abcdefg acbdgef abcdgfe cadbgfe"#带括号与不带括号的区别#不带括号regexre.compile("((\w)\s\w)&quo…

ubuntu16.04配置sonarqube+MySQL

环境&#xff1a;rootubuntu:~# uname -a Linux ubuntu 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux rootubuntu:~# rootubuntu:~# cat /etc/issue Ubuntu 16.04 LTS \n \lrootubuntu:~#安装配置mysql&#xff1a;1、更新源…

mysql 多表混全_mysql--浅谈多表查询1

这是对自己学习燕十八老师mysql教程的总结&#xff0c;非常感谢燕十八老师。依赖软件&#xff1a;mysql5.6系统环境&#xff1a;win连接查询在谈连接查询之前我们需要对数学上的笛卡尔积有一定的了解现在有两个集合m和nm (m1,m2,.....mx)n (n1,n2,.....ny)m*n得到的笛卡尔积有…

鼠标固定在屏幕中间_无线电竞黑科技,雷柏VT950Q游戏鼠标评测

雷柏作为目前小有声誉的PC外设品牌&#xff0c;其定位高性能游戏领域的VT系列产品&#xff0c;想必大家也比较熟悉了。VT系列的产品除了有超强的性能以及出色的设计感&#xff0c;同时还都是性价比非常高的产品&#xff0c;即便是采用了旗舰级传感器&#xff0c;定位最为高端的…

谈论源码_5,000名开发人员谈论他们的薪水

谈论源码Let’s dive into the most interesting results from the O’Reilly 2016 Salary Survey of 5,000 developers (which excluded managers and students).让我们来看看OReilly 2016年薪金调查对5,000名开发人员(其中不包括经理和学生)最有趣的结果。 性别工资差距是真…

WebSnapshotsHelper(HTML转换为图片)

1 /// <summary>2 /// WebBrowser Url生成图片3 /// HTML转图片4 /// </summary>5 public class WebSnapshotsHelper6 {7 Bitmap m_Bitmap;8 string m_Url;9 int m_BrowserWidth, m_BrowserHeight, m_ThumbnailWidth,…

两个多项式相乘求解系数数组算法

题目描述&#xff1a; 给出两个多项式&#xff0c;最高次幂分别为n和m&#xff0c;求解这两个系数相乘得到的系数数组。 分析&#xff1a; 最高次幂如果是m和n&#xff0c;那么他们相乘得到的系数数组的最高次幂一定是nm&#xff0c;对于其他的系数&#xff0c;不妨设a[],b[]是…

synchronized 和 reentrantlock 区别是什么_JUC源码系列之ReentrantLock源码解析

目录ReentrantLock 简介ReentrantLock 使用示例ReentrantLock 与 synchronized 的区别ReentrantLock 实现原理ReentrantLock 源码解析ReentrantLock 简介ReentrantLock 是 JDK 提供的一个可重入的独占锁&#xff0c;独占锁&#xff1a;同一时间只有一个线程可以持有锁可重入&am…

gulp 和npm_为什么我离开Gulp和Grunt去看npm脚本

gulp 和npmI know what you’re thinking. WAT?! Didn’t Gulp just kill Grunt? Why can’t we just be content for a few minutes here in JavaScript land? I hear ya, but…我知道你在想什么 WAT &#xff1f;&#xff01; 古尔普不是杀死了咕unt吗&#xff1f; 为什么…

mysql8.0递归_mysql8.0版本递归查询

1.先在mysql数据库添加数据DROP TABLE IF EXISTS dept;CREATE TABLE dept (id int(11) NOT NULL,pid int(11) DEFAULT NULL,name varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,date datetime(0) DEFAULT NULL,PRIMARY KEY (id) USING BTREE) ENGINE…

js 轮播插件

flexslider pc插件 个人用过flickerplate 移动端插件 个人用过个人觉得比较好的移动端插件swiper http://www.swiper.com.cn/ 用过个人觉得比较好的pc端插件待定

计算机中的字符编码

字符编码 什么是计算机编码 计算机只能处理二进制的数据&#xff0c;其它的数据都要进行转换&#xff0c;但转换必须要有一套字符编码(是字符与二进制的一个对应关系)。常用的字符&#xff1a;a-z、0-9、其它的符号等&#xff0c;计算机也不能直接处理。 &#xff08;字符编码类…

致力微商_致力于自己。 致力于公益组织。

致力微商by freeCodeCamp通过freeCodeCamp 致力于自己。 致力于公益组织。 (Commit to Yourself. Commit to a Nonprofit.) In case you missed it, our October Summit was jam-packed with several big announcements about our open source community.如果您错过了它&#…

应急照明市电检测_应急照明如何供电? 如何接线? 图文分析!

对于大部分刚接触建筑电气设计的工作者来说&#xff0c;应急照明的强启原理一直都是很头疼的问题。由于不知道应急照明的强启原理&#xff0c;所以&#xff0c;应急灯具应该用多少根线&#xff0c;其实也就无从谈起。下面以文字和图片结合的方式来说明应急灯怎么接线的&#xf…

win10网速慢

升级到win10之后发现网速特别慢&#xff0c;搜了下&#xff0c;网上的解决办法果然好使&#xff0c;按照如下操作即可。 返回桌面&#xff0c;按WINR键组合&#xff0c;运行gpedit.msc 打开组策略 依次展开管理模板-》网络-》QoS数据计划程序-》限制可保留宽带&#xff0c;双击…

ubuntu安装nodejs

下载nodejs https://nodejs.org/dist/v4.6.0/node-v4.6.0-linux-x64.tar.gz 解压 tar -zxvf node-v4.6.0-linux-x64.tar.gz 移动到/opt/下 mv node-v4.6.0-linux-x64 /opt/ 创建链接 ln -s /opt/node-v4.6.0-linux-x64/bin/node /usr/local/bin/node 转载于:https://www.cnblog…

android实用代码

Android实用代码七段&#xff08;一&#xff09; 前言 这里积累了一些不常见确又很实用的代码&#xff0c;每收集7条更新一次&#xff0c;希望能对大家有用。 声明 欢迎转载&#xff0c;但请保留文章原始出处:)     博客园&#xff1a;http://www.cnblogs.com 农民伯伯&…

mysql 全文本检索的列_排序数据列以检索MySQL中的最大文本值

为此&#xff0c;您可以将ORDER BY与一些聚合函数right()一起使用。让我们首先创建一个表-create table DemoTable1487-> (-> StudentCode text-> );使用插入命令在表中插入一些记录-insert into DemoTable1487 values(560);insert into DemoTable1487 values(789);in…