【C++精华铺】5.C++类和对象(中)类的六个默认成员函数

简介: 我们想到空类的时候肯定想到的是里面什么都没有的类称之为空类,但是事实却并非如此。当一个类里面什么都不写的时候编译器会默认生成六个默认成员函数来完成一个类的基本功能。构造函数:对象初始化工作。析构函数:空间清理工作。拷贝构造和赋值运算符重载:对象的拷贝复制工作。取地址和const取地址重载:一般很少自己实现,除非需要给用户返回指定的特殊的地址。

 目录

1. 六个默认成员函数

2. 构造函数

2.1 概念

2.2 默认构造

2.2.1 系统生成的默认构造

2.2.2 自定义默认构造函数

2.3 构造函数的重载

3. 析构函数

3.1 概念

3.2 系统生成的析构函数

3.3 自定义析构函数

4. 拷贝构造

4.1 概念

4.2 默认生成的拷贝构造(浅拷贝)

4.3 自定义拷贝构造(深拷贝)

5. 赋值运算符重载

5.1 运算符重载

5.2 赋值运算符重载

6. 取地址及const取地址操作符重载

7. 附:完整日期类(文章中的代码不取自这里的代码,都是为了讲解知识点临时敲的,这里的代码是完整的日期类(取自比特科技),可以借鉴学习)


