zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++逆向分析——继承与封装

C++封装继承 分析 逆向
2023-09-14 09:11:45 时间

面向对象程序设计之继承与封装

之前已经学习过继承和封装了,但是要在实际开发中使用,光学语法和原理是不够的,在设计层面我们需要做一些优化。

如下代码是继承的例子:

#include <stdio.h>
 
class Person {
public:
int Age;
int Sex;
 
void Work() {
printf("Person:Work()");
}
 
};
 
class Teacher:public Person {
public:
int Level;
};
 
void main() {
Teacher t;
t.Age = 10;
t.Sex = 1;
t.Level = 2;
 
t.Work();
 
return;
}

当t.Age=-1,这在代码层面(语法)是合法的,但是不合理,因为人的年龄不可能是负数;所以从设计层面以上代码就不正确、不合理。

所以,我们可以将不想被外界访问的成员隐藏起来,也就是使用private关键词:

#include <stdio.h>
 
class Person {
private:
int Age;
int Sex;
public:
void Work() {
printf("Person:Work()");
}
 
};
 
class Teacher:public Person {
private:
int Level;
};

但是这样,如上代码就会出现问题,因为我们没法直接访问到成员,因此从设计层面出发设计这个,我们可以提供按钮或者说一个函数用来控制这些值:

#include <stdio.h>
 
class Person {
private:
int Age;
int Sex;
public:
void Work() {
printf("Person:Work()");
}
void SetAge(int Age) {
this->Age = Age;
}
void SetSex(int Sex) {
this->Sex = Sex;
}
 
};
 
class Teacher:public Person {
private:
int Level;
public:
void SetLevel(int Level) {
this->Level = Level;
}
};

而后我们可以通过函数去设置这些值,那有人就会问了,你这样不还是可以输入-1吗?是的,是可以输入,单同样,我们可以在成员函数内做条件判断来控制输入的内容:

void SetAge(int Age) {
if (Age > 0) {
this->Age = Age;
} else {
this->Age = 0;
}
}

用成员函数控制就不会存在别人想要调用这个类的时候存在合法不合理的情况了,其根本的目的就是可控。(数据隐藏)

除了成员数据(变量)以外,还有一些提供给自己用的成员函数也要隐藏。

但这样随之而来的问题也就产生了,一般情况下,我们是想要在创建对象的时候就赋值了,也就是说我们使用构造函数去赋值,那这时候如果父类存在构造函数,使用子类创建对象的时候,子类默认会调用父类无参的构造函数,也就是说父类如果存在有参的构造函数被继承,就必须要有无参的构造函数。

所以一个好的习惯:当你写一个类的时候,就应该写一个无参的构造函数

class Person {
private:
int Age;
int Sex;
public:
Person() {
}
Person(int Age, int Sex) {
this->Age = Age;
this->Sex = Sex;
}
void Work() {
printf("Person:Work()");
}
 
};
 
class Teacher:public Person {
private:
int Level;
public:
Teacher() {
}
Teacher(int Level) {
this->Level = Level;
}
};

如上代码,调用Teacher创建对象,我们想通过构造函数赋值Age和Sex该怎么办?第一时间想到的时候使用this调用,但是这里是继承父类的,肯定不行。

C++也提供了这种情况下的语法:

Teacher(int Age, int Sex, int Level):Person(Age, Sex) {
this->Level = Level;
}

在子类有参构造函数中加入参数列表,而后在括号后门加上冒号跟上父类有参构造函数,传入变量即可。

有些人就疑问了,为什么这种写法不可以呢?

Teacher(int Age, int Sex, int Level) {
Person(Age, Sex); 
this->Level = Level;
}

这只有利用反汇编代码来解释了:

images/download/attachments/12714553/image2021-4-10_0-20-53.png

如上反汇编代码,可以很清楚的看见当我们不使用那种方法还是会调用一遍父类无参的构造函数,接着手动添加的构造函数,编译器会把堆栈中临时分的对象赋值,但是当我们这段构造函数执行完成之后就没了,所以没有任何意义。

 

注意:对于继承类的构造函数,如果没显示调用,则编译器默认会调用父类person的构造函数。

#include <stdio.h>

class Person {
private:
	int Age;
	int Sex;
public:
	Person() {
		Age = 0;
		Sex = 0;
		printf("default person ctor invoked.\n");
	}
	Person(int Age, int Sex) {
		this->Age = Age;
		this->Sex = Sex;
	}
	void Work() {
		printf("Person:Work()");
	}

};

class Teacher :public Person {
private:
	int Level;
public:
	Teacher() {
		Level = 0;
		printf("default teacher ctor invoked.\n");
	}

	Teacher(int Level) {
		this->Level = Level;
	}

	Teacher(int Age, int Sex, int Level) :Person(Age, Sex) {
		this->Level = Level;
	}
};

void main() {
	// Teacher t; // (40, 0, 3);
	Teacher t(0x18);
	t.Work();

	return;
}

上述代码输出为:

default person ctor invoked.
Person:Work()

虽然没有在

Teacher(int Level) {
		this->Level = Level;
	}
这里面去显示调用父类的构造函数,但是编译器依然调用了默认构造函数。

