类
在C++中可以使用struct和class来定义一个类
struct 和 class 的区别
struct的默认成员权限是publicclass的默认成员权限是private
struct 创建类对象
C++中创建对象并不像OC中需要Person *person = [Person new],而是只需要Person person就可以创建完成,同时person对象只占有4个字节的内存空间,因为Person类中只有一个int类型的成员变量
可以看到在汇编代码mov dword ptr [ebp-0Ch],14h执行后其对应的内存已经把20这个值存放在了对应的4个字节中
class 创建类对象
调用方法都是一样的,只不过不一样的在于如果使用class来定义类,其中成员权限都默认是private如果外部有调用的话需要手动修改权限为public
struct 和 class 的不同
通过查看汇编代码来观察这两者有何不同
使用class
使用struct
汇编代码是一样的,说明两者只是在成员权限上有所区别,但是实际开发中还是使用class居多
内存分配
刚才的例子中Person类只有一个成员变量,现在可以多放几个成员变量来观察
此处Person类中有三个成员变量,其占有内存12个连续的字节
this指针
对象访问和指针访问
这里分别用对象和指针去访问成员变量,看看有何不同
利用对象访问时很容易看出汇编代码拿到person的首地址开始每次取4个字节来进行赋值
利用指针访问成员变量时,ebp-14h是person对象的地址,先将person的地址放入eax寄存器,再将eax寄存器的内容放入ebp-20h中,ebp-20h就是指针p的地址,然后每次从指针p中取出person的地址,同样每次偏移4个字节来访问成员变量
如何利用指针间接访问所指向对象的成员变量?
-
从指针中取出对象的地址
-
利用对象的地址 + 成员变量的偏移量计算出成员变量的地址
-
根据成员变量的地址访问成员变量的存储空间
指针访问成员变量的本质
接下来来看一个例子
这样会如何打印呢?答案是:10,30,40
首先可以明确使用指针其实并非指向person的首地址,而是指向了m_age所在的位置也就是从person的地址偏移了4个字节,那么通过指针访问m_id和m_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
在调用函数前,一个将地址ebp-14h(person)传给this指针,而另一个将ebp-10h(m_age)传给this指针,此后在访问成员变量时已经会发生不同,因为访问的地址并不一样后者比前者多偏移4个字节
注意
如果上面不对m_height进行赋值,则打印出来m_height会是一个非常小的负数
通过内存来看就是0xcccccccc,这里0xcccccccc其实是机器码int3中断的意思,主要目的是为了在误跳转到此处时防止执行危险的指令,所以一旦跳转到这里直接发生中断,也就是我们看到的断点,一般分配到空间的时候此处的数据是脏数据,需要将其抹掉,所以采用了这种方法。
同时在栈空间分配的时候也一样会使用0xcccccccc
在调用函数时,函数存放在代码区,或者说函数的机器码存放于代码区,但是执行函数要开辟栈空间,因为代码区是只读的,而且只存放函数的机器码,而函数中的变量需要存储空间,所以是需要在开辟栈空间的。
内存空间
每个应用都有自己的独立内存空间,其内存空间大致分为以下几大区域:
代码段:用于存放代码数据段:用于存放全局变量等栈空间:每调用一个函数就会分配一段连续的栈空间,等函数调用完成后系统自动回收这段空间堆空间:需要主动申请和释放
堆空间
malloc
通过malloc申请了16个字节的堆空间,并用指针偏移来赋值
再将上述例子中char * 换成 int *
因为指针类型的不同,每次偏移的量也不同,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;
总结
malloc和free配对使用new和delete配合使用- 如果是申请一段内存空间则是
new[]和delete[]配合使用
堆空间的初始化
int size = sizeof(int) * 10;
int* p = (int *)malloc(size);
直接使用malloc来申请堆内存是并未对其空间进行初始化的,可以通过汇编和查看内存看到并未对空间进行初始化
如果是我们需要对堆空间进行初始化的话建议使用memset函数
int size = sizeof(int) * 10;
int* p = (int *)malloc(size);
memset(p, 1, size);
使用memset的效果是从指针p指向的内存空间开始的40个字节中每一个字节都初始化为1,如图所示
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)
构造函数
构造函数(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类中创建两个构造函数,在调用的时候一个不写参数,另一个带参数,那么创建对象的时候就会分别调用两个不同的构造函数,具体在汇编代码中可以看到是调用了两个不同的构造函数
另外如果使用 Person* person = (Person*)malloc(4);来创建对象的话并不会调用任何构造函数
如果这里将类中的构造方法都删除,这里发现并不会调用汇编默认生成的构造函数
但是如果给类中成员变量一个默认值,情况就会不一样了,编译器此时会添加一个默认的无参构造函数,同时在这个构造函数其中可以看到默认值20的存在
构造函数的调用
上图中的代码一共创建了7个Person对象,其中调用了4个无参构造函数,3个有参构造函数,有2个仅为函数声明
编译器自动生成的构造函数
C++编译器在某些特定的情况下会给类自动生成无参的构造函数,比如:
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承了其他类
- 包含了对象类型成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)