前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【C++】Chapter01 类与对象

【C++】Chapter01 类与对象

作者头像
Skrrapper
发布2025-03-25 13:29:27
发布2025-03-25 13:29:27
5700
代码可运行
举报
文章被收录于专栏:技术分享技术分享
运行总次数:0
代码可运行

面向过程:关注事件的逻辑性流程

面向对象:关注事件出现的对象

image-20250208103902925
image-20250208103902925

面向对象更高级于面向过程

面向对象的三大特性:封装、继承、多态

类的定义

代码语言:javascript
代码运行次数:0
运行
复制
class className
{
// 类体:由成员函数和成员变量组成
};  // 一定要注意后面的分号
  • 类体中内容称为类的成员
    • 类中的变量称为类的属性或成员变量
    • 类中的函数称为类的方法或者成员函数

    注:计算对象的大小只计算成员变量大小总和,而不计算函数。

  • 类的两种定义方式:
    • 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成inline内联函数处理。
    image-20250208111224832
    image-20250208111224832
    • 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名 ::(声明定义分离)
    image-20250208111255296
    image-20250208111255296

类的访问限定符

image-20250208105742174
image-20250208105742174
  1. public修饰的成员在类外可以直接被访问(公开)
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)(私密)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. 如果后面没有访问限定符,作用域就到 } 即类结束。
  5. class的默认访问权限为privatestructpublic(因为struct要兼容C)

封装

封装的定义:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。

对于stack的实现C语言与C++的区别:

image-20250208151539867
image-20250208151539867

从底层来说没有区别。

image-20250208151343134
image-20250208151343134

封装的实现:通过访问限定符的变换,将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

类的实例化

在类中,成员变量仅仅是被声明而没有被定义;

