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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【一、构造函数与析构函数】深度解析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详解

相关文章
|
20天前
|
数据可视化 数据挖掘 BI
团队管理者必读:高效看板类协同软件的功能解析
在现代职场中,团队协作的效率直接影响项目成败。看板类协同软件通过可视化界面,帮助团队清晰规划任务、追踪进度,提高协作效率。本文介绍看板类软件的优势,并推荐五款优质工具:板栗看板、Trello、Monday.com、ClickUp 和 Asana,助力团队实现高效管理。
45 2
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
5天前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
1月前
|
PHP 开发者 UED
PHP中的异常处理机制解析####
本文深入探讨了PHP中的异常处理机制,通过实例解析try-catch语句的用法,并对比传统错误处理方式,揭示其在提升代码健壮性与可维护性方面的优势。文章还简要介绍了自定义异常类的创建及其应用场景,为开发者提供实用的技术参考。 ####
|
2月前
|
存储 缓存 监控
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
|
2月前
|
缓存 NoSQL Java
千万级电商线上无阻塞双buffer缓冲优化ID生成机制深度解析
【11月更文挑战第30天】在千万级电商系统中,ID生成机制是核心基础设施之一。一个高效、可靠的ID生成系统对于保障系统的稳定性和性能至关重要。本文将深入探讨一种在千万级电商线上广泛应用的ID生成机制——无阻塞双buffer缓冲优化方案。本文从概述、功能点、背景、业务点、底层原理等多个维度进行解析,并通过Java语言实现多个示例,指出各自实践的优缺点。希望给需要的同学提供一些参考。
51 7
|
1月前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
2月前
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
77 8
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
63 2
|
2月前
|
设计模式 安全 数据库连接
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
41 2

推荐镜像

更多