【C++指南】类和对象(二):类的默认成员函数——全面剖析 :构造函数

           ???? 博客主页:倔强的石头的****主页 

           ????Gitee主页:倔强的石头的gitee主页

            ⏩ 文章专栏:《C++指南》

                                  期待您的关注

 

47f09392526c71b5885ec838a3ea7ffe.gif

 

阅读本篇文章之前,你需要具备的前置知识:类和对象的基础

点击下方链接

【C++指南】类和对象(一):类和对象的定义和使用 基础讲解-****博客

目录

引言

默认成员函数的介绍

深入解析C++类的构造函数

构造函数的概念

构造函数的特性

默认构造函数

编译器默认生成的构造函数的行为

 

需要自己实现构造函数的情况

  初始化列表

构造函数与析构函数的关系

结尾


 

引言

在C++编程中,类的设计是实现面向对象编程(OOP)理念的核心。类不仅封装了数据(即属性)和操作这些数据的方法(即成员函数),还通过特定的成员函数——默认成员函数,管理着对象的生命周期和状态变化。

这些默认成员函数,包括构造函数、析构函数、拷贝构造函数以及赋值运算符重载函数,是C++类设计中不可或缺的部分,它们定义了对象如何被创建、销毁、复制以及赋值。

 

  • 构造函数是对象生命周期的起点,负责初始化对象的内部状态。
  • 析构函数则标志着对象生命周期的结束,用于执行必要的清理工作,确保资源得到妥善管理。
  • 拷贝构造函数赋值运算符重载函数则与对象的复制行为紧密相关,它们定义了如何创建一个对象的副本以及如何将一个对象的状态复制到另一个对象上。 

 

理解并正确实现这些默认成员函数对于编写健壮、可维护的C++代码至关重要。它们不仅影响着对象的性能,还直接关系到程序的安全性和正确性。然而,这些函数的自动生成和默认行为往往无法满足所有情况的需求,特别是在涉及资源管理、动态内存分配或复杂数据结构时。因此,作为C++开发者,我们有必要深入了解这些默认成员函数的工作原理,学会在适当的时候自定义它们以满足特定的需求。

 

本文旨在详细讲解C++中类的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载函数以及取地址运算符重载函数的基本概念、使用场景、实现细节和注意事项。

通过本文的学习,读者将能够更加熟练地运用这些默认成员函数,编写出更加高效、安全、易于维护的C++代码。

 

注:

因默认构造函数内容较多,限于篇幅分成系列文章发布:

 

可点击下方链接阅读:

【C++指南】类和对象(三):类的默认成员函数——全面剖析析构函数-****博客

【C++指南】类和对象(四):类的默认成员函数——全面剖析 拷贝构造函数-****博客

【C++指南】类和对象(五):类的默认成员函数——全面剖析 赋值运算符重载函数-****博客

 

 

默认成员函数的介绍

在C++中,当定义一个类时,编译器会自动为该类生成几个特殊的成员函数,如果开发者没有显式定义它们的话。这些函数被称为默认成员函数特殊成员函数。它们对于类的对象管理至关重要,包括对象的创建、销毁、复制以及赋值操作。

以下是几个关键的默认成员函数:

  1. 构造函数(Constructor):用于在创建对象时初始化对象。
  2. 析构函数(Destructor):用于在对象生命周期结束时执行清理工作。
  3. 拷贝构造函数(Copy Constructor):用于创建一个新对象,作为已存在对象的副本。
  4. 赋值运算符重载函数(Assignment Operator Overloading):用于实现对象之间的赋值操作。
  5. 取地址运算符重载函数:实际上,C++标准中并没有直接为取地址运算符(&)提供默认的重载机制,因为对象的地址总是由编译器自动处理。但理解何时需要重载其他运算符(如*,对于指针类)对于完整理解运算符重载是有帮助的。这里我们将重点放在前四个默认成员函数上。

 

类的默认成员函数虽然看起来复杂,但其实一点也不简单。 掌握默认成员函数对于后期的学习是非常重要的基础,想要理解透彻,除了默认成员函数本身的特性,还应该从两个方面入手:

  • 第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
  • 第⼆:编译器默认⽣成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现

 

深入解析C++类的构造函数

构造函数的概念

C++中的构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的数据成员。构造函数的主要任务是在对象实例化时,根据提供的参数(如果有的话)来设置对象的初始状态。要注意构造函数的主要任务并 不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化。 对象构造函数的名字与类名完全相同,并且没有返回类型(连void也不允许)。

 

构造函数的特性

  1. 名字与类名相同:构造函数的名字必须与类名完全一致,包括大小写。
  2. 无返回类型:构造函数不能有任何返回类型,包括void
  3. 自动调用:在创建对象时,编译器会自动调用相应的构造函数。
  4. 可以重载:一个类可以有多个构造函数,只要它们的参数列表不同,就可以实现重载。
  5. 默认构造函数:如果程序员没有显式定义任何构造函数,编译器会自动生成一个默认的无参构造函数。但一旦定义了任何构造函数,编译器就不会再自动生成默认构造函数。
  6. 构造初始化列表:构造函数可以使用初始化列表来给成员变量赋值,这种方式比在构造函数体内赋值更高效。

