【C++】类和对象 (中篇)(1)

简介: 【C++】类和对象 (中篇)(1)

一、默认成员函数

在使用C语言练习初阶数据结构,即线性表、链表、栈、队列、二叉树、排序等内容时,大家可能会经常犯两个错误,特别是第二个错误,可以说是十分普遍:

  1. 在使用数据结构创建变量时忘记对其进行初始化操作而直接进行插入等操作;
  2. 在使用完毕后忘记对动态开辟的空间进行释放而直接返回;

而C++是在C语言的基础上生长起来的 – 修正C语言中的一些不足,并加入面向对象的思想;面对上面C语言存在的问题,C++设计出了默认成员;

默认成员函数:当用户没有显式实现时,编译器会自动生成的成员函数称为默认成员函数;

2020062310470442.png

其中,构造函数和析构函数针对的是我们上面提出的两个问题,而其他四个函数则是适用了其他场景与需求;下面,我们来仔细探讨这6个默认成员函数的细节;

二、构造函数

1、基础知识

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的任务并不是创建对象,而是当对象被创建之后完成对象的初始化工作;同时构造函数不能由用户调用,而是在创建类类型对象时由编译器自动调用,并且在对象整个生命周期内只调用一次。

构造函数有如下特性:

    函数名与类名相同;

    无返回值;

    对象实例化时编译器自动调用对应的构造函数;

    构造函数支持重载与缺省参数;

    如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,但一旦用户显式定义编译器将不再自动生成;

    构造函数对内置类型不处理,对自定义类型调用它自身的默认构造;

    无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个


    我们以Date类为例:

    class Date
    {
    public:
      Date()  //无参构造
      {
        _year = 1970;
        _month = 1;
        _day = 1;
      }
      Date(int year, int month = 1, int day = 1)  //带参构造
      {
        _year = year;
        _month = month;
        _day = day;
      }
      void Print()
      {
        cout << _year << "-" << _month << "-" << _day << endl;
      }
    private:
      int _year;
      int _month;
      int _day;
    };
    int main()
    {
      Date d1;  //无参
      d1.Print();
      Date d2(2022, 10, 4);  //带参
      d2.Print();
    }

    2020062310470442.png

    可以看到,我们并没有显示的去调用构造函数,而是由编译器自动调用;而构造函数的功能其实就是我们之前Init函数的功能;

    这里有两个需要注意的地方:

    1、构造函数虽然支持重载和缺省参数,但是无参构造和有参全缺省构造不能同时出现,因为在调用时会产生二义性;

    2020062310470442.png

    同时,当参数有多个时可以构成很多个重载,使得构造函数变得十分冗余,所以一般我们只会显式定义一个全缺省的构造函数,因为这一种就可以构造就可以代表很多种参数情况;

    2020062310470442.png

    2、当我们调用无参构造或者全缺省构造来初始化对象时,不要在对象后面带括号,这样使得编译器分不清这是在实例化对象还是函数声明;

    2020062310470442.png

    2、特性分析 – 自动生成

    在构造函数的第5点特性中我们提到:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,但一旦用户显式定义编译器将不再自动生成;下面我们来验证这个特性;

    class Date
    {
    public:
      void Print()
      {
        cout << _year << "-" << _month << "-" << _day << endl;
      }
    private:
      int _year;
      int _month;
      int _day;
    };

    可以看到上面的日期类中我们是没有显式的去自己实现构造函数的,所以编译器应该会自己生成一个无参的默认构造函数完成初始化工作:

    2020062310470442.png

    但是我们发现一个问题,默认的构造函数好像并没有完成初始化工作,即d1对象中的_year,_month,_day仍然是随机值;那么是因为这里编译器生成的默认构造函数并没有什么用吗?这个问题我们需要构造函数的第六个特性来回答;

    3、特性分析 – 选择处理

    内置类型与自定义类型:C++把类型分成内置类型 (基本类型) 和自定义类型;内置类型就是语言本身提供的数据类型,如:int/char/double/指针,自定义类型就是我们使用 class/struct/union 等自己定义的类型,如:Stack/Queue/Date;

    而构造函数的第六点特性如下:构造函数对内置类型不处理,对自定义类型调用它自身的默认构造

    对于这个特性,我们使用 Date、Stack 和 Myqueue 三个类来对比理解:(注:Myqueue 即 用两个栈实现一个队列)

    Date:

    class Date
    {
    public:
      void Print()
      {
        cout << _year << "-" << _month << "-" << _day << endl;
      }
    private:
      int _year;
      int _month;
      int _day;
    };

    Stack:

    class Stack
    {
    public:
      Stack(int capacity = 4)
      {
        _a = (int*)malloc(sizeof(int) * capacity);
        if (_a == nullptr)
        {
          perror("malloc fail\n");
          exit(-1);
        }
        _top = 0;
        _capacity = capacity;
        cout << "Stack 构造" << endl;
      }
      void Push(int x)
      {
        _a[_top++] = x;
      }
    private:
      int* _a;
      int _top;
      int _capacity;
    };

    Queue:

    class MyQueue
    {
    public:
      void Push(int x)
      {
        _pushST.Push(x);
      }
      Stack _pushST;
      Stack _popST;
    };

    可以看到,Stack 的成员变量全部为内置类型,所以当我们不显式定义构造函数时,编译器自动生成一个默认构造函数,但默认生成的构造函数并不会对内置类型进行处理,所以这里我们看到的是随机值;Date 类的情况也是如此:

    2020062310470442.png

    20200623104134875.png

    也就是说,编译器自动生成的构造函数不能满足我们的需求,所以我们需要手动定义构造函数:

    2020062310470442.png

    20200623104134875.png

    而对于MyQueue来说,它的成员变量全部为自定义类型,所以即使我们不提供构造函数时,编译器自动生成的构造函数也会去调用自定义类型的默认构造,满足需求:

    2020062310470442.png

    总结

    那么,到底什么时候需要我们自己提供构造函数,什么时候使用编译器默认生成的构造函数呢?是内置类型全部自己定义,自定义类型全部使用默认生成的吗?答案是:面向需求 – 当编译器默认生成的构造函数就能满足我们的需求时我们就不需要自己提供构造函数,如MyQueue;当编译器提供/的构造函数不能满足我们的需求时就需要我们自己定义,如Date/Stack;

    4、特性分析 – 默认构造

    构造函数的第七点特性如下:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个;


    上面这句话的意思就是当我们使用无参的方式实例化一个对象时,编译器会自动去调用该对象的默认构造函数,而默认构造函数有三种:编译器自动提供的无参构造函数、显式定义的无参构造函数、显式定义的全缺省的构造函数;

    2020062310470442.png

    20200623104134875.png

    20200623104650275.png

    如果类中没有默认构造函数,那么我们实例化对象时就必须传递参数:

    2020062310470442.png

    相关文章
    |
    2天前
    |
    编译器 C++
    C++ 类构造函数初始化列表
    构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
    43 30
    |
    17天前
    |
    C++
    C++(十一)对象数组
    本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
    |
    17天前
    |
    存储 编译器 C++
    C ++初阶:类和对象(中)
    C ++初阶:类和对象(中)
    |
    17天前
    |
    C++
    C++(十六)类之间转化
    在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。
    |
    17天前
    |
    存储 设计模式 编译器
    C++(十三) 类的扩展
    本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。
    |
    30天前
    |
    存储 算法 编译器
    c++--类(上)
    c++--类(上)
    |
    1月前
    |
    编译器 C++
    virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
    virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
    |
    1月前
    |
    编译器 C++
    virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
    virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
    |
    17天前
    |
    存储 C++
    C++(五)String 字符串类
    本文档详细介绍了C++中的`string`类,包括定义、初始化、字符串比较及数值与字符串之间的转换方法。`string`类简化了字符串处理,提供了丰富的功能如字符串查找、比较、拼接和替换等。文档通过示例代码展示了如何使用这些功能,并介绍了如何将数值转换为字符串以及反之亦然的方法。此外,还展示了如何使用`string`数组存储和遍历多个字符串。
    |
    25天前
    |
    存储 C++
    C++ dll 传 string 类 问题
    C++ dll 传 string 类 问题
    16 0