CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,我会很高兴~
作用域
作用域(Scope)用于组织代码和避免命名冲突,也就是说你可以用定义两个不同类型但相同名字的变量(在不同作用域)
- 全局作用域作用域
全局作用域就是宏,数据存在.bss段或者.data段之类的地方
- 名字空间作用域(命名空间)
定义一个名字作为标识符,通过这个标识符(名字)::变量名
的方式访问里面的变量,C++标准库中的所有标识符都定义在std命名空间中
这也就是为什么用cout函数的时候要std::cout
- 局部作用域:
局部变量,存在stack上的数据
- 类作用域(class scope):
一个类的结构范围就是一个类作用域,被定义在类里面的数据被称为成员
- 语句作用域:
在for while if的语句里也可以定义数据,这里就是语句作用域
this指针
指向当前对象的指针,类型为指向类
类型的指针(例如,对于 Base 类,this 的类型是 Base*)
底层
源码
#include<iostream>
#include<string.h>
using namespace std;
class Base{
public:
void fun(){ /* 成员函数的代码是存储在程序的代码段中,而不是存储在类的实例中。每个类的成员函数在内存中只有一份实现(也符合代码复用的特点) */
cout<<name<<endl;
}
char name[10];
};
class A : public Base{
public:
void foo(){
strcpy(this->name,"A");
this->fun(); // 相当于fun
}
};
class B : public Base{
public:
void foo(){
strcpy(this->name,"B");
this->fun();
}
};
int main(void){
A *a = new A(); /* 调用类函数时,会将其 new 出来的堆内存当做第一个参数传入(相当于传入了该对象的数据结构体) */
B *b = new B();
a->foo();
b->foo();
}
ida版本的
__int64 __fastcall B::foo(B *this) /* foo()这个成员函数 */
{
*(_WORD *)this = 66;
return Base::fun(this);
}
__int64 __fastcall A::foo(A *this)
{
*(_WORD *)this = 65;
return Base::fun(this);
}
__int64 __fastcall Base::fun(Base *this)
{
std::ostream *v1; // rax
v1 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, (char *)this);
return refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v1);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rax
B *v6; // [rsp+20h] [rbp-10h]
A *v7; // [rsp+28h] [rbp-8h]
_main();
v3 = operator new(0xAui64);
*(_QWORD *)v3 = 0i64;
*(_WORD *)(v3 + 8) = 0;
v7 = (A *)v3;
v4 = operator new(0xAui64); /* 返回的应该是一个指向当前对象实例的this指针 */
*(_QWORD *)v4 = 0i64; /* 这两行是编译器的优化,帮我们初始化了分配name段的内存 */
*(_WORD *)(v4 + 8) = 0;
v6 = (B *)v4;
A::foo(v7); /* 编译器会在调用时自动将对象的地址(即 this 指针)传递给该函数。这使得不在类内部的函数也可以访问类的成员变量 */
B::foo(v6); /* 这也就是为什么我们什么在代码里什么都没传但ida的里面却有参数 */
return 0;
}
重载
函数重载
允许在同一个作用域中定义多个同名的函数,但是这些函数必须有不同的参数列表(参数类型,顺序,个数)。
编译器在函数调用时根据参数的类型和数量来决定调用哪个具体的函数版本,这一过程称为静态绑定
(留个印象)
#include <iostream>
using namespace std;
class Example {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
};
int main() {
Example obj;
cout << obj.add(2, 3) << endl; /* 输出 5 */
cout << obj.add(2.2, 3.5) << endl; /* 输出 5.7 */
cout << obj.add(1, 2, 3) << endl; /* 输出 6 */
/* 用户只需记住一个函数名,而不必为每个参数组合记住不同的函数名 */
return 0;
}
ida版本的
对于类函数来说,编译器会把 类名称,函数名称,参数列表
放入哈希函数转化为一个哈希值,并用这个哈希值
来当做函数的名称
push rbp
mov rbp, rsp
sub rsp, 40h
call __main
lea rax, [rbp+var_1]
mov r8d, 3 ; int
mov edx, 2 ; int
mov rcx, rax ; this
call _ZN7Example3addEii ; Example::add(int,int)
mov edx, eax
mov rcx, cs:_refptr__ZSt4cout
call _ZNSolsEi ; std::ostream::operator<<(int)
mov rdx, cs:_refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rcx, rax
call _ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
mov rcx, 400C000000000000h
mov rdx, 400199999999999Ah
lea rax, [rbp+var_1]
mov [rbp+var_18], rcx
movsd xmm2, [rbp+var_18] ; double
mov [rbp+var_18], rdx
movsd xmm1, [rbp+var_18] ; double
mov rcx, rax ; this
call _ZN7Example3addEdd ; Example::add(double,double)
movapd xmm1, xmm0
mov rcx, cs:_refptr__ZSt4cout
call _ZNSolsEd ; std::ostream::operator<<(double)
mov rdx, cs:_refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rcx, rax
call _ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
lea rax, [rbp+var_1]
mov r9d, 3 ; int
mov r8d, 2 ; int
mov edx, 1 ; int
mov rcx, rax ; this /* 通过该程序的符号信息我们可以知道 */
call _ZN7Example3addEiii ; Example::add(int,int,int) /* 类名后面的3,add,E,iii分别代表:函数名长度,函数名,类名首字母,参数简写(i就是int) */
mov edx, eax
mov rcx, cs:_refptr__ZSt4cout
call _ZNSolsEi ; std::ostream::operator<<(int)
mov rdx, cs:_refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rcx, rax
call _ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
mov eax, 0
add rsp, 40h
pop rbp
retn
编译器在语法分析时,就通过参数列表确定了应该调用的函数,然后把程序编译成了汇编
运算符重载
本质上就是函数重载,但语法有点不一样
定义一个函数,并告诉C++编译器,当遇到该运算符时就调用此函数
来执行运算符功能。这个函数叫做运算符重载函数(常为类的成员函数)
#include <iostream>
class Box
{
public:
double getVolume(void){
return length * breadth * height;
}
void setAll(double len,double bre,double hei){
length = len;
breadth = bre;
height = hei;
}
Box operator + (const Box& b){ /* 重载运算符'+'(只有'+'两边都是Box类(因此传入常Box指针类型 const Box&)型时,才会触发该函数) */
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
double length;
double breadth;
double height;
};
int main(){
Box Box1;
Box Box2;
Box Box3;
double volume = 0.0;
Box1.setAll(1.0,1.0,1.0);
Box2.setAll(2.0,2.0,2.0);
volume = Box1.getVolume();
volume = Box2.getVolume();
Box3 = Box1 + Box2;
volume = Box3.getVolume();
return 0;
}
ida版本的
double *__fastcall Box::operator+(double *a1, double *a2, double *a3) /* 重载的+运算符,本质是一个函数,把返回值给a1,也就是box类的this指针 */
{
*a1 = *a2 + *a3;
a1[1] = a2[1] + a3[1];
a1[2] = a2[2] + a3[2];
return a1;
}
{
__int64 v3; // xmm0_8
__int64 v5[4]; // [rsp+30h] [rbp-80h] BYREF
__int64 v6[4]; // [rsp+50h] [rbp-60h] BYREF
char v7[32]; // [rsp+70h] [rbp-40h] BYREF
char v8[24]; // [rsp+90h] [rbp-20h] BYREF
__int64 v9; // [rsp+A8h] [rbp-8h]
_main();
v9 = 0i64;
Box::setAll((Box *)v8, 1.0, 1.0, 1.0);
Box::setAll((Box *)v7, 2.0, 2.0, 2.0);
Box::getVolume((Box *)v8);
v9 = v3;
Box::getVolume((Box *)v7);
v9 = v3;
Box::operator+(v5, v8, v7); /* volume = Box3.getVolume(); */
v6[0] = v5[0];
v6[1] = v5[1];
v6[2] = v5[2];
Box::getVolume((Box *)v6);
return 0;
}
因为我们知道类中只会存放数据结构,所以我们可以将其定义为结构体
struct Box
{
double len; /* 就是我们在类中定义的属性变量 */
double bre;
double hei;
};
Box *__fastcall Box::operator+(Box *a1, Box *a2, Box *a3)
{
a1->len = a2->len + a3->len; /* a2也是this指针 */
a1->bre = a2->bre + a3->bre;
a1->hei = a2->hei + a3->hei;
return a1;
}
构造函数
和析构函数相对立的函数对象
class Test1{
public:
Test1(); /* 构造函数,当类的实例被创建后自动调用的函数 */
};
class Test2 : public Test1{
};
Test1::Test1(void)
{
cout << "Test1" << endl;
}
int main(int argc, char* argv[])
{
Test2 t; /* 由于 Test2 没有构造函数,但父类 Test1 有构造函数 */
return 0; /* 创建t对象的时候自动调用Test1 的构造函数 */
}
底层
Test2 *__fastcall Test2::Test2(Test2 *this)
{
return Test1::Test1(this); /* 从ida也可以看出来:实例化类的过程也是 对传入的参数 调用构造函数的过程 */
}
Test1 *__fastcall Test1::Test1(Test1 *this)
{
std::ostream *v1; // rax
v1 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Test1");
return (Test1 *)refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v1);
} /* 编译器自动把调用链补全了 */
容器声明
关联容器
- unordered_map
#include <iostream> #include <unordered_map> int main() { // 声明一个 unordered_map,名字为test //这是 C++ 标准库中的一个关联容器,定义在 <unordered_map> 头文件中 std::unordered_map<int, long> test; //<int, long> 指定了容器中键(key)和值(value)的类型 // 插入元素 test[1] = 100000L; // 访问元素 std::cout << "Key: 1, Value: " << test[1] << std::endl; return 0; }
ida
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbx
_QWORD *v4; // rax
__int64 v5; // rax
int v7; // [rsp+Ch] [rbp-54h] BYREF
char v8[56]; // [rsp+10h] [rbp-50h] BYREF
std::unordered_map<int,long>::unordered_map(v8, argv, envp);/* 返回一个unordered_map指针到v8 */
v7 = 1;
*(_QWORD *)std::unordered_map<int,long>::operator[](v8, &v7) = 100000LL;/* 用[]重载符访问v8的1索引的数据 */
v3 = std::operator<<<std::char_traits<char>>(&_bss_start, "Key: 1, Value: ");
v7 = 1;
v4 = (_QWORD *)std::unordered_map<int,long>::operator[](v8, &v7); /* 访问v8的1索引的数据并把值传给v4 */
v5 = std::ostream::operator<<(v3, *v4);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>); /* 全部输出 */
std::unordered_map<int,long>::~unordered_map(v8); /* 析构函数,释放资源 */
return 0;
}
虚函数
#include <iostream>
#include <string>
using namespace std;
class Test{
public:
virtual void foo(){
cout<<"Test::foo() is called"<<endl;
}
};
int main(void){
Test *t = new Test();
t->foo();
return 0;
}
ida版本
int __cdecl main(int argc, const char **argv, const char **envp)
{
Test *v3; // rbx
_main();
v3 = (Test *)operator new(8ui64); /* 分配8字节给 虚函数表指针vptr */
*(_QWORD *)v3 = 0i64; /* 数值初始化 */
Test::Test(v3); /* 对 v3 指向的内存进行构造,调用了 Test 类的构造函数,完成对象的初始化 */
(**(void (__fastcall ***)(Test *))v3)(v3);
return 0;
}
(void (__fastcall ***)(Test *))v3
:这句将 v3 解释为一个指向虚函数表的指针,是一个指向指针的指针。这里的第一个 * 用于指向虚函数表的指针,第二个 * 用于指向该虚函数表中的函数指针,第三个 * 是具体的函数指针类型,(Test ) 表示这个函数指针的参数类型是 Test,即调用时将以 Test 类的对象作为参数
这里两次解引用,获取虚函数表中第一个虚函数的指针。这是因为虚函数表中的指针是按顺序排列的。在 Test 类中,foo() 是第一个也是唯一的虚函数,因此这个解引用将给出指向 foo() 的指针。
同样,可以来定义一下结构体
struct test
{
__int64 vptr;
};
把v3转成结构体:
v3 = (test *)operator new(8ui64);
v3->vptr = 0i64;
Test::Test((Test *)v3);
(*(void (__fastcall **)(test *))v3->vptr)(v3);
虚函数
面向对象的语言有三大特性:继承、封装、多态。虚函数就是 cpp 实现多态的方式。多态:使得程序能够在运行时根据对象的实际类型
来决定调用哪个版本的函数
具体来说就是:虚函数由指针指向的实际类型
决定
解释:在c语言中,指针的类型
和指针指向的类型
一般需要相同(比如char和int虽然都是__int64,但是不能相互赋值),但是类指针强制类型转换不会改变对象
的实际类型,只会改变指针的解释方式
,所以
#include <iostream>
#include <string>
using namespace std;
class Base{
public:
virtual void foo(){
cout<<"Base::foo() is called"<<endl;
}
};
class A:public Base{
public:
void foo(){
cout<<"A::foo() is called"<<endl;
}
};
class B:public Base{
public:
void foo(){
cout<<"B::foo() is called"<<endl;
}
};
int main(void){
Base *a = new B(); /* a 是 Base* 类型的指针,但它实际上指向的是 B 类型的对象 */
a->foo(); /* 当调用 a->foo() 时,程序会查找 B 的虚函数表,找到 B::foo() 的地址并执行它 */
((A *)a)->foo(); /* 将 a 转换为 A* 类型,但由于 a 实际指向的是一个 B 类型的对象,因此调用的仍然是 B 的 foo() 方法 */
return 0;
}
ida
int __cdecl main(int argc, const char **argv, const char **envp)
{
B *v3; // rbx
_main();
v3 = (B *)operator new(8ui64);
*(_QWORD *)v3 = 0i64;
B::B(v3);
(**(void (__fastcall ***)(B *))v3)(v3);
(**(void (__fastcall ***)(B *))v3)(v3);
return 0;
}
可以看到编译器实际上都是在对指针指向的实际类型B类进行操作,无论指针如何被解释。
虚函数最重要的继承机制(实现多态的关键)就是在基类的虚函数表上添加自己的虚函数
迭代器
迭代器是一种检查容器(可以理解为类,里面有数据结构和配套的算法)内元素并遍历元素的数据类型
,通常用于对C++中各种容器内元素的访问,为了方便对不同的容器有相同的访问方法而设计出来,不同的容器有不同的迭代器,可以将迭代器理解为指针
begin()就是指向容器第一个元素的迭代器,end()是指向容器最后一个元素的下一个位置的迭代器
void text()
{
vector<int> vtr;
//初始化容器
for (int i = 0; i < 10; ++i)
{
vtr.push_back(i);
}
//利用迭代器遍历容器
cout << "方式1:";
for (vector<int>::iterator it = vtr.begin(); it != vtr.end(); ++it)
{
cout << *it << " ";
}
}