当我们在其他地方使用这个变量的时候,才会对其进行定义(也就是实例化

tips:从这里也可以得知声明和定义的区别:声明仅仅是一个抽象化的规定;而定义才是将其实例化,将其变成实际的东西

image-20250208115514270
image-20250208115514270

类对象的存储

内存对齐规则:牺牲空间提高性能

  • 结构体的起始地址必须是其最大成员对齐数的整数倍。
  • 结构体的总大小必须是最大对齐数的整数倍。如果结构体中嵌套了其他结构体,嵌套的结构体也要对齐到自己的最大对齐数的整数倍处。
代码语言:javascript
代码运行次数:0
运行
复制
// 类中既有成员变量,又有成员函数 占成员变量的字节和,并且遵循内存对齐规则
class A1 {
public:
    void f1(){}
private:
    int _a;
};
// 类中仅有成员函数              占一个字节,表示成员函数
class A2 {
public:
   void f2() {}
};
// 类中什么都没有---空类         占一个字节,用于占位
class A3
{};

this指针

调用同一个函数,但是结果不一样——使用了隐含的this指针。

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数this,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。

只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。

this指针的特性

  • this指针类型:类类型* const ,所以成员函数无法给this指针赋值
  • 只能在成员函数的内部使用
  • 本质上就是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针
  • this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
image-20250208151047744
image-20250208151047744

构造函数和析构函数

  • 问题: 初始化和销毁经常忘记 有些地方写起来很繁琐
  • 解决方法:
  • 构造函数析构函数,属于默认成员函数(我们不写,编译器也会自动生成。)

构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,完成对象的初始化。并且在对象整个生命周期内只调用一次。注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。

构造函数的特点
  • 与类名相同,没有返回值(甚至不能写 void)。
  • 在对象创建时自动调用,不需要手动调用。
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。----------------------------也就是说我们不主动去写,构造函数就是自动生成。
  • 可以重载(多个构造函数参数不同)。
  • 默认构造函数、拷贝构造函数、移动构造函数、委托构造函数等
  • 对于内置类型不做处理;对于自定义类型会去调用它的默认构造。
构造函数的基本用法

1 默认构造函数(无参数)

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
using namespace std;

class Person {
public:
    string name;

    // 默认构造函数(无参数)
    Person() {
        cout << "默认构造函数被调用!" << endl;
        name = "Unknown";
    }

    void show() { cout << "Name: " << name << endl; }
};

int main() {
    Person p;  // 自动调用构造函数
    p.show();
    return 0;
}

输出

代码语言:javascript
代码运行次数:0
运行
复制
默认构造函数被调用!//并未显式调用,但是却依旧打印------说明自动调用
Name: Unknown

📌 特点

  • 无参数,在对象创建时自动调用。
  • 如果不提供,编译器会自动生成一个默认构造函数

2.带参数的构造函数

代码语言:javascript
代码运行次数:0
运行
复制
class Person {
public:
    string name;
    int age;

    // 带参数的构造函数
    Person(string n, int a) {
        name = n;
        age = a;
    }

    void show() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Person p("Alice", 25);  // 传入参数
    p.show();
    return 0;
}

📌 特点

  • 允许传递参数,使对象初始化更灵活。
  • 不会自动生成默认构造函数(如果定义了带参数构造函数,必须手动定义无参构造函数,否则 Person p; 将报错)。
image-20250217205727302
image-20250217205727302
image-20250217210100876
image-20250217210100876
image-20250217210638218
image-20250217210638218

构造函数相当于: Init()

构造函数的调用和普通函数也不一样。

通常构造函数都需要自己写

image-20250217210928520
image-20250217210928520
拷贝构造函数
image-20250218204226885
image-20250218204226885
1.拷贝构造函数简介

是 C++ 中用于创建一个新对象,并用已有对象进行初始化的特殊构造函数。

特点

  • 形式:ClassName(const ClassName& other);
  • 参数必须是 const &(该形参是对本类类型对象的引用),否则会导致无限递归调用
image-20250218205003104
image-20250218205003104
  • 作用:实现对象的复制,如果类中包含指针成员,则需深拷贝以避免内存泄漏。
  • 拷贝构造函数实际上也是构造函数的一个重载形式。

2. 拷贝构造函数的调用时机

C++ 在以下情况会自动调用拷贝构造函数

对象初始化

代码语言:javascript
代码运行次数:0
运行
复制
ClassName obj1;
ClassName obj2 = obj1;  // 调用拷贝构造函数

对象按值传递

代码语言:javascript
代码运行次数:0
运行
复制
void func(ClassName obj);  // 传递参数时调用拷贝构造

对象按值返回

代码语言:javascript
代码运行次数:0
运行
复制
ClassName func() { return obj; }  // 返回对象时调用拷贝构造

用已有对象创建新对象

代码语言:javascript
代码运行次数:0
运行
复制
ClassName obj1;
ClassName obj2(obj1);  // 调用拷贝构造函数

3. 默认拷贝构造函数(浅拷贝)

C++ 如果不定义拷贝构造函数,编译器会自动生成一个默认版本,进行浅拷贝(Shallow Copy)

3.1 默认拷贝构造(编译器自动生成)

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    Person(string n, int a) : name(n), age(a) {} // 普通构造

    void show() { cout << "Name: " << name << ", Age: " << age << endl; }
};

int main() {
    Person p1("Alice", 25);
    Person p2 = p1;  // 触发默认拷贝构造
    p2.show();  // Name: Alice, Age: 25
}

📌 特点

  • 默认拷贝构造执行成员变量的逐个复制(= 赋值)。
  • 适用于没有指针成员的类

3.2 浅拷贝的缺陷

当类包含指针成员时,默认拷贝构造会复制指针地址,而不是复制指针指向的内容,导致多个对象共享同一块内存析构时会二次释放,导致程序崩溃

代码语言:javascript
代码运行次数:0
运行
复制
class Person {
public:
    char* name;

    Person(const char* n) {
        name = new char[strlen(n) + 1];  // 动态分配内存
        strcpy(name, n);
    }

    ~Person() { delete[] name; }  // 释放内存
};

