栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++基础知识及相关底层汇编实现(更新中)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++基础知识及相关底层汇编实现(更新中)

1.sizeof()是C++的编译特性,而不是函数。

具体看代码及反汇编:

#include 
using namespace std;

int main()
{
	cout << 4 << endl;

	cout << sizeof(int) << endl;

	getchar();
	return 0;
}

我们在sizeof处设置断点,并debug,同时右键点击反汇编: 

汇编代码如下:

可以看到:sizeof(int) 和 4 的汇编代码一模一样,也就是说编译器将sizeof(int)转化为了4,它们底层的汇编实现是一样的。

所以这里回答了很多人误解的错误答案:sizeof()是函数,这是错的;

进一步回答:那函数在汇编中是什么样的?

代码如下:

#include 
using namespace std;

void test()
{
	return;
}

int main()
{
	test();

	cout << 4 << endl;

	cout << sizeof(int) << endl;

	getchar();
	return 0;
}

如之前的操作,进行反汇编:

在汇编中,函数直接调用了call指令,转向对应的函数地址;而在sizeof中,并没有利用call进行跳转到该函数本身的地址上,而是直接push进去了一个4,也就是值的内容。

综上所述,sizeof()是C++的一种运算符(或者操作符),并不是一个函数。

2.C++默认参数  2.1.默认参数的相关注意事项

这部分是基础,值得复习一下:

  • C++允许函数设置默认参数,在调用时可以根据情况省略实参
  • 默认参数只能按照从右到左的顺序
  • 如果函数同时有声明、实现,默认参数只能放在函数声明中
  • 默认参数的值可以是常量,全局符号(全局变量、函数名)

2.2 默认参数在汇编中的实现

直接上代码:

#include 
using namespace std;

int sum(int a, int b = 4)
{
	return a + b;
}

int main()
{
	cout << sum(1) << endl;
	cout << sum(1, 4) << endl;
	getchar();
	return 0;
}

debug,反汇编:

这里我们只关注关键指令,可以看到,两个sum函数所调用的函数地址以及push的值都是一样的。

而左边的机器码,基本也是一样的,只有call指令的机器码不同,但这并不是说它们的汇编代码不同,而是这条指令中关乎下一指令的地址,故略微不同。

3.C++中的const
  • const是常量的意思,被修饰的变量不可更改
  • 如果修饰的是类、结构体(的指针),其成员也不可修改

核心问题:以下五个指针分别是什么含义?

int age=10;
const int* p0 = &age;
int const *p1 = &age;
int * const p2 = &age;
const int * const = &age;
int const * const p4 = &age;

以上指针问题可以用以下结论来解决:

const修饰的是其右边的内容

int age=10;

// p1不是常量, *p1是常量
const int* p1 = &age;

// p2不常量, *p2是常量
int const *p2 = &age;

// p3是常量, *p3不是常量
int * const p3 = &age;

// p4是常量, *p4也是常量
const int * const p4 = &age;

// p5是常量, *p5也是常量
int const * const p5 = &age;

类似的,我们将其扩展到struct或class中:

struct Student
{
	int age;
};

Student s1 = { 10 };
Student s2 = { 20 };

其操作1为:

因为const修饰的是*p1,也就是解引用的一个Student实例,那么对此实例进行修改或更改其成员变量是不允许的。

操作2:

其const修饰的是一个Student指针,也就是说对其解引用的Student实例进行修改是没有问题的。而要是想修改它所指向的地方,也就是它的地址,则会报错。 

综上所述,只要秉持着“const修饰的是其右边的内容”这一原则,我们就可以清楚的直到const到底修饰的是什么,这样什么可以修改或不可以修改便一目了然。

4.C++中的引用

C语言中可以使用指针(Pointer)间接获取、修改某个值

C++中,使用引用(Reference)可以起到跟指针类似的功能

值得注意的地方:

  • 引用相当于变量的别名(基本数据类型、枚举、结构体、类、指针、数组等)
  • 对引用做计算,就是对引用所指向的变量进行计算
  • 在定义的时候就必须初始化,一旦指向了某个变量,就不可以再更改
  • 可以利用引用初始化另一个引用,相当于某个变量的多个别名
  • 不存在【引用的引用】【指向引用的指针】【引用数组】

引用存在的价值之一:比指针更安全、函数返回值可以被赋值

4.1 引用的本质
  • 引用的本质就是指针,只是编译器削弱了它的功能,所以引用就是弱化了的指针
  • 一个引用占用一个指针的大小

简单的,从汇编的角度来讲,它们是一样的:

	int age = 10;

	int* p = &age;
	*p = 30;


	int& ref = age;
	ref = 40;

