C++ 类和对象(2)
目录
1. 类的默认成员函数
1.1 定义
1.2 由来
2. 构造函数
2.1 定义
2.2 核心特性
2.3 默认构造函数(编译器自动生成的)
2.4 手动定义构造函数
1. 无参构造函数
2. 带参构造函数
2.5 补充
3. 析构函数
3.1 定义
3.2 语法规则
3.3 编译器生成的默认析构函数
3.4 手动定义析构函数
3.5 析构函数的“链式调用”
3.6 局部对象的析构顺序
3.7 补充
3.8 总结
4. 总结
1. 类的默认成员函数
1.1 定义
- 在 C++ 中,默认成员函数指的是:当类中没有显式定义某些特殊成员函数时,编译器会自动为该类生成的、用于支持对象基本操作的函数。这些函数是类的“默认配置”,无需开发者手动编写,就能让类的对象完成创建、销毁、复制、赋值等基础行为,是类与对象机制正常运行的基础。
- 默认成员函数共 6 个,分别是:构造函数、析构函数、拷贝构造函数、赋值运算符重载、普通取地址运算符重载、const 取地址运算符重载。
C++类的默认成员函数共有6个,各自的核心作用如下:
- 构造函数:对象创建时自动调用,初始化对象(如给成员变量赋值)。
- 析构函数:对象销毁时自动调用,清理资源(如释放动态内存)。
- 拷贝构造函数:用已存在的对象初始化新对象(如 Date d2(d1) )。
- 赋值运算符重载:两个已存在对象之间的赋值(如 d2 = d1 )。
- 取地址运算符重载:返回对象的地址(默认即可满足多数需求)。
- const 取地址运算符重载:返回 const 对象的地址(适配 const 场景)。
默认成员函数的核心作用是保障类对象能正常进行创建、销毁、复制、赋值等基础操作,让类在没有手动定义这些函数时,仍能满足最基本的使用需求,降低类的设计和使用门槛。
- 简单说,它们是类的“基础工具”,默认情况下由编译器自动生成,确保对象的生命周期管理和基本操作能正常运行;当类涉及资源(如动态内存)时,可能需要手动重写其中部分函数(如拷贝构造、析构)以避免问题。
1.2 由来
- 默认成员函数的由来,本质是C++为了让类能够像内置类型(如int、double)一样进行自然的操作(创建、销毁、复制等),同时兼顾类的封装特性而设计的机制。
- 在C语言中,结构体仅能存储数据,操作数据需要手动写函数,且无法自动处理初始化、清理等动作。而C++的类将数据和操作封装在一起,为了让类的对象在创建、销毁、赋值等场景下能“自动”完成必要的工作(比如初始化成员、释放资源),编译器就被设计为:当用户没有显式定义这些关键函数时,自动生成一套默认实现,确保类的基本功能可正常运行。
比如:
- 创建对象时需要初始化,于是有了默认构造函数;
- 销毁对象时需要清理资源,于是有了默认析构函数;
- 用一个对象复制出另一个新对象,于是有了默认拷贝构造函数;
- 两个对象之间赋值,于是有了默认赋值运算符重载。
这些默认函数的存在,让类在最基础的场景下无需手动编写代码就能工作,既保持了类的封装性,又降低了使用门槛,是C++类与对象机制得以顺畅运行的基础保障。
2. 构造函数
在 C++ 类的默认成员函数中,构造函数是对象创建时自动调用的特殊函数,其核心作用是初始化对象(包括给成员变量赋初值、分配资源等)。
2.1 定义
构造函数是与类同名的非静态成员函数,用于在对象创建时(即内存分配后)初始化对象。
2.2 核心特性
1. 自动调用:
- 对象创建时(如 Date d; 或 Date* d = new Date; ),编译器会自动调用构造函数,无需手动调用(也不能手动调用)。
2. 无返回值:
- 不同于普通函数,构造函数没有返回类型,甚至不能写 void 。
3. 可以重载:
- 一个类可以有多个构造函数,只要参数列表(参数类型、个数、顺序)不同,就构成重载。目的是支持对象在不同场景下的初始化(比如带参数初始化、无参数默认初始化)。
4. 与类名相同:
- 函数名必须和类名完全一致(包括大小写),例如 class Date 的构造函数只能叫 Date 。
5. 初始化对象:
- 核心作用是给对象的成员变量赋初值,或执行对象创建时的必要操作(如打开文件、分配动态内存等)。
2.3 默认构造函数(编译器自动生成的)
如果类中没有显式定义任何构造函数,编译器会自动生成一个默认构造函数(无参构造函数)。其默认实现规则如下:
- 对于内置类型成员(如 int , double , 指针等) : 默认不做初始化(即成员变量的值是随机的 , 比如 _year 可能是垃圾值)。
- 对于自定义类型成员(如类中包含另一个类的对象) : 会自动调用该自定义类型的默认构造函数(即无参构造函数)。
示例:
class Time
{
public:Time() { // Time的默认构造函数cout << "Time()" << endl;}
};class Date
{
private:int _year; // 内置类型Time _t; // 自定义类型
};int main()
{Date d; // 创建 Date对象,会调用编译器生成的Date默认构造函数// 结果:会打印"Time()"(因为_year不初始化,_t调用Time的默认构造)return 0;
}
2.4 手动定义构造函数
为什么需要手动定义构造函数?
默认构造函数的局限性很明显 : 对内置类型成员不初始化 , 可能导致对象状态不确定(比如指针未初始化就使用 , 会引发崩溃)。因此,实际开发中通常需要手动定义构造函数 , 主要场景包括:
- 需要给内置类型成员赋初始值(如 Date 类必须初始化年月日);
- 对象创建时需要分配资源(如类中包含 char* 指针 , 需要在构造函数中 new 内存);
- 需要支持多种初始化方式(如无参默认值 , 带参指定值)。
注意:一旦手动定义了任何构造函数 , 编译器就不再生成默认构造函数。如果需要无参构造, 必须手动定义。
在 C++ 中,当编译器自动生成的默认构造函数无法满足需求时,就需要显式定义默认构造函数。具体场景如下:
1. 无参构造函数
手动定义的无参构造函数,就是开发者显式写出的 , 不需要参数就能调用的构造函数 , 格式是 类名() 。它的作用是在创建对象时(不需要传参) , 对对象的成员进行初始化。
关键特点:
1. 格式固定:函数名与类名相同 , 无参数 , 无返回值(连 void 都不能写)。class Date { private:int year;int month;int day; public:// 手动定义的无参构造函数Date() {year = 2000; // 初始化成员month = 1;day = 1;} };
2. 调用方式:创建对象时不需要传参 , 直接写 类名 对象名; 。
示例:Date d; // 调用上面的无参构造函数,d的初始值是2000-1-1
3. 与编译器生成的默认构造的区别:
- 编译器自动生成的默认构造函数(当你没写任何构造时)对内置类型成员(如 int , double)不初始化(值是随机的) , 只初始化自定义类型成员。
- 手动定义的无参构造函数可以主动初始化所有成员(包括内置类型) , 更灵活。
4. 注意点:
- 一旦手动定义了任何构造函数(包括无参构造) , 编译器就不再生成默认构造函数。
- 一个类中只能有一个无参构造函数(否则会冲突 , 因为调用时无法区分)。
5. 什么时候用?
- 当你希望创建对象时(不需要传参) , 成员能被赋予特定的初始值(而不是随机值) , 就需要手动定义无参构造函数。
- 例如上面的 Date 类 , 用无参构造创建对象时 , 默认初始化为 2000-1-1 , 比编译器生成的“随机值”更合理。
对比一个错误案例:
如果只写了带参构造,没写无参构造,就不能无参创建对象:class Date { public:// 只写了带参构造Date(int year, int month, int day) {_year = year;_month = month;_day = day;} };Date d; // 报错!没有无参构造,编译器也不再生成默认构造
此时如果想无参创建对象,就必须手动加一个无参构造函数。
2. 带参构造函数
手动定义的带参构造函数 , 是开发者显式写出的 , 需要传入参数才能调用的构造函数。它的核心作用是在创建对象时 , 通过外部传入的参数来初始化对象的成员 , 让对象的初始状态更灵活可控。
关键特点:
1. 格式 : 函数名与类名相同 , 有参数列表(至少1个参数) , 无返回值(不能写 void )。
示例:class Date { private:int year;int month;int day; public:// 手动定义的带参构造函数(3个参数)Date(int y, int m, int d) {year = y; // 用参数初始化成员month = m;day = d;} };
2. 调用方式:创建对象时必须传入对应参数 , 格式为 类名 对象名(参数1, 参数2, ...); 。
示例:Date d(2023, 10, 1); // 调用带参构造,d被初始化为2023-10-1
3. 与无参构造的区别:
- 无参构造:创建对象时不需要传参 , 成员初始化是固定的(比如默认值)。
- 带参构造:创建对象时必须传参 , 成员初始化由外部参数决定 , 更灵活(同一类可以创建出不同初始状态的对象)。
4. 特殊变种:全缺省构造函数
- 带参构造函数可以给参数设置默认值,当所有参数都有默认值时,就叫“全缺省构造函数”。
示例:
class Date { public:// 全缺省构造(带参,但可无参调用)Date(int y=2000, int m=1, int d=1) {year = y;month = m;day = d;} };
调用方式灵活:
Date d1; // 无参调用,用默认值2000-1-1 Date d2(2023); // 传1个参数,月和日用默认值2023-1-1 Date d3(2023, 10); // 传2个参数,日用默认值2023-10-1 Date d4(2023, 10, 1); // 传3个参数,2023-10-1
5. 注意点:
- 一旦手动定义了带参构造函数(无论是否全缺省) , 编译器不再自动生成默认构造函数。
- 在 C++ 中 , 如果一个类显式定义了带参构造函数(无论参数数量和类型如何) , 编译器不会再自动生成默认无参构造函数。此时,如果你创建对象时不传递任何参数(试图调用无参构造),编译器会因为找不到匹配的无参构造函数而报错。例如:如果只写了上面的 Date(int y, int m, int d) , 就不能用 Date d; 创建对象(会报错 , 因为没有无参构造)。
- 带参构造可以重载(一个类中可以有多个带参构造 , 只要参数个数或类型不同)。
示例:
class Date { public:Date(int y, int m, int d) { ... } // 3参数Date(int y, int m) { ... } // 2参数(重载)Date(int y) { ... } // 1参数(重载) };
调用时根据参数匹配对应的构造函数:
Date d1(2023, 10, 1); // 调用3参数 Date d2(2023, 10); // 调用2参数
6. 什么时候用?
- 当你需要创建不同初始状态的对象时(比如不同日期、不同年龄的人) , 就需要带参构造函数。它能让对象的初始化更灵活 , 避免创建后再手动修改成员的麻烦。
例如 : 一个 Person 类:
class Person { private:string name;int age; public:// 带参构造,初始化姓名和年龄Person(string n, int a) {name = n;age = a;} };// 创建不同的人 Person p1("张三", 18); Person p2("李四", 20);
这样就能直接创建出“张三(18岁)”和“李四(20岁)”两个不同的对象,非常直观。
2.5 补充
更精确地区分“默认构造函数”的调用规则和成员初始化的细节。下面分两种情况详细说明:
1. 类中只有内置类型成员(无自定义类型成员)
当类中只包含内置类型成员(如 int , double , 指针等)时:
- 编译器会生成默认构造函数(如果没有显式定义任何构造函数)。
- 但这个默认构造函数是“无操作”的——它不会主动初始化内置类型成员(内置类型的默认初始化是“未定义的”,值可能是随机的)。
- 实例化对象时的行为:
- 如果用默认初始化(如 MyClass obj ) , 内置类型成员的值是不确定的(未初始化)。
- C++11引入了统一初始化语法,使用大括号 {} 的形式 , 它可以让初始化语义更加明确和统一。使用 {} 进行初始化时 , 对于内置类型明确表达了要将其初始化为合适的零值的意图 ;对于自定义类型 , 也能更好地适配不同的构造情况 , 比如针对没有默认构造函数的自定义类型也能实现合理的初始化方式。
示例:
class MyClass { public:int a; // 内置类型double b; // 内置类型 };int main() {MyClass obj1; // 默认初始化:a 和 b 的值是随机的(未定义)MyClass obj2{}; // 值初始化:a=0,b=0.0(内置类型被零初始化)return 0; }
这里 MyClass 没有显式定义构造函数 , 编译器生成默认构造函数 , 但它不做任何初始化工作(内置类型的初始化需要显式处理)。
2. 类中包含自定义类型成员(嵌套类对象)
当类中包含自定义类型成员(如另一个类的对象)时:
- 1. 编译器仍会生成默认构造函数(如果没有显式定义任何构造函数)。
- 2. 默认构造函数会自动调用自定义类型成员的默认构造函数,完成嵌套对象的初始化。
也就是说,嵌套的自定义类型成员会被“自动初始化”(通过其自身的默认构造函数),无需手动干预。
示例:typedef int STDataType;class Stack { public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (STDataType*)malloc(sizeof(STDataType) * n);if (!_a) {perror("malloc failed");return;}_capacity = n;_top = 0;}private:STDataType* _a;size_t _capacity;size_t _top; };// 使用两个栈实现队列 class MyQueue { private:Stack pushSt;Stack popSt; };int main() {MyQueue q;Stack st;return 0; }
在这个的代码中 , MyQueue 类没有显式定义构造函数 , 因此编译器会自动生成一个默认构造函数(属于默认成员函数的一种)。以下是关于 MyQueue 构造函数的详细分析:
1. 编译器自动生成的默认构造函数做了什么?
当类中没有显式定义任何构造函数时,编译器会为 MyQueue 生成一个默认构造函数,其行为是:
- 对于 MyQueue 的内置类型成员(如果有的话,比如 int 、指针等):不做任何初始化(值是随机的)。
- 对于 MyQueue 的自定义类型成员(这里是 Stack _pushst 和 Stack _popst ):调用它们的默认构造函数(即 Stack 类的默认构造函数)。
2. MyQueue 的自定义类型成员如何初始化?
MyQueue 的成员 _pushst 和 _popst 是 Stack 类型(自定义类型) , 因此 : 编译器生成的 MyQueue 默认构造函数 , 会自动调用 Stack 类的默认构造函数来初始化这两个成员。
而 Stack 类的构造函数是:
Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (STDataType*)malloc(sizeof(STDataType) * n);if (!_a) {perror("malloc failed");return;}_capacity = n;_top = 0;}
由于 Stack 的构造函数是全缺省构造函数(参数 n 有默认值 4 ) , 因此它也属于 Stack 类的默认构造函数(可以无参调用)。
所以 , 当 MyQueue 的默认构造函数初始化 _pushst 和 _popst 时 , 会调用 Stack 的全缺省构造函数(Stack(int n = 4)) , 相当于:MyQueue() {// 编译器自动生成的逻辑:_pushst.Stack::Stack(4); // 调用 Stack 的全缺省构造,n=4_popst.Stack::Stack(4); // 同理 }
3. 验证:运行时的初始化过程
在 main 函数中:MyQueue q; // 创建 MyQueue 对象,调用编译器生成的默认构造函数
执行 q 的构造时 , 会依次初始化其成员 _pushst 和 _popst :
- _pushst 调用 Stack(int n = 4) , n 取默认值 4 , 因此 _pushst 的栈容量是 4 。
- _popst 同理 , 也会调用 Stack(int n = 4) , 栈容量是 4 。
4. 总结
- MyQueue 的构造函数是编译器自动生成的默认构造函数。
- 它的作用是:通过调用其自定义类型成员( _pushst 和 _popst )的默认构造函数( Stack 的全缺省构造) , 完成 MyQueue 对象的初始化。
- 如果 Stack 没有默认构造函数(比如Stack只定义了带参构造且无默认值) ,MyQueue 的默认构造函数会报错(因为无法初始化 Stack 成员)。
简单来说:编译器帮 MyQueue 自动生成了构造函数,这个构造函数会自动调用其成员 Stack 的构造函数完成初始化。
3. 析构函数
析构函数是 C++ 中类的第二个特殊成员函数 , 主要用于在对象生命周期结束时,释放对象占用的资源(如动态分配的内存、打开的文件、网络连接等 ) , 避免内存泄漏等问题。
3.1 定义
定义:析构函数是类的特殊成员函数 , 名称与类名相同 , 前面加 ~ 符号 , 没有返回值(连 void 都不能写 ) , 也不能有参数 , 一个类有且仅有一个析构函数(无法重载 , 因为参数列表固定为空)。
3.2 语法规则
- 1. 命名规则:
- 类名前加 ~ , 例如 class Stack 的析构函数是 ~Stack() 。
- 2. 参数与返回值:
- 无参数 , 无返回值(连 void 都不加)。
- 不能重载(一个类只能有1个析构函数)。
- 3. 自动调用时机:
- 对象生命周期结束时触发 , 常见场景:
- 栈对象:作用域(如函数 , {} 代码块)结束时(如 Stack s; 在函数返回时调用 ~Stack() )。
- 堆对象:delete 释放时(如 Stack* p = new Stack; delete p; 先调用 ~Stack() 再释放内存)。
- 全局/静态对象:程序结束(或静态作用域结束)时。
实例:
class Stack { public:// 构造函数Stack(int n = 4) {_a = (STDataType*)malloc(sizeof(STDataType) * n);_capacity = n;_top = 0;}// 析构函数~Stack() {// 释放动态分配的内存free(_a);_a = nullptr;_capacity = 0;_top = 0;} private:STDataType* _a;int _capacity;int _top; };
- 上述代码中 , 构造函数( Stack(int n=4) )负责“初始化资源”(如 malloc 申请内存) , 析构函数(~Stack())负责“销毁资源”(如 free 释放内存) , 避免资源泄漏。
- ~Stack 就是 Stack 类的析构函数 , 在 Stack 对象销毁时自动调用 , 用于释放构造函数中用 malloc 分配的堆内存。
3.3 编译器生成的默认析构函数
如果不手动定义析构函数 , 编译器会自动生成默认析构函数 , 但行为有明显“区分对待”:
- 1. 内置类型成员(如 int 、指针):
- 默认析构函数不做任何处理。
- 2. 自定义类型成员(如其他类的对象):
- 默认析构函数会自动调用成员的析构函数。
例如:
class A { public:~A() {cout << "A 的析构函数" << endl;} }; class B {A a_obj; // 自定义类型成员int num; // 内置类型成员// 未显式定义析构函数 }; int main() {B b;return 0; // 销毁 b 时,编译器生成的 B 的析构函数会调用 a_obj 的析构函数 }
上述代码中 , B 未显式定义析构函数 , 编译器生成的默认析构函数会调用 a_obj (A 类型 )的析构函数 , 但不会处理 num ( int 类型 )。不过 , 若 B 中有动态分配资源( 如指针指向堆内存 ), 仅靠编译器生成的析构函数无法释放 , 就需要显式定义析构函数来处理。
3.4 手动定义析构函数
“必须手动写析构”的场景:
- 当类持有“需要主动释放的资源”时(如动态内存 , 文件 , 锁) , 必须手动定义析构函数 , 否则会泄漏资源。
典型案例( Stack 类):
class Stack {STDataType* _a; // 动态分配的内存(需要释放) public:~Stack() {free(_a); // 手动释放 malloc 申请的内存_a = nullptr; // ... 其他清理(如重置 _top、_capacity)} };
反面案例(内存泄漏):
如果不写 ~Stack() , _a 指向的内存永远不会释放 , 程序运行久了会“内存爆炸”。
“无需手动写析构”的场景
- 如果类不持有需要主动释放的资源(仅包含内置类型 , std::string / std::vector 等“智能”成员) , 无需手动定义析构函数 , 直接用编译器生成的默认析构即可。
示例(“无需析构”的类):
class Date {int year = 2025; double month = 8.0;// 只是声明,无动态资源,默认析构函数足够 };
原理:
std::string / std::vector 等标准库类型的析构函数会自动释放资源(如 vector 析构时会 delete[] 内部数组) , 无需手动干预。
3.5 析构函数的“链式调用”
无论是否手动定义析构函数 , 自定义类型成员的析构函数一定会被调用 , 形成“链式析构”:
- 手动定义的析构函数执行时 , 会先执行自己的清理逻辑(如 free(_a) )。
- 然后自动调用所有自定义类型成员的析构函数(即使没手动写析构 , 编译器生成的默认析构也会做这一步)。
示例:
class Container {Stack s1; // 自定义类型成员Stack s2; // 自定义类型成员 public:~Container() {// 1. 执行自己的清理逻辑(如果有)cout << "Container 析构" << endl;// 2. 自动调用 s1.~Stack() 和 s2.~Stack()} };
3.6 局部对象的析构顺序
C++ 规定:局部作用域中 , 后定义的对象先析构(“栈式” 顺序,与构造顺序相反)。
示例:void func() {Stack s1; // 先构造Stack s2; // 后构造// 析构顺序:s2 先析构,s1 后析构 }
3.7 补充
- 内置类型(如 int , double , 指针等)没有析构函数。析构函数是面向自定义类型(类/结构体)的概念 , 用于在对象生命周期结束时释放资源(如动态内存等)。
- 对于内置类型,当它们的生命周期结束(如局部变量出作用域)时 , 系统会直接回收它们占用的内存 , 无需任何“析构”操作 , 因为它们本身不管理额外资源(仅存储值)。
关键区别:
- 1. 自定义类型:
- 析构函数由开发者定义(或编译器自动生成) , 用于释放对象持有的资源(如 new 分配的内存 , 打开的文件等) , 在对象生命周期结束时自动调用。
- 2. 内置类型:
- 没有析构函数 , 生命周期结束时直接被系统销毁(内存回收) , 不涉及任何资源释放逻辑(因为它们本身不持有需要手动释放的资源)。
- 举例:
#include <iostream>class MyClass { public:int* ptr; // 内置类型(指针)MyClass() {ptr = new int(10); // 自定义类型申请了动态内存(需要释放)std::cout << "构造:分配了动态内存" << std::endl;}~MyClass(){delete ptr; // 析构函数释放动态内存(自定义逻辑)std::cout << "析构:释放了动态内存" << std::endl;} };int main() {{int a = 5; // 内置类型,无析构MyClass obj; // 自定义类型,有析构} // 作用域结束// a 直接被销毁(内存回收),无任何操作// obj 调用析构函数,释放 ptr 指向的内存return 0; }
总结:
析构函数仅针对自定义类型 , 用于处理资源释放;内置类型无需析构 , 生命周期结束时由系统直接回收内存。
3.8 总结
- 是 “资源管理”的最后一道保障 , 尤其对动态内存 , 文件等资源 , 必须手动写析构释放。
- 编译器生成的默认析构 , 仅能自动处理“自定义类型成员的析构” , 对动态资源(如指针)无能为力。
- 记住“有资源申请(如 new / malloc ) , 就必须有对应的析构释放” , 否则必然泄漏。
结合 Stack 类的示例 , 理解析构函数如何“补全”资源管理的闭环——构造时 malloc 申请内存,析构时 free 释放,这就是 C++ 中最基础的“RAII 惯用法”(资源获取即初始化,资源释放即析构)。
总之 , 析构函数是保障 C++ 程序资源正确管理的重要机制 , 核心作用是在对象生命周期结束时清理资源 , 和构造函数协同工作 , 让对象的使用更安全、可靠。
4. 总结
构造函数与析构函数是类的“左右手”:
- 构造函数:对象创建时调用 , 负责初始化(分配资源、设置初始值), 让对象“可用”。
- 析构函数:对象销毁时调用 , 负责清理(释放资源、关闭连接), 让对象“收尾”。
二者配合保障对象从生到死的资源合理管理 , 避免泄漏(如动态内存、文件句柄 ), 是 C++ 类实现“自洽生命周期”的核心。
以上是关于类的默认成员函数中的构造函数和析构函数 , 后面还有4个类似的默认函数 , 我们在下篇文章中再说 , 感谢大家的观看!