【C++】类与对象(上)

简介: 六个默认成员函数的讲解,学会使用const成员函数,利用代码技巧减少程序的负担⭐⭐

系列文章

之前的文章中讲解了,什么是类、类的实例化,以及封装的意义,若仍有不理解的部分可以移步上一篇文章  【C++】类与对象(引入)


目录

系列文章

1.默认成员函数

2.构造函数

2.1定义

2.2特性

2.2.1重载构造函数

2.2.2与缺省参数混合使用

2.2.3默认构造函数

3.析构函数

3.1定义

3.2特性

4.拷贝构造

5.赋值运算符重载

5.1运算符重载

5.2赋值运算符重载

5.3区分调用时的赋值运算符重载与拷贝构造

6.const成员

7.取地址操作符重载

8.总结


1.默认成员函数

🧀如果一个类中一个成员都没有的话,就称这个类为空类

🧀但空类并不是什么都没有。若用以下的代码查看对象 a 的大小,你会发现输出的结果是 1 而不是 0 。虽然这是个空类,但是系统为了表现他至少存在,便为其分配了 1 字节的空间(占位符)以表示其存在

class A     //声明一个空类
{
};
int main()
{
  A a;
  cout << sizeof(a);  //打印对象a的大小
  return 0;
}

image.gif

不仅如此, 类中还带有六个天选之子,也就是我们说的默认成员函数。只要用户没有显示实现,编译器就会自动生成,下面就开始一个个学习吧。

2.构造函数

2.1定义

🧀在以前,我们使用C语言写数据结构的时候,都会写一个初始化的函数,之后还要手动调用它。显得非常麻烦,而构造函数就相当于实现在类中的初始化函数,不用我们自己手动操作,实例化对象的时候便会自动调用,可谓是十分方便。

🧀百闻不如一见,一起来看看构造函数是怎么实现的。

class A           //定义一个类A
{
public:
  A(int a)      //实现A的构造函数
  {
    _a = a;   //用传入的参数初始化成员a
  }
private:
  int _a;       //定义成员a
};
int main()
{
  A a(5);       //实例化对象a
  return 0;
}

image.gif

2.2特性

