• (黑马C++)L03 对象的构造和析构


    对象的初始化和清理

    构造函数和析构函数会被编译器自动调用,完成对象初始化和对象清理工作。即使自己不提供初始化操作和清理操作,编译器也会增加默认的操作,所以编写类就应该顺便提供初始化函数。

    构造函数和析构函数必须写在public下才可以调用到。

    1)构造函数和析构函数的概念

    构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用;析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。

    2)构造函数和析构函数的语法

    构造函数与类名相同,没有返回值,不写void,可以发生重载(可以有参数),由编译器自动调用而不是手动,而且只会调用一次。

    析构函数与类名相同,但类名前面需要加"~",没有返回值,不写void,不可以有参数(不能发生重载)。

    1. class Person{
    2. public:
    3. Person(){
    4. cout << "构造函数调用" << endl; //自动调用
    5. }
    6. ~Person(){
    7. cout << "构造函数调用" << endl; //自动调用,在函数结束后
    8. }
    9. }

    3)构造函数的分类及调用

    构造函数按照参数进行分类,分为无参构造函数(默认构造函数)和有参构造函数;

    1. class Person{
    2. public:
    3. //默认构造函数调用时不要加小括号
    4. //加上括号编译器认为是函数声明,不会调用构造和析构
    5. Person() {
    6. cout << "默认构造函数" << endl;
    7. }
    8. Person(int a) {
    9. cout << "有参构造函数" << endl;
    10. }
    11. };

    按照类型进行分类,可分为普通构造函数和拷贝构造函数

    1. class Person{
    2. public:
    3. //拷贝构造函数
    4. Person(const Person& p) {
    5. cout << "拷贝构造函数" << endl;
    6. }
    7. Person(int a) {
    8. cout << "有参构造函数" << endl;
    9. }
    10. };

    构造函数的调用方式分为括号法调用、显示法调用以及隐式类型转换。

    使用括号法调用时,默认构造函数不要加(),加括号编译器会认为是函数声明;

    Person(100)叫做匿名对象,匿名对象特定,如果编译器发现对象是匿名的,那么在这行代码结束就释放这个对象;

    不能用拷贝构造函数初始化匿名对象,如果写成Person(p5)编译器会认为写的是Person p5,按照对象的声明处理,写成右值才会按拷贝构造进行处理。

    1. void test01(){
    2. //括号法调用
    3. Person p1(1); //有参
    4. p1.m_age = 10;
    5. Person p2(p1); //拷贝
    6. Person p3; //默认构造函数,不要加(),加括号编译器会认为是函数声明
    7. //显示法调用
    8. Person p4 = Person(100); //有参函数调用
    9. Person p5 = Person(p4); //拷贝函数调用
    10. Person(100); //叫做匿名对象
    11. //隐式类型转换
    12. Person p = 100; //会使用隐式类型转换,转换成Person p = Person(100);
    13. Person p6 = p5; //会转换成拷贝函数
    14. }

    4)拷贝构造函数调用时机

    1.用已经创建好的对象来初始化新的对象

    1. void test01(){
    2. Person p1;
    3. p1.m_age = 10;
    4. Person p2(p1);
    5. }

    2.以值传递的方式给函数参数传值

    1. //值传递会调用拷贝构造,引用传递不会调用拷贝构造
    2. void doWork(Person p1) { //Person p1 = Person(p)
    3. }
    4. void test02() {
    5. Person p;
    6. p.m_age = 10;
    7. doWork(p);
    8. }

    3.以值的方式返回局部对象

    1. Person doWork2(){
    2. Person p1;
    3. return p1;
    4. }
    5. void test03() {
    6. Person p = doWork2();
    7. }

    5)构造函数的调用规则

    系统默认会给一个类提供三个函数:默认构造,拷贝构造,析构函数。

    1.当提供了有参构造函数,系统就不会再提供默认构造函数,但是系统还会提供默认的拷贝构造函数;

    2.当提供了拷贝构造,系统就不会提供其他构造了。

    6)深拷贝和浅拷贝

    有参构造函数就是为了初始化属性。

    1. class Person {
    2. public:
    3. Person() {}
    4. //初始化属性
    5. Person(char * name, int age) {
    6. m_Name = (char *)malloc(strlen(name) + 1);
    7. strcpy(m_Name, name);
    8. m_age = age;
    9. }
    10. //拷贝函数系统会默认提供,并且是简单的值拷贝
    11. char * m_Name;
    12. int m_age;
    13. };
    14. void test01() {
    15. Person p1("老王"10);
    16. Person p2(p1); //调用了拷贝构造
    17. }

    当上述代码加上拷贝函数后,无法运行成功,因为当p1调用析构函数之后指针指向的地址里的内容已经被释放,当p2再调用析构时,m_Name也不为空,进行释地址内容时会发生异常。

    1. //析构函数
    2. ~Person() {
    3. cout << "析构函数调用" <
    4. if(m_Name != NULL) { //m_Name是一个指针,需要释放
    5. free(m_Name);
    6. m_Name = NULL;
    7. }
    8. }

    上述拷贝方式叫做浅拷贝,浅拷贝直接将p1的地址拷贝给p2,导致析构时释放堆区空间两次发生异常,解决方法是使用深拷贝。深拷贝不会直接拷贝地址,而是开辟一块新的空间,此时都调用析构函数时不会再发生错误。

    1. class Person {
    2. public:
    3. Person() {}
    4. //初始化属性
    5. Person(char * name, int age) {
    6. m_Name = (char *)malloc(strlen(name) + 1);
    7. strcpy(m_Name, name);
    8. m_age = age;
    9. }
    10. //拷贝函数系统会默认提供,并且是简单的值拷贝
    11. //深拷贝写法
    12. Person(const Person&p) {
    13. m_age = p.m_age;
    14. m_Name = (char *)malloc(strlen(p.m_Name) + 1);
    15. strcpy(m_Name, p.m_Name);
    16. }
    17. //析构函数
    18. ~Person() {
    19. cout << "析构函数调用" <
    20. if(m_Name != NULL) { //m_Name是一个指针,需要释放
    21. free(m_Name);
    22. m_Name = NULL;
    23. }
    24. }
    25. char * m_Name;
    26. int m_age;
    27. };
    28. void test01() {
    29. Person p1("老王"10);
    30. Person p2(p1); //调用了拷贝构造
    31. }

    7)初始化列表

    数据的初始化可以使用有参构造函数以及初始化列表。

    利用构造函数初始化数据:

    1. class Person {
    2. public:
    3. //有参构造函数进行数据初始化
    4. Person(int a, int b, int c) {
    5. ma = a;
    6. mb = b;
    7. mc = c;
    8. }
    9. int ma;
    10. int mb;
    11. int mc;
    12. };
    13. void test() {
    14. Person p1(10, 20, 30);
    15. }

     利用初始化列表初始化数据:构造函数后面 + : 属性(参数),属性(参数)...

    1. class Person {
    2. public:
    3. //有参构造函数进行数据初始化
    4. Person(int a, int b, int c) : ma(a), mb(b), mc(c) {}
    5. int ma;
    6. int mb;
    7. int mc;
    8. };
    9. void test() {
    10. Person p1(10, 20, 30);
    11. }

    8)类对象作为类成员的实例

    1. class Phone {
    2. public:
    3. Phone(){
    4. cout << "手机的默认构造函数调用" << endl;
    5. }
    6. Phone(string name){
    7. Phone_Name = name;
    8. }
    9. ~Phone(){
    10. cout << "手机的析构函数调用" << endl;
    11. }
    12. string Phone_Name;
    13. };
    14. class Game {
    15. public:
    16. Game(){
    17. cout << "游戏的默认构造函数调用" << endl;
    18. }
    19. Game(string name){
    20. Game_Name = name;
    21. }
    22. ~Game(){
    23. cout << "游戏的析构函数调用" << endl;
    24. }
    25. string Game_Name;
    26. };
    27. class Person{
    28. public:
    29. Person(){
    30. cout << "Person的默认构造函数调用" << endl;
    31. }
    32. Person(string name, string phoneName, string gameName){
    33. m_name = name;
    34. m_phone = phoneName;
    35. m_game = gameName;
    36. }
    37. ~Person(){
    38. cout << "Person的析构函数调用" << endl;
    39. }
    40. string m_name;
    41. Phone m_phone;
    42. Game m_game;
    43. };
    44. void test() {
    45. Person p("老王", "华为", "斗地主");
    46. }

    类对象作为类成员的时候,构造顺序先将类对象一一构造,然后构造自己,析构的顺序相反。

    9)explicit关键字作用

    使用了explicit关键字就不能用隐式类型转换来构造对象,explicit用来防止隐式类型转换。

    10)动态对象创建

    C语言提供了动态内存分配函数malloc和free,用于从堆中分配和释放存储单元,然而这些函数在C++中不能很好的运行,因为它不能很好完成对象的初始化工作。

    1.使用malloc存在的问题:

    程序员必须确定对象的长度;malloc返回一个void*指针,C++不允许将void*赋值给其他指针,必须强制类型转换;malloc可能申请内存失败,所以必须判断返回值确保内存分配成功;用户在使用对象之前必须对他初始化,构造函数不能显示调用初始化,用户可能会忘记调用。

    2.可以使用new来开辟空间,new的对象会默认调用构造函数,使用方法如下:

    Person * p2 = new Person;

    3.new和malloc的区别:

    new在堆区开辟空间,所有new出来的对象都会返回该类型的指针;

    malloc不会调用构造,new会调用构造;

    new是一个运算符,malloc是一个函数。

    释放堆区的空间用delete,delete也是一个运算符,要配合new使用;malloc配合free使用。

    void * p = new Person;

    void*接收new的对象会造成无法释放,所以要避免这种写法。

    4.通过new开辟数组

    1. void test(){
    2. Person * array = new Person[10]; //会调用十次默认构造函数
    3. delete [] array; //释放数组对象必须要delete[]
    4. }
  • 相关阅读:
    vue-element-admin+springboot登录功能实现
    python自动化测试(二):获取元素定位的基本方式
    Java基础 - 模拟医院挂号系统
    PyQt5 QLabel标签
    详解API接口如何安全的传输数据
    神经网络反向传播的数学原理
    letcode 2171. 拿出最少数目的魔法豆
    QT day3
    java基于springboot+vue的疫情网课管理系统 elementui
    JVM类加载机制详解
  • 原文地址:https://blog.csdn.net/qq_43697646/article/details/126979519