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

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

4、特性分析 – 深浅拷贝

赋值重载函数的特性和拷贝构造函数非常类似 – 如果我们没有显式定义赋值重载,则编译器会自动生成一个赋值重载,且自动生成的函数对内置类型以字节为单位直接进行拷贝,对自定义类型会去调用其自身的赋值重载函数

所以对于没有资源申请的类来说,我们不用自己去写赋值重载函数,直接使用默认生成的即可,因为这种类只需要进行浅拷贝 (值拷贝),比如 Date 类:

2020062310470442.png注:拷贝构造函数完成的是初始化工作,在创建对象时自动调用;赋值重载完成的是已存在的对象之间的拷贝,需要手动调用;而上图中 Date d2 = d1 是在创建 d2 并对其进行初始化,所以调用的是拷贝构造函数;d3 才是调用赋值重载函数;

而对于有资源申请的类来说,我们必须自己手动实现赋值重载函数,来完成深拷贝工作;比如 Stack 类:

2020062310470442.png

20200623104134875.png

如图:这里的情况和 Stack 默认析构函数的情况很类似,但是比它要严重一些 – 自动生成的赋值重载函数进行浅拷贝,使得 st1._a 和 st2._a 指向同一块空间,而 st1 和 st2 对象销毁时编译器会自动调用析构函数,导致 st2._a 指向的空间被析构两次;同时,st1._a 原本指向的空间并没有被释放,所以还发生了内存泄漏;


所以,对于有资源申请的类我们都需要显式定义赋值重载函数;Stack 类的赋值重载函数如下:

//赋值重载
Stack& operator=(const Stack& st)
{
    free(_a);
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
        perror("malloc fail\n");
        exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._capacity);
    _top = st._top;
    _capacity = st._capacity;
    return *this;
}

2020062310470442.png

对于上面这段程序,可能有的同学会有这样一种疑问:我们可不可以直接对 st1._a 进行扩容呢?那样就不必释放后再出现申请空间了;答案是:直接扩容不是不行,但是不好,因为如果当 st1._capacity 大于 st2._capacity ,我们这时调用 realloc 就是缩容,而缩容需要重新开辟空间并拷贝原数据,效率太低;而如果面对这种情况我们不缩小空间直接拷贝数据的话又会造成空间的浪费;所以先释放原空间再开辟新空间是一种折中的办法;

现在我们为 Stack 类显示定义了赋值重载函数,那么我们再来运行一个新的测试用例:

2020062310470442.png

我们发现,当我们使用 st2 自己给自己赋值时,st2._a 中的数据变成了随机值;原因如下:operator= 函数首先会将 st2._a 指向的空间释放,然后再为其申请新空间,但是由于 st2 自己给自己赋值,所以使用 memcpy 拷贝的是新开辟的空间中的数据,即随机值;

2020062310470442.png

所以说,在赋值重载函数的函数格式规范中我们强调一定要检查自我赋值;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;
  }
  ~Stack()  //析构
  {
    free(_a);
    _a = NULL;
    _top = _capacity = 0;
  }
  Stack(const Stack& st)  //拷贝构造
  {
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail\n");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._capacity);
    _top = st._top;
    _capacity = st._capacity;
  }
  Stack& operator=(const Stack& st)  //赋值重载
  {
    //自我赋值
    if (this == &st)
    {
      return *this;
    }
    free(_a);
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail\n");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._capacity);
    _top = st._top;
    _capacity = st._capacity;
    return *this;
  }
  void Push(int x)
  {
    _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};

另外,和拷贝构造一样,并不是说只要有资源申请我们就必须写赋值重载函数,比如 MyQueue 类,我们不写编译器调用默认生成的赋值重载函数,而默认生成的对于自定义类型会去调用它们自身的赋值重载函数;

2020062310470442.png

总结

自动生成的赋值重载函数对成员变量的处理规则和析构函数一样 – 对内置类型以字节方式按值拷贝,对自定义类型调用其自身的赋值重载函数;我们可以理解为:需要写析构函数的类就需要写赋值重载函数,不需要写析构函数的类就不需要写赋值重载函数

七、取地址及 const 取地址重载

1、const 成员函数

我们将 const 修饰的 “成员函数” 称之为 const 成员函数,const 修饰类成员函数实际上修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对 this 指向的类中的任何成员变量进行修改

我们以Date类为例:

2020062310470442.png

我们看到,当我们定义了一个只读的Date对象 d2 时,我们再去调用 d2 的成员函数 Print 和 operator+ 时编译器会报错;原因在于类成员函数的第一个参数默认是 this 指针,而 this 指针的类型是 Date const*,而我们的第一个参数即 d2 的类型是 const Date*;将一个只读变量赋值给一个可读可写的变量时权限扩大,导致编译器报错;


注:成员函数默认第一个参数为 Date* const this,这里的 const 别放在 * 号后面,修饰的是 this 本身,表示 this 不能被修改,而 this 指向的内容即 d2 可以被修改;


