selph
selph
Published on 2021-09-30 / 587 Visits
0
0

逆向分析:C++虚函数和继承

虚函数

有虚函数情况下的构造函数

代码:

#include <stdio.h>

class Person {
public:
	virtual int getAge() {
		return age;
	}
	virtual void setAge(int age) {
		this->age = age;
	}
private:
	int age;
};

int main(int argc, char* argv[]) {
	Person person;
	return 0;
}

分析:

构造函数:

image-20210929150020091

虚函数调用

代码:

#include <stdio.h>

class Person {
public:
	virtual int getAge() {
		return age;
	}
	virtual void setAge(int age) {
		this->age = age;
	}
private:
	int age;
};

int main(int argc, char* argv[]) {
	Person person;
	person.setAge(20);
	printf("%d\n", person.getAge());
	return 0;
}

分析:

主函数:构造函数,通过thiscall调用成员函数,看起来没啥不一样的

image-20210929150530263

setAge函数:把参数赋值给成员变量,这里要注意,对象地址的第一个位置是vftable,第一个成员变量的位置在4个字节后

image-20210929150608559

有虚函数情况下的虚构函数

代码:

#include <stdio.h>

class Person {
public:
	~Person() {
		printf("~Person()\n");
	}
	virtual int getAge() {
		return age;
	}
	virtual void setAge(int age) {
		this->age = age;
	}
private:
	int age;
};

int main(int argc, char* argv[]) {
	Person person;
	person.setAge(20);
	printf("%d\n", person.getAge());
	return 0;
}

分析:

析构函数:这里把虚表拿出来,是为继承关系做准备,没继承关系则无事发生,有继承关系,需要把虚表指针指向对应的子类或父类

有继承关系的析构中调用虚函数,有继承的类析构顺序是:子类析构->父类析构......因此在执行析构时需要将虚表指针指向对应的子类或父类的虚表

image-20210929151443307

继承

单继承

代码:

派生类对象调用了派生类的函数

#include <stdio.h>

class Base {	//基类定义
public:
	Base() {
		printf("Base\n");
	}
	~Base() {
		printf("~Base\n");
	}
	void setNumber(int n) {
		base = n;
	}
	int getNumber() {
		return base;
	}
public:
	int	base;
};

class Derive : public Base {  //派生类定义
public:
	void showNumber(int n) {
		setNumber(n);
		derive = n + 1;
		printf("%d\n", getNumber());
		printf("%d\n", derive);
	}
public:
	int derive;
};

int main(int argc, char* argv[]) {
	Derive derive;
	derive.showNumber(argc);
	return 0;
}

分析:

主函数:主函数主要就两个地方,一个是构造函数,一个是成员函数的调用

image-20210929184136120

构造函数:因为子类没有构造函数,所以这里就只调用了父类的构造函数

image-20210929184218775

成员函数:调用父类函数,父类函数内部给对象第一个成员赋值,后面给子类成员变量赋值则是给对象地址第二个成员赋值,说明对象地址里按顺序依次存放的是父类成员,然后子类成员

image-20210929184307626

派生多子类

代码:

#include <stdio.h>

class Person {	// 基类—" 人 " 类
public:
	Person() {}
	virtual ~Person() {}
	virtual void showSpeak() {} // 纯虚函数,后面会讲解
};

class Chinese : public Person {	// 中国人:继承自人类
public:
	Chinese() {}
	virtual ~Chinese() {}
	virtual void showSpeak() {	// 覆盖基类虚函数
		printf("Speak Chinese\r\n");
	}
};

class American : public Person { //美国人:继承自人类
public:
	American() {}
	virtual ~American() {}
	virtual void showSpeak() { //覆盖基类虚函数
		printf("Speak American\r\n");
	}
};

class German : public Person { //德国人:继承自人类
public:
	German() {}
	virtual ~German() {}
	virtual void showSpeak() { //覆盖基类虚函数
		printf("Speak German\r\n");
	}
};

void speak(Person* person) { //根据虚表信息获取虚函数首地址并调用
	person->showSpeak();
}

int main(int argc, char* argv[]) {
	Chinese chinese;
	American american;
	German german;
	speak(&chinese);
	speak(&american);
	speak(&german);
	return 0;
}

分析:

主函数:连续三个构造函数,然后连续三个speak函数调用,分别传入这三个对象的首地址

image-20210929185604565

对象1的构造函数:取出对象首地址,调用父类构造函数,因为父类有虚函数,所以这里父类构造函数会给虚表赋值,因为子类重写父类虚函数,所以子类虚表覆盖对象首地址父类虚表

image-20210929185644623

speak函数:从虚表里找到第二个函数,然后拿来调用,为什么是第二个函数呢,看源代码可以发现,子类成员方法有两个是虚方法,当前调用的这个是第二个声明的

image-20210929185755559

多重继承

代码:

#include <stdio.h>

