C++

C++类-this指针

基础概念

C语言中,结构体作为函数参数时传参传的是副本,而数组传递的是指针,所以我们将结构体作为参数时,最好传入指针

封装-就是将函数放入结构体内部,每次调用函数都会传递当前结构体的首地址,这样我们使用里面的变量就会比较方便

类-这个结构体就叫做类,在结构体插入函数时,结构体大小不变,因为函数不在结构体内部,可以看到sizeoftest=8,也就是两个int

对象-当使用使用类型创建变量时,创建的名称就叫做对象,比如Base base,Base是类,那么base就叫做对象

成员-里面的参数都是成员

成员函数-顾名思义,结构体中函数类型的成员

成员、类、对象

this指针

在反汇编中,调用类中的函数之前会先把结构体对象的首地址传入ecx中,ecx存放的值就是this指针,我们可以通过它来使用结构体成员,this->x,this->y(在类的函数中),当函数参数和结构体变量同名时,使用this可以帮助编译器区分

调用类方法时的初始化操作

我们知道指针可以进行加法减法操作,但是this指针不能进行这些操作,编译器不允许

继承、构造-析构函数

构造函数

首先他是个函数,其次他没有返回值,第三他跟当前的类名是完全一样的

他跟成员函数不同,他是在创建对象的时候使用的

作用:通过构造函数,在创建对象的同时给成员赋值

使用构造函数初始化

注意事项:构造函数不是一定要有的,但是定义了构造函数就一定要使用,否则会报错

重载构造函数:多个构造函数,但是构造函数的参数个数不能一样

只要函数的参数个数或者参数类型不一样,就可以存在多个同名函数,这些函数就是重载函数-成员函数

析构函数

不是必须提供的-当使用分配器需要用到

在类中,当我们malloc时,在堆中开辟空间,但是堆中的空间使用完一般要释放,而我们无法知道什么时候才可以释放掉这个堆的空间-因为其他函数可能会继续使用。

但是,当对象不再使用的时候(对象为局部变量时),堆就可以释放掉了

析构函数-无需调用、不能重载、名字与类名相同并在其之前加个~

1
2
3
4
5
6
7
~Persion()

{

free(arr);

}

继承

本质是数据的复制

当在多个结构体存在相同的成员时,我们可以将相同的成员提取出来创建一个新的结构体,在使用时只需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct persion
{
int age;
int sex;
}

struct student:persion
{
int grade;
int count;
}

struct teacher:persion
{
int level;
int p;
}
teacher s;
student t;

上面的代码在teacher和student结构体都有四个成员,相同成员有两个

相同部分

修改后以及Student结构体大小

1、Persion称为父类或者基类

2、teacher、student称为子类或者派生类

3、s、t称为对象或者实例

4、可以用父类的指针指向子类的对象

示例:

1
persion*pt=&t;

子、父类关系

所以我们取得子类对象地址,就可以通过其来访问父类成员,但是不能访问子类成员,因为是Persion类型的指针

通过子类访问父类

但是不能用子类指针指向父类对象

会一直往上继承父类

多重继承

当多重继承出现相同的名称是,需要告诉编译器是谁的a,z.x::a,z.y::a​

可以同时继承两个父类

权限控制

在类中,如果我们将函数写入会让类变得十分庞大,所以可以在类中进行函数声明,然后再去类外实现,但是我们需要告诉编译器,该函数属于谁,就需要用到 void 类名::函数名(函数参数)

函数声明

public和private

存储的地方没变,只是告诉编译器不能直接访问private

可以修饰函数也可以修饰变量

public是指该成员在哪里都可以使用,不用担心被修改,所以一旦发布成public成员,是不能修改名字的。而private这个成员只用于内部使用,不要在其他地方使用

一般不想被外部访问的或者以后还会修改的就发布为private

常见的类的定义应该是把变量设为private,将修改private的方法-也就是函数发布为public,并在其中加入一定的限制

使用指针可访问private成员

class

class

class和struct的不同是权限不一样,class默认为private,struct则为public,注意class的构造函数也需要声明为public

继承

如果按照struct的格式进行继承,编译器会修改成员属性为private,所以如果需要访问,需要在继承时使用:public 父类名​​,可以看到我们只能继承public类的,一旦修改为private就会报错

父类继承

private成员只能在当前类被使用,不能被继承

编译器会默认生成构造函数,如果存在继承关系,他会先调用父类的构造函数,因为父辈的内容也需要初始化

子类父类构造函数执行顺序

我们可以先得到子类的地址,然后访问,即使父类的成员是private,也会被复制过来,只是不能直接访问,下面可以看到这是3个int的大小

但是我们可以知道子类的地址,然后通过指针访问私有的父类