另外,上面这个问题除了在定义对象时出现之外,在成员函数中也会出现,且十分频繁,特别是运算符重载 – 当运算符重载的两个参数都是类的对象时,如果我们不会改变类的内容,比如只比较大小,我们通常会将函数形参定义为 const Date& 类型,这时候问题就出现了:


我们不能在该成员函数中调用第二个对象的其他成员函数,因为在当前函数中该对象的类型为 const Date,当其调用其他成员函数时自身会作为第一个参数传递给成员函数的 this 指针,而 this 的类型为 Date* const,这时候又会发生权限扩大;

image.png

为了解决上面这个问题,C++ 允许我们定义 const 成员函数,即在函数最后面使用 const 修饰,该 const 只修饰函数的第一个参数,即使得 this 指针的类型变为 const Date const*;函数的其他参数不受影响;

2020062310470442.png

20200623104134875.png

将成员函数的 this 指针类型修饰为 const Date* const 后,不仅 const Date 的对象可以调用相应成员函数;正常的 Date 对象也可以调用,因为权限虽然不能扩大,但能缩小;

2020062310470442.png

所以,当我们在实现一个类时,如果我们不需要改变类的成员函数的第一个参数,即不改变 *this,那么我们就应该使用 const 来修饰 this 指针,以便类的 const 对象在其他 (成员) 函数中也可以调用本函数*

以Date为例:

class Date
{
public:
  //构造
  Date();
  //获取每一个月的天数
  int GetMonthDay(int year, int month) const;
  //获取日期对应天数
  int GetDateDay() const;
  //打印
  void Print() const;
  //运算符重载
  //+=
  Date& operator+=(int day);
  //+
  Date operator+(int day) const;
  //-=
  Date& operator-=(int day);
  //-
  Date operator-(int day) const;
  //前置++
  Date& operator++();
  //后置++
  Date operator++(int);
  //前置--
  Date& operator--();
  //后置--
  Date operator--(int);
  //日期-日期
  int 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;
  //!=
  bool operator!=(const Date& d) const;
private:
  int _year;
  int _month;
  int _day;
};

如上,不需要改变 *this 内容的 (即不改变指向对象的成员变量) 成员函数全部使用 const 修饰;