int main() {
    Person p1("Alice");
    Person p2 = p1;  // ❌ 默认拷贝构造,导致 p1 和 p2 指向同一块内存
    return 0;
}  // ❌ p1 和 p2 都调用析构函数,导致 double free 错误

📌 问题

  • p1 和 p2 共享相同的 name 内存,当 p1p2 析构时,会两次释放相同的内存,引发 “double free or corruption” 错误。

解决方案:手动实现深拷贝!


4. 深拷贝(自定义拷贝构造函数)

深拷贝(Deep Copy)重新分配内存,并复制指针指向的内容,保证每个对象都有自己独立的资源。

如果类涉及动态分配内存,建议实现深拷贝,以确保内存安全!

4.1 实现深拷贝

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <cstring>
using namespace std;

class Person {
public:
    char* name;

    // 构造函数
    Person(const char* n) {
        name = new char[strlen(n) + 1];  // 分配新内存
        strcpy(name, n);
    }

    // 自定义拷贝构造(深拷贝)
    Person(const Person& p) {
        cout << "拷贝构造函数被调用!" << endl;
        name = new char[strlen(p.name) + 1];  // 重新分配内存
        strcpy(name, p.name);
    }

    // 析构函数
    ~Person() { delete[] name; }

    void show() { cout << "Name: " << name << endl; }
};

int main() {
    Person p1("Alice");
    Person p2 = p1;  // 触发拷贝构造(深拷贝)
    p2.show();
}

📌 特点

  • name = new char[strlen(p.name) + 1]; 重新分配内存,避免共享同一块地址。
  • 每个对象都有自己的独立资源,防止 double free 错误。

5. 禁用拷贝构造

在某些情况下,我们不希望对象被拷贝,可以显式删除拷贝构造函数(C++11)。

代码语言:javascript
代码运行次数:0
运行
复制
class Person {
public:
    Person(const Person&) = delete;  // 禁止拷贝
};

📌 作用

  • 防止对象被复制
  • 适用于管理资源的类(如单例模式)

6. 拷贝构造 vs 赋值运算符

拷贝构造和赋值运算符的区别:

对比项

拷贝构造函数

赋值运算符 =

作用

创建新对象并初始化

复制已有对象的值

触发时机

ClassName obj2 = obj1;

obj2 = obj1;(对象已存在)

默认行为

浅拷贝

浅拷贝

适用场景

对象初始化

对象已存在,需赋新值

示例

代码语言:javascript
代码运行次数:0
运行
复制
class Person {
public:
    string name;

    // 拷贝构造函数
    Person(const Person& p) { name = p.name; }

    // 赋值运算符
    Person& operator=(const Person& p) {
        if (this == &p) return *this;  // 避免自赋值
        name = p.name;
        return *this;
    }
};

int main() {
    Person p1("Alice");
    Person p2 = p1;  // 调用拷贝构造
    p2 = p1;         // 调用赋值运算符
}

7. C++11 移动构造函数

C++11 引入移动构造函数,可以避免拷贝,提高性能。

代码语言:javascript
代码运行次数:0
运行
复制
class Person {
public:
    string name;

    // 移动构造函数
    Person(Person&& p) noexcept : name(move(p.name)) {
        cout << "移动构造函数被调用!" << endl;
    }
};

📌 区别

  • 拷贝构造Person(const Person&) 复制数据。
  • 移动构造Person(Person&&) 直接转移资源,避免不必要的拷贝

析构函数

构造函数的反操作,在对象销毁时自动调用。完成资源清理工作(功能与构造函数相反)。

格式:~类名(),无参数、无返回值

若未显式定义,系统会自动生成默认的析构函数

主要用于释放动态分配的资源(如 new)。在函数生命周期结束时,析构函数自动调用

析构函数无重载,所以每个类只有一个析构函数。

代码语言:javascript
代码运行次数:0
运行
复制
class Person {
public:
    Person() { cout << "构造函数" << endl; }
    ~Person() { cout << "析构函数" << endl; }
};

int main() {
    Person p;  // 作用域结束,调用析构函数
}

相当于:

