前言

C++ 是在 C 语言的基础上发展而来的一种面向对象的高级程序设计语言,它在保留 C 语言强大功能的同时,引入了许多新的特性。C++98 和 C++11 是两个常用标准。C++ 在很多领域有着广泛的应用,比如游戏开发、操作系统开发等。


1. C++ 的特点

  • 继承 C 语言的优点:语言简洁紧凑,语法结构明确,拥有丰富的运算符和数据结构,程序执行效率高,可移植性好。
  • 既是面向过程的语言又是面向对象的语言,且以面向对象为主。

2. 开发工具

一个完整的 C++ 开发流程离不开编译工具链。以下是常用的 C++ 编译工具:

  • GCC (GNU Compiler Collection):最经典的开源编译器套件,支持 C、C++、Fortran 等多种语言。在 Linux 和 macOS 上通常作为默认编译器,Windows 上可通过 MinGW 或 Cygwin 使用。
  • Clang/LLVM:现代化的开源编译器前端,以其出色的错误提示、编译速度和模块化设计著称。是 macOS 的默认编译器,在 Linux 和 Windows 上也广泛使用。
  • Microsoft Visual C++ (MSVC):微软开发的 C++ 编译器,是 Windows 平台开发的主要工具,与 Visual Studio IDE 深度集成。
  • Intel C++ Compiler (ICC):英特尔开发的编译器,针对英特尔处理器进行了深度优化,常用于高性能计算领域。
  • 构建工具:除了编译器,还需要构建工具来管理编译过程,例如:
    • Make:经典的自动化构建工具,通过 Makefile 文件定义构建规则。
    • CMake:跨平台的构建系统生成器,可以生成适用于不同平台和编译器的构建文件(如 Makefile、Visual Studio 项目文件等)。
    • Ninja:注重速度的小型构建系统,常作为 CMake 的后端生成器。
  • 集成开发环境 (IDE):提供代码编辑、编译、调试等一体化功能的软件,例如:
    • Visual Studio (Windows):功能最全面,有强大的调试器、智能提示 (IntelliSense) 和完善的 Windows 开发支持。社区版对个人免费。适合 Windows 平台的开发者,尤其是游戏、桌面应用开发者和初学者。
    • CLion (跨平台):智能、跨平台(Win/macOS/Linux),与 CMake 深度集成,重构工具和代码导航非常强大。付费软件,但对学生和教师免费。适合追求高效编码体验的专业开发者,或经常进行跨平台项目开发的团队。
    • Code::Blocks (跨平台):免费开源、轻量,适合在配置不高的电脑上使用。功能相对基础,但足够应对学习和小型项目。适合初学者,或电脑硬件资源有限、需要一个简单开箱即用环境的用户。
    • Qt Creator (跨平台,尤其适合 Qt 开发):与 Qt 框架无缝集成,内置了可视化的 UI 设计器,对于开发图形界面应用非常方便。主要使用 Qt 框架开发 GUI 应用或跨平台软件的开发者。
    • Visual Studio Code (跨平台):轻量、免费、灵活,通过安装 C/C++ 插件可以变成功能强大的 IDE。启动快,生态丰富,但需要手动配置编译和调试环境。适合喜欢轻量级工具、需要跨平台,或偏好高度自定义的开发者(包括很多 Linux 用户)。

3. C++ 基础语法特性

3.1 命名空间

命名空间是 C++ 引入的全新机制,核心作用是解决标识符名称冲突问题。C++ 标准库中的所有组件,都收纳在 std 命名空间内。在 C 与 C++ 中,程序会定义大量变量、函数以及后续学习的类,这些标识符默认处于全局作用域,极易引发命名冲突。命名空间能够将标识符作用域本地化,以此规避命名污染,namespace 关键字正是为解决这一痛点而生。

使用 namespace 关键字定义命名空间,后面跟命名空间的名字,然后接一对花括号 {} 即可。下面创建名为 N1 的命名空间。

namespace N1 {
    int a1 = 0;
    void fun() {
        cout << "N1命名空间" << endl;
    }
}

命名空间的使用,通过 using namespace 命名空间名称使用命名空间中全部成员。全量导入,将 std 中所有名字都引入当前作用域。

using namespace std;

通过 using 命名空间名称::成员名使用命名空间中某个的成员。精准导入 using 声明遵循 C++ 的作用域规则。它只在它声明所在的当前块({})内有效。

int main() {
    // 只导入 std 中的 cout 和 endl 这两个成员
    using std::cout;
    using std::endl;

    // 现在可以直接使用,无需写 std:: 前缀
    cout << "Hello, World!" << endl;  // 正确

    // 但如果你没有 using std::cin,则必须加前缀
    std::cin >> a; // 正确
    // cin >> a;   // 错误!因为只用了 using 声明导入了 cout,没有导入 cin
    return 0;
}

通过命名空间名称::成员名在程序中直接使用某个成员。

cout << N1::a1 << endl;

3.2 输入输出

C++ 的输入输出主要通过标准库 <iostream> 提供,它定义了用于处理标准输入输出的对象和操作。

3.2.1 标准输出 (cout)

cout(读作 "see-out")是标准输出流对象,通常与显示器(控制台)关联。它使用插入运算符 << 将数据输出到屏幕。

#include <iostream>
using namespace std;

int main() {
    int age = 25;
    double salary = 5000.50;
    char name[20] = "Alice";
    
    cout << "Hello, World!" << endl;           // 输出字符串并换行
    cout << "Name: " << name << endl;         // 输出字符串(最多19个字符)和变量
    cout << "Age: " << age << endl;           // 输出整数
    cout << "Salary: $" << salary << endl;    // 输出浮点数
    
    // 连续输出多个值
    cout << "User: " << name << ", Age: " << age << endl;

    cout << "使用输入运算符插入" << 97 << endl;

    // put方法输出字符,当输入数字时转换为对应的ASCII码中的对应字符
    cout.put(97).put('A');
    cout << endl;

    // write方法输出字符串
    char c[] = "C++ Primer PLUS";
    cout.write(c, strlen(c));
    
    return 0;
}
3.2.2 标准输入 (cin)

cin(读作 "see-in")是标准输入流对象,通常与键盘关联。它使用提取运算符 >> 从用户获取输入。

#include <iostream>
#include <string>
using namespace std;

int main() {
    int age;
    double height;
    char name[20];
    
    cout << "请输入您的姓名: ";
    cin >> name;                     // 输入字符串(遇到空格停止,最多19个字符)
    
    cout << "请输入您的年龄: ";
    cin >> age;                      // 输入整数
    
    cout << "请输入您的身高(米): ";
    cin >> height;                   // 输入浮点数
    
    cout << endl << "=== 用户信息 ===" << endl;
    cout << "姓名: " << name << endl;
    cout << "年龄: " << age << endl;
    cout << "身高: " << height << "米" << endl;
    
    return 0;
}
3.2.3 处理带空格的字符串输入

使用 cin >> 输入字符串时,遇到空格会停止。要读取整行(包括空格),可以使用 getline() 函数。

#include <iostream>
#include <string>
using namespace std;

