【c++面向对象编程】第8篇:const成员与mutable:常对象与常函数

目录

一、一个编译错误引发的思考

二、const成员函数:只读的承诺

语法

为什么要区分?

三、mutable:const函数里的"例外"

基本用法

什么时候用mutable?

mutable的误解

四、重载:const版本和非const版本

五、完整例子:学生成绩管理系统

六、三个常见误区

[1. 以为const成员函数不能调用非const函数(正确)](#1. 以为const成员函数不能调用非const函数(正确))

[2. 混淆const位置](#2. 混淆const位置)

[3. 滥用mutable](#3. 滥用mutable)

七、这一篇的收获


一、一个编译错误引发的思考

先看这段代码,猜猜哪里会报错:

cpp

复制代码
class Student {
private:
    string name;
    int score;
public:
    Student(string n, int s) : name(n), score(s) {}
    
    string getName() { return name; }      // 不是const函数
    int getScore() const { return score; } // const函数
    
    void setScore(int s) { score = s; }    // 修改成员
};

int main() {
    const Student s("张三", 85);   // 常对象
    
    cout << s.getName();    // 第1处:能编译吗?
    cout << s.getScore();   // 第2处:能编译吗?
    s.setScore(90);         // 第3处:能编译吗?
}

答案:

  • 第1处:❌ 错误。sconst对象,getName()不是const函数,可能修改对象

  • 第2处:✅ 正确。sconstgetScore()const,保证不修改

  • 第3处:❌ 错误。setScore明显要修改成员

规则很简单:常对象只能调用常函数。


二、const成员函数:只读的承诺

语法

cpp

复制代码
class Demo {
private:
    int x;
public:
    int getValue() const {   // const放在参数列表后面
        return x;            // ✅ 可以读
        // x = 100;          // ❌ 不能写
    }
};

这个const修饰的是谁?

答案是:它修饰的是this指针。回忆第6篇:

函数类型 this的类型
普通成员函数 Demo* const
const成员函数 const Demo* const

const成员函数里,this指向的是一个const对象,所以:

  • 不能修改任何成员变量

  • 只能调用其他const成员函数

为什么要区分?

cpp

复制代码
class Printer {
private:
    int printCount;
public:
    void print(const string& text) const {  // 不应该修改状态
        cout << text << endl;
        // printCount++;  // 想统计打印次数,但const阻止了
    }
};

有时候你想统计打印次数,但逻辑上print不应该改变对象(它只是输出)。这时候有两种选择:

  1. 去掉const------但这样常对象就不能调用print

  2. mutable------下节讲


三、mutable:const函数里的"例外"

mutable的意思是"可变的"。它告诉编译器:即使是在const函数里,这个成员变量也可以被修改

基本用法

cpp

复制代码
class Printer {
private:
    mutable int printCount;   // 加mutable
public:
    Printer() : printCount(0) {}
    
    void print(const string& text) const {
        cout << text << endl;
        printCount++;   // ✅ 可以!mutable成员在const函数里也能改
    }
    
    int getCount() const { return printCount; }
};

int main() {
    const Printer p;    // 常对象
    p.print("Hello");   // ✅ 可以调用const函数
    p.print("World");
    cout << p.getCount();  // 输出2
}

没有mutable的话,上面的代码无法编译。有了mutable,你可以在保持"逻辑上对象没变"的同时,修改一些"实现细节"。

什么时候用mutable?

典型场景:

1. 缓存计算结果

cpp

复制代码
class Matrix {
private:
    double data[100][100];
    mutable double cachedDeterminant;
    mutable bool determinantValid;
public:
    double determinant() const {
        if (!determinantValid) {
            // 计算行列式(很耗时)
            cachedDeterminant = compute();
            determinantValid = true;
        }
        return cachedDeterminant;
    }
};

determinant()逻辑上不应该修改矩阵,但为了提高性能,需要缓存计算结果。mutable完美解决。

2. 引用计数

cpp

复制代码
class SharedObject {
private:
    mutable int refCount;
public:
    SharedObject() : refCount(0) {}
    void addRef() const { refCount++; }   // const函数但需要修改计数
    void release() const { refCount--; }
};

3. 互斥锁(线程安全)

cpp

复制代码
class ThreadSafeCache {
private:
    mutable mutex mtx;   // 锁需要被修改
    string data;
public:
    string get() const {
        lock_guard<mutex> lock(mtx);  // 锁住mtx会修改它
        return data;
    }
};

mutable的误解

mutable不是让你随意破坏const语义。它应该只用于那些不影响对象"外部可见状态"的内部细节

如果修改mutable成员后,对象的逻辑行为发生了变化,那用mutable就是错的。


四、重载:const版本和非const版本

一个类可以同时提供const和非const版本的同一个函数,编译器会根据对象是否是const来选择。

cpp

复制代码
class Array {
private:
    int data[10];
public:
    // 非const版本:返回引用,允许修改
    int& operator[](int index) {
        cout << "non-const version" << endl;
        return data[index];
    }
    
    // const版本:返回const引用,只读
    const int& operator[](int index) const {
        cout << "const version" << endl;
        return data[index];
    }
};

int main() {
    Array arr;
    arr[0] = 100;           // 调用non-const版本
    
    const Array& ref = arr;
    cout << ref[0];         // 调用const版本
}

这是标准库中的常见设计模式(比如vector::operator[])。


五、完整例子:学生成绩管理系统

把前面的知识点串起来:

cpp

复制代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Student {
private:
    string name;
    int score;
    mutable int accessCount;      // 记录getScore被调用了多少次(const里也能改)
    mutable int modifyCount;      // 记录setScore被调用了多少次
    
public:
    Student(string n, int s) : name(n), score(s), accessCount(0), modifyCount(0) {}
    
    // const函数:读取成绩,同时记录访问次数
    int getScore() const {
        accessCount++;    // mutable成员,允许修改
        return score;
    }
    
    // 非const函数:修改成绩,记录修改次数
    void setScore(int s) {
        score = s;
        modifyCount++;
    }
    
    string getName() const {
        return name;
    }
    
    // const函数里不能修改name/score,但可以读
    void print() const {
        cout << name << ": " << score << "分 (被查询" << accessCount 
             << "次,被修改" << modifyCount << "次)" << endl;
    }
    
    // 获取统计信息(const函数,只读访问)
    void printStats() const {
        cout << "统计: " << name << " 的成绩被查询" << accessCount 
             << "次,被修改" << modifyCount << "次" << endl;
    }
};

class ClassRoom {
private:
    vector<Student> students;
    mutable int totalQueryCount;   // 记录全班查询次数
    
public:
    ClassRoom() : totalQueryCount(0) {}
    
    void addStudent(const Student& s) {
        students.push_back(s);
    }
    
    // const函数:查询全班平均分,同时记录查询次数
    double getAverage() const {
        if (students.empty()) return 0;
        
        totalQueryCount++;   // mutable,可以修改
        
        int sum = 0;
        for (const auto& s : students) {
            sum += s.getScore();   // 调用const版本的getScore
        }
        return static_cast<double>(sum) / students.size();
    }
    
    void printClassInfo() const {
        cout << "全班平均分查询次数:" << totalQueryCount << endl;
    }
    
    // 非const函数:修改学生成绩
    void boostScore(int delta) {
        for (auto& s : students) {
            s.setScore(s.getScore() + delta);
        }
    }
};

int main() {
    cout << "=== 演示const对象和mutable ===" << endl;
    
    const Student s1("张三", 85);   // 常对象
    cout << s1.getName() << "的成绩: " << s1.getScore() << endl;  // ✅ getScore是const
    // s1.setScore(90);  // ❌ 编译错误,setScore不是const
    s1.print();         // ✅ print是const
    s1.printStats();    // ✅ const函数,显示mutable计数
    
    cout << "\n=== 演示const重载 ===" << endl;
    ClassRoom classroom;
    classroom.addStudent(Student("李四", 90));
    classroom.addStudent(Student("王五", 75));
    
    const ClassRoom& constClassroom = classroom;
    cout << "平均分: " << constClassroom.getAverage() << endl;  // const版本
    constClassroom.printClassInfo();
    
    // classroom.boostScore(5);  // 这行会修改对象,但const引用不能调
    classroom.boostScore(5);      // 通过原对象可以调非const函数
    
    cout << "加分后平均分: " << constClassroom.getAverage() << endl;
    constClassroom.printClassInfo();
    
    return 0;
}

输出:

text

复制代码
=== 演示const对象和mutable ===
张三的成绩: 85
张三: 85分 (被查询1次,被修改0次)
统计: 张三 的成绩被查询1次,被修改0次

=== 演示const重载 ===
平均分: 82.5
全班平均分查询次数:1
加分后平均分: 87.5
全班平均分查询次数:2

六、三个常见误区

1. 以为const成员函数不能调用非const函数(正确)

cpp

复制代码
class Demo {
    void normal() {}
    void constFunc() const {
        normal();   // ❌ 错误,normal不是const
    }
};

2. 混淆const位置

cpp

复制代码
int getValue() const;   // ✅ const成员函数
const int getValue();   // ❌ 返回const int,不是const成员函数

两个const位置不同,含义完全不同。

3. 滥用mutable

cpp

复制代码
class Bad {
private:
    mutable int importantData;   // 不应该随便设mutable
public:
    void changeImportantData() const {
        importantData = 100;     // 逻辑上这改变了对象的重要状态
    }
};

如果修改mutable成员后,obj1 == obj2的判断会改变,那这个成员就不应该是mutable


七、这一篇的收获

你现在应该明白:

  • 常对象const T obj)只能调用常函数const成员函数)

  • 常函数承诺不修改对象,thisconst T* const

  • **mutable**成员可以在常函数中被修改,用于缓存、计数、锁等内部状态

  • 可以重载const和非const版本的函数,编译器根据对象类型选择

💡 小作业:写一个Timer类,记录代码执行时间。要求:

  • start()stop()是非const函数

  • getElapsed() const是const函数,但内部需要修改缓存的时间值(用mutable

  • 测试常对象能否正常工作


下一篇预告 :第9篇《友元(friend):破坏封装的"特权"------真的有害吗?》------friend给了外部函数或类访问private成员的权限,就像给了你家钥匙。什么时候该用,什么时候是设计臭味?下一篇分析。

相关推荐
RPGMZ3 小时前
RPGMZ游戏引擎 一个窗口 文本居中显示
开发语言·javascript·游戏引擎·rpgmz
草莓熊Lotso3 小时前
【Linux网络】UDP Socket 编程全解析:从回显服务到通用字典服务,从零实现工业级代码
linux·运维·服务器·数据库·c++·单片机·udp
飞鸿踏雪(蓝屏选手)9 小时前
137 ≤ Chrome 主密钥获取研究
c++·chrome·windows·网络安全·逆向分析
爱滑雪的码农9 小时前
详细说说React大型项目结构以及日常开发核心语法
前端·javascript·react.js
@大迁世界10 小时前
43.HTML 事件处理和 React 事件处理有什么区别?
前端·javascript·react.js·html·ecmascript
代钦塔拉11 小时前
Qt4 vs Qt5 带参数信号槽的连接方式详解
开发语言·数据库·qt
ZC跨境爬虫11 小时前
跟着 MDN 学 HTML day_38:(DocumentFragment 文档片段接口详解)
前端·javascript·ui·html·音视频
@大迁世界12 小时前
41.ShadCN 是什么?它如何和 Tailwind CSS 集成,从而更容易构建可访问且可自定义的 React 组件?
前端·javascript·css·react.js·前端框架
InfinteJustice13 小时前
踩坑分享C 语言文件操作全攻略:从基础读写到随机访问与缓冲区原理
c语言·开发语言·microsoft