image-20250217213628548
image-20250217213628548

注意:如果类中没有动态申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

构造函数和析构函数用于简化代码;同时这两个函数的出现使得代码更具有安全性和便携性。

默认成员函数

用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数

image-20250314122431061
image-20250314122431061

#mermaid-svg-lYRtb51Htzp7Khfa {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-lYRtb51Htzp7Khfa .error-icon{fill:#552222;}#mermaid-svg-lYRtb51Htzp7Khfa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lYRtb51Htzp7Khfa .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-lYRtb51Htzp7Khfa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lYRtb51Htzp7Khfa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lYRtb51Htzp7Khfa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lYRtb51Htzp7Khfa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lYRtb51Htzp7Khfa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lYRtb51Htzp7Khfa .marker.cross{stroke:#333333;}#mermaid-svg-lYRtb51Htzp7Khfa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lYRtb51Htzp7Khfa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lYRtb51Htzp7Khfa .cluster-label text{fill:#333;}#mermaid-svg-lYRtb51Htzp7Khfa .cluster-label span{color:#333;}#mermaid-svg-lYRtb51Htzp7Khfa .label text,#mermaid-svg-lYRtb51Htzp7Khfa span{fill:#333;color:#333;}#mermaid-svg-lYRtb51Htzp7Khfa .node rect,#mermaid-svg-lYRtb51Htzp7Khfa .node circle,#mermaid-svg-lYRtb51Htzp7Khfa .node ellipse,#mermaid-svg-lYRtb51Htzp7Khfa .node polygon,#mermaid-svg-lYRtb51Htzp7Khfa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lYRtb51Htzp7Khfa .node .label{text-align:center;}#mermaid-svg-lYRtb51Htzp7Khfa .node.clickable{cursor:pointer;}#mermaid-svg-lYRtb51Htzp7Khfa .arrowheadPath{fill:#333333;}#mermaid-svg-lYRtb51Htzp7Khfa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lYRtb51Htzp7Khfa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lYRtb51Htzp7Khfa .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-lYRtb51Htzp7Khfa .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-lYRtb51Htzp7Khfa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lYRtb51Htzp7Khfa .cluster text{fill:#333;}#mermaid-svg-lYRtb51Htzp7Khfa .cluster span{color:#333;}#mermaid-svg-lYRtb51Htzp7Khfa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lYRtb51Htzp7Khfa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

参数不足

参数足够

读取失败

读取成功

创建失败

创建成功

分配失败

分配成功

开始

检查命令行参数

提示用户并退出

读取TXT文件

提示错误并退出

转换TXT数据

创建BMP文件

提示错误并退出

写入文件头

写入信息头

分配内存

提示错误并退出

设置图像数据

写入图像数据

释放内存

关闭文件

提示生成成功

结束

赋值运算符重载

C++为了增强代码的可读性引入运算符重载。在介绍赋值运算符重载之前我们先来介绍运算符重载。

运算符重载

普通的运算符是类似于“=”、“+”等带有运算含义的符号,运算符重载就是具有特殊函数名的函数,这些函数名都会接需要重载的运算符。

格式如下:

代码语言:javascript
代码运行次数:0
运行
复制
返回值类型 operator操作符(参数列表)

举个例子更好理解:

代码语言:javascript
代码运行次数:0
运行
复制
bool operator==(const Date& d1, const Date& d2)

重载了“==”这个运算符,使它带有额外的含义。

  • 注意1:当我们在使用重载运算符的时候,不能改变内置类型的运算符的含义。 比如对于+这个运算符,它依旧起着类似于1+1=2的作用,这是不会改变的。 这其实也体现了重载的意义:不改变原先的用法,而是重新新增一种用法。
  • 注意2:不能通过连接其他不是运算符的符号来创建新的操作符,例如operator@,这是不被允许的。
  • 注意3:有五种运算符不能被重载:.* :: sizeof?: .
  • 注意4:我们发现作为类中的成员函数进行重载时,其形参看起来比操作数数目要少1,实际上是因为成员函数的第一个参数时隐藏的this