最后,我们来做几个思考题:

    const对象可以调用非const成员函数吗?-- 不可以,权限扩大;

    非const对象可以调用const成员函数吗?-- 可以,权限缩小;

    const成员函数内可以调用其它的非const成员函数吗?-- 不可以,权限扩大;

    非const成员函数内可以调用其它的const成员函数吗?-- 可以,权限缩小;

    2、取地址重载

    取地址重载函数是C++的默认六个成员函数之一,同时它也是运算符重载的一种,它的作用是返回对象的地址;

    Date* operator&()
    {
        return this;
    }

    2020062310470442.png

    3、const 取地址重载

    const 取地址重载也是C++的默认六个成员函数之一,它是取地址重载的重载函数,其作用是返回 const 对象的地址;

    const Date* operator&() const
    {
        return this;
    }

    2020062310470442.png

    如果我们没有显式定义取地址重载和 const 取地址重载函数,那么编译器会自动生成,因为这两个默认成员函数十分固定,所以大多数情况下我们直接使用编译器默认生成的即可,不必自己定义;

    某些极少数的特殊情况下需要我们自己实现取地址重载与 const 取地址重载函数,比如不允许获取对象的地址,那么在函数内部我们直接返回 nullptr 即可:

    //取地址重载
    Date* operator&()
    {
        return nullptr;
    }
    //const 取地址重载
    const Date* operator&() const
    {
        return nullptr;
    }

    2020062310470442.png

    八、总结

    C++的类里面存在六个默认成员函数 – 构造、析构、拷贝构造、赋值重载、取地址重载、const 取地址重载,其中前面四个函数非常重要,也非常复杂,需要我们根据具体情况判断是否需要显式定义,而最后两个函数通常不需要显示定义,使用编译器默认生成的即可;

    1、构造函数


      构造函数完成对象的初始化工作,由编译器在实例化对象时自动调用;

    默认构造函数是指不需要传递参数的构造函数,一共有三种 – 编译器自动生成的、显式定义且无参数的、显式定义且全缺省的;

    如果用户显式定义了构造函数,那么编译器会根据构造函数的内容进行初始化,如果用户没有显式定义,那么编译器会调用默生成的构造函数;

    默认生成的构造函数对内置类型不处理,对自定义类型会去调用自定义类型的默认构造;

    为了弥补构造函数对内置类型不处理的缺陷,C++11打了一个补丁 – 允许在成员变量声明的地方给缺省值;如果构造函数没有对该变量进行初始化,则该变量会被初始化为缺省值;

    构造函数还存在一个初始化列表,初始化列表的存在有着非常大的意义,具体内容我们在 [类和对象下篇] 讲解;

    2、析构函数

      析构函数完成对象中资源的清理工作,由编译器在销毁对象时自动调用;

      如果用户显式定义了析构函数,编译器会根据析构函数的内容进行析构;如果用户没有显示定义,编译器会调用默认生成的析构函数;

      默认生成的析构函数对内置类型不处理,对自定义类型会去调用自定义类型的析构函数;

      如果类中有资源的申请,比如动态开辟空间、打开文件,那么需要我们显式定义析构函数;


      3、拷贝构造


        拷贝构造函数是用一个已存在的对象去初始化另一个正在实例化的对象,由编译器在实例化对象时自动调用;

      拷贝构造的参数必须为引用类型,否则编译器报错 – 值传递会引发拷贝构造函数的无穷递归;

      如果用户显式定义了拷贝构造函数,编译器会根据拷贝构造函数的内容进行拷贝;如果用户没有显示定义,编译器会调用默认生成的拷贝构造函数;

      默认生成的拷贝构造函数对于内置类型完成值拷贝 (浅拷贝),对于自定义类型会去调用自定义类型的拷贝构造函数;

      当类里面有空间的动态开辟时,直接进行值拷贝会让两个指针指向同一块动态内存,从而使得对象销毁时对同一块空间析构两次;所以这种情况下我们需要自己显式定义拷贝构造函数完成深拷贝;

      4、运算符重载


        运算符重载是C++为了增强代码的可读性而引入的语法,它只能对自定义类型使用,其函数名为 operator 关键字加相关运算符;

      由于运算符重载函数通常都要访问类的成员变量,所以我们一般将其定义为类的成员函数;同时,因为类的成员函数的一个参数为隐藏的 this 指针,所以其看起来会少一个参数;

      同一运算符的重载函数之间也可以构成函数重载,比如 operator++ 与 operator++(int);

      5、赋值重载


        赋值重载函数是将一个已存在对象中的数据赋值给另一个已存在的对象,注意不是初始化,需要自己显示调用;它属于运算符重载的一种;

      如果用户显式定义了赋值重载函数,编译器会根据赋值重载函数的内容进行赋值;如果用户没有显示定义,编译器会调用默认生成的赋值重载函数;

      默认生成的赋值重载函数对于内置类型完成值拷贝 (浅拷贝),对于自定义类型会去调用自定义类型的赋值重载函数;

      赋值重载函数和拷贝构造函数一样,也存在着深浅拷贝的问题,且其与拷贝构造函数不同的地方在于它还很有可能造成内存泄漏;所以当类中有空间的动态开辟时我们需要自己显式定义赋值重载函数来释放原空间以及完成深拷贝;

      为了提高函数效率与保护对象,通常使用引用作参数,并加以 const 修饰;同时为了满足连续赋值,通常使用引用作返回值,且一般返回左操作数,即 *this;

      赋值重载函数必须定义为类的成员函数,否则编译器默认生成的赋值重载会与类外自定义的赋值重载冲突;

      6、const 成员函数


        由于指针和引用传递参数时存在权限的扩大、缩小与平移的问题,所以 const 类型的对象不能调用成员函数,因为成员函数的 this 指针默认是非 const 的,二者之间传参存在权限扩大的问题;

      同时我们为了提高函数效率以及保护对象,一般都会将成员函数的第二个参数使用 const 修饰,这就导致了该对象在成员函数内也不能调用其他成员函数;

      为了解决这个问题,C++设计出了 const 成员函数 – 在函数最后面添加 const 修饰,该 const 只修饰 this 指针,不修饰函数的其他参数;

      所以如果我们在设计类时,只要成员函数不改变第一个对象,我们建议最后都使用 const 修饰;

      7、取地址重载与 const 取地址重载

      • 取地址重载与 const 取地址重载是获取一个对象/一个只读对象的地址,需要自己显式调用;它们属于运算符重载,同时它们二者之间还构成函数重载;
      • 大多数情况下我们都不会去显示实现这两个函数,使用编译器默认生成的即可;只有极少数情况需要我们自己定义,比如防止用户获取到一个对象的地址;
      相关文章
      |
      2天前
      |
      编译器 C语言 C++
      类和对象的简述(c++篇)
      类和对象的简述(c++篇)
      |
      1月前
      |
      C++ 芯片
      【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
      声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
      68 19
      |
      1月前
      |
      存储 编译器 数据安全/隐私保护
      【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
      声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
      50 13
      |
      1月前
      |
      编译器 数据安全/隐私保护 C++
      【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
      本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
      52 5
      |
      1月前
      |
      存储 算法 搜索推荐
      【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
      1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
      40 5
      |
      1月前
      |
      Serverless 编译器 C++
      【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
      本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
      48 4
      |
      1月前
      |
      设计模式 IDE 编译器
      【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
      本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
      32 3
      |
      3月前
      |
      存储 编译器 C语言
      【c++丨STL】string类的使用
      本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
      89 2
      |
      3月前
      |
      存储 编译器 C++
      【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
      本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
      156 5
      |
      3月前
      |
      存储 编译器 C++
      【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
      本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
      170 4