第四章 封装、内存布局(malloc、free、new、delete)、构造函数
封装
-
引例
struct Person { int m_age; }; //使用 Person person; person.m_age = -4;
- 年龄设置为-4很显然是现实中是不可能的,但是由于m_age这个成员变量是暴露出来的,可以在外部任意修改
- 为了防止这种情况,就出现了封装这个概念。
- 封装就是为了让外部不能直接访问对象的成员变量,仅仅通过我们封装后的方法才能访问。
- 我们可以在封装方法中设置一些过滤,防止非正常数据的出现。
-
封装的本质:
-
成员变量私有化,提供公共的getter和setter给外界去访问成员变量
struct Person { private: int m_age; public: //封装 void setAge(int age) { if (age<=0) { cout << "设置的值不合理" << endl; return; } this->m_age = age; } int getAge() { return this->m_age; } }; int main() { Person person; person.setAge(20); cout << "age=" << person.getAge() << endl; person.setAge(-20); getchar(); return 0; }
-
内存空间的布局
- 每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域
- 代码段(代码区)
- 用于存放代码
- 数据段(全局区)
- 用于存放全局变量等
- 栈空间
- 每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间
- 自动分配和回收
- 堆空间
- 需要主动去申请和释放
- 代码段(代码区)
堆空间
- 为什么需要堆空间? 堆空间存在的意义?
- 我们知道栈空间分配的内存,一旦函数调用完毕就会被释放
- 但是如果我们需要在B函数中使用A函数定义的变量,怎么办呢? 这就需要延长A函数中变量的生命周期
- 设置为全局变量:但是一旦成为全局变量,整个应用关闭之前这段内存就一直存在,无法释放,设置多了就消耗内存,因此,不现实
- 能不能有一种方法:自己管理内存,使用前延长内存的生命周期,使用后手动释放掉? —堆区就是这种功能
- 在程序运行过程,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存
- 堆空间的申请\释放
malloc \ free
new \ delete
new [] \ delete []
- 注意
- 申请堆空间成功后,会返回那一段内存空间的地址
- 申请和释放必须是1对1的关系,不然可能会存在内存泄露
- 现在的很多高级编程语言不需要开发人员去管理内存(比如Java),屏蔽了很多内存细节,利弊同时存在
- 利:提高开发效率,避免内存使用不当或泄露
- 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手
//c语言 void test() { //申请4个字节的堆空间 int *p = (int *)malloc(4); //上面的p在栈空间,分配一段内存用于存储堆空间的地址 //堆中分配4个字节的内存 free(p); } //c++语言 void test2() { //申请4个字节内存 int *p = new int; //释放 delete p; //申请40个字节 int *p2 = new int[10]; //释放 delete[] p2; }
堆空间的初始化
-
案例如下:
//c语言初始化 void test3(){ int *P1 = (int *)malloc(sizeof(int));//未初始化 int *P2 = (int *)malloc(sizeof(int)); //将p2指向的4个字节的堆空间都初始化为0; memset(P2, 0, sizeof(int)); cout << *P2 << endl; free(P1); free(P2); } //c++语言初始化 void test4() { int *p1 = new int; //*p1 未初始化 int *p2 = new int(); // *p2 初始化为0 int *p3 = new int(5); // *p3 初始化为5 int *p4 = new int[3];//*p4 数组元素未被初始化 int *P5 = new int[3]();//3个数组元素都被初始化为0 int *p6 = new int[3]{};//3个数组元素都被初始化为0 int *p7 = new int[3]{ 5 };//数组首元素被初始化为5,其他元素被初始化为0 }
函数补充memset
- memset函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法 (不管是堆上还是栈上)
对象的内存
- 对象的内存可以存在于3种地方
- 全局区(数据段):全局变量
- 栈空间:函数里面的局部变量
- 堆空间:动态申请内存(malloc、new等)
//全局区 Person g_person; int main(){ //栈空间 Person person; //堆空间 Person *p = new Person; return 0; }
构造函数(Constructor)
- 构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作
- 顾名思义:就是在创建、构造、初始化一个对象的时候调用的函数
- 构造函数特点
-
函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数
struct Person { int m_age; //构造函数 Person() { cout << "执行了person()" << endl; this->m_age = 0; } //重载 Person(int age) { cout << "执行了person(int)" << endl; this->m_age = age; } }; int main() { //调用构造函数Person(); Person person1; //调用构造函数Person(int); Person person(20); cout << person.m_age << endl; getchar(); return 0; }
-
一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象
-
没有自定义构造函数
struct Student { }; //创建对象 //正确没有问题 Student student;
-
自定义构造函数
struct Student { Student(int age) { } }; int main() { //这么调用有问题,因为没有构造函数 Student(){} Student student; //这么调用没有有问题 Student student(20);
-
自定义构造函数
struct Student { Student() { } Student(int age) { } }; int main() { //这么调用没有问题 Student student; //这么调用没有有问题 Student student(20);
-
-
- 注意
-
通过malloc分配的对象不会调用构造函数
//通过堆创建构造函数 //调用构造函数 Person() Person *P1 = new Person(); delete P1; //不会调用任何构造函数。 Person *p2 = (Person *)malloc(sizeof(Person)); free(p2);
-
- 一个广为流传的、很多教程\书籍都推崇的错误结论:
- 默认情况下,编译器会为每一个类生成空的无参的构造函数 (错误结论!!!)
- 正确理解:在某些特定的情况下,编译器才会为类生成空的无参的构造函数
- (哪些特定的情况?以后再提)
构造函数的调用
- 写法注意:
-
C++通过new创建对象的格式是:
类型 *x = new 类型 类型 *x = new 类型() 类型 *x = new 类型(参数)
-
栈上创建对象的方式是:
类型 标识符; 类型 标识符(参数);
-
函数声明:
类型 标识符();
-
举例:
struct Person { int m_age; //构造函数 Person() { cout << "执行了person()" << endl; this->m_age = 0; } //重载 Person(int age) { cout << "执行了person(int)" << endl; this->m_age = age; } }; //全局区 Person g_perosn1; //Person(); //这是一个函数声明,函数名叫g_perosn2,无参数,返回值为Person类型。 Person g_perosn2(); Person g_perosn3(10);//Person(int); int main() { //栈空间 Person person; //Person(); //同样这也是一个函数声明,函数名叫person2,无参,返回值类型为Person Person person2(); Person person3(10);//Person(int); //堆空间 Person *p1 = new Person; //Person(); Person *p2 = new Person(); //Person(); Person *p3 = new Person(10); //Person(int); return 0; }
- 调用结果,有3次调用有参的构造函数,有4次调用无参的构造函数
- 表面看是应该有6次调用无参的构造函数。为什么呢?因为上面有2个是函数声明!
-
成员变量的初始化
-
默认情况下,成员变量的初始化
struct Person { int m_age; }; //全局区 Person g_person;//成员变量初始化为0; int main() { //栈空间 Person person; //成员变量不会被初始化 //对空间 Person *p1 = new Person;//成员变量不会被初始化 Person *p2 = new Person();//成员变量被初始化为0 Person *P3 = new Person[3];//成员变量不会被初始化 Person *P4 = new Person[3]();//3个person对象的成员变量被初始化为0 Person *P5 = new Person[3]{};//3个person对象的成员变量被初始化为0 return 0; }
-
如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化
struct Person { int m_age; Person() { } }; //全局区 Person g_person;//成员变量初始化为0; int main() { //栈空间 Person person; //成员变量不会被初始化 //对空间 Person *p1 = new Person;//成员变量不会被初始化 Person *p2 = new Person();//成员变量不会被初始化 Person *P3 = new Person[3];//成员变量不会被初始化 Person *P4 = new Person[3]();//3个person对象的成员变量不会被初始化 Person *P5 = new Person[3]{};//3个person对象的成员变量被初始化为0 return 0 }
-
需要自己在自定义的构造函数中初始化
struct Person { int m_age; Person() { memset(this, 0, sizeof(Person)); } };
-