在我的早期印象中,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++常用特性原理解析的更多相关文章

  1. [原][Docker]特性与原理解析

    Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...

  2. 3D游戏常用技巧Normal Mapping (法线贴图)原理解析——高级篇

    1.概述 上一篇博客,3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇,讲了法线贴图的基本概念和使用方法.而法线贴图和一般的纹理贴图一样,都需要进行压缩,也需要生成mipm ...

  3. JavaScript 模板引擎实现原理解析

    1.入门实例 首先我们来看一个简单模板: <script type="template" id="template"> <h2> < ...

  4. 超详细的Guava RateLimiter限流原理解析

    超详细的Guava RateLimiter限流原理解析  mp.weixin.qq.com 点击上方“方志朋”,选择“置顶或者星标” 你的关注意义重大! 限流是保护高并发系统的三把利器之一,另外两个是 ...

  5. Tengine HTTPS原理解析、实践与调试【转】

    本文邀请阿里云CDN HTTPS技术专家金九,分享Tengine的一些HTTPS实践经验.内容主要有四个方面:HTTPS趋势.HTTPS基础.HTTPS实践.HTTPS调试. 一.HTTPS趋势 这一 ...

  6. 基于OpenCV进行图像拼接原理解析和编码实现(提纲 代码和具体内容在课件中)

    一.背景 1.1概念定义 我们这里想要实现的图像拼接,既不是如题图1和2这样的"图片艺术拼接",也不是如图3这样的"显示拼接",而是实现类似"BaiD ...

  7. Spring IOC设计原理解析:本文乃学习整理参考而来

    Spring IOC设计原理解析:本文乃学习整理参考而来 一. 什么是Ioc/DI? 二. Spring IOC体系结构 (1) BeanFactory (2) BeanDefinition 三. I ...

  8. ulua、tolua原理解析

    在聊ulua.tolua之前,我们先来看看Unity热更新相关知识. 什么是热更新 举例来说: 游戏上线后,玩家下载第一个版本(70M左右或者更大),在运营的过程中,如果需要更换UI显示,或者修改游戏 ...

  9. RocketMQ架构原理解析(四):消息生产端(Producer)

    RocketMQ架构原理解析(一):整体架构 RocketMQ架构原理解析(二):消息存储(CommitLog) RocketMQ架构原理解析(三):消息索引(ConsumeQueue & I ...

随机推荐

  1. oracle遇到的锁异常,oralce record is locked by another user

    由于我在前不久的一次项目调试的时候,将一条数据的ID与另一条数据的ID相同了,但不知为什么没有报错,当在页面发现问题时,删除这条数据时就报错了,oralce record is locked by a ...

  2. pdfbox加载pdf时遇到wrappedioexception报错处理方式

    现在一个项目要对pdf做处理.由于其中一个pdf约为80M左右,用pdfbox读取pdf时遇到了wrappedioexception错误.监控得到说内存不足.于是请教项目经理.他告诉我在Open De ...

  3. AM335x kernel 4.4.12 i2c eeprom AT24c02驱动移植

    kernel 4.4.12 i2c eeprom AT24c02驱动移植 在kernel make menuconfig ARCH=ARM 中打开: Device Drivers ---> Mi ...

  4. TBitmapSurface.StretchFrom

    procedure TBitmapSurface.StretchFrom(const Source: TBitmapSurface; const NewWidth, NewHeight: Intege ...

  5. BZOJ 4668: 冷战

    Description 在一个图上,在两个点间连一条边,问这两个点最早在什么时候联通. Sol 并查集+启发式合并. 按秩合并的并查集...我也不知道什么是按秩合并,反正就跟启发式合并差不多,合并的时 ...

  6. JS验证只能输入数字,数字和字母等的正则表达式

    JS判断只能是数字和小数点 0.不能输入中文1)<input onpaste="return false;" type="text" name=" ...

  7. css3 自定义字体的使用方法

    @font-face是CSS3中的一个模块,他主要是把自己定义的Web字体嵌入到你的网页中,随着@font-face模块的出现,我们在Web的开发中使用字体不怕只能使用Web安全字体,你们当中或许有许 ...

  8. powershell通过wps excel导出csv

    powershell比较强大,比较好用,比较方便. $et=New-Object -ComObject et.application #$et.Visible=$true $et.DisplayAle ...

  9. java 对象序列化与反序列化

    Java序列化与反序列化是什么? 为什么需要序列化与反序列化? 如何实现Java序列化与反序列化? 本文围绕这些问题进行了探讨. 1.Java序列化与反序列化  Java序列化是指把Java对象转换为 ...

  10. tp5 中 model 的修改器

    修改器可以在数据赋值的时候自动进行转换处理 class User extends Model { public function setNameAttr($value){ return strtolo ...