1. 六个默认成员函数

       当我们想到空类的时候肯定想到的是里面什么都没有的类称之为空类,但是事实却并非如此。当一个类里面什么都不写的时候编译器会默认生成六个默认成员函数来完成一个类的基本功能。

    1. 构造函数:对象初始化工作
    2. 析构函数:空间清理工作
    3. 拷贝构造和赋值运算符重载:对象的拷贝复制工作
    4. 取地址和const取地址重载:一般很少自己实现,除非需要给用户返回指定的特殊的地址。

    2. 构造函数

    2.1 概念

           构造函数是一个特殊的成员函数,他会在对象创建时由编译器自动调用完成对象的初始化工作,构造函数没有返回值,并且函数名与类名相同。

           构造函数特征如下:

      1. 函数名与类名相同。
      2. 无返回值。
      3. 对象实例化时编译器自动调用对应的构造函数。
      4. 构造函数可以重载
      class Date
      {
      public:
        Date()
        {
          //构造函数
        }
      private:
      };

      image.gif

      2.2 默认构造

             什么是默认构造?无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。一般来说我们所实现的自定义类型都必须具有一个默认构造函数,原因会在下面进行叙述。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。

      2.2.1 系统生成的默认构造

              当我们没有去手动定义构造函数的时候系统会默认生成一个无参的构造函数,这个无参的构造函数会对类里面的成员变量进行初始化,其中包括对自定义类型调用它的默认构造函数(这也是是为什么每个类必须要有一个默认构造的原因),对内置类型不做处理,对,你没听错,系统生成的默认构造不会对内置类型进行处理,这是一个非常bug的点。比如下面的案例:

      class Time
      {
      public:
        Time()
        {
          cout << "Time()" << endl;
          _hour = 0;
          _minute = 0;
          _second = 0;
        }
      private:
        int _hour;
        int _minute;
        int _second;
      };
      class Date
      {
      public:
        // 基本类型(内置类型)
        int _year;
        int _month;
        int _day;
      private:
        // 自定义类型
        Time _t;
      };
      int main()
      {
        Date d;
        cout << d._year << d._month << d._day << endl;
        return 0;
      }

      image.gif

      输出:

      Time()

      -858993460-858993460-858993460

             由此可见,默认生成的构造函数不会对内置类型进行处理,对自定义类型会调用它的默认构造。 由于这一点,让系统生成的默认构造毫无用武之地,所以在C++11中给这个漏洞打了一个补丁:C++11 中针对内置类型成员不初始化的缺陷,打了一个补丁,即:内置类型成员变量在 类中声明时可以给默认值。(以给缺省值的方式给内置类型一个默认值)如下:

      class Time
      {
      public:
        Time()
        {
          cout << "Time()" << endl;
          _hour = 0;
          _minute = 0;
          _second = 0;
        }
      private:
        int _hour;
        int _minute;
        int _second;
      };
      class Date
      {
      public:
        // 基本类型(内置类型)
        int _year = 2023;
        int _month = 8;
        int _day = 9;
      private:
        // 自定义类型
        Time _t;
      };
      int main()
      {
        Date d;
        cout << d._year << ' ' << d._month << ' '  << d._day << endl;
        return 0;
      }

      image.gif

      输出:

      Time()

      2023 8 9

      2.2.2 自定义默认构造函数

             自定义的默认构造函数只有俩种,无参的和全缺省的构造函数,并且这俩种构造只能存在一个。但是有人可能就要问了,这俩种构造函数不是构成重载了吗,为什么不能同时存在呢?首先就是语法上规定了默认构造只能存在一个,另外就是这俩个函数虽然符合函数重载的语法,但是在调用的时候会出现歧义,要知道,任何语法上的规定都是有原因的,如下:

      class Time
      {
      public:
        Time()
        {
          cout << "Time()" << endl;
          _hour = 0;
          _minute = 0;
          _second = 0;
        }
      private:
        int _hour;
        int _minute;
        int _second;
      };
      class Date
      {
      public:
        Date()
        {
          cout << "Date()" << endl;
        }
        Date(int a = 1, int b = 2)
        {
          cout << "Date(int a = 1, int b = 2)" << endl;
        }
      private:
        int _year = 2023;
        int _month = 8;
        int _day = 9;
        Time _t;
      };
      int main()
      {
        Date d;  //这里出现了歧义,
               //不知道调用的是全缺省的构造还是无参的构造
               //因为这俩种函数都可以用 Date() 的形式来调用就出现了歧义
        return 0;
      }

      image.gif

      错误:

      C2668    “Date::Date”: 对重载函数的调用不明确。

      E0339    类 "Date" 包含多个默认构造函数。

             但是我们如果自己传实参给构造函数我们发现就可以调用了,所以语法上规定只能存在一个默认构造本质就是因为上述情况会出现歧义,而在我们平时实例化的时候传个实参就可以避免这种歧义,这个时候即使定义了俩个默认构造也依然不会报错,但是我们不建议这样去写,因为这样在项目中的风险是巨大的,比如下面:

      class Time
      {
      public:
        Time()
        {
          cout << "Time()" << endl;
          _hour = 0;
          _minute = 0;
          _second = 0;
        }
      private:
        int _hour;
        int _minute;
        int _second;
      };
      class Date
      {
      public:
        Date()
        {
          cout << "Date()" << endl;
        }
        Date(int a = 1, int b = 2)
        {
          cout << "Date(int a = 1, int b = 2)" << endl;
        }
      private:
        int _year = 2023;
        int _month = 8;
        int _day = 9;
        Time _t;
      };
      int main()
      {
        Date d(1,2);  //不建议,不能因为避免报错就去特殊处理初始化方式。
                        //不能同时定义俩个默认构造,即使编译器没有报错
        return 0;
      }

      image.gif

      2.3 构造函数的重载

             构造函数是支持重载的,可以让我们应对不同的初始化场景(这里在强调一遍:无参构造和全缺省构造不能同时定义,会出现歧义)。

      public:
        Date()
        {
          cout << "Date()" << endl;
        }
        Date(int a, int b = 2)
        {
          cout << "Date(int a = 1, int b = 2)" << endl;
        }
        Date(int year, int month, int day)
        {
          cout << "Date(int year, int month, int day)" << endl;
        }
      private:
        int _year = 2023;
        int _month = 8;
        int _day = 9;
        Time _t;
      };

      image.gif

      3. 析构函数

      3.1 概念

             析构函数的性质与构造函数十分的类似,但是与构造函数功能相反,析构函数用于清理对象销毁后的空间,但它不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

             析构函数和构造函数一样无参数无返回值,命名有所不同,并且析构函数是在对象销毁时由编译器自动调用的,这一点和构造函数类似。特征如下:

        1. 析构函数名是在类名前加上字符 ~。
        2. 无参数无返回值类型。
        3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载。
        4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
        class Date
        {
        public:
          ~Date()
          {
            //析构函数
          }
        private:
          int _year = 2023;
          int _month = 8;
          int _day = 9;
        };

        image.gif

        3.2 系统生成的析构函数

               如果一个类里面没有显式的定义析构函数,系统会自动生成一个默认的析构函数,这个析构函数会在对象生命周期结束自动调用,默认生成的析构函数对内置类型不做处理,对自定义类型会去调用它的析构函数。如下:

        class Time
        {
        public:
          ~Time()
          {
            cout << "~Time()" << endl;
          }
        private:
          int _hour;
          int _minute;
          int _second;
        };
        class Date
        {
        public:
          ~Date()
          {
            cout << "~Date()" << endl;
          }
        private:
          int _year = 2023;
          int _month = 8;
          int _day = 9;
          Time _t;
        };
        int main()
        {
          Date d;
          return 0;
        }

        image.gif

        输出:

        ~Date()

        ~Time()

        3.3 自定义析构函数

               上面说过,析构函数对内置类型是不做处理的,但是我们在日常的使用过程中常常会涉及到内存申请(比如malloc)然后用指针类型去存储地址,诸如此类的空间我们就不能依赖系统生成的默认析构以免导致内存泄漏,需要自己定义析构函数去手动释放。

        class Date
        {
        public:
          Date()
          {
            p = (int*)malloc(10 * sizeof(int));
          }
          ~Date()
          {
            free(p);
            cout << "~Date()" << endl;
          }
        private:
          int _year = 2023;
          int _month = 8;
          int _day = 9;
          int* p = nullptr;
        };

        image.gif

        4. 拷贝构造

        4.1 概念

               拷贝构造是构造函数的一种的重载形式,拷贝构造只有一个形参,一般是这个类型的const引用(只能传引用,不能传值,传值会造成无穷递归),在用已存在的类型对象创建新对象时由编译器自动调用。

               传引用(正确):

        class Date
        {
        public:
          Date(int year = 1900, int month = 1, int day = 1)
          {
            _year = year;
            _month = month;
            _day = day;
          }
          Date(const Date& d)  //拷贝构造函数
          {
            _year = d._year;
            _month = d._month;
            _day = d._day;
          }
        private:
          int _year = 2023;
          int _month = 8;
          int _day = 9;
          int* p = nullptr;
        };

        image.gif

               传值(无穷递归):

        image.gif编辑

        4.2 默认生成的拷贝构造(浅拷贝)

               在我们没有显式的定义拷贝构造的时候会自动生成一个默认的拷贝构造,默认的拷贝构造会对我们的成员变量按字节拷贝,也成为值拷贝或者浅拷贝。

        class Date
        {
        public:
          Date(int year = 1900, int month = 1, int day = 1)
          {
            _year = year;
            _month = month;
            _day = day;
            p = (int*)malloc(10 * sizeof(int));
          }
            ~Date()
          {
            free(p);
            cout << "~Date()" << endl;
          }
        private:
          int _year;
          int _month;
          int _day;
          int* p = nullptr;
        };
        int main()
        {
          Date d1(12, 12, 12);
          Date d2(d1);   //引发异常
        }

        image.gif

        image.gif编辑

        4.3 自定义拷贝构造(深拷贝)

               当涉及到内存管理的时候编译器默认生成的拷贝构造就不够用了,这个时候就需要我们对其进行深拷贝。什么是深拷贝呢:就是创建一个新的对象,将原对象的各项属性的“值”(数组的所有元素)拷贝过来。如果是上面的情况我们就需要重新申请一块空间去保存d.p指向空间里面的数据。

        class Date
        {
        public:
          Date(int year = 1900, int month = 1, int day = 1)
          {
            _year = year;
            _month = month;
            _day = day;
            p = (int*)malloc(10 * sizeof(int));
            if (p == NULL)
            {
              perror("malloc fail");
              exit(-1);
            }
            for (int i = 0; i < 10; i++)
            {
              p[i] = 10;
            }
          }
          ~Date()
          {
            free(p);
            cout << "~Date()" << endl;
          }
          Date(const Date& d)
          {
            _year = d._year;
            _month = d._month;
            _day = d._day;
            p = (int*)malloc(10 * sizeof(int));
            if (p == NULL)
            {
              perror("malloc fail");
              exit(-1);
            }
            for (int i = 0; i < 10; i++)
            {
              p[i] = d.p[i];
            }
          }
        private:
          int _year;
          int _month;
          int _day;
          int* p = nullptr;
        };
        int main()
        {
          Date d1(12, 12, 12);
          Date d2(d1);
        }

        image.gif

        image.gif编辑

        输出:

        ~Date()

        ~Date()

        5. 赋值运算符重载

        5.1 运算符重载

               C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。 函数名字为:关键字operator后面接需要重载的运算符符号。 函数原型:返回值类型 operator操作符(参数列表)。

        注意:

          1. 不能通过连接其他符号来创建新的操作符:比如operator@ 重载操作符必须有一个类类型参数
          2. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
          3. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
          4. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

                 运算符重载我们有俩种定义方式,可以定义成全局的,也可以直接定义成函数成员,但是定义成函数成员我们所看到的形式参数就会少一个(由于this指针隐含参数) ,下面以==为例:

          //全局
          bool operator==(const Date& d1, const Date& d2)
          {
              return d1._year == d2._year
             && d1._month == d2._month
                  && d1._day == d2._day;
          }
          //函数成员
          class Date
          {
          public:
            bool operator==(const Date& d)
            {
              return (_year == d._year)
                && (_month == d._month)
                && (_day == d._day);
            }
          private:
            int _year;
            int _month;
            int _day;
          };

          image.gif

                 在运算符重载里面需要注意的就是“++”和“--”了,因为这俩个运算符有前置和后置的区别,所以在前置和后置上也作了区分如下(以++为例):

          Date& operator++();//前置
            Date operator++(int);//后置 多了一个int形参,仅作标记,没有实际含义

          image.gif

          5.2 赋值运算符重载

                 赋值运算符重载函数如果没有被显示定义,编译器会自动生成一个默认的赋值运算符重载,拷贝的方式是浅拷贝。与其他运算符重载函数不同的地方是赋值运算符重载必须定义成成员函数,不能定义成全局,如果定义成全局,类体里没有赋值运算符重载就会自动生成,这会与我们在全局定义的发生冲突。

          class Date
          {
          public:
            Date(int year = 1900, int month = 1, int day = 1)
            {
              _year = year;
              _month = month;
              _day = day;
            }
            Date& operator=(const Date& d) //引用传参避免拷贝提高效率               
            {                              //引用返回因为赋值运算符支持连续赋值 d1 = d2 = d3;
              if (this != &d)
              {
                _year= d._year;
                _month = d._month;
                _day = d._day;
              }
              return *this;
            }
          private:
            int _year;
            int _month;
            int _day;
          };

          image.gif

                 如果类型涉及到内存管理就需要深拷贝,这里就与拷贝构造完全相同,不理解的话可以往前看。

          6. 取地址及const取地址操作符重载

                 这俩种运算符重载编译器也会默认生成,绝大部分情况下都不需要我们自己去定义它,除非想让别人获取到指定的内容!

          class Date
          {
          public:
            Date* operator&()
            {
              return this;
            }
            const Date* operator&()const
            {
              return this;
            }
          private:
            int _year;
            int _month;
            int _day;
          };

          image.gif

          7. 附:完整日期类(文章中的代码不取自这里的代码,都是为了讲解知识点临时敲的,这里的代码是完整的日期类(取自比特科技),可以借鉴学习)

          #pragma once
          #include <iostream>
          #include <assert.h>
          using namespace std;
          class Date
          {
            friend ostream& operator<<(ostream& out, const Date& d);
            friend istream& operator>>(istream& in, Date& d);
          public:
            Date(int year = 2023, int month = 8, int day = 10);
            void Print() const;
            int GetMonthDay(int year, int month) const;
            bool operator==(const Date& d) const;
            bool operator!=(const Date& d) const;
            bool operator<(const Date& d) const;
            bool operator<=(const Date& d) const;
            bool operator>(const Date& d) const;
            bool operator>=(const Date& d) const;
            Date& operator+=(int day);
            Date operator+(int day) const;
            Date& operator-=(int day);
            Date operator-(int day) const;
            int operator-(const Date& d) const;
            Date& operator++();
            Date operator++(int);
            Date& operator--();
            Date operator--(int);
          private:
            int _year;
            int _month;
            int _day;
          };
          inline ostream& operator<<(ostream& out, const Date& d)
          {
            out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
            return out;
          }
          inline istream& operator>>(istream& in, Date& d)
          {
            in >> d._year >> d._month >> d._day;
            return in;
          }

          image.gif

          #include"Date.h"
          int Date::GetMonthDay(int year, int month) const
          {
            assert(month > 0 && month < 13);
            int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
            if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
            {
              return 29;
            }
            else
            {
              return monthArray[month];
            }
          }
          Date::Date(int year, int month, int day)
          {
            if (month > 0 && month < 13
              && (day > 0 && day <= GetMonthDay(year, month)))
            {
              _year = year;
              _month = month;
              _day = day;
            }
            else
            {
              cout << "日期非法,初始化失败" << endl;
            }
          }
          void Date::Print() const
          {
            cout << _year << " " << _month << " " << _day << endl;
          }
          bool Date::operator==(const Date& d) const
          {
            return _year == d._year
              && _month == d._month
              && _day == d._day;
          }
          bool Date::operator<(const Date& d) const
          {
            return _year < d._year
              || (_year == d._year && _month < d._month)
              || (_year == d._year && _month == d._month && _day < d._day);
          }
          bool Date::operator<=(const Date& d) const
          {
            return *this < d || *this == d;
          }
          bool Date::operator>(const Date& d) const
          {
            return !(*this <= d);
          }
          bool Date::operator>=(const Date& d) const
          {
            return !(*this < d);
          }
          bool Date::operator!=(const Date& d) const
          {
            return !(*this == d);
          }
          Date& Date::operator+=(int day)
          {
            if (day < 0)
            {
              *this -= -day;
              return *this;
            }
            _day += day;
            while (_day > GetMonthDay(_year, _month))
            {
              _day -= GetMonthDay(_year, _month);
              _month++;
              if (_month == 13)
              {
                ++_year;
                _month = 1;
              }
            }
            return *this;
          }
          Date Date::operator+(int day) const
          {
            Date tmp(*this);
            tmp += day;
            return tmp;
          }
          Date& Date::operator-=(int day) 
          {
            if (day < 0)
            {
              *this += -day;
              return *this;
            }
            _day -= day;
            while (_day <= 0)
            {
              --_month;
              if (_month == 0)
              {
                --_year;
                _month = 12;
              }
              _day += GetMonthDay(_year, _month);
            }
            return *this;
          }
          Date Date::operator-(int day) const
          {
            Date tmp(*this);
            tmp -= day;
            return tmp;
          }
          Date& Date::operator++()
          {
            *this += 1;
            return *this;
          }
          Date Date::operator++(int)
          {
            Date tmp(*this);
            *this += 1;
            return tmp;
          }
          Date& Date::operator--()
          {
            *this -= 1;
            return *this;
          }
          Date Date::operator--(int)
          {
            Date tmp(*this);
            *this -= 1;
            return tmp;
          }
          int Date::operator-(const Date& d) const
          {
            Date max = *this;
            Date min = d;
            int flag = 1;
            if (*this < d)
            {
              max = d;
              min = *this;
              flag = -1;
            }
            int n = 0;
            while (min != max)
            {
              ++min;
              ++n;
            }
            return n*flag;
          }

          image.gif


          相关文章
          |
          6天前
          |
          存储 编译器 C++
          【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
          本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
          33 4
          |
          8天前
          |
          存储 编译器 Linux
          【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
          本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
          27 4
          |
          27天前
          |
          程序员 C++ 容器
          在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
          在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
          |
          30天前
          |
          存储 安全 C++
          【C++打怪之路Lv8】-- string类
          【C++打怪之路Lv8】-- string类
          21 1
          |
          30天前
          |
          存储 编译器 对象存储
          【C++打怪之路Lv5】-- 类和对象(下)
          【C++打怪之路Lv5】-- 类和对象(下)
          27 4
          |
          30天前
          |
          编译器 C语言 C++
          【C++打怪之路Lv4】-- 类和对象(中)
          【C++打怪之路Lv4】-- 类和对象(中)
          23 4
          |
          1月前
          |
          存储 编译器 C++
          【C++类和对象(下)】——我与C++的不解之缘(五)
          【C++类和对象(下)】——我与C++的不解之缘(五)
          |
          1月前
          |
          编译器 C++
          【C++类和对象(中)】—— 我与C++的不解之缘(四)
          【C++类和对象(中)】—— 我与C++的不解之缘(四)
          |
          1月前
          |
          C++
          C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
          C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
          53 1
          |
          1月前
          |
          编译器 C语言 C++
          C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
          C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
          19 1