int main() {

    char c1, c2[20];
    // 输入abc d时 只读取abc
    cin >> c2;
    cout << c2 << endl;
    // 使用提取运算符读取数据时,会忽略数据前的空格、换行等空白字符
    // 读取d
    cin >> c1;
    cout << c1 << endl;
    // get获取一个字符,不会忽略空白字符
    // 由于之前输入abc d时,敲了回车,get读取到这个回车的换行符,所以不提示输入
    cin.get(c1);
    cout << "c1:" << c1 << endl;
    // 此时缓冲区为空,提示输入数据,输入b后读取到字符b
    cin.get(c1);
    cout << "c1:" << c1 << endl;

    // 忽略一个字符
    cin.ignore();
    // getline()读取一行,第一个参数为存放数据的字符串,第二个字符串为长度
    cin.getline(c2, 20);
    cout << "c2:" << c2 << endl;


    char fullName[20];
    int age;
    
    cout << "请输入您的全名: ";
    cin.getline(fullName, 20);  // 最多读取19个字符
    
    cout << "请输入您的年龄: ";
    cin >> age;
    
    cout << "您好, " << fullName << "! 您今年 " << age << " 岁。" << endl;
    
    return 0;
}
3.2.4 格式化输出

C++ 提供了一些操纵符来控制输出格式。

#include <iostream>
#include <iomanip>  // 用于格式化输出
using namespace std;

int main() {
    double pi = 3.1415926535;
    int number = 255;
    
    // 设置浮点数精度
    // fixed:以固定小数点格式显示浮点数(而不是科学计数法)
    // setprecision(2):设置小数精度为 2 位
    // 没有 fixed 时,setprecision(2) 表示总有效数字为 2 位(包含整数部分)。
    // 有 fixed 时,setprecision(2) 表示小数点后保留 2 位。
    cout << "默认精度: " << pi << endl;
    cout << fixed << setprecision(2);  // 固定小数点,保留2位
    cout << "保留2位小数: " << pi << endl;
    
    // 使用控制符setw设置输出宽度和对齐   仅对下次的输出有效
    cout << setw(10) << "姓名" << setw(10) << "年龄" << endl;
    cout << setw(10) << "张三" << setw(10) << 25 << endl;
    cout << setw(10) << "李四" << setw(10) << 30 << endl;

    // 使用成员函数width设置宽度为6
    cout.width(6);
    cout << 4.5 << endl;

    // 使用setfill控制符设置宽度  一直有效
    cout << setfill('*');
    cout << setw(6) << 4.5 << endl;
    cout << setw(6) << 4.5 << endl;

    // 使用成员函数设置宽度,并返回正在使用的填充字符
    char c = cout.fill('#');
    cout << "设置为#前的填充字符" << c << endl;
    cout << setw(6) << 4.5 << endl;
    cout << setw(6) << 4.5 << endl
    
    // 不同进制输出
    cout << "十进制: " << number << endl;
    cout << "十六进制: " << hex << number << endl;
    cout << "八进制: " << oct << number << endl;
    
    return 0;
}
3.2.5 错误处理

输入时可能会遇到错误,比如用户输入了非数字字符。可以使用 cin 的状态标志来检测和处理错误。

#include <iostream>
using namespace std;

int main() {
    int number;
    
    cout << "请输入一个整数: ";
    cin >> number;
    
    if (cin.fail()) {
        cout << "输入错误!请输入有效的整数。" << endl;
        cin.clear();                    // 清除错误标志
        cin.ignore(1000, '\n');         // 忽略错误输入
    } else {
        cout << "您输入的是: " << number << endl;
    }
    
    return 0;
}
3.2.6 文件输入输出

除了标准输入输出,C++ 还支持文件操作,需要包含 <fstream> 头文件。

void open(const char *filename, ios::openmode mode);

在这里,open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。

下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

您可以把以上两种或两种以上的模式结合使用。例如,如果您想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么您可以使用下面的语法:

ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );

类似地,您如果想要打开一个文件用于读写,可以使用下面的语法:

ifstream  afile;
afile.open("file.dat", ios::out | ios::in );
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    // 写入文件
    ofstream outFile("data.txt");
    if (outFile.is_open()) {
        outFile << "Hello, File!" << endl;
        outFile << "This is a test file." << endl;
        outFile.close();
        cout << "文件写入成功!" << endl;
    } else {
        cout << "无法打开文件!" << endl;
    }
    
    // 读取文件
    ifstream inFile("data.txt");
    string line;
    if (inFile.is_open()) {
        cout << "文件内容:" << endl;
        while (getline(inFile, line)) {
            cout << line << endl;
        }
        inFile.close();
    } else {
        cout << "无法打开文件!" << endl;
    }
    
    return 0;
}
3.2.7 注意事项
  • 使用 cincout 前需要包含 <iostream> 头文件
  • 通常使用 using namespace std;std:: 前缀来访问标准库组件
  • endl 不仅输出换行符,还会刷新输出缓冲区
  • 混合使用 cin >>getline() 时要注意缓冲区中的换行符
  • 文件操作完成后要记得关闭文件

3.3 初始化

初始化看起来简单,但C++提供了至少三种方式。

3.3.1 拷贝初始化

先构造临时对象(或直接使用字面量),再拷贝给 a。对内置类型没区别,但对类类型可能产生隐式类型转换。允许窄化转换(如 int a = 3.14; 只警告,不报错,值变为 3)。

3.3.2 直接初始化

直接调用构造函数。对内置类型等同于赋值。如果写成 MyClass b(); 会被当成函数声明。

MyClass b(10);   // 这才是直接初始化,调用 MyClass(int) 构造函数
MyClass b();     // 这是函数声明,不是初始化,编译会通过但行为完全不是你想的那样
3.3.3 拷贝列表初始化

使用花括号,C++11 引入。禁止窄化转换(int c = {3.14}; 直接编译报错),最安全。

#include <iostream>
using namespace std;

int main() {
    // 传统
    int a = 1;
    // 对象
    int b(10);
    // 统一
    int c { 100 };
    cout << a << "," << b << "," << c << endl;
    return 0;
}
初始化方式 语法示例 允许窄化 允许隐式转换 典型场景
拷贝初始化 T a = x; 允许,但警告 允许 传参、返回值
直接初始化 T a(x); 允许 不允许,虚匹配构造函数 显式构造
列表初始化 T a{x}; 报错 不允许 推荐优先使用

3.4 内存管理

3.4.1 new、delete分配、释放内存空间

newdelete 是 C++ 中用于动态内存管理的运算符。它们分别用于在程序运行时从自由存储区(free store)分配和释放内存。

内存区域:堆(Heap)与自由存储区(Free Store)

在 C++ 中,堆(heap)自由存储区(free store)是两个经常被混用但概念上不同的术语:

  • 堆(Heap):是 C 语言和操作系统层面的概念,通过 malloc/freecallocrealloc 等函数管理的内存区域。

  • 自由存储区(Free Store):是 C++ 标准引入的概念,特指通过 newdelete 运算符管理的内存区域。

在大多数实现中(如 GCC、Clang、MSVC),自由存储区和堆是同一块内存区域,但 C++ 标准只规定 new 从自由存储区分配,不保证它一定在堆上。从编程实践角度,可以认为两者通常指向同一区域,但理解其概念区别有助于避免术语混淆。

栈(Stack)与堆/自由存储区的对比

