构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(局部对象在栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。 构造函数有如下特点: 1. 函数名与类型同名 2. 可以重载 3. 没有返回值(不用写void) 4. 如果用户没有显式写构造函数,编译器会生成一个默认的无参构造函数,一旦用户显式定义编译器将不再生成。
代码语言:javascript
AI代码解释
// 构造函数 // 1. 函数名和类名同名 2. 可以重载 3. 没有返回值 4. 用户不写编译器会默认生成无参的构造函数 class Date { public: // 无参构造 Date() { _year = 1; _month = 1; _day = 1; } // 带参数构造 Date(size_t year, size_t month, size_t day) { _year = year; _month = month; _day = day; } //// 全缺省构造 //Date(size_t year = 1, size_t month = 1, size_t day = 1) //{ // _year = year; // _month = month; // _day = day; //} // void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; int main() { // 调用带参数的构造 Date d1(2025,7,5); d1.Print(); //// 无参构造和全缺省构造会产生调用歧义 //Date d2; //d2.Print(); // 无参的不能这么写 会和函数声明搞混 eg: void func // 这是函数声明还是函数定义呢? /*Date d2(); d2.Print();*/ //// 如果注释掉无参的构造和全缺省构造,会报错 //// C2512 没有合适的默认构造函数可用 //Date d2; //d2.Print(); // 调用无参的构造函数 Date d3; d3.Print(); return 0; }默认构造函数分为三类:
- 全缺省构造函数
- 无参构造函数
- 编译器默认生成的构造函数总结一下:不传参的构造函数就是默认构造函数,这三个函数不能同时存在 而全缺省构造函数和无参构造函数虽然构成函数重载,但是调用时会产生调用歧义我们不显式写构造函数,编译器默认生成的构造函数会如何处理数据?
代码语言:javascript
AI代码解释
using namespace std; class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } private: size_t _hour; size_t _minute; size_t _second; }; class Date { public: // 不写构造函数 编译器会自动生成默认构造函数 // 对于内置类型 编译器是否处理没有明确要求 // 对于自定义类型 调用该类型的默认构造函数 void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; Time _t; }; int main() { Date d1; d1.Print(); return 0; }在这里插入图片描述
观察调试结果,我们可以得到如下结论: 对于编译器默认生成的构造函数,处理不同类型数据有不同行为:
- 对于内置类型,编译器没有特别要求,对于
VS环境,给出随机值 - 对于自定义类型,该类型会调用它默认的构造函数 如果把
Time类的无参构造函数注释掉,会有如下现象:
在这里插入图片描述
Time类调用它的默认构造函数,而Time类的默认构造函数是编译器生成的,又是处理内置类型,所以VS不做处理,给出随机值 针对这个问题C++11打了个补丁:内置类型成员变量在声明时给缺省值,用缺省值初始化
代码语言:javascript
AI代码解释
using namespace std; class Time { public: /*Time() { _hour = 1; _minute = 1; _second = 1; }*/ private: // C++11 在声明时给缺省值 size_t _hour = 1; size_t _minute = 1; size_t _second = 1; }; class Date { public: // 不写构造函数 编译器会自动生成默认构造函数 // 对于内置类型 编译器是否处理没有明确要求 // 对于自定义类型 调用该类型的默认构造函数 void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: // C++11 在声明时给缺省值 size_t _year = 1; size_t _month = 1; size_t _day = 1; Time _t; }; int main() { // 此时 Time类和Date类只有编译器默认生成的构造函数 Date d1; d1.Print(); return 0; }![[Pasted image 20250707095835.png]]
总结:什么时候要显式定义构造函数?
- 一般情况构造函数都要显式实现
- 只有成员全为自定义类型的类不用显式实现
3. 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的 析构函数有如下特点: 1. 函数名和类名相同,在函数名前加~2. 没有返回值 3. 不能重载,意味着一个类只有一个析构函数 4. 如果用户没有显式写,编译器会默认生成析构函数 5. 对象的生命周期结束,编译器自动调用析构函数
代码语言:javascript
AI代码解释
class Stack { public: Stack(size_t n = 4) { cout << "Stack(size_t n = 4) 析构" << endl; _arr = (int*)malloc(sizeof(int) * n); if (_arr == nullptr) { perror("malloc err!"); return; } _capacity = n; _top = 0; } ~Stack() { cout << "~Stack() 析构" << endl; assert(_arr); free(_arr); _arr = nullptr; _capacity = _top = 0; } private: int* _arr; int _capacity; int _top; }; int main() { Stack st1; return 0; }和构造函数一样,如果我们不显式实现析构函数,编译器生成的析构函数对于内置类型不做处理,对于定义类型会调用它的析构函数,值得一提的是,是我们显式写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
代码语言:javascript
AI代码解释
class tmp { public: ~tmp() { cout << "~tmp() 析构" << endl; } private: int _num; }; class Stack { public: Stack(size_t n = 4) { cout << "Stack(size_t n = 4) 构造" << endl; _arr = (int*)malloc(sizeof(int) * n); if (_arr == nullptr) { perror("malloc err!"); return; } _capacity = n; _top = 0; } /*~Stack() { cout << "~Stack() 析构" << endl; assert(_arr); free(_arr); _arr = nullptr; _capacity = _top = 0; }*/ private: int* _arr; int _capacity; int _top; tmp _t; }; int main() { Stack st1; return 0; }我们可以通过调试观察:
在这里插入图片描述
总结:什么时候需要显式实现析构函数?
- 有资源需要清理,就必须写析构函数,例如:
StackList… - 无资源要清理,可以不写
- 内置类型成员没有资源要清理,剩下全是自定义类型,可以不写 还有一个重要的点:一个局部域的多个对象,后定义的先析构
代码语言:javascript
AI代码解释
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( ) C c; int main() { A a; B b; static D d; return 0; }- 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意
static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象 - 全局对象先于局部对象进行构造
- 局部对象按照出现的顺序进行构造,无论是否为
static - 所以构造的顺序为
c a b d - 析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构
- 因此析构顺序为
B A D C
4. 拷贝构造函数
拷贝构造函数的第一个参数是自身类型的引用,且任何额外的参数都有缺省值,这样的函数叫做拷贝构造函数,用于同类对象的拷贝初始化,是构造函数的重载。 本文以最常规情况的拷贝构造函数展开,即有且仅有一个参数:类类型对象的引用拷贝构造函数有如下特点:
- 拷贝构造函数是构造函数的一个重载
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会报错(会引发无穷递归调用),拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值
代码语言:javascript
AI代码解释
// 拷贝构造函数 // 构造函数的重载,第一个参数必须是类类型对象的引用 // 用于同类对象的拷贝初始化 class Date { public: Date() { _year = 1; _month = 1; _day = 1; } Date(Date& d) { cout << "call Date(Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; int main() { Date d1; // 两种写法都可以 Date d2 = d1; // d是d1的别名,d3是this指针 Date d3(d1); d1.Print(); d2.Print(); d3.Print(); return 0; }再来看一段代码:
代码语言:javascript
AI代码解释
Date(Date& d) { cout << "call Date(Date& d)" << endl; // 如果不小心写反了会发生什么? d._year = _year; d._month = _month; d._day = _day; }其余部分不变
在这里插入图片描述
初始的d1也被修改成随机值了,我们进行拷贝构造,提供拷贝值的对象是不能被修改的,所以为了防止这样的情况发生,我们做如下处理:Date(const Date& d)保证d的只读性
代码语言:javascript
AI代码解释
// 拷贝构造函数 // 构造函数的重载,第一个参数必须是类类型对象的引用 // 用于同类对象的拷贝初始化 class Date { public: Date() { _year = 1; _month = 1; _day = 1; } Date(const Date& d) { cout << "call Date(Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: size_t _year; size_t _month; size_t _day; }; int main() { Date d1; // 两种写法都可以 Date d2 = d1; // d是d1的别名,d3是this指针 Date d3(d1); d1.Print(); d2.Print(); d3.Print(); return 0; }C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造