欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

C++ 中的类和对象 (I)

最编程 2024-05-04 07:47:55
...

C++中可以使用structclass来定义一个类

structclass 的区别

  • struct的默认成员权限是public
  • class的默认成员权限是private

struct 创建类对象

image.png

image.png

C++中创建对象并不像OC中需要Person *person = [Person new],而是只需要Person person就可以创建完成,同时person对象只占有4个字节的内存空间,因为Person类中只有一个int类型的成员变量

image.png

可以看到在汇编代码mov  dword ptr [ebp-0Ch],14h执行后其对应的内存已经把20这个值存放在了对应的4个字节中

class 创建类对象

image.png

调用方法都是一样的,只不过不一样的在于如果使用class来定义类,其中成员权限都默认是private如果外部有调用的话需要手动修改权限为public

struct 和 class 的不同

通过查看汇编代码来观察这两者有何不同

使用class image.png

使用struct

image.png

汇编代码是一样的,说明两者只是在成员权限上有所区别,但是实际开发中还是使用class居多

内存分配

刚才的例子中Person类只有一个成员变量,现在可以多放几个成员变量来观察

image.png

image.png

此处Person类中有三个成员变量,其占有内存12个连续的字节

this指针

对象访问和指针访问

这里分别用对象和指针去访问成员变量,看看有何不同 image.png

image.png

利用对象访问时很容易看出汇编代码拿到person的首地址开始每次取4个字节来进行赋值

image.png

利用指针访问成员变量时,ebp-14hperson对象的地址,先将person的地址放入eax寄存器,再将eax寄存器的内容放入ebp-20h中,ebp-20h就是指针p的地址,然后每次从指针p中取出person的地址,同样每次偏移4个字节来访问成员变量

image.png

如何利用指针间接访问所指向对象的成员变量?

  1. 从指针中取出对象的地址

  2. 利用对象的地址 + 成员变量的偏移量计算出成员变量的地址

  3. 根据成员变量的地址访问成员变量的存储空间

指针访问成员变量的本质

接下来来看一个例子

image.png

image.png

这样会如何打印呢?答案是:10,30,40

首先可以明确使用指针其实并非指向person的首地址,而是指向了m_age所在的位置也就是从person的地址偏移了4个字节,那么通过指针访问m_idm_age时是从m_age的位置开始赋值和偏移4个字节再次赋值,也就是说30赋值给了m_age,40赋值给了m_height,而m_id并未得到修改所以依然是10


17: Person person;

    18: person.m_id = 10;

005A269F C7 45 EC 0A 00 00 00 mov         dword ptr [ebp-14h],0Ah  

    19: person.m_age = 20;

005A26A6 C7 45 F0 14 00 00 00 mov         dword ptr [ebp-10h],14h  

    20: person.m_height = 180;

005A26AD C7 45 F4 B4 00 00 00 mov         dword ptr [ebp-0Ch],0B4h



24: Person* p = (Person *) & person.m_age;

005A26BC 8D 45 F0             lea         eax,[ebp-10h]  

005A26BF 89 45 E0             mov         dword ptr [ebp-20h],eax  

    25: p->m_id = 30;

005A26C2 8B 45 E0             mov         eax,dword ptr [ebp-20h]  

005A26C5 C7 00 1E 00 00 00    mov         dword ptr [eax],1Eh  

    26: p->m_age = 40;

005A26CB 8B 45 E0             mov         eax,dword ptr [ebp-20h]  

005A26CE C7 40 04 28 00 00 00 mov         dword ptr [eax+4],28h  

    27: p->m_height = 50;

005A26D5 8B 45 E0             mov         eax,dword ptr [ebp-20h]  

005A26D8 C7 40 08 32 00 00 00 mov         dword ptr [eax+8],32h

通过汇编代码也可以轻松看出其问题所在,那么如果最后一句打印使用指针来调用会如何呢?

也即是p->display()

通过对象调用和使用指针调用两者的区别在于会影响隐藏参数this而导致不同,内部访问成员变量时其实是会使用this->m_id这样来使用,那么使用对象调用会传入person的地址,而指针调用会传入m_age的地址,那么结果就会打印30,40,50

image.png

在调用函数前,一个将地址ebp-14h(person)传给this指针,而另一个将ebp-10h(m_age)传给this指针,此后在访问成员变量时已经会发生不同,因为访问的地址并不一样后者比前者多偏移4个字节

注意

如果上面不对m_height进行赋值,则打印出来m_height会是一个非常小的负数

image.png

通过内存来看就是0xcccccccc,这里0xcccccccc其实是机器码int3中断的意思,主要目的是为了在误跳转到此处时防止执行危险的指令,所以一旦跳转到这里直接发生中断,也就是我们看到的断点,一般分配到空间的时候此处的数据是脏数据,需要将其抹掉,所以采用了这种方法。