特性 栈(Stack) 堆/自由存储区(Heap/Free Store)
管理方式 编译器自动管理,函数调用时分配,返回时释放 程序员手动管理(new/deletemalloc/free
分配速度 快(仅移动栈指针) 慢(需查找合适内存块,可能引发碎片)
生命周期 与作用域绑定,自动销毁 直到显式释放(delete/free)或程序结束
大小限制 较小(通常几 MB) 较大(受系统虚拟内存限制)
碎片问题 有(频繁分配释放可能产生内存碎片)
典型用途 局部变量、函数参数、返回值 动态数据结构(链表、树)、大对象、需跨函数存活的数据

new 和 delete 的工作原理

new 实际上完成两件事,缺一不可:

  1. 调用 operator new(或 operator new[])分配原始内存;

  2. 在分配的内存上调用构造函数(对于类类型)或进行值初始化(对于内置类型)。

同理,delete 也分两步:

  1. 调用析构函数(对于类类型);

  2. 调用 operator delete(或 operator delete[])释放内存。

单个对象的内存分配与释放

语法指针变量 = new 类型名(初值)(初值可选)

释放delete 指针

#include <iostream>
using namespace std;

int main() {
    // 分配单个内存,值初始化为0
    int* p = new int();
    cout << *p << endl;   // 输出: 0
    delete p;
    
    // 分配并初始化
    p = new int(1000);
    cout << *p << endl;   // 输出: 1000
    delete p;
    
    // 对于类类型
    class MyClass {
    public:
        MyClass() { cout << "构造函数调用" << endl; }
        ~MyClass() { cout << "析构函数调用" << endl; }
    };
    
    MyClass* obj = new MyClass();
    delete obj;
    
    return 0;
}

数组的内存分配与释放

语法指针变量 = new 类型名[长度]{初始化列表}(长度可为变量,初始化列表可选)

释放delete[] 指针

#include <iostream>
using namespace std;

int main() {
    // 分配10个int的数组,未初始化(值不确定)
    int* arr1 = new int[10];
    delete[] arr1;
    
    // 分配并部分初始化(前两个元素为1和2,其余为0)
    int* arr2 = new int[10]{1, 2};
    for (int i = 0; i < 10; i++) {
        cout << arr2[i] << " ";
    }
    cout << endl;
    delete[] arr2;
    
    // 动态长度数组
    int n;
    cout << "请输入数组长度: ";
    cin >> n;
    double* dynamicArr = new double[n];
    // ... 使用数组
    delete[] dynamicArr;
    
    return 0;
}

核心规则new 必须配 deletenew[] 必须配 delete[],绝不可混用。原因是编译器需要在数组内存块头部记录元素个数,delete[] 才知道要调用多少次析构函数,而 delete 不会读取这个信息。

常见错误与注意事项

  • 内存泄漏:分配后未释放

  • 重复释放:对同一指针多次调用 delete

  • 使用已释放内存delete 后继续访问指针(悬垂指针)

  • 数组与单个对象混用new[]delete 释放,或 newdelete[] 释放

  • 异常安全new 可能抛出 std::bad_alloc 异常

3.4.2 new 与 malloc 的区别
对比项 new / delete malloc / free
语言 C++ 运算符 C 语言函数
内存计算 自动计算类型大小 需手动计算字节数
返回值 返回正确类型的指针 返回 void*,需强制转换
失败处理 抛出 std::bad_alloc 异常 返回 NULL
构造/析构 调用构造函数/析构函数 不调用构造/析构函数
重载 可全局或类内重载 不可重载
内存区域 自由存储区(free store) 堆(heap)
初始化 支持直接初始化、列表初始化 只能分配原始内存,需手动初始化

总结:在 C++ 中应优先使用 newdelete,因为它们与 C++ 的对象模型(构造/析构、异常安全)更契合。对于需要与 C 库交互或需要重新分配内存(realloc)的场景,才考虑使用 malloc/free

3.5 函数

3.5.1 函数重载

在C++中可以定义多个同名函数,他们名字相同,功能相同,只要它们形参的个数、类型或顺序不完全一样就可以,编译程序根据实参与形参的类型及个数自动确定调用哪一个同名函数,这就是函数重载,这些同名函数称为重载函数。

如果多个同名函数只是函数类型(函数返回值类型)不同时,它们不是重载函数。

    #include <iostream>
    using namespace std;

    int max(int a, int b) {
	    if (a > b) {
	    	return a;
	    } else {
	    	return b;
	    }
    }

    float max(float a, float b) {
	    if (a > b) {
		    return a;
	    }
	    else {
		    return b;
	    }
    }

    int main() {
	    cout << max(10, 20) << endl;
	    cout << max(10.1f, 20.1f) << endl;
	    return 0;
    }
3.5.2 默认参数

定义或声明函数时,可以预先为函数参数指定默认参数值。如果调用函数时未提供该参数,则使用默认值;如果提供了,则覆盖默认值。

    // a, b, c 都有默认参数
    void Func(int a = 10, int b = 20, int c = 30)  {
        cout << "a = " << a << endl;
        cout << "b = " << b << endl;
        cout << "c = " << c << endl << endl;
    }
    int main() {
        Func();         // a = 10    b = 20    c = 30
        Func(1);        // a = 1     b = 20    c = 30
        Func(1, 2);     // a = 1     b = 2     c = 30
        Func(1, 2, 3);  // a = 1     b = 2     c = 3
    }
    void Func2(int a, int b = 10, int c = 20)
    {
        cout << "a = " << a << endl;
        cout << "b = " << b << endl;
        cout << "c = " << c << endl << endl;
    }
    int main()
    {
        Func2(1);         // a = 1    b = 10    c = 20
        Func2(1, 2);      // a = 1    b = 2     c = 20
        Func2(1, 2, 3);   // a = 1    b = 2     c = 3

        return 0;
    }

核心规则:

  • 在带默认参数的函数被调用时,C++规定必须从左往右依次给实参,不能跳跃给实参。
  • 在给函数参数赋默认值时,必须从右往左依次连续赋值,不能间隔跳跃给默认值。
  • 缺省参数不能在函数声明和定义中同时出现。

3.6 语法符号

3.6.1 引用

"&"用于定义引用类型。引用是已有变量或表达式的一种别名机制,主要用于函数的形参和返回值。它并不是一个独立的对象,不占用内存空间(从逻辑概念上说)。它一旦绑定(初始化)到一个变量,就终生绑定,无法再指向其他变量。

特点:

  • 必须初始化:引用在定义时必须被赋值,不能像指针那样先定义再赋值。
  • 不能重新绑定:引用一旦绑定,就无法再指向另一个变量。
  • 不存在空引用:引用必须绑定到有效内存,因此比指针更安全(不需要像指针那样检查是否为空)。

#include "iostream"
    using namespace std;

    void myswap(int& a, int& b) {
	    int temp = a;
	    a = b;
	    b = temp;
    }

    int main() {
	    int a = 10;
        int n = 100;

	    // 定义b是a的别名
	    int& b = a;
	    b = n;   // 注意!这不是让b指向n,而是把n的值(100)赋值给a,a变成了100。

	    cout << a << "," << b << endl;

	    int c = 10, d = 20;
	    myswap(c, d);
	    cout << c << "," << d << endl;

	    return 0;
    }
3.6.2 范围 for 循环的语法标记

":"用于范围for循环。范围 for 循环是 C++11 引入的语法糖,专门用于简洁、安全地遍历容器或数组中的所有元素。

    #include <iostream>
    using namespace std;

    int main() {
    	int a[] = {1,2,3,4,5,6};
	    // 范围for中的变量为普通形参,遍历不修改a的值
	    for (int e : a) {
		    cout << e << ",";
	    }
	    cout << endl;
	    // 范围for中的变量为引用形式的形参,遍历可以修改a的值
	    for (int& e : a) e++;
	    for (int e : a) {
	    	cout << e << ",";
	    }
	    cout << endl;
	    // 变量为const引用形式,遍历时避免拷贝和修改原来数据
	    for (const int& e : a) {
		    cout << e << ",";
	    }
	    cout << endl;


	    return 0;
    }

3.7 关键字

3.7.1 空指针nullptr

在C语言中常量0有着int和空指针(NULL)双重角色,C++支持函数重载后使用整数0表示空指针存在潜在隐患,C++11通过引入nullptr关键字来纠正这个问题。

nullptr可以隐式转换为任意类型的指针并且和这些类型指针进行比较。为了向下兼容NULL依然是一个有效的空指针。

    #include <iostream>
    using namespace std;

    void fun(int* p) {
	    cout << "void fun(int* p)" << endl;
    }

    void fun(int a) {
	    cout << "void fun(int a)" << endl;
    }


    int main() {
	    int* p = NULL;
	    fun(p);
	    fun(NULL);   // 调用 fun(int),而非 fun(void*)  可能不是预期行为
	    // 在c中0有int和NULL两种含义,在c++中传入0调用整型重载,为避免存在问题,引入空指针nullptr调用指针重载
	    fun(nullptr);
	    return 0;
    }
3.7.2 布尔类型bool

在C语言中,0表示假,非0整数(通常为1)表示真;在C++中,引入了基本类型bool,1个字节,取值为true和false。

3.7.3 内联函数inline

 C++在编译时可以将调用的函数代码嵌入到主调函数中,这种嵌入到主调函数中的函数称为内联函数,又称为内嵌函数或内置函数。

inline的核心作用是向编译器提供一个优化建议:在编译阶段,将函数调用的地方直接替换为函数体的实际代码,从而省去函数调用时压栈、返回等额外开销。内联函数不是在调用时发生控制转移,而在编译时将被调用函数嵌入每个函数调用处,节省函数调用的开销。当内联函数中实现过于复杂时,编译器会将它作为一个普通函数处理,所以内联函数内不能包含循环语句和switch语句。

#include <iostream>
using namespace std;

inline int add(int a, int b) {
	return a + b;
}

int main() {
	cout << add(1, 2) << endl;
	return 0;
}

适合内联的情况

  • 函数体非常小(1-5行代码)

  • 频繁调用的函数(如循环内的getter/setter)

  • 类内部定义的成员函数(默认内联)

4. 面向对象程序设计

4.1 面向对象编程(oop)

核心思想:以对象为中心,将需求涉及的事务封装成类,由对象间相互协作完成任务。

主要特点:以类为最小,强调数据与行为统一(封装)。

核心公式:面向对象 = 类 + 对象 + 交互(对象间通过调用方式互相协作)。

基本概念:类与对象。

类:类是对现实世界中具有共同属性(数据)和行为(方法)的一类事物的抽象模板,它定义了该类型对象“长什么样”(状态)和“能做什么”(功能)。

对象:对象是根据类这个模板在堆内存中实际创建出来的具体实体(实例),它拥有类所定义的属性值,并能够执行类所定义的行为。每个对象在内存中拥有独立的数据存储空间(状态)。

4.2 类与对象

4.2.1 类的组成

属性(数据成员):类中的数据。

  • 类中的数据成员描述该类的对象的属性。其定义方式与一般变量相似,但对数据成员的访问要进行权限控制。
  • 数据成员初始化与变量初始化形式有所不同,不能使用()号,可以使用=或{}。
  • 数据成员的类型可以是C++支持的任意类型。
  • 多个数据成员之间不能重名,一个类作为一个作用域,不允许命名冲突。

行为(成员函数):类中的函数。

  • 成员函数描述类对象的行为,即该对象所能执行的操作,向类外提供服务。
  • 一个类中的多个成员函数可以重载;成员函数的形参也可以带有默认值。
4.2.2 成员访问控制

访问修饰符private/protected/public)用于控制类成员的访问权限,作用域是从该关键字开始,直到遇到第一个“}”右花括号(即类定义结束)为止,或者被下一个访问修饰符打断。

