当前位置: 首页 > news >正文

从零开始的C++学习生活 3:类和对象(中)

                                                     个人主页:Yupureki-CSDN博客

                                              C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

1. 类的默认成员函数

2. 构造函数

概念

实际用例

注意事项

3. 析构函数

概念

析构函数规则

实际用例

注意事项

3. 拷贝构造函数

概念

拷贝构造规则总结

4. 赋值运算符重载

运算符重载

赋值运算符重载

5. 日期类实现

获取某一年某一月的天数

比较运算符

日期加减法

6. 取地址运算符重载

const 成员函数

取地址运算符重载

7. 总结

默认成员函数使用场景

重要规则总结

最佳实践


前言

在上一篇《C++类和对象(上)》中,我们学习了类的基本概念、封装特性、this指针等基础知识,初步领略了面向对象编程的魅力。然而,这仅仅是打开了C++面向对象编程的大门。要真正掌握C++面向对象编程的精髓,我们必须深入理解类的默认成员函数这一核心机制。

想象一下这样的场景:你创建了一个对象,它如何被初始化?当对象生命周期结束时,如何确保资源被正确释放?当一个对象被复制给另一个对象时,会发生什么?这些看似简单的问题背后,隐藏着C++面向对象编程最深刻的设计哲学。

本篇《C++类和对象(中)》将带你深入探索:

  • 构造函数的多种形式及其初始化规则

  • 析构函数的资源管理智慧

  • 拷贝构造函数的深浅拷贝之谜

  • 运算符重载让自定义类型拥有内置类型般的表达能力

  • const成员函数的类型安全保障

这些默认成员函数不仅是语法规则,更是C++设计思想的体现。它们决定了对象的生命周期管理、资源安全、代码效率等关键问题。理解它们,就意味着你从"会写C++代码"迈向"懂C++面向对象设计"的重要一步。

1. 类的默认成员函数

在C++中,当我们定义一个类时,即使不显式编写某些成员函数,编译器也会自动生成6个默认成员函数。这些函数构成了类的核心功能:

  • 构造函数 - 对象初始化

  • 析构函数 - 对象清理

  • 拷贝构造函数 - 对象拷贝初始化

  • 赋值运算符重载 - 对象间赋值

  • 取地址运算符重载 - 普通对象取地址

  • const取地址运算符重载 - const对象取地址

C++11之后还增加了移动构造和移动赋值,这些我们后续再讨论。

2. 构造函数

概念

构造函数的核心任务是初始化对象,而不是创建对象(对象空间在实例化时已分配)。

其功能类似于我们之前所写的Init函数。我们初始化栈时,会利用malloc给data数组分配空间,再设置size和capacity的大小。构造函数也是如此

构造函数的特点:

1. 函数名与类名相同。

2. 无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

3. 对象实例化时系统会自动调用对应的构造函数。

4. 构造函数可以重载。

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显 式定义编译器将不再生成。

实际用例

#include <iostream>
using namespace std;typedef int STDataType;class Stack {
public:// 构造函数替代Init函数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 {
public:// 编译器自动生成构造函数,调用Stack的构造函数完成初始化
private:Stack _pushSt;Stack _popSt;
};int main() {MyQueue mq;  // 自动调用Stack构造函数初始化_pushSt和_popStreturn 0;
}

注意事项

1. 无参构造函数,全缺省函数和编译器自动创建的构造函数都为默认构造函数。这三种默认构造函数有且只能存在一种,不能同时存在。总结⼀下就是不传实参就可以调用的构造就叫默认构造。

#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();  // 输出:1/1/1d2.Print();  // 输出:2025/1/1return 0;
}

2. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。

3. 对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,

说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字自己定义的类型。

3. 析构函数

概念

析构函数的功能与构造函数相反,在对象销毁时自动调用,用于资源清理。

这个功能也类似于我们所写的StackDestory函数,在程序结束时,我们要释放掉向内存申请过空间的data数组。C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放⼯作。

析构函数规则

  1. 函数名:~类名

  2. 无参数无返回值

  3. 一个类只能有一个析构函数

  4. 对象生命周期结束时自动调用

  5. 内置类型成员不处理,自定义类型成员调用其析构函数

实际用例

 ~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}

注意事项

1. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器生成的默认析构函数,如Date;如 果默认生成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要 自己写析构,否则会造成资源泄漏,如Stack。

2. ⼀个局部域的多个对象,C++规定后定义的先析构。


3. 拷贝构造函数

概念

拷贝构造函数是一个特殊的构造函数。拷贝的构造函数是根据现有的一个类对象直接复制一个一模一样的类

例如已有了一个类为A 的A1的对象,利用拷贝构造函数可以在构造时直接以A1复制一个A2对象

拷贝构造的特点:

1. 拷贝构造函数是构造函数的⼀个重载。

2. 拷贝构造函数的第⼀个参数必须是类类型对象的引用,不然会直接报错。

3. C++规定⾃定义类型对象进⾏拷贝行为必须调⽤拷贝构造,所以这⾥⾃定义类型传值传参和传值返 回都会调⽤拷贝构造完成。

4. 若未显式定义拷贝构造,编译器会⽣成⾃动⽣成拷贝构造函数。⾃动⽣成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对⾃定义类型成员变量会调⽤他的拷贝构造

#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;}// 错误:传值会导致无限递归// Date(Date d) { ... }void Print() {cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};void Func1(Date d) {  // 传值调用拷贝构造cout << &d << endl;d.Print();
}Date& Func2() {  // 引用返回,避免拷贝static Date tmp(2024, 7, 5);return tmp;
}int main() {Date d1(2024, 7, 5);// 传值调用拷贝构造Func1(d1);// 拷贝构造的几种形式Date d2(d1);      // 直接初始化Date d3 = d1;     // 拷贝初始化d1.Print();d2.Print();d3.Print();return 0;
}