指针访问私有父类

重载

函数重载

运算符重载

虚函数

关键字:virtual

当直接访问类中的函数时,都是直接call,而当使用指针访问时,虚函数是间接call,也就是取地址后再call

当添加一个虚函数时,类的大小增加了四字节

当类中存在两个虚函数时依然增加四字节

这是因为在对象中存在虚函数时会创建虚函数表用来存储各个虚函数的地址,并且虚函数表存储的位置是对象的首地址

eax的值也就是this指针,第一个是虚函数表的地址,后两个是类的成员。

所以这两处调用虚函数时先将eax地址(this指针指向的地址)存储的值放入edx中,然后再取出edx地址中的值进行call,使用指针进行虚函数的调用就是虚函数的虚调用,是虚函数加载完成之后调用的

虚调用和实调用

当类中有virtual,类大小增加四字节,并且类的首地址存储虚函数表的地址,而虚函数表存储了虚函数的地址

遍历虚函数表进行对虚函数的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<string>
#include <iostream>
using namespace std;
class Base
{
public:
int x;
int y;
Base();
virtual void Function_1()
{
cout << "Function_1!" << endl;
}
virtual void Function_2();
};
Base::Base()
{
x = 0;
y = 1;
}
void Base::Function_2()
{
cout << "Function_2!" << endl;
return ;
}
int main()
{
//test的地址就是this指针
Base test;

//this指针指向类的首地址,而首地址存储虚表的地址,定义函数指针进行调用
typedef void(*pfunc)();//void型无参函数指针

//定义函数指针成员
pfunc fp;//取出虚表的地址
for (int i = 0; i < 2; ++i)
{
fp= (pfunc) * ((int*)(*(int*)&test) + i);//(int*)&test是this指针,指向虚函数表的地址,*取出虚函数表地址,(int*)转为int型的指针,+i遍历,*取出虚函数表中的地址,(pfunc)转为函数指针
fp();//调用
}
return 0;
}

虚函数表结构

Hook虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define  _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
#include<string.h>

class Test_virtual {
public:
virtual void Print();
virtual void Print2();
};
void Test_virtual::Print() {
printf("This is a Virtual Function\n");
}

void Test_virtual::Print2() {
printf("This is a Virtual Function2\n");
}
void TrueFunc() {
printf("This is the True Function\n");
}

typedef void (*func)(char*);//函数指针
int main(int argc, char* argv[])
{
Test_virtual mytest;
Test_virtual* test = &mytest;//为了能够进行虚调用
//虚函数表的地址存储于this指针
int vfunc_addr = *(int*)(&mytest);//虚函数表的地址,存储的就是虚函数的地址

//先调用原来的虚函数
test->Print2();
DWORD lpflOldProtect = 0;
VirtualProtect((void*)vfunc_addr, 0x1000, PAGE_EXECUTE_READWRITE, & lpflOldProtect);//修改地址属性为可读可写可执行
//函数hook赋值
*(((int*)vfunc_addr)+1) = (int)(TrueFunc);//虚函数表存储虚函数的地址,需要解引用后赋值,+1表示hook的是第二个虚函数
test->Print2();
return 0;
}

使用虚调用对虚函数进行调用,并且在这之前先对虚函数表中存储的虚函数地址修改为我们想执行的函数地址

单继承覆盖函数

子类中对父类的虚函数进行重写

多层继承无覆盖

可以看到此时继承了两个父类,base类的大小为8,所以存在两个虚表

对两个虚表进行分别调用打印,发现子类的虚函数只在第一个父类的虚表中,第二个虚表只存放第二个父类的虚函数

多层继承会覆盖掉父类的虚函数

多重继承继承和覆盖

对于子类中的虚函数对父类的虚函数进行重写覆盖(即函数名、参数类型和个数相同)时,调用的时候会调用覆盖后的

多态

多态即一个对象表现出多种行为

绑定就是将函数的调用与地址关联起来

前期绑定

也叫做编译时绑定

晚绑定、动态绑定-运行时绑定

动态绑定即多态,多态使得我我们可以通过父类指针来访问子类,而晚绑定和动态绑定的对象都是虚函数

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<string>
#include <iostream>
using namespace std;

class base
{
public:
int x;
public:

base()
{
x = 30;
}
void Func_1()
{
cout << "base:Func_1!" << endl;
}
virtual void Func_2()
{
cout << "virtual-base:Function2!" << endl;
}
};

//继承了父类的子类然后进行修改
class Base :public base
{
public:
int x;
public:
Base()
{
x = 20;
}
void Func_1()
{
cout << "Base:Func_1!" << endl;
}
virtual void Func_2()
{
cout << "virtual-Base:Function2!" << endl;
}
};


