【C++篇】C++类与对象深度解析(二)
前言
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!
在上篇文章《C++类与对象深度解析(一):从抽象到实践的全面入门指南》,我们初步探讨了C++类的基本概念和核心特性。在这篇文章中,我们将深入理解C++中的默认成员函数,这些函数是类的基石,理解它们对于掌握C++面向对象编程至关重要。本篇将侧重于解析构造函数、析构函数及拷贝构造函数,这些都是C++自动为类生成的成员函数,它们在类对象的生命周期管理中扮演着关键角色。
1. 类的默认成员函数
在C++中,默认成员函数是指用户没有显式实现,而由编译器自动生成的成员函数。一个类在没有显式定义特定成员函数的情况下,编译器会自动生成以下6个默认成员函数。理解这些默认成员函数的行为和作用是掌握C++类机制的基础。
补充:
移动构造函数和移动赋值运算符是在C++11引入的,用于优化资源的移动操作,减少不必要的拷贝。如果用户没有显式定义,编译器会自动生成这两个函数。
- 行为:默认的移动构造函数和移动赋值运算符会将资源从一个对象“移动”到另一个对象,源对象的资源会被“剥离”。
- 需求:对于那些管理动态资源的类,显式定义这些函数可以显著提高程序的效率,避免冗余的资源分配和释放操作。
本篇详细介绍前三个成员函数
2. 构造函数
构造函数是用于初始化对象的特殊成员函数。虽然名称为“构造”,但它的主要任务是初始化对象的成员变量,而不是为对象分配内存。构造函数的使用对于确保对象在创建时处于有效状态至关重要。
2.1 函数名与类名相同
构造函数的名字必须与类名相同。这是C++的语法要求
- 解释:构造函数的名字与类名相同,使得编译器能够识别它是用于初始化对象的函数,而不是普通的成员函数。
- 示例:
class MyClass { public: MyClass() { /* 构造函数体 */ } };
2.2 无返回值
构造函数没有返回值,甚至不能声明
void
。这是C++语言规定的,构造函数的唯一目的是初始化对象,因此不需要返回任何值。
- 解释:构造函数的任务是初始化对象,而不是返回数据。返回值的存在会违背构造函数的设计目的。
- 示例:
class MyClass { public: MyClass() { /* 构造函数体,无返回值 */ } };
2.3 对象实例化时系统会自动调用
构造函数在对象实例化时自动调用。开发者不需要显式调用构造函数,编译器会在对象创建时自动执行它。
- 解释:构造函数的自动调用确保了对象在创建时立即处于有效状态。无论对象是作为局部变量、全局变量还是动态分配的变量,构造函数都会在创建时运行。
- 示例:
MyClass obj; // 调用构造函数
2.4 构造函数可以重载
构造函数可以重载,即同一个类中可以有多个构造函数,它们的参数列表必须不同。这允许对象在创建时根据不同的需求进行不同的初始化。
- 解释:通过构造函数的重载,可以灵活地初始化对象。例如,一个类可以有无参构造函数和带参构造函数,以满足不同的初始化需求。
- 示例:
class MyClass { public: MyClass() { /* 无参构造函数 */ } MyClass(int x) { /* 带参构造函数 */ } };
2.5 默认构造函数的生成规则
如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数。但一旦用户定义了任何构造函数,编译器就不再生成默认构造函数。
- 解释:默认构造函数提供了一个基本的初始化方式。如果用户定义了其他形式的构造函数(如带参数的),编译器认为用户不再需要默认构造函数,因此不会自动生成。
- 示例:
class MyClass { // 没有显式定义构造函数,编译器会生成默认的无参构造函数 }; MyClass obj; // 调用默认构造函数
2.6 无参构造函数与全缺省构造函数的关系
无参构造函数、全缺省构造函数、默认生成的构造函数不能同时存在。如果定义了无参构造函数或全缺省构造函数,编译器将不会再生成默认构造函数。
- 解释:这三种构造函数提供了相似的功能,即初始化对象而不需要显式提供参数。为了避免冲突,它们只能存在其中之一。
- 示例:
class MyClass { public: MyClass() { /* 无参构造函数 */ } // MyClass(int x = 0) { /* 全缺省构造函数,不能与无参构造函数同时存在 */ } };
注意:无参构造函数、全缺省构造函数、编译器自动默认生成的构造函数全都叫默认构造函数!!!总结来说,可以不传参的就是默认构造函数,这三个不能同时存在,但是默认构造函数可以和带参的构造函数同时存在(即上文所说的函数重载),例如半缺省、没有缺省值等的普通构造函数。
有点绕,多品读一下😊
示例如下:
#include <iostream> using namespace std; class MyClass { public: // 全缺省构造函数,即默认构造函数 MyClass(int x = 10, int y = 20) { cout << "Called MyClass(int x = 10, int y = 20)" << endl; cout << "x = " << x << ", y = " << y << endl; } // 普通带参构造函数(没有缺省值) MyClass(double z) { cout << "Called MyClass(double z)" << endl; cout << "z = " << z << endl; } }; int main() { MyClass obj1; // 调用全缺省构造函数 MyClass obj2(100); // 调用全缺省构造函数,因为100是int类型 MyClass obj3(3.14); // 调用普通的带参构造函数 return 0; }
2.7 内置类型与自定义类型成员变量的初始化
如果是编译器自动生成的默认构造函数对内置类型成员变量的初始化没有要求,其值不确定。对于自定义类型的成员变量,编译器会调用它们的默认构造函数进行初始化。
- 解释:内置类型(如
int
、char
)的成员变量如果没有显式初始化,其值可能是未定义的。自定义类型的成员变量则必须通过其默认构造函数初始化。 - 示例:(这里是初始化列表,在这个系列的之后博客会讲到)
class MyClass { public: MyClass() : _value(0) { /* 初始化内置类型成员变量 */ } private: int _value; std::string _name;// 自动调用std::string的默认构造函数 }:
示例代码梳理
以下是展示上述特点的详细代码示例:
这里只是为了方便写的示例哈,如前文所述,无参和全缺省的默认构造函数只能存在一种
#include<iostream> using namespace std; class Date { public: // 1. 无参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 2. 带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } // 3. 全缺省构造函数 Date(int year = 1, int month = 1, int day = 1) { _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(2025, 1, 1); // 调用全缺省构造函数 Date d3; d1.Print(); d2.Print(); return 0; }
通过这个详细的解析和示例代码,我们可以清晰地理解C++类的默认成员函数和构造函数的特点及其作用。这样,开发者可以根据具体需求灵活地使用和自定义这些函数,以便更好地控制对象的生命周期和资源管理。
3. 析构函数
析构函数是与构造函数功能相反的一个函数,它用于在对象生命周期结束时释放资源。C++中规定,析构函数会在对象销毁时自动调用,以完成对象中资源的清理工作。这一特性使得C++能够有效地管理内存和其他资源,防止资源泄漏。
1. 析构函数名
析构函数的名称是类名的前面加上一个“~”符号,这是C++语法中的规定。它用于明确表示该函数是析构函数。
- 解释:析构函数的名字与类名相同,但在前面加上“
~
”符号,这使得编译器能够识别这是一个析构函数。 - 示例:
class MyClass { public: ~MyClass() { // 析构函数体 } };
2. 无参数无返回值
析构函数不接受任何参数,也没有返回值。它的唯一任务是清理对象的资源。
- 解释:由于析构函数是系统自动调用的,因此它不能有参数,也不需要返回任何值。
- 示例:
class MyClass { public: ~MyClass() { // 无参数,无返回值 } };
3. 一个类只能有一个析构函数
每个类只能定义一个析构函数。如果类中没有显式定义析构函数,系统会自动生成一个默认的析构函数。
- 解释:C++规定,一个类只能有一个析构函数,因为一个对象只能在生命周期结束时被销毁一次。
- 示例:
class MyClass { public: ~MyClass() { // 只能有一个析构函数 } };
4. 对象生命周期结束时,系统会自动调用析构函数
当一个对象的生命周期结束(如对象超出作用域或显式删除对象)时,系统会自动调用析构函数来清理资源。
- 解释:析构函数的自动调用确保了对象在被销毁时可以正确地释放资源,防止资源泄漏。
- 示例:
class MyClass { public: ~MyClass() { cout << "Object is being destroyed" << endl; } }; int main() { MyClass obj; // 当obj超出作用域时,系统会自动调用析构函数 return 0; }
5. 跟构造函数类似,编译器自动生成的析构函数对内置类型成员不做处理
如果类中没有显式定义析构函数,编译器会自动生成一个默认析构函数。这个默认析构函数对内置类型的成员变量不做任何处理。
- 解释:对于内置类型(如
int
、char
等),默认析构函数不需要释放资源。但对于自定义类型的成员,编译器生成的析构函数会调用这些成员的析构函数。 - 示例:
class MyClass { private: int _value; public: // 编译器自动生成的析构函数对内置类型不做处理 };
6. 显式写析构函数情况
如果显式定义了析构函数,对于自定义类型的成员变量,它们的析构函数也会被自动调用
- 解释:当显式定义析构函数时,C++确保所有自定义类型的成员都会在对象销毁时调用其析构函数,正确地释放资源。
- 示例:
class MyClass { private: std::string _name; // 自定义类型成员 public: ~MyClass() { // 自定义类型的成员变量会自动调用其析构函数 } };
7. 析构函数可以不写的情况
如果类中没有动态分配的资源或其他需要手动释放的资源,可以不显式定义析构函数,使用编译器生成的默认析构函数。
- 解释:对于没有动态资源的类,编译器生成的析构函数已经足够使用,不需要额外的析构逻辑。
- 示例:
class MyClass { private: int _value; // 没有动态资源,编译器生成的析构函数已足够 };
8. 一个局部域的多个对象,C++规定后定义的先析构
在一个局部作用域内定义的多个对象,C++规定后定义的对象会先调用析构函数。
- 解释:这一规则确保了对象按照“后进先出”的顺序销毁,符合栈的逻辑。
- 示例:
class MyClass { public: ~MyClass() { cout << "Destructor called" << endl; } }; int main() { MyClass obj1; MyClass obj2; // obj2会在obj1之前被销毁 return 0; }
示例代码梳理
以下是完整展示上述析构函数特点的详细代码示例:
#include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (_a == nullptr) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } ~Stack() { // 自定义析构函数,释放动态分配的内存 cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: MyQueue() : pushst(), popst() {} // 默认析构函数自动调用两个Stack成员的析构函数 // 显式定义的析构函数,也会自动调用Stack成员的析构函数 /*~MyQueue() {}*/ private: Stack pushst; Stack popst; }; int main() { Stack st; MyQueue mq; // MyQueue的析构函数会自动调用pushst和popst的析构函数 return 0; }
使用C++实现的Stack解决括号匹配问题
#include<iostream> using namespace std; bool isValid(const char* s) { Stack st; while (*s) { if (*s == '[' || *s == '(' || *s == '{') { st.Push(*s); } else { if (st.Empty()) { return false; } char top = st.Top(); st.Pop(); if ((*s == ']' && top != '[') || (*s == '}' && top != '{') || (*s == ')' && top != '(')) { return false; } } ++s; } return st.Empty(); // 确保所有括号都匹配 } int main() { cout << isValid("[()][]") << endl; // 输出1(true) cout << isValid("[(])[]") << endl; // 输出0(false) return 0; }
通过上面的代码,我们可以清楚地看到,C++的构造函数和析构函数帮助管理资源,避免了手动调用初始化和清理函数的麻烦。C++的这种自动化特性极大地简化了资源管理,尤其是在动态内存管理的场景下。
4. 拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它用于通过已有对象来创建一个新的对象。在C++中,如果构造函数的第一个参数是自身类类型的引用,并且任何额外的参数都有默认值,那么这个构造函数就是拷贝构造函数。
1. 拷贝构造函数是构造函数的一个重载
拷贝构造函数实际上是构造函数的一种重载形式,它与普通构造函数的区别在于其参数类型和目的。
- 解释:拷贝构造函数的定义方式与普通构造函数类似,但它的第一个参数必须是同类对象的引用,用于创建新对象时进行对象的复制。
- 示例:
class MyClass { public: MyClass(int value) { _value = value; } { } // 普通构造函数 MyClass(const MyClass& other) { // 拷贝构造函数 _value = other._value; } private: int _value; };
2. 拷贝构造函数的第一个参数必须是类类型对象的引用
拷贝构造函数的第一个参数必须是类类型的引用,不能是传值,因为传值会导致编译器不断调用拷贝构造函数,最终引发无限递归,导致编译错误。
- 解释:通过引用传递对象可以避免不必要的拷贝操作,并且能够直接访问原对象的内容。这种设计是为了防止调用拷贝构造函数时再次触发拷贝,从而引发无限递归。
- 示例:
class MyClass { public: MyClass(const MyClass& other) { // 正确:使用引用作为参数 _value = other._value; } // MyClass(MyClass other) { // 错误:传值会导致无限递归 // _value = other._value; // } private: int _value; };
3. C++规定自定义类型对象进行拷贝时必须调用拷贝构造函数
在C++中,当自定义类型对象需要被拷贝时(如传值传参或返回对象时),系统会自动调用拷贝构造函数。这是C++管理对象生命周期的一个基本机制。
- 解释:无论是通过值传递一个对象,还是从函数中返回一个对象,C++都会调用拷贝构造函数来创建新的对象副本。这确保了对象能够被正确地拷贝和初始化。
- 示例:
void Func(MyClass obj) { // 传值调用,自动调用拷贝构造函数 } MyClass ReturnObject() { MyClass temp(10); return temp; // 返回对象时,自动调用拷贝构造函数 } int main() { MyClass obj1(10); MyClass obj2 = obj1; // 调用拷贝构造函数 Func(obj1); // 调用拷贝构造函数 MyClass obj3 = ReturnObject(); // 调用拷贝构造函数 return 0; }
4. 若未显式定义拷贝构造函数,编译器会自动生成
如果类中没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会对内置类型成员变量进行浅拷贝,对自定义类型成员变量调用它们的拷贝构造函数。
- 解释:编译器生成的默认拷贝构造函数能够满足大部分情况下的需求,尤其是对于没有指针成员或动态资源的类。然而,对于涉及动态分配的资源,浅拷贝不合适,需要自定义拷贝构造函数来实现深拷贝。(如下会讲)
- 示例:
class SimpleClass { public: int _value; // 未显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数 }; int main() { SimpleClass obj1; obj1._value = 42; SimpleClass obj2 = obj1; // 自动生成的拷贝构造函数 return 0; }
5. 编译器自动生成的拷贝构造函数对内置类型和自定义类型的处理
如果类成员全部是内置类型(如
int
、char
),编译器自动生成的拷贝构造函数可以完成所需的拷贝,无需显式定义。然而,如果类成员包含指针或动态资源,编译器生成的浅拷贝可能不合适,需要自定义实现深拷贝。
- 解释:浅拷贝只会复制指针的地址,而不会复制指针所指向的数据。这在动态内存管理中可能导致多个对象共享同一块内存,从而引发资源释放时的冲突。因此,对于涉及动态内存的类,通常需要自定义深拷贝构造函数。
- 示例:
class Stack { public: Stack(int size) { _data = new int[size]; _size = size; } // 自定义拷贝构造函数,实现深拷贝 Stack(const Stack& other) { _data = new int[other._size];//之后在内存管理会讲到 _size = other._size; for (int i = 0; i < _size; ++i) { _data[i] = other._data[i]; } } ~Stack() { delete[] _data; // 析构函数释放资源 } private: int* _data; int _size; };
6. 拷贝构造函数在传值返回时的行为
当通过传值返回一个对象时,会产生一个临时对象,系统会调用拷贝构造函数来完成对象的复制。然而,传引用返回不会调用拷贝构造函数,而是返回对象的引用。
- 解释:在C++中,通过值返回对象时,编译器会调用拷贝构造函数来创建返回值的副本。如果通过引用返回对象,则没有拷贝发生。然而,引用返回需要确保返回的对象在函数结束后仍然存在,否则会导致悬空引用。
- 示例:
MyClass ReturnByValue() { MyClass temp(10); return temp; // 调用拷贝构造函数,返回对象副本 } MyClass& ReturnByReference() { static MyClass temp(10); // 使用static,确保返回的引用有效 return temp; // 返回引用,不调用拷贝构造函数 } int main() { MyClass obj1 = ReturnByValue(); // 调用拷贝构造函数 MyClass& obj2 = ReturnByReference(); // 不调用拷贝构造函数 return 0; }
示例代码梳理
以下是展示上述拷贝构造函数特点的详细代码示例:
#include<iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} // 自定义拷贝构造函数 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; void Func1(Date d) { d.Print(); } Date Func2() { Date tmp(2024, 7, 5); return tmp; // 返回值传递,调用拷贝构造函数 } int main() { Date d1(2024, 7, 5); Func1(d1); // 传值传参,调用拷贝构造函数 Date d2(d1); // 显式调用拷贝构造函数 d2.Print(); Date d3 = d1; // 另一种形式的拷贝构造 d3.Print(); Date ret = Func2(); // 调用拷贝构造函数 ret.Print(); return 0; }
通过这些代码示例和解释,我们可以深入理解C++中拷贝构造函数的特性及其应用场景。这些知识点对编写高效、安全的C++代码至关重要,特别是在处理自定义类型和动态资源时,掌握拷贝构造函数的用法可以有效防止潜在的错误和资源泄漏。
写在最后
在编写C++代码时,理解类与对象的这些核心概念不仅能够提升你的编程水平,还能帮助你避免资源泄漏、内存管理错误等潜在的编程陷阱。通过掌握这些知识,你将在编写面向对象的C++代码时游刃有余。希望这篇博客能够成为你深入理解C++类与对象的重要参考,让你的C++编程之旅更加顺畅、高效。如果你对这篇博客内容有任何疑问或希望进一步探讨的地方,欢迎留言讨论,我们一起学习进步!
以上就是关于【C++篇】C++类与对象深度解析(二):类的默认成员函数详解的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️