【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

相关文章
|
1月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
58 12
|
2月前
|
消息中间件 Java 应用服务中间件
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
|
2月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
2月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
2月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
2月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
152 6
|
2月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
2月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
3月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
197 0
|
3月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。

热门文章

最新文章