int main()
{
Base test;
base* pb = &test;
cout << pb->x << endl;
pb->Func_1();
pb->Func_2();
return 0;
}

运行结果

我们通过将子类对象地址赋值给父类指针,然后使用父类指针访问可以发现:在父类和子类都定义了相同的变量x,函数Func_1(),虚函数Func_2(),但是在打印时x和Func_1都是父类的结果,而Func_2则是子类的结果,这是因为前两者为静态绑定-即编译时就写好了所以位直接call地址,而后者为虚函数,为动态绑定,子类中对父类的虚函数进行重写

使用

所以析构函数需要定义为虚函数,即virtua ~Base(),这是因为当我们使用父类指针访问子类对象时,访问完成后会调用父类的析构函数(若不使用虚函数)

模板

STL基础知识

stl的容器用法

https://www.jianshu.com/p/497843e403b4

stl容器详解

https://www.cnblogs.com/sea520/p/12711554.html

operaotr

https://zhuanlan.zhihu.com/p/353189480

测试

String

string使用动态内存分配来存储字符串

string初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
int main()
{
//使用const char*初始化
string s1 = "aaaa";

//构造函数初始化
string s2("bbbbb");

//通过拷贝构造函数来初始化
string s3 = s2;

//直接初始化字符串
string s4(10, 'd');
return 0;
}

使用g++编译链接

basic_string是一个类模板

string遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
string str("abcdefg");

//数组形式
for (int i = 0; i < str.length(); ++i)
{
cout << str[i] << endl;
}

//at方法遍历
for (int i = 0; i < str.length(); ++i)
{
cout << str.at(i) << endl;
}

//迭代器遍历
for (string::iterator it = str.begin(); it != str.end(); it++)
{
cout << *it << endl;
}

string打印

1
2
string str("abcdefg");
cout<<str<<endl;//ida识别时把换行符分开了

这里的是char,而前面打印单个字符是char_traits(字符特征类)

std::operator是运算符重载

string与char*的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//string与char*的转换
//1.string转char*
string str("aaaaa");
const char* p = str.c_str();


//2.char*转string
const char* p1 = "abcdedg";
string str2 = p1;

//3.string拷贝到buf[]中
char buf[128] = { 0 };
//从0开始拷贝3个字符到buf指定的内存空间,如果buf容纳不下会越界,拷贝时不会在buf末尾添加0
str.copy(buf, 3, 0);

string拼接

1
2
3
4
5
6
7
8
9
10
11
12
//字符串拼接
string s1 = "123456";
string s2 = "67890";

//1.直接使用加号运算符拼接
string s3 = s1 + s2;

//2.使用成员函数拼接
string s4 = s1.append(s2);

cout << s3 << endl;
cout << s4 << endl;

string查找和替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//string的查找和替换
string s1 = "hello hello hello hello 1234 7686";

//从0位置开始查找第一个hello的位置,size_t = unsigned int
size_t index1 = s1.find("hello", 0);
cout << index1 << endl;

//查找第一个hello出现时的首位位置
size_t index2 = s1.find_first_of("hello");
cout << index2 << endl;

//查找最后一个hello出现时的末尾位置,末尾位置是hello的最后一个字符位置
size_t index3 = s1.find_last_of("hello");
cout << index3 << endl;

//求hello出现的次数,以及对应的下标
int count = 0;
size_t offindex = s1.find("hello", 0);
while (offindex != string::npos)//npos用来表示不存在的位置
{//如果offindex!=-1
//找到
cout << "索引:" << offindex << endl;
count++;
offindex++;
offindex = s1.find("hello", offindex);
}

//把hello替换成welcome
size_t offindex1 = s1.find("hello", 0);
while (offindex1 != string::npos)
{
//从offindex1的位置开始删除5个位置,并插入新的字符串welcome
s1.replace(offindex1, strlen("hello"), "welcome");
//从后面的位置开始
offindex1 += strlen("welcome");
//继续查找
offindex1 = s1.find("hello", offindex1);//第一个参数是查找的字符串,第二个参数是开始往后查找的位置
}
cout << "替换后的字符串:" << s1 << endl;

string区间删除和插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//string区间删除和插入
string s1 = "hello1 world!";
//1、删除字符串中的'1'
//先通过find函数,查找'1'所在的迭代器位置
//string::iterator it = find(s1.begin(), s1.end(), '1');

////找到后进行删除
//if (it != s1.end())
//{
// s1.erase(it);
//}
//cout << s1 << endl;