private(私有):私有访问权限,私有成员只允许本类的成员函数访问,对类的外部不可见。

protected(保护):保护访问权限,能被本类和本类的派生类的成员函数访问,对类的外部不可见。

public(公有):公有访问权限,共有成员外部可见,类内部也能访问。

数据成员通常为private,成员函数通常为public,数据成员一般通过对应的get()、set()方法访问。

    class 类名
    {
	    public:
	    	公有数据成员;
		    公有成员函数;
	    private:
		    私有数据成员;
	    	私有成员函数;
	    protected:
		    保护数据成员;
		    保护成员函数;
    };
4.2.3 类与对象的关系

类与对象是抽象与具体的关系。类是创建对象的样板,由这个样板可以创建多个具有相同属性的行为的对象。

类本质上是一种数据类型,对象是某个类的实例,一个类的多个对象分别拥有自己的属性且相互独立。

4.2.4 类的特殊成员

构造函数:

定义:类的一种特殊成员函数,用于初始化对象(为数据成员赋初值)。

作用:构造函数的作用是在创建对象时,利用特定的值构造对象,将对象初始化为一个特定的状态。

特点:

  • 函数名与类名相同,无返回值(也不能写void)。
  • 构造函数一般情况下声明为公有权限。
  • 构造函数不能在程序中调用,对象创建时被编译器自动调用。
  • 支持重载,可以定义多个参数不同的构造函数。
  • 默认行为:如果用户未定义构造函数,编译器会自动生成一个无参的构造函数。
#include <iostream>
using namespace std;

class Rectangle {   
private:
	int mLength;    
	int mWidth;
public:
    // 构造函数
	Rectangle(int length, int width) {
		mLength = length;
		mWidth = width;
	}

	int getLength() {
		return mLength;
	}

	void setLength(int length) {
		mLength = length;
	}

	int getWidth() {
		return mWidth;
	}

	void setWidth(int width) {
		mWidth = width;
	}

	int getArea() {
		return mLength * mWidth;
	}

};

int main() {
	Rectangle rect = {10, 20};
	cout << rect.getArea() << endl;
	return 0;
}

析构函数:

定义类的一种特殊成员函数,用于清理对象资源(释放动态内存、关闭文件/连接)。

作用:在销毁一个对象前被调用,用于释放该对象申请的动态分配内存空间以及其他一些清理工作。

特点:

  • 函数名为~类名,无返回值(也不能写void)。
  • 析构函数不能重载,一个类中只能定义一个析构函数。
  • 当一个对象销毁时,析构函数被自动调用。
  • 默认行为:编译器自动生成空析构函数,仅释放栈内存,不会自动delete堆指针。
  • 关键注意:如果类中涉及动态内存分配new),必须自定义析构函数并delete,否则内存泄漏。

拷贝构造函数

定义:用一个已存在的对象初始化新的对象。

函数名:类名(const 类名& other),参数必须是引用(否则会无限递归)。

调用时机:

  • 对象以值传递方式传入函数参数。

  • 对象以值传递方式从函数返回。

  • 使用一个对象初始化另一个对象(如 Class A = B;)。

默认行为:如果没有显式定义拷贝构造函数,编译器将生成一个默认的拷贝构造函数(浅拷贝——简单复制成员变量的值(指针只复制地址))。

关键注意:如果类中有指针成员,必须自定义拷贝构造函数实现深拷贝(Deep Copy,重新分配内存并复制内容),否则多个对象指向同一块内存,析构时会重复释放(Double Free)导致程序崩溃。

