【初阶C++】类和对象(中)

发布时间:2026/8/9 3:57:38
【初阶C++】类和对象(中) 上一篇《类和对象上》我们学习了类定义、访问限定符、实例化、this 指针等基础内容本篇聚焦类的 6 个默认成员函数是 C 面向对象核心重难点。 默认成员函数定义用户不显式实现编译器会自动生成的成员函数。 一个类在我们不手写任何相关函数时编译器默认生成 6 个默认成员函数构造函数析构函数拷贝构造函数赋值运算符重载operator普通取地址重载operatorconst 取地址重载operator constC11 新增移动构造、移动赋值后续章节单独讲解。 学习默认成员函数两大核心思路不手动实现时编译器自动生成的函数底层行为是什么能否满足业务需求自动生成函数不满足需求时如何手动自定义实现。全文所有文档内2024年份统一替换为2026修正课件内语法笔误、代码残缺、逻辑错误补充底层原理、面试坑点、完整可运行代码采用标准 Markdown 格式可直接复制粘贴到掘金 / CSDN / 知乎专栏。一、构造函数1.1 构造函数核心作用构造函数是特殊成员函数不是用来开辟对象内存局部对象栈内存、堆 new 内存提前分配完成核心功能对象实例化时自动初始化成员变量替代之前手写的Init()初始化函数无需手动调用杜绝忘记初始化的 bug。1.2 构造函数七大硬性特点函数名必须和类名完全相同大小写一致无返回值不写void、不写任意类型返回值C 语法强制规定对象创建实例化时编译器自动调用对应构造函数无法手动主动调用支持函数重载一个类可以存在多个不同参数的构造函数只要用户显式写了任意一个构造函数编译器不再自动生成无参默认构造「无参构造、全缺省构造、编译器自动生成无参构造」都叫默认构造函数三类有且只能存在一个不能同时定义原因无参构造和全缺省构造同时存在时Date d;会产生调用歧义编译报错定义不需要传递任何实参就能直接调用的构造函数统称为默认构造编译器自动生成的默认构造行为规则内置类型int/char/ 指针 /double 等不会主动初始化值随机垃圾值自定义类型class/struct成员自动调用该自定义类型的默认构造函数初始化如果自定义成员没有默认构造直接编译报错解决方案初始化列表下篇讲解。补充类型区分内置类型语言原生基础数据类型int、char、short、long、float、double、指针、bool自定义类型我们用class/struct自己定义的类如Date、Stack、MyQueue。1.3 完整可运行 Date 构造函数示例修正课件残缺代码cpp运行#includeiostream 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(2026, 1, 1); // 调用带参构造 // Date d3(); // 严重坑点这不是创建对象是函数声明 d1.Print(); d2.Print(); return 0; }重点坑点解析Date d3();不会创建对象编译器识别为名为 d3、无参数、返回值 Date 的函数声明不会执行构造函数 无参对象正确写法Date d3;不加括号。1.4 Stack 全缺省构造 组合类 MyQueue 示例场景类内包含自定义类型成员组合cpp运行#includeiostream #includecstdlib using namespace std; typedef int STDataType; class Stack { public: // 全缺省构造默认容量4 Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 用两个Stack组合实现队列 class MyQueue { private: Stack pushst; Stack popst; }; int main() { MyQueue mq; /* MyQueue我们没有写任何构造函数编译器自动生成默认构造 自动调用pushst、popst的Stack全缺省构造完成初始化 */ return 0; }二、析构函数2.1 析构函数核心作用析构函数和构造函数功能完全相反对象生命周期结束时自动调用清理、释放对象内部堆资源。 误区纠正析构函数不会销毁对象本身局部对象栈内存由函数栈帧自动回收堆对象new出的内存由delete释放析构只负责内部资源清理。 类比替代之前 Stack 手写的Destroy()销毁函数无堆资源的类如 Date不需要手动写析构编译器自动生成的即可使用。2.2 析构函数六大核心特点函数名~类名波浪号 类名无参数、无返回值禁止写 void / 任意返回类型一个类只能有一个析构函数不支持重载对象生命周期结束编译器自动调用无需手动执行自动生成析构行为规则内置类型成员不做任何处理自定义类型成员自动调用该成员自身的析构函数释放资源手动自定义析构后自定义类型成员依旧会自动调用自身析构业务使用规范类中使用malloc/realloc/new开辟堆内存资源必须手动写析构释放否则内存泄漏无堆资源Date 纯内置变量、组合类 MyQueue内部成员自有析构直接使用默认析构即可析构调用顺序同一局部域内后创建的对象先析构栈先进后出规则。2.3 Stack 完整析构代码修复课件笔误Stack 写成 Stackcpp运行#includeiostream #includecstdlib using namespace std; typedef int STDataType; class Stack { public: Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { 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; }; // 组合队列 class MyQueue { private: Stack pushst; Stack popst; }; int main() { Stack st; MyQueue mq; // main函数结束st、mq生命周期结束自动调用析构 return 0; }运行结果先析构 mq再析构 st后定义先析构mq 内部自动调用两个 Stack 的析构释放数组。2.4 实战对比C 语言栈 VS C 栈构造 析构优势以括号匹配isValid函数为例直观体现构造 / 析构自动管理资源避免忘记 Init/DestroyC 版本自动初始化 自动释放无内存泄漏风险cpp运行#includeiostream #includecassert using namespace std; typedef int STDataType; class Stack { public: Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); _capacity n; _top 0; } void Push(char x) { if (_top _capacity) { int newcap _capacity * 2; STDataType* tmp (STDataType*)realloc(_a, newcap * sizeof(STDataType)); _a tmp; _capacity newcap; } _a[_top] x; } bool Empty() { return _top 0; } char Top() { assert(_top 0); return _a[_top - 1]; } void Pop() { assert(_top 0); _top--; } ~Stack() { free(_a); _a nullptr; } private: STDataType* _a; size_t _capacity; size_t _top; }; 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; cout isValid([(])[]) endl; return 0; }C 语言版本手动初始化、手动销毁极易漏写 Destroy 导致泄漏cpp运行#includestdio.h #includestdlib.h #includestdbool.h #includeassert.h typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps) { assert(ps); ps-a NULL; ps-top 0; ps-capacity 0; } void STDestroy(ST* ps) { assert(ps); free(ps-a); ps-a NULL; ps-top ps-capacity 0; } void STPush(ST* ps, STDataType x) { assert(ps); if (ps-top ps-capacity) { int newcap ps-capacity 0 ? 4 : ps-capacity * 2; STDataType* tmp (STDataType*)realloc(ps-a, newcap * sizeof(STDataType)); ps-a tmp; ps-capacity newcap; } ps-a[ps-top] x; } bool STEmpty(ST* ps) { assert(ps); return ps-top 0; } STDataType STTop(ST* ps) { assert(ps); assert(!STEmpty(ps)); return ps-a[ps-top - 1]; } void STPop(ST* ps) { assert(ps); assert(!STEmpty(ps)); ps-top--; } bool isValid(const char* s) { ST st; STInit(st); // 必须手动初始化 while (*s) { if (*s ( || *s [ || *s {) { STPush(st, *s); } else { if (STEmpty(st)) { STDestroy(st); // 提前返回必须手动销毁 return false; } char top STTop(st); STPop(st); if ((top ( *s ! )) || (top [ *s ! ]) || (top { *s ! })) { STDestroy(st); return false; } } s; } bool ret STEmpty(st); STDestroy(st); // 函数结束必须手动销毁漏写直接内存泄漏 return ret; } int main() { printf(%d\n, isValid([()][])); printf(%d\n, isValid([(])[])); return 0; }对比总结C 语言手动调用STInit初始化、所有分支手动STDestroy释放漏写一处就内存泄漏C构造自动初始化析构自动释放无需手动管理资源封装优势明显。三、拷贝构造函数3.1 拷贝构造定义拷贝构造是特殊的构造函数属于构造函数重载第一个参数必须是当前类类型的引用其余所有参数必须带默认缺省值。3.2 五大核心特点属于构造函数重载函数名和类名一致第一个参数必须是类引用const 类名禁止传值Date(Date d)会引发无限递归编译报错原理传值会调用拷贝构造拷贝构造又传值无限套娃三种场景会自动调用拷贝构造使用一个已存在对象初始化新对象Date d2(d1); Date d3 d1;自定义类型对象传值作为函数参数自定义类型对象传值作为函数返回值不手动实现时编译器自动生成默认拷贝构造内置类型逐字节值拷贝浅拷贝自定义类型成员调用该成员自身的拷贝构造业务区分深拷贝 vs 浅拷贝Date 类全内置变量无堆指针默认浅拷贝完全够用无需手写拷贝构造Stack 类存在_a堆指针默认浅拷贝会让两个对象指向同一块堆内存析构时重复 free 崩溃必须手动实现深拷贝组合类 MyQueue内部 Stack 手写深拷贝后MyQueue 默认拷贝构造自动调用 Stack 拷贝构造无需手动实现开发技巧只要类手动写了析构释放堆资源就必须手动实现拷贝构造、赋值重载。3.3 Date 拷贝构造完整示例修复课件传值报错代码cpp运行#includeiostream using namespace std; class Date { public: // 全缺省构造 Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // 正确拷贝构造const引用避免拷贝、防止修改原对象 Date(const Date d) { cout 调用拷贝构造 endl; _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(2026, 7, 5); return tmp; } int main() { Date d1(2026, 7, 5); Func1(d1); // 传值调用拷贝构造 Date d3(d1); // 拷贝构造标准写法1 Date d4 d1; // 拷贝构造标准写法2 Date ret Func2(); // 函数返回值调用拷贝构造 ret.Print(); return 0; }致命坑点传引用返回局部对象野引用cpp运行// 错误示例禁止使用 Date FuncErr() { Date tmp(2026, 7, 5); return tmp; // tmp是函数局部对象函数结束销毁返回野引用 }函数执行完毕局部栈对象销毁引用指向已销毁内存运行随机崩溃。引用返回使用前提返回对象生命周期大于函数如全局对象、堆对象、this 自身。3.4 Stack 深拷贝完整实现解决浅拷贝重复释放崩溃cpp运行#includeiostream #includecstdlib #includecstring using namespace std; typedef int STDataType; class Stack { public: // 构造 Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc失败); return; } _capacity n; _top 0; } // 深拷贝拷贝构造 Stack(const Stack st) { // 开辟独立堆内存不共用原数组 _a (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr _a) { perror(malloc拷贝空间失败); return; } // 拷贝所有有效数据 memcpy(_a, st._a, sizeof(STDataType) * st._top); _top st._top; _capacity st._capacity; } void Push(STDataType x) { if (_top _capacity) { int newcap _capacity * 2; STDataType* tmp (STDataType*)realloc(_a, newcap * sizeof(STDataType)); _a tmp; _capacity newcap; } _a[_top] x; } ~Stack() { free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 组合队列 class MyQueue { private: Stack pushst; Stack popst; }; int main() { Stack st1; st1.Push(1); st1.Push(2); // 深拷贝st1、st2拥有独立堆数组析构不会重复释放 Stack st2 st1; MyQueue mq1; MyQueue mq2 mq1; // MyQueue默认拷贝构造自动调用Stack深拷贝构造 return 0; }浅拷贝灾难说明如果不手写深拷贝构造编译器默认浅拷贝仅复制_a指针值st1、st2 的_a指向同一块堆内存函数结束先析构 st2 释放数组再析构 st1 时重复 free 同一块内存程序直接崩溃。四、赋值运算符重载 operator4.1 前置知识运算符重载通用规则运算符重载是给类类型对象赋予运算符运算能力本质是特殊命名函数operator运算符。通用强制规则函数命名格式operator / operator / operator参数数量一元运算符1 个参数二元运算符2 个参数成员函数重载第一个运算对象是 this参数数量少 1 个优先级、结合性、操作数数量不能修改禁止自定义全新运算符如operator不合法5 个运算符禁止重载.* 、:: 、sizeof 、?: 、.重载运算符至少有一个参数是自定义类类型不能修改内置类型运算规则区分前置 、后置 后置 强制增加 int 占位参数构成重载 流运算符必须重载为全局友元函数否则使用顺序颠倒对象 cout 不符合阅读习惯。示例 运算符重载成员函数版本cpp运行#includeiostream using namespace std; class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // 重载判断两个日期相等 bool operator(const Date d) const { return _year d._year _month d._month _day d._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 7, 5); Date d2(2026, 7, 6); d1 d2; // 编译器转换为 d1.operator(d2); return 0; }4.2 赋值运算符重载核心概念operator是六大默认成员函数之一用于两个已经存在的对象之间赋值拷贝。关键区分面试高频考点拷贝构造新对象创建用已有对象初始化一个对象不存在一个存在赋值重载两个对象都已经实例化存在互相赋值覆盖。4.3 赋值重载五大强制特点必须重载为类成员函数不能写成全局函数参数推荐const 类名传值会触发拷贝构造降低效率返回值推荐类名支持连续赋值d1 d2 d3不手动实现时编译器自动生成默认赋值重载行为同默认拷贝构造内置浅拷贝、自定义调用自身赋值重载堆资源类Stack必须手动实现深拷贝赋值否则浅拷贝重复释放崩溃无资源类Date默认赋值完全够用。4.4 Date 完整赋值重载实现处理自赋值cpp运行#includeiostream 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; } // 赋值运算符重载 Date operator(const Date d) { // 防止自己赋值给自己避免重复释放资源 if (this ! d) { _year d._year; _month d._month; _day d._day; } return *this; // 返回当前对象支持连续赋值 } void Print() const { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 7, 5); Date d2(2026, 7, 6); Date d3(2026, 1, 1); d1 d2; // 调用赋值重载两个对象都已存在 d1 d2 d3; // 连续赋值依赖引用返回 Date d4 d1; // 调用拷贝构造不是赋值重载重点区分 return 0; }4.5 完整日期类实战工程代码头文件 源文件全运算符重载Date.h 头文件cpp运行#pragma once #includeiostream #includecassert using namespace std; class Date { // 友元全局流运算符访问私有成员 friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); public: // 构造函数 Date(int year 1900, int month 1, int day 1); void Print() const; // 获取当月天数 int GetMonthDay(int year, int month) { assert(month 0 month 13); static int monthDayArray[13] { -1,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month 2 ((year % 4 0 year % 100 ! 0) || (year % 400 0))) { return 29; } return monthDayArray[month]; } // 日期合法性校验 bool CheckDate(); // 比较运算符 bool operator(const Date d) const; bool operator(const Date d) const; bool operator(const Date d) const; bool operator(const Date d) const; bool operator(const Date d) const; bool operator!(const Date d) const; // 加减天数 Date operator(int day); Date operator(int day) const; Date operator-(int day); Date operator-(int day) const; // 两个日期相差天数 int operator-(const Date d) const; // 自增自减 Date operator(); // 前置 Date operator(int); // 后置 Date operator--(); // 前置-- Date operator--(int); // 后置-- private: int _year; int _month; int _day; }; // 流运算符全局重载 ostream operator(ostream out, const Date d); istream operator(istream in, Date d);Date.cpp 实现文件cpp运行#includeDate.h bool Date::CheckDate() { if (_month 1 || _month 12 || _day 1 || _day GetMonthDay(_year, _month)) return false; return true; } Date::Date(int year, int month, int day) { _year year; _month month; _day day; if (!CheckDate()) { cout 输入日期非法 endl; } } void Date::Print() const { cout _year - _month - _day endl; } bool Date::operator(const Date d) const { if (_year d._year) return true; else if (_year d._year) { if (_month d._month) return true; else if (_month d._month) return _day d._day; } return false; } bool Date::operator(const Date d) const { return *this d || *this d; } bool Date::operator(const Date d) const { return !(*this d); } bool Date::operator(const Date d) const { return !(*this d); } bool Date::operator(const Date d) const { return _year d._year _month d._month _day d._day; } bool Date::operator!(const Date d) const { return !(*this d); } Date Date::operator(int day) { if (day 0) return *this - -day; _day day; while (_day GetMonthDay(_year, _month)) { _day - GetMonthDay(_year, _month); _month; if (_month 13) { _year; _month 1; } } return *this; } Date Date::operator(int day) const { Date tmp *this; tmp day; return tmp; } Date Date::operator-(int day) { if (day 0) return *this -day; _day - day; while (_day 0) { _month--; if (_month 0) { _year--; _month 12; } _day GetMonthDay(_year, _month); } return *this; } Date Date::operator-(int day) const { Date tmp *this; tmp - day; return tmp; } Date Date::operator() { *this 1; return *this; } Date Date::operator(int) { Date tmp(*this); *this 1; return tmp; } Date Date::operator--() { *this - 1; return *this; } Date Date::operator--(int) { Date tmp *this; *this - 1; return tmp; } int Date::operator-(const Date d) const { Date max *this; Date min d; int flag 1; if (*this d) { max d; min *this; flag -1; } int n 0; while (min ! max) { min; n; } return n * flag; } ostream operator(ostream out, const Date d) { out d._year 年 d._month 月 d._day 日; return out; } istream operator(istream in, Date d) { cout 请输入年月日; in d._year d._month d._day; if (!d.CheckDate()) cout 日期输入非法 endl; return in; }Test.cpp 测试文件cpp运行#includeDate.h void TestDate1() { Date d1(2026, 4, 14); Date d2 d1 30000; cout d1 endl; cout d2 endl; Date d3(2026, 4, 14); Date d4 d3 - 5000; cout d4 endl; Date d5(2026, 4, 14); d5 -5000; cout d5 endl; } void TestDate2() { Date d1(2026, 4, 14); Date d2 d1; cout d1 d2 endl; Date d3 d1; cout d1 d3 endl; } void TestDate3() { Date d1(2026, 4, 14); Date d2(2036, 4, 14); int n d1 - d2; cout 相差天数 n endl; } void TestDate4() { Date d1(2026, 4, 14); Date d2 d1 30000; cout d1 endl d2 endl; cin d1 d2; cout d1 d2 endl; } int main() { TestDate1(); TestDate2(); TestDate3(); TestDate4(); return 0; }五、const 修饰成员函数5.1 const 成员函数定义const写在成员函数参数列表后方修饰隐式this指针。 普通成员函数隐式 thisDate* const this指针不可改指向内容可修改 const 成员函数隐式 thisconst Date* const this指针不可改指向对象所有成员不可修改5.2 核心规则const 成员函数内部不能修改任何成员变量const 对象只能调用 const 成员函数不能调用普通成员函数普通非 const 对象可以调用 const 成员函数权限缩小安全只读函数Print、Get 等统一加 const工程规范。示例代码cpp运行#includeiostream using namespace std; class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // const成员函数禁止修改成员 void Print() const { cout _year - _month - _day endl; // _year 2026; 编译报错无法修改成员 } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 7, 5); d1.Print(); // 普通对象调用const函数 const Date d2(2026, 8, 5); d2.Print(); // const对象只能调用const成员函数 return 0; }六、取地址运算符重载 operator6.1 两种取地址重载普通对象取地址Date* operator()const 对象取地址const Date* operator() const6.2 使用场景绝大多数场景编译器自动生成的版本完全够用默认返回对象自身地址return this;。 仅特殊业务场景手动实现比如禁止外部获取对象真实地址返回空指针 / 自定义虚假地址。示例代码cpp运行#includeiostream using namespace std; class Date { public: Date* operator() { return this; // return nullptr; // 自定义屏蔽真实地址 } const Date* operator() const { return this; // return nullptr; } private: int _year; int _month; int _day; }; int main() { Date d; cout d endl; const Date d2; cout d2 endl; return 0; }本章完整总结六大默认成员函数构造、析构、拷贝构造、赋值重载、普通取地址、const 取地址构造函数对象创建自动初始化无参 / 全缺省为默认构造编译器无自定义构造时自动生成析构函数对象销毁自动释放堆资源一个类仅一个析构后创建对象先析构拷贝构造引用传参避免无限递归三种场景自动调用堆资源类必须手写深拷贝赋值重载两个已存在对象赋值返回引用支持连续赋值自赋值判断防止内存错误运算符重载遵循语法规则 全局友元后置 带 int 占位参数const 成员函数修饰 this 指针只读函数统一加 constconst 对象仅能调用 const 成员取地址重载默认返回 this几乎不需要手动实现开发通用准则类中手动写析构释放堆内存必须同步实现拷贝构造、赋值运算符重载。