默认构造函数

默认构造函数是没有参数或者所有参数都有默认值的构造函数。如果类中没有显式定义任何构造函数,编译器会自动生成一个默认的无参构造函数。

无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。

无参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。要注意很多人会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造

示例: 

class Date
{
public:
    // 1.无参构造函数

    /*Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }*/
     //2.全缺省构造函数

   Date(int year = 1, int month = 1, int day = 1)
   {
   _year = year;
   _month = month;
   _day = day;
   }
    // 3.带参构造函数

   // 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;
};

比如上述类的定义中:我们定义了三个构造函数,其中第一个、第二个以及不写构造函数时编译器默认生成的构造函数,都属于默认构造函数

一般情况下,建议写一个全缺省的构造函数,这样就可以应对各种传参的情况

第三个构造函数——带参的构造函数,就属于正常的构造函数 

 

再次强调:

无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,这三个函数有且只有⼀个存在,不能同时存在。

当自己实现了构造函数之后,编译器就不会生成默认构造函数,而且无参构造函数和全缺省构造函数虽然构成 函数重载,但是调用时会存在歧义,两个构造函数只能存在一个,否则就会报错。

 

编译器默认生成的构造函数的行为

我们不写,编译器默认⽣成的构造

  • 对内置类型成员变量的初始化没有要求,也就是说是否初始 化是不确定的,看编译器。
  • 对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决

 

需要自己实现构造函数的情况

结论: 大多数情况下,构造函数都需要自己实现

比如:

  1. 类中存在内置类型,需要在构造时初始化赋值(这种情况多数存在)
  2. 类初始化需要申请资源,比如通过指针指向一块动态申请的空间

少数情况下不需要写构造函数:

比如:当类中的成员变量全部为自定义类型(类类型),会自动调用成员变量的默认构造函数,这时不需要自己实现构造函数

 

  初始化列表

构造函数初始化列表是构造函数体执行之前执行的一部分,用于初始化成员变量。它使用冒号:分隔参数列表和成员初始化列表。使用初始化列表比在构造函数体内赋值通常更高效,因为它直接调用成员的构造函数(如果有的话)。

MyClass(int x) : member(x) {} // 使用初始化列表初始化member
class ClassName {  
public:  
    ClassName(parameters) : member1(initializer1), member2(initializer2), ... {  
        // 构造函数体  
    }  
  
private:  
    MemberType1 member1;  
    MemberType2 member2;  
    // 其他成员变量  
};

 构造函数初始化列表的规则

  • 语法理解上初始化列表可以认为是每个成员变量定义 初始化的地方
  • 引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始 化,否则会编译报错
  1. 对于常量成员变量,它们必须在构造函数的初始化列表中初始化,因为常量一旦被定义就不能被修改。
  2. 对于引用成员变量,它们也必须在初始化列表中初始化,因为引用一旦被定义就必须指向一个有效的对象。
  3. 对于某些类型的对象(如没有默认构造函数的类类型对象),它们可能需要在初始化列表中通过特定的值或另一个对象的拷贝来初始化。
  • C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的 成员使⽤的
  • 初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆ 关。建议声明顺序和初始化列表顺序保持⼀致

 

初始化列表的使用示例

#include <iostream>  
#include <string>  
  
// 定义一个简单的类Point,表示二维空间中的一个点  
class Point {  
private:  
    int x; // x坐标  
    int y; // y坐标  
  
public:  
    // 构造函数使用初始化列表来初始化成员变量x和y  
    Point(int xVal, int yVal) : x(xVal), y(yVal) {  
        // 构造函数体可以为空,因为成员变量已经在初始化列表中初始化了  
        std::cout << "Point constructed at (" << x << ", " << y << ")" << std::endl;  
    }  
  
    // 一个方法来打印点的坐标  
    void print() const {  
        std::cout << "Point is at (" << x << ", " << y << ")" << std::endl;  
    }  
};  
  
int main() {  
    // 创建一个Point对象,并使用初始化列表来设置x和y的值  
    Point p1(5, 10);  
  
    // 调用print方法来打印点的坐标  
    p1.print();  
  
    return 0;  
}

 

 

构造函数的执行顺序参考图

a3d80ef6151a459ba196beda1feb8936.png

 

构造函数与析构函数的关系

  • 构造函数和析构函数是对象生命周期的两端。
  • 构造函数负责初始化对象,而析构函数负责清理对象所占用的资源。
  • 一旦对象被创建,其构造函数就会被调用一次,并且在对象的整个生命周期内不会被再次调用。
  • 而析构函数则在对象的生命周期结束时被调用,确保所有资源得到释放,避免内存泄漏等问题。

 

结尾

构造函数是C++面向对象编程中的核心概念之一,它决定了对象如何被初始化和配置。了解构造函数的工作原理、类型、初始化列表以及其与析构函数的关系,对于编写高效、安全、易于维护的C++代码至关重要。通过合理利用构造函数,我们可以更加灵活地控制对象的创建和初始化过程,为程序的稳定性和性能打下坚实的基础

 

 

 

 

上一篇:linux桌面软件(wps)内嵌到主窗口后的关闭问题


下一篇:Android input系统原理二