资讯动态

C++(类与对象)(中)

发布时间:2026/8/7 7:40:34 来源:尧图企业网站定制
欢迎阅读 欢迎来到「C 类和对象中——默认成员函数」深入讲解本文将带你从“一个空类究竟包含了哪些成员”这一疑问出发逐步深入理解构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址操作符重载以及 const 成员让你彻底掌握 C 类的六大默认成员函数。在开始之前建议你先了解背景知识在 C 中即使你定义了一个空类编译器也会在需要时自动生成六个默认成员函数。理解这些函数的作用、生成时机和自定义方法是写出正确、高效 C 代码的关键。明确学习目标掌握以下六个核心知识点——①构造函数掌握构造函数的函数名与类名相同、无返回值、支持重载、自动调用的特点理解其用于初始化对象成员变量的核心作用以及初始化列表的用法②析构函数掌握析构函数的函数名在类名前加~、无参数无返回值、不可重载的特点理解其在对象生命周期结束时用于资源清理如释放动态内存的核心作用③拷贝构造函数掌握拷贝构造函数的函数名与类名相同、参数为本类对象的const引用的特点理解其在用已有对象初始化新对象以及函数传参/返回值时的调用时机以及深浅拷贝的区别与实现④赋值运算符重载掌握赋值运算符operator的返回值类型为类引用、参数为const类引用的标准写法理解其在已有对象之间赋值时的调用时机并注意自赋值检测和深浅拷贝的处理⑤取地址操作符重载普通版与 const 版了解取地址运算符operator的重载形式分为普通对象和 const 对象两个版本一般情况下默认生成的已够用但在特殊场景如隐藏对象地址下可自定义⑥const 成员掌握 const 成员函数的写法函数声明末尾加const理解其承诺不修改对象成员变量的核心含义并明确 const 对象只能调用 const 成员函数的规则。一.类的6个默认成员函数如果一个类中什么成员都没有简称为空类。空类中真的什么都没有吗并不是任何类在什么都不写时编译器会自动生成以下6个默认成员函数。默认成员函数用户没有显式实现编译器会生成的成员函数称为默认成员函数我们主要学的是上面4个默认成员函数二.构造函数class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(2022,1,11); d2.Init(2022,1,12); d1.Print(); d2.Print(); return 0; }对于Date类可以通过 Init 公有方法给对象设置日期但如果每次创建对象时都调用该方法设置 信息未免有点麻烦那能否在对象创建时就将信息设置进去呢构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用以保证 每个数据成员都有 一个合适的初始值并且在对象整个生命周期内只调用一次2.1 构造函数特性构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象而是初始化对象。其特征如下1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载。class Data { public: // 1.无参构造函数 Date() {} // 2.带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } private: int _year; int _month; int _day; }; int main() { Date d1; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数 }要点如果通过无参构造函数创建对象时对象后面不用跟括号否则会认为是函数声明无法使用5. 如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。6.关于编译器生成的默认成员函数聪明的你会有疑惑不实现构造函数的情况下编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用d对象调用了编译器生成的默认构造函数但是d对象_year/_month/_day依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用解答C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型如int/char...自定义类型就是我们使用class/struct/union等自己定义的类型看看下面的程序就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。class Time { public: Time() { cout Time() endl; _hour 0; _minute 0; _second 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year 1970; int _month 1; int _day 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }所以说默认构造函数对内置类型它不处理但是自定义类型如果其有默认构造函数那么则会调用注意是默认构造函数编译器自动生成的无参全缺省注意C11 中针对内置类型成员不初始化的缺陷又打了补丁即内置类型成员变量在 类中声明时可以给默认值。class Date { private: // 基本类型(内置类型) int _year 1970; int _month 1; int _day 1; // 自定义类型 Time _t; };7. 无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个。 注意无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以被认为是默认构造函数。三.析构函数概念通过前面构造函数的学习我们知道一个对象是怎么来的那一个对象又是怎么没呢的 析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。3.1 析构函数的特性1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。注意析构函数不能重载4. 对象生命周期结束时C编译系统系统自动调用析构函数class Time { public: ~Time() { cout ~Time() endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year 1970; int _month 1; int _day 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }5.默认析构函数如何处理内置类型不处理自定义类型使用它自己的析构函数6.如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数大家到这可能会有一个疑问就是为什么默认构造函数对自定义类型采用是其默认构造而这个为什么是采用它自己的析构函数因为析构函数只有一个但是构造函数可能有无数个因此如果你写了很多构造函数它这么知道调用那个因此它只能选择调用默认构造因为构造函数只有一个默认构造四.拷贝构造函数拷贝构造函数用已存在的类类型对象创建新对象时自动调用。4.1 拷贝构造函数的特性拷贝构造函数是构造函数的一个重载形式拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错因为会引发无穷递归调用。若未显式定义生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序拷贝这种拷贝叫做浅拷贝当设计资源申请时拷贝构造函数需要写因为默认的是浅拷贝会出问题使用场景①使用已存在对象创建新对象②函数参数类型为类类型对象③函数返回值类型为类类型对象class Date { public: Date(int year 1900, int month 1, int day 1) { _year year; _month month; _day day; } // Date(const Date d) // 错误写法编译报错会引发无穷递归 Date(const Date d) // 正确写法 { _year d._year; _month d._month; _day d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }我们这边讲一下浅拷贝到底是什么情况下面我会画一张图什么是浅拷贝就是两个对象共用了一份资源那么浅拷贝为什么会出现问题呢你想呀如果你对象2对资源进行改变后你对象1是不是看到的资源是被更改过的如果你觉得不严重你在想如果对象2对资源进行释放那我对象1不就无法访问到资源然后报错那我们是如何解决这种问题的呢采用的是深拷贝解决的下面我们画一下深拷贝的图如果是这样的话你对象2在这么修改它的资源都不会影响对象1了这不就解决了浅拷贝的问题吗但是你也发现了深拷贝是会多用内存的这也是为什么还会有浅拷贝这家伙五.赋值运算符重载5.1 运算符重载C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似。格式返回值类型 operator操作符(参数列表)注意1.不能通过连接其他符号来创建新的操作符比如operator2.重载操作符必须有一个类类型参数3.用于内置类型的运算符其含义不能改变例如内置的整型不能改变其含义。作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数的第一个参数为隐藏的this4..* :: sizeof ?. . 注意以上5个运算符不能重载。// 全局的operator class Date { public: Date(int year 1900, int month 1, int day 1) { _year year; _month month; _day day; } //private: int _year; int _month; int _day; }; // 这里会发现运算符重载成全局的就需要成员变量是公有的那么问题来了封装性如何保证 // 这里其实可以用我们后面学习的友元解决或者干脆重载成成员函数。 bool operator(const Date d1, const Date d2) { return d1._year d2._year d1._month d2._month d1._day d2._day; } void Test () { Date d1(2018, 9, 26); Date d2(2018, 9, 27); cout(d1 d2)endl; }这个是重载成成员函数// 全局的operator class Date { public: Date(int year 1900, int month 1, int day 1) { _year year; _month month; _day day; } bool operator(const Date d2) { return _year d2._year _month d2._month _day d2._day; } private: int _year; int _month; int _day; };5.2赋值运算符重载1. 赋值运算符重载格式参数类型const T传递引用可以提高传参效率返回值类型T返回引用可以提高返回的效率有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this要复合连续赋值的含义class Date { public : Date(int year 1900, int month 1, int day 1) { _year year; _month month; _day day; } Date operator(const Date d) { if(this ! d) { _year d._year; _month d._month; _day d._day; } return *this; } private: int _year ; int _month ; int _day ; };2.赋值运算符只能重载成类的成员函数不能重载成全局函数原因赋值运算符如果不显式实现编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载就和编译器在类中生成的默认赋值运算符重载冲突了故赋值运算符重载只能是类的成员函数。3.没有显式实现生成一个默认赋值运算符重载以值的方式逐字节拷贝4.如果类中未涉及到资源管理赋值运算符是否实现都可以一旦涉及到资源管理有开空间则必须要实现5.和拷贝构造函数区分前者是一个对象去创造一个对象后者是一个对象改变另一个对象5.3 前置和后置重载Date operator()//前置 { _day 1; return *this; } Date operator(int)//后置 { Date temp(*this); _day 1; return temp; }默认operator就算前置那么如何实现后置呢就是对前者进行重载函数重载然后聪明的你发现二者写法上有很大区别注意后置是先使用后1因此需要返回1之前的旧值故需在实现时需要先将this保存 一份然后给this1而temp是临时对象因此只能以值的方式返回不能返回引用。六.const成员将const修饰的“成员函数”称之为const成员函数const修饰类成员函数实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进行修改格式void 函数名constclass Date { public: Date(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout Print() endl; cout year: _year endl; cout month: _month endl; cout day: _day endl endl; } void Print() const { cout Print()const endl; cout year: _year endl; cout month: _month endl; cout day: _day endl endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; void Test() { Date d1(2022,1,13); d1.Print(); const Date d2(2022,1,13); d2.Print(); }请思考下面的几个问题1. const对象可以调用非const成员函数吗不可 权限放大2. 非const对象可以调用const成员函数吗可 权限缩小3. const成员函数内可以调用其它的非const成员函数吗 不可 权限放大4. 非const成员函数内可以调用其它的const成员函数吗可 权限缩小那么关于今天的内容就讲到这里了类的知识是非常重要的类是面向对象的一个重点知识因此我们要对它有深刻的理解类的剩余部分就留给下一次经行编写了 闭幕 恭喜你完成了 C 类与对象核心知识的学习今天我们系统梳理了构造函数、析构函数、拷贝构造函数、赋值运算符重载和const 成员这五大核心模块。为了巩固知识并进一步拓展建议你动手实践在本地 IDE 或在线编译器中编写代码尝试不同的测试用例亲身体验每个知识点的用法。深入思考构造函数构造函数的特点是什么什么是缺省构造函数初始化列表和赋值有什么区别哪些场景必须使用初始化列表析构函数析构函数的特点是什么什么情况下需要显式定义析构函数析构函数的调用顺序是怎样的拷贝构造函数拷贝构造函数的调用时机有哪些深拷贝和浅拷贝的区别是什么什么情况下必须显式定义拷贝构造函数赋值运算符重载赋值运算符和拷贝构造函数的区别是什么赋值运算符重载的返回值为什么是引用如何实现链式赋值const 成员const 成员函数的作用是什么const 对象能调用非 const 成员函数吗const 成员变量如何初始化mutable 关键字的作用是什么延伸挑战定义一个Date类实现构造函数带缺省参数、析构函数和打印函数。定义一个String类显式实现拷贝构造函数和赋值运算符重载解决浅拷贝问题。在Date类中添加const 成员函数和const 成员变量验证 const 对象的调用规则。实现一个Stack类动态数组包含构造函数、析构函数、拷贝构造函数和赋值运算符重载。在一个类中演示初始化列表和赋值的区别例如const成员和引用成员的初始化。如果你觉得本文对你有所帮助欢迎点赞 / 收藏关注作者获取更多题解留言交流你的疑问或优化思路深入思考参考答案构造函数构造函数自动调用可以重载无返回值。缺省构造函数是无参或全缺省参数的构造函数。初始化列表在对象创建时直接初始化成员效率更高赋值是先创建再赋值。const成员、引用成员、没有默认构造函数的类成员必须使用初始化列表。析构函数析构函数自动调用无返回值不可重载只能有一个。管理堆资源的类如动态数组、文件句柄必须显式定义析构函数。析构顺序与构造顺序相反先构造的后析构。拷贝构造函数拷贝构造的调用时机对象以值传递传入函数、对象以值返回、对象用另一个对象初始化。浅拷贝只复制指针值深拷贝复制指针指向的内容。管理堆资源的类必须显式定义深拷贝。赋值运算符重载拷贝构造是创建新对象时初始化赋值是已有对象赋值。赋值运算符返回*this的引用是为了支持链式赋值a b c和连续调用。const 成员const 成员函数不能修改成员变量const 对象只能调用 const 成员函数。const 成员变量必须通过初始化列表初始化。mutable成员变量在 const 成员函数中可修改。️延伸挑战参考解法1. Date 类构造函数、析构函数、缺省参数/* * 功能Date 类演示构造函数带缺省参数和析构函数 * 构造函数全缺省参数支持无参和带参创建 * 析构函数打印销毁信息验证生命周期 */ #include iostream using namespace std; class Date { private: int _year; int _month; int _day; public: // ----- 全缺省构造函数 ----- // 无参调用用默认值 2024-1-1 // 有参调用用传入值 Date(int year 2024, int month 1, int day 1) : _year(year), _month(month), _day(day) // 初始化列表 { cout Date() constructor called: _year - _month - _day endl; } // ----- 析构函数 ----- // 对象销毁时自动调用用于释放资源无堆资源时只做日志 ~Date() { cout ~Date() destructor called: _year - _month - _day endl; } void Print() const { cout _year - _month - _day endl; } }; int main() { Date d1; // 调用缺省构造2024-1-1 Date d2(2025, 12, 25); // 调用带参构造2025-12-25 d1.Print(); d2.Print(); // d1 和 d2 在 main 结束时自动析构 return 0; }2. String 类深拷贝、拷贝构造、赋值重载/* * 功能String 类演示深拷贝、拷贝构造函数、赋值运算符重载 * 解决浅拷贝问题管理堆内存的类必须显式实现拷贝构造和赋值 * 重点深拷贝、资源管理、自赋值检测 */ #include iostream #include cstring using namespace std; class String { private: char* _str; // 指向堆内存的指针 size_t _size; public: // ----- 构造函数 ----- // 分配堆内存存储字符串 String(const char* str ) : _size(strlen(str) 1) { _str new char[_size]; // 堆分配 strcpy(_str, str); // 拷贝内容 cout String() constructed: _str endl; } // ----- 拷贝构造函数深拷贝 ----- // 参数为 const 引用避免无限递归拷贝 // 重要必须写深拷贝否则两个对象指向同一块堆内存浅拷贝问题 String(const String other) : _size(other._size) { _str new char[_size]; // 新分配堆内存 strcpy(_str, other._str); // 拷贝内容深拷贝 cout String(const String) copied: _str endl; } // ----- 赋值运算符重载深拷贝 ----- // 返回值String 支持链式赋值a b c // 参数const 引用避免拷贝 // 关键检测自赋值释放旧资源分配新资源 String operator(const String other) { cout operator called endl; // 1. 检测自赋值防止自己给自己赋值时释放了资源 if (this ! other) { // 2. 释放旧资源 delete[] _str; // 3. 分配新资源并拷贝内容深拷贝 _size other._size; _str new char[_size]; strcpy(_str, other._str); } // 4. 返回自身引用 return *this; } // ----- 析构函数 ----- // 释放堆内存防止内存泄漏 ~String() { cout ~String() destructor: _str endl; delete[] _str; // 释放堆内存 _str nullptr; // 置空防止野指针 } void Print() const { cout _str endl; } }; int main() { String s1(Hello); // 构造函数 String s2(s1); // 拷贝构造函数深拷贝 String s3(World); s3 s1; // 赋值运算符重载深拷贝 s1.Print(); s2.Print(); s3.Print(); // s1, s2, s3 在 main 结束时自动析构 return 0; }3. const 成员函数和 const 成员变量/* * 功能演示 const 成员函数、const 成员变量、mutable 关键字 * 重点const 对象只能调用 const 成员函数 * const 成员变量必须用初始化列表初始化 * mutable 成员变量在 const 成员函数中可修改 */ #include iostream using namespace std; class Date { private: // const 成员变量只能在初始化列表中初始化 const int _year; const int _month; const int _day; // mutable即使对象是 const该成员也可在 const 函数中被修改 mutable int _accessCount; // 记录访问次数 public: // ----- 构造函数const 成员必须在初始化列表初始化 ----- Date(int year, int month, int day) : _year(year), _month(month), _day(day), _accessCount(0) { // _year year; // 错误const 成员不能在函数体内赋值 } // ----- const 成员函数 ----- // 承诺不修改成员变量但可修改 mutable 成员 void Print() const { _accessCount; // mutable 成员在 const 函数中可修改 cout 访问次数: _accessCount endl; cout _year - _month - _day endl; } // ----- 非 const 成员函数 ----- void ModifyDay(int day) { // _day day; // 错误_day 是 const 成员 // 只能修改非 const 成员 } // const 对象只能调用 const 成员函数 // 非 const 对象可以调用 const 和非 const }; int main() { // ----- 非 const 对象 ----- Date d1(2024, 8, 5); d1.Print(); // 可以调用 const 成员函数 // ----- const 对象 ----- const Date d2(2025, 1, 1); d2.Print(); // 正确const 对象只能调用 const 成员函数 // d2.ModifyDay(10); // 错误const 对象不能调用非 const 成员函数 return 0; }4. Stack 类动态数组完整类/* * 功能Stack 类演示动态数组的完整实现 * 包含构造函数、析构函数、拷贝构造函数、赋值运算符重载 * 场景管理堆资源的类必须显式实现这四个函数Rule of Three */ #include iostream using namespace std; class Stack { private: int* _data; // 动态数组指针 int _top; // 栈顶索引 int _capacity; // 容量 public: // ----- 构造函数 ----- // 分配初始容量初始化栈 Stack(int capacity 10) : _capacity(capacity), _top(-1) { _data new int[_capacity]; // 堆分配 cout Stack() constructed, capacity _capacity endl; } // ----- 拷贝构造函数深拷贝 ----- // 必须深拷贝否则两个栈共享同一块内存 Stack(const Stack other) : _capacity(other._capacity), _top(other._top) { _data new int[_capacity]; // 新分配堆内存 for (int i 0; i _top; i) { _data[i] other._data[i]; // 拷贝数据深拷贝 } cout Stack(const Stack) copied endl; } // ----- 赋值运算符重载深拷贝 ----- Stack operator(const Stack other) { cout operator called endl; if (this ! other) // 检测自赋值 { // 释放旧资源 delete[] _data; // 分配新资源 _capacity other._capacity; _top other._top; _data new int[_capacity]; // 拷贝数据 for (int i 0; i _top; i) { _data[i] other._data[i]; } } return *this; } // ----- 析构函数 ----- // 释放堆内存 ~Stack() { cout ~Stack() destructor, capacity _capacity endl; delete[] _data; _data nullptr; } // ----- 入栈 ----- void Push(int val) { if (_top 1 _capacity) { // 容量不足扩容 Expand(); } _data[_top] val; } // ----- 出栈 ----- int Pop() { if (_top -1) { cout Stack is empty! endl; return -1; } return _data[_top--]; } void Print() const { cout Stack: ; for (int i 0; i _top; i) { cout _data[i] ; } cout endl; } private: void Expand() { int newCapacity _capacity * 2; int* newData new int[newCapacity]; for (int i 0; i _top; i) { newData[i] _data[i]; } delete[] _data; _data newData; _capacity newCapacity; cout Expanded to capacity _capacity endl; } }; int main() { Stack s1(5); s1.Push(1); s1.Push(2); s1.Push(3); s1.Print(); // 测试拷贝构造 Stack s2(s1); s2.Print(); // 测试赋值 Stack s3; s3 s1; s3.Print(); // s1, s2, s3 在 main 结束时自动析构 return 0; }5. 初始化列表 vs 赋值/* * 功能演示初始化列表和赋值的区别 * 初始化列表直接构造效率高 * 赋值先构造再赋值效率低 * 必须用初始化列表的场景 * 1. const 成员变量 * 2. 引用成员变量 * 3. 没有默认构造函数的类成员 */ #include iostream using namespace std; // ----- 辅助类没有默认构造函数 ----- class NoDefault { private: int _val; public: // 只有带参构造函数没有默认构造函数 NoDefault(int val) : _val(val) { cout NoDefault(int) constructed: _val endl; } void Print() const { cout NoDefault: _val endl; } }; // ----- 演示类 ----- class Test { private: // const 成员必须用初始化列表 const int _constVal; // 引用成员必须用初始化列表 int _refVal; // 没有默认构造函数的类成员必须用初始化列表 NoDefault _nd; int _normalVal; public: // ----- 正确使用初始化列表 ----- Test(int constVal, int refVal, int ndVal, int normalVal) : _constVal(constVal) // const 成员必须初始化 , _refVal(refVal) // 引用成员必须初始化 , _nd(ndVal) // 无默认构造的成员必须初始化 , _normalVal(normalVal) // 普通成员可以选择初始化或赋值 { // _constVal 10; // 错误const 成员不能在函数体内赋值 // _refVal refVal; // 错误引用成员不能在函数体内赋值 // _nd NoDefault(100); // 错误无默认构造的成员不能在函数体内初始化 // 普通成员可以赋值 _normalVal normalVal; cout Test constructed using initialization list endl; } void Print() const { cout _constVal: _constVal endl; cout _refVal: _refVal endl; _nd.Print(); cout _normalVal: _normalVal endl; } }; int main() { int x 100; Test t(10, x, 50, 30); t.Print(); return 0; }祝你在 C 的学习之路上越走越稳早日掌握更多编程技巧下次见 ✨

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价