所以,本质上ref保存的就是age的地址。

而至于引用无法指向另一个对象这个特性,则是由编译器决定的,也就是编译器特性。

4.2 指针能够改变变量值的本质(汇编层面)

有如下代码:

	int age = 3;

	int* p = &age;
	*p = 5;

结果很容易看出,age通过指针p被赋值为5;

我们debug,反汇编:

具体是什么意思呢?

  1. 通过mov将值赋给age的这块内存。 
  2. 通过lea将 age的地址 赋给rax。
  3. 将rax中的地址(&age)赋给[p],也就是指针p所占内存。同时开辟了一块8字节的内存区域(qword)
  4. 将[p]送到rax
  5. 最后将5 送入到 rax中地址指向的一块内存区域。因为此时rax的内容是一个地址,所以是[rax]
4.3 常引用(const reference)
  • 引用可以被const修饰,这样就无法通过引用修改数据了,可以称为常引用
  • const必须写在 & 的左边,才能算是常引用
  • const引用的特点
    • 可以指向临时数据
      const int a = 10;
    • 可以指向不同类型的数据

    • 作为函数参数时(此规则也适用于const指针)

      • 可以接受const和非const实参(非const引用,只能接受非const实参)

      • 可以跟非const引用构成重载

int age = 10;
const double &ref = age;

int a = 1;
int b = 2;
const int &ref = 30;
const int &ref = a+b;
const int &ref = func();
  • 当常引用指向了不同类型的数据时,会产生临时变量,即引用指向的并不是初始化的那个变量有这样一段代码:

	int a = 10;
	const long &b = a;
	a = 30;

	int c = 10;
	const int& d = c;
	c = 30;

运行结果:

可以看到a的引用b并没有跟随着a的改变而改变,因为b的类型是double,那么我们反汇编看一下

