【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

    相关文章
    |
    1天前
    |
    编译器 C语言 C++
    |
    1天前
    |
    编译器 C++
    【C++】详解初始化列表,隐式类型转化,类静态成员,友元
    【C++】详解初始化列表,隐式类型转化,类静态成员,友元
    |
    4天前
    |
    存储 编译器 C++
    【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
    C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
    |
    4天前
    |
    编译器 C++
    【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
    本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
    |
    4天前
    |
    C语言 C++
    【C++】日期类Date(详解)③
    该文介绍了C++中直接相减法计算两个日期之间差值的方法,包括确定max和min、按年计算天数、日期矫正及计算差值。同时,文章讲解了const成员函数,用于不修改类成员的函数,并给出了`GetMonthDay`和`CheckDate`的const版本。此外,讨论了流插入和流提取的重载,需在类外部定义以符合内置类型输入输出习惯,并介绍了友元机制,允许非成员函数访问类的私有成员。全文旨在深化对运算符重载、const成员和流操作的理解。
    |
    4天前
    |
    C++
    【C++】日期类Date(详解)②
    - `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
    |
    4天前
    |
    定位技术 C语言 C++
    C++】日期类Date(详解)①
    这篇教程讲解了如何使用C++实现一个日期类`Date`,涵盖操作符重载、拷贝构造、赋值运算符及友元函数。类包含年、月、日私有成员,提供合法性检查、获取某月天数、日期加减运算、比较运算符等功能。示例代码包括`GetMonthDay`、`CheckDate`、构造函数、拷贝构造函数、赋值运算符和相关运算符重载的实现。
    |
    4天前
    |
    编译器 C++
    【C++】类和对象③(类的默认成员函数:赋值运算符重载)
    在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
    |
    4天前
    |
    存储 编译器 C++
    【C++】类和对象③(类的默认成员函数:拷贝构造函数)
    本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
    |
    4天前
    |
    存储 编译器 C语言
    【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
    C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。