//也可以使用s1.find(),这样返回的是字符串的位置,然后再使用erase
size_t index = s1.find('1', 0);//寻找'1',起始位置是0
if (index == string::npos)
{
cout << "not found!" << endl;
}
else
{
s1.erase(index, 1);//erase(int pos,int num),指定位置和删除的长度,如果不设置长度,则在此位置之后的全部删除
cout << s1 << endl;
}

//2、删除起始迭代器位置的字符
s1.erase(s1.begin(), s1.begin() + 3);//删去前三个字符
cout << s1 << endl;

//3、在0位置处插入"AAA"
s1.insert(0, "AAA");
cout << s1 << endl;

operator运算符重载

string全部大小写转换

需要先include algorithm

1
2
3
4
5
6
7
8
9
10
//string大小写转换
string s1 = "abcdefg";
string s2 = "ABCDEFGHIJKL";

//小写全部转换成大写,转换的结果放在s1.begin()的位置,后面的操作需要强制转换成指定的函数类型
transform(s1.begin(), s1.end(), s1.begin(), ::toupper);
cout << s1 << endl;
//大写全部转换为小写
transform(s2.begin(), s2.end(), s2.begin(), ::tolower);
cout << s2 << endl;

Vector

vector是将元素放到动态数组中加以管理的容器。vector容器可以随机存取元素,可以使用[]运算和at方式存取

vector在尾部添加或者移除元素非常快,在中间操作时非常耗时,因为需要移动元素

vector基本用法和遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include<vector>
using namespace std;
int main()
{
//定义一个vector容器,类型为int
vector<int> v1;

//插入元素,尾插法
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);

//迭代器遍历打印
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++)
{
cout << *it << ' ';
}

//修改头部元素的值(front()返回是引用,可以当左值)
v1.front() = 44;
//输出头部元素
cout << endl << "头部元素为:" << v1.front() << endl;
//使用[]运算符遍历
for (int i = 0; i < v1.size(); ++i)
{
cout << v1[i] << ' ';
}
//修改尾部的值(back()返回是引用,可以当左值)
v1.back() = 99;

//输出尾部元素
cout << endl << "尾部元素为:" << v1.back() << endl;

//删除元素,尾部删除
v1.pop_back();

//使用at方法遍历
for (int i = 0; i < v1.size(); ++i)
{
cout << v1.at(i) << ' ';
}

return 0;
}

vector初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//直接构造函数初始化
vector<int>v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);

//通过拷贝构造函数进行初始化
vector<int>v2 = v1;

//使用部分元素来构造
vector<int>v3(v1.begin(), v1.begin() + 1);
vector<int>v4(v1.begin(), v1.end());
//存放三个元素,每个元素都是9
vector<int>v5(3, 9);

vector的push_back强化

push_back是在当前vector的内存末尾拷贝元素进入容器。注意这个地方可能产生浅拷贝,所以容器中的对象要支持拷贝操作。另外,如果vector初始化了个数,而不初始化具体的值,push_back也只会在最后面追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//vector的push_back强化,push_back是在当前vector的内存末尾拷贝元素进入容器
//初始化10个元素的容器
vector<int> v(10);

//打印容器大小
cout << v.size() << endl;

//push_back添加元素
v.push_back(100);

//打印容器大小
cout << v.size() << endl;

for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
{
cout << *it << ' ';
}
return 0;
1
2
3
10
11
0 0 0 0 0 0 0 0 0 0 100

vector的元素删除和插入

vector的删除,是根据位置进行删除,如果想删除某个元素,需要找到当前元素的迭代器位置,再进行删除

删除之后后面的元素依次前移,相当于迭代器自动指向下一位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//创建容器并进行初始化
vector<int>v(10);
for (int i = 0; i < v.size(); ++i)
{
v[i] = i;
}

//区间删除
//1、删除前三个元素
v.erase(v.begin(), v.begin() + 3);
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
{
cout << *it << ' ';
}
cout << ' ' << endl;
//2、删除指定位置的元素,删除之后v1已经变为删除元素后的vector
v.erase(v.begin() + 3);
for (int i = 0; i < v.size(); ++i)
{
cout << v.at(i) << ' ';
}
cout << ' ' << endl;

v.push_back(2);
v.push_back(2);
for (int i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}
cout << ' ' << endl;
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it == 2)
{
it = v.erase(it);//删除后,迭代器指针会执行下一个位置并且返回
}
else {
it++;
}
}

for (int i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}
cout << endl;
//在指定的位置插入元素10的拷贝
v.insert(v.begin() + 3, 11);//第一个参数是位置,第二个参数是插入的元素
for (int i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}
cout << endl;
//在指定位置插入指定个数的元素
v.insert(v.begin(), 3, 11);
for (int i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}

deque

deque是一个双端数组容器,可以在头部和尾部操作元素