同时在栈空间分配的时候也一样会使用0xcccccccc

在调用函数时,函数存放在代码区,或者说函数的机器码存放于代码区,但是执行函数要开辟栈空间,因为代码区是只读的,而且只存放函数的机器码,而函数中的变量需要存储空间,所以是需要在开辟栈空间的。

内存空间

每个应用都有自己的独立内存空间,其内存空间大致分为以下几大区域:

  • 代码段:用于存放代码
  • 数据段:用于存放全局变量等
  • 栈空间:每调用一个函数就会分配一段连续的栈空间,等函数调用完成后系统自动回收这段空间
  • 堆空间:需要主动申请和释放

堆空间

malloc

image.png

通过malloc申请了16个字节的堆空间,并用指针偏移来赋值

image.png

再将上述例子中char * 换成 int *

image.png

image.png

因为指针类型的不同,每次偏移的量也不同,char * 类型 每次只取1个字节来写入而 int * 每次取4个字节

而且这段代码是在函数中的,所以每次函数调用结束时指针p将被销毁而堆空间分配的内存则不会,只是没有指针再指向它,可能会造成内存泄露,所以使用malloc时需要搭配使用free来及时释放堆空间

new/delete

堆空间的申请和释放总是成对出现的,上面演示了使用malloc,除了它还有另外的方法就是new

申请一个int类型的堆空间

int* p = new int;

*p = 10;


delete p;

接下来演示申请一段连续的空间,如下面代码所示即为申请了一段int型的数组空间

int* p = new int[4];

*p = 10;

*(p + 1) = 20;

*(p + 2) = 30;

*(p + 3) = 40;


delete[] p;

总结

  • mallocfree 配对使用
  • newdelete 配合使用
  • 如果是申请一段内存空间则是 new[]delete[] 配合使用

堆空间的初始化

int size = sizeof(int) * 10;

int* p = (int *)malloc(size);

直接使用malloc来申请堆内存是并未对其空间进行初始化的,可以通过汇编和查看内存看到并未对空间进行初始化

image.png

如果是我们需要对堆空间进行初始化的话建议使用memset函数

int size = sizeof(int) * 10;

int* p = (int *)malloc(size);

memset(p, 1, size);

使用memset的效果是从指针p指向的内存空间开始的40个字节中每一个字节都初始化为1,如图所示

image.png

int* p0 = new int;//未初始化

int* p1 = new int();//初始化为0

int* p2 = new int(5);//初始化为5

int* p4 = new int[3];//数组未被初始化

int* p5 = new int[3]();//数组元素被初始化为0

int* p6 = new int[3]{};//数组元素被初始化为0

int* p7 = new int[3]{ 10 };//数组首元素初始化为10,其他被初始化为0

对象的内存存放位置

对象的内存可以存放于3个地方:

  • 全局区(数据段):全局变量
  • 栈空间:函数内的局部变量
  • 堆空间:动态申请内存(malloc、new)

image.png

构造函数

构造函数(constructor):

  • 在对象创建的时候自动调用,一般用于完成对象的初始化操作
  • 函数名与类名同名,没有返回值,可以有参数,可以重载,可以有多个构造函数
  • 一旦定义了构造函数,必须要使用其中一个自定义的构造函数来初始化对象
  • 通过malloc分配的对象不会使用构造函数
  • 在某些特定的情况下,编译器才会为类生成空的无参的构造函数
struct Person {

    int m_age;



    Person() {

    m_age = 0;

    cout << "Person()" << endl;

    }



    Person(int age) {

    m_age = age;

    cout << "Person(int age)" << endl;

    }

};


    int main() {

        Person person1;

        Person person2(10);

        return 0;

    }


Person类中创建两个构造函数,在调用的时候一个不写参数,另一个带参数,那么创建对象的时候就会分别调用两个不同的构造函数,具体在汇编代码中可以看到是调用了两个不同的构造函数

image.png

另外如果使用 Person* person = (Person*)malloc(4);来创建对象的话并不会调用任何构造函数

image.png

如果这里将类中的构造方法都删除,这里发现并不会调用汇编默认生成的构造函数

image.png

但是如果给类中成员变量一个默认值,情况就会不一样了,编译器此时会添加一个默认的无参构造函数,同时在这个构造函数其中可以看到默认值20的存在

image.png

image.png

构造函数的调用

image.png

image.png

上图中的代码一共创建了7Person对象,其中调用了4个无参构造函数,3个有参构造函数,有2个仅为函数声明

编译器自动生成的构造函数

C++编译器在某些特定的情况下会给类自动生成无参的构造函数,比如:

  • 成员变量在声明的同时进行了初始化
  • 有定义虚函数
  • 虚继承了其他类
  • 包含了对象类型成员,且这个成员有构造函数(编译器生成或自定义)
  • 父类有构造函数(编译器生成或自定义)

推荐阅读