赋值函数:

定义:用赋值语句把一个对象赋值给已经存在的同类对象时,将调用赋值函数。(两个对象都存在)。

函数名:类名& operator=(const 类名& other),返回值通常为*this(支持连续赋值)。

调用时机:赋值操作。(A = B,其中A和B已经构造完成。)

默认行为:编译器自动生成,同样执行浅拷贝

关键注意(经典三步曲):

  1. 自赋值检测if (this == &other) return *this;(防止销毁自己)。

  2. 释放旧资源delete[] m_data;(清空被赋值对象原有的内存)。

  3. 分配新资源并复制:重新new并拷贝内容。

最后,遵循五法则(Rule of Five):如果自定义了析构、拷贝构造、拷贝赋值、移动构造、移动赋值中的任何一个,通常需要全部自定义。

class Book {
private:
	char mName[50];
	char mAuther[50];
	char mPublisher[50];
	float mPrice;
	char* mTitle;
public:
	// 构造函数
	Book(const char* name, const char* auther, const char* publisher, float price, const char* title ) {
		strcpy(mName, name);
		strcpy(mAuther, auther);
		strcpy(mPublisher, publisher);
		mPrice = price;
		mTitle = new char[strlen(title) + 1];
		strcpy(mTitle, title);
	}
	
	// 析构函数
	~Book() {
		delete[] mTitle;
	}

	// 拷贝构造函数
	Book(const Book &other) {
		strcpy(mName, other.mName);
		strcpy(mAuther, other.mAuther);
		strcpy(mPublisher, other.mPublisher);
		mPrice = other.mPrice;
		mTitle = new char[strlen(other.mTitle) + 1];
		strcpy(mTitle, other.mTitle);
	}

	// 第一个const 表示函数返回的是一个指向常量字符的指针,即调用者不能通过这个指针修改字符串内容。
	const char* getTitle() {
		return mTitle;
	}

	void setTitle(const char* title) {
		delete[] mTitle;
		mTitle = new char[strlen(title) + 1];
		strcpy(mTitle, title);
	}

	const char* getName() {
		return mName;
	}

	void setName(const char* name) {
		strcpy(mName, name);
	}

	void display() {
		cout << "书名:" << mName << endl;
		cout << "作者:" << mAuther << endl;
		cout << "出版社:" << mPublisher << endl;
		cout << "价格:" << mPrice << endl;
		cout << "简介:" << mTitle << endl;
	}
};

int main() {
	Book book1("C++ Primer Plus", "Stephen Prata", "人民邮电出版社", 99.99, "《C++ Primer Plus》是一本广受欢迎的C++入门书籍,由Stephen Prata编写。该书详细介绍了C语言的基础知识以及C++新增的特性,是许多初学者学习C++的首选书籍。");
	book1.display();
	cout << endl;
	//cout << book1.getName() << endl;

	book1.setName("C Primer Plus");
	book1.setTitle("《C Primer Plus》是一本经典的C语言入门书籍,由Stephen Prata编写,适合希望系统学习C语言的读者,也适用于精通其他编程语言并希望进一步掌握和巩固C编程技术的程序员。");
	book1.display();
	cout << book1.getName() << endl;
	cout << endl;

	// 验证拷贝构造函数
	Book book2 = book1;
	book2.display();
    cout << endl;
	return 0;
}

4.3 面向对象的三大特征

4.3.1 封装

定义:将数据(属性)和操作数据的方法(行为)捆绑在一起,并隐藏内部实现细节,仅通过公开的接口与外界交互。

实现手段:通过使用访问修饰符(private /protected /public)控制成员的可见性。

核心目的:数据安全(防止外部随意修改) + 降低耦合(内部变化不影响外部调用)。

通过类(class)实现封装,struct也可以但默认权限不同。

关键要点:

  • 数据成员通常设为private,通过publicget/set方法访问(可加校验逻辑)。

  • 封装 ≠ 信息隐藏:封装是手段,信息隐藏是目的。

class Person {
private:
	char mName[20];   // 数据隐藏
	char mSex[5];
	int mAge;
public:
	Person(const char* name, const char* sex, int age) {      // 公开接口
		strcpy(mName, name);
		strcpy(mSex, sex);
		mAge = age;
	}

	void display() {
		cout << "姓名:" << mName << endl;
		cout << "性别:" << mSex << endl;
		cout << "年龄:" << mAge << endl;
	}

	char* getName() {
		return mName;
	}
};
4.3.2 继承

定义:允许一个类(派生类/子类)基于另一个类(基类/父类)来构建,自动拥有基类的成员(除构造函数/析构函数/赋值运算符)。

继承的类型:

单继承:子类只有一个父类(基类)。

多继承:子类有多个父类(基类)。

直接继承:教师类时教师管理人员的直接父类。

间接继承:人类是教师管理人员的间接父类。

子类中定义了和父类方法同名的方法时,父类方法(包括重载方法)被屏蔽,叫做方法的隐藏。

继承方式:C++支持三种继承方式:publicprotectedprivate,影响基类成员在派生类中的访问权限。

继承方式确定了基类成员在子类成员中的可见性。

公有继承:基类成员在派生类成员中保持原有访问权限不变。

保护继承:基类的公有成员在派生类中变为保护成员。

私有继承:基类的公有成员和保护成员均变为私有成员。默认继承方式为私有继承。

继承方式 基类private成员 基类protected成员 基类public成员
private 不可访问 派生类中为private 派生类中为private
protected 不可访问 派生类中为protected 派生类中为protected
public 不可访问 派生类中为protected 派生类中为public

派生类的构造和析构过程:

  • 派生类的构造过程:先执行父类(基类)的构造函数,再执行派生类的构造方法。
  • 派生类的析构过程:先执行派生类的析构函数,再执行父类(基类)的析构函数。

多继承:

  • 一个派生类可以有多于一个的父类(基类),称为多继承,但这会带来菱形继承问题(需用virtual解决)。
  • 多继承的构造顺序是,按照声明顺序依次调用各基类构造函数。

菱形继承:

class A { public: int data; };
class B : public A {};
class C : public A {};
class D : public B, public C {};  // 菱形继承
 

此时,D类对象中会包含两份A的成员data,访问时可能引发二义性。

解决方案:通过虚继承(Virtual Inheritance)可以解决菱形继承问题,确保基类在派生类中仅保留一份实例。

class A { public: int data; };
class B : virtual public A {};    // 虚继承
class C : virtual public A {};    // 虚继承
class D : public B, public C {};  // 仅保留一份A的成员
 

虚继承的特性:

  • 共享基类子对象:虚继承的基类在最终派生类中仅有一个实例。
  • 初始化责任:虚基类的构造函数由最底层的派生类直接调用。
  • 内存布局:虚继承通常通过虚基类指针实现,可能增加内存开销。
4.3.3 多态

定义:同一操作作用于不同对象时,产生不同行为。即"一个接口,多种实现"。

分类:C++中的多态分为两种编译时多态(静态多态)和运行时多态(动态多态)。

编译时多态(静态多态):函数重载、运算符重载、模板,其多态性是在编译时确定的

#include <iostream>
using namespace std;

class Cat {
private:
	char Name[20];
	int Full = 50;
public:
	Cat(const char* name) {
		strcpy(Name, name);
	}

	// 吃一份
	void eat() {
		if (Full >= 100) {
			cout << "吃饱了" << endl;
		}
		Full++;
	}