拷贝构造规则总结

  1. 参数必须是同类对象的引用

  2. 自定义类型传值传参和传值返回都会调用拷贝构造

  3. 编译器默认生成浅拷贝

  4. 有资源管理的类需要深拷贝

4. 赋值运算符重载

运算符重载

赋值运算符重载函数operator用于重新定义运算符号的使用方式,但是只能用于类对象。

// 成员函数重载==bool operator==(const Date& d) {return _year == d._year &&_month == d._month &&_day == d._day;}

特点

1. 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。

2. 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元 运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

3. 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个。

4. .* :: sizeof ?: . 注意以上5个运算符不能重载。

5. 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: operator+(int x, int y)

6. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。

// 前置++Date& operator++() {// 实现递增逻辑return *this;}// 后置++(用int参数区分)Date operator++(int) {Date tmp = *this;// 实现递增逻辑return tmp;}

赋值运算符重载

既然是赋值,那么就相当于拷贝,将已有的数据拷贝到一个类中。这个功能类似于拷贝构造函数

但注意的是,拷贝构造函数用于一个类的初始化,而赋值运算符重载相当于是二次拷贝,在初始化再进行拷贝

// 赋值运算符重载Date& operator=(const Date& d) {if (this != &d) {  // 避免自赋值_year = d._year;_month = d._month;_day = d._day;}return *this;  // 支持连续赋值}

5. 日期类实现

在手机上的日历中,我们能看见两个日期间差了多少天等等,现在我们要尝试实现一下

实现日期的加减,大小比较,两个日期间的差值

获取某一年某一月的天数

// 获取某年某月的天数int GetMonthDay(int year, int month) const {assert(month > 0 && month < 13);static int monthDayArray[13] = {0, 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 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;
}

日期加减法

// 日期加法
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;
}// 两个日期间的差值
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;
}

6. 取地址运算符重载

const 成员函数

我们将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。

const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

// const成员函数:不能修改成员变量void Print() const {// _year = 2025;  // 错误:不能修改成员cout << _year << "-" << _month << "-" << _day << endl;}// 非const成员函数void SetYear(int year) {_year = year;}

取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显示实现。

// 普通取地址重载Date* operator&() {return this;// return nullptr;  // 特殊场景:不想让别人获取真实地址}// const取地址重载const Date* operator&() const {return this;// return nullptr;}

7. 总结

默认成员函数使用场景

场景构造函数析构函数拷贝构造赋值重载
Date类需要不需要编译器生成编译器生成
Stack类需要需要需要深拷贝需要深拷贝
MyQueue类编译器生成编译器生成编译器生成编译器生成

重要规则总结

  1. 构造函数:对象创建时自动调用,完成初始化

  2. 析构函数:对象销毁时自动调用,完成清理

  3. 拷贝构造:用同类对象初始化新对象,参数必须是引用

  4. 赋值重载:两个已存在对象间的赋值,返回引用支持连续赋值

  5. const成员函数:保证不修改成员变量,const对象只能调用const函数

最佳实践

  1. 资源管理类必须实现析构、拷贝构造、赋值重载

  2. 简单数据类可使用编译器默认生成的函数

  3. 运算符重载要考虑使用习惯和效率

  4. const正确性是高质量代码的重要标志

通过深入理解这些默认成员函数,我们能够编写出更安全、更高效的C++代码,真正掌握面向对象编程的精髓。

http://www.dtcms.com/a/445722.html

相关文章:

  • 做网站的技术员包装设计概念
  • 【深度学习02】TensorBoard 基础与 torchvision 图像变换工具详解(附代码演示)
  • k8s中Pod和Node的故事(1):过滤、打分、亲和性和拓扑分布
  • springboot自助甜品网站的设计与实现(代码+数据库+LW)
  • 网站建设业动态wordpress出现404
  • Vue3组件通信8大方式详解
  • LeetCode 刷题【100. 相同的树、101. 对称二叉树、102. 二叉树的层序遍历】
  • Go基础:Go语言应用的各种部署
  • 团购网站 seo电商网站怎么做
  • 无Dockerfile构建:云原生部署新姿势
  • 深入解析 IDM 插件开发挑战赛:技术要点与实践指南
  • 颜群JVM【03】类的初始化
  • 达梦数据库常用初始化参数与客户端工具使用
  • 命令行安装 MySQL 8.4.6
  • 数据库--数据库约束和表的设计
  • [Windows] 磁盘映像管理工具:WimTool v1.7.2025.1001
  • 公司自己做网站晋城企业网站建设价格
  • 【SpringCloud(1)】初识微服务架构:创建一个简单的微服务;java与Spring与微服务;初入RestTemplate
  • leetcode 79 单词搜索
  • 站长之家官网查询电子商务网站建设与实践上机指导
  • 二叉树实战笔记:结构、遍历、接口与 OJ 实战
  • 哈尔滨php网站开发公司设置网站默认首页
  • 华为OD机试C卷 - 分披萨 - 贪心 DFS - (Java C++ JavaScript Python)
  • 仿照STM32 HAL库设计思想使用FreeRTOS实现异步非阻塞式设备驱动
  • 铜川做网站电话网页设计规范
  • ssc-FinLLM 金融大模型 相关链接
  • 二叉排序树(建树、查找、删除)
  • Linux学习笔记--i2cget 命令
  • 网站建设的标签指的是响应式网页设计图片
  • 用 CodeBuddy CLI + Prompt,从零到可运行:前后端混合管理系统的高效实战