🧀构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。总结为以下特性:

    • 函数名与类名称相同。
    • 没有返回值(不是void,而是直接不写)。
    • 对象实例化时自动调用对应的构造函数。
    • 可以重载。

    2.2.1重载构造函数

    🧀正是因为构造函数支持重载,这才使其有多种的初始化方式除了赋初值的方式进行构造,还能够使用无参的构造函数,适用于各个场景。

    class A           //定义一个类A
    {
    public:
      A(int a)      //实现A的构造函数
      {
        _a = a;   //用传入的参数初始化成员a
      }
      A()           //无参的构造函数
      {}
    private:
      int _a;       //定义成员a
    };
    int main()
    {
      A a1(5);       //内部a被初始化成5
      A a2;          //内部a未被初始化,是随机值
      return 0;
    }

    image.gif

    image.gif编辑

    2.2.2与缺省参数混合使用

    🧀但像上面那样写成两个构造函数未免过于麻烦了,现在我们想要一个函数中,如果我们传参就使用传过去的参数进行初始化,若没有传参的话也希望有一个初始值对其进行初始化而不是系统的随机值。

    🧀这时候我们突然想起来,之前学过的缺省参数正好符合我们的目标要求。便可以在定义带参数的构造函数中加上缺省参数,实现目标效果。

    class A           //定义一个类A
    {
    public:
      A(int a = 0)      //实现A的构造函数缺省值为0
      {
        _a = a;   //用传入的参数初始化成员a
      }
    private:
      int _a;       //定义成员a
    };
    int main()
    {
      A a1(5);       //内部a被初始化成5
      A a2;          //内部a被初始化成0
      return 0;
    }

    image.gif

    2.2.3默认构造函数

    🧀我们知道当我们未显式定义时,编译器会自动生成一个默认成员函数,用于对类进行初始化。但这个默认构造函数对内置类型不做处理,对于自定义类型则会去调用其默认构造函数进行构造。

    🧀其中所说的内置类型就是:int char ...以及指针,这种语言提供给我们的数据类型。

    🧀同样,自定义类型就是我们自己定义出来的类、结构体、union这类的类型。

    🧀C++11 中针对内置类型成员不初始化的缺陷,打了补丁:内置类型成员变量在类中声明时可以给默认值

    🧀于是我们就可以像下方代码这样,给特定的成员变量默认值。

    class A           //定义一个类A
    {
    public:
      A(int a)      //实现A的构造函数
      {
        _a = a;    //用传入的参数初始化成员a
      }
      A()            //无参的构造函数
      {}
    private:
      int _a = 0;       //定义成员a给定初始值为0
    };
    int main()
    {
      A a1(5);       //内部a被初始化成5
      A a2;          //内部a被初始化成0
      return 0;
    }

    image.gif

    [注意]

    🧀无参的构造函数全缺省的构造函数都称为默认构造函数无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认构造函数。并且默认构造函数只能有一个,若全缺省和无参数的构造函数同时出现,则在无参实例化对象的时候出现歧义。因此二者不能同时出现。

    image.gif编辑

    3.析构函数

    3.1定义

    🧀析构函数与构造函数的功能正相反,析构函数不是完成对对象本身的销毁,而是对象在销毁时会自动调用析构函数,就像以前写的 destory 函数,完成对象中资源的清理工作。

    3.2特性

      • 析构函数名就是 ~+类名
      • 没有参数,也没有返回值
      • 一个类只能有一个析构函数,且不能重载
      • 在对象生命周期结束时,C++编译系统自动调用。

      🧀我们用如下代码验证析构函数在对象销毁时会自动调用:

      class A
      {
      public:
        A()
        {
          cout << "调用构造函数:A()" << endl;   //调用构造函数的话就输出
        }
        ~A()
        {
          cout << "调用析构函数:~A()" << endl;  //调用析构函数的话就输出
        }
      };
      int main()
      {
        A a;
        return 0;
      }

      image.gif

      🧀可以直接看到程序结束时, 编译器自动地调用了目标类的析构函数:

      image.gif编辑

      🧀不仅如此,若该类中若有自定义类型的成员变量,则会调用该自定义类型成员变量的析构函数。

      image.gif编辑

      class B
      {
      public:
        ~B()
        {
          cout << "调用B的析构函数:~B()" << endl;  //调用B的析构函数的话就输出
        }
      };
      class A
      {
      public:
        A()
        {
          cout << "调用A的构造函数:~A()" << endl;    //调用构造函数的话就输出
        }
        ~A()
        {
          cout << "调用A的析构函数:~A()" << endl;   //调用A的析构函数的话就输出
        }
      private:
        B b;     //有个自定义的成员变量
      };
      int main()
      {
        A a;
        return 0;
      }

      image.gif

      🧀小结:创建哪个类的对象则调用那个类的构造函数,销毁那个类的对象也调用该类的析构函数

      4.拷贝构造

      拷贝构造其实是构造函数的一个重载形式,用于创建一个与当前已存在对象一模一样的对象。在用已存在的类类型对象创建新对象时由编译器自动调用。

      🧀拷贝构造函数的参数只有一个且必须是类类型对象的引用,由于是拷贝因此不修改原对象的值所以一般还使用 const 修饰形参。

      class A
      {
      public:
        A(int a = 5)     //A的构造函数
        {
          _a = a;
        }
        A(const A& a)    //A的拷贝构造函数
        {
          _a = a._a;
        }
      private:
        int _a;         //A的成员变量
      };
      int main()
      {
        A a;            //实例化a
        A a1(a);        //用a拷贝构造a1
        //A a1 = a;     //也可以这样写
        return 0;
      }

      image.gif

      🧀若使用传值方式传递拷贝构造函数的参数,编译器会直接报错,因为会引发无限递归调用

      🧀我们都知道,在函数之中形参是实参的一份临时拷贝,当我们通过传值传参给拷贝构造函数时,系统会调用拷贝构造函数生成形参。因为又调用了拷贝构造函数所以又要生成形参,为了生成形参又要调用拷贝构造函数。如此反复就会产生无限递归的情况。因此,使用引用作为拷贝构造函数的参数就能够解决当前问题。因为引用就是原来对象的别名,因此不会再次调用拷贝构造函数。

      image.gif编辑

      🧀若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝。

      🧀就像这样,我们并没有直接写 A 的拷贝构造函数,但是系统的默认生成的拷贝构造函数还是能够实现对对象的拷贝。其中对于内置类型则直接拷贝,而自定义类型则会调用对应的拷贝构造函数

      image.gif编辑

      🧀但是否编译器生成的默认拷贝构造函数就能够满足我们的需求呢?答案是否定的,当类涉及资源申请的时候,默认拷贝构造函数就无法完成任务。

      class A
      {
      public:
        A()
        {
          _a = (int*)malloc(sizeof(int));  //动态开辟空间
        }
        ~A()
        {
          free(_a);    //释放_a
        }
      private:
        int* _a;         //A的成员变量
      };
      int main()
      {
        A a;            //实例化a
        A a1(a);        //用a拷贝构造a1
        return 0;
      }

      image.gif

      若运行上文的代码,系统便会崩溃报错, 这是为什么呢?通过调试我们能够观察到:两个类中的指针是一样的。这意味着在拷贝构造的时候,默认的拷贝构造只是将数值给了新构造类中的变量,并没有再次动态开辟内存,因此最后调用析构函数时,由于只开辟了一块空间却释放了两次,因此在free处报错。

      image.gif编辑

      🧀若我们自己实现一个拷贝构造函数为变量开辟空间,程序便不会崩溃报错了。

      class A
      {
      public:
        A()
        {
          _a = (int*)malloc(sizeof(int));  //动态开辟空间
        }
        A(const A& a)
        {
          _a = (int*)malloc(sizeof(int));  //动态开辟
          *_a = *a._a;                     //赋值拷贝
        }
        ~A()
        {
          free(_a);    //释放_a
        }
      private:
        int* _a;         //A的成员变量
      };
      int main()
      {
        A a;            //实例化a
        A a1(a);        //用a拷贝构造a1
        return 0;
      }

      image.gif

      拷贝构造函数典型调用场景:

        • 使用已存在对象创建新对象
        • 函数参数类型为类类型对象
        • 函数返回值类型为类类型对象

        🧀每次使用传值传参都要调用一次拷贝构造,其代价是随着代码复杂度而上升的,为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时则根据实际场景,能用引用尽量使用引用。

        5.赋值运算符重载

        5.1运算符重载

        🧀 C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似,使得自定义对象可以使用运算符

        🧀函数原型:返回值类型 + operator + 重载的操作符 + (参数列表)

        class A
        {
        public:
          A(int a = 5)
          {
            _a = a;
          }
          int operator+(const A& a)  //重载+号
          {
            return _a + a._a;
          }
        private:
          int _a;         //A的成员变量
        };
        int main()
        {
          A a(3);            //实例化a
          A a1(a);           //用a拷贝构造a1
          cout << a + a1;    
          return 0;
        }

        image.gif

        🧀凡事都有例外,需注意以下五点:

          • 不能通过连接其他符号来创建新的操作符。
          • 重载操作符必须有一个类类型参数。
          • 用于内置类型的运算符,不能改变其含义,例如:内置的整型+,不能改变其含义。
          • 作为类成员函数重载时,其形参看起来比操作数数目少1因为成员函数的第一个参数为隐藏的this
          • .*    ::   sizeof    ? :     .   以上5个运算符不能重载。

          5.2赋值运算符重载

          🧀赋值运算符重载就是对 ‘=’ 的重载,需要注意以下细节:

            • 为了能够多次连续赋值,应将自身作为返回值
            • 检测是否给自己赋值
            • 只能重载成类的函数而不能重载成全局函数
            class A
            {
            public:
              A(int a = 5)
              {
                _a = a;
              }
              A& operator=(const A& a)  //赋值重载
              {
                    if(&a == this) return *this   //检测是否给自己赋值
                _a = a._a;                    //赋值
                return *this;                 //返回自己
              }
              void print()
              {
                cout << _a << endl;
              }
            private:
              int _a;         //A的成员变量
            };
            int main()
            {
              A a(3);            //实例化a
              A a1(5);           //实例化a1
              a.print();
              a1.print();
              a = a1;            //用a1赋值a
              a.print();
              return 0;
            }

            image.gif

            🧀如此便完成了赋值运算符重载的实现。

            🧀用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

            🧀与拷贝构造函数相同,当类中涉及动态开辟时,默认的运算符重载的缺点就暴露出来了。如果类中未涉及到资源管理,赋值运算符是否实现都可以,一旦涉及到资源管理则必须要实现

            5.3区分调用时的赋值运算符重载与拷贝构造

            🧀我们知道,拷贝构造还有以下这种写法,不禁让人想对比其与运算符重载之间的区别。

            A a1 = a;

            image.gif

            🧀拷贝的双方:赋值运算符重载的两个对象都是已经实例化的对象,而拷贝构造中是以一个已经实例化的对象为基础来实例化一个新的对象

            区分的小细节:只要看当前行是否存在对象的声明,即找当前行是否出现了类名,若出现则是拷贝构造,否则为赋值运算符重载。

            A a1(a);     //调用拷贝构造
            a = a1;      //调用赋值重载
            A a1 = a;    //调用拷贝构造

            image.gif

            6.const成员

            🧀将const修饰的成员函数称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改。

            🧀例如我们此时有一个 const A 类型的对象,若我们直接调用我们的一个打印函数,编译器会报错,因为会出现权限放大

            image.gif编辑

            🧀放作平时,我们可以直接给参数列表里的参数加上一个 const ,这样既能避免权限放大,同时普通的对象也能够调用这个函数

            🧀但类中的这个函数没有参数,若想修饰其中的 this 指针就需要使用 const 来修饰函数,进而避免权限放大。

            🧀为了函数的泛用性,内部不改变成员变量的成员函数,最好用 const 来修饰

            7.取地址操作符重载

            🧀取地址操作符重载包括了 取地址及 const 取地址操作符重载,一般不用重新定义 ,编译器默认会生成。可以这么写但是意义不大。

            class A
            {
            public:
              A(int a = 5)
              {
                _a = a;
              }
              A* operator&()                //取地址操作符重载
              {
                return this;
              }
              const A* operator&() const    //const取地址操作符重载
              {
                return this;
              }
            private:
              int _a;         //A的成员变量
            };
            int main()
            {
              const A a(3);    //实例化a
              A a1(5);
              printf("%p\n%p\n", &a, &a1);
              return 0;
            }

            image.gif

            🧀若想让别人获得特定的内容,就可以使用这个重载。 无论怎么取该对象的地址返回的都是空指针。

            class A
            {
            public:
              A(int a = 5)
              {
                _a = a;
              }
              A* operator&()                //取地址操作符重载
              {
                return nullptr;           //无论如何返回空指针
              }
              const A* operator&() const    //const取地址操作符重载
              {
                return nullptr;
              }
            private:
              int _a;         //A的成员变量
            };
            int main()
            {
              const A a(3);    //实例化a
              A a1(5);
              printf("%p\n%p\n", &a, &a1);
              return 0;
            }

            image.gif

            8.总结

            今天简单地讲了类中 6 个默认成员函数、运算符重载以及 const 成员函数。需要具体区分的是拷贝构造函数赋值运算符重载,前面四个函数都要了解清楚其性质以及如何实现,并落实到代码上。而最后两个倒没有那么重要作为了解就可以。学会利用一些代码的技巧去减少程序的负担,就比如上文讲到的,情况允许的话使用引用传参和返回

            好了,今天类与对象上半部分讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

            目录
            相关文章
            |
            19天前
            |
            存储 编译器 C语言
            【c++丨STL】string类的使用
            本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
            29 2
            |
            25天前
            |
            存储 编译器 C++
            【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
            本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
            61 5
            |
            1月前
            |
            存储 编译器 C++
            【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
            本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
            66 4
            |
            1月前
            |
            存储 编译器 Linux
            【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
            本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
            76 4
            |
            2月前
            |
            存储 编译器 对象存储
            【C++打怪之路Lv5】-- 类和对象(下)
            【C++打怪之路Lv5】-- 类和对象(下)
            30 4
            |
            2月前
            |
            编译器 C语言 C++
            【C++打怪之路Lv4】-- 类和对象(中)
            【C++打怪之路Lv4】-- 类和对象(中)
            26 4
            |
            2月前
            |
            存储 安全 C++
            【C++打怪之路Lv8】-- string类
            【C++打怪之路Lv8】-- string类
            25 1
            |
            2月前
            |
            存储 编译器 C语言
            【C++打怪之路Lv3】-- 类和对象(上)
            【C++打怪之路Lv3】-- 类和对象(上)
            17 0
            |
            2月前
            |
            存储 编译器 C++
            【C++类和对象(下)】——我与C++的不解之缘(五)
            【C++类和对象(下)】——我与C++的不解之缘(五)
            |
            2月前
            |
            编译器 C++
            【C++类和对象(中)】—— 我与C++的不解之缘(四)
            【C++类和对象(中)】—— 我与C++的不解之缘(四)