class Sofa {
public:
	Sofa() {
		color = 2;
	}

	virtual ~Sofa() {	// 沙发类虚析构函数
		printf("virtual ~Sofa()\n");
	}

	virtual int getColor() {	// 获取沙发颜色
		return color;
	}
	virtual int sitDown() {	// 沙发可以坐下休息
		return printf("Sit down and rest your legs\r\n");
	}
protected:
	int color;	// 沙发类成员变量
};

//定义床类
class Bed {
public:
	Bed() {
		length = 4;
		width = 5;
	}

	virtual ~Bed() {  //床类虚析构函数
		printf("virtual ~Bed()\n");
	}

	virtual int getArea() { //获取床面积
		return length * width;
	}

	virtual int sleep() {  //床可以用来睡觉
		return printf("go to sleep\r\n");
	}
protected:
	int length;	//床类成员变量
	int width;
};

//子类沙发床定义,派生自 Sofa 类和 Bed 类
class SofaBed : public Sofa, public Bed {
public:
	SofaBed() {
		height = 6;
	}

	virtual ~SofaBed() {	//沙发床类的虚析构函数
		printf("virtual ~SofaBed()\n");
	}

	virtual int sitDown() {	//沙发可以坐下休息
		return printf("Sit down on the sofa bed\r\n");
	}

	virtual int sleep() {	//床可以用来睡觉
		return printf("go to sleep on the sofa bed\r\n");
	}

	virtual int getHeight() {
		return height;
	}

protected:
	int height;
};


int main(int argc, char* argv[]) {
	SofaBed sofabed;
	return 0;
}

分析:

主函数:这里主要看构造函数和析构函数是怎么工作的

image-20210929192102209

构造函数:这里定义的顺序调用父类的构造函数,第一个父类占地址是8字节,第二个父类构造函数则从对象地址+8开始构造,然后子类的构造首地址接着上一个父类构造地址的末尾进行

父类构造函数里会给首地址赋予父类虚表,最后子类构造函数会把所有父类虚表都换成子类的虚表

构造函数的功能就是填充一片内存

image-20210929192126166

析构函数:跟调用构造函数的顺序相反,随便进一个父类析构函数(见下面)

image-20210929192325852

父类析构函数:在父类析构函数里,会把虚表覆盖成原父类的虚表

image-20210929192409107

构造和析构的流程是相反的,这里的流程包括了执行顺序和对虚表的操作

  • 构造:先父类构造,再子类构造,然后把虚表全部覆盖成当前子类的
  • 析构:先把虚表全部替换成当前子类的,然后子类析构,再父类析构

子类对象转父类指针

代码:

#include <stdio.h>

class Sofa {
public:
	Sofa() {
		color = 2;
	}

	virtual ~Sofa() {	// 沙发类虚析构函数
		printf("virtual ~Sofa()\n");
	}

	virtual int getColor() {	// 获取沙发颜色
		return color;
	}
	virtual int sitDown() {	// 沙发可以坐下休息
		return printf("Sit down and rest your legs\r\n");
	}
protected:
	int color;	// 沙发类成员变量
};

//定义床类
class Bed {
public:
	Bed() {
		length = 4;
		width = 5;
	}

	virtual ~Bed() {  //床类虚析构函数
		printf("virtual ~Bed()\n");
	}

	virtual int getArea() { //获取床面积
		return length * width;
	}

	virtual int sleep() {  //床可以用来睡觉
		return printf("go to sleep\r\n");
	}
protected:
	int length;	//床类成员变量
	int width;
};

//子类沙发床定义,派生自 Sofa 类和 Bed 类
class SofaBed : public Sofa, public Bed {
public:
	SofaBed() {
		height = 6;
	}

	virtual ~SofaBed() {	//沙发床类的虚析构函数
		printf("virtual ~SofaBed()\n");
	}

	virtual int sitDown() {	//沙发可以坐下休息
		return printf("Sit down on the sofa bed\r\n");
	}

	virtual int sleep() {	//床可以用来睡觉
		return printf("go to sleep on the sofa bed\r\n");
	}

	virtual int getHeight() {
		return height;
	}

protected:
	int height;
};


int main(int argc, char* argv[]) {
	SofaBed sofabed;
	Sofa *sofa = &sofabed;
	Bed *bed = &sofabed;
	return 0;
}

分析:

主函数:子类对象转父类指针,实际上就是把父类构造用的那个首地址拿出来给父类指针操作

image-20210929193257914

总结

有虚函数的情况下,对象地址里首地址存储虚表指针

派生类的情况下,子类会依次调用父类的构造函数进行构造,然后再进行子类构造,如果父类有虚函数,则构造里会出现父类的虚函数

派生类的对象地址里会依次存放定义顺序父类成员最后才是当前派生类成员,里面会出现多个虚表,但都会被派生类的虚表覆盖


Comment