面向对象程序设计之多态

C++是一门面向对象的编程语言,所有的面向对象语言都有一个特征:封装、继承、多态;之前已经了解过封装、继承了,这里来了解一下多态。

所有的面向对象的编程语言在设计的时候都是为了解决一个问题,那就是避免重复造轮子,也就是避免写2遍重复的代码,我们也可以称之为代码复用,其体现方式有2种:1.继承;2.共用相同的函数。

现在我们有一个需求,需要打印对象的成员变量,如下代码:

#include <stdio.h>
 
class Person {
private:
int Age;
int Sex;
public:
Person() {
}
Person(int Age, int Sex) {
this->Age = Age;
this->Sex = Sex;
}
void Print() {
printf("%d \n", this->Sex);
}
 
};
 
class Teacher:public Person {
private:
int Level;
public:
Teacher() {
}
Teacher(int Age, int Sex, int Level):Person(Age, Sex) {
this->Level = Level;
}
};
 
void PrintPerson(Person& p) {
p.Print();
}

我们创建了一个PrintPerson函数来调用Person的Print函数,但是在这里如果我们想要打印Teacher的成员呢?那就需要创建2个打印函数了,也就是违背了面向对象的初衷,重复造轮子了。

在C++中我们可以使用父类的指针来指向子类的对象:

void main() {
Person p(1,3);
Teacher t(1,2,3);
 
Person* px = &t;
 
return;
}

如下图我们可以很清晰的看见内存的结构,当我们形容子类B内存结构的时候,一定是有三个成员的,而不是一个成员z,当我们创建A*指针的时候指向的是子类对象的首地址,通过这个指针可以访问x、y,刚好子类对象B的开始位置是父类类型对象的第一个成员,所以我们可以使用父类类型的指针指向子类类型对象;但是反之(子类类型的指针指向父类类型的对象)我们却不可以,这是因为使用父类类型的指针指向子类类型对象有一个弊端,那就是没法访问子类类型的z,反过来的话,父类类型对象的成员只有x、y没有z,所以我们通过子类类型指针访问的时候是可以访问到三个成员的:x、y、z,但实际上父类对象是没有z的,那么在访问的过程中就会存在问题。==》但是我们学了继承的内存布局,使用指针也是有手段可以访问到的。见后:

images/download/attachments/12714553/image2021-4-11_23-53-55.png

通过父类指针,访问子类成员的方法,核心是利用继承的内存布局,base指针:

#include <stdio.h>

class Person {
public:
	int Age;
	int Sex;
	Person() :Age(30), Sex(1) {}
};

class Teacher :public Person {
public:
	int Level;
	Teacher():Level(99){}
};

void main() {
	Teacher t;
	int* p = (int *) & t;
	printf("age=%d sex=%d level=%d\n", *(p), *(p+1), *(p+2));
	return;
}

 输出:age=30 sex=1 level=99

 

 

 

所以我们可以只保留PrintPerson函数,而不再去重复造轮子:

images/download/attachments/12714553/image2021-4-12_0-23-19.png

如上代码仅仅是为了解决这种问题而举例的,所以代码严谨性可以忽略。

但是这样的弊端,就很清楚了,就是我们通过父类类型的指针指向子类类型的对象,是无法访问到子类类型自己本身的成员,只能访问到继承父类类型的成员。

所以这个还是无法满足我们的实际需求,那我们想不改变原有PrintPerson函数的情况下,只有在子类中重写Print函数才能到达需求(函数重写):

class Teacher:public Person {
private:
int Level;
public:
Teacher() {
}
Teacher(int Age, int Sex, int Level):Person(Age, Sex) {
this->Level = Level;
}
void Print() {
Person::Print();
printf("%d \n", this->Level);
}
};

Person::Print();是先调用父类的函数,但是在这里就可以打印了吗?实则不然:

images/download/attachments/12714553/image2021-4-12_0-22-38.png

我们可以看下反汇编代码,查看函数PrintPerson:

images/download/attachments/12714553/image2021-4-12_0-14-35.png

首先这里传递的是父类的引用类型,而后去调用的Print函数也是Person父类的,所以这样还是没法满足我们的需求。

我们可以使用一个关键词去解决这个问题,那就是在父类的Print函数类型前面加上virtual,则表示这是一个虚函数(其作用:当你PrintPerson函数传入的对象是子类就调用子类的,是父类就调用父类的):

images/download/attachments/12714553/image2021-4-12_0-24-8.png

这时候我们就可以引出多态的概念:多态就是可以让父类的指针有多种形态,C++中是通过虚函数实现的多态性

多种形态的表现,我们就已经在如上例子中说么了。

没有方法体的函数我们称之为纯虚函数,也就是说如下例子:

virtual int area() = 0;

纯虚函数

  1. 虚函数目的是提供一个统一的接口,被继承的子类重载,以多态的形式被调用;

  2. 如果父类中的虚函数可以任何意义,那么可以定义成纯虚函数;

  3. 含有纯虚函数的类被称之为抽象类,不能创建对象;

  4. 虚函数可以被直接调用,也可以被子类重写后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用。

虚函数和多态在下一节详细介绍其本质。