【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(一)

简介: 【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作

一、类的六个默认成员函数

默认成员函数是指用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。

对于空类,并不是什么都没有,编译器会自动默认生成以下六个默认成员函数

二、构造函数

2.1 构造函数概念

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

构造函数目的:默认构造函数是为了解决创建对象,忘记对其对象进行初始化操作,同时解决麻烦地调用Init函数。

造函数虽然名称叫构造,但是目的不是开辟空间创建对象,而是对象初始化

构造函数特性:

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用对应的构造函数
  4. 构造函数支持函数重载

2.2 构造函数分类

无参构造函数与全缺省构造函数、忘记显示写构造函数,编译器默认生成构造函数都称为默认构造函数,在使用过程中默认构造函数只能调用其中一种,这里推荐调用全缺省构造函数

class Date
{
public:
  //1.无参构造函数
  Date()
  {
    _year = 2024;
    _day = 6;
  }
  //2.带参构造函数
  Date(int year,int month,int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;//调用无参构造函数
  Date d2(2024, 3, 6);//调用有参构造函数
  d1.Print();
  d2.Print();
  Date d3();//未调用原型函数(是否有意用变量定义?)
  return 0;
}

关于Date d3(void)报错,由于编译器很难区分对象实例化是调用无参构造函数还是函数声明。为了避免混洗这两种情况,要求对象实例化调用无参构造函数,不允许添加括号

对于无参构造与有参构造,无参构造需要函数内部设置好的数值,而有参构造采用外部实参数值。对于这里两种情况可以考虑合并为全缺省的构造函数。虽然编译器支持全缺省构造函数与无参构造函数同时出现,语法上允许这种行为,但是调用构成中会存在歧义,编译器无法区分(有多种初始化方式,在条件允许实现一个全缺省最好用,比较灵活控制参数

2.3 构造函数对于内置/自定义类型处理方式

C/C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型(int/char/double ),自定义类型就是自己通过关键字定义的类型(struct /class/union)

对于内置与自定义类型处理:

  • 对内置类型不做处理
  • 对自定义类型的成员,会去调用他们的默认构造(无参构造函数、全缺省构造函数、我们没有写编译器默认生成的构成函数)

2.4 编译器默认生成构造函数意义及相关问题

提出疑问:从编译结果来看,无论是显示构造函数或编译器默认生成构造函数,对于内置类型初始化处理为随机值。虽然完成每个对象初始化,但是这些初始化的数值对于我们来说并没有多大意义,是否可以认为编译器默认生成构造函数没有意义呢?同时是否可以认为既然默认生成构造函数,我们什么事情都不用做了呢?

给出回答:我们从对于内置与自定义类型处理上来看,编译器虽然对于内置类型初始化数值为随机值,但是确保了内置类型完成了初始化操作,避免了缺乏构造函数而导致的编译错误。同时我们需要知道无论是内置类型或者是自定义类型,数据都是需要我们自己处理,只不过是间接和直接而已(套娃:所谓的自定义类型不过是包含内置类型,其中可能还有自定义类型,但是自定义类型最后一定是内置类型,是内置类型都需要人去设置处理)

对于编译器默认生成构造函数还有很有价值的,比如在MyQueue里面定义 stack s1stack s2,这里会调用默认构造,完成对象s1s2的初始化(虽然内部还是需要手动设置,但是调用MyQueue就会很爽)

2.5 不对内置类型处理

不对内置类型做处理是语言设计过程中遗留下来问题,在C++11中对于内置类型是否处理有了争执,当然内置类型不处理也可能有它的原因,对此C++11还是保持对内置类型不处理的态度,但是打了补丁,即是:内置类型成员变量在类中声明事可以给缺省值


三、析构函数

3.1 析构函数概念

析构函数与构造函数功能相反,该函数任务并不是完成对象本身销毁(局部对象的销毁时由编译器完成),而是对象在销毁时自动调用析构函数,完成对象中资源的清理工作

这里资源一般指动态开辟的资源,如果没有析构函数进行处理,而是单纯地开辟和销毁对象。没有考虑对象内部申请的动态空间,导致内存泄漏(对象是存储在栈帧上,是由系统进行处理的,也称为自动变量)

从图中也可以观察到动态开辟的资源没有释放掉

析构函数特性:

  1. 析构函数名为同类名前加上字符~
  2. 无参数无返回值类型,导致析构函数不支持重载函数
  3. 一个类只能有一个析构函数。若未显式定义,系统会在自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统自动调用析构函数

3.2 验证是否会自动调用析构函数

析构函数对于内置与自定义类型的处理方式(调用析构函数中this指针存储对象的地址)

对于内置与自定义类型处理:

  • 内置类型不处理
  • 自定义类型成员,调用对应的析构函数

3.3 析构函数处理顺序

关于析构函数顺序涉及到函数栈帧,不知道你们是否注意到上面打印顺序跟栈特性是相关的。那么可以得出两点【先定义、先构造】【后定义、先析构】

class Date
{
public:
  Date(int year=1)
  {
    _year = year;
  }
  ~Date()
  {
    cout << "~Date()->" <<_year<< endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
Date d5(5);//全局对象
static Date d6(6);//全局对象
void func()
{
  Date d3(3);//局部变量
  static Date d4(4);//局部的静态
}
int main()
{
  Date d1(1);//局部变量
  Date d2(2);//局部变量
  func();
  return 0;
}

从中得到结论:

1.局部对象(后定义先析构)

2.局部的静态

3.全局对象(后定义先析构)

析构函数清理细节

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

提问:默认析构函数对内置类型不处理,我就想让析构函数对内置类型进行处理,怎么办?

  1. 对于这个问题,我们可以采用显式析构函数,里面的逻辑是自己设计的,可以要求对内置类型进行操作,但是这样子没有价值。
  2. 内置类型不需要进行资源清除,同时将内置类型全部设置为0,同样没有完成清除的任务,对此在程序结束后,系统会自动回收内置类型的空间,不需要我们多此一举

3.4 调用类中类的析构函数细节

d对象的销毁时,要将其内部包含的Time类的_t对象销毁,但是这里不是直接调用Time类的析构函数。因为实际要释放的是Date类对象,对此调用Date类对象对应的析构函数(编译器默认生成的析构函数),目的是在其内部调用Time。(没有直接调用Time类析构函数,通过Date类中析构函数间接调用)

小结:

  1. 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收
  2. 创建哪个类的对象,则调用该类的析构函数,销毁那个类的对象,则调用该类的析构函数
  3. 关于析构函数是否显示写,主要是看是否存在资源申请,并不是每个类都需要析构。
  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) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    Date d2(d1);
    return 0;
}

拷贝构造函数特性:

  1. 拷贝构造函数本身属于构造函数一种重载,同类型对象进行初始化
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用(编译器可能会强制检查)

4.2 关于对拷贝构造疑问

1.拷贝构造函数为什么只有一个参数?

拷贝构造函数需要拷贝对象参数即可,由于存在this指针,将调用对象地址传进来(编译器会自动处理)

2.为什么传值会引发无穷递归调用呢?是否可以提前写个返回条件进行拦截呢?可以使用指针类型进行接收吗?

通过函数栈帧中学习,传值过程需要开辟空间去拷贝实参数据,这里就需要调用拷贝函数。导致了传值需要调用拷贝构造,调用拷贝构造需要传值的套娃当中。

对于返回条件拦截,实际上这里压根没有进去函数体,返回条件都用不上。指针是可以,但是指针不适合这里。使用引用给实参取别名,指向对象共占用一块内存空间,就不需要拷贝数据去调用拷贝函数,减少拷贝次数

3.使用const修饰引用

  1. 使用const修饰的引用意味着我们不会修改传入的对象。保证被拷贝对象不会被修改,可以及时地报错检查是否位置放反。
  2. 如果拷贝构造传的是const修饰的变量,并且拷贝构造函数 参数部分没使用const修饰,就会造成权限放大


【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)https://developer.aliyun.com/article/1617296

相关文章
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
29 4
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
26 4
|
9天前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
16 3
|
11天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
11天前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
27天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
30天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
30天前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
37 0
【C++打怪之路Lv6】-- 内存管理
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
366 0
|
21天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
44 1