类的Big-Three:构造函数、析构函数与赋值函数
构造函数、析构函数和赋值函数是类的“Big-Three”。
每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将(仅在需要的时候)自动为A产生四个缺省的函数:
1 2 3 4 | A( void ); //缺省的无参构造函数 A( const A &a); //缺省的拷贝构造函数 ~A( void ); //缺省的析构函数 A & operator=( const A &a); //缺省的赋值函数 |
手动编写构造函数、析构函数的优点:可以自主初始化、清除资源;缺省拷贝构造函数、赋值函数采用了“位拷贝”(浅拷贝)的方式来实现,对指针类变量无法正确拷贝。
1、构造函数的初始化表
定义:初始化表在函数参数表之后,在函数体{}之前,即初始化表的初始化工作在函数体内的任何代码被执行之前。
限制:如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数。
特例:类的const和引用数据成员只能在初始化列表中被初始化,因为它不能在函数体内用赋值的方式来初始化。
类的数据成员可以采用初始化表或函数体内赋值两种方式:非内部数据类型的成员应当采用第一种方式,以获取更高的效率(仅调用拷贝构造函数,否则将调用无参构造函数加赋值函数);对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有差别,但后者逻辑更加清晰。
2、构造和析构顺序
构造从类层次的最根处开始,在每一层中,先调用基类的构造函数,然后调用成员对象的构造函数。析构则按照与构造相反的顺序执行,并且该顺序是唯一的。
成员对象的初始化顺序只由其在类中声明的顺序决定,以便析构函数能够以唯一的顺序析构。
构造和析构的执行顺序特点决定了不能在构造函数或析构函数中调用虚函数,因为基类构造或析构执行时派生类没有定义或已经被销毁。
3、赋值函数中指针类成员浅拷贝的缺点(a = b)
(1)b中原有指针的内存没有被释放,造成内存泄漏;
(2)b和a的指针指向同一块内存,任意一方的改变影响另一方;
(3)在对象被析构时,指针成员将被释放两次。
4、拷贝构造函数和赋值函数的不同
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
1 2 3 4 | String a( "a" ); //调用普通构造函数 String b( "b" ); //调用普通构造函数 String c = a; //调用拷贝构造函数,应写为:String c(a); c = b; //调用赋值函数 |
5、在派生类中实现类的基本函数
基类的构造函数、析构函数、赋值函数都不能被派生类继承。
(1)派生类的构造函数应该在其初始化列表中调用基类的构造函数;
(2)需要调用基类的析构函数时(在基类需要释放资源),基类与派生类的析构函数应该为虚函数。
(3)在编写派生类的赋值函数时,要对基类的数据成员重新赋值(Base::operator=(other);)。
6、偷懒办法处理拷贝构造函数和赋值函数
如果不想编写拷贝构造函数和复制函数,又不允许别人使用编译器生成的缺省函数,只需将其声明为私有函数,不用编写实现代码。
将一个函数声明为私有,但不实现的原因:成员函数和友元函数可以调用私有成员,但是调用没有实现的函数会出现链接错误,因而阻止了被调用。
此外,为类添加一个仅声明拷贝构造函数、赋值函数的基类,可以在编译期就能确定这些函数不能被调用。
1 2 3 4 5 6 | class A { private : A( const A &a); //私有的拷贝构造函数 A & operator=( const A& a); //私有的赋值函数 }; |
附录:String类Big-Three实现
1、String类Big-Three定义
1 2 3 4 5 6 7 8 9 10 | class String { public : String( const char *str = NULL); //普通构造函数 String( const String &other); //拷贝构造函数 ~String( void ); //析构函数 String & operator=( const String &other); //赋值函数 private : char *m_data; //用于保存字符串 }; |
2、String类String类Big-Three实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | //String的普通构造函数 String::String( const char *str) { if (str == NULL) { m_data = new char [1]; m_data[0] = '\0' ; } else { const int length = strlen (str); //有效字符串长度,不包含结束符‘\0’ m_data = new char [length + 1]; strcpy (m_data, str); //strcpy连结束符‘\0’一起复制 } } //String的拷贝构造函数 String::String( const String &other) { //允许操作other的私有成员 const int length = strlen (other.m_data); m_data = new char [length + 1]; strcpy (m_data, other.m_data); } //String的析构函数 String::~String() { //由于m_data是内部数据类型,也可以写成delete m_data; delete [] m_data; } //String的赋值函数 String & String::operator=( const String &other) { //(1) 检查自赋值 if ( this == &other) return * this ; //(2) 释放原有的内存资源 delete [] m_data; //(3) 分配新的内存资源,并赋值 const int length = strlen (other.m_data); m_data = new char [length + 1]; memcpy (m_data, other.m_data, length + 1); //(4) 返回本对象的引用(实现链式赋值,也避免other生命周期不明确) return * this ; } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器