A-01-类和对象
面向对象的知识
又类就有对象,可以用来来创建对象
成员变量、成员函数
封装、继承、多态
我们不仅要了解语法,还要知道底层实现了什么,在内存中是什么样的
C++可以使用struct、class来定义一个类
而在C语言中struct是结构体只能定义变量,而在C++可以定义成员变量和成员函数
在C++中就认为struct是类
#include <iostream>
using namespace std;
struct Person{
//成员变量(属性)
int age;
//成员函数(方法)
void run(){
cout << "Person::run()" << age << endl;//在类中成员函数可以访问成员变量
}
};//定义类,此处分号不能少
int main(){
//利用类创建对象,访问成员变量和成员函数
Person person;
person.age = 10; //给成员变量赋值
person.run(); //调用成员函数
getchar();
return 0;
}
Struct和class的区别只在权限上有,其余完全一样
那么struct和class有什么区别呢?
如果用struct定义类,它的成员变量和成员函数默认是public,大家都可以访问,外面直接拿到对象,直接可以访问其内部成员
而class默认是private
下面我们将public的成员私有化
struct Person{
private://从此行开始往后的成员都是私有的
//成员变量(属性)
int age;
//成员函数(方法)
void run(){
cout << "Person::run()" << age << endl;
}
}
#struct默认成员public
struct Person{
}
#class默认成员private
class Person{
}
#同理,将class的成员私有化,私有的外面就不能访问了
class Person{
Pulic:
}
为了上课方便前期会使用struct,为了演示方便,这个默认就是公共的很容易
但在开发过程中尽量不要使用struct开发类,而要使用class定义类
既然class可以定义类,为什么还要用struct来定义类呢?为了让C语言可以过渡到C++,在以前没有面向对象的语言环境下,我们可以用C语言的struct模拟对象,这个就不详述了,此时有了C++就不用模拟了
我们编程时要有编程规范,利于编程
struct Person{
int m_age; #局部变量
void run(){
cout << "Person::run()" << m_age << endl;
}
};
int g_age; #全局变量
//利用类创建对象,访问成员变量和成员函数
Person person;
person.m_age = 10; //给成员变量赋值
person.run(); //调用成员函数
//上面可以person对象点访问,而指针访问成员,需要使用箭头->指向对象
Person *p = &person; #右边是Person类型的,所有左边指针也要写出Person
p->m_age = 20;
p->run();
注意上面定义的person对象和,指针P都在函数的栈空间
void test(){
//利用类创建对象,访问成员变量和成员函数
Person person;
person.m_age = 10; //给成员变量赋值
person.run(); //调用成员函数
//上面可以person对象点访问,而指针访问成员,需要使用箭头->指向对象
Person *p = &person;
p->m_age = 20;
p->run();
}
int main(){
test();
getchar();
return 0;
}
当main函数调用test函数时,就会给test函数分配一段连续的栈空间,定义的person对象和指针变量P,他们占用的内存都是test函数中的栈空间,一旦test函数调用完毕,此栈空间就会被回收,也就意味着person对象和指针变量P所占内存会被回收,对象和指针就没了
注意此处person对象占4个字节,因为定义的person类里面只有一个成员变量int(不管函数),而int占4个字节,故此person对象占4个字节
每创建一个对象,每个对象都有自己的成员变量,都会给不同对象的成员变量内存分配,而成员函数就没必要有这么多分了,函数在内存中只有一份,将来不同对象调用成员函数通过内存地址而找到这唯一一份就够了
函数只有一份内存,而这个内存不会塞到对象里面去,成员函数内存不会在对象里面,所以创建的对象里的成员函数不会占内存
函数没被调用就没有地址,这是编译器优化的结果
Struct和class的区别只在权限上有,其余完全一样
A-02-对象的内存
类中如果有多个成员变量,它的内存有时如何
struct Person {
int m_id;
int m_age;
int m_height;
void display(){
cout << "id = " << m_id
<< ",age = " << m_age
<< ",height = " << m_height << endl;
}
};
int main(){
//这个person对象内存在栈空间
Person person;
person.m_id = 1;
person.m_age = 2;
person.m_height = 3;
cout << "&person = " << &person << endl;
cout << "&person.m_id = " << &person.m_id << endl;
cout << "&person.m_age = " << &person.m_age << endl;
cout << "&person.m_height = " << &person.m_height << endl;
getchar();
return 0;
}
&person = 0113F720
&person.m_id = 0113F720
&person.m_age = 0113F724
&person.m_height = 0113F728
我们进入调试,在输出person地址处打断点,出来person的内存地址后,复制在调试的窗口,有内存窗口,将地址复制进去,右侧选择每列4个字节,从这个对象地址可以看到id,age的值,而且是小端模式,4个字节从右向左读
0x00DFFD20 01 00 00 00 ....
0x00DFFD24 02 00 00 00 ....
0x00DFFD28 03 00 00 00 ....
这里的person对象内存是在栈空间的,而以前学的面向对象内存在堆里,但是这个在栈空间,以后再告诉大家如何将对象放到堆里面去,既然对象内存在栈空间,那么它的内存我们就不用去管,因为栈空间的东西会自动回收
如果定义的对象person在函数里,就是局部变量,而局部变量的内存在栈空间,而如果定义的对象放在外面就是全局变量,全局变量的内存是在全局区(数据段)
内存对齐的问题这里就不详解
A-03-this
实际我们的的内存是分很多的区的
栈空间
堆空间
代码区
全局区
今天先理解代码区和栈区
struct Person{
int m_age;
/*void run(){
cout << "Person::run()" << m_age << endl;
}*/
void run(){
// this = &person1
}
};
int main(){
Person person1;
person1.m_age = 10;
person1.run();
Person person2;
person2.m_age = 20;
person2.run();
getchar();
return 0;
}
我们的函数无论是定义在类里面还是在外面如main函数,只要是函数,函数代码统一放在代码区
我们定义的person1和person2对象是局部变量,他们的内存都放在栈空间
我们看到了代码区的函数访问到了栈区的变量age
编译器给我们提供了一个指针this
我们如果通过person1对象调用run函数,就会将person1对象的地址值偷偷传给里面的this,同理,person2也是如此
void run(){
// this指针存储着函数调用者的地址
//this指向了函数调用者
}
void run(){
// this指针存储着函数调用者的地址
//this指向了函数调用者
cout << "Person::run()" << this->m_age << endl;//通过this可以间接访问person的成员m_age ,此处的this可以省略,表面上没有this,本质上是使用了this
}
此时就能明白不同对象调用同一个函数,函数里能访问不同对象的成员了
22: test();
00322C68 call test (03214ABh)
23:
24: Person person1;
25: person1.m_age = 10;
00322C6D mov dword ptr [person1],0Ah
26: person1.run();
00322C74 lea ecx,[person1]
00322C77 call Person::run (032128Fh)
我们可以发现调用普通函数test(),只需一句汇编,而调用成员函数需要两句汇编,其中第一句用来取的是对象person1的地址值
001F58B0 mov dword ptr [this],ecx
14: this->m_age = 3;
001F58B3 mov eax,dword ptr [this]
001F58B6 mov dword ptr [eax],3
将ecx中存储的person1的地址传给this指针,再从this指针将地址取出传给eax,再将3传给person1地址所对应的存储空间(也就是第一个成员变量age地址对应的存储空间)
This是指针所以,它如果访问成员变量必须用箭头->否则用点就会报错
点左边只能放对象,this不是对象,而是指向对象的指针
27: Person person1;
28: person1.m_id = 10;
00B544ED mov dword ptr [person1],0Ah
29: person1.m_age = 20;
00B544F4 mov dword ptr [ebp-10h],14h
30: person1.m_height = 30;
00B544FB mov dword ptr [ebp-0Ch],1Eh
31: person1.run();
00B54502 lea ecx,[person1]
00B54505 call Person::run (0B5128Fh)
33: Person *p=&person1;
00B5450A lea eax,[person1]
00B5450D mov dword ptr [p],eax
34: p->m_id=10;
00B54510 mov eax,dword ptr [p]
00B54513 mov dword ptr [eax],0Ah
35: p->m_age = 20;
00B54519 mov eax,dword ptr [p]
00B5451C mov dword ptr [eax+4],14h
36: p->m_height = 30;
00B54523 mov eax,dword ptr [p]
00B54526 mov dword ptr [eax+8],1Eh
37: p->run();
00B5452D mov ecx,dword ptr [p]
00B54530 call Person::run (0B5128Fh)
34: p->m_id=10;
00B54510 mov eax,dword ptr [ebp-20h]
00B54513 mov dword ptr [eax],0Ah
注意这两句,第二句,在中括号里的eax一定是地址,而将令一个地址空间的数据传给eax当做地址来用,那么这一定是指针,经验足够一下就能分析出
先根据地址取东西,取出的东西又当做地址值,像这样的一般是指针
原理:如何利用指针间接访问所指向对象的成员变量
1,首先从指针取出它存储的对象地址值
2,再利用对象地址值 + 成员变量的偏移量 计算出成员变量的地址值
3,根据成员变量的地址访问成员变量的存储空间
我们平时使用指针的目的:有些地方必须用指针,如堆空间只能用指针,只能使用堆空间的地址值访问堆空间的数据,那么谁去存储这个地址值呢,指针
A-05-指针的思考题
struct Person{
int m_id;
int m_age;
int m_height;
void run(){
// this指针存储着函数调用者的地址
//this指向了函数调用者
cout << "m_id" << m_id << endl;
cout << "m_age" << m_age << endl;
cout << "m_height" << m_height << endl;
}
};
int main(){
Person person1;
person1.m_id = 10;
person1.m_age = 20;
person1.m_height = 30;
person1.run();
Person *p =(Person *) &person1.m_age;//强转类型,age是int型的,而左侧指针定义为person型,所以要欺骗编译器右侧是person型的,否则报错
//eax==&person1.m_age==&person1+4
//mov eax, dword ptr[p]
//mov dword ptr[eax+0],40
//mov dword ptr[&person+4+0],40
p->m_id=10;
//mov dword ptr[eax+4],50
//mov dword ptr[&person + 4 + 4],50
p->m_age = 20;
person1.run();
getchar();
return 0;
}
输出结果是10,40,,50
注意person1.run();
与p->run();
的输出结果是不一样的
//会将person1对象的地址传递给run函数中的this
person1.run(); #这句代码如果是通过对象去调用函数,就是将person1对象的地址值传进函数run中
//会将指针p里面存储的地址传递给run函数中的this
//将&person1.m_age传递给run函数中的this
p->run(); #如果是通过指针间接调用这个函数,会将指针中存储的地址值传过去,而不是将指针变量自己的地址值传进去
打印出的m_age是-858993460
我们看其内存是就能发现是OXCC,这个是啥,是函数调用栈的问题,每当调用一个函数时,会给函数分配一段栈空间,这个栈空间的数据可能是上一个函数用过的,可能是一些垃圾数据,真正把给这个函数栈空间用起来之前,会先将分配好的栈空间全部用CC填充,会用一大堆的C去填充这个栈空间,说白了,这个栈空间里面全是C,为什么要填充CC,不填充0呢?为什么用C抹掉数据而不是0呢?
CC对应的机器码也就是汇编代码是int3:起到断点作用,我们平常调试时,有断点调试功能,代码到断点停止不会继续执行,就是执行的int3这个汇编指令,就会停下来,不会执行下面的指令。
CC ->int3
Int3是断点的意思
Int不是整数的意思,在汇编中是中断,后面的3是中断码
中断:interrupt
这样有什么好处,假设这个地址值不小心是函数的栈空间,不小心指针指到了别的地方,假设这些内存很危险呢,如删除内存关机等其他操作,之前存储的是垃圾数据,不知道其对应的汇编是什么,不安全,全部将其变为断点,就算将函数栈空间当做代码执行,一执行的也是断点断点,就算函数栈空间不小心被别人当成代码执行也是很安全的,就不会做出一些出格的事情
调用display是,看其汇编代码
12: void run(){
00445E30 push ebp
00445E31 mov ebp,esp
00445E33 sub esp,0CCh 此处为分配栈空间,分配了OCCH空间也就是204个字节
函数代码最终会转成0101机器码,机器码也就是汇编代码放在代码区,接下来的话CPU得看是什么CPU,假设X86,CPU中有个IP寄存器指针,会指向下一条需要执行的机器指令的地址
调用函数,执行函数代码,其实就是CPU在访问代码区的内存(指令)
我们的函数代码存储在代码段,存储在代码区,我们平时调用函数,执行函数代码,其实就是执行代码区的机器指令,执行代码区的代码
函数代码在代码区,怎么给函数分配到栈空间?这是疑惑的地方
我们执行函数的每个代码肯定是在代码区执行的,但在执行代码过程中是要定义局部变量的,而且定义的局部变量是执行着执行着可以改的,函数中的局部变量是可以改的,既然函数里面要用到局部变量,定义局部变量,所以在调用函数的时候要分配一段额外的空间用来存放函数内部的局部变量
函数代码存储在在代码区,函数代码内存在代码区,函数代码在代码区的内存是用来存放机器指令的,是拿来放汇编代码,不是变量的存储空间
函数在代码区的空间时用来放函数代码,放机器指令的,不是放局部变量的
说白了,代码区并没有额外的空间放变量,代码区只放代码,所以调用函数的时候必须分配额外的存储空间来存储函数内部的局部变量
代码区是只读的,内存受保护的,不可更改,局部变量既然要改变,就不能在代码区分配,而在栈空间分配,会在栈空间中分配一段连续的栈空间来存储函数的变量,这是不矛盾的
函数代码存储在代码区,局部变量存储在栈空间,两者不矛盾
12: void run(){
00445E30 push ebp
00445E31 mov ebp,esp
00445E33 sub esp,0CCh 此处为分配栈空间,分配了OCCH空间也就是204个字节
一调用函数,就会在栈空间内分配一段连续的存储空间,拿来放局部变量
学会汇编可以自己去验证自己想法
A-07-内存、封装、内存布局、堆空间
封装
成员变量私有化,提供公共的getter和setter给外界去访问成员变量
比较简单,过一遍
#include <iostream>
using namespace std;
struct Person{
int m_age;
};
int main(){
Person person;
person.m_age = 10;
getchar();
return 0;
}
我们创建一个对象person,可以直接访问成员变量age给其赋值,为什么可以直接访问,struct默认访问权限是public,这样是有问题的,成员变量公开的话,别人会赋值一些不正常的给他,如-4,很明显年龄不能为负
为了过滤掉不怀好意的值,将其成员变量私有化private
我们将成员变量私有化后,可以做一个公共的成员函数,进行set方法和get方法,在set中对传入的值进行过滤,将<=0
的值全变为1,正常的值就赋值给成员变量,若是想要获得值使用get方法获得。
#include <iostream>
using namespace std;
struct Person{
private:
int m_age;
public:
void setAge(int age){
if (age <= 0){
m_age = 1;
}
else{
m_age = age;
}
}
int getAge(){
return m_age;
}
};
int main(){
Person person;
//person.m_age = 10; 此处就不能这么写了
person.setAge(-4);
cout << person.getAge() << endl;
getchar();
return 0;
}
至于set方法中要不要过滤,怎样过滤就看自己的了,这就是封装。
代码段(代码区)
应用程序跑起来,这些机器码放在内存的代码区,代码区是只读的,不能更改
数据段(全局区)
用来存放全局变量
在函数外定义的变量为全局变量,这变量就存放在全局区
全局区中的东西是整个程序运行过程中都存在的东西,不会自动销毁
栈空间
没调用一个函数就会给他分配一段连续的栈空间(用来存放函数内定义的局部变量),等函数调用完毕后会自动回收这段栈空间
自动分配和回收
堆空间
需要主动去申请和释放(栈空间,只要调用函数操作系统自动分配栈空间,而堆空间,向操作系统申请空间)
内存空间的布局及其重要
假设打开三个软件,微信、QQ、谷歌,每个软件都有自己的内存空间(栈空间、堆空间、全局区、代码区,这四个是最主要的)
堆空间(什么情况下使用,为什么使用?)
为了自由控制内存的生命周期、大小(希望什么时候用到内存,什么时候就申请,什么时候回收就回收,用多少使用多少,用完了可以回收,这个可以自己控制)
假设所有东西都放到全局区,会有什么后果,举个例子,一个植物大战僵尸游戏,假设植物、子弹、僵尸对象全部放在全局区,可以思考一下后果,一旦放到全局区就全部在那,没办法回收了
那如果将对象放到函数中呢,也不行函数一调用就创建对象,函数调用完僵尸就死了,什么时候死,不是看函数有没有调用完,而是看僵尸生命值有没有减。我们僵尸的内存并不希望函数调用完就没了,而是看具体情况,达到一定条件猜让它死,我们需要自由的控制僵尸什么时候死,内存什么时候回收。
这个时候我们就采用堆空间,比如,僵尸来了,如果我们希望,我们向堆空间申请一堆连续的堆空间用来放僵尸对象,这样僵尸就出来了。假设有一个僵尸被打死了,就将僵尸的内存回收掉,没死的就不回收。
只有全局区和栈空间是不能解决很多问题的,有了堆空间可以自由控制内存生命周期,什么时候申请,什么时候释放,都可以自由控制。内存的大小也得以控制。
学过面向对象的语言,都知道有类有对象,但大家都知道面向对象的语言中创建的对象一般放在什么地方,放到堆空间
我们在C++中如何申请释放堆空间malloc\free
void test(){
//malloc(4);//像操作系统堆空间申请4个字节,将首地址返回,既然是地址,左边应为指针接收
int *p = (int *)malloc(4);//指针类型根据自己意愿使用,要将这4个字节作为int型返回,需要强转换,默认返回void型
*p = 10;//将10存到4个字节的堆空间
//我们将申请堆空间的变量放到函数中,此函数调用完成,堆空间也不会回收,这4个字节在堆空间,栈空间才回收
//我们用完,要将这个空间释放
free(p);//p是地址,堆空间的地址,回收对空间,注意不能回收两次,某些平台会崩溃
//当初申请多大空间,就回收多大空间
char *p = (char *)malloc(4);//这个与上面的int不同,上面4个字节当做整型来用,所以可以认为指针指向这4个字节,指向这个整型变量的内存空间
//而char只能指向一个char类型的数据,指向一个字节,可以认为此指针p指向此4个字节的第一个字节
*p = 10;//此时的10赋给p指向的那一个字节
p[0] = 10;
p[1] = 11;
p[2] = 12;
//与下方完全等价
/**p = 10;
*(p + 1) = 11;
*(p + 2) = 12;*/
free(p);//malloc和free是对应的,不会因为char是一个字节就释放一个字节,是将申请的字节全部回收掉
}
int main(){
test();
getchar();
return 0;
}
因为是32位环境,指针变量P占4个字节在栈空间,对空间4个字节存放的是10
这个图总结,栈空间的指针变量p指向了堆空间的4个字节
函数调用完后堆空间不会被释放除非是free,但函数调用按后指针变量p就消失了,P中的值也没有,但P消失的是栈空间,堆空间并没有消失,只是没有指针指向堆空间了,但堆空间还是存在的。
在C++中还有new和delete可以向堆空间申请释放内存,而且这样可读性更高。
只要看到new就是想堆空间申请内存,直接new int;就是想堆空间申请4个字节,不用强制转换直接int *p = new int;
要比前面好用很多
void test2(){
int *p = new int;//
*p = 10;
delete p;//注意用new出来堆空间不要用free释放,会出错,new和delete是一一对应的。new对比malloc要做很多事情
//char *p = new char;//申请一个字节
//*p = 10;
//delete p;
//char *p = new char[4];//char类型的数组,p指向第一个字节
//*p = 10;
//delete[] p;//此处【】必须存在,否则delete p;值销毁一个字节
}
在java中申请堆空间对象不需要自己管堆空间的释放,它检测这个对象没人用了会自动回收,有个垃圾回收机制,很多高级语言不需要我们管理内存,把很多内存细节屏蔽掉了
Person person=new Person();
利:提供开发效率避免内存使用不当,发生泄漏,开发者中点放在用什么类,解决业务,不用考虑什么时候释放类,系统会帮您释放
弊:停留在API调用和表层语法糖,对性能优化无法下手
A-10-堆空间的初始化
int *p = (int *)malloc(4);//此处堆空间4个字节是没有进行初始化的
*p = 0; //我们可以用此方式对其进行初始化
int size = sizeof(int)* 10;
int *p = (int *)malloc(size);//此处里面申请了40个字节的堆空间
//我们不能*p = 0;,这个只是将4个字节变为0,并没有将堆空间整个初始化
//要想初始化,我们需要使用memory set
memset(p, 0, size);//首地址,设置为0,多少字节需要设置
//从p地址开始的连续4个字节中的每个字节设置为1
memset(p, 1, 4);
//将4个字节设置为1
//00000000 00000000 00000000 00000001
//将4个字节中的每个字节都设置为1
//00000001 00000001 00000001 00000001
Malloc我们使用的非常少,我们一般使用new
void test4(){
int *p0 = new int; //未被初始化
int *p1 = new int(); //初始化为0,()在汇编调用memset进行初始化
int *p2 = new int(5); //被初始化为5
cout << *p0 << endl;
cout << *p1 << endl;
cout << *p2 << endl;
}
结果如下
-842150451
0
5