	//吃n份
	void eat(int n) {
		int count = 0;
		for (int i = 0; i < n; i++)
		{
			if (Full < 100) {
				count++;
				Full++;
			}
		}
		if (count == 0) {
			cout << "吃饱了" << endl;
		}
		else {
			cout << "我吃了" << count << "份食物" << endl;
		}
	}
};

int main() {
	Cat cat("Tom");
	cat.eat();
	cat.eat(80);
	return 0;
}

运行时多态(动态多态):虚函数(virtual)+ 继承 + 基类指针/引用指向派生类对象,其多态性是运行时确定的。

#include <iostream>
using namespace std;

class Person {
public:
	// 定义虚函数
	// 定义为虚函数后,父类指针指向子类对象,且子类覆盖了父类方法,调用子类方法
	virtual void buyTicket() {
		cout << "普通人买票" << endl;
	}
};

class Student : public Person {
public:
	void buyTicket() {
		cout << "学生票" << endl;
	}
};

int main() {
	Person p;
	p.buyTicket();
	Student s;
	s.buyTicket();

	// 父类指针可以指向子类对象,反之错误
	Person* person = &s;
	person->buyTicket();
	return 0;
}

虚函数使用virtual关键字定义:父类成员函数被声明为虚函数,子类重写父类虚函数后,父类指针指向子类对象时,通过父类指针调用的函数是子类的函数。

运行时多态(动态多态)的核心条件(必须同时满足):

  • 基类中具有虚函数(virtual),派生类中重写该函数。
  • 存在继承关系
  • 使用基类指针基类引用指向派生类对象。

核心目的:统一接口(面向抽象编程) + 可扩展性(新增子类无需修改调用代码)。

4.3.4 虚函数

定义:使用virtual关键字声明的成员函数,允许派生类重写(override) 该函数,实现运行时多态

核心作用:通过基类指针/引用调用虚函数时,实际执行的是派生类重写后的版本(动态绑定)。

语法:virtual 返回类型 函数名(参数列表);

关键要点:

  • 将基类中的函数声明为虚函数,派生类中的同名函数自动为虚函数。
  • 构造函数不能声明为虚函数,析构函数可以声明为虚函数,静态函数不能是虚函数。

虚函数表:每个包含虚函数的类,在编译期生成一个函数指针数组,存放该类所有虚函数的实际地址。

虚指针:每个对象(包含虚函数的类实例)在内存中隐藏一个指针(虚函数指针),指向该类的虚函数表。对象大小会因此增加一个指针大小(32位系统4字节,64位系统8字节)。

调用虚函数时,通过vptr查表找到实际函数地址,实现运行时绑定。

class Base {
    public:
        virtual void show() const { cout << "Base"; }
};
class Derived : public Base {
    public:
        void show() const override { cout << "Derived"; } // 重写
};

int main() {
    Base* p = new Derived();
    p->show();  // 输出 "Derived"(运行时决定)
}

纯虚函数与抽象类:

纯虚函数是在虚函数声明末尾加上= 0,表示该函数没有实现(或实现可选)。

函数语法:virtual 返回类型 函数名(参数列表) = 0;

抽象类就是包含纯虚函数的类,当我们需要使用父类指针访问子类方法且不需要父类实例化时,可以使用抽象类。

核心规则:

  • 抽象类无法实例化,只能作为基类被继承。
  • 派生类必须重写所有的纯虚函数,否则派生类仍为抽象类,无法实例化。
  • 构造函数不能是纯虚函数,纯虚函数只针对成员函数,构造/析构不能声明为纯虚。
  • 可以定义纯虚函数的实现,但C++允许为纯虚函数提供实现(在类外定义),供派生类显式调用(Base::func())。
class Shape {  // 抽象类
public:
    virtual void draw() const = 0;  // 纯虚函数
    virtual double area() const = 0;
};

虚析构函数:

析构函数也可以定义为虚析构函数,如果基类的析构函数定义为虚函数,则派生类的析构函数就会自动成为虚析构函数。

核心作用:当通过基类指针删除派生类对象时,确保先调用派生类析构函数,再调用基类析构函数,完整释放所有资源。

class Base {
public:
    ~Base() { cout << "~Base"; }         // 非虚
};
class Derived : public Base {
    int* p;
public:
    ~Derived() { delete p; cout << "~Derived"; }  // 释放动态内存
};

Base* obj = new Derived();
delete obj;  // 只调用 ~Base,~Derived 不会被调用  p 泄漏!
class BaseA {
public:
	virtual ~BaseA() {
		cout << "BaseA()" << endl;
	}
};

class DrivedA : public BaseA {
public:
	~DrivedA() {
		cout << "DrivedA()" << endl;
	}
};

int main() {
	// 一般情况下,即使父类的析构函数不是虚的,父类和子类的析构函数都会调用
	DrivedA a;
	// 当父类指针指向子类对象,且通过父类指针释放子类对象时,如果父类析构函数不是虚析构函数,就只会调用父类的虚构函数
	// 因为编译器p 是 Person* 类型,如果 ~Person() 不是虚函数,就只调用 ~Person(),不会调用 ~Student(),导致子类的资源没有释放!
	BaseA* p = new DrivedA;
	delete p;
}

4.4 类的高级特性

4.4.1 深拷贝与浅拷贝

浅拷贝:编译器默认生成的拷贝构造/赋值函数,只复制成员变量的值。若成员是指针,则只复制地址(多个对象指向同一块内存)。

class String {
    char* data;
public:
    String(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); }
    ~String() { delete[] data; }
};
String s1("Hello");
String s2 = s1;  // 浅拷贝 → s1.data 和 s2.data 指向同一地址
// 析构时重复释放 → 程序崩溃(Double Free)

深拷贝:用户自定义拷贝构造/赋值函数,不仅复制指针本身,还重新分配内存并复制指针指向的内容(每个对象拥有独立内存)。何时必须深拷贝,在类中有指针成员且指向动态内存分配。

三 / 五法则:若自定义了析构函数,拷贝构造函数,拷贝赋值中的一任意一个,通常都需要全部自定义。

// 拷贝构造函数
String(const String& other) {
    data = new char[strlen(other.data) + 1];
    strcpy(data, other.data);  // 独立内存
}

// 赋值函数
String& operator=(const String& other) {
    if (this == &other) return *this;  // 自赋值检测
    delete[] data;                     // 释放旧内存
    data = new char[strlen(other.data) + 1];
    strcpy(data, other.data);
    return *this;
}
4.4.2 类的组合

定义:类的组合就是在一个类中内嵌其它类的对象作为成员。

构造过程:

  • 如果内嵌对象的类有无参的构造函数,则在组合类构造函数的初始化表中可以不提供该内嵌对象的初始化参数,编译器会自动调用其无参构造函数创建内嵌对象。
  • 如果内嵌对象的类只有带参的构造函数,则在组合类构造函数的初始化表中必须提供该内嵌对象的初始化参数。
  • 如果类中有多个内嵌对象,则组合类的构造函数的执行顺序是:1.按照内嵌对象的声明顺序依次调用内嵌对象的构造函数;2.组合类本身的构造函数。

注意要点:

  • 成员对象的初始化必须在初始化列表中完成(不能在构造函数体内赋值)。

  • 初始化顺序:先成员对象(按声明顺序),后自身;析构顺序相反。

class Point {
private:
	int X;
	int Y;
public:
	Point(int x, int y) {
		X = x;
		Y = y;
	}

	int getX() {
		return X;
	}

