深入理解汇编/C语言
2024-09-22 10:16:43 2 举报
AI智能生成
"深入理解汇编/C语言"是一本关于汇编语言和C语言的权威书籍,它旨在帮助读者深入理解这两种低级和高级编程语言。汇编语言是一种低级语言,它直接操作计算机的硬件,而C语言是一种高级语言,它更接近人类理解的自然语言。通过这本书,读者可以学习到汇编语言的基本语法和指令,以及C语言的数据类型、控制结构、函数和指针等核心内容。此外,它还详细介绍了如何将汇编语言和C语言结合起来,以提高程序的执行效率和可移植性。这本书适用于计算机科学、电子工程和软件开发等相关专业的学生和从业者,是一本极具参考价值的工具书。
作者其他创作
大纲/内容
执行流
执行流是什么?
执行流就是一段逻辑上独立的指令区域,对应于代码,大到可以是整个正序文件,即进程,小到可以是小到功能独立的代码块,即函数,而线程本质上就是函数。执行流是独立的,其独立性体现再每个执行流都有自己的占、一套自己的寄存器映像和内存资源,其实这就是执行流的上下文环境。
因此,我们要想构造一个执行流,就要为其提供一整套的资源。
任何代码块,无论大小都可以独立成为执行流,只要在它运行的时候,我们提前准备好它所依赖的上下文环境即可,这里的上下文环境就是它所使用的寄存器映像、栈、内存等资源
1.使用Java、C++写程序,基本单位是类的方法
2.使用C语言写程序,基本单位是函数
3.使用汇编写程序,基本单位就称为执行流(CPU执行引擎执行程序也称为执行流)
执行流就是一段逻辑上独立的指令区域,对应于代码,大到可以是整个正序文件,即进程,小到可以是小到功能独立的代码块,即函数,而线程本质上就是函数。执行流是独立的,其独立性体现再每个执行流都有自己的占、一套自己的寄存器映像和内存资源,其实这就是执行流的上下文环境。
因此,我们要想构造一个执行流,就要为其提供一整套的资源。
任何代码块,无论大小都可以独立成为执行流,只要在它运行的时候,我们提前准备好它所依赖的上下文环境即可,这里的上下文环境就是它所使用的寄存器映像、栈、内存等资源
1.使用Java、C++写程序,基本单位是类的方法
2.使用C语言写程序,基本单位是函数
3.使用汇编写程序,基本单位就称为执行流(CPU执行引擎执行程序也称为执行流)
执行流的作用是什么? 那么,成为独立的执行流有什么用呢?
在任务调度器的眼里,只有执行流才是调度单元,即处理器上运行的,每个任务都是调度器给分配的执行流,只要成为执行流就能够独立在处理器上运行了,也就是说可以分配处理器的时间,处理器会专门分时来处理这个执行流的指令。
在任务调度器的眼里,只有执行流才是调度单元,即处理器上运行的,每个任务都是调度器给分配的执行流,只要成为执行流就能够独立在处理器上运行了,也就是说可以分配处理器的时间,处理器会专门分时来处理这个执行流的指令。
扩展知识:
执行流,通常在计算机科学和软件工程领域中指的是程序执行的顺序和过程。在不同的技术背景下,执行流的概念可以有所不同。下面是几个常见的角度解释执行流的概念:
1.程序执行流
在程序设计中,执行流值得是程序代码从开始到结束的执行顺序.它遵循程序的逻辑结构,如顺序结构、选择结构(条件判断)和循环结构。程序执行流按照代码编写的顺序执行,除非遇到分支语句(如if-else)或循环语句,这会导致控制流转移到其他代码块
2.数据流
在数据处理和数据流编程中,执行流关注的是数据在程序中的移动和变换过程。例如,在流式处理中,数据元素按照一定的顺序依次经过多个处理阶段,每个阶段对数据进行转换或操作
3.并发执行流
在并发编程中,执行流涉及到多个任务或线程的执行顺序。操作系统或执行环境会调度这些任务或线程,使得它们能在单个处理器上以某种方式交错执行,从而提高效率
4.事务执行流
在数据库管理系统中,事务执行流是指事务中各个操作的执行顺序。事务是一些列操作,要么全部成功,要么全部失败,这种执行顺序确保了事务的原子性
5.业务流程执行流
在业务流程管理中,执行流定义了业务流程中各个任务或活动的执行顺序。业务流程执行语言(如BPEL)允许定义复杂的工作流,以自动化商业过程
在上述的每一个场景中,执行流都是确保程序或系统正确、高效运行的关键。开发者必须仔细考虑执行流的逻辑,以确保程序的行为符合预期,尤其在并发或分布式系统中,执行流的控制和管理尤为重要
执行流,通常在计算机科学和软件工程领域中指的是程序执行的顺序和过程。在不同的技术背景下,执行流的概念可以有所不同。下面是几个常见的角度解释执行流的概念:
1.程序执行流
在程序设计中,执行流值得是程序代码从开始到结束的执行顺序.它遵循程序的逻辑结构,如顺序结构、选择结构(条件判断)和循环结构。程序执行流按照代码编写的顺序执行,除非遇到分支语句(如if-else)或循环语句,这会导致控制流转移到其他代码块
2.数据流
在数据处理和数据流编程中,执行流关注的是数据在程序中的移动和变换过程。例如,在流式处理中,数据元素按照一定的顺序依次经过多个处理阶段,每个阶段对数据进行转换或操作
3.并发执行流
在并发编程中,执行流涉及到多个任务或线程的执行顺序。操作系统或执行环境会调度这些任务或线程,使得它们能在单个处理器上以某种方式交错执行,从而提高效率
4.事务执行流
在数据库管理系统中,事务执行流是指事务中各个操作的执行顺序。事务是一些列操作,要么全部成功,要么全部失败,这种执行顺序确保了事务的原子性
5.业务流程执行流
在业务流程管理中,执行流定义了业务流程中各个任务或活动的执行顺序。业务流程执行语言(如BPEL)允许定义复杂的工作流,以自动化商业过程
在上述的每一个场景中,执行流都是确保程序或系统正确、高效运行的关键。开发者必须仔细考虑执行流的逻辑,以确保程序的行为符合预期,尤其在并发或分布式系统中,执行流的控制和管理尤为重要
硬编码
硬编码(Hardcoding)
硬编码是指将具体的数值、路径、参数等直接写入程序代码中,而不通过变量或配置文件来表示。这样的做法使得程序中的这些数值和参数变得固定,不容易修改,且缺乏灵活性。硬编码的值通常被称为"魔法数(Magic Numbers)"或"魔法字符串",因为它们没有直观的含义,只能通过查看代码来了解。
例如,以下是一个硬编码的实例,其中数值10直接出现在代码中
```java
for i in range(10):
print("Iteration", i)
```
硬编码是指将具体的数值、路径、参数等直接写入程序代码中,而不通过变量或配置文件来表示。这样的做法使得程序中的这些数值和参数变得固定,不容易修改,且缺乏灵活性。硬编码的值通常被称为"魔法数(Magic Numbers)"或"魔法字符串",因为它们没有直观的含义,只能通过查看代码来了解。
例如,以下是一个硬编码的实例,其中数值10直接出现在代码中
```java
for i in range(10):
print("Iteration", i)
```
软编码(Softcoding)
软编码是指通过变量、配置文件、参数等方式将具体数值或参数抽象出来,而不直接写入代码。通过软编码,程序变得更加灵活,可以更容易地进行修改和维护,且适应性更强。
使用软编码的例子:
```java
iterations = 10;
for i in range(iterations):
print("Iteration", i);
```
在这个例子中,迭代次数被赋值给变量iterations,使得程序更易读、易维护,并且如果需要更改迭代次数,只需要修改变量的值即可
软编码是指通过变量、配置文件、参数等方式将具体数值或参数抽象出来,而不直接写入代码。通过软编码,程序变得更加灵活,可以更容易地进行修改和维护,且适应性更强。
使用软编码的例子:
```java
iterations = 10;
for i in range(iterations):
print("Iteration", i);
```
在这个例子中,迭代次数被赋值给变量iterations,使得程序更易读、易维护,并且如果需要更改迭代次数,只需要修改变量的值即可
总结
硬编码:将具体数值、参数等直接写入程序代码中,缺乏灵活性,不易修改和维护
软编码:通过变量、配置文件等方式将数值或参数抽象出来,使得程序更具灵活性,易于修改和维护
硬编码:将具体数值、参数等直接写入程序代码中,缺乏灵活性,不易修改和维护
软编码:通过变量、配置文件等方式将数值或参数抽象出来,使得程序更具灵活性,易于修改和维护
汇编语言
汇编语言是什么
汇编语言(Assembly Language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。普遍地说,特定地汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。
许多汇编程序为程序开发、汇编控制、辅助调试提供了额外的支持机制。有的汇编语言编程工具经常会提供宏,它们也被称为宏汇编器。汇编语言不像其他大多数的程序设计语言一样被广泛用于程序设计。在今天的实际应用中,它通常被应用在底层,硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作系统和实时运行程序都需要汇编语言。
Microsoft宏汇编器(称为MNASM)是Windows下常用的汇编器
GAS(GNU汇编器)和NASM是两种基于Linux的汇编器。其中NASM的语法与MASM的最相似。
ARM汇编
汇编语言是最古老的编程语言,在所有的语言中,它与原生机器语言最为接近。它能直接访问计算机硬件,要求用户了解计算机架构和操作系统
MASM能创建哪些类型的程序?
32位保护模式(32-Bit Protected Mode):32位保护模式程序运行于所有的32位和64位版本的Mircrosoft Windows系统。它们通常比实模式更容易编写和理解。从现在开始,将其简称为32位模式。
64位模式(64-Bit Mode):64位程序运行于所有的64位版本Microsoft Windows系统
16位实地址模式(16-Bit Real-Address Mode):16位程序运行于32位版本Windows和嵌入式系统。64位Windows不支持这类程序
汇编语言(Assembly Language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。普遍地说,特定地汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。
许多汇编程序为程序开发、汇编控制、辅助调试提供了额外的支持机制。有的汇编语言编程工具经常会提供宏,它们也被称为宏汇编器。汇编语言不像其他大多数的程序设计语言一样被广泛用于程序设计。在今天的实际应用中,它通常被应用在底层,硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作系统和实时运行程序都需要汇编语言。
Microsoft宏汇编器(称为MNASM)是Windows下常用的汇编器
GAS(GNU汇编器)和NASM是两种基于Linux的汇编器。其中NASM的语法与MASM的最相似。
ARM汇编
汇编语言是最古老的编程语言,在所有的语言中,它与原生机器语言最为接近。它能直接访问计算机硬件,要求用户了解计算机架构和操作系统
MASM能创建哪些类型的程序?
32位保护模式(32-Bit Protected Mode):32位保护模式程序运行于所有的32位和64位版本的Mircrosoft Windows系统。它们通常比实模式更容易编写和理解。从现在开始,将其简称为32位模式。
64位模式(64-Bit Mode):64位程序运行于所有的64位版本Microsoft Windows系统
16位实地址模式(16-Bit Real-Address Mode):16位程序运行于32位版本Windows和嵌入式系统。64位Windows不支持这类程序
什么是汇编器和链接器?
汇编器(assembler)是一种工具程序,用于将汇编语言源程序转换为机器语言。链接器(linkder)也是一种工具程序,它把汇编器生成的单个文件组合为一个可执行程序。还有一个相关的工具,称为调试器(debugger),使得程序员可以在程序运行时,但不执行程序并检查寄存器和内存状态。
编译器分为gcc、g++
调试器分为:gdb、lldb、kgdb,其中gdb和lldb是应用层调试器,kgdb+qemu是Linux内核层调试器,Windows下的内核调试器是windbg
汇编器(assembler)是一种工具程序,用于将汇编语言源程序转换为机器语言。链接器(linkder)也是一种工具程序,它把汇编器生成的单个文件组合为一个可执行程序。还有一个相关的工具,称为调试器(debugger),使得程序员可以在程序运行时,但不执行程序并检查寄存器和内存状态。
编译器分为gcc、g++
调试器分为:gdb、lldb、kgdb,其中gdb和lldb是应用层调试器,kgdb+qemu是Linux内核层调试器,Windows下的内核调试器是windbg
汇编语言与机器语言有关系?
机器语言(machine language)是一种数字语言,专门设计成能被计算机处理器(CPU)理解。所有x86处理器都理解共同的机器语言。
汇编语言(assembly language)包含用短助记符如ADD、MOV、SUB和CALL书写的语句。汇编语言与机器语言是一对一(one-to-one)的关系,每一条汇编语言指令对应一条机器语言指令。寄存器(register)是CPU种被明明的存储为止,用于保存操作的中间结果
C++和Java与汇编语言有什么关系?
高级语言如Python、C++和Java与汇编语言和机器语言的关系是一对多(one-to-many)。比如,C++的一条语句就会扩展为多条汇编指令或机器指令
汇编语言可移植吗?
一种语言,如果它的源程序能够在各种各样的计算机系统种进行编译和运行,那么这种语言被称为是可移植的(portable).例如,一个C++程序,除非需要特别引用某种操作系统的库函数,否则它就几乎可以在任何一台计算机上编译和运行。Java语言的一大特点就是,其编译好的程序几乎能在所有计算机系统种运行。汇编语言不是可移植的,因为它是为特定处理器系列设计的。比如Intel、AMD。目前广泛使用的有多种不同的汇编语言,每一种都基于一个处理器系列。
机器语言(machine language)是一种数字语言,专门设计成能被计算机处理器(CPU)理解。所有x86处理器都理解共同的机器语言。
汇编语言(assembly language)包含用短助记符如ADD、MOV、SUB和CALL书写的语句。汇编语言与机器语言是一对一(one-to-one)的关系,每一条汇编语言指令对应一条机器语言指令。寄存器(register)是CPU种被明明的存储为止,用于保存操作的中间结果
C++和Java与汇编语言有什么关系?
高级语言如Python、C++和Java与汇编语言和机器语言的关系是一对多(one-to-many)。比如,C++的一条语句就会扩展为多条汇编指令或机器指令
汇编语言可移植吗?
一种语言,如果它的源程序能够在各种各样的计算机系统种进行编译和运行,那么这种语言被称为是可移植的(portable).例如,一个C++程序,除非需要特别引用某种操作系统的库函数,否则它就几乎可以在任何一台计算机上编译和运行。Java语言的一大特点就是,其编译好的程序几乎能在所有计算机系统种运行。汇编语言不是可移植的,因为它是为特定处理器系列设计的。比如Intel、AMD。目前广泛使用的有多种不同的汇编语言,每一种都基于一个处理器系列。
什么是机器语言?
机器语言(machine language)是一种指令集的体系。这种指令集称为机器代码(machine code),计算机的CPU或GPU可直接解读的资料。
机器语言是用二进制代码表示的、计算机能直接识别和执行的一种机器指令的集合。它是计算机的设计者通过计算机的硬件结构赋予计算机的操作功能。机器语言具有灵活、直接执行和速度快等特点。不同种类的计算机其机器语言是不兼容的,按照某种计算机的机器指令编制的程序不能在另一种计算机上执行。
要用机器语言编写程序,编程人员需要首先熟记所用计算机的全部指令代码和代码的含义。手编程序时,程序员要自己处理每条指令和每一项数据的存储分配和输入输出,还需记住编程过程中每步锁使用的工作单元处在何种状态。这是一件十分繁琐的工作,编写程序花费的时间往往是实际运行时间的几十倍或几百倍。而且,这样编写出的程序完全是0和1的指令代码,可读性差且容易出错。
1.机器语言是微处理器理解和使用的用于控制它的操作的二进制代码
2.8086到Pentium的机器语言指令长度可以从1字节到13字节
3.尽管机器语言看似非常复杂,但它是有规律的
4.现今存在着超过100 000种机器语言的指令
指令部分示例
1.0000 代表 加载(LOAD)
2.0001 代表 存储(STORE)
寄存器部分示例
1.0000 代表寄存器A
2.0001 代表寄存器 B
存储器部分的示例
1.000000000000 代表地址为0 的存储器
2.000000000001 代表地址为1的存储器
3.000000010000 代表地址为16的存储器
4.100000000000 代表地址为2^11的存储器
集成示例
1.0000, 0000,000000010000 代表 LOAD A,16
2.0000, 0001,000000000001 代表 LOAD B,1
3.0001, 0001,000000010000 代表 STORE B,16
4.0001, 0001,000000000001 代表STORE B,1
机器语言(machine language)是一种指令集的体系。这种指令集称为机器代码(machine code),计算机的CPU或GPU可直接解读的资料。
机器语言是用二进制代码表示的、计算机能直接识别和执行的一种机器指令的集合。它是计算机的设计者通过计算机的硬件结构赋予计算机的操作功能。机器语言具有灵活、直接执行和速度快等特点。不同种类的计算机其机器语言是不兼容的,按照某种计算机的机器指令编制的程序不能在另一种计算机上执行。
要用机器语言编写程序,编程人员需要首先熟记所用计算机的全部指令代码和代码的含义。手编程序时,程序员要自己处理每条指令和每一项数据的存储分配和输入输出,还需记住编程过程中每步锁使用的工作单元处在何种状态。这是一件十分繁琐的工作,编写程序花费的时间往往是实际运行时间的几十倍或几百倍。而且,这样编写出的程序完全是0和1的指令代码,可读性差且容易出错。
1.机器语言是微处理器理解和使用的用于控制它的操作的二进制代码
2.8086到Pentium的机器语言指令长度可以从1字节到13字节
3.尽管机器语言看似非常复杂,但它是有规律的
4.现今存在着超过100 000种机器语言的指令
指令部分示例
1.0000 代表 加载(LOAD)
2.0001 代表 存储(STORE)
寄存器部分示例
1.0000 代表寄存器A
2.0001 代表寄存器 B
存储器部分的示例
1.000000000000 代表地址为0 的存储器
2.000000000001 代表地址为1的存储器
3.000000010000 代表地址为16的存储器
4.100000000000 代表地址为2^11的存储器
集成示例
1.0000, 0000,000000010000 代表 LOAD A,16
2.0000, 0001,000000000001 代表 LOAD B,1
3.0001, 0001,000000010000 代表 STORE B,16
4.0001, 0001,000000000001 代表STORE B,1
汇编语言的发展历程
说到汇编语言的产生,就不得不提到机器语言。机器语言是机器指令的集合。机器指令展开来讲就是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。计算机将之转变为一列高低电平,以使计算机的电子器件收到驱动,进行运算。
上面所说的计算机指的是可以执行机器指令,进行运算的机器。这是早期计算机的概念。在我们常用的PC机种,有一个芯片来完成上面所说的计算机的功能。这个芯片就是我们常说的CPU(Central Processing Unit,中央处理单元)。每一种微处理器,由于硬件设计和内部结构的不同,就需要用不同的电平脉冲来控制,使它工作,所以每一种微处理器都有自己的机器指令集,也就是机器语言。CPU只负责计算,本身不具备职能,你输入一条指令,它就运行一次,然后停下来,等待下一条指令。这些指令都是二进制的,称为操作码(opcode),比如假发指令就是00000011.编译器的作用就是将高级语言写好的程序,翻译成一条条操作码。对于人类来说,二进制程序是不可读的,根本看出不出机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如假发指令00000011携程汇编就是ADD.只要还原成二进制,汇编语言就可以CPU直接执行,所以它是最底层的低级语言。
早期的程序设计均使用机器语言。程序员们将用0、1数字编成的程序代码打在纸带或卡片上,1打孔0不打孔,再将程序通过制袋机或卡片机输入计算机,进行运算。这样的机器语言由纯粹的0和1构成,十分复杂,不方便阅读和修改,也容易产生错误。于是很快就发现了使用机器语言带来的麻烦,它们难于辨别和记忆,给整个产业的发展带来了障碍,于是汇编语言产生了。
汇编语言的主题是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。
```bash
操作寄存器BX的内容送到AX种
1000100111011000 机器指令
mov, ax,bx 汇编指令
```
此后,程序员们就用汇编指令编写源程序,可是,计算机能读懂的只有机器指令,那么如何让计算机执行程序员用汇编指令编写的程序呢?这时就需要有一个能够将汇编指令转换成机器指令的翻译程序,这样的程序我们称为编译器,程序员用汇编语言写出源程序,再用汇编编译器将其编译为机器码,由计算机最终执行
说到汇编语言的产生,就不得不提到机器语言。机器语言是机器指令的集合。机器指令展开来讲就是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。计算机将之转变为一列高低电平,以使计算机的电子器件收到驱动,进行运算。
上面所说的计算机指的是可以执行机器指令,进行运算的机器。这是早期计算机的概念。在我们常用的PC机种,有一个芯片来完成上面所说的计算机的功能。这个芯片就是我们常说的CPU(Central Processing Unit,中央处理单元)。每一种微处理器,由于硬件设计和内部结构的不同,就需要用不同的电平脉冲来控制,使它工作,所以每一种微处理器都有自己的机器指令集,也就是机器语言。CPU只负责计算,本身不具备职能,你输入一条指令,它就运行一次,然后停下来,等待下一条指令。这些指令都是二进制的,称为操作码(opcode),比如假发指令就是00000011.编译器的作用就是将高级语言写好的程序,翻译成一条条操作码。对于人类来说,二进制程序是不可读的,根本看出不出机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如假发指令00000011携程汇编就是ADD.只要还原成二进制,汇编语言就可以CPU直接执行,所以它是最底层的低级语言。
早期的程序设计均使用机器语言。程序员们将用0、1数字编成的程序代码打在纸带或卡片上,1打孔0不打孔,再将程序通过制袋机或卡片机输入计算机,进行运算。这样的机器语言由纯粹的0和1构成,十分复杂,不方便阅读和修改,也容易产生错误。于是很快就发现了使用机器语言带来的麻烦,它们难于辨别和记忆,给整个产业的发展带来了障碍,于是汇编语言产生了。
汇编语言的主题是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。
```bash
操作寄存器BX的内容送到AX种
1000100111011000 机器指令
mov, ax,bx 汇编指令
```
此后,程序员们就用汇编指令编写源程序,可是,计算机能读懂的只有机器指令,那么如何让计算机执行程序员用汇编指令编写的程序呢?这时就需要有一个能够将汇编指令转换成机器指令的翻译程序,这样的程序我们称为编译器,程序员用汇编语言写出源程序,再用汇编编译器将其编译为机器码,由计算机最终执行
汇编语言的优缺点
优点
1.因为用汇编语言设计的程序最终被转换成机器指令,故能够保持机器语言的一致性,直接、简捷,并能够像机器指令一样访问、控制计算机的各种硬件设备,如磁盘、存储器、CPU、IO端口等。使用汇编语言,可以访问所有能够被访问的软、硬件资源
2.目标代码简短,占用内存少,执行速度快,是高效的程序设计语言,经常与高级语言配合使用,以改善程序的执行速度和效率,弥补高级语言在硬件控制方面的不足,应用十分广泛
缺点
1.汇编语言是面向机器的,处于整个计算机语言层次结构的底层,故被视为一种低级语言,通常是为特定的计算机或系列计算机专门设计的。不同的处理器有不同的汇编语言语法和编译器,编译的程序无法在不同的处理器上执行,缺乏可移植性
2.难于从汇编语言代码上理解程序设计意图,可维护性差,即时完成简单的工作也需要大量的汇编语言代码,很容易产生bug,难于调试
3.使用汇编语言必须对某种处理器非常了解,而且只能针对特定的体系结构和处理器进行优化,开发效率很低,周期长且单调
优点
1.因为用汇编语言设计的程序最终被转换成机器指令,故能够保持机器语言的一致性,直接、简捷,并能够像机器指令一样访问、控制计算机的各种硬件设备,如磁盘、存储器、CPU、IO端口等。使用汇编语言,可以访问所有能够被访问的软、硬件资源
2.目标代码简短,占用内存少,执行速度快,是高效的程序设计语言,经常与高级语言配合使用,以改善程序的执行速度和效率,弥补高级语言在硬件控制方面的不足,应用十分广泛
缺点
1.汇编语言是面向机器的,处于整个计算机语言层次结构的底层,故被视为一种低级语言,通常是为特定的计算机或系列计算机专门设计的。不同的处理器有不同的汇编语言语法和编译器,编译的程序无法在不同的处理器上执行,缺乏可移植性
2.难于从汇编语言代码上理解程序设计意图,可维护性差,即时完成简单的工作也需要大量的汇编语言代码,很容易产生bug,难于调试
3.使用汇编语言必须对某种处理器非常了解,而且只能针对特定的体系结构和处理器进行优化,开发效率很低,周期长且单调
C/C++语言
面向过程(OPP)、面向对象(OOP)、面向切面(AOP)
面向过程编程OPP:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注"怎么做",即完成任务的具体细节
面向对象编程OOP: Object Oriented Programming,是一种以对象为基础的编程思想。主要关注"谁来做",即完成任务的对象。
面向切面编程AOP:Aspect Oriented Programming, 基于OOP延申出来的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之前低耦合性的隔离效果
每种编程思想都有各自的优点,它们使用在不同的情况下:面向过程性能很高,面向对象比较易于管理和维护,面向切面使软件变得更加灵活。
新的编程凡是,并不一定完全各方面都优于旧的编程凡是,它们只是在某一特定领域或特殊场景下有着独到的优势。
编程凡是只有适合不适合项目特性,没有绝对的好坏
面向过程编程OPP:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注"怎么做",即完成任务的具体细节
面向对象编程OOP: Object Oriented Programming,是一种以对象为基础的编程思想。主要关注"谁来做",即完成任务的对象。
面向切面编程AOP:Aspect Oriented Programming, 基于OOP延申出来的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之前低耦合性的隔离效果
每种编程思想都有各自的优点,它们使用在不同的情况下:面向过程性能很高,面向对象比较易于管理和维护,面向切面使软件变得更加灵活。
新的编程凡是,并不一定完全各方面都优于旧的编程凡是,它们只是在某一特定领域或特殊场景下有着独到的优势。
编程凡是只有适合不适合项目特性,没有绝对的好坏
OPP和OOP
面向过程是最为实际的一种思考方式,就算是面向对象的方法也是含有面向过程的思想。可以说面向过程是一种基础的方法。它考虑的是实际的实现。一般的过程是从上往下步步求解,所以面向过程最重要的是模块化的思想方法。当程序规模不是很大时,面向过程的方法还会体现出一种又是。因为程序的流程很清楚,按着模块与函数的方法可以很好的组织。
面向对象是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统。类和继承是适应人们一般思维方式的描述范式。方法是允许作用于该类对象上的各种操作。这种对象、类、消息和方法的程序设计范式的基本点在于对象的封装性和类的继承性。通过封装能将对象的定义和对象的实现分开,通过继承能体现类与类之间的关系,以及由此带来的动态联编和实体的多态性
面向过程是最为实际的一种思考方式,就算是面向对象的方法也是含有面向过程的思想。可以说面向过程是一种基础的方法。它考虑的是实际的实现。一般的过程是从上往下步步求解,所以面向过程最重要的是模块化的思想方法。当程序规模不是很大时,面向过程的方法还会体现出一种又是。因为程序的流程很清楚,按着模块与函数的方法可以很好的组织。
面向对象是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统。类和继承是适应人们一般思维方式的描述范式。方法是允许作用于该类对象上的各种操作。这种对象、类、消息和方法的程序设计范式的基本点在于对象的封装性和类的继承性。通过封装能将对象的定义和对象的实现分开,通过继承能体现类与类之间的关系,以及由此带来的动态联编和实体的多态性
举个例子
比如说完成"吃饭"这个任务
如果是人吃肉,则eat(人,肉);
如果是猫吃鱼,则eat(猫,鱼);
eat是人和猫共用的吃饭本能,那如果之后要处理鱼吃虾、奥特曼池小怪兽呢?eat函数中就会存在大量的if-else的判断,这段代码,无疑是很恶心的。
如果是面向对象思想,如何来解决这个问题呢?
我们发现,人、猫、鱼、奥特曼,都有一个"吃"的共性。我么你抽象出每个受体的类,然后继承,这样都具有"吃"的方法。当我们想要执行人吃肉的时候那就人->eat(肉),这样,我们从面向过程维护eat()的焦点,转移到了面向对象维护角色的焦点上来。我们只需要维护好不同的角色(类)就好了,并且人的eat不会影响到猫的eat,猫的eat也不会影响到人的eat
所以,oop思想非常贴近软件工程高内聚的思想:自己管好自己的东西,自己做好自己的事情。
大多数支持面向对象的语言,同时也支持面向过程,不论是JAVA、PHP还是JS,它们都还无法完全面向对象,因为面向过程是必然的,面向过程代表着必要的程序流程,调动对象进行组合或对象内部能力的实现,都一定会存在"过程",它最终还是需要通过拆分步骤来知道最具体的执行细节。在此,我们也能得到一些感悟,许多事情并非完全非黑即白,非OOP就必然是OPP,特别是思想层面的东西,它们呈现出互相结合的形态,从OPP到OOP,这是一个思想进步的过程
比如说完成"吃饭"这个任务
如果是人吃肉,则eat(人,肉);
如果是猫吃鱼,则eat(猫,鱼);
eat是人和猫共用的吃饭本能,那如果之后要处理鱼吃虾、奥特曼池小怪兽呢?eat函数中就会存在大量的if-else的判断,这段代码,无疑是很恶心的。
如果是面向对象思想,如何来解决这个问题呢?
我们发现,人、猫、鱼、奥特曼,都有一个"吃"的共性。我么你抽象出每个受体的类,然后继承,这样都具有"吃"的方法。当我们想要执行人吃肉的时候那就人->eat(肉),这样,我们从面向过程维护eat()的焦点,转移到了面向对象维护角色的焦点上来。我们只需要维护好不同的角色(类)就好了,并且人的eat不会影响到猫的eat,猫的eat也不会影响到人的eat
所以,oop思想非常贴近软件工程高内聚的思想:自己管好自己的东西,自己做好自己的事情。
大多数支持面向对象的语言,同时也支持面向过程,不论是JAVA、PHP还是JS,它们都还无法完全面向对象,因为面向过程是必然的,面向过程代表着必要的程序流程,调动对象进行组合或对象内部能力的实现,都一定会存在"过程",它最终还是需要通过拆分步骤来知道最具体的执行细节。在此,我们也能得到一些感悟,许多事情并非完全非黑即白,非OOP就必然是OPP,特别是思想层面的东西,它们呈现出互相结合的形态,从OPP到OOP,这是一个思想进步的过程
AOP
面向切面编程主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。那么,AOP如何体现?
这里可以联想一下laravel的中间件、javaweb的拦截器、vue的Decorator...它们都是AOP思想的时间。装饰器模式、代理模式,它们也是基于AOP思想的设计模式。AOP思想,指导我们通过找到平整切面的形式,插入新的代码,使新插入的代码对切面上下原有流程的伤害降到最低。
面向切面编程主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。那么,AOP如何体现?
这里可以联想一下laravel的中间件、javaweb的拦截器、vue的Decorator...它们都是AOP思想的时间。装饰器模式、代理模式,它们也是基于AOP思想的设计模式。AOP思想,指导我们通过找到平整切面的形式,插入新的代码,使新插入的代码对切面上下原有流程的伤害降到最低。
举个例子
我们拿(PHP的Web框架)laravel中间件做什么?权限、日志、请求过滤、请求频率限制、csrf过滤....
我们知道,中间件对controller的业务逻辑,不会有任何伤害,如果没有这个切面,我们想要记录请求日志,可能需要在每个controller的具体方法中写日志记录的代码,或者调用日志记录的函数、方法。这会使一段记录代码的日志,或调用记录日志的调用语句出现在许多controller中,这与controller原本要关注的逻辑无关,使controller职责不再单一,提高维护成本。当然,我们可能会写一个父类,让许多controller来继承这个父类,然后统一在父类的constructor方法中记录日志,以此来解决耦合问题。
但实际上,这个父类的constructor方法,不正是一个切面吗?它在原有流程中截取了一个切面,在切面中植入代码,以达到承上启下的作用,并且不对上下文产生伤害。从这个例子中,我们也能得出另外一个思考:AOP知道我们寻找切面,但找到合适的切面,也尤为重要,就像上文,父类构造方法的切面和中间件的切面比起来,显然中间件这个切面更利于维护,你可以灵活选择中间件,但你无法灵活选择父类,因为决定你的controller继承什么父类的,不是切面中的代码,而是controller本身处理什么逻辑。
许多项目,OPP、OOP、AOP使同时存在的,它们是编程范式,是一种指导编程的思想,并非不能互相配合
我们拿(PHP的Web框架)laravel中间件做什么?权限、日志、请求过滤、请求频率限制、csrf过滤....
我们知道,中间件对controller的业务逻辑,不会有任何伤害,如果没有这个切面,我们想要记录请求日志,可能需要在每个controller的具体方法中写日志记录的代码,或者调用日志记录的函数、方法。这会使一段记录代码的日志,或调用记录日志的调用语句出现在许多controller中,这与controller原本要关注的逻辑无关,使controller职责不再单一,提高维护成本。当然,我们可能会写一个父类,让许多controller来继承这个父类,然后统一在父类的constructor方法中记录日志,以此来解决耦合问题。
但实际上,这个父类的constructor方法,不正是一个切面吗?它在原有流程中截取了一个切面,在切面中植入代码,以达到承上启下的作用,并且不对上下文产生伤害。从这个例子中,我们也能得出另外一个思考:AOP知道我们寻找切面,但找到合适的切面,也尤为重要,就像上文,父类构造方法的切面和中间件的切面比起来,显然中间件这个切面更利于维护,你可以灵活选择中间件,但你无法灵活选择父类,因为决定你的controller继承什么父类的,不是切面中的代码,而是controller本身处理什么逻辑。
许多项目,OPP、OOP、AOP使同时存在的,它们是编程范式,是一种指导编程的思想,并非不能互相配合
C语言真的不能面向对象吗?
一直依赖,有关于C++、Java、C#等语言的书总喜欢在开篇介绍中拿C语言来比较一番,在承认C语言无可争议的运行效率的同时,也总爱拿C语言不具备面向对象血统的短板说事儿。难道C语言真的不具备面向对象的能力吗?
考虑这个问题之前,首先要明确一点,什么是面向对象,或者说什么是对象?可以认为,对象=属性+行为。这跟那著名的公式"算法+数据结构=程序"没什么本质区别,但是两者的侧重点不同,抽象层次也不同。面向对象强调的是以数据为核心,在程序中复活事物的抽象本质,让它们"智能地"参与问题的求解,而不是传统的以算法或函数过程为核心去解决问题。所以面向对象绝不只是一种技术,更重要的是一种思想,是一种考虑问题和解决问题的思维方式。既然是一种思想,那么它就不是C#程序员或者Java程序员的专属。
通常在评价一种语言是否支持面向对象时,首先要看就是这种语言中是否有类的概念。注意这里说的是"类的概念"(更多的是指意识形态上的类的概念),而不是"类"。如果非要抓住C语言没有class关键字不放,而就此否定C语言面向对象的可能性,那也只能呵呵了。抛开这一点,可以宽泛地说任何能从技术上为面向对象提供遍历的语言都是面向对象的。当然,就目前来说,实现面向对象的技术手段主要还是类。哪些被公认为面向对象的语言正是凭借类这一技术为面向对象提供了强有力的支持。那么类型是什么样的呢?
想想当初C++的类是怎么来的,不就是结构体里面定义几个函数,然后把关键字改成class,再加上访问权限和继承机制吗?实际上再C++里面关键字都可以不用改,直接在struct里面就可以定义函数。虽然C语言没有类,可结构体好说啊,只不过C语言的结构体里面不能直接定义函数。但是单从思想的角度来说,对象就是属性和行为的集合,一切行为都围绕属性组成的数据核心展开,所以函数是否能在结构体中定义并不是必不可少的,虽然着对面向对象确实是一种强大的技术支持。实际上,结构体里面可以定义函数指针,而C语言中对函数指针以及各种其他指指针神入化的运用令人拍案叫绝。
所以无论信或不信,C语言实现面向对象编程是可能的。试想对于C语言这样一种灵活而底层的语言来说,别的高级语言能做到的事儿它有什么理由做不到呢?只是做起来的复杂程度的问题罢了,但巧的是,面向对象这事儿对于C语言来说还真不麻烦。实际上这事儿早就有人做了,只是不明白为什么一直没有这种提法,我也开始思考这个问题了。
当年面向对象的提出,就是因为面向过程的思想无法满足日益膨胀的软件规模所带来的编程效率和维护的问题。也就是说,软件工业界普遍认为面向过程的C语言不适合编写大型软件,但是为何如火如荼的开源领域却基本都采用了C语言,并且出产了包括Linux系统在内的无数优秀的软件?开源领域选用C语言无非是看重其运行效率,但软件规模的问题是怎么解决的?它们给出的大那就是把面向对象的思想运用到C语言中去
一直依赖,有关于C++、Java、C#等语言的书总喜欢在开篇介绍中拿C语言来比较一番,在承认C语言无可争议的运行效率的同时,也总爱拿C语言不具备面向对象血统的短板说事儿。难道C语言真的不具备面向对象的能力吗?
考虑这个问题之前,首先要明确一点,什么是面向对象,或者说什么是对象?可以认为,对象=属性+行为。这跟那著名的公式"算法+数据结构=程序"没什么本质区别,但是两者的侧重点不同,抽象层次也不同。面向对象强调的是以数据为核心,在程序中复活事物的抽象本质,让它们"智能地"参与问题的求解,而不是传统的以算法或函数过程为核心去解决问题。所以面向对象绝不只是一种技术,更重要的是一种思想,是一种考虑问题和解决问题的思维方式。既然是一种思想,那么它就不是C#程序员或者Java程序员的专属。
通常在评价一种语言是否支持面向对象时,首先要看就是这种语言中是否有类的概念。注意这里说的是"类的概念"(更多的是指意识形态上的类的概念),而不是"类"。如果非要抓住C语言没有class关键字不放,而就此否定C语言面向对象的可能性,那也只能呵呵了。抛开这一点,可以宽泛地说任何能从技术上为面向对象提供遍历的语言都是面向对象的。当然,就目前来说,实现面向对象的技术手段主要还是类。哪些被公认为面向对象的语言正是凭借类这一技术为面向对象提供了强有力的支持。那么类型是什么样的呢?
想想当初C++的类是怎么来的,不就是结构体里面定义几个函数,然后把关键字改成class,再加上访问权限和继承机制吗?实际上再C++里面关键字都可以不用改,直接在struct里面就可以定义函数。虽然C语言没有类,可结构体好说啊,只不过C语言的结构体里面不能直接定义函数。但是单从思想的角度来说,对象就是属性和行为的集合,一切行为都围绕属性组成的数据核心展开,所以函数是否能在结构体中定义并不是必不可少的,虽然着对面向对象确实是一种强大的技术支持。实际上,结构体里面可以定义函数指针,而C语言中对函数指针以及各种其他指指针神入化的运用令人拍案叫绝。
所以无论信或不信,C语言实现面向对象编程是可能的。试想对于C语言这样一种灵活而底层的语言来说,别的高级语言能做到的事儿它有什么理由做不到呢?只是做起来的复杂程度的问题罢了,但巧的是,面向对象这事儿对于C语言来说还真不麻烦。实际上这事儿早就有人做了,只是不明白为什么一直没有这种提法,我也开始思考这个问题了。
当年面向对象的提出,就是因为面向过程的思想无法满足日益膨胀的软件规模所带来的编程效率和维护的问题。也就是说,软件工业界普遍认为面向过程的C语言不适合编写大型软件,但是为何如火如荼的开源领域却基本都采用了C语言,并且出产了包括Linux系统在内的无数优秀的软件?开源领域选用C语言无非是看重其运行效率,但软件规模的问题是怎么解决的?它们给出的大那就是把面向对象的思想运用到C语言中去
C++中在结构体中可以定义函数,前提是在g++编译器的支持下
C中结构体中定义函数指针
一个语言的特性是由编译系统与运行系统共同支持的,C++与C语言的区别,其实本质还是g++和gcc编译器的区别,要看编译器做了什么,比如this指针是g++编译器生成汇编才可以使用的
操作系统基本认识
CPU
32Bit:x86汇编(Intel)
64Bit:x64 64位汇编(AMD)
差别:寄存器的数量、写法上的差异(调用约定)
指令集类型
CISC(Complex Instructioin Set Computer,复杂指令集)是一种微处理器指令集架构,每个指令可执行若干低端操作,诸如从存储器读取、存储、和计算操作,全部集于单一指令之中。与之相对的是精简指令集。复杂指令集的特点是指令数目多而复杂,每条指令字长并不相等,电脑必须加以判断,并为此付出了性能的代价。主要应用于PC端
RISC(Reduced Instruction Set Computer,精简指令集)是计算机中央处理器的一种设计模式。这种设计思路可以想象成一家模块化的组装工厂,对指令数目和寻址方式都做了精简,使其实现更容易,指令并行执行程度更好,编译器的效率更高。主要应用于手持设备
汇编风格
Netwide Assembler(简称NASM)是一款集于英特尔x86架构的汇编与反汇编工具,它可以用来编写16位、32位、64位的程序,NASM被认为是Linux平台上最受欢迎的汇编工具之一。它采用的是复杂指令集CISC
Microsoft Macro Assembler(简称MASM),它是微软位x868微处理器家族,所写的一套宏汇编器。它最初是用来发展MS-DOS上面执行的软件,同时,它也是该系统最流行的汇编器。采用的的是复杂指令集CISC
ARM(Advanced RISC Machine)汇编是一种精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计。由于节能的特点,ARM处理器非常适用于移动通信李玲玉,符合其主要设计目标为低成本、高性能、低耗电的特性。另一方面,超级计算机消耗大量电能,ARM同样被视作更高效的选择,其在其他领域上也有很多作为。M1(集于ARM架构)和Arm芯片
虽然汇编庚哥不同,但是底层的硬编码是一样的
32Bit:x86汇编(Intel)
64Bit:x64 64位汇编(AMD)
差别:寄存器的数量、写法上的差异(调用约定)
指令集类型
CISC(Complex Instructioin Set Computer,复杂指令集)是一种微处理器指令集架构,每个指令可执行若干低端操作,诸如从存储器读取、存储、和计算操作,全部集于单一指令之中。与之相对的是精简指令集。复杂指令集的特点是指令数目多而复杂,每条指令字长并不相等,电脑必须加以判断,并为此付出了性能的代价。主要应用于PC端
RISC(Reduced Instruction Set Computer,精简指令集)是计算机中央处理器的一种设计模式。这种设计思路可以想象成一家模块化的组装工厂,对指令数目和寻址方式都做了精简,使其实现更容易,指令并行执行程度更好,编译器的效率更高。主要应用于手持设备
汇编风格
Netwide Assembler(简称NASM)是一款集于英特尔x86架构的汇编与反汇编工具,它可以用来编写16位、32位、64位的程序,NASM被认为是Linux平台上最受欢迎的汇编工具之一。它采用的是复杂指令集CISC
Microsoft Macro Assembler(简称MASM),它是微软位x868微处理器家族,所写的一套宏汇编器。它最初是用来发展MS-DOS上面执行的软件,同时,它也是该系统最流行的汇编器。采用的的是复杂指令集CISC
ARM(Advanced RISC Machine)汇编是一种精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计。由于节能的特点,ARM处理器非常适用于移动通信李玲玉,符合其主要设计目标为低成本、高性能、低耗电的特性。另一方面,超级计算机消耗大量电能,ARM同样被视作更高效的选择,其在其他领域上也有很多作为。M1(集于ARM架构)和Arm芯片
虽然汇编庚哥不同,但是底层的硬编码是一样的
1.深入理解寄存器
CPU有多少寄存器?(一个核一组寄存器)
x86架构中有8个通用寄存器(GPR)、6段寄存器、1个标志寄存器和指令指针。64位的x86有附加的计算器
8个GPR是:
1.累加器寄存器(AX)。用在算术运算.函数返回值存储
2.基址寄存器(BX):作为一个指向数据的指针(在分段模式下,位于段寄存器DS)
3.计数器寄存器(CX):用于移位/循环指令和循环。循环次数、this指针
4.数据寄存器(DX):用在算术运算和IO操作
5.堆栈指针寄存器(SP):用于指向堆栈的顶部。形式上的栈帧
6.栈基址指针寄存器(BP):用于指向堆栈的底部。形式上的栈帧
7.源变址寄存器(SI):在流操作中用作源的一个指针。数据拷贝用
8.目标索引寄存器(DI):用作在流操作中指向目标的指针。 数据拷贝用
7.程序指针寄存器(IP):程序计数器(OS层面)、常量池索引(r13寄存器).这个寄存器只有操作系统能改
8.其他寄存器(r8、r9、r10、r11、r12、r13):函数调用有一种快速调用,前6个参数通过寄存器来传递,但是超过6个参数,则通过堆栈,其中r8存储第5个参数、第6个参数,剩下的r10、r11、r12、r13寄存器则没有限制
用OD可以看到当前系统中的寄存器类型,在64bit系统中,寄存器是以r开头,32bit中,寄存器是以e开头,比如rax和eax的寄存器类型是相同的,它们都作为函数返回值存储来使用
通用指令
pushad:将所有32位通用寄存器压入堆栈
pusha:将所有的16位通用寄存器压入堆栈
pushfd:然后将32位标志寄存器EFLAGS压入堆栈,包含push ebx/pushesi/ push edi
pushf:将所有16位标志寄存器EFLAGS压入堆栈
popad:将所有的32位通用寄存器取出堆栈
popa:将所有的16位通用寄存器取出堆栈
popfd:将32位标志寄存器EFLAGS取出堆栈 包含pop ebx/pop esi/ pop edi
popf:将16位标志寄存器EFLAGS取出堆栈
在JVM中的程序计数器用的是r13寄存器,OS用的EIP寄存器
x86架构中有8个通用寄存器(GPR)、6段寄存器、1个标志寄存器和指令指针。64位的x86有附加的计算器
8个GPR是:
1.累加器寄存器(AX)。用在算术运算.函数返回值存储
2.基址寄存器(BX):作为一个指向数据的指针(在分段模式下,位于段寄存器DS)
3.计数器寄存器(CX):用于移位/循环指令和循环。循环次数、this指针
4.数据寄存器(DX):用在算术运算和IO操作
5.堆栈指针寄存器(SP):用于指向堆栈的顶部。形式上的栈帧
6.栈基址指针寄存器(BP):用于指向堆栈的底部。形式上的栈帧
7.源变址寄存器(SI):在流操作中用作源的一个指针。数据拷贝用
8.目标索引寄存器(DI):用作在流操作中指向目标的指针。 数据拷贝用
7.程序指针寄存器(IP):程序计数器(OS层面)、常量池索引(r13寄存器).这个寄存器只有操作系统能改
8.其他寄存器(r8、r9、r10、r11、r12、r13):函数调用有一种快速调用,前6个参数通过寄存器来传递,但是超过6个参数,则通过堆栈,其中r8存储第5个参数、第6个参数,剩下的r10、r11、r12、r13寄存器则没有限制
用OD可以看到当前系统中的寄存器类型,在64bit系统中,寄存器是以r开头,32bit中,寄存器是以e开头,比如rax和eax的寄存器类型是相同的,它们都作为函数返回值存储来使用
通用指令
pushad:将所有32位通用寄存器压入堆栈
pusha:将所有的16位通用寄存器压入堆栈
pushfd:然后将32位标志寄存器EFLAGS压入堆栈,包含push ebx/pushesi/ push edi
pushf:将所有16位标志寄存器EFLAGS压入堆栈
popad:将所有的32位通用寄存器取出堆栈
popa:将所有的16位通用寄存器取出堆栈
popfd:将32位标志寄存器EFLAGS取出堆栈 包含pop ebx/pop esi/ pop edi
popf:将16位标志寄存器EFLAGS取出堆栈
在JVM中的程序计数器用的是r13寄存器,OS用的EIP寄存器
线程切换
线程->修改的数据的线程->怎么知道
线程切换-> TSS寄存器。会映射到kuser_shared_data
线程->修改的数据的线程->怎么知道
线程切换-> TSS寄存器。会映射到kuser_shared_data
eflags状态寄存器:这是一个很特别的寄存器,没有指令能够直接操作这个寄存器,是CPU根据指令的执行结果,自己操作这个寄存器
1.CF标志位(Carry Flag,进位标志):无符号算术运算的结果太大而目的操作数无法容纳时置位
2.OF标志位(Overflow Flag,溢出标志):有符号算术运算的结果太大或太小而目的操作数无法容纳时置位
3.SF标志位(Sign Flag,符号标志):在算术或逻辑运算的结果为负时置位
4.ZF标志位(Zero Flag, 零标志):在算术或逻辑运算的结果为零时置位
5.AC(Auxviliary Flag, 辅助进位标志):在算术运算导致8位操作数的位3到位4产生进位是置位
6.PF(Parity Flag, 奇偶标志):结果的最低有效字节为1的位的数目位偶数时置位,可用于错误检查
7.DF(Direction Flag, 方向标志): 在串操作指令执行时有关指针寄存器发生调整的方向递减时置位
8.IF(Interrupt Flag, 中断允许标志):CPU可以响应CPU外部的可屏蔽中断发出的中断请求
9.TF(Trap Flag, 陷阱标志):当设置TF=1, CPU处于单步执行指令的方式;当设置TF=0时,CPU正常执行程序.
1.CF标志位(Carry Flag,进位标志):无符号算术运算的结果太大而目的操作数无法容纳时置位
2.OF标志位(Overflow Flag,溢出标志):有符号算术运算的结果太大或太小而目的操作数无法容纳时置位
3.SF标志位(Sign Flag,符号标志):在算术或逻辑运算的结果为负时置位
4.ZF标志位(Zero Flag, 零标志):在算术或逻辑运算的结果为零时置位
5.AC(Auxviliary Flag, 辅助进位标志):在算术运算导致8位操作数的位3到位4产生进位是置位
6.PF(Parity Flag, 奇偶标志):结果的最低有效字节为1的位的数目位偶数时置位,可用于错误检查
7.DF(Direction Flag, 方向标志): 在串操作指令执行时有关指针寄存器发生调整的方向递减时置位
8.IF(Interrupt Flag, 中断允许标志):CPU可以响应CPU外部的可屏蔽中断发出的中断请求
9.TF(Trap Flag, 陷阱标志):当设置TF=1, CPU处于单步执行指令的方式;当设置TF=0时,CPU正常执行程序.
JCC指令
任何语言的底层,循环结构及条件判断,都是集于eflags寄存器+JCC指令实现的。
任何语言的底层,循环结构及条件判断,都是集于eflags寄存器+JCC指令实现的。
条件判断
cmp指令
本质上做减法运算
cmp eax, 0 等价于sub eax, 0
差别是cmp的运算结果只会该eflags寄存器,不会修改eax寄存器的值
通常配合JCC指令使用实现条件跳转
test指令
本质上做与运算
test eax,0 等价于 and eax,0
差别时test的运算结果只会改eflags寄存器,不会修改eax寄存器的值
通常配合JCC指令使用实现条件跳转
JCC指令 有条件跳转
jmp 无条件跳转
call 无条件跳转
cmp指令
本质上做减法运算
cmp eax, 0 等价于sub eax, 0
差别是cmp的运算结果只会该eflags寄存器,不会修改eax寄存器的值
通常配合JCC指令使用实现条件跳转
test指令
本质上做与运算
test eax,0 等价于 and eax,0
差别时test的运算结果只会改eflags寄存器,不会修改eax寄存器的值
通常配合JCC指令使用实现条件跳转
JCC指令 有条件跳转
jmp 无条件跳转
call 无条件跳转
案例
```c
int main()
{
int a = 10;
if (a == 10) {
int b = 10;
}
return 0;
}
```
```basic
8: int a = 10;
00201856 C7 45 F8 0A 00 00 00 mov dword ptr [a],0Ah
9: if (a == 10) {
0020185D 83 7D F8 0A cmp dword ptr [a],0Ah
00201861 75 07 jne __$EncStackInitStart+2Eh (020186Ah)
10: int b = 10;
00201863 C7 45 EC 0A 00 00 00 mov dword ptr [ebp-14h],0Ah
11: }
12:
13: return 0;
0020186A 33 C0 xor eax,eax
```
```c
int main()
{
int a = 10;
if (a == 10) {
int b = 10;
}
return 0;
}
```
```basic
8: int a = 10;
00201856 C7 45 F8 0A 00 00 00 mov dword ptr [a],0Ah
9: if (a == 10) {
0020185D 83 7D F8 0A cmp dword ptr [a],0Ah
00201861 75 07 jne __$EncStackInitStart+2Eh (020186Ah)
10: int b = 10;
00201863 C7 45 EC 0A 00 00 00 mov dword ptr [ebp-14h],0Ah
11: }
12:
13: return 0;
0020186A 33 C0 xor eax,eax
```
2.深入理解函数、堆栈图
一个函数的汇编结构
```c
void add(int) {
int a = 10;
int b = 20;
int sum = a + b;
}
int main()
{
add(10);
return 0;
}
```
void add(int) {
int a = 10;
int b = 20;
int sum = a + b;
}
int main()
{
add(10);
return 0;
}
```
```bash
6: void add(int) {
001F1EC0 push ebp
001F1EC1 mov ebp,esp
001F1EC3 sub esp,0E4h
001F1EC9 push ebx
001F1ECA push esi
001F1ECB push edi
001F1ECC lea edi,[ebp-24h]
001F1ECF mov ecx,9
001F1ED4 mov eax,0CCCCCCCCh
001F1ED9 rep stos dword ptr es:[edi]
001F1EDB mov ecx,1FC068h
001F1EE0 call 001F1320
001F1EE5 nop
7: int a = 10;
001F1EE6 mov dword ptr [ebp-8],0Ah
8: int b = 20;
001F1EED mov dword ptr [ebp-14h],14h
9: int sum = a + b;
001F1EF4 mov eax,dword ptr [ebp-8]
001F1EF7 add eax,dword ptr [ebp-14h]
001F1EFA mov dword ptr [ebp-20h],eax
10: }
001F1EFD pop edi
001F1EFE pop esi
001F1EFF pop ebx
001F1F00 add esp,0E4h
001F1F06 cmp ebp,esp
001F1F08 call 001F1244
001F1F0D mov esp,ebp
001F1F0F pop ebp
001F1F10 ret
```
生成反汇编代码
6: void add(int) {
001F1EC0 push ebp
001F1EC1 mov ebp,esp
001F1EC3 sub esp,0E4h
001F1EC9 push ebx
001F1ECA push esi
001F1ECB push edi
001F1ECC lea edi,[ebp-24h]
001F1ECF mov ecx,9
001F1ED4 mov eax,0CCCCCCCCh
001F1ED9 rep stos dword ptr es:[edi]
001F1EDB mov ecx,1FC068h
001F1EE0 call 001F1320
001F1EE5 nop
7: int a = 10;
001F1EE6 mov dword ptr [ebp-8],0Ah
8: int b = 20;
001F1EED mov dword ptr [ebp-14h],14h
9: int sum = a + b;
001F1EF4 mov eax,dword ptr [ebp-8]
001F1EF7 add eax,dword ptr [ebp-14h]
001F1EFA mov dword ptr [ebp-20h],eax
10: }
001F1EFD pop edi
001F1EFE pop esi
001F1EFF pop ebx
001F1F00 add esp,0E4h
001F1F06 cmp ebp,esp
001F1F08 call 001F1244
001F1F0D mov esp,ebp
001F1F0F pop ebp
001F1F10 ret
```
生成反汇编代码
反汇编代码解释。
在恢复完现场之后,我们还可以看到两条指令,cmp和call指令,有的人可能会好奇,它们做了什么?其实在操作系统之中,恢复完现场之后,操作系统还会检查一下是否正确。
```bash
001F1F06 cmp ebp,esp
001F1F08 call __RTC_CheckEsp (01F1244h)
```
在恢复完现场之后,我们还可以看到两条指令,cmp和call指令,有的人可能会好奇,它们做了什么?其实在操作系统之中,恢复完现场之后,操作系统还会检查一下是否正确。
```bash
001F1F06 cmp ebp,esp
001F1F08 call __RTC_CheckEsp (01F1244h)
```
汇编如何操作内存
1.调用传参数
栈指令
mov
2.局部变量,两种形式
mov
push
3.全局变量
mov dword ptr ds[0E0F008h],64h
指定数据宽度:byte、word、dword、qword
内存地址要加上括号
内存地址都是从大到小用
32bit,栈以4字节为一个单位进行操作
64bit,栈以8字节为一个单位进行操作
1.调用传参数
栈指令
mov
2.局部变量,两种形式
mov
push
3.全局变量
mov dword ptr ds[0E0F008h],64h
指定数据宽度:byte、word、dword、qword
内存地址要加上括号
内存地址都是从大到小用
32bit,栈以4字节为一个单位进行操作
64bit,栈以8字节为一个单位进行操作
pop指令的本质(先取再加)
先减,再从低地址向高地址放值
1.栈顶指针-4(32bit, 64bit)
sub esp ,4
2.数值写入
mov [esp], 10 (把10赋值给esp指向的那块内存)
mov eax, 10 (eax=10)
push指令的本质
1.栈顶指针-4(32bit) -8(64bit) sub esp,4
2.数值写入 mov [esp], 10
内存地址从大到小使用
先减,再从低地址向高地址放值
1.栈顶指针-4(32bit, 64bit)
sub esp ,4
2.数值写入
mov [esp], 10 (把10赋值给esp指向的那块内存)
mov eax, 10 (eax=10)
push指令的本质
1.栈顶指针-4(32bit) -8(64bit) sub esp,4
2.数值写入 mov [esp], 10
内存地址从大到小使用
堆栈图
```c
int add() {
return 10;
}
int main()
{
int a = add();
return 0;
}
```
```c
int add() {
return 10;
}
int main()
{
int a = add();
return 0;
}
```
生成的堆栈图
堆栈指令
汇编中操作堆栈的指令就这么多
push、pop
pushad、popad(操作AX、CX、DX、BX、原始SP、BP、SI及D的顺序复合操作指令I)
call、jmp、goto
ret
mov dword ptr ds[]0e0F008h,64h
其中dword ptr ds 属于自动填充部分
__RTC_CheckEsp检查堆栈操作之后的栈帧指针是否正常
Windows填充都是0xcccc,则是0x000,而0xccc翻译成中文就是烫烫烫
汇编中操作堆栈的指令就这么多
push、pop
pushad、popad(操作AX、CX、DX、BX、原始SP、BP、SI及D的顺序复合操作指令I)
call、jmp、goto
ret
mov dword ptr ds[]0e0F008h,64h
其中dword ptr ds 属于自动填充部分
__RTC_CheckEsp检查堆栈操作之后的栈帧指针是否正常
Windows填充都是0xcccc,则是0x000,而0xccc翻译成中文就是烫烫烫
如何调试JVM的执行流?
在Clion里面用gdb调试器命令行进行调试
1.找到
2.b *0x0007fffed01eb00(这是一个entry point的起始地址)
3.再输入c,就可以跳到hotspot的汇编指令处
在Clion里面用gdb调试器命令行进行调试
1.找到
2.b *0x0007fffed01eb00(这是一个entry point的起始地址)
3.再输入c,就可以跳到hotspot的汇编指令处
3.函数调用约定、内联汇编、裸函数
函数调用约定
类型
__cdecl
__stdcall
__fastcall
调用约定
传参的方式
1.纯粹用栈
2.纯粹用寄存器
3.栈+寄存器
堆平衡
1.内平栈
2.外平栈
编译器生成的代码的两个原则:
1.函数中用到的寄存器,进入方法之前,退出以后,寄存器的状态应该是一样的
2.堆栈需要平衡(内平栈与外平栈)
Windows下全部支持
Ubuntu16仅支持快速调用
类型
__cdecl
__stdcall
__fastcall
调用约定
传参的方式
1.纯粹用栈
2.纯粹用寄存器
3.栈+寄存器
堆平衡
1.内平栈
2.外平栈
编译器生成的代码的两个原则:
1.函数中用到的寄存器,进入方法之前,退出以后,寄存器的状态应该是一样的
2.堆栈需要平衡(内平栈与外平栈)
Windows下全部支持
Ubuntu16仅支持快速调用
__cdecl:
Windows普通的程序 默认的
堆栈平衡方式: 外平栈
C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡
int a= add(10,20);
push 14h
push 0Ah
call 00A51433
add esp 8
__stdcall:
Windows动态链接库 dll文件
堆栈平衡的方式:内平栈
Windows API默认方式,参数从右向左入栈,被调函数负责栈平衡
tmain:
int a = add(10,20);
push 14h
push 0Ah
call 00681438
mov dword ptr[ebp-8]. eax
add:
.....
ret 8 (内平栈)
__fastcall:
与JVM关系紧密,JVM中的call_stub就是一种快速调用
传参方式:寄存器传参,从右向左。参数少的时候,寄存器传参。参数多的时候,寄存器+堆栈
参数阈值是多少
在32bit下,前两个参数用寄存器
int a = add(10,20,30);
push 1Eh
mov edx 14h
mov ecx, 0Ah
call 00461442
mov dword ptr [ebp-8], eax
64bit下是6个阈值
快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX/EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰"__fastcall"; 内平栈
int a= add(10,20);
mov edx 14h
mov ecx, 0Ah
call 0014143D
mov dword ptr [ebp-8], eax
Windows普通的程序 默认的
堆栈平衡方式: 外平栈
C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡
int a= add(10,20);
push 14h
push 0Ah
call 00A51433
add esp 8
__stdcall:
Windows动态链接库 dll文件
堆栈平衡的方式:内平栈
Windows API默认方式,参数从右向左入栈,被调函数负责栈平衡
tmain:
int a = add(10,20);
push 14h
push 0Ah
call 00681438
mov dword ptr[ebp-8]. eax
add:
.....
ret 8 (内平栈)
__fastcall:
与JVM关系紧密,JVM中的call_stub就是一种快速调用
传参方式:寄存器传参,从右向左。参数少的时候,寄存器传参。参数多的时候,寄存器+堆栈
参数阈值是多少
在32bit下,前两个参数用寄存器
int a = add(10,20,30);
push 1Eh
mov edx 14h
mov ecx, 0Ah
call 00461442
mov dword ptr [ebp-8], eax
64bit下是6个阈值
快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX/EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰"__fastcall"; 内平栈
int a= add(10,20);
mov edx 14h
mov ecx, 0Ah
call 0014143D
mov dword ptr [ebp-8], eax
裸函数是什么?模板解释器为什么用硬编码?
在函数顶以前加关键字__deplspec(naked)可以把函数声明为裸函数,裸函数是指什么都不具备的函数,不会自动给你压栈,也不会自动给你返回,什么都需要自己做.
```c++
// 裸函数
void __declspec(naked) YKiSystemCall() {
__asm {
mov eax, 10;
}
}
```
特点如下:
1.只能写内联汇编,不能写C/C++语言代码
2.Windows下独有,Linux不支持
3.内联代码还有一个好处,你写什么汇编,就生成什么,不会生成额外的汇编指令
用户态和内核态切换的时候,需要对称,返回值需要放入寄存器,如果不使用裸函数,生成的代码会乱七八糟的
内联汇编是什么?
内联汇编时指在调用的时候不使用call指令,而是用内联的代码替换这里的函数调用,可以节省这里时间上的效率损失,常见于逻辑简单,需要频繁调用的函数,这里省下的时间开销就很大,在函数定义前加上关键字inline可以把函数声明为内联,当然,逻辑复杂的函数也可以使用inline,但是编译器不一定会听,编译器会综合考虑要不要对这个函数使用内联
编写时风格不一样,NASM和MASM
NASM:(写法比较反人性)
Linux: asm("mov eax, 10") 编译报错
asm("mov $10, %eax")
MASM:
__asm {
mov eax,10
}
不同平台的内联汇编不一样
JVM的模板解释器为什么不用汇编实现?非要用硬编码实现?
因为Linux不支持裸函数
在函数顶以前加关键字__deplspec(naked)可以把函数声明为裸函数,裸函数是指什么都不具备的函数,不会自动给你压栈,也不会自动给你返回,什么都需要自己做.
```c++
// 裸函数
void __declspec(naked) YKiSystemCall() {
__asm {
mov eax, 10;
}
}
```
特点如下:
1.只能写内联汇编,不能写C/C++语言代码
2.Windows下独有,Linux不支持
3.内联代码还有一个好处,你写什么汇编,就生成什么,不会生成额外的汇编指令
用户态和内核态切换的时候,需要对称,返回值需要放入寄存器,如果不使用裸函数,生成的代码会乱七八糟的
内联汇编是什么?
内联汇编时指在调用的时候不使用call指令,而是用内联的代码替换这里的函数调用,可以节省这里时间上的效率损失,常见于逻辑简单,需要频繁调用的函数,这里省下的时间开销就很大,在函数定义前加上关键字inline可以把函数声明为内联,当然,逻辑复杂的函数也可以使用inline,但是编译器不一定会听,编译器会综合考虑要不要对这个函数使用内联
编写时风格不一样,NASM和MASM
NASM:(写法比较反人性)
Linux: asm("mov eax, 10") 编译报错
asm("mov $10, %eax")
MASM:
__asm {
mov eax,10
}
不同平台的内联汇编不一样
JVM的模板解释器为什么不用汇编实现?非要用硬编码实现?
因为Linux不支持裸函数
裸函数的反汇编代码
lock_object中的用法
4.C语言基础
能做什么?gcc网站,godbot.org
1.Web程序都可以,但是生态不如Java广,学了C语言能更好地理解C++
2.开发操作系统 Linux下是纯C
3.内核驱动 C
4.应用层: C++偏多一点
5.底层只能用C
C语言其实什么都可以做,也可以面向对象编程。方法内部定义的局部变量名,在C中代码是有效的,不会写入可执行文件.但是在Java中,局部变量会写入class文件中
1.Web程序都可以,但是生态不如Java广,学了C语言能更好地理解C++
2.开发操作系统 Linux下是纯C
3.内核驱动 C
4.应用层: C++偏多一点
5.底层只能用C
C语言其实什么都可以做,也可以面向对象编程。方法内部定义的局部变量名,在C中代码是有效的,不会写入可执行文件.但是在Java中,局部变量会写入class文件中
gcc、g++、gdb、lldb、kgdb
C语言编译器:gcc
C++编译器: g++
应用层调试器:gdb、lldb
内核层:
Windows: windbg
Linux: kgdb + qemu
C语言编译器:gcc
C++编译器: g++
应用层调试器:gdb、lldb
内核层:
Windows: windbg
Linux: kgdb + qemu
C语言中和Java中的基本数据类型
C
char (1B)
short (2B)
int(4B)
long(4B)
float(4B)
double(8B)
指针
Java
boolean(视情况而定,如果是一个对象,那么占4B,如果是数组则占元素数量个字节)
byte 1B
char 2B(通过short实现)
short 2b
int 4B
float 4
long 8B
double 8B
oop
可以看到在原生语言中是没有boolean类型的,都是自己封装的
C
char (1B)
short (2B)
int(4B)
long(4B)
float(4B)
double(8B)
指针
Java
boolean(视情况而定,如果是一个对象,那么占4B,如果是数组则占元素数量个字节)
byte 1B
char 2B(通过short实现)
short 2b
int 4B
float 4
long 8B
double 8B
oop
可以看到在原生语言中是没有boolean类型的,都是自己封装的
从应用层理解字符串
原生语言是没有祖父穿这个数据结构的,所以如果要想使用字符串,那么就必须要自己封装。
比如Java是用char[]数组或者byte[]数组来实现的,C++中也是用char[]数组实现的,Redis中是用SDS封装的
那么,字符串太多,就会引入字符串常量池,引入常量池,就会引起hashtable、key-value和内存的控制,以及内存淘汰算法,以及常量池去重
字符串存在形式
1.字符数组
2.常量池无法修改
3.字符串类型指针
OS怎么知道字符串的结束呢?需要\0结尾
比如说
char *str = "cover";
str[0]="C";
c005异常 ,这是不能修改的
在一个进程中的常量区,其实也并非不能修改,需要把CPU段页中的PTE标识置位1,使常量区可写,STW就是使用这个特性实现的,触发段异常,安全点生效,修改页属性->可读可写->none触发
test poling_page, %eax。如果不利用这个特性,就需要调用大量的API比如Thread->ParkEvent()->Park();大量API操作,还不一定是原子操作,还会涉及到枷锁,所以说很巧妙。触发段异常之后,HotSpot会在段异常的回调函数中捕获,进而调用park()方法
原生语言是没有祖父穿这个数据结构的,所以如果要想使用字符串,那么就必须要自己封装。
比如Java是用char[]数组或者byte[]数组来实现的,C++中也是用char[]数组实现的,Redis中是用SDS封装的
那么,字符串太多,就会引入字符串常量池,引入常量池,就会引起hashtable、key-value和内存的控制,以及内存淘汰算法,以及常量池去重
字符串存在形式
1.字符数组
2.常量池无法修改
3.字符串类型指针
OS怎么知道字符串的结束呢?需要\0结尾
比如说
char *str = "cover";
str[0]="C";
c005异常 ,这是不能修改的
在一个进程中的常量区,其实也并非不能修改,需要把CPU段页中的PTE标识置位1,使常量区可写,STW就是使用这个特性实现的,触发段异常,安全点生效,修改页属性->可读可写->none触发
test poling_page, %eax。如果不利用这个特性,就需要调用大量的API比如Thread->ParkEvent()->Park();大量API操作,还不一定是原子操作,还会涉及到枷锁,所以说很巧妙。触发段异常之后,HotSpot会在段异常的回调函数中捕获,进而调用park()方法
定义的字符串数组不能被修改,否则会报段异常
5.玩转指针
指针的两种理解
1.特殊数据类型 **
特性于操作内存地址是完全匹配的
指针类型,其实它也是一个容器
2.内存地址
很重要的一句话:你对内存的理解,决定了你用指针的熟练度
1.特殊数据类型 **
特性于操作内存地址是完全匹配的
指针类型,其实它也是一个容器
2.内存地址
很重要的一句话:你对内存的理解,决定了你用指针的熟练度
指针的用法
```c
char* c1 = (char*)10;
int* i1 = (int*)10;
char* c2 = c1 + 1;
int* i2 = i1 + 1;
printf("%d, %d", c2, i2);
```
如何理解这种加法?
1. +1它的意思不是数值+1,而是步长+1(step)
2.步长就是去掉一个*,数据类型的宽度
```c
char* c1 = (char*)10;
int* i1 = (int*)10;
char* c2 = c1 + 1;
int* i2 = i1 + 1;
printf("%d, %d", c2, i2);
```
如何理解这种加法?
1. +1它的意思不是数值+1,而是步长+1(step)
2.步长就是去掉一个*,数据类型的宽度
指针的用法2
指针的宽度:
32bit 4B
64bit 8B
指针的宽度:
32bit 4B
64bit 8B
指针与运算
只支持加减
大多数情况,指针与整型进行运算,指针与指针之间运算也存在,但是极少
只支持加减
大多数情况,指针与整型进行运算,指针与指针之间运算也存在,但是极少
指针的使用案例
```c
int i = 0x11223344;
int* p = &i;
// 想通过p获取33,代码怎么写?
// 写法一
printf("%x\n", *((char*)p + 1));
// 写法二 强转
printf("%X\n", p);
printf("%X\n",((int)p + 1));
printf("%x\n", *(char*)((int)p + 1));
```
指针非常灵活,用好指针的关键是你对自己程序用的内存的边界要清晰。
解引用:
1.取多少字节宽度的数据
2.前面的类型去掉一个*就是它要取的数据宽度
用的内存地址是从大到小用的
```c
int i = 0x11223344;
int* p = &i;
// 想通过p获取33,代码怎么写?
// 写法一
printf("%x\n", *((char*)p + 1));
// 写法二 强转
printf("%X\n", p);
printf("%X\n",((int)p + 1));
printf("%x\n", *(char*)((int)p + 1));
```
指针非常灵活,用好指针的关键是你对自己程序用的内存的边界要清晰。
解引用:
1.取多少字节宽度的数据
2.前面的类型去掉一个*就是它要取的数据宽度
用的内存地址是从大到小用的
6.C语言如何开发
C语言中最重要的就是指针和结构体
我们想构建执行流
1.申请内存(可读可写不可执行的)
改进:不能用普通的申请内存的函数malloc、callloc得用mmap
2.执行流的字节码指令哪里来?
new字节码指令对应的执行流代码怎么来的
获取执行流指令的两种方式
1.写好C代码,然后把编译后生成的代码copy
2.编译原理 硬编码编织
3.第二部拿到的执行写入第一步申请的内存中
4.如何调用
需要有函数名指向它
汇编:jmp+返回地址压栈、call
C语言:函数指针(指针函数 返回指针的函数)
1.声明一个函数指针的类型
2.定义一个函数指针
1.申请内存(可读可写不可执行的)
改进:不能用普通的申请内存的函数malloc、callloc得用mmap
2.执行流的字节码指令哪里来?
new字节码指令对应的执行流代码怎么来的
获取执行流指令的两种方式
1.写好C代码,然后把编译后生成的代码copy
2.编译原理 硬编码编织
3.第二部拿到的执行写入第一步申请的内存中
4.如何调用
需要有函数名指向它
汇编:jmp+返回地址压栈、call
C语言:函数指针(指针函数 返回指针的函数)
1.声明一个函数指针的类型
2.定义一个函数指针
如何研究JVM的执行流
模板解释器,编译优化的彻底性在哪里,节省栈帧的开辟
函数名意味着什么?
指向这个函数的代码段
call调用一个代码片段需要有一个前提条件,那段内存得有一个执行权限
JVM中的执行流代码存在JVM进程的堆当中。
OS里有很多进程,每个进程有
1.代码段
2.数据区、字符串常量
3.栈
4.堆(JVM的运行时数据区就在其中)
但是这个运行时数据区是可读可写不可知性的,所以需要修改页属性,赋予其可执行权限,所以说需要用mmap申请,它既可以申请内存,又可以申请权限
模板解释器,编译优化的彻底性在哪里,节省栈帧的开辟
函数名意味着什么?
指向这个函数的代码段
call调用一个代码片段需要有一个前提条件,那段内存得有一个执行权限
JVM中的执行流代码存在JVM进程的堆当中。
OS里有很多进程,每个进程有
1.代码段
2.数据区、字符串常量
3.栈
4.堆(JVM的运行时数据区就在其中)
但是这个运行时数据区是可读可写不可知性的,所以需要修改页属性,赋予其可执行权限,所以说需要用mmap申请,它既可以申请内存,又可以申请权限
OpenJDK的执行流如何研究?
打开VM参数 -XX:+PrintInterpreter.它的作用是会把执行流的首地址会打出来
method entry point (kind = zerolocals) [ 0x00007fffed01eb00, 0x00007fffed001f8e0] 3552bytes
这是我们普通方法调用的entry point [开始地址, 结束地址]
method entry point (kind = zerolocalas_synchrnoized) [0x0000efffed01f920, 0x00007fffed020ac0] 4512 bytes
这是带synchronized的方法 [开始地址,结束地址]
一个指令一个字节,分别共有3552 和4512个指令
接着打开GDB调试器输入
1. b *开始地址 (设置断点)
2.输入c就可以跳到汇编指令处
3.查看所有汇编指令
display /i 内存开始地址
打开VM参数 -XX:+PrintInterpreter.它的作用是会把执行流的首地址会打出来
method entry point (kind = zerolocals) [ 0x00007fffed01eb00, 0x00007fffed001f8e0] 3552bytes
这是我们普通方法调用的entry point [开始地址, 结束地址]
method entry point (kind = zerolocalas_synchrnoized) [0x0000efffed01f920, 0x00007fffed020ac0] 4512 bytes
这是带synchronized的方法 [开始地址,结束地址]
一个指令一个字节,分别共有3552 和4512个指令
接着打开GDB调试器输入
1. b *开始地址 (设置断点)
2.输入c就可以跳到汇编指令处
3.查看所有汇编指令
display /i 内存开始地址
HotSpot可以代表软件工业界(C/C++/汇编的最高水平)
call_stub和entry_point是Java与JVM的一道桥。
JVM中生成的执行流生成栈帧的汇编指令如图所示
generate_fixed_frame()
call_stub和entry_point是Java与JVM的一道桥。
JVM中生成的执行流生成栈帧的汇编指令如图所示
generate_fixed_frame()
结构体与指针
结构体是值传递,一般用结构体指针
结构体也有对齐(VS JVM的内存编织,Java对象Oop)
C语言中也可以用.来栈上分配。->堆上分配
在C中,32bit,结构体是4B对齐,64bit是8字节对齐,结构体中的内存占用如图所示
在struct A中,由于在32bit下是4字节对齐,c1、c2各占1B,int类型的i由于是4B,所以在一个对齐空间里面放不下,4-2<4,所以此时会再申请一个内存片段,用来存放int类型的数据,于是公式为1B(c1)+1B(c2)+2B(对齐填充)+4B(i) = 8B
在struct B中,同样的,c1单独占一个内存片段,i单独占一个,c2和c1一样,占用一个。于是三个内存片段共占3 * 4B =12B
如果对内存足够敏感的话,变量顺序需要注意
关于内存对齐,其实也可以修改
#pragma pack(2)
这是一种高级用法,如果不是对内存极致的追求
空结构体占几个字节?
为什么是1B? 因为0字节怎么申请内存
同理在Java中一个空对象占几个字节?
MarkWord(32bit=4B,64bit=8B)+Klass类型指针(64bit下才有指针压缩,指针压缩开启=4B, 关闭=8B)=8B~16B
结构体是值传递,一般用结构体指针
结构体也有对齐(VS JVM的内存编织,Java对象Oop)
C语言中也可以用.来栈上分配。->堆上分配
在C中,32bit,结构体是4B对齐,64bit是8字节对齐,结构体中的内存占用如图所示
在struct A中,由于在32bit下是4字节对齐,c1、c2各占1B,int类型的i由于是4B,所以在一个对齐空间里面放不下,4-2<4,所以此时会再申请一个内存片段,用来存放int类型的数据,于是公式为1B(c1)+1B(c2)+2B(对齐填充)+4B(i) = 8B
在struct B中,同样的,c1单独占一个内存片段,i单独占一个,c2和c1一样,占用一个。于是三个内存片段共占3 * 4B =12B
如果对内存足够敏感的话,变量顺序需要注意
关于内存对齐,其实也可以修改
#pragma pack(2)
这是一种高级用法,如果不是对内存极致的追求
空结构体占几个字节?
为什么是1B? 因为0字节怎么申请内存
同理在Java中一个空对象占几个字节?
MarkWord(32bit=4B,64bit=8B)+Klass类型指针(64bit下才有指针压缩,指针压缩开启=4B, 关闭=8B)=8B~16B
7.C++
JVM内存编织
为什么采用内存编织的方式?为什么不是传统的:一个普通的C++对象对应一个java对象
struct A {
char c1; // 1B
int i; // 4B
char c2; // 1B
}
结构体A大小为6B,但是由于内存编织的问题却要占12B,JVM的HotSpot对这种内存浪费的情况提出了一种解决方案,目的是减少内存缝隙。叫内存编织
所谓的内存编织就是申请一块大的内存,然后往内存中贴数据,有三种规则
JVM中的数据类型
boolean
byte
char
short
int
long
float
double
oop(ordinary object pointer) (32bit=4B,64bit=指针压缩开启4B,关闭8B)
Java对象(java层面)映射到->HotSpot C++对象 oop(OopDesc, oop其实就是一块内存区域,对其进行编织,调整属性的顺序)
三种编织规则
1.allocation_style=0
属性按由大到小的顺序进行编织,oop优先,编织顺序为oop、long/double、int、short、short、char、byte.编织所有属性后如果对象大小非8B对齐,尾部增加填充区域
2.allocation_style=1(默认)
属性还是由大到小的属性进行编织,不过这种方式oop最后编码。同样,非8字节对齐需要填充该区域
3.allocation_tyle=2
这种规则会将子类的oop与父类oop综合起来考虑,略显复杂
C++也可以对内存进行编织,但是没有做,JVM对内存做到了极致的压榨。
为什么采用内存编织的方式?为什么不是传统的:一个普通的C++对象对应一个java对象
struct A {
char c1; // 1B
int i; // 4B
char c2; // 1B
}
结构体A大小为6B,但是由于内存编织的问题却要占12B,JVM的HotSpot对这种内存浪费的情况提出了一种解决方案,目的是减少内存缝隙。叫内存编织
所谓的内存编织就是申请一块大的内存,然后往内存中贴数据,有三种规则
JVM中的数据类型
boolean
byte
char
short
int
long
float
double
oop(ordinary object pointer) (32bit=4B,64bit=指针压缩开启4B,关闭8B)
Java对象(java层面)映射到->HotSpot C++对象 oop(OopDesc, oop其实就是一块内存区域,对其进行编织,调整属性的顺序)
三种编织规则
1.allocation_style=0
属性按由大到小的顺序进行编织,oop优先,编织顺序为oop、long/double、int、short、short、char、byte.编织所有属性后如果对象大小非8B对齐,尾部增加填充区域
2.allocation_style=1(默认)
属性还是由大到小的属性进行编织,不过这种方式oop最后编码。同样,非8字节对齐需要填充该区域
3.allocation_tyle=2
这种规则会将子类的oop与父类oop综合起来考虑,略显复杂
C++也可以对内存进行编织,但是没有做,JVM对内存做到了极致的压榨。
union共用体与JVM指针压缩
JVM中的OopDesc中的_metadata属性就使用了union结构体,它的特点如下:
1.大小由最大宽度的属性决定
2.这个内存同一时刻只能代表一个意思
_metadata._klass(指针压缩关闭)
_metadata._compressed_klass(指针压缩开启)
这个设计对内存大小的影响,内存编织会受指针压缩的影响
klass pointer 类型指针
1.jvm的对象是8字节对齐的(言外之意:二进制的后三位都是0)
2.两段对齐:头部要对齐、整体要对齐
对象头的填充又分两种情况
对于一个普通的Java对象来说,MarkWord(8B) + Klass Pointer(指针压缩关闭,8B),如果是数组对象的话,还会有数组长度4B.
于是8+8+4=20B,在头部填充之前,如果实例数据是小于4B的实例数据,就会塞入这个8字节对齐的区域,比如说一个int数组,这样它的实例数据就会少占4B.如果一个数组对象,只有3个元素。在不把实例数据填充到数组长度的区域后面时,那么就会出现
对象头:8+8+4+4(头部填充)=24B
实例数据:4+4+4=12B
24+12=36B,显然不够8字节的倍数,然后还要进行尾部填充4B.那么在这个过程中,头部填充了4B,尾部又填充了4B
那么如果我们在把实例数据塞入一个元素会发生什么?
对象头:8+8+4+4(数组0号索引)=24B
实例数据:4+4=8B
24+8=32,正好够8字节对齐,这样就节省了8字节的内存缝隙。
JVM中的OopDesc中的_metadata属性就使用了union结构体,它的特点如下:
1.大小由最大宽度的属性决定
2.这个内存同一时刻只能代表一个意思
_metadata._klass(指针压缩关闭)
_metadata._compressed_klass(指针压缩开启)
这个设计对内存大小的影响,内存编织会受指针压缩的影响
klass pointer 类型指针
1.jvm的对象是8字节对齐的(言外之意:二进制的后三位都是0)
2.两段对齐:头部要对齐、整体要对齐
对象头的填充又分两种情况
对于一个普通的Java对象来说,MarkWord(8B) + Klass Pointer(指针压缩关闭,8B),如果是数组对象的话,还会有数组长度4B.
于是8+8+4=20B,在头部填充之前,如果实例数据是小于4B的实例数据,就会塞入这个8字节对齐的区域,比如说一个int数组,这样它的实例数据就会少占4B.如果一个数组对象,只有3个元素。在不把实例数据填充到数组长度的区域后面时,那么就会出现
对象头:8+8+4+4(头部填充)=24B
实例数据:4+4+4=12B
24+12=36B,显然不够8字节的倍数,然后还要进行尾部填充4B.那么在这个过程中,头部填充了4B,尾部又填充了4B
那么如果我们在把实例数据塞入一个元素会发生什么?
对象头:8+8+4+4(数组0号索引)=24B
实例数据:4+4=8B
24+8=32,正好够8字节对齐,这样就节省了8字节的内存缝隙。
C语言面向对象编程
前提时需要g++编译器的支持,原生gcc是不支持
1.封装 struct封装
2.构造函数 struct里面写构造函数
3.this指针
4.析构函数
写法
5.继承,支持多重继承,公有继承、私有继承、保护继承,一般也是用单继承,公有继承
6.多态
前提时需要g++编译器的支持,原生gcc是不支持
1.封装 struct封装
2.构造函数 struct里面写构造函数
3.this指针
4.析构函数
写法
5.继承,支持多重继承,公有继承、私有继承、保护继承,一般也是用单继承,公有继承
6.多态
3.this指针
eax把对象实体赋值给ecx,然后再调用方法时,把ecx赋值给this,this指针其实是一个局部变量的存在。
指针是特殊的数据类型
1.容器:基本类型、内存地址(存放)
2.运算:加减步长、与整数之间的互转
this指针比较特殊
1.容器,只能存放数据
2.不能进行运算。(原因在于它是一个常量指针常量)
Const Node* const this;指向内容不可改、指向不可改。。
this指针是g++编译器生成的汇编,才可以使用
00EB1A2E 8B 4D EC mov ecx,dword ptr [a]
00EB1A31 E8 7C F8 FF FF call A::myprint (0EB12B2h)
myprint:
00EB18DD 89 4D F8 mov dword ptr [this],ecx
eax把对象实体赋值给ecx,然后再调用方法时,把ecx赋值给this,this指针其实是一个局部变量的存在。
指针是特殊的数据类型
1.容器:基本类型、内存地址(存放)
2.运算:加减步长、与整数之间的互转
this指针比较特殊
1.容器,只能存放数据
2.不能进行运算。(原因在于它是一个常量指针常量)
Const Node* const this;指向内容不可改、指向不可改。。
this指针是g++编译器生成的汇编,才可以使用
00EB1A2E 8B 4D EC mov ecx,dword ptr [a]
00EB1A31 E8 7C F8 FF FF call A::myprint (0EB12B2h)
myprint:
00EB18DD 89 4D F8 mov dword ptr [this],ecx
4.析构函数
写法
~A() {
}
特点
1.没有返回值
2.没有参数
3.调用:不用手动调用,delete对象的时候、对象的作用域过了以后,系统自动调用
作用
1.回收内存
2.妙用(HotSpot)
我们知道一个类只能被初始化一次,会通过加锁的方式来保证线程安全,那么在HotSpot源码中,这个锁是怎么加的呢?
答案就是通过代码块将锁的作用域限制住,当代码块被执行完的时候会调用锁的析构函数,在析构函数里面进行锁的释放
写法
~A() {
}
特点
1.没有返回值
2.没有参数
3.调用:不用手动调用,delete对象的时候、对象的作用域过了以后,系统自动调用
作用
1.回收内存
2.妙用(HotSpot)
我们知道一个类只能被初始化一次,会通过加锁的方式来保证线程安全,那么在HotSpot源码中,这个锁是怎么加的呢?
答案就是通过代码块将锁的作用域限制住,当代码块被执行完的时候会调用锁的析构函数,在析构函数里面进行锁的释放
HotSpot中的类初始化的加锁逻辑
ObjectLocker中释放锁的逻辑
6.多态
静态绑定 call 0x7fff1421
动态绑定、晚绑定、间接call call[0x7ff63636]
动态绑定在调用时,调用的是虚表的地址。
虚表是多态得以实现的基础。虚表是一个数组结构。
虚表
1.虚表的位置:
对象的头部
2.虚函数对结构体大小的影响
不管有多少虚函数,只有一个虚表
32bit=4B
64bit=8B
3.OS如何调用虚表中的方法
4.模拟调用虚表
静态绑定 call 0x7fff1421
动态绑定、晚绑定、间接call call[0x7ff63636]
动态绑定在调用时,调用的是虚表的地址。
虚表是多态得以实现的基础。虚表是一个数组结构。
虚表
1.虚表的位置:
对象的头部
2.虚函数对结构体大小的影响
不管有多少虚函数,只有一个虚表
32bit=4B
64bit=8B
3.OS如何调用虚表中的方法
4.模拟调用虚表
调用虚函数。
eax寄存器存储了函数返回值
eax寄存器存储了函数返回值
虚函数内部的结构,只要有1个虚函数,就会生成虚表,无论虚函数的数量多少,虚表也是占用4B
结构体的两种分配方式
struct A {
}
栈上分配
A a;
a.test();
堆上分配
A a = new A;
a->test();
struct A {
}
栈上分配
A a;
a.test();
堆上分配
A a = new A;
a->test();
struct与class异同
在C++中,class和struct做类型定义
1.默认继承权限不同,class继承默认是private继承,而struct默认是public继承
2.class还可以用作定义模板参数,像typename,但是关键字struct不能用于定义模板参数
C++保留struct关键字,原因如下:
1.保证与C语言的向下兼容性,C++必须提供一个struct
2.C++中的struct定义必须百分百地保证与C语言中地struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制
3.对struct定义的扩展使得C语言的代码能够更容易的被移植到C++中
在C++中,class和struct做类型定义
1.默认继承权限不同,class继承默认是private继承,而struct默认是public继承
2.class还可以用作定义模板参数,像typename,但是关键字struct不能用于定义模板参数
C++保留struct关键字,原因如下:
1.保证与C语言的向下兼容性,C++必须提供一个struct
2.C++中的struct定义必须百分百地保证与C语言中地struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制
3.对struct定义的扩展使得C语言的代码能够更容易的被移植到C++中
公有继承、私有继承、保护继承的区别
在面向对象编程中,继承是一种非常重要的特性,它允许我们创建新的类(子类)来继承现有类(父类)的属性和方法。通过继承,子类可以获得父类的所有非私有属性和方法,并可以添加或覆盖它们以实现特定的功能。然而,继承并非总是将所有父类的成员直接暴露给子类,而是根据继承方式的不同,子类的父类成员的访问权限也会有所不同。
1.公有继承(public inheritance)(Java中的继承是公有继承)
公有继承是最常见的继承方式。在公有继承中,父类的公有成员和保护成员在子类中仍然保持其原有的访问权限,即公有成员和保护成员在子类中仍然是公有的,可以被子类的实例直接访问。而父类的私有成员在子类中是不可见的,即子类无法直接访问父类的私有成员。公有继承允许子类访问并扩展父类的公有和保护成员,这使得子类能够灵活地继承父类的功能并实现新的功能
2.私有继承(private inheritance)
私有继承是一种相对不常见的继承方式。在私有继承中,父类的公有成员和保护成员在子类中都被视为私有成员,即子类无法直接访问父类的公有成员和保护成员,这种继承方式将父类的所有成员都隐藏起来,只允许子类通过继承来的方法进行间接访问。私有继承的一个主要应用场景是实现接口或抽象类,即子类只需要实现父类的方法,而不需要直接访问父类的成员
3.保护继承(protected inheritance)
保护继承是另一种相对不常见的继承方式。在保护继承中,父类的公有成员和和保护成员在子类中都被视为保护成员,即子类无法直接访问父类的公有成员和保护成员,只能通过派生类(子类的子类)或友元函数进行访问。这种继承方式在一定程度上介于公有继承和私有继承之间,它既不像公有继承那样暴露父类的所有成员,也不像私有继承那样完全隐藏父类的成员。保护继承通常用于实现一些特定的设计模式和架构,例如模板方法模式等
总的来说,公有继承、私有继承和保护继承在面向对象编程中具有不同的应用场景和特点。公有继承允许子类灵活地访问和灵活地访问和扩展父类地公有和保护成员;私有继承将父类地所有成员都隐藏起来,只允许子类通过继承来的方法进行间接访问;保护继承则介于两者之间,允许子类通过派生类或友元函数访问父类的公有和保护成员。在实际编程中,我们应该根据具体的需求和场景来选择合适的继承方式来实现代码复用和扩展功能
此外,需要注意的是,不同的编程语言可能对继承方式的支持程度和语法细节有所不同。因此,在实际编程中,我们应该参考具体的编程语言文档和规范来了解和使用继承方式。同时,我们应该遵循良好的面向对象设计原则和实践,如单一职责原则、开放封闭原则、里氏替换原则等,来确保代码的可读性、可维护性和可扩展性。
最后,值得一提的是,虽然继承是一种强大的代码复用和扩展机制,但它并非总是最佳的选择,在某些情况下,组合(Composition)可能是一个更好的选择。组合允许我们将一个类的对象当作另一个类的成员来使用,从而实现代码复用和扩展功能。与继承相比,组合通常更加灵活和易于理解。因此在实际编程中,我们应该根据具体需求和场景来权衡继承和组合的使用
在面向对象编程中,继承是一种非常重要的特性,它允许我们创建新的类(子类)来继承现有类(父类)的属性和方法。通过继承,子类可以获得父类的所有非私有属性和方法,并可以添加或覆盖它们以实现特定的功能。然而,继承并非总是将所有父类的成员直接暴露给子类,而是根据继承方式的不同,子类的父类成员的访问权限也会有所不同。
1.公有继承(public inheritance)(Java中的继承是公有继承)
公有继承是最常见的继承方式。在公有继承中,父类的公有成员和保护成员在子类中仍然保持其原有的访问权限,即公有成员和保护成员在子类中仍然是公有的,可以被子类的实例直接访问。而父类的私有成员在子类中是不可见的,即子类无法直接访问父类的私有成员。公有继承允许子类访问并扩展父类的公有和保护成员,这使得子类能够灵活地继承父类的功能并实现新的功能
2.私有继承(private inheritance)
私有继承是一种相对不常见的继承方式。在私有继承中,父类的公有成员和保护成员在子类中都被视为私有成员,即子类无法直接访问父类的公有成员和保护成员,这种继承方式将父类的所有成员都隐藏起来,只允许子类通过继承来的方法进行间接访问。私有继承的一个主要应用场景是实现接口或抽象类,即子类只需要实现父类的方法,而不需要直接访问父类的成员
3.保护继承(protected inheritance)
保护继承是另一种相对不常见的继承方式。在保护继承中,父类的公有成员和和保护成员在子类中都被视为保护成员,即子类无法直接访问父类的公有成员和保护成员,只能通过派生类(子类的子类)或友元函数进行访问。这种继承方式在一定程度上介于公有继承和私有继承之间,它既不像公有继承那样暴露父类的所有成员,也不像私有继承那样完全隐藏父类的成员。保护继承通常用于实现一些特定的设计模式和架构,例如模板方法模式等
总的来说,公有继承、私有继承和保护继承在面向对象编程中具有不同的应用场景和特点。公有继承允许子类灵活地访问和灵活地访问和扩展父类地公有和保护成员;私有继承将父类地所有成员都隐藏起来,只允许子类通过继承来的方法进行间接访问;保护继承则介于两者之间,允许子类通过派生类或友元函数访问父类的公有和保护成员。在实际编程中,我们应该根据具体的需求和场景来选择合适的继承方式来实现代码复用和扩展功能
此外,需要注意的是,不同的编程语言可能对继承方式的支持程度和语法细节有所不同。因此,在实际编程中,我们应该参考具体的编程语言文档和规范来了解和使用继承方式。同时,我们应该遵循良好的面向对象设计原则和实践,如单一职责原则、开放封闭原则、里氏替换原则等,来确保代码的可读性、可维护性和可扩展性。
最后,值得一提的是,虽然继承是一种强大的代码复用和扩展机制,但它并非总是最佳的选择,在某些情况下,组合(Composition)可能是一个更好的选择。组合允许我们将一个类的对象当作另一个类的成员来使用,从而实现代码复用和扩展功能。与继承相比,组合通常更加灵活和易于理解。因此在实际编程中,我们应该根据具体需求和场景来权衡继承和组合的使用
C++中的多态
C++多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。导致错误输出的原因是,调用函数area()被编译器设置为基类中的版本。这就是所谓的静态多态,或静态链接-函数调用在程序执行前就准备好了,有时候这也被称为早期绑定,因为area()函数在程序编译器就已经设置好了。但现在,让我们对程序稍作修改,在Shape类中,area()的声明前放置关键字virtual,他就会产生以下结果:
Rectangle class area :
Triangle class area :
此时,编译器看的是指针的内容,而不是它的类型。因此,由于tri和rec类的对象地址存储在*shape中,所以会调用各自的area()函数。正如我们所看到的,每个子类都有一个函数area()的独立实现。这就是多态的一般使用方式。有了多态,我们就可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
C++多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。导致错误输出的原因是,调用函数area()被编译器设置为基类中的版本。这就是所谓的静态多态,或静态链接-函数调用在程序执行前就准备好了,有时候这也被称为早期绑定,因为area()函数在程序编译器就已经设置好了。但现在,让我们对程序稍作修改,在Shape类中,area()的声明前放置关键字virtual,他就会产生以下结果:
Rectangle class area :
Triangle class area :
此时,编译器看的是指针的内容,而不是它的类型。因此,由于tri和rec类的对象地址存储在*shape中,所以会调用各自的area()函数。正如我们所看到的,每个子类都有一个函数area()的独立实现。这就是多态的一般使用方式。有了多态,我们就可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
虚函数
它是在基类中使用关键字virtual声明的函数。在派生类中重新定基类中定义的虚函数时,会告诉静态链接到该函数。我们想要的时在程序中任一点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定/晚绑定
纯虚函数
你可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是你在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
```c++
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
```
=0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数
它是在基类中使用关键字virtual声明的函数。在派生类中重新定基类中定义的虚函数时,会告诉静态链接到该函数。我们想要的时在程序中任一点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定/晚绑定
纯虚函数
你可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是你在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
```c++
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
```
=0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数
Java中的多态
重写(OveriWrite)
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变,即外壳不变,核心重写。
重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如,父类的一个方法声明了一个检查异常IOException,但是在重写这个方法的时候不能抛出Exception异常,因为Exception是IOException的父类,只能抛出IOException的子类异常。在面向对象原则里,重写意味着可以重写任何现有方法
方法的重写规则:
1.参数列表必须完全与被重写方法相同
2.返回类型必须完全与被重写方法的返回类型相同
3.访问权限不能比父类中被重写方法的权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected.
4.父类的成员方法只能被它的子类重写
5.声明为final的方法不能被重写
6.声明为static的方法不能被重写,但是能够被再次声明
7.子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法
8.子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法
9.重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以
10.构造方法不能被重写
11.如果不能继承一个方法,则不能重写方法
方法的重写发生在运行时,因为在编译时,编译器是无法知道我们到底是调用子类的方法,相反的,只有在实际运行的时候,我们才知道应该调用哪个方法。这个也是Java运行时多态的体现。
重载(Overload)
对于类的方法(包括从父类中继承的方法)。如果有两个方法的方法名相同,但参数不一样,则一个方法是另一个方法的重载方法。
重载方法必须满足以下条件:
1.方法名相同
2.方法的参数类型、个数、顺序至少有一项不同
3.方法的返回值类型可以不同
4.方法的修饰符可以不同。
在一个类中不允许定义两个方法名相同,并且参数签名也完全相同的方法。因为加入存在这样的方法,Java虚拟机在运行时就无法决定到底执行哪个方法,参数签名是指参数的类型、个数和顺序。
注意:重载方法跟其返回类型没有关系
方法的重载发生在编译期。在编译过程中,编译器必须根据参数类型以及长度来确定到底是调用的哪个方法,这也是Java编译时多态的体现。
重写(OveriWrite)
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变,即外壳不变,核心重写。
重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如,父类的一个方法声明了一个检查异常IOException,但是在重写这个方法的时候不能抛出Exception异常,因为Exception是IOException的父类,只能抛出IOException的子类异常。在面向对象原则里,重写意味着可以重写任何现有方法
方法的重写规则:
1.参数列表必须完全与被重写方法相同
2.返回类型必须完全与被重写方法的返回类型相同
3.访问权限不能比父类中被重写方法的权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected.
4.父类的成员方法只能被它的子类重写
5.声明为final的方法不能被重写
6.声明为static的方法不能被重写,但是能够被再次声明
7.子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法
8.子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法
9.重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以
10.构造方法不能被重写
11.如果不能继承一个方法,则不能重写方法
方法的重写发生在运行时,因为在编译时,编译器是无法知道我们到底是调用子类的方法,相反的,只有在实际运行的时候,我们才知道应该调用哪个方法。这个也是Java运行时多态的体现。
重载(Overload)
对于类的方法(包括从父类中继承的方法)。如果有两个方法的方法名相同,但参数不一样,则一个方法是另一个方法的重载方法。
重载方法必须满足以下条件:
1.方法名相同
2.方法的参数类型、个数、顺序至少有一项不同
3.方法的返回值类型可以不同
4.方法的修饰符可以不同。
在一个类中不允许定义两个方法名相同,并且参数签名也完全相同的方法。因为加入存在这样的方法,Java虚拟机在运行时就无法决定到底执行哪个方法,参数签名是指参数的类型、个数和顺序。
注意:重载方法跟其返回类型没有关系
方法的重载发生在编译期。在编译过程中,编译器必须根据参数类型以及长度来确定到底是调用的哪个方法,这也是Java编译时多态的体现。
0 条评论
下一页