【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

相关文章
|
5月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
497 55
|
3月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
141 26
|
4月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
72 1
|
7月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
7月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
8月前
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
107 6
|
7月前
|
Java
非静态内部类持有外部类引用导致内存溢出
非静态内部类持有外部类引用导致内存溢出
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
106 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
181 0
下一篇
oss教程