	int getY() {
		return Y;
	}
};

// 组合类的构造顺序和对象声明的顺序有关,在本身的构造函数
class Triangle {
private:
	Point point1;
	Point point2;
	Point point3;
public:
	Triangle(int x1, int y1, int x2, int y2, int x3, int y3) : point1(x1, y1), point2(x2, y2), point3(x3, y3) {

	}

	void show() {
		cout << "point1:" << "(" << point1.getX() << "," << point1.getY() << ")" << endl;
		cout << "point2:" << "(" << point2.getX() << "," << point2.getY() << ")" << endl;
		cout << "point3:" << "(" << point3.getX() << "," << point3.getY() << ")" << endl;
	}
};
4.4.3 静态成员

定义:

静态数据成员:使用static修饰的数据成员,所有对象共享同一份数据,属于类本身而非某一个类对象。

静态成员函数:使用static修饰的成员函数,无this指针,静态成员函数只能访问静态成员,不能访问非静态成员。

注意要点:

  • 静态数据成员必须在类外单独定义和初始化(唯一例外:const static整型可在类内初始化)。

  • 静态成员函数不能是const(因为const修饰的是this指针,而静态函数无this)。

  • 静态成员不占用对象空间,不影响sizeof

#include <iostream>
using namespace std;

class Child {
private:
	static int mCount;
	int test = 0;
public:
	Child() {
		mCount++;
	}
	~Child() {
		mCount--;
	}
	void foo() {
		cout << mCount << endl;  // 访问静态数据成员
		cout << getCount() << endl;   //调用静态成员函数
	}
	// 定义静态方法
	static int getCount() {
		// 静态方法不能访问普通数据成员,同样不能调用普通成员函数
		return mCount;
	}

};

// 静态数据成员必须在类外部初始化
int Child::mCount = 0;

int main() {
	Child c1, c2, c3;
	// 普通对象可以调用静态方法
	cout << c3.getCount() << endl;

	// 通过类名直接访问静态方法
	cout << Child::getCount() << endl;
	return 0;
}
4.4.4 常对象与常成员函数

常对象:使用const修饰的对象,其数据成员不能修改,只能调用常成员函数。常函数可以访问静态成员,也可以调用静态函数。

常成员函数:在函数声明末尾加const,承诺不修改对象的成员数据。普通对象既可以调用普通成员函数,也可以调用常成员函数,但会优先调用普通成员函数。常成员函数可以重载普通版本(同名函数,一个const,一个非const)。

#include <iostream>
using namespace std;
// 常函数可以访问静态成员,也可以调用静态函数。
// 普通对象既可以调用普通成员函数,也可以调用常成员函数,但会优先调用普通成员函数。

class Dog {
private:
	int mAge;
	static int mCount;
public:
	Dog(int age) {
		mAge = age;
		mCount++;
	}

	void setAge(int age) {
		mAge = age;
	}

	static int getCount() {
		return mCount;
	}

	// 定义常成员函数 
	int getAge() const {
		cout << "getAge().const" << endl;
		return mAge;
	}

	// 定义普通成员函数 
	int getAge() {
		cout << "getAge()" << endl;
		return mAge;
	}

	// 常函数可以调用静态成员和静态成员函数
	void show() const {
		mCount++;
		mCount--;
		cout << getCount() << ":" << mAge << endl;
	}
};

int Dog::mCount = 0;

int main() {
	Dog d1(10);
	// 使用const定义常对象
	const Dog d2(100);
	cout << d1.getAge() << endl;

	cout << d2.getAge() << endl;

	d2.show();

	return 0;
}
4.4.5 对象数组与对象指针

对象数组:存放对象的数组称为对象数组。每个元素独立调用构造函数。

对象指针:存放对象地址的变量称为对象指针,支持多态。

Date d;          // 对象
Date* p = &d;    // 对象指针
p->setYear(2025);

// 多态用法
Base* base = new Derived();  // 基类指针指向派生类对象
base->virtualFunc();         // 动态绑定

4.4.6 this指针

定义:C++中每个非静态成员函数都有一个隐式的this指针参数,指向调用该函数的当前对象

this是一个右值指针(不能取地址/赋值),类型为ClassName* const(常量指针,不能改变指向)。

this指针的使用:

  • 成员函数的形参和类属性同名时,使用 this->属性 避免和形参同名。
  • 返回对象本身时使用。函数类型为对象指针,直接返回this,函数类型为对象引用,返回*this
class Student {
    int age;
public:
    void setAge(int age) {
        this->age = age;        // 区分成员变量和局部变量
    }
    Student& grow() {
        age++;
        return *this;           // 返回当前对象引用(支持链式调用)
    }
};
4.4.7 友元

友元函数:使用friend关键字声明的非成员函数,可以访问该类的私有/保护成员。

友元类:一个类声明为另一个类的友元,则该类的所有成员函数都可以访问对方的私有成员。

友元的特点:

  • 友元是单项关系,如果A是B的友元,B不会自动成为A的友元。
  • 友元关系是不可被继承的:如果类 B 是类 A 的友元,且类 C 继承自类 B,那么类 C 不会自动成为 A 类的友元。
  • 友元函数也可以直接将函数体放在类内,但仍然不属于这个类,也不受权限关键字约束。
  • 友元破坏了封装性,应谨慎使用(通常在运算符重载、单元测试中用到)。

class A {
private:
    int secret;
public:
    friend void showSecret(const A& a);  // 友元函数声明
    friend class B;                      // 友元类声明
    // 友元函数可以直接将函数体放在类内,但仍不属于这个类,也不受关键字约束
    friend void display(const A& a) {
	    cout << a.secret << endl;
    }
};

void showSecret(const A& a) { cout << a.secret; }  // 可访问私有成员

class B {
public:
    void accessA(const A& a) { cout << a.secret; }  // B的所有函数都可访问
};
4.4.8 运算符重载

为自定义类型定义运算符的行为,使对象支持+-==等自然语法。运算符重载按重载方式可以分为全局函数重载和成员函数重载。

全局函数重载:

  • 特点:运算符函数定义在类的外部,通常需要声明为类的 friend(友元)以便访问私有成员。
  • 左侧不是自定义类型时(例如 int + obj 或 cout << obj),必须使用全局函数。

成员函数重载:

  • 特点:运算符函数定义在类的内部,因此事实上并不需要传入运算符左侧对象。
  • 适用于左侧为自定义类型,以及C++强制要求的四个运算符: =赋值、[]下标、()函数调用、->成员访问。

注意事项:

  • 不管何种的运算符重载,参与运算的类型中,必须至少有一个参数是用户自定义的类型。
  • 全局函数重载运算符时,参数列表中必须显式包含至少一个用户自定义类型(类或枚举),若参数全为内置类型,编译器将直接拒绝编译。为了保证 C++ 内置类型行为的绝对安全和一致,编译器禁重载两个内置类型。
  • 成员函数重载运算符时,显式参数可以是任何合法类型(包括全为内置类型),因为调用对象(左操作数)必定是用户自定义类型。

不可重载的运算符::(作用域解析),.(成员访问),.*(成员指针),?:(三目),sizeoftypeidstatic_cast等。

核心规则:

  1. 不能改变运算符优先级操作数个数(如+必须二元)。

  2. 赋值运算符=必须为成员函数(默认生成)。

  3. []()->=必须为成员函数。

  4. <<>>通常重载为友元(左操作数为ostream/istream)。

