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 _Z6invokei
invoke(*pa); // movq -16(%rbp),%rax
// movl (%rax),%eax
// movl %eax,%edi
// call _Z6invokei
invoke(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) { // _Z6invokei
return ++a;
}
class Animal {
public:
int age;
int weight;
Animal(): age(0), weight(0.0) {} // _ZN6AnimalC2Ev
void run() { } // _ZN6Animal3runEv
};
class Human {
public:
Human() {} // _ZN5HumanC2Ev
};
int main(int argc, char **argv) {
Animal cat; // leaq -16(%rbp), %rax
// movq %rax, %rdi
// call _ZN6AnimalC1Ev
cat.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) {} // _ZN6Animal3runEi
void run(char b) {} // _ZN6Animal3runEc
void 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++是一个可以伴随你成长的语言。
欢迎批评和讨论。
C++常用特性原理解析的更多相关文章
- [原][Docker]特性与原理解析
Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...
- 3D游戏常用技巧Normal Mapping (法线贴图)原理解析——高级篇
1.概述 上一篇博客,3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇,讲了法线贴图的基本概念和使用方法.而法线贴图和一般的纹理贴图一样,都需要进行压缩,也需要生成mipm ...
- JavaScript 模板引擎实现原理解析
1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> < ...
- 超详细的Guava RateLimiter限流原理解析
超详细的Guava RateLimiter限流原理解析 mp.weixin.qq.com 点击上方“方志朋”,选择“置顶或者星标” 你的关注意义重大! 限流是保护高并发系统的三把利器之一,另外两个是 ...
- Tengine HTTPS原理解析、实践与调试【转】
本文邀请阿里云CDN HTTPS技术专家金九,分享Tengine的一些HTTPS实践经验.内容主要有四个方面:HTTPS趋势.HTTPS基础.HTTPS实践.HTTPS调试. 一.HTTPS趋势 这一 ...
- 基于OpenCV进行图像拼接原理解析和编码实现(提纲 代码和具体内容在课件中)
一.背景 1.1概念定义 我们这里想要实现的图像拼接,既不是如题图1和2这样的"图片艺术拼接",也不是如图3这样的"显示拼接",而是实现类似"BaiD ...
- Spring IOC设计原理解析:本文乃学习整理参考而来
Spring IOC设计原理解析:本文乃学习整理参考而来 一. 什么是Ioc/DI? 二. Spring IOC体系结构 (1) BeanFactory (2) BeanDefinition 三. I ...
- ulua、tolua原理解析
在聊ulua.tolua之前,我们先来看看Unity热更新相关知识. 什么是热更新 举例来说: 游戏上线后,玩家下载第一个版本(70M左右或者更大),在运营的过程中,如果需要更换UI显示,或者修改游戏 ...
- RocketMQ架构原理解析(四):消息生产端(Producer)
RocketMQ架构原理解析(一):整体架构 RocketMQ架构原理解析(二):消息存储(CommitLog) RocketMQ架构原理解析(三):消息索引(ConsumeQueue & I ...
随机推荐
- 小小C程序(九九乘法表)
用一个简单的嵌套循环实现: #include <stdio.h> int main() { int i,j; ,j=i;i<=&&j<=;) { if (i== ...
- C++程序设计基础
01 1 预编译常用的有,宏定义和包含库.2 库:是实用工具的集和,由程序员编写,可以完成一些特定的功能.3 <> 系统库 ""用户自定义库.4 宏定义:定义符号常量, ...
- Jquery制作--焦点图淡出淡入
之前写了一个焦点图左右轮播的,感觉淡出淡入用得也比较多,就干脆一起放上来啦.这个容器用了百分比宽度,图片始终保持居中处理,定宽或者自适应宽度都是可以的. 兼容到IE6+以上浏览器,有淡出淡入速度和切换 ...
- [Python] from scipy import sparse 报 DLL load failed:找不到指定模块错误
依赖vc运行环境.需要安装 vc_redist Py3.5要安装2015版 传送门: https://www.microsoft.com/zh-CN/download/details.aspx?id= ...
- php编译 :virtual memory exhausted: Cannot allocate memory
有时候用vps建站时需要通过编译的方式来安装主机控制面板.对于大内存的VPS来说一般问题不大,但是对于小内存,比如512MB内存的VPS来说,很有可能会出现问题,因为编译过程是一个内存消耗较大的动作. ...
- 跟着百度学PHP[6]超级全局变量
超级全局变量在PHP 4.1.0之后被启用, 是PHP系统中自带的变量,在一个脚本的全部作用域中都可用. 参考文献:http://www.runoob.com/php/php-superglobals ...
- (转)实现DataList的分页 新增列
前几天在做网上商城,要展示商品信息(有图片,有文字),DataView虽然可以分页,但它的缺点是不能自定义显示格式.而DataList解决了它的缺点,但DataList本身却不能分页.很是头痛,于是在 ...
- JS验证只能输入数字,数字和字母等的正则表达式
JS判断只能是数字和小数点 0.不能输入中文1)<input onpaste="return false;" type="text" name=" ...
- java中面向对象的一些知识(二)
一. 封装的讲解 什么是封装?为什么要封装?怎么实现封装? 封装的目的是为了提高程序的安全性.封装就是把不想让第三者看的属性,方法隐藏起来. 封装的实现方法是: 1.修改属性的可见性,限制访问. 2. ...
- nginx访问量统计
1.根据访问IP统计UV awk '{print $1}' access.log|sort | uniq -c |wc -l 2.统计访问URL统计PV awk '{print $7}' acces ...