一、面向对象编程
笔者由于学习过JAVA,所以对面向对象编程已经有一些自己的理解,下面不会讲解太详细
1.结构体<=>类
-
先复习一下结构体作为参数传递:结构体作为参数传给函数,本质上是将结构体中所有成员复制一份传到函数的栈中。这样会比较浪费空间和时间
-
所以我们会将结构体指针作为参数传递给函数:
#include "stdafx.h" struct Base{ int x; int y; }; int Max(Base* basep){ //结构体指针传参 return basep->x + basep->y; } int main(int argc,char* argv[]){ Base base; base.x = 1; base.y = 2; printf("%d\n",Max(&base)); printf("%d",sizeof(base)); //打印一下结构体的大小 return 0; }
-
这里我们查看一下现在结构体的大小:8字节。现在如果把Max函数放到Base结构体中,再看看是否报错,并且查看此时结构体的大小:
#include "stdafx.h" struct Base{ int x; int y; int Max(Base* basep){ //把Max函数放到Base结构体中 return basep->x + basep->y; } }; int main(int argc,char* argv[]){ Base base; base.x = 1; base.y = 2; printf("%d\n",base.Max(&base)); //这里调用函数时就需要用“结构体变量.”的方式调用 printf("%d",sizeof(base)); //打印一下结构体的大小 return 0; }
- 函数作用范围受限:会发现完全可以把函数定义到结构体当中,但是在调用这个函数时需要把函数当成结构体中成员来调用:即
base.Max()
的方式来调用! - 结构体中函数不影响结构体大小:且发现结构体大小还是8字节!即Max函数当做结构体成员使用,但是却不包含在结构体当中
- 函数作用范围受限:会发现完全可以把函数定义到结构体当中,但是在调用这个函数时需要把函数当成结构体中成员来调用:即
-
综上:可以发现和面向对象的编程语言很像!结构体就是类,结构体中的变量就是成员变量,结构体中的函数就是成员方法,使用
Base base
可以创建类的对象base,使用对象可以调用类中的变量和方法等,这些就是封装的思想
2.成员方法的参数传递
-
见上述例子中:
base.Max(&base)
,调用Base类中的方法,查看一下反汇编:发现虽然Max方法只有一个参数,但是却传入了两个参数!我们来分析一下:①lea eax,[ebp-8]
和push eax
,这两个语句就是把base结构体变量的首地址使用栈的方式传递给了Max函数;②lea ecx,[ebp-8]
,这条语句就是把base结构体变量的首地址以寄存器传参的方式传递给了函数(因为进入进入函数中发现ecx中的值最终是被存储到了函数的栈中ebp-4的位置) -
思考:为什么明明只应该有&base这一个参数,但是编译器又自动传递了一个结构体变量首地址给函数?所以我们大胆尝试一下:如果在结构体中定义一个无参数的成员函数,编译器会怎么做呢?
#include "stdafx.h" struct Base{ int x; int y; int Max(){ return x + y; } }; int main(int argc,char* argv[]){ Base base; base.x = 1; base.y = 2; printf("%d\n",base.Max()); //3 return 0; }
使用反汇编再查看一下:发现即使类中的方法没有设置参数,编译器还是会自动把所属结构体变量的首地址作为参数传递给函数;而且即使函数中没有指明x,y是哪里来的,但是编译器会根据传入的结构体变量首地址去找结构体当中的x和y
-
综上:编译器调用类中的成员方法时,会自动传递一个参数,且这个参数就是此结构体变量的首地址,我们把这个指针类型变量就叫做==this指针==
-
所以和C语言相比如果按照C++的语法格式和思想方法,定义类、成员变量、成员方法等,编译器会帮我们添加很多额外的功能,节省很多时间,代码编写也会便利很多。我们需要学的就是使用C++时,编译器帮我们做了哪些事情,使开发效率更高更便捷
3.this指针的使用
-
既然this指针指向的是当前结构体变量的首地址,那么我们就可以按照最开始
&base
的使用方式使用this指针,比如:#include "stdafx.h" struct Base{ int x; int y; int Add(){ return this->x + this->y; //this-> } }; int main(int argc,char* argv[]){ Base base; base.x = 1; base.y = 2; printf("%d\n",base.Add()); return 0; }
使用结构体指针的方式使用类中的成员:
this->x
,但是这样做好像有点多余,那么this指针什么时候用呢? -
当成员方法的参数和所在类中的成员变量同名时,使用this来区分这个变量是成员变量
#include "stdafx.h" struct Base{ int x; int y; void Init(int x,int y){ //定义初始化成员变量的方法 this->x = x; this->y = y; } }; int main(int argc,char* argv[]){ Base base; base.Init(1,2); //直接使用Init方法对成员变量赋值 return 0; }
this->x = x
就区分了左边的x是成员变量,右边是传进来的参数,即将传进来的参数x的值赋给成员变量x -
还可以使用this指针,返回当前结构体变量的首地址
#include "stdafx.h" struct Base{ int x; int y; int getAddress(){ return *((int*)this); //返回当前结构体变量首地址 } }; int main(int argc,char* argv[]){ Base base; base.getAddress(); return 0; }
因为this指针指向当前结构体变量的首地址,即==指向当前对象的首地址==,且this是一个
结构体*
类型的变量,所以要将当前对象的首地址以int类型返回,就需要先把Base*
转成int*
,再加一个*
,把这个地址值取出来 -
注意:前面C语言学过的各种指针类型变量都可以做++,–,+/-一个数,比较等操作,但是this指针不允许重新赋值!
4.成员方法压栈顺序和堆栈平衡
-
编写一个普通函数和类的成员方法,分别调用这两个函数比较区别:
#include "stdafx.h" struct Base{ int x; int y; void Init(int x,int y){ //定义成员方法 this->x = x; this->y = y; } }; int method(int x,int y){ //定义函数 return x + y; } int main(int argc,char* argv[]){ Base base; base.Init(1,2); method(1,2); return 0; }
-
通过反汇编发现:
-
一般函数采用栈传递参数,参数从右到左压栈,使用外平栈(即cdecl调用约定);
-
成员方法参数采用栈传递参数,参数从右到左压栈,但是使用内平栈,且this指针参数最后一个传递,并采用寄存器的方式传递给函数(thiscall调用约定)
-
二、作业
1.设计一个类
-
要求设计一个类,有两个Int类型的成员x,y,并在类内部定义4个函数分别实现对x,y的加法、减法、乘法与除法的功能
#include "stdafx.h" struct Calculator{ int x; int y; void Init(int x,int y){ this->x = x; this->y = y; } int add(){ return this->x + this->y; } int sub(){ return x - y; } int mul(){ return x * y; } int div(){ return this->x / this->y; } }; int main(int argc,char* argv[]){ Calculator cal; cal.Init(4,2); printf("%d %d %d %d",cal.add(),cal.sub(),cal.mul(),cal.div()); return 0; }
2.空结构体
-
定义一个空结构体,查看其大小
#include "stdafx.h" struct Nothing{ }; int main(int argc,char* argv[]){ Nothing nothing; Nothing nothing1; Nothing nothing2; int size = sizeof(nothing); printf("%X %X %X",¬hing,¬hing1,¬hing2); printf("%d",size); return 0; }
-
会发现空结构体的大小为1字节。而且通过反汇编发现,这个1字节直接是编译器直接赋上去的!
-
而且就算是空的类(结构体),也可以生成对象!说明编译器肯定会给一个空结构体对象至少给1字节空间,否则如果空结构体真是空的,编译器凭什么还会给结构体对象分配内存空间。
-
通过反汇编可以发现:定义的三个类的对象,但是对象所在地址都不一样!第一个nothing对象首地址为ebp-4;第二个nothing1对象首地址为ebp -8;第三个nothing2对象首地址为ebp-0xC
-
3.空指针调用成员方法
-
判断下面两个代码能否正常执行,并说明理由
-
下面代码中:定义了一个空指针p,接着又用这个空指针p调用的成员函数,C++中是允许空指针调用成员函数的,那么
p->Fn_1();
到底是什么意思呢?- 我们知道当程序编译完成后,Person类中的成员函数
Fn_1()
和Fn_2()
的地址就已经确定了(因为此类所有的对象都共用一个成员函数!可以理解为一个全局变量的感觉),所以**p->Fn_1()
,就是相当于执行Person::Fn_1(this);
,且把this指向空**,而此时Fn_1()
中并没有使用到this!所以this空不空无所谓!Fn_1还是可以正常执行 - Fn_2同理
#include "stdafx.h" struct Person{ void Fn_1(){ printf("Person:Fn_1()\n"); } void Fn_2(){ printf("Person:Fn_2()%x\n"); } }; int main(int argc, char* argv[]){ Person* p = NULL; p->Fn_1(); p->Fn_2(); return 0; }
- 我们知道当程序编译完成后,Person类中的成员函数
-
而下面的代码:
Fn_1()
中同样还是没有使用this指针,所以按照上述理由,还是可以正常执行;但是Fn_2()
中,x = 10
相当于this->x = 10
,这里由于this指向空,但是又用空指针->x,而空指针的地址是0x00000000,->x的本质前面学过,就是从0x00000000开始向后取4字节的值,这个地址是非法访问的,所以会报错!(或者直接理解成不能使用空指针即可)#include "stdafx.h" struct Person{ int x; void Fn_1(){ printf("Person:Fn_1()\n"); } void Fn_2(){ x = 10; //这里相当于this->x = 10; printf("Person:Fn_2()%x\n"); } }; int main(int argc, char* argv[]){ Person* p = NULL; p->Fn_1(); p->Fn_2(); return 0; }
-
那我们把代码改一下:把person对象地址赋给结构体类型的p指针,再用p指针调用成员方法,此时
p->Fn_2()
,就相当于调用Fn_2()成员方法,并且将this指针指向person对象,此时就可以正常执行this.x = 10
了#include "stdafx.h" struct Person{ int x; void Fn_1(){ printf("Person:Fn_1()\n"); } void Fn_2(){ x = 10; printf("Person:Fn_2()%x\n"); } }; int main(int argc, char* argv[]){ Person* p = NULL; p->Fn_1(); Person person; person.Fn_2(); //这是正常用对象调成员函数 p = &person; //把person对象地址赋给结构体类型的p指针 p->Fn_2(); //此时p->Fn_2(),就相当于调用Fn_2()成员方法,并且将this指针指向person对象 return 0; }