#include <iostream>
#include <string>
using namespace std;

class Complex {
private:
    double real;
    double imag;

public:
    //  构造函数 
    Complex(double r = 0.0, double i = 0.0);

    //  输入输出运算符(友元) 
    friend ostream& operator<<(ostream& os, const Complex& c);
    friend istream& operator>>(istream& is, Complex& c);

    //  算术运算符(友元,支持左右交换) 
    friend Complex operator+(const Complex& a, const Complex& b);
    friend Complex operator-(const Complex& a, const Complex& b);
    friend Complex operator*(const Complex& a, const Complex& b);
    friend Complex operator/(const Complex& a, const Complex& b);

    //  复合赋值运算符(成员) 
    Complex& operator+=(const Complex& other);
    Complex& operator-=(const Complex& other);
    Complex& operator*=(const Complex& other);
    Complex& operator/=(const Complex& other);

    //  关系运算符(成员) 
    bool operator==(const Complex& other) const;
    bool operator!=(const Complex& other) const;
    bool operator<(const Complex& other) const;   // 按模长比较
    bool operator>(const Complex& other) const;

    //  一元运算符(成员) 
    Complex operator-() const;          // 取负
    Complex operator+() const;          // 取正

    //  自增/自减(成员) 
    Complex& operator++();              // 前置++
    Complex operator++(int);            // 后置++
    Complex& operator--();              // 前置--
    Complex operator--(int);            // 后置--

    //  下标运算符(成员) 
    double& operator[](int index);      // 0→real, 1→imag
    const double& operator[](int index) const;

    //  函数调用运算符(成员,仿函数) 
    double operator()(double r, double i) const;  // 返回模长平方

    //  类型转换运算符(成员)
    operator double() const;            // 转换为模长(double)

    //  获取器(供友元/内部使用) 
    double getReal() const;
    double getImag() const;
};


// 构造函数的实现 
Complex::Complex(double r, double i) : real(r), imag(i) {}


//  输入输出运算符 
ostream& operator<<(ostream& os, const Complex& c) {
    if (c.imag >= 0)
        os << c.real << "+" << c.imag << "i";
    else
        os << c.real << c.imag << "i";
    return os;
}

istream& operator>>(istream& is, Complex& c) {
    is >> c.real >> c.imag;
    return is;
}


//  算术运算符 
Complex operator+(const Complex& a, const Complex& b) {
    return Complex(a.real + b.real, a.imag + b.imag);
}

Complex operator-(const Complex& a, const Complex& b) {
    return Complex(a.real - b.real, a.imag - b.imag);
}

Complex operator*(const Complex& a, const Complex& b) {
    return Complex(a.real * b.real - a.imag * b.imag,
                   a.real * b.imag + a.imag * b.real);
}

Complex operator/(const Complex& a, const Complex& b) {
    double denominator = b.real * b.real + b.imag * b.imag;
    return Complex((a.real * b.real + a.imag * b.imag) / denominator,
                   (a.imag * b.real - a.real * b.imag) / denominator);
}


//  复合赋值运算符 
Complex& Complex::operator+=(const Complex& other) {
    real += other.real;
    imag += other.imag;
    return *this;
}

Complex& Complex::operator-=(const Complex& other) {
    real -= other.real;
    imag -= other.imag;
    return *this;
}

Complex& Complex::operator*=(const Complex& other) {
    *this = *this * other;
    return *this;
}

Complex& Complex::operator/=(const Complex& other) {
    *this = *this / other;
    return *this;
}


//  关系运算符 
bool Complex::operator==(const Complex& other) const {
    return real == other.real && imag == other.imag;
}

bool Complex::operator!=(const Complex& other) const {
    return !(*this == other);
}

bool Complex::operator<(const Complex& other) const {
    return (real * real + imag * imag) < (other.real * other.real + other.imag * other.imag);
}

bool Complex::operator>(const Complex& other) const {
    return other < *this;
}


//  一元运算符 
Complex Complex::operator-() const {
    return Complex(-real, -imag);
}

Complex Complex::operator+() const {
    return *this;
}


//  自增/自减运算符 
Complex& Complex::operator++() {
    ++real;
    return *this;
}

Complex Complex::operator++(int) {
    Complex temp = *this;
    ++real;
    return temp;
}

Complex& Complex::operator--() {
    --real;
    return *this;
}

Complex Complex::operator--(int) {
    Complex temp = *this;
    --real;
    return temp;
}


//  下标运算符 
double& Complex::operator[](int index) {
    if (index == 0) return real;
    if (index == 1) return imag;
    throw out_of_range("索引越界:Complex 只有 0(real) 和 1(imag)");
}

const double& Complex::operator[](int index) const {
    if (index == 0) return real;
    if (index == 1) return imag;
    throw out_of_range("索引越界:Complex 只有 0(real) 和 1(imag)");
}


//  函数调用运算符 
double Complex::operator()(double r, double i) const {
    double dr = real - r;
    double di = imag - i;
    return dr * dr + di * di;  // 返回与点(r,i)的距离平方
}


//  类型转换运算符 
Complex::operator double() const {
    return sqrt(real * real + imag * imag);  // 模长
}


//  获取器 
double Complex::getReal() const {
    return real;
}

double Complex::getImag() const {
    return imag;
}
4.4.9 赋值运算符重载

定义:用于将已存在的对象赋值给另一个已存在的对象(区别于拷贝构造,拷贝构造用于创建新对象)。

如果类中只包含非指针类型属性,使用默认赋值运算即可实现一个对象给另外另外一个对象赋值。如果设计动态内存分配则需要自己定义。

注意事项:

  • 返回值类型,类名&(返回引用,避免拷贝开销)。
  • 参数类型,const 类名&(避免拷贝,常引用接受临时对象)。
  • 自赋值检测,if (this == &other) return *this;(防止a = a时释放自身内存)
  • 必须为成员函数。
  • 继承中的赋值,派生类赋值运算符需显式调用基类赋值运算符:Base::operator=(other);
  • 禁止赋值,将赋值运算符声明为private或不实现,可禁止对象赋值。
#include <iostream>
using namespace std;

class Circle {
private:
	char* name = nullptr;
	int r;
public:
	Circle(const char* name, int r) {
		this->r = r;
		this->name = new char[strlen(name) + 1];
		strcpy(this->name, name);
	}

	// 拷贝构造函数
	Circle(const Circle& circle) {
		r = circle.r;
		name = new char[strlen(circle.name) + 1];
		strcpy(name, circle.name);
	}

	~Circle() {
		delete[] name;
	}

	// 定义赋值函数
	Circle& operator=(Circle& circle) {
		// 判断是否给自己赋值,如果给自己赋值不做任何操作,直接返回自身
		if (this == &circle) {
			return *this;
		}
		// 赋值给已经存在的对象,赋值前要释放动态分配的内存,避免内存泄露
		if (name != nullptr) {
			delete[] name;
			r = circle.r;
			name = new char[strlen(circle.name) + 1];
			strcpy(name, circle.name);
		}
	}

	void setR(int r) {
		this->r = r;
	}

	float getArea() {
		return 3.14 * r * r;
	}
};

int main() {
	Circle c1("C1", 10);
	Circle c2 = c1;   // c2不存在调用拷贝构造函数
	cout << c2.getArea() << endl;

	c2.setR(100);
	cout << c2.getArea() << endl;

	c2 = c1;  // c2已经存在,调用=运算符重载
	cout << c2.getArea() << endl;
	return 0;
}

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