举例1:Date类
代码语言:javascript
代码运行次数:0
运行
复制
// 全局的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);
 }
举例2:前置++和后置++

(1)前置 ++

  • 直接对当前对象 *this 进行修改
  • 返回 自身引用ClassName&),支持链式调用
代码语言:javascript
代码运行次数:0
运行
复制
cpp复制编辑class Counter {
private:
    int value;
public:
    Counter(int v) : value(v) {}

    // 前置++ 重载
    Counter& operator++() {
        ++value;  // 直接修改当前对象
        return *this;  // 返回自身引用
    }

    void show() const {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    Counter c(5);
    ++c;   // 调用前置++
    c.show();  // Value: 6
}

(2)后置 ++

  • 需要一个 int 作为参数(用于区分前置 ++
  • 先保存原对象的副本
  • 让当前对象 +1
  • 返回 旧对象的副本
代码语言:javascript
代码运行次数:0
运行
复制
cpp复制编辑class Counter {
private:
    int value;
public:
    Counter(int v) : value(v) {}

    // 后置++ 重载
    Counter operator++(int) {
        Counter temp = *this;  // 先保存旧值
        ++value;  // 然后自增
        return temp;  // 返回旧对象
    }

    void show() const {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    Counter c(5);
    Counter d = c++;  // 先返回旧对象,再自增

    c.show();  // Value: 6
    d.show();  // Value: 5  (因为 d 赋值的是旧对象)
}

所以只需要记住一句话:前置++无需额外的参数,而后置++需要一个int参数来用于与前置++区分。

赋值运算符重载

赋值运算符重载 (operator=) 是 特殊的运算符重载

我们知道,在C++中,=这个运算符的含义是赋值,所以这也是它名字的由来。

那么对于赋值运算符的重载,会有哪些好处呢?

默认情况下,编译器会为类提供一个 默认的赋值运算符=,用于进行对象的浅拷贝(即逐成员赋值)

但是在某些情况下(如涉及动态内存分配),需要 重载赋值运算符 以进行深拷贝,避免浅拷贝带来的问题(如双重释放)。

赋值运算符格式如下:

代码语言:javascript
代码运行次数:0
运行
复制
class ClassName {
public:
    ClassName& operator=(const ClassName& other) {
        if (this == &other) {
            return *this;  // 避免自赋值
        }
        // 清理已有资源(如果有)
        
        // 复制 `other` 的资源
        
        return *this;  // 返回自身引用
    }
};
赋值运算符重载的关键点

参数类型const ClassName&,避免不必要的拷贝,传递引用可以提高传参效率

返回类型:返回 ClassName&,以支持连续赋值,如 a = b = c;

检查自赋值:避免 obj = obj; 这种情况导致资源被错误释放

释放旧资源(如果涉及动态内存)

深拷贝(如果类涉及指针资源)

赋值运算符只能重载成类的成员函数,而不能重载成全员函数。

对于这一点,我们看一个例子:

代码语言:javascript
代码运行次数:0
运行
复制
class Date
 {
 public:
 	Date(int year = 1900, int month = 1, int day = 1)
 	{
 	_year = year;
 	_month = month;
 	_day = day;
 	}
 int _year;
 int _month;
 int _day;
 };
 
 // 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
 {
 	if (&left != &right)
 	{
 	left._year = right._year;
 	left._month = right._month;
 	left._day = right._day;
 	}
 	return left;
}
 // 编译失败:
// error C2801: “operator =”必须是非静态成员

原因就是用户在类外实现的一个全局的赋值运算符重载,会和类中的默认的赋值运算符冲突

C++ const 成员(常成员)

在 C++ 中,const 关键字可以用于类的 数据成员成员函数对象,主要目的是防止修改、提高代码安全性


1. const 数据成员(常数据成员)

  • const 数据成员在对象构造时必须初始化,之后不能修改。
  • 只能使用 初始化列表 进行赋值,不能在构造函数内部赋值。
示例:常数据成员
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class Test {
private:
    const int x;  // 常数据成员
public:
    // 必须在初始化列表中赋值
    Test(int val) : x(val) {}

    void show() {
        std::cout << "x = " << x << std::endl;
    }
};

int main() {
    Test t(10);
    t.show();  // x = 10

    // t.x = 20;  // ❌ 错误:const 成员不能修改
}

📌 关键点

  • const int x; 不能在构造函数内赋值,只能在 初始化列表 中(也就是例子中的Test t(10))赋值。
  • 常数据成员在对象 生命周期内保持不变

2. const 成员函数(常成员函数)

const 成员函数 不能修改对象的成员变量(除了 mutable 修饰的变量)。

语法:在函数声明和定义后加 const关键字:

代码语言:javascript
代码运行次数:0
运行
复制
返回类型 函数名(参数) const;

常成员函数 只能调用其他的 const 成员函数不能调用非 const 的成员函数

示例:常成员函数
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class Test {
private:
    int value;
public:
    Test(int v) : value(v) {}

    // const 成员函数
    void show() const {
        std::cout << "Value: " << value << std::endl;
    }

    // 非 const 成员函数(可以修改成员变量)
    void setValue(int v) {
        value = v;
    }
};

int main() {
    const Test t(100);  // 常对象
    t.show();   // ✅ OK: show() 是 const 成员函数
    // t.setValue(200);  // ❌ 错误: 不能调用非 const 成员函数
}

📌 关键点

  • void show() const; 保证函数不会修改成员变量
  • 常对象 const Test t(100); 只能调用 const 成员函数,不能调用 setValue()

3. const 对象(常对象)

  • const 对象只能调用 const 成员函数,不能调用非 const 的成员函数。
  • 不能修改 const 对象的成员变量。
示例:const 对象
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class Test {
private:
    int data;
public:
    Test(int v) : data(v) {}

    void show() const {
        std::cout << "Data: " << data << std::endl;
    }

    void setData(int v) {
        data = v;
    }
};

int main() {
    const Test t(42);  // 常对象

    t.show();  // ✅ OK:可以调用 const 成员函数
    // t.setData(100);  // ❌ 错误:不能调用非 const 成员函数
}

📌 关键点

  • const Test t(42); 不能调用 setData(),但可以调用 show()
  • const 对象 只能调用 const 成员函数

4. mutable 关键字

  • mutable 允许 const 成员函数 修改特定成员变量
  • 适用于 缓存、统计信息、日志 等场景。
示例:mutable 可变成员
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class Logger {
private:
    mutable int accessCount;  // 可变成员变量
public:
    Logger() : accessCount(0) {}

    void log() const {
        accessCount++;  // ✅ 在 const 成员函数中修改
        std::cout << "Access count: " << accessCount << std::endl;
    }
};

int main() {
    const Logger logger;  // 常对象
    logger.log();  // ✅ 即使是 const 对象,也能修改 mutable 变量
}

📌 关键点

  • mutable int accessCount; 允许在 const 成员函数中修改
  • 适用于日志计数、缓存变量不会影响对象核心数据的场景。

总的来说, const 提高了安全性,防止意外修改数据,是 C++ 代码的好习惯。

初始化列表

初始化列表是构造函数的一种特殊形式,它用于在构造函数体之前初始化成员变量。

我们需要注意,在普通的构造函数体赋值中,我们不过是给了成员变量一个合适的初始值,而并非叫做初始化。

因为初始化只能初始化一次。而赋值可以多次赋值。

为什么要有初始化列表?

在某些应用场景下,额外的赋值会降低调用的效率,那么我们直接将变量初始化,就可以避免不必要的默认构造和赋值,从而提高效率。

那么我们就需要一种方式来进行真正的初始化,初始化列表就出现了。

初始化列表不会先调用默认构造函数再赋值,直接初始化,提高性能。

格式与应用

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

如下:

代码语言:javascript
代码运行次数:0
运行
复制
ClassName(参数列表): 成员1(初始化值),成员2(初始化值),...{...}

举例:

代码语言:javascript
代码运行次数:0
运行
复制
class Date
{
public:
	Date(int year,int month,int day):
	: _year(year)
    , _month(month)
    , _day(day) 
    {}
    
private:
    int _year;
    int _month;
    int _day;
};

注意:

每个成员变量在初始化列表中只能出现一次,这也刚好对应了初始化只能初始化一次;

以下成员必须要在初始化列表进行初始化:

1.引用成员变量

2.const成员变量

3.自定义类型成员(且该类没有默认构造函数)

然而,实际上除了以上成员,我们应该尽可能使用初始化列表,对于任何成员。

初始化列表的执行顺序按照成员变量的声明顺序,而不是初始化列表中的书写顺序。

看一下例子:

代码语言:javascript
代码运行次数:0
运行
复制
class Test {
private:
    int a;
    int b;
public:
    Test() : b(20), a(10) {  // a 先声明,但在初始化列表中书写在b后面
        cout << "a: " << a << ", b: " << b << endl;
    }
};

打印出来是:

代码语言:javascript
代码运行次数:0
运行
复制
a: 0, b: 20

因为成员变量的初始化顺序是按照声明顺序a先声明,所以a先初始化),而不是初始化列表的顺序。

下面对三种情况进行分别举例。

const成员变量

代码语言:javascript
代码运行次数:0
运行
复制
class Example {
private:
	const int value;
public:
	Example(int v): value(v) {}
};

引用成员变量:引用成员变量不能再构造函数体内赋值

代码语言:javascript
代码运行次数:0
运行
复制
class Example{
private:
	int &ref;
public:
	Example(int &r): ref(r) {}
};

自定义成员变量(且该类没有默认构造函数)

代码语言:javascript
代码运行次数:0
运行
复制
class MyClass {
public:
    // 只有带参数的构造函数,没有默认构造函数(即没有 MyClass())
    MyClass(int value) { 
        // 构造函数逻辑
    }
};

class Container{
private:
	MyClass obj; //成员变量,必须初始化
public:
	Container(int v): obj(v) {}
};

Explicit关键字

构造函数不仅可以构造与初始化一个对象,

对于接受单个参数的构造函数,还可以起到类型转换的作用。

先理解接受单个参数的构造函数的概念,它有三种情况:

  • 构造函数只有一个参数
  • 构造函数有多个参数,但只有第一个参数没有默认值,其他参数都由默认值(也就是只需要接受这个没有默认值的参数)
  • 全缺省构造函数

然后对于其类型转换的作用,这里举一个例子,来看看使用不当可能导致什么结果

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
using namespace std;

class Example {
public:
    Example(int value) { cout << "Constructor called with value: " << value << endl; }
};

int main() {
    Example ex1 = 10;  // ❶ 允许隐式转换
    func(20);          // ❷ 允许隐式转换,将 20 变为 Example 类型

    return 0;
}

输出结果:

代码语言:javascript
代码运行次数:0
运行
复制
Constructor called with value: 10 //触发隐式转换,使得 10 变成了Example(10)
Constructor called with value: 20 //func(20); 本意是传递一个 Example 类型对象,但 20 被隐式转换成了 Example(20),导致意外的构造函数调用,可能引起潜在的逻辑错误。

那么如果我们加上Explicit关键字,就可以规避这种隐式转换。

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
using namespace std;

class Example {
public:
    explicit Example(int value) { cout << "Constructor called with value: " << value << endl; }
};

void func(Example ex) { cout << "Function executed!" << endl; }

int main() {
    Example ex1 = 10;  // ❌ 错误:explicit 禁止隐式转换
    Example ex2(10);   // ✅ 正确:直接初始化
    func(20);          // ❌ 错误:不能隐式转换 int → Example
    func(Example(20)); // ✅ 正确:显式转换

    return 0;
}

在例子中我们看到,既然不能隐式转换,那么就只能显示转换了,这是可行的。

对于错误的部分,会报错:

代码语言:javascript
代码运行次数:0
运行
复制
error: no viable conversion from 'int' to 'Example'
    Example ex1 = 10;  // ❌ 这里会报错

Explicit修饰构造函数,将会禁止构造函数的隐式转换。

静态(static)修饰类成员

声明为static的类成员称为类的静态成员

static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化;

static修饰的成员函数,称之为静态成员函数

静态成员与静态成员变量

静态成员所有类对象共享,不属于某个具体对象,存放在静态区;

通常来说,静态成员变量会在类外定义和初始化(因为静态成员变量是类级别的,不属于任何一个实例),但是在类内可以进行声明,不为它分配存储空间。

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class Example {
public:
    static int count; // 仅声明,添加static关键字,不能在类内直接初始化
};

// 在类外定义和初始化静态变量,不添加static关键字
int Example::count = 0; 

int main() {
    std::cout << "Count: " << Example::count << std::endl;
    return 0;
}

静态成员函数

属于整个类,而不是某个对象

可以直接通过类名调用ClassName::Function())。

不能访问非静态成员变量或成员函数(因为它不依赖具体对象,没有this指针)。 但是, 非静态可以调用静态

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
class Example {
private:
    static int count;
    int id;
public:
    Example(int x) : id(x) { count++; }
    static void showCount() {
        std::cout << "Total objects: " << count << std::endl;
        // std::cout << "ID: " << id; // ❌ 错误,静态函数不能访问非静态成员
    }
};

int Example::count = 0;

int main() {
    Example obj1(1), obj2(2);
    Example::showCount(); // 输出 2
    return 0;
}

匿名对象

匿名对象是 没有显式变量名的临时对象,在 C++ 中,它通常用于简化代码、减少不必要的变量声明,并提高程序的效率。

匿名对象具有以下特点:

  • 没有名字,无法在后续代码中引用或访问
  • 匿名对象的生命周期通常是短暂的,通常只在表达式的那一行代码中有效,一旦执行完毕,就会被销毁。
  • 不会影响作用域,不会像普通变量一样占用栈的空间
  • 无法被赋值,因为无法找到该对象名字。
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class Number {
public:
    Number(int x) { std::cout << "Number: " << x << " 构造\n"; }
    ~Number() { std::cout << "Number 析构\n"; }
};

int main() {
    // 不使用匿名对象
    Number n1(10);
    
    // 使用匿名对象
    Number(20);  // 创建后立即销毁

    return 0;
}
  • 匿名对象具有常性,默认是const的,即鉴于其极短的生命周期,编译器对其进行了常性优化,使得: 无法修改匿名对象、只能调用const成员函数。

针对匿名对象的生命周期,可以普遍理解为就在当前行;

有名对象的生命周期在当前函数局部域;

如果想要延长匿名对象的生命周期,可以使用const A&进行绑定,A是它对应的类。从而使得它的生命周期延长到当前局部函数域。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
    • 类的定义
    • 类的访问限定符
    • 封装
    • 类的实例化
    • 类对象的存储
    • this指针
  • 构造函数和析构函数
    • 构造函数
      • 构造函数的特点
      • 构造函数的基本用法
      • 拷贝构造函数
    • 析构函数
    • 默认成员函数
  • 赋值运算符重载
    • 运算符重载
      • 举例1:Date类
      • 举例2:前置++和后置++
    • 赋值运算符重载
      • 赋值运算符重载的关键点
  • C++ const 成员(常成员)
    • 1. const 数据成员(常数据成员)
      • 示例:常数据成员
    • 2. const 成员函数(常成员函数)
      • 示例:常成员函数
    • 3. const 对象(常对象)
      • 示例:const 对象
    • 4. mutable 关键字
      • 示例:mutable 可变成员
  • 初始化列表
    • 为什么要有初始化列表?
    • 格式与应用
  • Explicit关键字
  • 静态(static)修饰类成员
    • 静态成员与静态成员变量
    • 静态成员函数
  • 匿名对象
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档