首页 > 编程语言 >C++逆向分析——继承与封装

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

时间:2023-08-02 18:34:16浏览次数:36  
标签:逆向 封装 Level int Age C++ Sex Person Teacher

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

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

如下代码是继承的例子:

#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;

}

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

C++逆向分析——继承与封装_构造函数

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

 

注意:对于继承类的构造函数,如果没显示调用,则编译器默认会调用父类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的,那么在访问的过程中就会存在问题。==》但是我们学了继承的内存布局,使用指针也是有手段可以访问到的。见后:

C++逆向分析——继承与封装_父类_02

通过父类指针,访问子类成员的方法,核心是利用继承的内存布局,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函数,而不再去重复造轮子:

C++逆向分析——继承与封装_构造函数_03

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

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

所以这个还是无法满足我们的实际需求,那我们想不改变原有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();是先调用父类的函数,但是在这里就可以打印了吗?实则不然:

C++逆向分析——继承与封装_父类_04

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

C++逆向分析——继承与封装_父类_05

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

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

C++逆向分析——继承与封装_父类_06

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

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

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

virtual int area() = 0;

纯虚函数

  1. 虚函数目的是提供一个统一的接口,被继承的子类重载,以多态的形式被调用;
  2. 如果父类中的虚函数可以任何意义,那么可以定义成纯虚函数;
  3. 含有纯虚函数的类被称之为抽象类,不能创建对象;
  4. 虚函数可以被直接调用,也可以被子类重写后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用。

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

标签:逆向,封装,Level,int,Age,C++,Sex,Person,Teacher
From: https://blog.51cto.com/u_11908275/6941566

相关文章

  • C++逆向分析——引用
    voidmain(){intx=1;int&ref=x;ref=2;printf("%d\n",ref);return;}反汇编代码:intx=1;00724A5FC745F401000000movdwordptr[x],1int&ref=x;00724A668D45F4lea......
  • C++逆向分析——类成员的访问控制
    类成员的访问控制课外→好的编程习惯:定义与实现分开写,提升代码可读性。如下代码,Student这个类的所有成员我们都可以调用,但是我们不想让被人调用Print1这个方法该怎么?structStudent{intage;intsex;voidPrint1(){printf("FuncPrint1");}voidPrint(){......
  • C++逆向分析——new和delete new[]和delete[]
    在堆中创建对象我们可以在什么地方创建对象?全局变量区,在函数外面在栈中创建对象,也就是函数内在堆中创建对象注意:之前一直提到的堆栈实际上是两个概念->堆、栈,我们之前所讲的就是栈,从本章开始要严格区分。在C语言中,我们可以通过一个函数去申请一块内存,就是malloc(N);申请的这一块内存......
  • C++逆向分析——继承
    继承structPerson{intage;intsex;};structTeacher{intage;intsex;intlevel;intclassId;};如上代码中可以看见,Teacher类与Person类都存在着相同的2个成员age和sex,那么这就相当于重复编写了,我们可以通过继承的方式避免这样重复的编写(当前类名称:要......
  • C++逆向分析——this指针
    this指针概述C++是对C的拓展,C原有的语法C++都支持,并在此基础上拓展了一些语法:封装、继承、多态、模板等等。C++拓展新的语法是为了让使用更加方便、高效,这样就需要编译器多做了很多事情,接下来我们就需要一一学习这些概念。封装之前我们学习过结构体这个概念,那么结构体可以做参数传......
  • C语言逆向——预处理之宏定义、条件编译与文件包含
    预处理之宏定义、条件编译与文件包含预处理一般是指在程序源代码被转换为二进制代码之前,由预处理器对程序源代码文本进行处理,处理后的结果再由编译器进一步编译。预处理功能主要包括宏定义、文件包含、条件编译三部分。宏定义简单的宏:#define标识符字符序列#defineFALSE0#d......
  • C语言逆向分析——Switch语句,为何大多数情况较if语句更高效?就是因为查找表
    Switch语句Switch语句也是分支语句的一种,其语法如下:switch(表达式){case常量表达式1:语句;break;case常量表达式:语句;break;case常量表达式:语句;break;......default:语句;break;}需要注意如下几点:表达式结束不能是浮点数case后的常量......
  • C语言逆向——数组和结构体,数组多维只是一个编译构造的假象,本质会转成一维数组,结构体
    数组数组是C语言中非常重要的一个概念,学习C语言主要就是两个知识点:数组、指针,学好这两个,那么你的C语言一定也会很好。什么是数组?或者说什么情况下我们需要使用数组,比如说我们需要定义一个人的年龄,我们可以定义一个变量来表示,但是如果我们需要定义三个人的年龄呢?那就需要三个变量来......
  • 逆向——字符与字符串,中文字符GB2312编码由来
    字符与字符串在之前的课程中我们了解到变量的定义决定两个事情,第一是决定存储的数据宽度,第二是决定了存储的数据格式,那么我们来看下下面的代码:inta=123;//变量x,数据宽度为4个字节,里面存储的是补码(在计算机系统中,数值一律用补码来存储)intfloatb=123.4F;//IEEE编码(浮点)......
  • 【C++数据结构】启航,打开新世界的大门!
    @TOC一、学习数据结构的原因学习数据结构对于计算机科学和软件开发非常重要,它提供了处理和组织数据的有效方法和技术。以下是几个学习数据结构的重要原因:提高问题解决能力:数据结构教会了我们如何选择和使用适当的数据结构来解决问题。了解各种数据结构的特性和性能可以帮助我们分......