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

个人主页:Yupureki-CSDN博客

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

目录

前言

[1. 类的默认成员函数](#1. 类的默认成员函数)

[2. 构造函数](#2. 构造函数)

概念

实际用例

注意事项

[3. 析构函数](#3. 析构函数)

概念

析构函数规则

实际用例

注意事项

[3. 拷贝构造函数](#3. 拷贝构造函数)

概念

拷贝构造规则总结

[4. 赋值运算符重载](#4. 赋值运算符重载)

运算符重载

赋值运算符重载

[5. 日期类实现](#5. 日期类实现)

获取某一年某一月的天数

比较运算符

日期加减法

[6. 取地址运算符重载](#6. 取地址运算符重载)

[const 成员函数](#const 成员函数)

取地址运算符重载

[7. 总结](#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++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显 式定义编译器将不再生成。

实际用例

cpp 复制代码
#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和_popSt
    return 0;
}

注意事项

  1. 无参构造函数,全缺省函数和编译器自动创建的构造函数都为**默认构造函数。**这三种默认构造函数有且只能存在一种,不能同时存在。总结⼀下就是不传实参就可以调用的构造就叫默认构造。
cpp 复制代码
#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/1
    d2.Print();  // 输出:2025/1/1
    
    return 0;
}
  1. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。

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

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

3. 析构函数

概念

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

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

析构函数规则

  1. 函数名:~类名

  2. 无参数无返回值

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

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

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

实际用例

cpp 复制代码
 ~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. 若未显式定义拷贝构造,编译器会⽣成⾃动⽣成拷贝构造函数。⾃动⽣成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对⾃定义类型成员变量会调⽤他的拷贝构造

cpp 复制代码
#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用于重新定义运算符号的使用方式,但是只能用于类对象。

cpp 复制代码
// 成员函数重载==
    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形参,跟前置++构成函数重载,⽅便区分。

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

赋值运算符重载

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

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

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

5. 日期类实现

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

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

获取某一年某一月的天数

cpp 复制代码
// 获取某年某月的天数
    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];
    }

比较运算符

cpp 复制代码
// 小于比较
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;
}

日期加减法

cpp 复制代码
// 日期加法
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指针,表明在该成员函数中不能对类的任何成员进行修改。

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

取地址运算符重载

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

cpp 复制代码
// 普通取地址重载
    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++代码,真正掌握面向对象编程的精髓。

相关推荐
十安_数学好题速析2 小时前
根式方程:结构联想巧用三角代换
笔记·学习·高考
励志不掉头发的内向程序员2 小时前
【Linux系列】并发世界的基石:透彻理解 Linux 进程 — 进程状态
linux·运维·服务器·开发语言·学习
知识分享小能手3 小时前
微信小程序入门学习教程,从入门到精通,WXSS样式处理语法基础(9)
前端·javascript·vscode·学习·微信小程序·小程序·vue
尘似鹤3 小时前
微信小程序学习(四)
学习·微信小程序
凤年徐4 小时前
【C++】string类
c语言·开发语言·c++
小龙报4 小时前
《KelpBar海带Linux智慧屏项目》
linux·c语言·vscode·单片机·物联网·ubuntu·学习方法
ajassi20004 小时前
开源 C++ QT QML 开发(五)复杂控件--Gridview
c++·qt·开源
能不能别报错4 小时前
K8s学习笔记(十三) StatefulSet
笔记·学习·kubernetes
zhangrelay5 小时前
蓝桥云课中支持的ROS1版本有哪些?-2025在线尝试ROS1全家福最方便的打开模式-
linux·笔记·学习·ubuntu