先看正常的例子:const int = int(这里简写

可以看到,汇编通过lea 指令保存了变量c的地址到rax,再通过mov 将c的地址 也就是rax的内容保存到了另一块区域。

再来看看另一个例子:const long = int:

其主要区别在哪?在于最后一行mov ,也就是c=30前一行、a=30的前一行的mov

可以看出,b和d所保存的内容,相对于它们的引用来说是不一样的。

b保存的完全是另一个区域的地址(而不是a的地址),而d保存的就是c的地址!

所以也就说明了为什么常引用指向不同类型会产生临时变量,并且指向的并不是初始变量的问题。

其他关于const引用的用法,都比较简单,和指针同理:

引用的本质就是指针。

其实和上面3中提到的const是一码事,这里不多赘述了。 

5.C++中的面向对象 5.1 this和类指针
  • this指针存储着函数调用者的地址,或者说this指向了函数调用者,再或者说把调用者 的地址传进去

有如下代码:

class Person {
public:
	int id;
	int age;
	int height;
	void display();
};

void Person::display() {
	return;
}

int main() {

	Person person;
	person.id = 10;
	person.age = 20;
	person.height = 30;
	person.display();

	Person* p = &person;
	p->id = 10;
	p->age = 20;
	p->height = 30;
	p->display();


	return 0;
}

可以看到通过直接访问成员和通过类指针访问成员生成的汇编代码是不一样的。

同时也可以注意到,通过获取类实例的首地址,通过成员变量的地址偏移量进行内存的赋值。

从汇编指令的生成数量多少来说,直接访问成员变量确实更优,但这并不代表性能和效率的最优。同时,在许多情况下只能只用指针来进行操作,如在堆上分配内存。

5.2 构造函数
  • 构造函数(Constructor),在对象创建的时候自动调用,一般用于完成对象的初始化工作
  • 特点:
    • 函数名与类同名,无返回值,可以有参数,可以重载,可以有多个构造函数
    • 一旦自定义了构造函数,必须使用其中一个自定义的构造函数来初始化对象
  • 注意:
    • 通过malloc分配的对象不会调用构造函数

这里很好理解:malloc是C语言的库函数,自然不会有什么构造函数这一面向对象的概念,也就不会调用构造函数,只做了分配内存的工作;而C++则不同,new关键字会调用构造函数并且分配内存,由此可见在C++中要谨慎使用malloc,防止对象未初始化等不必要的错误发生。

  • 一个广为流传的、很多教程书记都推崇的错误结论:

默认情况下,编译器会自动生成一个无参的、空的构造函数

这个情况就不会生成无参的空函数:

class A {
public:
	int value;
};

	A a;
	a.value = 1;

可以看到汇编中根本没有call这个调用函数的指令。

其简单理解是什么意思呢?在class中根本没有对value进行赋值操作,那么此时在画蛇添足生成一个空的、无参的构造函数就显得毫无意义,作为编译器自然不会做这样浪费的事情。但如果是

int value=5;这样的话,那么就会调用自动生成的构造函数:

5.2.1 默认情况下,成员变量的初始化

  •  但如果自定义了了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化。
5.3 析构函数
  • 析构函数在对象销毁的时候自动调用,一般用于完成对象的清理工作
  • 函数名义~开头,与类同名,无返回值,无参,不可以重载,有且只有一个析构函数
  • 注意:通过malloc分配的对象free的时候不会调用析构函数
  • 构造函数、析构函数要声明未public,才能被外界正常使用。
5.4 继承

成员访问权限:子类内部访问父类的权限,是以下两项中的最小的那个:

  • 成员本身的访问权限
  • 上一级父类的继承方式

访问权限不影响对象的内存布局

 

6. 内存空间的布局

每个应用都有自己独立的内存空间,其内存空间一般都有以下几种:

  • 代码段(代码区)
    • 用于存放代码
  • 数据段(全局区)
    • 用于存放全局变量等
  • 栈空间
    • 每调用一个函数就会给它分配一段连续的栈空间,等函数调用完后会自动回收这段栈空间
    • 自动分配和回收
  • 堆空间
    • 需要主动去申请和释放
6.0 对象的内存布局 6.0.1 分布

对象的内存可以存在于3种地方:

  • 全局区(数据段):全局变量
  • 栈空间:函数里的局部变量
  • 堆空间:动态申请内存(malloc、new等)
	//全局区
	Person g_person;

	int main() {
		//栈空间 
		Person person;

		//堆空间
		Person* p = new Person;
		return 0;
	}
6.0.2 对象的内存布局
struct Person
{
	int age;
};

struct Student:Person
{
	int m_no;
};

struct GoodStudent:Student
{
	int m_money;
};

GoodStudent gs;
gs.m_age = 20;
gs.m_no = 1;
gs.m_money = 666;

 父类成员变量在前,子类成员在后

 

6.1 堆空间
  • 在程序运行过程中,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存
  • 堆空间的申请释放
    • malloc free
    • new delete
    • new [] delete []
  • 注意:
    • 申请堆空间成功后,会返回那一段内存空间的地址
    • 申请和释放必须是1对1的关系,不然可能会存在内存泄漏
  • 现在很多高级编程语言不需要开发人员去管理内存(Java),屏蔽了很多细节,有利有弊
    • 利:提高开发效率,避免内存使用不当或泄露
    • 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手
6.1.1 堆空间的初始化

先上代码:

	int* p1 = (int*)malloc(sizeof(int));//p1未初始化
	int* p2 = (int*)malloc(sizeof(int));
	memset(p2, 0, sizeof(int));//将*p2的每一个字节都初始化为0

	int* p1 = new int;//未初始化
	int* p2 = new int();//被初始化为0
	int* p3 = new int(5);//被初始化为5
	int* p4 = new int[3];//数组元素未被初始化
	int* p5 = new int[3]();//3个数组元素都被初始化为0
	int* p6 = new int[3]{};//3个数组元素都被初始化为0
	int* p7 = new int[3]{ 5 };//数组首元素被初始化为5,其他元素被初始化为0

6.2 内存对齐

# TODO

Ref:汇编语言

汇编语言种类:

  • 8086汇编(16bit)
  • x86汇编(32bit)
  • x64汇编(64bit)
  • ARM汇编(嵌入式、移动设备)
  • ...

其中,x64汇编根据编译器的不同,有两种书写形式:

  • Intel
  • AT&T

其大致区别如下:

1.寄存器与内存 
  • 通常,CPU先将内存中的数据存储到寄存器中,然后在对寄存器中的数据进行运算
  • 假设内存有一个值为3的内存空间,打算对他加一,并存储到另一块区域:
    • CPU首先将3放到EAX寄存器中:mov eax,3
    • 然后将EAX与1相加:add eax,1
    • 最后将值赋给内存空间:mov [内存空间],eax

2.x64汇编寄存器

寄存器大致如上图。

在不同的架构的机器上,寄存器的名称也有不同,比如:

64bit:

RAXRBXRCXRDX:通用寄存器 

32bit:

EAXEBXECXEDX:通用寄存器 

16bit:

AXBXCXDX:通用寄存器 

但x64汇编是兼容以前版本的汇编的,如下图:

如图,x64汇编把低32位用EAX来表示, 低16位用AX来表示。

也就解释了为什么在64bit架构中的反汇编代码依然存在eax这个寄存器。

下图表示了常见的寄存器是如何组成的。  

3.在C++中嵌入汇编

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/997858.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号