【一、构造函数与析构函数】深度解析C++类的构造函数与析构函数调用机制

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【一、构造函数与析构函数】深度解析C++类的构造函数与析构函数调用机制

一、构造函数与析构函数

1.构造函数定义

构造函数是一种特殊的成员函数,它不需要用户手动调用(在某些情况下需要手动调用),而是在创建对象的的时候自动调用,构造函数的作用是初始化对象中的数据成员。

2.构造函数特点

①构造函数的名称必须与类名相同;

②构造函数没有返回值类型;

③构造函数可以重载(无参构造函数、有参构造函数、拷贝构造函数);

3.析构函数定义

析构函数是清理对象资源的一类特殊成员函数,析构函数的名称是类名前加~号,它在对象释放前自动调用,无参数(因为没有参数,所以无法重载—函数重载的判断依据是参数类型、参数顺序、参数个数)、无返回值类型且析构函数种禁止使用return语句。

二、构造函数分类与调用机制

1.无参构造函数

如果类中没有定义任何构造函数,编译器会提供一个默认无参构造函数(和默认拷贝构造函数);若类中定义了任意一个构造函数,编译器不会提供默认无参构造函数。下面结合代码详细分析无参构造函数的调用机制,代码如下:

#include <iostream>
using namespace std;
class MyClassA
{
public:
    MyClassA()
    {
        my_a = 1;
        my_b = 2;
        cout << "无参构造函数调用" << endl;
    }
    ~MyClassA()
    {
        cout << "析构函数调用" << endl;
    }
public:
    void PrintData()
    {
        cout << "my_a = " << my_a << " my_b = " << my_b << endl;
    }
private:
    int my_a;
    int my_b;
};
void ClassTest1()
{
    MyClassA A1; //创建对象的时候调用构造函数,默认无参构造函数
    A1.PrintData();
} //在对象的生命周期结束的时候调用析构函数
void ClassTest2()
{
  MyClassA A1, A2; //先调用A1构造函数,后调用A2构造函数//先定义先构造
} //先析构A2,后析构A1//后定义先析构
int main()
{
    ClassTest1(); //加断点单步调试来观察构造函数与析构函数的调用机制
    ClassTest2();
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

2.有参构造函数

有参构造函数有三种调用场景,现结合代码分析,代码如下:

#include <iostream>
using namespace std;
class MyClassA
{
public:
    MyClassA(int a)
    {
        my_a = a;
        my_b = a;
        cout << "一个参数的有参构造函数调用" << endl;
    }
    MyClassA(int a, int b)
    {
        my_a = a;
        my_b = b;
        cout << "两个参数的有参构造函数调用" << endl;
    }
    ~MyClassA()
    {
        my_a = 0;
        my_b = 0;
        cout << "析构函数调用" << endl;
    }
public:
    void PrintData()
    {
        cout << "my_a = " << my_a << " my_b = " << my_b << endl;
    }
private:
    int my_a;
    int my_b;
};
void ClassTest1()
{
    //MyClassA A1; //因为已经自己定义了有参构造函数,不再提供默认无参构造函数
             //错误 C2512 “MyClassA” : 没有合适的默认构造函数可用
    //A1.PrintData();
}
void ClassTest2()
{
    //有参构造函数的第一种调用场景
  MyClassA A1(1, 2);
  A1.PrintData();
    //有参构造函数的第二种调用场景//这里调用的是一个参数的构造函数!!!
                 //(3, 4)逗号表达式,值为4
    MyClassA A2 = (3, 4);
    A2.PrintData();
  MyClassA A22 = 4; //与A2的定义等价
  A22.PrintData();
    //有参构造函数的第三种调用场景
    MyClassA A3 = MyClassA(5, 6);
    A3.PrintData();
}
int main()
{
    //ClassTest1();
    ClassTest2(); //加断点单步调试来观察构造函数与析构函数的调用机制
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

3.拷贝构造函数

如果类中没有定义任何构造函数,编译器会提供一个默认拷贝构造函数;如果类中没有定义拷贝构造函数,编译器会提供一个默认拷贝构造函数,并执行浅拷贝操作。

(1)拷贝构造函数的三种调用场景

#include <iostream>
using namespace std;
class MyClassA
{
public:
    MyClassA()
    {
        my_a = 0;
        my_b = 0;
        cout << "无参构造函数" << endl;
    }
    MyClassA(int a, int b)
    {
        my_a = a;
        my_b = b;
        cout << "有参构造函数调用" << endl;
    }
    MyClassA(const MyClassA& A)
    {
        my_a = A.my_a;
        my_b = A.my_b;
        cout << "拷贝构造函数调用" << endl;
    }
    ~MyClassA()
    {
        my_a = 0;
        my_b = 0;
        cout << "析构函数调用" << endl;
    }
public:
    void PrintData()
    {
        cout << "my_a = " << my_a << " my_b = " << my_b << endl;
    }
private:
    int my_a;
    int my_b;
};
//第一种调用场景:用一个对象初始化另一个对象
void ClassTest1()
{
    MyClassA A1(1, 2); //调用有参构造函数
    MyClassA A2(A1); //括号法初始化调用拷贝构造函数
    A2.PrintData();
    MyClassA A3 = A1; //等号法初始化调用拷贝构造函数
    A3.PrintData();
    MyClassA A4; //调用了无参构造函数
    A4 = A1; //这里是等号赋值!!!和等号法初始化是两个概念
             //不调用构造函数,而是执行赋值操作,把A1的数据赋值给A4(默认=浅拷贝,将在后面介绍)
    A4.PrintData();
}
void ParaFuncTest1(MyClassA A)
{
    A.PrintData();
}
void ParaFuncTest2(MyClassA& A)
{
  A.PrintData();
}
void ParaFuncTest3(MyClassA* A)
{
  A->PrintData();
}
//第二种调用场景:类定义对象做函数参数
void ClassTest2()
{
    MyClassA A1(1, 2);
    ParaFuncTest1(A1); //实参A1初始化形参A对象元素的时候,会调用拷贝构造函数
    ParaFuncTest2(A1); //不会调用拷贝构造函数,因为引用是变量别名,引用传递并没有出现新对象,
               //只是给现有对象起个别名进行传递
    ParaFuncTest3(&A1); //不会调用拷贝构造函数,因为传递的是对象A1的地址,并没有新的对象元素出现
}
MyClassA RetuFuncTest1()
{
    MyClassA A(1, 2);
    return A; //创建一个匿名对象,并把匿名对象返回出去,此时调用拷贝构造函数
} //而A的生命周期到此结束,被析构
//MyClassA& RetuFuncTest2()
//MyClassA* RetuFuncTest3()
//{
//  MyClassA A(1, 2);
//  return &A; //A是局部变量,不能返回它的地址
//}
//第三种调用场景:函数返回类型为类定义的元素
void ClassTest3()
{
    RetuFuncTest1(); //如果不用变量来接这个函数,那么会在 RetuFuncTest1() 函数的 return A;
                     //语句处调用拷贝构造函数,并立即执行析构函数
                     //这是因为,函数返回一个对象元素,而局部变量A的生命周期只在函数体内,
                     //不能返回出来,所以会在return时创建一个匿名对象,主调函数种若没有
                     //对象元素来接,那么会立即调用析构函数把匿名对象析构
    //RetuFuncTest2();
    //RetuFuncTest3();
    MyClassA A1 = RetuFuncTest1(); //这里不会再次调用拷贝构造函数,
                     //因为编译器会把函数RetuFuncTest1()
                                   //返回出来的匿名对象直接转化为A1,因此匿名对象不会被析构,
                                   //在RetuFuncTest1()函数结束时只调用一次析构函数来析构局部变量A
                                   //匿名对象已经分配好了资源,并直接转化为A1,
                     //所以A1初始化不需要再次调用拷贝构造函数
    A1.PrintData();
    MyClassA A2; //调用无参构造函数
    A2 = RetuFuncTest1(); //这是等号赋值操作!!!此时匿名对象也不会立即析构,而是在执行完这句话,
                          //对A2赋值完之后,执行析构函数,析构匿名对象
                //(区别于匿名对象初始化A1,匿名对象转为A1,不会析构)
    A2.PrintData();
} //生命周期结束,析构所有局部变量 A1(匿名对象) A2
int main()
{
    ClassTest1(); //加断点单步调试来观察构造函数与析构函数的调用机制
    ClassTest2();
    ClassTest3();
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

(2)拷贝构造函数中的深拷贝与浅拷贝

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class MyClassB
{
public:
    MyClassB(const char* p)
  {
    my_len = strlen(p);
    my_str = (char*)malloc(my_len + 1);
    strcpy(my_str, p);
    cout << "无参构造函数" << endl;
  }
  ~MyClassB()
  {
    if (my_str != NULL)
    {
      free(my_str);
      my_str = NULL; //避免野指针
    }
    my_len = 0;
    cout << "析构函数调用" << endl;
  }
public:
  void PrintData()
  {
    cout << "my_len = " << my_len << endl;
    cout << "my_str = " << my_str << endl;
  }
private:
  int     my_len;
  char*   my_str;
};
void ClassTestB()
{
  MyClassB B1("hello C++ word!");
  B1.PrintData();
  MyClassB B2 = B1; //调用默认拷贝构造函数
  B2.PrintData();
} //程序会在此挂掉,原因分析 --- 默认拷贝构造函数的浅拷贝问题
  //类B中没有定义拷贝构造函数,会使用编译器提供的默认拷贝构造函数
  //默认拷贝构造函数只是简单的数值赋值,当类中含有指针时
  //只是单纯的把B1.my_str指针的值赋给了B2.my_str,而没有给B2.my_str分配内存,
  //这时两个指针指向同一个内存块,当函数执行完,在调用B2析构函数时,把B2.my_str
  //所指向的空间析构掉了,B1.my_str也指向这块内存,而内存已经被释放了,
  //这时,B1.my_str变成了野指针,当调用B1的析构函数析构B1.my_str指向的内存时,程序挂掉
class MyClassA
{
public:
    MyClassA()
    {
        ; //MyClassA A3; 需要无参构造函数
    }
    MyClassA(const char* p)
    {
        my_len = strlen(p);
        my_str = (char*)malloc(my_len + 1);
        strcpy(my_str, p);
        cout << "无参构造函数" << endl;
    }
    //自定义拷贝构造函数,实现深拷贝
    MyClassA(const MyClassA& A)
    {
        my_len = A.my_len;
        my_str = (char*)malloc(my_len + 1);
        strcpy(my_str, A.my_str);
    }
    ~MyClassA()
    {
        if (my_str != NULL)
        {
            free(my_str);
            my_str = NULL; //避免野指针
        }
        my_len = 0;
        cout << "析构函数调用" << endl;
    }
public:
    void PrintData()
    {
        cout << "my_len = " << my_len << endl;
        cout << "my_str = " << my_str << endl;
    }
private:
    int     my_len;
    char*   my_str;
};
void ClassTestA()
{
    MyClassA A1("hello C++ word!");
    A1.PrintData();
    MyClassA A2 = A1;
    A2.PrintData();
   // MyClassA A3; 
   // A3 = A1; //默认等号也是浅拷贝
   // A3.PrintData();
}
int main()
{
    //ClassTestB();
    ClassTestA(); //加断点单步调试来观察构造函数与析构函数的调用机制
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

4.总结

只有当类中没有定义任何构造函数时,才会生成默认无参构造函数,但凡类中有定义的任何一个构造函数,都不会生成默认无参构造函数。

只要类中没有定义拷贝构造函数,就会默认生成一个拷贝构造函数,执行浅拷贝,只要定义了一个拷贝构造函数,就不会在生成默认拷贝构造函数。

三、构造函数的初始化列表

应优先使用初始化列表来对数据成员进行初始化,因为就算不显示使用初始化列表,程序也会在执行构造函数的函数体之前将所有数据成员通过默认方式初始化,使用初始化列表可以避免二次赋值。

#include <iostream>
using namespace std;
class MyClassA
{
public:
  MyClassA(int a) //前提是类A中必须含有相应的有参构造函数
  {
    this->a = a;
    cout << "构造函数调用" << endl;
  }
  ~MyClassA()
  {
    cout << "析构函数调用" << endl;
  }
private:
  int a;
};
//必须使用构造函数参数初始化列表的两种情况
class MyClassB
{
public:
  MyClassB(int b, int m, int n, int k) : a1(m), a2(n), c(k)
  {
    this->b = b;
    cout << "构造函数调用" << endl;
  }
  ~MyClassB()
  {
    cout << "析构函数调用" << endl;
  }
private:
  int b;
    //1.一个类的成员中含有另一个类对象,需使用初始化列表来给成员类初始化
  MyClassA a1, a2;
    //2.const类型的变量必须使用初始化参数列表来初始化
  const int c;
};
void ClassTest()
{
  MyClassB B1(1, 2, 3, 4);
}
class MyClassA2
{
public:
  MyClassA2()
  {
    cout << "构造函数调用" << endl;
  }
  ~MyClassA2()
  {
    cout << "析构函数调用" << endl;
  }
private:
  int a;
};
class MyClassA3
{
public:
  MyClassA3(int a1, int a2)
  {
        this->a1 = a1;
        this->a2 = a2;
    cout << "构造函数调用" << endl;
  }
  ~MyClassA3()
  {
    cout << "析构函数调用" << endl;
  }
private:
  int a1;
    int a2;
};
class MyClassB2
{
public:
    //错误(活动)  E0289 没有与参数列表匹配的构造函数
    //因为MyClassA2 MyClassA3中均没有一个参数的构造函数,所以报错,没有匹配的构造函数
  MyClassB2(int b, int m, int n) : a2(m), a3(n) 
  {
        this->b = b;
    cout << "构造函数调用" << endl;
  }
    //正确做法,根据MyClassA2 MyClassA3定义的构造函数来设置MyClassB2的初始化列表
    /*
    MyClassB2(int b, int m, int n) : a2(), a3(m, n)
  {
    this->b = b;
    cout << "构造函数调用" << endl;
  }
    */
  ~MyClassB2()
  {
    cout << "析构函数调用" << endl;
  }
private:
  int b;
    MyClassA2 a2;
    MyClassA3 a3;
};
void ClassTest2()
{
    MyClassB2(1, 2, 3);
}
int main()
{
  ClassTest(); //加断点单步调试来观察构造函数与析构函数的调用机制
    ClassTest2();
  std::cout << "Hello World!\n";
  system("pause");
  return 0;
}

总结

学习C++构造函数与析构函数的调用时机、调用顺序、参数匹配,对象生命周期等,最好的方法就是在代码中进行调试实验,通过断点单步调试,一步步观察程序的执行。

系列文章

二、new/delete详解

相关文章
|
8天前
|
存储 缓存 算法
分布式锁服务深度解析:以Apache Flink的Checkpointing机制为例
【10月更文挑战第7天】在分布式系统中,多个进程或节点可能需要同时访问和操作共享资源。为了确保数据的一致性和系统的稳定性,我们需要一种机制来协调这些进程或节点的访问,避免并发冲突和竞态条件。分布式锁服务正是为此而生的一种解决方案。它通过在网络环境中实现锁机制,确保同一时间只有一个进程或节点能够访问和操作共享资源。
25 3
|
19天前
|
安全 编译器 程序员
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
38 2
中断处理机制解析
【10月更文挑战第5天】中断处理需定义中断处理函数`irq_handler_t`,参数包括中断信号`irq`和通用指针`dev_id`。返回值`IRQ_NONE`表示非本设备中断,`IRQ_HANDLED`表示已处理,`IRQ_WAKE_THREAD`表示需唤醒等待进程。处理程序常分上下半部,关键部分在中断处理函数中完成,延迟部分通过工作队列处理。注册中断处理函数需调用`request_irq`,参数包括中断信号、处理函数、标志位、设备名和通用指针。
|
19天前
|
安全 C语言 C++
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
31 4
|
17天前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
15 1
|
19天前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
41 2
|
19天前
|
存储 设计模式 编译器
【C++篇】C++类与对象深度解析(五):友元机制、内部类与匿名对象的高级应用
【C++篇】C++类与对象深度解析(五):友元机制、内部类与匿名对象的高级应用
19 2
|
4天前
|
JavaScript 前端开发 开发者
原型链深入解析:JavaScript中的核心机制
【10月更文挑战第13天】原型链深入解析:JavaScript中的核心机制
10 0
|
9天前
|
Java 开发者
Java中的异常处理机制深度解析
【10月更文挑战第8天】 在Java编程中,异常处理不仅是保证程序鲁棒性的关键手段,更是每个开发者必须精通的核心技能。本文将深入探讨Java异常处理的各个方面,包括异常的分类、捕获与处理、自定义异常以及最佳实践等,旨在帮助读者全面理解并有效应用这一机制,提升代码的可靠性和可维护性。
|
12天前
|
程序员 开发者 Python
深度解析Python中的元编程:从装饰器到自定义类创建工具
【10月更文挑战第5天】在现代软件开发中,元编程是一种高级技术,它允许程序员编写能够生成或修改其他程序的代码。这使得开发者可以更灵活地控制和扩展他们的应用逻辑。Python作为一种动态类型语言,提供了丰富的元编程特性,如装饰器、元类以及动态函数和类的创建等。本文将深入探讨这些特性,并通过具体的代码示例来展示如何有效地利用它们。
17 0

推荐镜像

更多