001 《C++语言纲要:从入门到精通》
🌟🌟🌟本文由Gemini 2.5 Flash Preview 04-17生成,用来辅助学习。🌟🌟🌟
书籍大纲
▮▮ 1. 起步:C++语言和编程环境
▮▮▮▮ 1.1 1.1 C++语言概述
▮▮▮▮▮▮ 1.1.1 1.1.1 C++的历史和演变
▮▮▮▮▮▮ 1.1.2 1.1.2 C++的特点和优势
▮▮▮▮▮▮ 1.1.3 1.1.3 C++的应用领域
▮▮▮▮ 1.2 1.2 开发环境搭建
▮▮▮▮▮▮ 1.2.1 1.2.1 选择C++编译器 (Compiler)
▮▮▮▮▮▮ 1.2.2 1.2.2 安装和配置IDE (Integrated Development Environment)
▮▮▮▮▮▮ 1.2.3 1.2.3 命令行编译和运行
▮▮▮▮ 1.3 1.3 第一个C++程序
▮▮▮▮▮▮ 1.3.1 1.3.1 main()
函数详解
▮▮▮▮▮▮ 1.3.2 1.3.2 输入/输出 (Input/Output) 初探
▮▮▮▮▮▮ 1.3.3 1.3.3 注释 (Comments) 的使用
▮▮ 2. C++基础语法:数据类型、运算符与表达式
▮▮▮▮ 2.1 2.1 基本数据类型 (Basic Data Types)
▮▮▮▮▮▮ 2.1.1 2.1.1 整型 (Integer Types):int
, short
, long
, long long
▮▮▮▮▮▮ 2.1.2 2.1.2 浮点型 (Floating-Point Types):float
, double
, long double
▮▮▮▮▮▮ 2.1.3 2.1.3 字符型 (Character Type):char
▮▮▮▮▮▮ 2.1.4 2.1.4 布尔型 (Boolean Type):bool
▮▮▮▮ 2.2 2.2 变量 (Variables) 和常量 (Constants)
▮▮▮▮▮▮ 2.2.1 2.2.1 变量的声明和初始化
▮▮▮▮▮▮ 2.2.2 2.2.2 变量的作用域 (Scope) 和生命周期 (Lifetime)
▮▮▮▮▮▮ 2.2.3 2.2.3 常量的定义:const
和 constexpr
▮▮▮▮ 2.3 2.3 运算符 (Operators) 与表达式 (Expressions)
▮▮▮▮▮▮ 2.3.1 2.3.1 算术运算符 (Arithmetic Operators)
▮▮▮▮▮▮ 2.3.2 2.3.2 关系运算符 (Relational Operators) 和逻辑运算符 (Logical Operators)
▮▮▮▮▮▮ 2.3.3 2.3.3 赋值运算符 (Assignment Operators) 和复合赋值运算符
▮▮▮▮▮▮ 2.3.4 2.3.4 位运算符 (Bitwise Operators)
▮▮▮▮▮▮ 2.3.5 2.3.5 运算符的优先级 (Precedence) 和结合性 (Associativity)
▮▮▮▮ 2.4 2.4 类型转换 (Type Conversion)
▮▮▮▮▮▮ 2.4.1 2.4.1 隐式类型转换 (Implicit Type Conversion)
▮▮▮▮▮▮ 2.4.2 2.4.2 显式类型转换 (Explicit Type Conversion):强制类型转换
▮▮ 3. 程序流程控制:语句与结构
▮▮▮▮ 3.1 3.1 语句 (Statements) 概述
▮▮▮▮ 3.2 3.2 选择结构:if
语句
▮▮▮▮▮▮ 3.2.1 3.2.1 if
语句
▮▮▮▮▮▮ 3.2.2 3.2.2 if-else
语句
▮▮▮▮▮▮ 3.2.3 3.2.3 if-else if-else
语句
▮▮▮▮▮▮ 3.2.4 3.2.4 嵌套 if
语句
▮▮▮▮ 3.3 3.3 选择结构:switch
语句
▮▮▮▮▮▮ 3.3.1 3.3.1 switch
语句的语法和执行流程
▮▮▮▮▮▮ 3.3.2 3.3.2 switch
语句的应用场景和注意事项
▮▮▮▮ 3.4 3.4 循环结构:while
语句
▮▮▮▮▮▮ 3.4.1 3.4.1 while
循环的语法和执行流程
▮▮▮▮▮▮ 3.4.2 3.4.2 while
循环的应用示例
▮▮▮▮▮▮ 3.4.3 3.4.3 死循环 (Infinite Loop) 和循环控制
▮▮▮▮ 3.5 3.5 循环结构:for
语句
▮▮▮▮▮▮ 3.5.1 3.5.1 for
循环的语法和执行流程
▮▮▮▮▮▮ 3.5.2 3.5.2 for
循环的应用示例
▮▮▮▮▮▮ 3.5.3 3.5.3 范围 for
循环 (Range-based for loop) (C++11)
▮▮▮▮ 3.6 3.6 循环结构:do-while
语句
▮▮▮▮▮▮ 3.6.1 3.6.1 do-while
循环的语法和执行流程
▮▮▮▮▮▮ 3.6.2 3.6.2 do-while
循环的应用场景
▮▮▮▮ 3.7 3.7 跳转语句:break
、continue
和 goto
▮▮▮▮▮▮ 3.7.1 3.7.1 break
语句
▮▮▮▮▮▮ 3.7.2 3.7.2 continue
语句
▮▮▮▮▮▮ 3.7.3 3.7.3 goto
语句 (慎用)
▮▮ 4. 函数:模块化程序设计
▮▮▮▮ 4.1 4.1 函数的定义 (Function Definition) 和声明 (Function Declaration)
▮▮▮▮▮▮ 4.1.1 4.1.1 函数的语法结构
▮▮▮▮▮▮ 4.1.2 4.1.2 函数声明的作用和语法
▮▮▮▮▮▮ 4.1.3 4.1.3 头文件 (Header Files) 和函数声明
▮▮▮▮ 4.2 4.2 函数的调用 (Function Call)
▮▮▮▮▮▮ 4.2.1 4.2.1 函数调用的一般形式
▮▮▮▮▮▮ 4.2.2 4.2.2 实参 (Arguments) 和形参 (Parameters) 的传递
▮▮▮▮▮▮ 4.2.3 4.2.3 函数调用的执行流程和栈帧 (Stack Frame)
▮▮▮▮ 4.3 4.3 参数传递:值传递、引用传递和指针传递
▮▮▮▮▮▮ 4.3.1 4.3.1 值传递 (Pass-by-Value)
▮▮▮▮▮▮ 4.3.2 4.3.2 引用传递 (Pass-by-Reference)
▮▮▮▮▮▮ 4.3.3 4.3.3 指针传递 (Pass-by-Pointer)
▮▮▮▮▮▮ 4.3.4 4.3.4 参数传递方式的选择
▮▮▮▮ 4.4 4.4 函数的返回值 (Return Value)
▮▮▮▮▮▮ 4.4.1 4.4.1 返回值类型和 return
语句
▮▮▮▮▮▮ 4.4.2 4.4.2 函数返回值的接收和处理
▮▮▮▮▮▮ 4.4.3 4.4.3 返回值优化 (Return Value Optimization, RVO) (C++11/17)
▮▮▮▮ 4.5 4.5 函数重载 (Function Overloading)
▮▮▮▮▮▮ 4.5.1 4.5.1 函数重载的定义和规则
▮▮▮▮▮▮ 4.5.2 4.5.2 函数重载的应用场景
▮▮▮▮▮▮ 4.5.3 4.5.3 重载函数的最佳实践和注意事项
▮▮▮▮ 4.6 4.6 递归函数 (Recursive Functions)
▮▮▮▮▮▮ 4.6.1 4.6.1 递归函数的定义和基本结构
▮▮▮▮▮▮ 4.6.2 4.6.2 递归函数的执行流程和栈溢出 (Stack Overflow)
▮▮▮▮▮▮ 4.6.3 4.6.3 递归与迭代 (Iteration) 的比较和选择
▮▮▮▮ 4.7 4.7 内联函数 (Inline Functions)
▮▮▮▮▮▮ 4.7.1 4.7.1 内联函数的定义和特点
▮▮▮▮▮▮ 4.7.2 4.7.2 inline
关键字的使用建议和限制
▮▮▮▮ 4.8 4.8 函数指针 (Function Pointers)
▮▮▮▮▮▮ 4.8.1 4.8.1 函数指针的定义和语法
▮▮▮▮▮▮ 4.8.2 4.8.2 函数指针的赋值和调用
▮▮▮▮▮▮ 4.8.3 4.8.3 函数指针的应用:回调函数
▮▮ 5. 数组和字符串:批量数据处理
▮▮ 6. 指针:内存的直接操纵
▮▮ 7. 结构体、联合体和枚举:自定义数据类型
▮▮ 8. 类和对象:面向对象编程基础
▮▮ 9. 深入类与对象:高级特性
▮▮ 10. 继承与多态:面向对象的核心机制
▮▮ 11. 模板:泛型编程
▮▮ 12. 异常处理:增强程序的健壮性
▮▮ 13. 输入/输出流:文件和控制台操作
▮▮ 14. 标准模板库 (STL):C++的强大工具库
▮▮ 15. 现代C++特性:C++11/14/17/20 新特性概览
▮▮ 16. C++项目实战:综合案例分析
▮▮ 17. C++性能优化和最佳实践
▮▮ 18. C++与其他语言的互操作:混合编程
▮▮ 附录A: 附录A:C++关键字 (Keywords) 列表
▮▮ 附录B: 附录B:运算符优先级 (Operator Precedence) 表
▮▮ 附录C: 附录C:ASCII码表
▮▮ 附录D: 附录D:常用C++开发工具和资源
▮▮ 附录E: 附录E:C++常见错误和调试技巧
1. 起步:C++语言和编程环境
本章介绍C++语言的历史、特点和应用领域,引导读者搭建C++开发环境,并编写第一个C++程序,为后续学习打下基础。
1.1 C++语言概述
介绍C++语言的起源、发展历程、设计哲学及其在软件开发中的地位和作用。
1.1.1 C++的历史和演变
回顾C++从C语言发展而来的历程,以及不同C++标准(如C++98, C++11, C++14, C++17, C++20)的演进和新特性。
C++ 是一种强大而通用的编程语言,它在系统编程、应用开发、游戏开发、嵌入式系统以及高性能计算等领域都占据着重要的地位。要理解 C++ 的全貌,首先需要回顾它的历史和演变过程。C++ 的发展历程与计算机科学的进步紧密相连,了解这段历史能够帮助我们更好地理解 C++ 的设计哲学和特性。
① 起源于C语言: C++ 起源于 C语言 (C Language),最初由 Bjarne Stroustrup 在 20 世纪 80 年代早期在贝尔实验室开发。C++ 最初的名字是 "C with Classes",从名字就可以看出,它是在 C 语言的基础上添加了面向对象编程 (Object-Oriented Programming, OOP) 的特性。C 语言以其高效、灵活和接近硬件的特性而闻名,但随着软件系统复杂性的增加,C 语言在大型项目开发和代码重用方面显露出一些不足。Stroustrup 的目标是在保留 C 语言性能和灵活性的同时,引入 OOP 的概念,以提高程序的可维护性和可扩展性。
② C++98 标准: 1998 年,国际标准化组织 (International Organization for Standardization, ISO) 发布了第一个 C++ 语言的国际标准 ISO/IEC 14882:1998,通常被称为 C++98。C++98 的发布是 C++ 发展史上的一个里程碑,它标志着 C++ 语言的标准化和成熟。C++98 标准确定了 C++ 语言的核心特性,包括类 (classes)、模板 (templates)、标准模板库 (Standard Template Library, STL)、异常处理 (exception handling)、命名空间 (namespaces) 等。STL 的引入极大地提高了 C++ 的编程效率,提供了丰富的通用数据结构和算法。
③ C++03 标准: 2003 年,发布了 C++03 标准,它是对 C++98 标准的一个小幅修订。C++03 并没有引入新的语言特性,主要是修复了 C++98 标准中的一些缺陷和不一致之处,提高了标准的清晰度和一致性。虽然 C++03 在功能上与 C++98 差别不大,但它为后续 C++ 标准的演进奠定了基础。
④ C++11 标准: 2011 年,C++11 标准正式发布,这是 C++ 语言自 C++98 标准以来最重要的一个更新。C++11 引入了大量的新特性,极大地扩展了 C++ 语言的功能,并提升了现代 C++ 编程的效率和安全性。C++11 的主要新特性包括:
▮▮▮▮ⓐ Lambda 表达式 (Lambda Expressions): 允许在代码中定义匿名函数,简化了函数对象的创建和使用,尤其是在 STL 算法中非常方便。
▮▮▮▮ⓑ 右值引用 (Rvalue References) 和移动语义 (Move Semantics): 引入了右值引用的概念,实现了移动语义,避免了不必要的对象拷贝,显著提高了性能,尤其是在处理大型对象时。
▮▮▮▮ⓒ auto
类型推导 (Type Inference): 允许编译器自动推导变量的类型,简化了代码,提高了代码的可读性,并减少了类型错误。
▮▮▮▮ⓓ 范围 for
循环 (Range-based for loop): 提供了一种更简洁的方式来遍历容器和数组,提高了代码的可读性。
▮▮▮▮ⓔ 智能指针 (Smart Pointers): 引入了 std::shared_ptr
, std::unique_ptr
, std::weak_ptr
等智能指针,自动管理动态分配的内存,避免了内存泄漏 (memory leak) 的问题。
▮▮▮▮ⓕ 线程库 (Thread Library): 标准化了多线程编程的支持,提供了 std::thread
, std::mutex
, std::condition_variable
等线程相关的类和函数,使得 C++ 能够更容易地进行并发编程。
▮▮▮▮ⓖ constexpr
: 扩展了常量表达式的概念,允许在编译时进行更多的计算,提高了程序的性能和效率。
⑤ C++14 标准: 2014 年,C++14 标准发布,它是对 C++11 标准的一个 சிறிய扩展。C++14 主要是在 C++11 的基础上进行了一些小的改进和增强,例如:
▮▮▮▮ⓐ 泛型 Lambda 表达式 (Generic Lambdas): Lambda 表达式的参数可以使用 auto
关键字,使其能够接受任意类型的参数。
▮▮▮▮ⓑ 变量模板 (Variable Templates): 允许定义模板变量,提高了泛型编程的灵活性。
▮▮▮▮ⓒ std::make_unique
: 提供了创建 std::unique_ptr
的便捷方法,避免了手动使用 new
和 delete
。
⑥ C++17 标准: 2017 年,C++17 标准发布,它进一步增强了 C++ 语言的功能,引入了更多现代编程的特性。C++17 的主要新特性包括:
▮▮▮▮ⓐ 折叠表达式 (Fold Expressions): 简化了对参数包 (parameter pack) 的操作,使得可以更方便地进行可变参数模板编程。
▮▮▮▮ⓑ 内联变量 (Inline Variables): 允许在头文件中定义内联变量,简化了头文件的编写。
▮▮▮▮ⓒ 结构化绑定 (Structured Bindings): 允许将元组 (tuples)、pair 和结构体 (struct) 的元素直接绑定到变量,提高了代码的可读性。
▮▮▮▮ⓓ if constexpr
: 允许在编译时进行条件判断,根据条件选择编译不同的代码分支,提高了代码的灵活性和效率。
▮▮▮▮ⓔ 文件系统库 (Filesystem Library): 标准化了文件和目录操作的库,提供了跨平台的文件系统操作接口。
▮▮▮▮ⓕ 并行算法 (Parallel Algorithms): STL 算法可以并行执行,利用多核处理器的性能,提高了程序的执行效率。
⑦ C++20 标准: 2020 年,C++20 标准发布,这是 C++ 语言最新的一个主要标准,引入了许多令人兴奋的新特性,进一步提升了 C++ 的现代性和功能性。C++20 的主要新特性包括:
▮▮▮▮ⓐ 概念 (Concepts): 为模板参数添加了约束,提高了模板的类型安全性,并改进了编译错误信息。
▮▮▮▮ⓑ 范围 (Ranges): 提供了一种新的处理数据范围的方式,简化了 STL 算法的使用,提高了代码的可读性和效率。
▮▮▮▮ⓒ 协程 (Coroutines): 引入了协程的概念,允许编写异步和非阻塞的代码,提高了程序的并发性和响应性。
▮▮▮▮ⓓ 模块 (Modules): 改进了代码的模块化机制,替代了传统的头文件包含方式,提高了编译速度,并解决了头文件依赖的问题。
▮▮▮▮ⓔ std::format
: 提供了一种更安全、更方便的格式化输出方式,替代了传统的 printf
和 std::ostream
格式化。
▮▮▮▮ⓕ 日历和时区库 (Calendar and Time Zone Library): 标准化了日期、时间和时区处理的库。
⑧ 持续演进: C++ 语言仍在不断发展演进,C++23 标准正在制定中,并且已经有许多新的提案和特性在讨论和实验中。C++ 标准委员会 (ISO C++ committee) 致力于不断改进和完善 C++ 语言,使其能够更好地适应新的技术发展和应用需求。
总结来说,C++ 的历史演变是一个不断吸收新技术、不断完善自身的过程。从最初的 "C with Classes" 到现代 C++,C++ 始终保持着对性能、效率和灵活性的追求,同时不断引入新的特性以提高编程效率和代码质量。了解 C++ 的历史和演变,有助于我们更好地理解现代 C++ 的设计理念,并能更好地利用 C++ 的强大功能来解决实际问题。
1.1.2 C++的特点和优势
阐述C++语言的关键特性,例如面向对象、性能高效、丰富的库支持等,以及它在系统编程、游戏开发、高性能计算等领域的优势。
C++ 作为一种被广泛应用的编程语言,拥有众多独特的特点和优势,使其在众多领域中脱颖而出。理解这些特点和优势,有助于我们认识到 C++ 的强大之处,并能更好地选择和使用 C++ 来解决各种编程问题。
① 面向对象编程 (Object-Oriented Programming, OOP): C++ 最显著的特点之一是它对面向对象编程范式的支持。OOP 是一种程序设计方法,它将数据和操作数据的方法组合成 对象 (objects),对象是类的实例。C++ 通过以下关键特性支持 OOP:
▮▮▮▮ⓐ 封装 (Encapsulation): 将数据 (属性) 和操作数据的方法 (成员函数) 捆绑在一起,形成类。通过访问控制 (public, private, protected) 实现信息隐藏,保护数据不受外部的非法访问,提高了代码的安全性和可维护性。
▮▮▮▮ⓑ 继承 (Inheritance): 允许创建一个新的类 (子类或派生类),继承现有类 (父类或基类) 的属性和方法。继承实现了代码的重用,并建立了类之间的层次关系,提高了代码的可扩展性和可维护性。
▮▮▮▮ⓒ 多态 (Polymorphism): 字面意思是“多种形态”。在 OOP 中,多态允许将子类对象当作父类对象来处理,从而实现更灵活的程序设计。C++ 支持两种多态形式:
▮▮▮▮▮▮▮▮❹ 编译时多态 (Compile-time Polymorphism) (或静态多态): 通过函数重载 (function overloading) 和模板 (templates) 实现。编译器在编译时根据参数类型或模板参数确定调用哪个函数。
▮▮▮▮▮▮▮▮❺ 运行时多态 (Runtime Polymorphism) (或动态多态): 通过虚函数 (virtual functions) 和继承实现。在运行时根据对象的实际类型确定调用哪个函数。
② 性能高效: C++ 继承了 C 语言的性能优势,被设计成一种性能非常高的语言。C++ 允许程序员直接访问和操作计算机的硬件资源,例如内存和处理器。这使得 C++ 非常适合开发对性能要求极高的应用程序,例如操作系统、游戏引擎、高性能计算软件等。C++ 的高效性主要体现在以下几个方面:
▮▮▮▮ⓐ 直接内存管理 (Direct Memory Management): C++ 允许程序员手动管理内存,包括动态内存分配和释放。虽然手动内存管理增加了编程的复杂性,但也提供了更高的性能控制,避免了垃圾回收 (garbage collection) 的开销。通过精细的内存管理,可以最大限度地提高程序的运行效率。
▮▮▮▮ⓑ 编译型语言 (Compiled Language): C++ 是一种编译型语言,代码在运行前需要经过编译器的编译,生成机器码 (machine code)。机器码是计算机可以直接执行的二进制指令,执行效率非常高。与解释型语言 (interpreted language) 相比,编译型语言通常具有更高的运行速度。
▮▮▮▮ⓒ 零开销抽象 (Zero-overhead Abstraction): C++ 的设计哲学之一是“零开销抽象”。这意味着 C++ 提供的抽象机制 (如类、模板、虚函数等) 不会引入额外的运行时开销,或者开销非常小。程序员可以使用这些抽象机制来提高代码的模块化和可维护性,而不用担心性能损失。
▮▮▮▮ⓓ 内联函数 (Inline Functions): C++ 支持内联函数,可以减少函数调用的开销。对于一些短小的、频繁调用的函数,可以声明为内联函数,编译器会将函数调用直接替换为函数体代码,从而提高程序的执行效率。
③ 丰富的库支持: C++ 拥有非常丰富的库生态系统,包括标准库 (Standard Library) 和大量的第三方库。这些库提供了各种各样的功能,涵盖了文件操作、网络编程、图形界面、数据库访问、科学计算、机器学习 (machine learning) 等多个领域。丰富的库支持大大提高了 C++ 的开发效率,程序员可以利用现有的库来快速构建复杂的应用程序。
▮▮▮▮ⓐ 标准模板库 (STL): STL 是 C++ 标准库的核心组成部分,提供了通用的模板类和模板函数,包括容器 (containers)、迭代器 (iterators)、算法 (algorithms)、函数对象 (function objects) 等。STL 提供了一套高效、可重用的数据结构和算法,例如 std::vector
, std::list
, std::map
, std::sort
, std::find
等。使用 STL 可以大大简化 C++ 程序的开发,提高代码的质量和性能。
▮▮▮▮ⓑ Boost 库: Boost 是一个高质量、开源、跨平台的 C++ 库集合,被誉为“准标准库”。Boost 库提供了许多 C++ 标准库中尚未包含的功能,例如智能指针、正则表达式 (regular expressions)、多线程、日期时间、图形、测试框架等。Boost 库对 C++ 标准的发展有重要的影响,许多 Boost 库的组件最终被吸收到 C++ 标准中。
▮▮▮▮ⓒ 第三方库: 除了 STL 和 Boost 库之外,还有大量的优秀的第三方 C++ 库,例如:
▮▮▮▮⚝ Qt: 一个跨平台的应用程序开发框架,主要用于 GUI (图形用户界面) 程序的开发,也提供了网络、数据库、多媒体等功能。
▮▮▮▮⚝ SFML (Simple and Fast Multimedia Library): 一个简单的、快速的、跨平台的多媒体库,主要用于 2D 游戏和多媒体应用程序的开发。
▮▮▮▮⚝ OpenGL: 一个跨平台的图形 API (应用程序编程接口),用于 2D 和 3D 图形渲染。
▮▮▮▮⚝ Eigen: 一个高性能的 C++ 线性代数库,用于矩阵运算、向量运算、数值求解等。
▮▮▮▮⚝ TensorFlow 和 PyTorch: 流行的机器学习框架,提供了 C++ API,用于构建和训练机器学习模型。
④ 跨平台性 (Cross-platform): C++ 是一种跨平台的语言。C++ 代码可以在不同的操作系统 (如 Windows, macOS, Linux, Android, iOS) 和硬件平台上编译和运行。C++ 的跨平台性主要得益于 C++ 标准的规范和各种 C++ 编译器的支持。程序员可以使用 C++ 开发跨平台的应用程序,减少了为不同平台编写不同代码的工作量。
⑤ 强大的社区支持: C++ 拥有庞大而活跃的开发者社区。在网上可以找到大量的 C++ 学习资源、教程、文档、示例代码和开源项目。开发者可以通过社区论坛、博客、Stack Overflow 等平台获取帮助、交流经验和分享知识。强大的社区支持为 C++ 的学习和应用提供了有力的保障。
⑥ 向后兼容性 (Backward Compatibility): C++ 在标准演进的过程中,非常注重向后兼容性。这意味着用旧的 C++ 标准编写的代码,通常可以用新的 C++ 标准的编译器编译和运行,而不需要做太多的修改。向后兼容性保护了已有的 C++ 代码投资,使得 C++ 能够平稳地向前发展。
C++ 的应用领域优势: 由于 C++ 具有上述特点和优势,它在许多应用领域都表现出色:
▮▮▮▮ⓐ 系统编程: C++ 非常适合系统编程,例如操作系统、设备驱动程序、嵌入式系统等。C++ 能够直接访问硬件资源,性能高效,能够编写底层的、对性能要求极高的系统软件。
▮▮▮▮ⓑ 游戏开发: 游戏开发是 C++ 的一个重要应用领域。大型 3D 游戏通常使用 C++ 作为主要的编程语言,因为 C++ 能够提供高性能的图形渲染、物理模拟、游戏逻辑等功能。许多流行的游戏引擎 (如 Unreal Engine, Unity) 都是用 C++ 开发的。
▮▮▮▮ⓒ 高性能计算 (High-Performance Computing, HPC): 在科学计算、工程计算、金融分析等领域,高性能计算至关重要。C++ 的性能优势使其成为高性能计算的首选语言之一。许多高性能计算库 (如 MPI, OpenMP) 都是用 C++ 开发的。
▮▮▮▮ⓓ 嵌入式系统: 嵌入式系统通常对资源 (如内存、处理器) 有严格的限制,同时对性能和实时性有较高的要求。C++ 的高效性和灵活性使其能够很好地满足嵌入式系统的需求。C++ 被广泛应用于汽车电子、智能设备、工业控制等嵌入式领域。
▮▮▮▮ⓔ 金融软件: 金融软件通常需要处理大量的交易数据,对性能、稳定性和安全性有极高的要求。C++ 的高性能和可靠性使其成为开发金融交易系统、风险管理系统等金融软件的重要选择。
▮▮▮▮ⓕ 数据库: 许多高性能数据库系统 (如 MySQL, PostgreSQL) 的核心部分是用 C++ 开发的。C++ 的性能优势使其能够有效地处理海量数据,并提供快速的数据访问和查询服务。
总而言之,C++ 以其面向对象特性、高性能、丰富的库支持和跨平台性等优势,成为一种在众多领域都具有强大竞争力的编程语言。掌握 C++,就掌握了一把解决各种复杂编程问题的利器。
1.1.3 C++的应用领域
列举C++在操作系统、数据库、游戏引擎、嵌入式系统、金融软件等多个领域的广泛应用案例。
C++ 语言以其高性能、灵活性和强大的功能,在众多领域都得到了广泛的应用。从底层的系统软件到上层的应用程序,从桌面应用到移动应用,从游戏开发到人工智能 (Artificial Intelligence, AI),C++ 都扮演着重要的角色。以下列举 C++ 在一些主要应用领域的具体案例,以展示 C++ 的广泛应用和强大实力。
① 操作系统 (Operating Systems): 操作系统是计算机系统的核心软件,负责管理计算机的硬件资源和软件资源,为用户提供运行应用程序的环境。由于操作系统对性能、稳定性和资源管理有极高的要求,C++ (以及 C 语言) 成为了开发操作系统的首选语言。
▮▮▮▮ⓐ Windows: 微软 Windows 操作系统的大部分核心组件都是用 C++ 开发的。Windows 的内核、驱动程序、系统服务、图形界面 (GUI) 等关键部分都使用了 C++。C++ 的高性能和面向对象特性使得 Windows 能够实现复杂的功能和良好的用户体验。
▮▮▮▮ⓑ macOS: 苹果 macOS 操作系统 (以前称为 OS X) 也是大量使用 C++ 和 Objective-C 开发的。macOS 的内核 (XNU 内核)、图形框架 (Cocoa)、应用程序框架等都使用了 C++。C++ 的性能和跨平台能力使得 macOS 能够运行在苹果的硬件平台上,并提供丰富的应用程序生态系统。
▮▮▮▮ⓒ Linux: Linux 操作系统内核主要使用 C 语言编写,但也有一部分组件和应用程序使用 C++ 开发。Linux 的桌面环境 (如 KDE, GNOME) 和许多系统工具、服务器软件 (如 Apache, MySQL) 都使用了 C++。C++ 的开源生态系统和跨平台性使得 Linux 成为服务器、嵌入式系统和开发平台的理想选择。
② 数据库 (Databases): 数据库管理系统 (DBMS) 负责存储、管理和检索数据,是现代信息系统的基石。高性能数据库需要处理海量数据和高并发访问,对性能和可靠性要求极高。C++ 因其性能优势和底层控制能力,成为开发高性能数据库的首选语言。
▮▮▮▮ⓐ MySQL: MySQL 是世界上最流行的开源关系型数据库管理系统之一。MySQL 的服务器端程序 (mysqld) 主要使用 C++ 开发,以实现高性能的数据存储、查询和事务处理。C++ 的性能和可移植性使得 MySQL 能够处理大规模的数据,并运行在各种平台上。
▮▮▮▮ⓑ PostgreSQL: PostgreSQL 也是一个强大的开源关系型数据库管理系统,被认为是功能最丰富的开源数据库之一。PostgreSQL 的服务器端程序 (postgres) 主要使用 C 语言开发,但其客户端库 (libpqxx) 和许多扩展模块使用 C++ 开发。C++ 的面向对象特性和扩展能力使得 PostgreSQL 能够灵活地扩展功能,满足各种应用需求。
▮▮▮▮ⓒ MongoDB: MongoDB 是一个流行的开源 NoSQL 数据库,采用文档型数据模型。MongoDB 的服务器端程序 (mongod) 主要使用 C++ 开发,以实现高性能的数据存储、查询和分布式处理。C++ 的性能和并发处理能力使得 MongoDB 能够处理海量非结构化数据,并支持高并发访问。
③ 游戏引擎 (Game Engines): 游戏引擎是用于开发电子游戏的软件框架,提供了图形渲染、物理模拟、音频处理、碰撞检测、脚本支持等功能。游戏通常对性能和图形效果有极高的要求,C++ 成为了游戏引擎开发的主流语言。
▮▮▮▮ⓐ Unreal Engine: Unreal Engine (虚幻引擎) 是世界上最流行的商业游戏引擎之一,被广泛用于开发各种类型的游戏,包括 3D 游戏、VR (虚拟现实) 游戏、AR (增强现实) 游戏等。Unreal Engine 的核心部分完全使用 C++ 开发,提供了强大的图形渲染能力、物理引擎 (PhysX)、动画系统、AI 系统等。C++ 的性能和灵活性使得 Unreal Engine 能够开发出视觉效果惊人的游戏。
▮▮▮▮ⓑ Unity: Unity 也是一个非常流行的跨平台游戏引擎,被广泛用于开发 2D 和 3D 游戏、移动游戏、VR/AR 应用等。Unity 的引擎核心部分使用 C++ 开发,但其脚本语言主要使用 C#。C++ 的性能和跨平台能力使得 Unity 能够支持多平台发布,并提供高性能的游戏运行。
▮▮▮▮ⓒ নিজস্ব游戏引擎: 许多大型游戏公司也会选择自行开发游戏引擎,以满足特定的游戏需求和技术积累。这些自研游戏引擎通常也使用 C++ 作为主要的开发语言,例如 EA 的 Frostbite Engine, Ubisoft 的 Anvil Engine 等。C++ 的底层控制能力和性能优势使得游戏开发者能够最大限度地优化游戏引擎的性能和效果。
④ 嵌入式系统 (Embedded Systems): 嵌入式系统是指嵌入到其他设备中的计算机系统,例如智能手机、汽车电子、工业控制系统、医疗设备等。嵌入式系统通常对资源 (如内存、处理器) 有限制,同时对性能和实时性有要求。C++ 的高效性和灵活性使其能够很好地满足嵌入式系统的需求。
▮▮▮▮ⓐ 汽车电子: 现代汽车中使用了大量的嵌入式系统,例如汽车电子控制单元 (ECU)、车载信息娱乐系统、自动驾驶系统等。这些系统通常使用 C++ 开发,以实现实时的控制、数据处理和通信。C++ 的性能和可靠性对于汽车安全至关重要。
▮▮▮▮ⓑ 智能手机: 智能手机操作系统 (如 Android, iOS) 的底层驱动程序、系统服务和一部分应用程序使用 C++ 开发。C++ 的性能和跨平台能力使得智能手机能够运行复杂的应用程序,并提供流畅的用户体验。Android NDK (Native Development Kit) 允许开发者使用 C++ 开发高性能的 Android 应用。
▮▮▮▮ⓒ 物联网 (Internet of Things, IoT) 设备: 物联网设备包括智能家居设备、可穿戴设备、传感器网络等。这些设备通常资源受限,但需要高性能和低功耗。C++ 的高效性和底层控制能力使其成为开发物联网设备的理想选择。例如,许多嵌入式 Linux 系统和实时操作系统 (RTOS) 使用 C++ 开发。
⑤ 金融软件 (Financial Software): 金融行业对软件的性能、稳定性和安全性有极高的要求。金融交易系统、风险管理系统、高频交易系统等需要处理大量的交易数据,并进行复杂的计算和分析。C++ 的高性能和可靠性使其成为开发金融软件的重要选择。
▮▮▮▮ⓐ 高频交易系统 (High-Frequency Trading, HFT): 高频交易系统需要在毫秒甚至微秒级别内完成交易决策和执行,对延迟 (latency) 和吞吐量 (throughput) 有极高的要求。C++ 的性能优势和底层控制能力使其成为开发 HFT 系统的首选语言。HFT 系统通常使用 C++ 编写高性能的交易引擎、市场数据处理程序和风险管理模块。
▮▮▮▮ⓑ 风险管理系统: 金融机构需要使用风险管理系统来评估和控制各种金融风险,例如市场风险、信用风险、操作风险等。风险管理系统通常需要进行复杂的数学建模和数值计算,C++ 的高性能和科学计算库 (如 Eigen) 使其能够胜任这些任务。
▮▮▮▮ⓒ 交易平台: 股票交易所、期货交易所、加密货币交易所等交易平台的核心系统通常使用 C++ 开发,以实现高性能的交易撮合、订单管理、市场数据发布等功能。C++ 的并发处理能力和网络编程库 (如 Boost.Asio) 使其能够构建高并发、低延迟的交易平台。
⑥ 高性能计算 (High-Performance Computing, HPC): 高性能计算是指使用并行计算技术解决复杂的科学和工程问题的计算领域,例如气象预报、气候模拟、生物信息学、材料科学、流体力学等。HPC 应用通常需要处理大规模的数据和进行复杂的计算,对性能要求极高。C++ 的性能优势和并行计算库使其成为 HPC 的重要语言。
▮▮▮▮ⓐ 科学计算库: 许多高性能科学计算库使用 C++ 开发,例如:
▮▮▮▮⚝ PETSc (Portable, Extensible Toolkit for Scientific Computation): 用于求解偏微分方程的并行数值库。
▮▮▮▮⚝ Trilinos: 用于求解大规模科学和工程问题的开源软件库。
▮▮▮▮⚝ deal.II: 用于自适应有限元分析的 C++ 库。
▮▮▮▮⚝ Armadillo: 用于线性代数和科学计算的 C++ 库。
▮▮▮▮⚝ Eigen: 用于线性代数运算的高性能 C++ 库。
▮▮▮▮ⓑ 并行计算框架: C++ 也常用于开发并行计算框架,例如:
▮▮▮▮⚝ MPI (Message Passing Interface): 用于分布式内存并行计算的标准库。
▮▮▮▮⚝ OpenMP (Open Multi-Processing): 用于共享内存并行计算的 API。
▮▮▮▮⚝ CUDA (Compute Unified Device Architecture) 和 OpenCL (Open Computing Language): 用于 GPU (图形处理器) 并行计算的平台和框架。
除了上述领域,C++ 还在编译器开发、图像处理、音视频处理、网络编程、人工智能 (机器学习、深度学习) 等领域有广泛的应用。例如,Google 的 Chromium 浏览器、Adobe 的 Photoshop 图像处理软件、VLC 媒体播放器、TensorFlow 和 PyTorch 机器学习框架等,都大量使用了 C++。
总而言之,C++ 语言凭借其卓越的性能、强大的功能和广泛的适用性,在计算机科学和信息技术领域扮演着至关重要的角色。无论是开发底层的系统软件,还是构建上层的应用程序,C++ 都是一个值得信赖和选择的编程语言。掌握 C++,意味着拥有了解决各种复杂问题的强大工具,并能站在技术的最前沿,参与到各行各业的创新和发展中。
1.2 开发环境搭建
指导读者选择和安装C++编译器(如GCC, Clang, MSVC)和集成开发环境 (IDE)(如VS Code, Visual Studio, CLion),配置基本的开发环境。
要开始 C++ 编程之旅,首先需要搭建一个合适的开发环境。开发环境主要包括 C++ 编译器 (C++ Compiler) 和 集成开发环境 (Integrated Development Environment, IDE)。编译器负责将 C++ 源代码翻译成计算机可以执行的机器码,而 IDE 则提供代码编辑、编译、调试等一系列开发工具,提高开发效率。本节将指导读者选择和安装 C++ 编译器和 IDE,并配置基本的开发环境。
1.2.1 选择C++编译器 (Compiler)
介绍主流C++编译器的特点和适用场景,例如GCC的跨平台性、Clang的错误提示友好、MSVC在Windows平台的集成性。
C++ 编译器是将 C++ 源代码转换成可执行程序的关键工具。选择合适的 C++ 编译器是搭建开发环境的首要步骤。目前,有多种优秀的 C++ 编译器可供选择,它们各有特点和适用场景。以下介绍几种主流的 C++ 编译器:
① GCC (GNU Compiler Collection): GCC (GNU Compiler Collection) 是一套由 GNU 项目开发的编程语言编译器,最初用于编译 C 语言,后来扩展支持 C++, Fortran, Java, Ada, Go 等多种编程语言。GCC 是开源、免费且跨平台的,可以在各种操作系统上运行,包括 Linux, macOS, Windows 等。GCC 是 Linux 系统下默认的编译器,也是许多开源项目和跨平台项目的首选编译器。
▮▮▮▮ⓐ 特点:
▮▮▮▮⚝ 跨平台性: GCC 支持多种操作系统和硬件平台,具有良好的跨平台性。
▮▮▮▮⚝ 开源免费: GCC 是开源软件,遵循 GPL 协议,可以免费使用和修改。
▮▮▮▮⚝ 成熟稳定: GCC 经过多年的发展和广泛应用,已经非常成熟和稳定。
▮▮▮▮⚝ 支持多种语言: GCC 不仅支持 C++,还支持多种其他编程语言。
▮▮▮▮⚝ 编译优化: GCC 提供了丰富的编译优化选项,可以生成高性能的可执行程序。
▮▮▮▮⚝ 标准兼容性: GCC 对 C++ 标准的支持良好,能够支持最新的 C++ 标准特性。
▮▮▮▮ⓑ 适用场景:
▮▮▮▮⚝ Linux 开发: GCC 是 Linux 系统下默认的编译器,是 Linux 开发的首选。
▮▮▮▮⚝ 跨平台开发: 如果需要开发跨平台的 C++ 应用程序,GCC 是一个很好的选择。
▮▮▮▮⚝ 开源项目: 许多开源项目使用 GCC 作为编译器,参与开源项目开发通常需要使用 GCC。
▮▮▮▮⚝ 学习和教学: GCC 是免费且易于获取的,适合学习和教学使用。
② Clang: Clang 是一个由 LLVM 项目开发的 C、C++、Objective-C 和 Objective-C++ 编译器前端。Clang 的目标是提供更快的编译速度、更好的错误提示和更模块化的设计。Clang 通常与 LLVM 后端一起使用,LLVM 是一套模块化的编译器工具链。Clang 也是开源、免费且跨平台的,可以在 Linux, macOS, Windows 等操作系统上运行。Clang 是 macOS 系统下默认的编译器,也越来越受到 Windows 和 Linux 开发者的欢迎。
▮▮▮▮ⓐ 特点:
▮▮▮▮⚝ 编译速度快: Clang 的编译速度通常比 GCC 更快,尤其是在大型项目中。
▮▮▮▮⚝ 错误提示友好: Clang 的错误和警告信息非常清晰和详细,有助于开发者快速定位和修复错误。
▮▮▮▮⚝ 模块化设计: Clang 采用模块化设计,易于扩展和集成到其他工具中。
▮▮▮▮⚝ 现代 C++ 支持: Clang 对现代 C++ 标准 (C++11/14/17/20) 的支持非常迅速和完整。
▮▮▮▮⚝ 静态分析: Clang 提供了强大的静态分析工具,可以帮助开发者在编译时发现潜在的代码缺陷。
▮▮▮▮⚝ 与 IDE 集成: Clang 与许多流行的 IDE (如 VS Code, CLion) 集成良好。
▮▮▮▮ⓑ 适用场景:
▮▮▮▮⚝ macOS 开发: Clang 是 macOS 系统下默认的编译器,是 macOS 开发的首选。
▮▮▮▮⚝ 追求编译速度: 如果项目编译时间较长,可以考虑使用 Clang 以提高编译速度。
▮▮▮▮⚝ 需要友好的错误提示: Clang 的错误提示信息有助于提高开发效率,尤其适合初学者。
▮▮▮▮⚝ 现代 C++ 开发: 如果项目使用了较新的 C++ 标准特性,Clang 的支持可能更好。
▮▮▮▮⚝ 代码静态分析: Clang 的静态分析工具可以提高代码质量。
③ MSVC (Microsoft Visual C++): MSVC (Microsoft Visual C++) 是微软 Visual Studio 集成开发环境 (IDE) 自带的 C++ 编译器。MSVC 主要用于 Windows 平台上的 C++ 开发,但也支持编译生成 Linux 和 macOS 平台的可执行程序 (通过 Visual Studio 的跨平台功能)。MSVC 是商业编译器,但微软提供了免费的 Visual Studio Community 版本,其中包含了 MSVC 编译器,可以免费用于学习、个人和小型团队的开发。
▮▮▮▮ⓐ 特点:
▮▮▮▮⚝ Windows 平台集成性: MSVC 与 Windows 操作系统和 Visual Studio IDE 集成度非常高,是 Windows 平台 C++ 开发的最佳选择。
▮▮▮▮⚝ Visual Studio IDE: MSVC 通常与 Visual Studio IDE 一起使用,Visual Studio 提供了强大的代码编辑、调试、项目管理等功能。
▮▮▮▮⚝ 性能优化: MSVC 针对 Windows 平台进行了优化,可以生成高性能的 Windows 可执行程序。
▮▮▮▮⚝ 标准兼容性: MSVC 对 C++ 标准的支持也在不断完善,逐渐追赶 GCC 和 Clang。
▮▮▮▮⚝ 商业支持: MSVC 是商业编译器,可以获得微软的官方技术支持 (商业版 Visual Studio)。
▮▮▮▮ⓑ 适用场景:
▮▮▮▮⚝ Windows 开发: MSVC 是 Windows 平台 C++ 开发的首选编译器。
▮▮▮▮⚝ Visual Studio IDE 用户: 如果习惯使用 Visual Studio IDE,MSVC 是最自然的选择。
▮▮▮▮⚝ Windows 平台性能优化: 如果需要针对 Windows 平台进行性能优化,MSVC 可能更具优势。
▮▮▮▮⚝ 商业项目: 对于商业项目,如果需要商业支持,可以考虑使用商业版的 Visual Studio 和 MSVC。
④ 其他编译器: 除了上述主流编译器,还有一些其他的 C++ 编译器,例如:
▮▮▮▮ⓐ Intel C++ Compiler: 英特尔 C++ 编译器,由英特尔公司开发,主要针对英特尔处理器进行优化,可以生成在英特尔平台上性能更高的可执行程序。Intel C++ Compiler 也支持跨平台编译。
▮▮▮▮ⓑ IBM XL C/C++ Compiler: IBM XL C/C++ 编译器,由 IBM 公司开发,主要用于 IBM 的 AIX 和 Linux 操作系统,以及 IBM Power 处理器架构。
▮▮▮▮ⓒ PGI (Portland Group) Compiler: PGI 编译器,由 NVIDIA 公司收购,主要用于高性能计算和 GPU 加速计算。
如何选择编译器: 选择 C++ 编译器时,可以考虑以下因素:
⚝ 操作系统: 如果在 Windows 平台开发,MSVC 是一个很好的选择。如果在 macOS 平台开发,Clang 是默认选择。如果在 Linux 平台开发,GCC 或 Clang 都是不错的选择。
⚝ 项目需求: 如果项目需要跨平台,GCC 或 Clang 的跨平台性更好。如果项目对编译速度有要求,Clang 可能更快。如果项目需要针对特定硬件平台优化,可以考虑 Intel C++ Compiler 或 PGI Compiler。
⚝ IDE 偏好: 如果习惯使用 Visual Studio IDE,MSVC 是最自然的搭配。如果喜欢轻量级的 IDE (如 VS Code) 或跨平台 IDE (如 CLion),GCC 或 Clang 都可以很好地集成。
⚝ 学习目的: 对于初学者,GCC 或 Clang 都是不错的选择,它们免费、易于获取,并且有广泛的社区支持。
在大多数情况下,GCC, Clang, MSVC 这三种编译器已经能够满足绝大多数 C++ 开发需求。对于初学者,建议根据自己的操作系统选择合适的编译器,并开始搭建开发环境。
1.2.2 安装和配置IDE (Integrated Development Environment)
详细步骤指导读者安装和配置常用的C++ IDE,并演示如何创建和管理C++项目。
集成开发环境 (IDE) 为程序员提供了代码编辑、编译、调试、项目管理等一站式开发工具,可以显著提高开发效率。对于 C++ 开发,有许多优秀的 IDE 可供选择。以下介绍几款常用的 C++ IDE,并指导读者安装和配置 IDE,以及创建和管理 C++ 项目。
① Visual Studio (Windows): Visual Studio 是微软开发的一款功能强大的 IDE,主要用于 Windows 平台上的软件开发。Visual Studio 提供了全面的 C++ 开发支持,包括代码编辑器、MSVC 编译器、调试器、项目管理器、GUI 设计器 (对于 Windows 桌面应用) 等。Visual Studio Community 版本是免费的,适合学习和个人开发。
▮▮▮▮ⓐ 安装:
▮▮▮▮▮▮▮▮❷ 访问 Visual Studio 官网 https://visualstudio.microsoft.com/zh-hans/,下载 Visual Studio Community 版本。
▮▮▮▮▮▮▮▮❸ 运行下载的安装程序,选择 "C++ 桌面开发" 工作负载 (Workload)。你也可以根据需要选择其他工作负载,例如 "通用 Windows 平台开发" (用于 UWP 应用) 或 "使用 C++ 的游戏开发" (用于游戏开发)。
▮▮▮▮▮▮▮▮❹ 选择安装位置和其他可选组件,点击 "安装" 开始安装。安装过程可能需要一段时间,取决于网络速度和选择的组件。
▮▮▮▮▮▮▮▮❺ 安装完成后,启动 Visual Studio。
▮▮▮▮ⓑ 配置: Visual Studio 安装完成后,通常不需要额外的配置即可进行 C++ 开发。MSVC 编译器已经默认配置好。
▮▮▮▮ⓒ 创建和管理项目:
▮▮▮▮▮▮▮▮❷ 启动 Visual Studio,点击 "创建新项目"。
▮▮▮▮▮▮▮▮❸ 在项目模板列表中,选择 "空项目" (Empty Project) 或 "Windows 桌面应用程序" (Windows Desktop Application) 等 C++ 项目模板。
▮▮▮▮▮▮▮▮❹ 输入项目名称、位置等信息,点击 "创建"。
▮▮▮▮▮▮▮▮❺ 在 "解决方案资源管理器" (Solution Explorer) 中,右键点击 "源文件" (Source Files) 文件夹,选择 "添加" -> "新建项"。
▮▮▮▮▮▮▮▮❻ 在 "添加新项" 对话框中,选择 "C++ 文件 (.cpp)",输入文件名 (例如 main.cpp
),点击 "添加"。
▮▮▮▮▮▮▮▮❼ 在代码编辑器中输入 C++ 代码,例如:
1
#include <iostream>
2
3
int main() {
4
std::cout << "Hello, Visual Studio!" << std::endl;
5
return 0;
6
}
▮▮▮▮▮▮▮▮❼ 按下 Ctrl + Shift + B
(或点击 "生成" -> "生成解决方案") 编译项目。
▮▮▮▮▮▮▮▮❽ 按下 Ctrl + F5
(或点击 "调试" -> "开始执行不调试") 运行程序。程序输出 "Hello, Visual Studio!" 到控制台。
② VS Code (跨平台): VS Code (Visual Studio Code) 是微软开发的一款轻量级但功能强大的代码编辑器,支持多种编程语言,包括 C++。VS Code 本身不是 IDE,但通过安装 C++ 扩展和配置 C++ 编译器,可以将其配置成一个功能完善的 C++ IDE。VS Code 是跨平台的,可以在 Windows, macOS, Linux 等操作系统上运行。
▮▮▮▮ⓐ 安装:
▮▮▮▮▮▮▮▮❷ 访问 VS Code 官网 https://code.visualstudio.com/,下载适合你操作系统的版本并安装。
▮▮▮▮▮▮▮▮❸ 启动 VS Code,点击左侧边栏的 "扩展" 图标 (或按下 Ctrl + Shift + X
),搜索 "C++",安装 Microsoft 提供的 "C/C++" 扩展。这个扩展提供了 C++ 语言支持、代码补全、语法高亮、调试等功能。
▮▮▮▮ⓑ 配置编译器: VS Code 本身不包含 C++ 编译器,需要先安装 C++ 编译器 (如 GCC, Clang, MSVC)。
▮▮▮▮⚝ Windows: 可以安装 MinGW-w64 (包含 GCC) 或 Visual Studio (包含 MSVC)。如果安装 MinGW-w64,需要将 MinGW-w64 的 bin
目录添加到系统环境变量 Path
中。
▮▮▮▮⚝ macOS: macOS 默认安装了 Clang 编译器。
▮▮▮▮⚝ Linux: 大多数 Linux 发行版默认安装了 GCC 编译器。
配置 VS Code 使用的编译器:
▮▮▮▮▮▮▮▮❶ 在 VS Code 中按下 Ctrl + Shift + P
(或 Cmd + Shift + P
在 macOS 上),输入 "C/C++: 编辑配置(UI)",打开 C/C++ 配置界面。
▮▮▮▮▮▮▮▮❷ 在 "编译器路径" (Compiler path) 中,输入你的 C++ 编译器的路径 (例如 g++
或 clang++
或 MSVC 的 cl.exe
路径)。
▮▮▮▮▮▮▮▮❸ 可以根据需要配置其他选项,例如 "IntelliSense 模式" (IntelliSense Mode)、"包含路径" (Include path)、"定义" (Defines) 等。
▮▮▮▮ⓒ 创建和管理项目:
▮▮▮▮▮▮▮▮❷ 在 VS Code 中,点击 "文件" -> "打开文件夹",选择一个文件夹作为项目根目录 (或创建一个新文件夹)。
▮▮▮▮▮▮▮▮❸ 在项目根目录下,创建一个新的 C++ 源文件 (例如 main.cpp
)。
▮▮▮▮▮▮▮▮❹ 输入 C++ 代码,例如:
1
#include <iostream>
2
3
int main() {
4
std::cout << "Hello, VS Code!" << std::endl;
5
return 0;
6
}
▮▮▮▮▮▮▮▮❹ 配置编译和运行任务:按下 Ctrl + Shift + P
,输入 "任务: 配置任务",选择 "创建 tasks.json 文件,通过模板",然后选择 "C++ (g++) 生成和调试活动文件" 或 "C++ (clang++) 生成和调试活动文件" 或 "C++ (MSVC) 生成和调试活动文件" (根据你安装的编译器选择)。VS Code 会在项目根目录下的 .vscode
文件夹中创建 tasks.json
文件,其中定义了编译和运行任务。
▮▮▮▮▮▮▮▮❺ 修改 tasks.json
文件 (如果需要)。例如,可以修改编译器选项、编译参数等。
▮▮▮▮▮▮▮▮❻ 编译程序:按下 Ctrl + Shift + B
(或点击 "终端" -> "运行任务",选择编译任务)。
▮▮▮▮▮▮▮▮❼ 运行程序:按下 Ctrl + Shift + P
,输入 "运行 C/C++ 文件"。或者,可以在终端中手动运行编译生成的可执行文件。
③ CLion (跨平台): CLion 是 JetBrains 公司开发的一款专门用于 C 和 C++ 开发的跨平台 IDE。CLion 基于 IntelliJ IDEA 平台,提供了智能代码编辑器、CMake 项目管理、强大的调试器、代码分析工具等。CLion 是商业 IDE,但提供了 30 天的免费试用期,对于学生和教师可能提供教育许可证。
▮▮▮▮ⓐ 安装:
▮▮▮▮▮▮▮▮❷ 访问 JetBrains CLion 官网 https://www.jetbrains.com/clion/,下载适合你操作系统的 CLion 安装程序。
▮▮▮▮▮▮▮▮❸ 运行安装程序,按照提示完成安装。
▮▮▮▮ⓑ 配置编译器: CLion 通常会自动检测系统上已安装的 C++ 编译器 (如 GCC, Clang, MSVC)。如果没有检测到编译器,或者需要手动配置编译器,可以在 CLion 的设置中进行配置 ("File" -> "Settings" 或 "CLion" -> "Preferences" 在 macOS 上,然后选择 "Build, Execution, Deployment" -> "Toolchains")。
▮▮▮▮ⓒ 创建和管理项目: CLion 使用 CMake 作为项目管理工具。
▮▮▮▮▮▮▮▮❷ 启动 CLion,点击 "Create New Project"。
▮▮▮▮▮▮▮▮❸ 选择 "C++ Executable" 项目类型。
▮▮▮▮▮▮▮▮❹ 输入项目名称、位置等信息,点击 "Create"。CLion 会自动生成 CMake 项目文件 (CMakeLists.txt
) 和一个默认的 main.cpp
源文件。
▮▮▮▮▮▮▮▮❺ 在 main.cpp
中输入 C++ 代码,例如:
1
#include <iostream>
2
3
int main() {
4
std::cout << "Hello, CLion!" << std::endl;
5
return 0;
6
}
▮▮▮▮▮▮▮▮❺ 点击工具栏上的 "Build" 按钮 (锤子图标) 编译项目。
▮▮▮▮▮▮▮▮❻ 点击工具栏上的 "Run" 按钮 (绿色三角形图标) 运行程序。程序输出 "Hello, CLion!" 到控制台。
④ 其他 IDE: 除了上述 IDE,还有一些其他的 C++ IDE,例如:
▮▮▮▮ⓐ Code::Blocks: 一个免费、开源、跨平台的 C++ IDE,轻量级,适合初学者。
▮▮▮▮ⓑ Eclipse CDT (C/C++ Development Tooling): Eclipse IDE 的 C/C++ 开发插件,功能强大,跨平台,适合大型项目开发。
▮▮▮▮ⓒ Qt Creator: Qt 框架的官方 IDE,主要用于 Qt 应用程序开发,但也支持通用的 C++ 开发。
选择 IDE: 选择 IDE 时,可以考虑以下因素:
⚝ 操作系统: Visual Studio 主要用于 Windows,CLion 和 VS Code 是跨平台的。
⚝ 功能需求: Visual Studio 和 CLion 功能全面,适合大型项目和专业开发。VS Code 轻量级但功能可扩展,适合各种规模的项目。Code::Blocks 和 Eclipse CDT 适合开源项目和特定需求。
⚝ 易用性: Visual Studio 和 CLion 界面友好,易于上手。VS Code 需要一定的配置,但灵活性高。Code::Blocks 和 Eclipse CDT 的界面可能相对复杂。
⚝ 收费: Visual Studio Community 和 VS Code 是免费的。CLion 是商业软件,但提供试用期和教育许可证。Code::Blocks 和 Eclipse CDT 是开源免费的。
对于初学者,Visual Studio (Windows), VS Code (跨平台), Code::Blocks (跨平台) 都是不错的选择。可以根据自己的操作系统、需求和偏好选择合适的 IDE,并开始 C++ 编程。
1.2.3 命令行编译和运行
讲解如何使用命令行工具手动编译和运行C++程序,加深对编译过程的理解。
除了使用 IDE,还可以使用 命令行工具 (command-line tools) 手动编译和运行 C++ 程序。命令行编译和运行可以帮助开发者更深入地理解 C++ 程序的编译和执行过程,也适用于一些轻量级的开发场景或自动化构建 (build automation) 脚本。以下介绍如何使用命令行工具编译和运行 C++ 程序,以 GCC 和 Clang 编译器为例。
① 使用 GCC 命令行编译和运行:
▮▮▮▮ⓐ 编写 C++ 源代码: 首先,使用文本编辑器 (例如 Notepad, Sublime Text, VS Code 等) 创建一个 C++ 源文件,例如 hello.cpp
,并输入以下代码:
1
#include <iostream>
2
3
int main() {
4
std::cout << "Hello, Command Line with GCC!" << std::endl;
5
return 0;
6
}
▮▮▮▮ⓑ 打开命令行终端: 打开操作系统的命令行终端 (Windows 的 "命令提示符" 或 "PowerShell",macOS 和 Linux 的 "终端")。
▮▮▮▮ⓒ 切换到源代码目录: 使用 cd
命令切换到 hello.cpp
文件所在的目录。例如,如果 hello.cpp
文件在 D:\cpp_projects
目录下,则在命令行中输入 cd D:\cpp_projects
(Windows) 或 cd /path/to/cpp_projects
(macOS/Linux)。
▮▮▮▮ⓓ 使用 g++
命令编译: 在命令行中输入以下命令,使用 GCC 编译器编译 hello.cpp
文件:
1
g++ hello.cpp -o hello
▮▮▮▮⚝ g++
是 GCC C++ 编译器的命令。
▮▮▮▮⚝ hello.cpp
是要编译的 C++ 源文件名。
▮▮▮▮⚝ -o hello
指定输出可执行文件的名称为 hello
(在 Windows 上会生成 hello.exe
,在 macOS/Linux 上生成 hello
)。
▮▮▮▮⚝ 如果编译成功,命令行不会显示任何错误信息,会在当前目录下生成可执行文件 hello
(或 hello.exe
)。
▮▮▮▮⚝ 如果编译失败,命令行会显示错误信息,需要根据错误信息修改源代码。
▮▮▮▮ⓔ 运行可执行程序: 在命令行中输入以下命令,运行编译生成的可执行程序:
▮▮▮▮⚝ Windows: 输入 hello.exe
或 .\hello.exe
(如果当前目录不在系统 Path
环境变量中)。
▮▮▮▮⚝ macOS/Linux: 输入 ./hello
(./
表示当前目录)。
▮▮▮▮⚝ 程序会输出 "Hello, Command Line with GCC!" 到命令行终端。
② 使用 Clang 命令行编译和运行:
▮▮▮▮ⓐ 编写 C++ 源代码: 与使用 GCC 相同,创建 hello.cpp
源文件并输入代码。
▮▮▮▮ⓑ 打开命令行终端: 打开命令行终端。
▮▮▮▮ⓒ 切换到源代码目录: 使用 cd
命令切换到 hello.cpp
文件所在的目录。
▮▮▮▮ⓓ 使用 clang++
命令编译: 在命令行中输入以下命令,使用 Clang 编译器编译 hello.cpp
文件:
1
clang++ hello.cpp -o hello
▮▮▮▮⚝ clang++
是 Clang C++ 编译器的命令。
▮▮▮▮⚝ 命令参数与 g++
类似,-o hello
指定输出可执行文件名为 hello
。
▮▮▮▮⚝ 编译成功或失败的处理方式与 GCC 相同。
▮▮▮▮ⓔ 运行可执行程序: 运行可执行程序的命令与 GCC 相同,根据操作系统输入 hello.exe
或 ./hello
。
③ 常用编译选项: 在命令行编译 C++ 程序时,可以使用一些常用的编译选项来控制编译过程和生成的可执行程序。以下列举一些常用选项:
▮▮▮▮ⓐ -o output_filename
: 指定输出可执行文件的名称为 output_filename
。
▮▮▮▮ⓑ -g
: 生成调试信息 (debugging information),用于调试程序。编译时添加 -g
选项,可以在调试器 (如 GDB, LLDB) 中进行断点调试、单步执行等。
1
g++ -g hello.cpp -o hello_debug
▮▮▮▮ⓒ -Wall
: 开启所有警告 (all warnings)。编译时添加 -Wall
选项,编译器会输出更多的警告信息,有助于发现潜在的代码问题,提高代码质量。
1
g++ -Wall hello.cpp -o hello_warn
▮▮▮▮ⓓ -std=c++版本
: 指定 C++ 标准版本。例如,使用 -std=c++11
编译 C++11 标准的代码,使用 -std=c++17
编译 C++17 标准的代码,使用 -std=c++20
编译 C++20 标准的代码。如果不指定,编译器通常会使用一个默认的 C++ 标准版本 (通常是较旧的版本,如 C++98 或 C++03)。建议在编译时显式指定 C++ 标准版本,以确保代码的兼容性和使用最新的语言特性。
1
g++ -std=c++17 hello.cpp -o hello_cpp17
▮▮▮▮ⓔ -O[level]
: 指定编译优化级别。优化级别越高,编译器会进行更多的代码优化,生成性能更高的可执行程序,但编译时间也会相应增加。常用的优化级别包括 -O0
(不优化,默认级别), -O1
(基本优化), -O2
(更积极的优化), -O3
(最高级别优化)。
1
g++ -O2 hello.cpp -o hello_optimized
▮▮▮▮ⓕ -Iinclude_path
: 指定头文件搜索路径。如果程序中使用了非标准库的头文件,需要使用 -I
选项指定头文件所在的目录。
1
g++ -I/path/to/include hello.cpp -o hello_include
▮▮▮▮ⓖ -Llibrary_path
: 指定库文件搜索路径。如果程序需要链接非标准库的库文件,需要使用 -L
选项指定库文件所在的目录。
1
g++ -L/path/to/lib hello.cpp -o hello_lib -lmylib
▮▮▮▮ⓗ -llibname
: 指定要链接的库文件。例如,-lm
链接数学库 libm.so
(或 libm.a
),-lmylib
链接自定义库 libmylib.so
(或 libmylib.a
)。
④ 命令行编译的优势: 使用命令行编译 C++ 程序有以下优势:
⚝ 更深入地理解编译过程: 命令行编译需要手动输入编译命令和选项,可以帮助开发者更深入地理解编译的各个环节,例如预处理 (preprocessing)、编译 (compilation)、汇编 (assembly)、链接 (linking)。
⚝ 轻量级和灵活: 命令行编译不需要启动庞大的 IDE,更加轻量级和快速。可以灵活地组合各种编译选项,满足不同的编译需求。
⚝ 自动化构建: 命令行编译非常适合用于自动化构建脚本 (如 Makefile, shell script, Python script)。可以使用命令行工具将编译、测试、部署等步骤自动化,提高软件开发的效率和质量。
⚝ 服务器环境: 在服务器环境下,通常没有图形界面,只能使用命令行工具进行开发和部署。掌握命令行编译技能对于服务器端 C++ 开发非常重要。
总结来说,命令行编译是 C++ 开发的重要技能之一。掌握命令行编译和常用的编译选项,可以帮助开发者更深入地理解 C++ 程序的编译和执行过程,提高开发效率,并为更高级的 C++ 开发技术打下坚实的基础。
1.3 第一个C++程序
引导读者编写并运行经典的“Hello, World!”程序,初步体验C++代码的结构和编译运行流程。
每个程序员的编程之旅通常从编写并运行第一个 "Hello, World!" 程序开始。这是一个简单但意义重大的程序,它可以验证开发环境是否配置正确,并初步体验编程语言的基本语法和运行流程。本节将引导读者编写并运行经典的 "Hello, World!" C++ 程序,并对程序代码进行初步的解释。
1.3.1 main()
函数详解
详细解释C++程序入口 main()
函数的作用、语法结构和返回值。
main()
函数是 C++ 程序的入口点 (entry point)。当程序运行时,操作系统会首先调用 main()
函数,程序从 main()
函数的第一行代码开始执行。每个 C++ 程序都必须有且仅有一个 main()
函数。main()
函数的定义和语法结构如下:
① main()
函数的语法结构:
1
int main() {
2
// 函数体 (Function body)
3
// 程序的主要代码
4
return 0; // 返回值
5
}
▮▮▮▮ⓐ int
返回类型 (Return Type): main()
函数的返回类型通常是 int
(integer,整型)。int
表示函数执行结束后会返回一个整数值给操作系统。这个返回值用于表示程序的退出状态 (exit status)。
▮▮▮▮ⓑ main
函数名 (Function Name): main
是函数的名字,C++ 规定程序入口函数必须命名为 main
,且必须是小写字母。
▮▮▮▮ⓒ ()
参数列表 (Parameter List): main()
函数名后面的 ()
表示参数列表。在最简单的形式中,main()
函数可以没有参数,即 ()
中为空。main()
函数也可以接受命令行参数,但这将在后续章节中介绍。
▮▮▮▮ⓓ {}
函数体 (Function Body): {}
包含的部分是 main()
函数的函数体,函数体中包含了程序要执行的具体代码。程序的所有逻辑都写在 main()
函数的函数体中。
▮▮▮▮ⓔ return 0;
返回语句 (Return Statement): return 0;
是 main()
函数的返回语句。return
关键字用于从函数返回一个值。0
是返回的整数值。按照惯例,返回 0
表示程序正常退出 (正常结束),返回非零值 (例如 1
, -1
) 通常表示程序异常退出 (发生错误)。
② main()
函数的作用:
▮▮▮▮ⓐ 程序入口: main()
函数是程序的入口点,操作系统通过调用 main()
函数启动程序。
▮▮▮▮ⓑ 控制程序流程: main()
函数负责控制程序的执行流程。程序从 main()
函数的第一条语句开始顺序执行,可以调用其他函数、执行循环、条件判断等,直到 main()
函数执行结束。
▮▮▮▮ⓒ 程序退出状态: main()
函数的返回值表示程序的退出状态。操作系统可以获取程序的退出状态,用于判断程序是否正常执行完成。在命令行环境中,可以使用 echo %ERRORLEVEL%
(Windows) 或 echo $?
(macOS/Linux) 命令查看上一个程序的退出状态。
③ main()
函数的示例:
以下是一个简单的 "Hello, World!" 程序的 main()
函数:
1
#include <iostream>
2
3
int main() {
4
std::cout << "Hello, World!" << std::endl;
5
return 0;
6
}
在这个例子中,main()
函数的函数体只有两条语句:
▮▮▮▮ⓐ std::cout << "Hello, World!" << std::endl;
: 这条语句使用 std::cout
对象将字符串 "Hello, World!" 输出到控制台。std::cout
是 C++ 标准库中的输出流对象,用于向标准输出设备 (通常是控制台) 输出数据。<<
是输出运算符,将右侧的数据输出到左侧的输出流。std::endl
是一个操纵符 (manipulator),用于在输出流中插入一个换行符 (newline character) 并刷新输出缓冲区。
▮▮▮▮ⓑ return 0;
: 这条语句返回整数值 0
,表示程序正常退出。
④ main()
函数的变体:
在某些情况下,main()
函数也可以有其他形式,例如:
▮▮▮▮ⓐ int main(int argc, char *argv[])
: 这种形式的 main()
函数可以接收命令行参数。
▮▮▮▮⚝ argc
(argument count) 是一个整数,表示命令行参数的个数 (包括程序名本身)。
▮▮▮▮⚝ argv
(argument vector) 是一个字符指针数组,argv[0]
指向程序名,argv[1]
到 argv[argc-1]
分别指向各个命令行参数字符串。
▮▮▮▮⚝ 这种形式的 main()
函数常用于需要接收命令行输入的程序。
1
#include <iostream>
2
3
int main(int argc, char *argv[]) {
4
std::cout << "程序名: " << argv[0] << std::endl;
5
if (argc > 1) {
6
std::cout << "命令行参数: " << std::endl;
7
for (int i = 1; i < argc; ++i) {
8
std::cout << "argv[" << i << "] = " << argv[i] << std::endl;
9
}
10
}
11
return 0;
12
}
▮▮▮▮ⓑ int main(int argc, char **argv)
: 与 char *argv[]
形式等价,char **argv
也是表示字符指针数组。
在大多数初学阶段,使用最简单的 int main()
形式即可。随着学习的深入,会逐渐接触到更复杂的 main()
函数形式和用法。理解 main()
函数的作用和语法结构是编写 C++ 程序的基础。
1.3.2 输入/输出 (Input/Output) 初探
介绍 iostream
库,以及 std::cout
和 std::cin
的基本用法,实现简单的控制台输入输出。
输入/输出 (Input/Output, I/O) 是程序与外部世界交互的重要方式。程序需要从外部获取数据 (输入),并将处理结果输出到外部 (输出)。C++ 提供了 iostream
库 来支持输入/输出操作。iostream
库是 C++ 标准库的一部分,提供了用于处理输入和输出的类和对象。本节将初步介绍 iostream
库,以及 std::cout
和 std::cin
的基本用法,实现简单的控制台输入输出。
① iostream
库:
▮▮▮▮ⓐ 包含头文件: 要使用 iostream
库,需要在 C++ 源文件中包含头文件 <iostream>
。
1
#include <iostream>
#include
是预处理指令,用于包含头文件。<iostream>
头文件包含了输入/输出流相关的类和对象的声明。
▮▮▮▮ⓑ 主要类和对象: iostream
库提供了以下主要的类和对象:
▮▮▮▮⚝ std::istream
: 输入流基类 (input stream base class)。
▮▮▮▮⚝ std::ostream
: 输出流基类 (output stream base class)。
▮▮▮▮⚝ std::iostream
: 输入输出流基类 (input/output stream base class),同时继承自 std::istream
和 std::ostream
。
▮▮▮▮⚝ std::cin
: std::istream
类的对象,用于从标准输入设备 (standard input device) 读取数据,通常是键盘。
▮▮▮▮⚝ std::cout
: std::ostream
类的对象,用于向标准输出设备 (standard output device) 输出数据,通常是控制台。
▮▮▮▮⚝ std::cerr
: std::ostream
类的对象,用于向标准错误输出设备 (standard error output device) 输出错误信息,通常是控制台。
▮▮▮▮⚝ std::clog
: std::ostream
类的对象,用于向标准日志输出设备 (standard log output device) 输出日志信息,通常是控制台,但输出是带缓冲的。
② std::cout
输出:
std::cout
对象用于向标准输出设备 (通常是控制台) 输出数据。使用 输出运算符 <<
将数据输出到 std::cout
。
▮▮▮▮ⓐ 输出字符串:
1
#include <iostream>
2
3
int main() {
4
std::cout << "Hello, World!" << std::endl; // 输出字符串字面量
5
std::string message = "Welcome to C++!";
6
std::cout << message << std::endl; // 输出字符串变量
7
return 0;
8
}
▮▮▮▮ⓑ 输出数字:
1
#include <iostream>
2
3
int main() {
4
int age = 30;
5
double price = 99.99;
6
std::cout << "年龄: " << age << std::endl; // 输出整型变量
7
std::cout << "价格: " << price << std::endl; // 输出浮点型变量
8
return 0;
9
}
▮▮▮▮ⓒ 输出多个值: 可以使用多个 <<
运算符连续输出多个值。
1
#include <iostream>
2
3
int main() {
4
std::string name = "Alice";
5
int score = 95;
6
std::cout << "姓名: " << name << ", 分数: " << score << std::endl;
7
return 0;
8
}
▮▮▮▮ⓓ 操纵符 (Manipulators): iostream
库提供了一些操纵符,用于控制输出格式。
▮▮▮▮⚝ std::endl
: 插入换行符并刷新输出缓冲区。
▮▮▮▮⚝ std::flush
: 刷新输出缓冲区,但不插入换行符。
▮▮▮▮⚝ std::setw(n)
: 设置输出字段宽度为 n
。需要包含头文件 <iomanip>
。
▮▮▮▮⚝ std::setprecision(n)
: 设置浮点数输出精度为 n
位有效数字。需要包含头文件 <iomanip>
。
▮▮▮▮⚝ std::fixed
: 以定点数格式输出浮点数。需要包含头文件 <iomanip>
。
▮▮▮▮⚝ std::scientific
: 以科学计数法格式输出浮点数。需要包含头文件 <iomanip>
。
1
#include <iostream>
2
#include <iomanip> // 需要包含 iomanip 头文件
3
4
int main() {
5
double pi = 3.1415926;
6
std::cout << "默认输出: " << pi << std::endl;
7
std::cout << "setprecision(3): " << std::setprecision(3) << pi << std::endl;
8
std::cout << "fixed 和 setprecision(5): " << std::fixed << std::setprecision(5) << pi << std::endl;
9
std::cout << "scientific 和 setprecision(2): " << std::scientific << std::setprecision(2) << pi << std::endl;
10
return 0;
11
}
③ std::cin
输入:
std::cin
对象用于从标准输入设备 (通常是键盘) 读取数据。使用 输入运算符 >>
从 std::cin
读取数据并存储到变量中。
▮▮▮▮ⓐ 读取整型输入:
1
#include <iostream>
2
3
int main() {
4
int age;
5
std::cout << "请输入你的年龄: ";
6
std::cin >> age; // 从键盘读取整型输入并存储到 age 变量
7
std::cout << "你的年龄是: " << age << std::endl;
8
return 0;
9
}
▮▮▮▮ⓑ 读取浮点型输入:
1
#include <iostream>
2
3
int main() {
4
double price;
5
std::cout << "请输入商品价格: ";
6
std::cin >> price; // 从键盘读取浮点型输入并存储到 price 变量
7
std::cout << "商品价格是: " << price << std::endl;
8
return 0;
9
}
▮▮▮▮ⓒ 读取字符串输入:
1
#include <iostream>
2
#include <string> // 需要包含 string 头文件
3
4
int main() {
5
std::string name;
6
std::cout << "请输入你的姓名: ";
7
std::cin >> name; // 从键盘读取字符串输入并存储到 name 变量 (读取到空格或换行符为止)
8
std::cout << "你的姓名是: " << name << std::endl;
9
return 0;
10
}
注意: std::cin >> name;
读取字符串时,默认以空格、制表符或换行符作为分隔符。如果需要读取包含空格的字符串,可以使用 std::getline()
函数。
▮▮▮▮ⓓ 使用 std::getline()
读取一行字符串:
1
#include <iostream>
2
#include <string>
3
4
int main() {
5
std::string line;
6
std::cout << "请输入一行文本: ";
7
std::getline(std::cin, line); // 读取一行文本 (包括空格) 并存储到 line 变量
8
std::cout << "你输入的文本是: " << line << std::endl;
9
return 0;
10
}
std::getline(std::cin, line);
函数从 std::cin
读取一行文本,直到遇到换行符为止,并将读取的文本 (不包括换行符) 存储到 line
字符串变量中。
④ 错误处理: 当使用 std::cin
读取输入时,可能会发生输入类型不匹配或输入错误的情况。例如,当程序期望读取整型输入,但用户输入了字符串时,std::cin
会进入错误状态。可以使用 std::cin.fail()
函数检查 std::cin
是否处于错误状态,并使用 std::cin.clear()
清除错误状态,使用 std::cin.ignore()
忽略错误输入。错误处理将在后续章节中详细介绍。
通过 std::cout
和 std::cin
,可以实现简单的控制台输入输出,与程序进行基本的交互。掌握输入输出是编写交互式程序的基础。
1.3.3 注释 (Comments) 的使用
讲解C++中单行注释和多行注释的语法和用途,强调代码注释的重要性。
注释 (comments) 是程序代码中用于解释代码功能和逻辑的文本,注释不会被编译器编译执行,只是为了提高代码的可读性和可维护性,方便程序员理解代码。良好的代码注释是编写高质量代码的重要组成部分。C++ 支持两种类型的注释:单行注释 (single-line comments) 和 多行注释 (multi-line comments)。
① 单行注释:
单行注释以 //
开头,从 //
开始到行末的所有内容都被视为注释。单行注释通常用于对代码的某一行或几行进行简短的解释。
1
#include <iostream> // 包含 iostream 头文件,用于输入输出
2
3
int main() {
4
int age = 30; // 声明一个整型变量 age 并初始化为 30
5
std::cout << "年龄: " << age << std::endl; // 输出年龄信息到控制台
6
return 0; // 程序正常退出
7
}
在上面的例子中,每一行代码的后面都添加了单行注释,用于解释代码的功能。
② 多行注释:
多行注释以 /*
开始,以 */
结束,/*
和 */
之间的所有内容都被视为注释,可以跨越多行。多行注释通常用于对一段代码、函数或文件进行详细的解释说明。
1
/*
2
* 这是一个 "Hello, World!" 程序。
3
* 程序的功能是在控制台输出 "Hello, World!" 字符串。
4
* 作者:Your Name
5
* 日期:2023-10-27
6
*/
7
#include <iostream>
8
9
int main() {
10
/*
11
* main 函数是程序的入口点。
12
* 程序从 main 函数开始执行。
13
*/
14
std::cout << "Hello, World!" << std::endl; // 输出 Hello, World!
15
return 0; // 返回 0 表示程序正常退出
16
}
在上面的例子中,文件头部和 main()
函数内部都使用了多行注释,用于提供更详细的说明信息。
③ 注释的用途和重要性:
▮▮▮▮ⓐ 提高代码可读性: 注释可以解释代码的功能、逻辑、算法和实现细节,帮助其他程序员 (包括未来的自己) 更容易地理解代码。尤其是在阅读和维护代码时,注释可以节省大量的时间和精力。
▮▮▮▮ⓑ 方便代码维护: 当需要修改或维护代码时,注释可以帮助程序员快速理解代码,并减少引入错误的风险。良好的注释可以使代码更易于维护和扩展。
▮▮▮▮ⓒ 生成文档: 一些文档生成工具 (如 Doxygen) 可以从代码注释中自动生成程序文档。按照一定的格式编写注释,可以方便地生成 API 文档、用户手册等。
▮▮▮▮ⓓ 代码调试和测试: 在调试和测试代码时,可以使用注释临时禁用 (comment out) 一段代码,而无需删除它。这在排查错误或进行实验性修改时非常有用。
▮▮▮▮ⓔ 团队协作: 在团队开发中,不同的程序员可能负责不同的模块。通过编写清晰的注释,可以促进团队成员之间的沟通和协作,减少误解和冲突。
④ 注释的最佳实践:
▮▮▮▮ⓐ 注释要清晰、简洁、准确: 注释应该用简洁明了的语言解释代码的功能和目的,避免使用含糊不清或错误的注释。注释的内容要与代码保持一致,代码修改时也要及时更新注释。
▮▮▮▮ⓑ 注释要适量: 注释不是越多越好,过多的注释反而会降低代码的可读性。注释应该重点解释代码中不易理解的部分,例如复杂的算法、重要的逻辑、特殊处理等。对于显而易懂的代码,可以省略注释。
▮▮▮▮ⓒ 注释风格要一致: 在同一个项目或团队中,应该统一注释风格,例如使用单行注释还是多行注释,注释的格式和内容等。保持注释风格的一致性可以提高代码的整体可读性。
▮▮▮▮ⓓ 避免使用无意义的注释: 不要写一些没有实际意义的注释,例如 "int i; // 声明一个整型变量 i"。这种注释没有提供任何额外的信息,反而显得冗余。注释应该解释 "为什么" (why) 而不是 "是什么" (what)。
▮▮▮▮ⓔ 及时更新注释: 当代码修改时,要及时更新相关的注释,确保注释与代码保持同步。过时或错误的注释比没有注释更糟糕。
总之,代码注释是编程中不可或缺的一部分。养成良好的注释习惯,编写清晰、简洁、准确的注释,可以显著提高代码的可读性、可维护性和可协作性,为编写高质量的 C++ 程序打下坚实的基础。
2. C++基础语法:数据类型、运算符与表达式
本章系统讲解C++的基本数据类型、运算符、表达式和语句,为编写更复杂的程序奠定坚实的基础。
2.1 基本数据类型 (Basic Data Types)
详细介绍C++的内置数据类型,包括整型、浮点型、字符型和布尔型,以及它们在内存中的表示和取值范围。
2.1.1 整型 (Integer Types):int
, short
, long
, long long
讲解不同整型类型的区别、选择原则以及有符号和无符号整型的概念。
整型 (integer types) 用于表示整数值,是编程中最基本的数据类型之一。C++ 提供了多种整型类型,以适应不同的内存需求和数值范围。主要的整型类型包括 int
, short
, long
和 long long
。此外,每种整型类型还可以分为有符号 (signed) 和无符号 (unsigned) 两种。
① 不同整型类型的区别:
⚝ int
: 标准整型,通常为 32 位,能表示的整数范围通常为 -2147483648 到 2147483647。在现代计算机体系结构中,int
通常是处理整数运算最有效率的类型。
⚝ short
: 短整型,通常为 16 位,能表示的整数范围通常为 -32768 到 32767。short
占用内存较小,适用于内存资源有限的场景或需要大量存储整数的场合。
⚝ long
: 长整型,在 32 位系统中通常与 int
大小相同,但在 64 位系统中通常为 64 位,能表示的整数范围更广。为了保证跨平台的兼容性,建议使用 long long
来明确表示 64 位整型。
⚝ long long
: 长长整型,C++11 引入,保证至少 64 位,能表示非常大的整数范围,通常为 -9223372036854775808 到 9223372036854775807。适用于需要处理超出 int
和 long
范围的大整数的场景。
不同整型类型的内存大小和取值范围取决于具体的编译器和操作系统,可以使用 sizeof
运算符来获取各种数据类型在当前系统中所占用的字节数。例如,sizeof(int)
、sizeof(short)
、sizeof(long)
、sizeof(long long)
可以分别得到 int
、short
、long
和 long long
类型所占用的字节数。
② 有符号 (signed) 和无符号 (unsigned) 整型:
默认情况下,整型类型 int
, short
, long
, long long
都是有符号的,即可以表示正数、负数和零。有符号整型使用最高位作为符号位,0 表示正数,1 表示负数。
在类型名前加上关键字 unsigned
可以声明无符号整型,例如 unsigned int
, unsigned short
, unsigned long
, unsigned long long
。无符号整型只能表示非负整数 (正整数和零),所有位都用于存储数值,因此相同位数的无符号整型可以表示更大的正整数范围。例如,unsigned int
的取值范围通常为 0 到 4294967295。
类型 | 字节数 (bytes) (典型值) | 位数 (bits) | 有符号 (signed) 范围 (典型值) | 无符号 (unsigned) 范围 (典型值) |
---|---|---|---|---|
short | 2 | 16 | -32,768 到 32,767 | 0 到 65,535 |
unsigned short | 2 | 16 | 不适用 | 0 到 65,535 |
int | 4 | 32 | -2,147,483,648 到 2,147,483,647 | 0 到 4,294,967,295 |
unsigned int | 4 | 32 | 不适用 | 0 到 4,294,967,295 |
long | 4 或 8 (取决于系统) | 32 或 64 | 与 int 或 long long 相同 (取决于系统) | 与 unsigned int 或 unsigned long long 相同 (取决于系统) |
unsigned long | 4 或 8 (取决于系统) | 32 或 64 | 不适用 | 与 unsigned int 或 unsigned long long 相同 (取决于系统) |
long long | 8 | 64 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0 到 18,446,744,073,709,551,615 |
unsigned long long | 8 | 64 | 不适用 | 0 到 18,446,744,073,709,551,615 |
③ 整型类型的选择原则:
选择整型类型时,需要根据实际需求考虑以下因素:
⚝ 数值范围: 首先要确定程序中需要处理的整数的数值范围。如果数值较小,可以使用 short
或 int
;如果数值很大,需要使用 long long
。对于非负整数,可以考虑使用无符号整型,以获得更大的正整数表示范围。
⚝ 内存占用: 在内存资源有限的场景下,应尽量选择占用内存较小的整型类型,如 short
或 int
。
⚝ 性能: 通常情况下,int
是处理器处理效率最高的整型类型。如果对性能有较高要求,且数值范围允许,应优先选择 int
。
⚝ 跨平台兼容性: 为了保证代码在不同平台上的兼容性,应尽量避免依赖于 long
的具体大小。如果需要 64 位整型,应明确使用 long long
。
示例代码:
1
#include <iostream>
2
#include <limits> // 引入 limits 头文件来查询各种类型的极限
3
4
int main() {
5
int signedInt = -10;
6
unsigned int unsignedInt = 100;
7
short shortInt = 32000;
8
long long longLongInt = 9000000000000000000LL; // 使用 LL 后缀表示 long long 类型字面量
9
10
std::cout << "int 类型大小: " << sizeof(int) << " 字节" << std::endl;
11
std::cout << "int 最小值: " << std::numeric_limits<int>::min() << std::endl;
12
std::cout << "int 最大值: " << std::numeric_limits<int>::max() << std::endl;
13
14
std::cout << "unsigned int 类型大小: " << sizeof(unsigned int) << " 字节" << std::endl;
15
std::cout << "unsigned int 最小值: " << std::numeric_limits<unsigned int>::min() << std::endl;
16
std::cout << "unsigned int 最大值: " << std::numeric_limits<unsigned int>::max() << std::endl;
17
18
std::cout << "short 类型大小: " << sizeof(short) << " 字节" << std::endl;
19
std::cout << "long long 类型大小: " << sizeof(long long) << " 字节" << std::endl;
20
21
std::cout << "有符号 int 变量: " << signedInt << std::endl;
22
std::cout << "无符号 int 变量: " << unsignedInt << std::endl;
23
std::cout << "short 变量: " << shortInt << std::endl;
24
std::cout << "long long 变量: " << longLongInt << std::endl;
25
26
return 0;
27
}
这段代码演示了各种整型类型的声明和使用,并使用 sizeof
运算符和 std::numeric_limits
类来获取各种整型类型的大小和取值范围。std::numeric_limits
类定义在 <limits>
头文件中,提供了查询各种数值类型极限的方法。
2.1.2 浮点型 (Floating-Point Types):float
, double
, long double
介绍浮点型类型的精度、表示范围以及浮点数在计算机中的存储方式。
浮点型 (floating-point types) 用于表示带有小数部分的数值,也称为实数 (real numbers)。C++ 提供了三种浮点型类型:float
, double
和 long double
,它们的主要区别在于精度和表示范围。
① 浮点型类型的精度和表示范围:
⚝ float
: 单精度浮点型,通常为 32 位,提供 6-7 位有效数字。float
的精度和表示范围相对较低,但占用内存小,运算速度较快。
⚝ double
: 双精度浮点型,通常为 64 位,提供 15-16 位有效数字。double
是默认的浮点型,精度和表示范围都较高,能够满足大多数科学计算和工程应用的需求。
⚝ long double
: 扩展精度浮点型,精度和表示范围通常高于 double
,具体位数和精度取决于编译器和平台,可能是 80 位、96 位或 128 位。long double
适用于对精度要求极高的场合。
类型 | 字节数 (bytes) (典型值) | 位数 (bits) | 有效数字 (十进制) (近似) | 指数范围 (近似) |
---|---|---|---|---|
float | 4 | 32 | 6-7 | \(±3.4 \times 10^{38}\) |
double | 8 | 64 | 15-16 | \(±1.7 \times 10^{308}\) |
long double | 8, 10, 或 16 | 64, 80, 或 128 | ≥ double | ≥ double |
② 浮点数在计算机中的存储方式:
浮点数在计算机中采用 IEEE 754 标准进行存储,主要由三个部分组成:
⚝ 符号位 (Sign bit): 1 位,表示浮点数的正负号,0 表示正数,1 表示负数。
⚝ 指数 (Exponent): 表示浮点数的数量级。指数部分采用移码 (biased exponent) 存储,即将实际指数值加上一个偏移量 (bias)。对于单精度浮点数,指数部分为 8 位,偏移量为 127;对于双精度浮点数,指数部分为 11 位,偏移量为 1023。
⚝ 尾数 (Mantissa) 或有效数字 (Significand): 表示浮点数的精度。尾数部分存储的是规格化 (normalized) 后的浮点数的小数部分。由于规格化表示法总是使得小数点前有一位非零数字,因此实际存储时可以省略这一位,从而提高精度。对于单精度浮点数,尾数部分为 23 位;对于双精度浮点数,尾数部分为 52 位。
浮点数的计算公式可以表示为:
\[ \text{Value} = (-1)^{\text{sign bit}} \times \text{Mantissa} \times 2^{\text{Exponent} - \text{bias}} \]
例如,对于单精度浮点数,如果符号位为 0,指数部分为 \(E\),尾数部分为 \(M\),则浮点数的值为:
\[ \text{Value} = (-1)^0 \times (1 + M) \times 2^{E - 127} \]
其中,\(M\) 是尾数部分表示的小数值 (介于 0 和 1 之间),加上 1 是因为规格化表示法省略了小数点前的一位。
③ 浮点型的使用注意事项:
⚝ 精度问题: 浮点数在计算机中以二进制形式存储,很多十进制小数无法精确地转换为二进制浮点数,会存在舍入误差。因此,不应直接使用浮点数进行相等比较。比较两个浮点数是否接近相等时,应使用容差 (tolerance) 或阈值 (epsilon)。
1
float a = 0.1f + 0.2f;
2
float b = 0.3f;
3
if (std::abs(a - b) < 1e-6f) { // 使用容差进行比较
4
std::cout << "a 和 b 近似相等" << std::endl;
5
} else {
6
std::cout << "a 和 b 不相等" << std::endl; // 实际上会输出这个
7
}
⚝ 表示范围: 浮点数可以表示非常大和非常小的数值,但也存在溢出 (overflow) 和下溢 (underflow) 的问题。当计算结果超出浮点数的表示范围时,会发生溢出或下溢,可能导致程序错误。
⚝ 运算速度: 浮点数运算通常比整数运算更耗时。在对性能敏感的场景下,应尽量避免不必要的浮点数运算。
示例代码:
1
#include <iostream>
2
#include <cmath>
3
#include <iomanip> // 需要包含 iomanip 头文件来使用 std::setprecision
4
5
int main() {
6
float floatVar = 3.1415926f; // 使用 f 后缀表示 float 类型字面量
7
double doubleVar = 3.141592653589793;
8
long double longDoubleVar = 3.141592653589793238462643383279L; // 使用 L 后缀表示 long double 类型字面量
9
10
std::cout << "float 变量: " << std::fixed << std::setprecision(7) << floatVar << std::endl; // 设置输出精度为 7 位小数
11
std::cout << "double 变量: " << std::fixed << std::setprecision(16) << doubleVar << std::endl; // 设置输出精度为 16 位小数
12
std::cout << "long double 变量: " << std::fixed << std::setprecision(20) << longDoubleVar << std::endl; // 设置输出精度为 20 位小数
13
14
std::cout << "float 类型大小: " << sizeof(float) << " 字节" << std::endl;
15
std::cout << "double 类型大小: " << sizeof(double) << " 字节" << std::endl;
16
std::cout << "long double 类型大小: " << sizeof(long double) << " 字节" << std::endl;
17
18
float sum = 0.0f;
19
for (int i = 0; i < 10; ++i) {
20
sum += 0.1f;
21
}
22
std::cout << "累加 10 次 0.1f 的结果: " << std::fixed << std::setprecision(7) << sum << std::endl; // 结果可能不是精确的 1.0
23
24
return 0;
25
}
这段代码演示了 float
, double
和 long double
类型的声明和使用,以及浮点数的精度问题。使用 std::setprecision
可以控制浮点数的输出精度,std::fixed
可以确保以固定的小数点形式输出。循环累加 0.1f 的例子展示了浮点数运算可能存在的舍入误差。
2.1.3 字符型 (Character Type):char
讲解字符型变量的存储和表示,ASCII码和Unicode编码,以及字符和字符串的区别。
字符型 (character type) char
用于表示单个字符,例如字母、数字、标点符号等。char
类型通常占用 1 个字节 (8 位) 的内存空间。
① 字符型变量的存储和表示:
char
类型变量实际上存储的是字符的编码值,而不是字符本身。最常用的字符编码标准是 ASCII (American Standard Code for Information Interchange) 码和 Unicode (统一码)。
⚝ ASCII 码: ASCII 码使用 7 位二进制数 (加上 1 位校验位,共 8 位) 表示 128 个常用字符,包括英文字母 (大小写)、数字、标点符号和控制字符。ASCII 码的范围是 0 到 127。例如,字符 'A' 的 ASCII 码值为 65,字符 'a' 的 ASCII 码值为 97,字符 '0' 的 ASCII 码值为 48。
⚝ 扩展 ASCII 码: 为了表示更多的字符,ASCII 码被扩展到 8 位,称为扩展 ASCII 码。扩展 ASCII 码可以表示 256 个字符,包括一些特殊符号和西欧字符。但扩展 ASCII 码不是统一的标准,不同的扩展方案可能有所不同。
⚝ Unicode 编码: 为了统一表示世界上各种语言的字符,国际标准组织制定了 Unicode 编码。Unicode 使用更长的编码长度 (例如 16 位、32 位) 来表示字符,可以容纳数十万个字符,包括各种语言的文字、符号、图形等。常用的 Unicode 编码方案包括 UTF-8, UTF-16, UTF-32 等。
▮▮▮▮⚝ UTF-8 (8-bit Unicode Transformation Format): UTF-8 是一种变长编码方案,使用 1 到 4 个字节表示一个字符。ASCII 字符使用 1 个字节表示,与 ASCII 码兼容。常用汉字通常使用 3 个字节表示。UTF-8 是目前互联网上最常用的 Unicode 编码方案。
▮▮▮▮⚝ UTF-16 (16-bit Unicode Transformation Format): UTF-16 使用 2 个或 4 个字节表示一个字符。常用字符 (基本多文种平面 BMP) 使用 2 个字节表示,辅助平面字符使用 4 个字节表示。UTF-16 在 Windows 系统和 Java 语言中被广泛使用。
▮▮▮▮⚝ UTF-32 (32-bit Unicode Transformation Format): UTF-32 使用 4 个字节表示每个字符,编码长度固定。UTF-32 编码简单,但占用空间较大。
在 C++ 中,char
类型默认使用系统默认字符集,通常是 ASCII 码或 UTF-8。可以使用宽字符类型 wchar_t
来表示 Unicode 字符,wchar_t
的大小通常为 2 个或 4 个字节,取决于系统。C++11 还引入了 char16_t
和 char32_t
类型,分别用于明确表示 UTF-16 和 UTF-32 编码的字符。
② 字符字面量 (Character Literals):
字符字面量用单引号括起来,例如 'A'
, '0'
, ' '
, '$'
。字符字面量的值是字符的编码值。
⚝ 普通字符字面量: 例如 'A'
, 'b'
, '9'
, '+'
。
⚝ 转义字符 (Escape Sequences): 用于表示一些特殊的字符,例如换行符 '\n'
, 制表符 '\t'
, 回车符 '\r'
, 反斜杠 '\\'
, 单引号 '\''
, 双引号 '"'
, 空字符 '\0'
等。
转义字符 | 含义 | ASCII 码值 (十进制) |
---|---|---|
\n | 换行符 | 10 |
\t | 制表符 | 9 |
\r | 回车符 | 13 |
\\ | 反斜杠 | 92 |
\' | 单引号 | 39 |
\" | 双引号 | 34 |
\0 | 空字符 (null) | 0 |
⚝ 通用字符名 (Universal Character Names): 用于表示 Unicode 字符,以 \u
或 \U
开头,后跟 4 位或 8 位十六进制数,表示 Unicode 码点。例如 '\u4E00'
表示汉字 “一”,'\U0001F600'
表示 Emoji 表情 “😀”。
③ 字符和字符串的区别:
⚝ 字符 (Character): 指单个的符号,例如字母、数字、标点符号。在 C++ 中用 char
类型表示。字符字面量用单引号括起来。
⚝ 字符串 (String): 指由多个字符组成的序列,例如单词、句子、文章等。在 C++ 中可以使用 C 风格字符串 (字符数组以空字符 '\0'
结尾) 或 std::string
类来表示。字符串字面量用双引号括起来。
1
char ch = 'A'; // 字符
2
const char* cStr = "Hello"; // C 风格字符串
3
std::string cppStr = "World"; // C++ 字符串
示例代码:
1
#include <iostream>
2
3
int main() {
4
char charVar = 'A';
5
char newlineChar = '\n';
6
char unicodeChar = '\u4E00'; // 汉字 "一"
7
8
std::cout << "字符变量: " << charVar << std::endl;
9
std::cout << "换行符: " << newlineChar; // 输出换行
10
std::cout << "Unicode 字符: " << unicodeChar << std::endl;
11
std::cout << "字符 'A' 的 ASCII 码值: " << static_cast<int>(charVar) << std::endl; // 将 char 转换为 int 输出 ASCII 码值
12
13
std::cout << "char 类型大小: " << sizeof(char) << " 字节" << std::endl;
14
15
const char* str = "C++ 语言"; // C 风格字符串
16
std::cout << "C 风格字符串: " << str << std::endl;
17
18
std::string cppStr = "C++ String"; // C++ std::string
19
std::cout << "C++ std::string: " << cppStr << std::endl;
20
21
return 0;
22
}
这段代码演示了 char
类型变量的声明和使用,包括普通字符、转义字符和 Unicode 字符。使用 static_cast<int>(charVar)
可以将 char
类型变量强制转换为 int
类型,从而输出字符的 ASCII 码值。代码还展示了 C 风格字符串和 C++ std::string
字符串的声明和使用。
2.1.4 布尔型 (Boolean Type):bool
介绍布尔型变量的取值和逻辑运算,以及在条件判断中的应用。
布尔型 (boolean type) bool
用于表示真 (true) 或假 (false) 的逻辑值。bool
类型只有两个取值:true
(真) 和 false
(假)。bool
类型通常占用 1 个字节 (8 位) 的内存空间,但实际存储时只需要 1 位即可表示真假值。
① 布尔型变量的取值:
bool
类型变量只能取两个值:
⚝ true
: 表示逻辑真,通常用整数值 1 或非零值表示。
⚝ false
: 表示逻辑假,通常用整数值 0 表示。
C++ 提供了关键字 true
和 false
来表示布尔字面量。
② 布尔型变量的逻辑运算:
布尔型变量主要用于逻辑运算,包括:
⚝ 逻辑与 (AND): 运算符 &&
。当且仅当两个操作数都为 true
时,结果才为 true
,否则为 false
。
⚝ 逻辑或 (OR): 运算符 ||
。当两个操作数中至少有一个为 true
时,结果为 true
,否则为 false
。
⚝ 逻辑非 (NOT): 运算符 !
。对操作数取反,如果操作数为 true
,结果为 false
;如果操作数为 false
,结果为 true
。
运算符 | 运算 | 操作数 1 | 操作数 2 | 结果 |
---|---|---|---|---|
&& | 逻辑与 | true | true | true |
&& | 逻辑与 | true | false | false |
&& | 逻辑与 | false | true | false |
&& | 逻辑与 | false | false | false |
\|\| | 逻辑或 | true | true | true |
\|\| | 逻辑或 | true | false | true |
\|\| | 逻辑或 | false | true | true |
\|\| | 逻辑或 | false | false | false |
! | 逻辑非 | true | 不适用 | false |
! | 逻辑非 | false | 不适用 | true |
③ 布尔型变量在条件判断中的应用:
布尔型变量常用于条件判断语句 (如 if
, while
, for
等) 的条件表达式中,控制程序的执行流程。条件表达式的结果会被隐式转换为布尔值,非零值被视为 true
,零值被视为 false
。
1
int age = 20;
2
bool isAdult = (age >= 18); // 条件表达式的结果是 bool 类型
3
4
if (isAdult) {
5
std::cout << "成年人" << std::endl;
6
} else {
7
std::cout << "未成年人" << std::endl;
8
}
9
10
bool condition1 = true;
11
bool condition2 = false;
12
13
if (condition1 && !condition2) { // 逻辑运算
14
std::cout << "条件成立" << std::endl;
15
}
④ 布尔型与整数的隐式转换:
bool
类型可以隐式转换为整数类型:true
转换为 1,false
转换为 0。整数类型也可以隐式转换为 bool
类型:非零值转换为 true
,零值转换为 false
。
1
bool boolVar = true;
2
int intVar = boolVar; // bool 隐式转换为 int,intVar 的值为 1
3
std::cout << "bool 转换为 int: " << intVar << std::endl;
4
5
int zero = 0;
6
int nonZero = 100;
7
bool boolZero = zero; // int 隐式转换为 bool,boolZero 的值为 false
8
bool boolNonZero = nonZero; // int 隐式转换为 bool,boolNonZero 的值为 true
9
std::cout << "0 转换为 bool: " << boolZero << std::endl;
10
std::cout << "非零值转换为 bool: " << boolNonZero << std::endl;
示例代码:
1
#include <iostream>
2
3
int main() {
4
bool boolTrue = true;
5
bool boolFalse = false;
6
7
std::cout << "bool true: " << boolTrue << std::endl; // 输出 1 (或 true,取决于输出格式)
8
std::cout << "bool false: " << boolFalse << std::endl; // 输出 0 (或 false,取决于输出格式)
9
10
std::cout << "bool 类型大小: " << sizeof(bool) << " 字节" << std::endl;
11
12
bool resultAnd = (boolTrue && boolFalse); // 逻辑与
13
bool resultOr = (boolTrue || boolFalse); // 逻辑或
14
bool resultNotTrue = (!boolTrue); // 逻辑非
15
bool resultNotFalse = (!boolFalse); // 逻辑非
16
17
std::cout << "true && false: " << resultAnd << std::endl; // 输出 0 (false)
18
std::cout << "true || false: " << resultOr << std::endl; // 输出 1 (true)
19
std::cout << "!true: " << resultNotTrue << std::endl; // 输出 0 (false)
20
std::cout << "!false: " << resultNotFalse << std::endl; // 输出 1 (true)
21
22
int number = 10;
23
if (number > 5) { // 条件判断,number > 5 的结果是 bool 类型
24
std::cout << "number 大于 5" << std::endl;
25
}
26
27
return 0;
28
}
这段代码演示了 bool
类型变量的声明、赋值和逻辑运算,以及 bool
类型在条件判断中的应用。代码输出了 bool
类型的字面值、大小以及逻辑运算的结果。
2.2 变量 (Variables) 和常量 (Constants)
阐述变量的声明、初始化、作用域和生命周期,以及常量的定义和使用场景。
2.2.1 变量的声明和初始化
详细讲解变量的声明语法,以及初始化变量的不同方式。
① 变量的声明 (Variable Declaration):
变量 (variables) 是程序中用于存储数据的具名内存位置。在使用变量之前,必须先声明变量的类型和名称。变量声明的语法形式如下:
1
类型 变量名;
其中,类型
(type) 指定了变量可以存储的数据类型,例如 int
, float
, char
, bool
等;变量名
(variable name) 是变量的标识符,用于在程序中引用该变量。变量名需要遵循标识符的命名规则:
⚝ 可以包含字母、数字和下划线 _
。
⚝ 必须以字母或下划线开头。
⚝ 区分大小写 (case-sensitive)。
⚝ 不能与 C++ 关键字 (keywords) 重名。
示例:
1
int age; // 声明一个整型变量 age
2
double salary; // 声明一个双精度浮点型变量 salary
3
char initial; // 声明一个字符型变量 initial
4
bool isStudent; // 声明一个布尔型变量 isStudent
可以同时声明多个同类型变量,变量名之间用逗号 ,
分隔:
1
int x, y, z; // 声明三个整型变量 x, y, z
② 变量的初始化 (Variable Initialization):
变量声明后,可以在声明时或声明后初始化变量,即给变量赋予一个初始值。未初始化的变量,其值是不确定的 (取决于内存中原有的数据,可能是垃圾值)。良好的编程习惯是在声明变量的同时进行初始化。
C++ 提供了多种变量初始化的方式:
⚝ 赋值初始化 (Assignment Initialization): 使用赋值运算符 =
在声明时给变量赋值。
1
int age = 20; // 声明并初始化整型变量 age 为 20
2
double salary = 5000.50; // 声明并初始化双精度浮点型变量 salary 为 5000.50
3
char initial = 'J'; // 声明并初始化字符型变量 initial 为 'J'
4
bool isStudent = true; // 声明并初始化布尔型变量 isStudent 为 true
⚝ 直接初始化 (Direct Initialization): 使用圆括号 ()
在变量名后指定初始值。
1
int age(20);
2
double salary(5000.50);
3
char initial('J');
4
bool isStudent(true);
⚝ 列表初始化 (List Initialization) 或统一初始化 (Uniform Initialization): 使用花括号 {}
在变量名后指定初始值。列表初始化是 C++11 引入的新特性,可以用于各种类型的初始化,包括基本类型、类类型、聚合类型等。列表初始化可以防止窄化转换 (narrowing conversion),即从宽类型向窄类型的隐式转换,例如从 double
到 int
的转换。
1
int age{20};
2
double salary{5000.50};
3
char initial{'J'};
4
bool isStudent{true};
5
int errorInt{3.14}; // 错误!窄化转换,double 到 int,列表初始化会报错
⚝ 默认初始化 (Default Initialization): 如果在声明变量时没有显式初始化,变量会被默认初始化。默认初始化的行为取决于变量的类型和存储位置:
▮▮▮▮⚝ 局部变量 (Local Variables) (在函数或语句块内部声明): 不会进行默认初始化,其值是不确定的。
▮▮▮▮⚝ 全局变量 (Global Variables) (在函数外部声明) 和 静态变量 (Static Variables) (使用 static
关键字声明): 会被默认初始化为零值。数值类型 (整型、浮点型) 初始化为 0,字符型初始化为空字符 '\0'
,布尔型初始化为 false
,指针类型初始化为空指针 nullptr
(C++11)。
1
int globalVar; // 全局变量,默认初始化为 0
2
static int staticVar; // 静态变量,默认初始化为 0
3
4
void func() {
5
int localVar; // 局部变量,未初始化,值不确定
6
std::cout << "全局变量 globalVar: " << globalVar << std::endl; // 输出 0
7
std::cout << "静态变量 staticVar: " << staticVar << std::endl; // 输出 0
8
// std::cout << "局部变量 localVar: " << localVar << std::endl; // 未初始化,读取其值是未定义行为,可能导致错误或输出垃圾值,取消注释可能会有警告
9
}
③ auto
类型推导 (Type Deduction) (C++11):
C++11 引入了 auto
关键字,可以自动推导变量的类型,编译器会根据初始化表达式的类型来确定变量的类型。使用 auto
关键字可以简化代码,提高代码的可读性,尤其是在类型名较长或类型不易确定的情况下。
1
auto age = 20; // age 被推导为 int 类型
2
auto salary = 5000.50; // salary 被推导为 double 类型
3
auto initial = 'J'; // initial 被推导为 char 类型
4
auto isStudent = true; // isStudent 被推导为 bool 类型
5
auto sum = 0; // sum 被推导为 int 类型
6
auto average = (double)sum / 10; // average 被推导为 double 类型
7
8
std::cout << "age 的类型: " << typeid(age).name() << std::endl; // 输出 i (int)
9
std::cout << "salary 的类型: " << typeid(salary).name() << std::endl; // 输出 d (double)
使用 auto
关键字时,必须同时进行初始化,否则编译器无法推导变量的类型。typeid(变量名).name()
可以获取变量的类型名 (编译器相关的表示)。
示例代码:
1
#include <iostream>
2
#include <string>
3
4
int main() {
5
int age = 25; // 赋值初始化
6
double price(99.99); // 直接初始化
7
std::string name{"Alice"}; // 列表初始化
8
char grade = {'A'}; // 列表初始化,注意花括号和单引号
9
bool isAdult = true;
10
11
std::cout << "Age: " << age << std::endl;
12
std::cout << "Price: " << price << std::endl;
13
std::cout << "Name: " << name << std::endl;
14
std::cout << "Grade: " << grade << std::endl;
15
std::cout << "Is Adult: " << isAdult << std::endl;
16
17
auto autoVar = 100; // auto 类型推导,推导为 int
18
std::cout << "autoVar 的值: " << autoVar << ", 类型: " << typeid(autoVar).name() << std::endl;
19
20
return 0;
21
}
这段代码演示了变量的声明和各种初始化方式,包括赋值初始化、直接初始化、列表初始化和 auto
类型推导。代码输出了各种类型变量的值和类型名。
2.2.2 变量的作用域 (Scope) 和生命周期 (Lifetime)
解释局部变量、全局变量、静态变量的作用域和生命周期,以及它们在程序执行过程中的内存分配和释放。
① 作用域 (Scope):
作用域 (scope) 指的是变量在程序中可以被访问和引用的代码区域。C++ 中主要有以下几种作用域:
⚝ 局部作用域 (Local Scope) 或块作用域 (Block Scope): 在函数或语句块 (用花括号 {}
) 内部声明的变量具有局部作用域,称为局部变量 (local variables)。局部变量只能在其声明所在的代码块内部以及嵌套的代码块内部访问。局部变量在声明时分配内存,在代码块执行结束时释放内存,其生命周期 (lifetime) 仅限于代码块的执行期间。
1
void func() {
2
int localVar = 10; // localVar 是局部变量,作用域在 func 函数内部
3
std::cout << "localVar in func: " << localVar << std::endl;
4
} // localVar 的作用域结束,内存被释放
5
6
int main() {
7
func();
8
// std::cout << "localVar in main: " << localVar << std::endl; // 错误!localVar 在 main 函数中不可见,超出作用域
9
return 0;
10
}
⚝ 全局作用域 (Global Scope): 在所有函数外部声明的变量具有全局作用域,称为全局变量 (global variables)。全局变量在整个程序中都是可见的,可以被任何函数访问。全局变量在程序启动时分配内存,在程序结束时释放内存,其生命周期贯穿整个程序的执行期间。
1
int globalVar = 100; // globalVar 是全局变量,作用域是整个程序
2
3
void func() {
4
std::cout << "globalVar in func: " << globalVar << std::endl; // 可以访问全局变量
5
}
6
7
int main() {
8
std::cout << "globalVar in main: " << globalVar << std::endl; // 可以访问全局变量
9
func();
10
return 0;
11
}
⚝ 命名空间作用域 (Namespace Scope): 在命名空间 (namespace) 中声明的变量具有命名空间作用域。命名空间用于组织代码,避免命名冲突。命名空间中的变量在其命名空间内部以及通过命名空间限定符 (如 命名空间名::变量名
) 在外部访问。
1
namespace MyNamespace {
2
int namespaceVar = 200; // namespaceVar 在 MyNamespace 命名空间中
3
}
4
5
int main() {
6
std::cout << "namespaceVar in main: " << MyNamespace::namespaceVar << std::endl; // 通过命名空间限定符访问
7
return 0;
8
}
⚝ 类作用域 (Class Scope): 在类 (class) 中声明的成员变量 (member variables) 具有类作用域。类作用域的变量只能通过类的对象或类名 (对于静态成员变量) 访问。
② 生命周期 (Lifetime) 或生存期 (Duration):
生命周期 (lifetime) 指的是变量从分配内存到释放内存的这段时间。变量的生命周期与其作用域密切相关。C++ 中变量的存储期 (storage duration) 主要有三种:
⚝ 自动存储期 (Automatic Storage Duration): 局部变量 具有自动存储期。当程序执行到局部变量的声明语句时,为变量分配内存空间;当局部变量所在的代码块执行结束时,系统自动释放为该变量分配的内存空间。局部变量的生命周期开始于声明处,结束于代码块结束处。
⚝ 静态存储期 (Static Storage Duration): 全局变量 和 静态局部变量 具有静态存储期。全局变量在程序启动时分配内存,在程序结束时释放内存。静态局部变量在程序第一次执行到其声明语句时分配内存,在程序结束时释放内存。静态存储期的变量在程序的整个运行期间都存在。
⚝ 动态存储期 (Dynamic Storage Duration): 通过动态内存分配 (如 new
运算符) 创建的变量具有动态存储期。动态分配的内存需要手动释放 (使用 delete
运算符),否则会造成内存泄漏 (memory leak)。动态存储期的变量的生命周期由程序员显式控制。
③ 静态变量 (Static Variables):
使用 static
关键字修饰的变量称为静态变量。静态变量可以分为静态局部变量和静态全局变量。
⚝ 静态局部变量 (Static Local Variables): 在函数内部使用 static
关键字声明的变量。静态局部变量具有局部作用域,只能在其声明所在的函数内部访问。但静态局部变量具有静态存储期,其生命周期从程序第一次执行到其声明语句时开始,到程序结束时结束。静态局部变量只在第一次函数调用时初始化,后续函数调用时保持上次调用结束时的值。
1
void func() {
2
static int count = 0; // 静态局部变量,只在第一次调用时初始化
3
count++;
4
std::cout << "count = " << count << std::endl;
5
}
6
7
int main() {
8
func(); // 输出 count = 1
9
func(); // 输出 count = 2
10
func(); // 输出 count = 3
11
return 0;
12
}
⚝ 静态全局变量 (Static Global Variables): 在函数外部使用 static
关键字声明的变量。静态全局变量具有全局作用域 (仅限于声明它的文件内部,C++ 中已不推荐使用静态全局变量,推荐使用匿名命名空间代替)。静态全局变量也具有静态存储期。
示例代码:
1
#include <iostream>
2
3
int globalVar = 10; // 全局变量
4
5
void func() {
6
int localVar = 20; // 局部变量
7
static int staticLocalVar = 30; // 静态局部变量,只初始化一次
8
staticLocalVar++;
9
10
std::cout << "全局变量 globalVar: " << globalVar << std::endl;
11
std::cout << "局部变量 localVar: " << localVar << std::endl;
12
std::cout << "静态局部变量 staticLocalVar: " << staticLocalVar << std::endl;
13
}
14
15
int main() {
16
func(); // 第一次调用 func()
17
func(); // 第二次调用 func(),staticLocalVar 的值会保持
18
// std::cout << "局部变量 localVar in main: " << localVar << std::endl; // 错误,超出作用域
19
std::cout << "全局变量 globalVar in main: " << globalVar << std::endl; // 全局变量在 main 函数中可见
20
return 0;
21
}
这段代码演示了局部变量、全局变量和静态局部变量的作用域和生命周期。静态局部变量 staticLocalVar
在多次函数调用之间保持其值。
2.2.3 常量的定义:const
和 constexpr
介绍 const
和 constexpr
关键字的区别和用法,以及常量在提高代码可读性和性能方面的作用。
常量 (constants) 是指在程序运行期间其值不能被改变的量。C++ 提供了两种主要的关键字来定义常量:const
和 constexpr
。
① const
常量 (Run-time Constants):
const
(constant 的缩写) 关键字用于声明运行时常量 (run-time constants)。const
修饰的变量在声明时必须初始化,并且在程序运行过程中其值不能被修改。const
常量的值可以在运行时确定。
const
可以修饰各种类型的变量,包括基本类型、指针、引用、类对象等。
⚝ const
修饰基本类型变量:
1
const int MAX_VALUE = 100; // const 整型常量
2
const double PI = 3.1415926; // const 浮点型常量
3
const char MESSAGE[] = "Hello"; // const 字符数组 (C 风格字符串常量)
4
5
// MAX_VALUE = 200; // 错误!const 常量不能被修改
⚝ const
修饰指针: const
修饰指针有多种情况,需要区分清楚常量指针 (const pointer) 和 指向常量的指针 (pointer to const)。
▮▮▮▮⚝ 指向常量的指针 (pointer to const): 指针指向的值是常量,不能通过该指针修改所指向的值,但指针本身的值 (即指针的指向) 可以改变。const
关键字在 *
的左侧。
1
const int* ptr1; // 指向 const int 的指针
2
int value = 10;
3
const int constValue = 20;
4
5
ptr1 = &constValue; // OK,指向 const int
6
ptr1 = &value; // OK,指向 int 也可以,int 可以隐式转换为 const int
7
// *ptr1 = 30; // 错误!不能通过指向 const int 的指针修改所指向的值
8
9
int anotherValue = 30;
10
ptr1 = &anotherValue; // OK,指针的指向可以改变
▮▮▮▮⚝ 常量指针 (const pointer): 指针本身是常量,指针的值 (即指针的指向) 不能改变,但可以通过该指针修改所指向的值 (如果所指向的值不是 const
)。const
关键字在 *
的右侧。
1
int* const ptr2 = &value; // const int* 指针,必须在声明时初始化
2
*ptr2 = 40; // OK,可以通过 const 指针修改所指向的值
3
// int newValue = 50;
4
// ptr2 = &newValue; // 错误!const 指针的指向不能改变
▮▮▮▮⚝ 指向常量的常量指针 (const pointer to const): 指针本身和指针指向的值都是常量,都不能被修改。const
关键字在 *
的左右两侧都有。
1
const int* const ptr3 = &constValue; // 指向 const int 的 const 指针,必须在声明时初始化
2
// *ptr3 = 60; // 错误!不能通过指向 const int 的指针修改所指向的值
3
// int yetAnotherValue = 70;
4
// ptr3 = &yetAnotherValue; // 错误!const 指针的指向不能改变
⚝ const
修饰引用 (reference): const
引用是指绑定到常量对象的引用,不能通过 const
引用修改所引用的对象的值。
1
const int& ref1 = constValue; // const 引用绑定到 const int
2
const int& ref2 = value; // const 引用绑定到 int 也可以,int 可以隐式转换为 const int
3
// ref2 = 80; // 错误!不能通过 const 引用修改所引用的值
② constexpr
常量 (Compile-time Constants) (C++11):
constexpr
(constant expression 的缩写) 关键字用于声明编译时常量 (compile-time constants)。constexpr
修饰的变量在编译时就必须能够计算出其值,并且在程序运行过程中其值不能被修改。constexpr
常量的值必须在编译时确定。
constexpr
比 const
的限制更严格,但 constexpr
常量可以用于需要编译时常量的场合,例如数组的长度、模板参数、枚举值、switch
语句的 case
标签等。constexpr
可以提高程序的性能,因为编译时常量可以在编译阶段进行计算和优化。
constexpr
可以修饰基本类型变量和函数 (C++11 起),C++14 后放宽了对 constexpr
函数的限制。
⚝ constexpr
修饰基本类型变量: 初始化表达式必须是常量表达式 (constant expression),即可以在编译时求值的表达式,例如字面量、constexpr
变量、常量表达式函数的调用等。
1
constexpr int SIZE = 10; // constexpr 整型常量,用字面量初始化
2
constexpr int ARRAY_LENGTH = SIZE * 2; // constexpr 整型常量,用 constexpr 变量和常量表达式初始化
3
// constexpr int runtimeValue = getValue(); // 错误!getValue() 不是常量表达式,运行时才能确定值
4
5
int arr[SIZE]; // OK,数组长度可以是 constexpr 常量
6
// int arr2[runtimeValue]; // 错误!数组长度不能是运行时变量
⚝ constexpr
修饰函数 (Constant Expression Functions): constexpr
函数是指返回值和所有参数都是字面值类型,函数体足够简单,可以在编译时求值的函数。constexpr
函数可以接受非 constexpr
类型的参数,但如果参数是非 constexpr
类型,则函数调用只能在运行时求值。
1
constexpr int square(int n) { // constexpr 函数,计算平方
2
return n * n;
3
}
4
5
constexpr int COMPILE_TIME_SQUARE = square(5); // 编译时求值
6
int runtimeValue = 10;
7
int RUNTIME_SQUARE = square(runtimeValue); // 运行时求值,虽然函数是 constexpr 的,但参数不是常量表达式,所以只能运行时求值
8
9
static_assert(COMPILE_TIME_SQUARE == 25, "编译时计算错误"); // 静态断言,在编译时检查条件是否为真,C++11
static_assert
(静态断言) 是 C++11 引入的特性,用于在编译时检查条件是否为真,如果条件为假,则产生编译错误。static_assert
的第一个参数是条件表达式,第二个参数是错误提示字符串。
③ 常量在提高代码可读性和性能方面的作用:
⚝ 提高代码可读性: 使用常量可以给程序中的 magic numbers (魔法数字,即没有明确含义的数字) 赋予有意义的名称,提高代码的可读性和可维护性。例如,使用 const double PI = 3.1415926;
比直接在代码中使用 3.1415926
更易于理解。
⚝ 提高代码安全性: 使用 const
可以防止意外修改不应该被修改的变量,提高代码的安全性。
⚝ 提高程序性能: constexpr
常量可以在编译时进行计算和优化,减少运行时的计算开销,提高程序的性能。编译器还可以对 const
常量进行一些优化,例如将 const
常量存储在只读内存区域,提高程序的效率。
示例代码:
1
#include <iostream>
2
3
int main() {
4
const int MAX_SIZE = 100; // const 常量
5
constexpr double GRAVITY = 9.8; // constexpr 常量
6
7
int array[MAX_SIZE]; // OK,const 常量可以用于数组长度
8
// int array2[GRAVITY]; // 错误!constexpr 浮点数不能直接用于数组长度,需要转换为整型
9
int array2[static_cast<int>(GRAVITY)]; // OK,强制类型转换
10
11
std::cout << "MAX_SIZE: " << MAX_SIZE << std::endl;
12
std::cout << "GRAVITY: " << GRAVITY << std::endl;
13
14
constexpr int compileTimeValue = 5 * 5; // constexpr 表达式,编译时求值
15
std::cout << "编译时常量 compileTimeValue: " << compileTimeValue << std::endl;
16
17
int runtimeValue = 10;
18
const int runTimeConst = runtimeValue * 2; // const 常量,运行时求值
19
std::cout << "运行时常量 runTimeConst: " << runTimeConst << std::endl;
20
21
return 0;
22
}
这段代码演示了 const
和 constexpr
常量的定义和使用,以及 constexpr
常量在编译时求值的特性。代码展示了 const
常量可以用于数组长度,constexpr
常量也可以用于编译时计算。
2.3 运算符 (Operators) 与表达式 (Expressions)
系统讲解C++的各种运算符,包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等,以及表达式的构成和求值规则。
2.3.1 算术运算符 (Arithmetic Operators)
介绍加、减、乘、除、取余等算术运算符,以及运算符的优先级和结合性。
算术运算符 (arithmetic operators) 用于执行基本的数学运算,包括加法、减法、乘法、除法、取余等。C++ 提供的算术运算符包括:
运算符 | 名称 | 描述 | 示例 |
---|---|---|---|
+ | 加法运算符 | 将两个操作数相加 | a + b |
- | 减法运算符 | 将第一个操作数减去第二个操作数 | a - b |
* | 乘法运算符 | 将两个操作数相乘 | a * b |
/ | 除法运算符 | 将第一个操作数除以第二个操作数 | a / b |
% | 取余运算符 | 计算第一个操作数除以第二个操作数的余数 | a % b |
++ | 自增运算符 | 将操作数的值加 1 (前缀和后缀形式) | ++a , a++ |
-- | 自减运算符 | 将操作数的值减 1 (前缀和后缀形式) | --a , a-- |
+ | 正号运算符 | 返回操作数的值 (一元运算符) | +a |
- | 负号运算符 | 返回操作数的相反数 (一元运算符) | -a |
① 二元算术运算符 (Binary Arithmetic Operators): +
, -
, *
, /
, %
是二元运算符,需要两个操作数。
⚝ 加法 +
、减法 -
、乘法 *
: 对整型和浮点型操作数都适用,执行标准的加法、减法和乘法运算。
⚝ 除法 /
: 对整型操作数执行整数除法 (integer division),结果只保留整数部分,舍弃小数部分 (truncation)。例如,5 / 2
的结果是 2
。对浮点型操作数执行浮点除法 (floating-point division),结果是浮点数。例如,5.0 / 2.0
的结果是 2.5
。
⚝ 取余 %
: 只适用于整型操作数,计算第一个操作数除以第二个操作数的余数 (remainder)。余数的符号与第一个操作数的符号相同。例如,5 % 2
的结果是 1
,-5 % 2
的结果是 -1
,5 % -2
的结果是 1
,-5 % -2
的结果是 -1
。如果第二个操作数为 0,则行为是未定义的 (通常会产生运行时错误)。
② 一元算术运算符 (Unary Arithmetic Operators): ++
, --
, +
, -
也可以作为一元运算符使用,只需要一个操作数。
⚝ 自增 ++
和自减 --
: ++
将操作数的值加 1,--
将操作数的值减 1。自增和自减运算符有前缀 (prefix) 和 后缀 (postfix) 两种形式:
▮▮▮▮⚝ 前缀形式 (e.g., ++a
, --a
): 先自增/自减操作数的值,然后返回自增/自减后的值。
▮▮▮▮⚝ 后缀形式 (e.g., a++
, a--
): 先返回操作数的原始值,然后自增/自减操作数的值。
1
int a = 5;
2
int b = ++a; // 前缀自增,a 先自增为 6,然后赋值给 b,b 的值为 6,a 的值为 6
3
int c = a++; // 后缀自增,先将 a 的原始值 6 赋值给 c,c 的值为 6,然后 a 自增为 7,a 的值为 7
4
5
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl; // 输出 a = 7, b = 6, c = 6
6
7
int d = 10;
8
int e = --d; // 前缀自减,d 先自减为 9,然后赋值给 e,e 的值为 9,d 的值为 9
9
int f = d--; // 后缀自减,先将 d 的原始值 9 赋值给 f,f 的值为 9,然后 d 自减为 8,d 的值为 8
10
11
std::cout << "d = " << d << ", e = " << e << ", f = " << f << std::endl; // 输出 d = 8, e = 9, f = 9
在表达式中,前缀形式的自增/自减运算符通常比后缀形式效率更高,因为后缀形式需要保存操作数的原始值以便返回。如果不需要使用原始值,应优先使用前缀形式。
⚝ 正号 +
和负号 -
: 一元正号运算符 +
通常不起作用,只是为了代码的对称性和可读性。一元负号运算符 -
返回操作数的相反数。
1
int g = 10;
2
int h = +g; // h 的值仍然是 10
3
int i = -g; // i 的值为 -10
4
5
std::cout << "g = " << g << ", h = " << h << ", i = " << i << std::endl; // 输出 g = 10, h = 10, i = -10
③ 运算符的优先级 (Precedence) 和结合性 (Associativity):
算术运算符有优先级 (precedence) 和 结合性 (associativity) 的规则,决定了表达式中多个运算符的求值顺序。
⚝ 优先级: 优先级高的运算符先于优先级低的运算符进行计算。算术运算符的优先级从高到低依次为:
1. 一元运算符: ++
, --
, +
, -
(正号和负号)
2. 乘法、除法、取余: *
, /
, %
3. 加法、减法: +
, -
(加法和减法)
⚝ 结合性: 当表达式中出现多个优先级相同的运算符时,结合性决定了运算顺序。算术运算符的结合性通常是从左到右 (left-to-right),除了赋值运算符和一元运算符是从右到左 (right-to-left)。
1
int result1 = 2 + 3 * 4; // 先算乘法 3 * 4 = 12,再算加法 2 + 12 = 14,结果为 14
2
int result2 = (2 + 3) * 4; // 使用括号改变运算顺序,先算加法 2 + 3 = 5,再算乘法 5 * 4 = 20,结果为 20
3
int result3 = 10 - 5 - 2; // 减法运算符从左到右结合,先算 10 - 5 = 5,再算 5 - 2 = 3,结果为 3
4
int result4 = 10 - (5 - 2); // 使用括号改变运算顺序,先算 5 - 2 = 3,再算 10 - 3 = 7,结果为 7
可以使用括号 ()
来显式地改变运算符的优先级和结合性,括号内的表达式会优先计算。为了提高代码的可读性,建议在复杂的表达式中使用括号来明确运算顺序。
示例代码:
1
#include <iostream>
2
3
int main() {
4
int a = 10, b = 3;
5
double x = 7.5, y = 2.0;
6
7
std::cout << "a + b = " << a + b << std::endl; // 加法
8
std::cout << "a - b = " << a - b << std::endl; // 减法
9
std::cout << "a * b = " << a * b << std::endl; // 乘法
10
std::cout << "a / b = " << a / b << std::endl; // 整数除法
11
std::cout << "a % b = " << a % b << std::endl; // 取余
12
13
std::cout << "x / y = " << x / y << std::endl; // 浮点除法
14
15
int c = 5;
16
std::cout << "c++ = " << c++ << ", c = " << c << std::endl; // 后缀自增
17
c = 5; // 重置 c 的值
18
std::cout << "++c = " << ++c << ", c = " << c << std::endl; // 前缀自增
19
20
int result = 2 + 3 * 4 / 2 - 1; // 运算符优先级和结合性
21
std::cout << "2 + 3 * 4 / 2 - 1 = " << result << std::endl; // 结果为 7
22
23
return 0;
24
}
这段代码演示了各种算术运算符的使用,包括二元运算符和一元运算符,以及前缀和后缀自增/自减运算符的区别。代码还展示了整数除法和浮点除法的区别,以及运算符的优先级和结合性规则。
2.3.2 关系运算符 (Relational Operators) 和逻辑运算符 (Logical Operators)
讲解等于、不等于、大于、小于等关系运算符,以及与、或、非等逻辑运算符,用于条件判断。
① 关系运算符 (Relational Operators):
关系运算符 (relational operators) 用于比较两个操作数之间的关系,返回一个 bool
类型的值,表示比较结果是真 (true) 还是假 (false)。C++ 提供的关系运算符包括:
运算符 | 名称 | 描述 | 示例 |
---|---|---|---|
== | 等于运算符 | 检查两个操作数是否相等 | a == b |
!= | 不等于运算符 | 检查两个操作数是否不相等 | a != b |
> | 大于运算符 | 检查第一个操作数是否大于第二个操作数 | a > b |
< | 小于运算符 | 检查第一个操作数是否小于第二个操作数 | a < b |
>= | 大于等于运算符 | 检查第一个操作数是否大于或等于第二个操作数 | a >= b |
<= | 小于等于运算符 | 检查第一个操作数是否小于或等于第二个操作数 | a <= b |
关系运算符可以用于整型、浮点型和字符型操作数。对于浮点型操作数,由于精度问题,不应直接使用 ==
和 !=
比较是否相等,应使用容差 (tolerance) 或阈值 (epsilon) 进行近似比较 (如前文所述)。对于字符型操作数,关系运算符比较的是字符的 ASCII 码值。
1
int a = 10, b = 20;
2
double x = 10.0, y = 10.0 + 1e-7; // y 和 x 近似相等
3
char ch1 = 'A', ch2 = 'B';
4
5
std::cout << "(a == b) is " << (a == b) << std::endl; // false (0)
6
std::cout << "(a != b) is " << (a != b) << std::endl; // true (1)
7
std::cout << "(a > b) is " << (a > b) << std::endl; // false (0)
8
std::cout << "(a < b) is " << (a < b) << std::endl; // true (1)
9
std::cout << "(a >= b) is " << (a >= b) << std::endl; // false (0)
10
std::cout << "(a <= b) is " << (a <= b) << std::endl; // true (1)
11
12
std::cout << "(x == y) is " << (x == y) << std::endl; // 可能为 false,由于浮点数精度问题
13
std::cout << "(std::abs(x - y) < 1e-6) is " << (std::abs(x - y) < 1e-6) << std::endl; // true,近似相等
14
15
std::cout << "(ch1 < ch2) is " << (ch1 < ch2) << std::endl; // true,'A' 的 ASCII 码值小于 'B'
② 逻辑运算符 (Logical Operators):
逻辑运算符 (logical operators) 用于组合或修改布尔表达式,返回一个 bool
类型的值。C++ 提供的逻辑运算符包括:
运算符 | 名称 | 描述 | 示例 |
---|---|---|---|
&& | 逻辑与 | 当且仅当两个操作数都为真时,结果为真 | a && b |
\|\| | 逻辑或 | 当两个操作数中至少有一个为真时,结果为真 | a \|\| b |
! | 逻辑非 | 对操作数取反,真变为假,假变为真 (一元运算符) | !a |
逻辑运算符的操作数必须是可以转换为布尔类型的值,例如 bool
类型、整型 (非零值为真,零值为假)、指针 (非空指针为真,空指针为假) 等。
⚝ 逻辑与 &&
(Logical AND): 只有当两个操作数都为真时,结果才为真,否则为假。逻辑与运算符具有短路求值 (short-circuit evaluation) 的特性:如果第一个操作数为假,则整个表达式的结果一定是假,不再计算第二个操作数。
1
bool condition1 = true;
2
bool condition2 = false;
3
bool condition3 = true;
4
5
std::cout << "(condition1 && condition3) is " << (condition1 && condition3) << std::endl; // true
6
std::cout << "(condition1 && condition2) is " << (condition1 && condition2) << std::endl; // false
7
std::cout << "(condition2 && condition3) is " << (condition2 && condition3) << std::endl; // false
8
9
int x = 0;
10
bool result = (condition2 && (++x > 0)); // 短路求值,condition2 为 false,不再计算 (++x > 0)
11
std::cout << "x = " << x << ", result = " << result << std::endl; // 输出 x = 0, result = false,x 的值没有改变
⚝ 逻辑或 \|\|
(Logical OR): 只要两个操作数中至少有一个为真,结果就为真,只有当两个操作数都为假时,结果才为假。逻辑或运算符也具有短路求值的特性:如果第一个操作数为真,则整个表达式的结果一定是真,不再计算第二个操作数。
1
std::cout << "(condition1 || condition2) is " << (condition1 || condition2) << std::endl; // true
2
std::cout << "(condition2 || condition3) is " << (condition2 || condition3) << std::endl; // true
3
std::cout << "(condition2 || condition2) is " << (condition2 || condition2) << std::endl; // false
4
5
int y = 0;
6
bool result2 = (condition1 || (++y > 0)); // 短路求值,condition1 为 true,不再计算 (++y > 0)
7
std::cout << "y = " << y << ", result2 = " << result2 << std::endl; // 输出 y = 0, result2 = true,y 的值没有改变
⚝ 逻辑非 !
(Logical NOT): 一元运算符,对操作数取反。如果操作数为真,结果为假;如果操作数为假,结果为真。
1
std::cout << "(!condition1) is " << (!condition1) << std::endl; // false
2
std::cout << "(!condition2) is " << (!condition2) << std::endl; // true
③ 关系运算符和逻辑运算符的优先级和结合性:
关系运算符和逻辑运算符也有优先级和结合性的规则。运算符的优先级从高到低依次为:
- 逻辑非
!
- 关系运算符:
>
,<
,>=
,<=
- 相等性运算符:
==
,!=
- 逻辑与
&&
- 逻辑或
\|\|
结合性:
⚝ 一元逻辑非运算符 !
是 从右到左 (right-to-left) 结合的。
⚝ 二元关系运算符和逻辑运算符是 从左到右 (left-to-right) 结合的。
可以使用括号 ()
来显式地改变运算符的优先级和结合性。
示例代码:
1
#include <iostream>
2
#include <cmath>
3
4
int main() {
5
int age = 25;
6
double score = 85.5;
7
bool isStudent = true;
8
9
bool condition1 = (age > 18 && score >= 60); // 逻辑与和关系运算符
10
bool condition2 = (isStudent || score >= 90); // 逻辑或和关系运算符
11
bool condition3 = !(age < 16); // 逻辑非和关系运算符
12
13
std::cout << "condition1 (age > 18 && score >= 60): " << condition1 << std::endl; // true
14
std::cout << "condition2 (isStudent || score >= 90): " << condition2 << std::endl; // true
15
std::cout << "condition3 (!(age < 16)): " << condition3 << std::endl; // true
16
17
int x = 5, y = 10;
18
bool result = (x > 0 && y < 20 || x == y); // 复杂的逻辑表达式,注意优先级和结合性
19
std::cout << "(x > 0 && y < 20 || x == y): " << result << std::endl; // true,等价于 ((x > 0 && y < 20) || (x == y))
20
21
return 0;
22
}
这段代码演示了关系运算符和逻辑运算符的使用,包括等于、不等于、大于、小于、大于等于、小于等于、逻辑与、逻辑或、逻辑非。代码展示了如何使用关系运算符和逻辑运算符构建复杂的条件表达式,以及逻辑运算符的短路求值特性。
2.3.3 赋值运算符 (Assignment Operators) 和复合赋值运算符
介绍赋值运算符 =
以及 +=
, -=
, *=
, /=
, %=
等复合赋值运算符。
① 赋值运算符 =
(Assignment Operator):
赋值运算符 =
用于将右侧操作数的值赋给左侧的操作数 (变量)。赋值运算符是从右到左 (right-to-left) 结合的。
1
int a;
2
a = 10; // 将 10 赋值给变量 a
3
double x = 3.14; // 声明并初始化变量 x
4
5
int b = a; // 将变量 a 的值赋值给变量 b
赋值运算符的左侧操作数必须是可修改的左值 (lvalue),通常是变量。右侧操作数可以是任何表达式,其值类型必须与左侧操作数的类型兼容,或者可以隐式转换为左侧操作数的类型。
赋值表达式本身也有一个值,其值就是赋值后左侧操作数的值。赋值表达式可以作为子表达式出现在更大的表达式中。
1
int c;
2
int d = (c = 20); // 先将 20 赋值给 c,然后将赋值表达式 (c = 20) 的值 (即 20) 赋值给 d,c 和 d 的值都为 20
3
std::cout << "c = " << c << ", d = " << d << std::endl; // 输出 c = 20, d = 20
4
5
int e = 5;
6
int f = 10;
7
e = f = 15; // 连续赋值,从右到左结合,先将 15 赋值给 f,然后将赋值表达式 (f = 15) 的值 (即 15) 赋值给 e,e 和 f 的值都为 15
8
std::cout << "e = " << e << ", f = " << f << std::endl; // 输出 e = 15, f = 15
② 复合赋值运算符 (Compound Assignment Operators):
复合赋值运算符 (compound assignment operators) 是将算术运算符或其他运算符与赋值运算符 =
组合而成的运算符,用于简化代码,提高代码的可读性和效率。C++ 提供的复合赋值运算符包括:
运算符 | 示例 | 等价于 | 描述 |
---|---|---|---|
+= | a += b | a = a + b | 加法赋值:将 a + b 的结果赋值给 a |
-= | a -= b | a = a - b | 减法赋值:将 a - b 的结果赋值给 a |
*= | a *= b | a = a * b | 乘法赋值:将 a * b 的结果赋值给 a |
/= | a /= b | a = a / b | 除法赋值:将 a / b 的结果赋值给 a |
%= | a %= b | a = a % b | 取余赋值:将 a % b 的结果赋值给 a |
&= | a &= b | a = a & b | 按位与赋值:将 a & b 的结果赋值给 a |
\|= | a \|= b | a = a \| b | 按位或赋值:将 a \| b 的结果赋值给 a |
^= | a ^= b | a = a ^ b | 按位异或赋值:将 a ^ b 的结果赋值给 a |
<<= | a <<= b | a = a << b | 左移赋值:将 a << b 的结果赋值给 a |
>>= | a >>= b | a = a >> b | 右移赋值:将 a >> b 的结果赋值给 a |
复合赋值运算符的左侧操作数必须是可修改的左值,右侧操作数可以是任何表达式,其值类型必须与左侧操作数的类型兼容,或者可以隐式转换为左侧操作数的类型。
复合赋值运算符的效率通常比等价的普通赋值运算符更高,因为复合赋值运算符只对左侧操作数求值一次,而普通赋值运算符需要对左侧操作数求值两次 (一次取值,一次赋值)。
1
int g = 10;
2
g += 5; // 等价于 g = g + 5,g 的值为 15
3
std::cout << "g += 5: " << g << std::endl; // 输出 g += 5: 15
4
5
int h = 20;
6
h -= 8; // 等价于 h = h - 8,h 的值为 12
7
std::cout << "h -= 8: " << h << std::endl; // 输出 h -= 8: 12
8
9
int i = 3;
10
i *= 4; // 等价于 i = i * 4,i 的值为 12
11
std::cout << "i *= 4: " << i << std::endl; // 输出 i *= 4: 12
12
13
int j = 25;
14
j /= 5; // 等价于 j = j / 5,j 的值为 5
15
std::cout << "j /= 5: " << j << std::endl; // 输出 j /= 5: 5
16
17
int k = 17;
18
k %= 3; // 等价于 k = k % 3,k 的值为 2
19
std::cout << "k %= 3: " << k << std::endl; // 输出 k %= 3: 2
③ 赋值运算符的优先级和结合性:
赋值运算符 (包括普通赋值运算符和复合赋值运算符) 的优先级非常低,仅高于逗号运算符。赋值运算符是 从右到左 (right-to-left) 结合的。
1
int m;
2
int n;
3
m = n = 100; // 连续赋值,从右到左结合,先将 100 赋值给 n,然后将赋值表达式 (n = 100) 的值 (即 100) 赋值给 m
4
std::cout << "m = " << m << ", n = " << n << std::endl; // 输出 m = 100, n = 100
5
6
int p = 5;
7
int q = 2 + (p += 3); // 先计算 (p += 3),p 的值变为 8,(p += 3) 的值也为 8,然后计算 2 + 8 = 10,赋值给 q,q 的值为 10
8
std::cout << "p = " << p << ", q = " << q << std::endl; // 输出 p = 8, q = 10
示例代码:
1
#include <iostream>
2
3
int main() {
4
int x, y, z;
5
6
x = 5; // 赋值运算符
7
std::cout << "x = " << x << std::endl;
8
9
y = x = 10; // 连续赋值
10
std::cout << "y = " << y << ", x = " << x << std::endl;
11
12
z += x; // 复合赋值运算符,等价于 z = z + x (假设 z 之前已声明并初始化)
13
std::cout << "z += x (假设 z 初始化为 0), z = " << z << std::endl; // 假设 z 初始化为 0,输出 10
14
15
int a = 5;
16
a *= (2 + 3); // 复合赋值运算符,右侧可以是复杂表达式
17
std::cout << "a *= (2 + 3), a = " << a << std::endl; // 输出 25
18
19
return 0;
20
}
这段代码演示了赋值运算符和复合赋值运算符的使用,包括普通赋值运算符、连续赋值和各种复合赋值运算符。代码展示了赋值运算符的右结合性,以及复合赋值运算符的简化代码和提高效率的作用。
2.3.4 位运算符 (Bitwise Operators)
讲解按位与、按位或、按位异或、按位取反、左移、右移等位运算符,及其在底层编程中的应用。
位运算符 (bitwise operators) 用于对整数类型的操作数进行位 (bit) 级别的操作。位运算符将操作数视为二进制位序列进行处理。C++ 提供的位运算符包括:
运算符 | 名称 | 描述 | 示例 |
---|---|---|---|
& | 按位与 | 对两个操作数的每一位进行逻辑与运算 | a & b |
\| | 按位或 | 对两个操作数的每一位进行逻辑或运算 | a \| b |
^ | 按位异或 | 对两个操作数的每一位进行逻辑异或运算 | a ^ b |
~ | 按位取反 | 对操作数的每一位进行逻辑非运算 (一元运算符) | ~a |
<< | 左移 | 将操作数的所有位向左移动指定的位数 | a << n |
>> | 右移 | 将操作数的所有位向右移动指定的位数 | a >> n |
位运算符只适用于整数类型的操作数 (如 int
, unsigned int
, char
等)。位运算符将操作数视为二进制补码表示的整数进行位运算。
① 按位与 &
(Bitwise AND):
按位与运算符 &
对两个操作数的每一位进行逻辑与运算。只有当两个操作数的对应位都为 1 时,结果的对应位才为 1,否则为 0。
操作数 1 的位 | 操作数 2 的位 | 结果的位 (操作数 1 & 操作数 2 ) |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
示例:
1
二进制数 10 (十进制) 的二进制表示 (假设 8 位): 00001010
2
& 二进制数 12 (十进制) 的二进制表示 (假设 8 位): 00001100
3
--------------------------------------------------
4
按位与的结果 : 00001000 (十进制 8)
1
unsigned int a = 10; // 二进制: 00001010
2
unsigned int b = 12; // 二进制: 00001100
3
unsigned int resultAnd = a & b; // 按位与,结果二进制: 00001000,十进制: 8
4
std::cout << "a & b = " << resultAnd << std::endl; // 输出 8
② 按位或 \|
(Bitwise OR):
按位或运算符 \|
对两个操作数的每一位进行逻辑或运算。只要两个操作数的对应位中至少有一个为 1 时,结果的对应位就为 1,只有当两个操作数的对应位都为 0 时,结果的对应位才为 0。
操作数 1 的位 | 操作数 2 的位 | 结果的位 (操作数 1 \| 操作数 2 ) |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
示例:
1
二进制数 10 (十进制) 的二进制表示 (假设 8 位): 00001010
2
| 二进制数 12 (十进制) 的二进制表示 (假设 8 位): 00001100
3
--------------------------------------------------
4
按位或的结果 : 00001110 (十进制 14)
1
unsigned int resultOr = a \| b; // 按位或,结果二进制: 00001110,十进制: 14
2
std::cout << "a | b = " << resultOr << std::endl; // 输出 14
③ 按位异或 ^
(Bitwise XOR):
按位异或运算符 ^
对两个操作数的每一位进行逻辑异或运算。当两个操作数的对应位不同时,结果的对应位为 1,当两个操作数的对应位相同时,结果的对应位为 0。
操作数 1 的位 | 操作数 2 的位 | 结果的位 (操作数 1 ^ 操作数 2 ) |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
示例:
1
二进制数 10 (十进制) 的二进制表示 (假设 8 位): 00001010
2
^ 二进制数 12 (十进制) 的二进制表示 (假设 8 位): 00001100
3
--------------------------------------------------
4
按位异或的结果 : 00000110 (十进制 6)
1
unsigned int resultXor = a ^ b; // 按位异或,结果二进制: 00000110,十进制: 6
2
std::cout << "a ^ b = " << resultXor << std::endl; // 输出 6
④ 按位取反 ~
(Bitwise NOT):
按位取反运算符 ~
是一元运算符,对操作数的每一位进行逻辑非运算,即 0 变为 1,1 变为 0。
操作数的位 | 结果的位 (~操作数 ) |
---|---|
0 | 1 |
1 | 0 |
示例:
1
二进制数 10 (十进制) 的二进制表示 (假设 8 位): 00001010
2
~ 按位取反的结果 : 11110101 (二进制补码表示的负数,十进制 -11,假设 int 是 32 位,则高位都是 1)
1
unsigned int resultNot = ~a; // 按位取反,结果二进制: 1111...11110101 (假设 unsigned int 是 32 位),十进制: 4294967285 (对于 32 位 unsigned int)
2
std::cout << "~a = " << resultNot << std::endl; // 输出 4294967285 (对于 32 位 unsigned int)
⑤ 左移 <<
(Left Shift):
左移运算符 <<
将操作数的所有位向左移动指定的位数。左移运算会在右侧空出的位补 0。左移 n
位相当于将操作数乘以 \(2^n\) (在不溢出的情况下)。
示例:
1
二进制数 10 (十进制) 的二进制表示 (假设 8 位): 00001010
2
<< 左移 2 位 : 00101000 (十进制 40)
1
unsigned int resultLeftShift = a << 2; // 左移 2 位,结果二进制: 00101000,十进制: 40
2
std::cout << "a << 2 = " << resultLeftShift << std::endl; // 输出 40
⑥ 右移 >>
(Right Shift):
右移运算符 >>
将操作数的所有位向右移动指定的位数。右移运算的左侧空出的位的填充方式取决于操作数的类型:
⚝ 对于无符号整数 (unsigned integer): 左侧空出的位补 0,称为逻辑右移 (logical right shift) 或 无符号右移 (unsigned right shift)。
⚝ 对于有符号整数 (signed integer): 左侧空出的位可能补 0 (逻辑右移),也可能补符号位 (最高位) (算术右移 (arithmetic right shift)),具体取决于编译器和平台,称为算术右移 (arithmetic right shift) 或 有符号右移 (signed right shift)。大多数编译器对有符号整数执行算术右移,以保持符号不变。
右移 n
位相当于将操作数除以 \(2^n\) (对于正数和无符号数,在不丢失精度的情况下)。
示例 (假设对 unsigned int
进行逻辑右移):
1
二进制数 40 (十进制) 的二进制表示 (假设 8 位): 00101000
2
>> 右移 2 位 : 00001010 (十进制 10)
1
unsigned int resultRightShift = resultLeftShift >> 2; // 右移 2 位 (逻辑右移),结果二进制: 00001010,十进制: 10
2
std::cout << "resultLeftShift >> 2 = " << resultRightShift << std::endl; // 输出 10
3
4
int negativeNum = -10; // 二进制补码表示 (假设 32 位):...11110110
5
int resultSignedRightShift = negativeNum >> 2; // 右移 2 位 (算术右移,假设补符号位),结果二进制: ...11111101,十进制: -3 (大多数编译器)
6
std::cout << "negativeNum >> 2 = " << resultSignedRightShift << std::endl; // 输出 -3 (大多数编译器)
⑦ 位运算符的应用:
位运算符在底层编程、嵌入式系统、硬件驱动、图形图像处理、密码学等领域有广泛应用,例如:
⚝ 位掩码 (Bit Masking): 使用位运算符和掩码 (mask) 来提取或设置整数的特定位。例如,可以使用按位与运算符 &
和掩码来提取一个整数的低 4 位,使用按位或运算符 \|
和掩码来设置一个整数的第 3 位为 1。
⚝ 标志位 (Flags): 使用整数的每一位作为一个标志位,表示一种状态或属性。可以使用位运算符来设置、清除和检查标志位。
⚝ 高效的乘除法: 左移运算 <<
可以实现乘以 \(2^n\) 的效果,右移运算 >>
可以实现除以 \(2^n\) 的效果,位运算比乘除法运算速度更快。
⚝ 交换两个变量的值: 可以使用按位异或运算符 ^
在不使用临时变量的情况下交换两个变量的值 (基于异或运算的自反性:a ^ b ^ b == a
)。
1
int p = 5, q = 10;
2
p = p ^ q;
3
q = p ^ q; // 此时 q = (p ^ q) ^ q = p
4
p = p ^ q; // 此时 p = (p ^ q) ^ p = q
5
std::cout << "交换后 p = " << p << ", q = " << q << std::endl; // 输出 交换后 p = 10, q = 5
⑧ 位运算符的优先级和结合性:
位运算符也有优先级和结合性的规则。运算符的优先级从高到低依次为:
- 按位取反
~
- 移位运算符:
<<
,>>
- 按位与
&
- 按位异或
^
- 按位或
\|
结合性:
⚝ 一元按位取反运算符 ~
是 从右到左 (right-to-left) 结合的。
⚝ 二元位运算符 (按位与、按位或、按位异或、左移、右移) 是 从左到右 (left-to-right) 结合的。
可以使用括号 ()
来显式地改变运算符的优先级和结合性。
示例代码:
1
#include <iostream>
2
3
int main() {
4
unsigned int num = 60; // 二进制: 00111100
5
6
unsigned int mask = 0x0F; // 掩码,二进制: 00001111,十六进制 0x0F
7
unsigned int lowerNibble = num & mask; // 按位与,提取低 4 位
8
std::cout << "低 4 位: " << lowerNibble << std::endl; // 输出 12 (二进制 00001100)
9
10
unsigned int setBitMask = 0x08; // 掩码,二进制: 00001000,十六进制 0x08
11
unsigned int numWithBitSet = num | setBitMask; // 按位或,设置第 4 位为 1 (从右往左数,第 1 位为最低位)
12
std::cout << "设置第 4 位后: " << numWithBitSet << std::endl; // 输出 68 (二进制 01000100)
13
14
unsigned int flags = 0; // 标志位,初始为 0
15
const unsigned int FLAG_A = 0x01; // 标志 A,二进制 00000001
16
const unsigned int FLAG_B = 0x02; // 标志 B,二进制 00000010
17
const unsigned int FLAG_C = 0x04; // 标志 C,二进制 00000100
18
19
flags |= FLAG_A; // 设置标志 A
20
std::cout << "设置标志 A 后 flags: " << flags << std::endl; // 输出 1
21
flags |= FLAG_C; // 设置标志 C
22
std::cout << "设置标志 C 后 flags: " << flags << std::endl; // 输出 5 (二进制 00000101,标志 A 和 C 被设置)
23
24
if (flags & FLAG_A) { // 检查标志 A 是否被设置
25
std::cout << "标志 A 已设置" << std::endl; // 输出 标志 A 已设置
26
}
27
if (!(flags & FLAG_B)) { // 检查标志 B 是否未被设置
28
std::cout << "标志 B 未设置" << std::endl; // 输出 标志 B 未设置
29
}
30
31
flags &= ~FLAG_C; // 清除标志 C
32
std::cout << "清除标志 C 后 flags: " << flags << std::endl; // 输出 1 (二进制 00000001,只有标志 A 被设置)
33
34
return 0;
35
}
这段代码演示了位运算符的常见应用,包括位掩码、标志位、高效乘除法和交换变量值。代码展示了如何使用位运算符来提取、设置和清除整数的特定位,以及如何使用位运算符实现标志位的功能。
2.3.5 运算符的优先级 (Precedence) 和结合性 (Associativity)
详细解释运算符的优先级和结合性规则,以及如何使用括号改变运算顺序。
运算符的优先级 (precedence) 和结合性 (associativity) 是 C++ 中控制表达式求值顺序的两个重要规则。当一个表达式中包含多个运算符时,优先级和结合性决定了哪个运算符先被执行,哪个运算符后被执行。
① 运算符优先级 (Operator Precedence):
运算符优先级决定了不同优先级运算符之间的运算顺序。优先级较高的运算符先于优先级较低的运算符进行计算。例如,乘法运算符 *
的优先级高于加法运算符 +
,因此在表达式 2 + 3 * 4
中,先计算乘法 3 * 4
,再计算加法 2 + 12
。
C++ 运算符的优先级从高到低 (部分运算符):
优先级 | 运算符 | 结合性 |
---|---|---|
1 | :: (作用域解析) | 从左到右 |
2 | () (函数调用、类型转换)、[] (数组下标)、. (成员访问)、-> (成员指针访问)、++ (后缀自增)、-- (后缀自减)、typeid 、dynamic_cast 、static_cast 、reinterpret_cast 、const_cast | 从左到右 |
3 | ++ (前缀自增)、-- (前缀自减)、+ (正号)、- (负号)、! (逻辑非)、~ (按位取反)、* (解引用)、& (取地址)、sizeof 、alignof 、new 、delete | 从右到左 |
4 | .* (成员指针解引用)、->* (成员指针解引用) | 从左到右 |
5 | * (乘法)、/ (除法)、% (取余) | 从左到右 |
6 | + (加法)、- (减法) | 从左到右 |
7 | << (左移)、>> (右移) | 从左到右 |
8 | < (小于)、<= (小于等于)、> (大于)、>= (大于等于) | 从左到右 |
9 | == (等于)、!= (不等于) | 从左到右 |
10 | & (按位与) | 从左到右 |
11 | ^ (按位异或) | 从左到右 |
12 | \| (按位或) | 从左到右 |
13 | && (逻辑与) | 从左到右 |
14 | \|\| (逻辑或) | 从左到右 |
15 | ?: (条件运算符) | 从右到左 |
16 | = (赋值)、+= 、-= 、*= 、/= 、%= 、&= 、\|= 、^= 、<<= 、>>= | 从右到左 |
17 | , (逗号运算符) | 从左到右 |
注意: 完整的运算符优先级表请参考附录 B:运算符优先级 (Operator Precedence) 表。
② 运算符结合性 (Operator Associativity):
运算符结合性决定了同一优先级运算符之间的运算顺序。结合性可以是从左到右 (left-to-right) 或 从右到左 (right-to-left)。例如,减法运算符 -
的结合性是从左到右,因此在表达式 10 - 5 - 2
中,先计算 10 - 5
,再计算 5 - 2
。赋值运算符 =
的结合性是从右到左,因此在表达式 a = b = c
中,先计算 b = c
,再计算 a = (b = c)
。
结合性方向:
⚝ 从左到右 (Left-to-right): 也称为左结合,运算符从左向右依次执行。例如,二元算术运算符 (加、减、乘、除、取余)、关系运算符、逻辑与、逻辑或、位运算符等通常是左结合的。
⚝ 从右到左 (Right-to-left): 也称为右结合,运算符从右向左依次执行。例如,一元运算符 (正号、负号、逻辑非、按位取反、前缀自增/自减、解引用、取地址等)、赋值运算符、条件运算符等通常是右结合的。
③ 使用括号 ()
改变运算顺序:
可以使用括号 ()
来显式地改变运算符的优先级和结合性。括号内的表达式会优先计算,无论括号内的运算符优先级高低。括号可以嵌套使用,内层括号的表达式先于外层括号的表达式计算。
1
int result1 = 2 + 3 * 4; // 乘法优先级高于加法,先算乘法,结果为 14
2
int result2 = (2 + 3) * 4; // 使用括号改变运算顺序,先算加法,结果为 20
3
4
int result3 = 10 - 5 - 2; // 减法运算符从左到右结合,先算左侧减法,结果为 3
5
int result4 = 10 - (5 - 2); // 使用括号改变结合性,先算括号内的减法,结果为 7
6
7
int a = 1, b = 2, c = 3;
8
int result5 = (a = b) + c; // 赋值运算符优先级低于加法,但括号内的赋值表达式先计算,先将 b 的值赋给 a,然后计算 (a + c),结果为 5,a 的值为 2
9
std::cout << "a = " << a << ", result5 = " << result5 << std::endl; // 输出 a = 2, result5 = 5
为了提高代码的可读性和避免歧义,建议在复杂的表达式中使用括号来明确运算顺序,即使括号不是必需的。特别是在涉及多种运算符、优先级和结合性规则不明确时,更应该使用括号。
示例代码:
1
#include <iostream>
2
3
int main() {
4
int a = 10, b = 5, c = 2;
5
6
int result1 = a + b * c; // 乘法优先级高于加法
7
std::cout << "a + b * c = " << result1 << std::endl; // 输出 20 (10 + (5 * 2))
8
9
int result2 = (a + b) * c; // 使用括号改变运算顺序
10
std::cout << "(a + b) * c = " << result2 << std::endl; // 输出 30 ((10 + 5) * 2)
11
12
int result3 = a / b - c; // 除法和减法优先级相同,从左到右结合
13
std::cout << "a / b - c = " << result3 << std::endl; // 输出 -0 ( (10 / 5) - 2, 整数除法,结果为 0, 0 - 2 = -2 ) <- **修正: 结果应为 0,因为整数除法 10/5 = 2, 2-2 = 0**
14
15
int result4 = a / (b - c); // 使用括号改变结合性
16
std::cout << "a / (b - c) = " << result4 << std::endl; // 输出 5 ( 10 / (5 - 2), 整数除法 10/3 = 3, 修正: 10 / (5-2) = 10 / 3 = 3) <- **修正: 结果应为 3,因为整数除法 10/3 = 3**
17
18
19
int x = 1, y = 2, z = 3;
20
int result5 = x = y = z + 4; // 赋值运算符从右到左结合
21
std::cout << "x = y = z + 4, x = " << x << ", y = " << y << ", z = " << z << ", result5 = " << result5 << std::endl; // 输出 x = 7, y = 7, z = 3, result5 = 7
22
23
return 0;
24
}
这段代码演示了运算符的优先级和结合性规则,以及如何使用括号改变运算顺序。代码展示了乘法和加法的优先级,除法和减法的结合性,以及赋值运算符的右结合性。使用括号可以明确代码的运算顺序,提高代码的可读性和可维护性。
2.4 类型转换 (Type Conversion)
介绍C++中的隐式类型转换和显式类型转换,以及类型转换可能带来的问题和注意事项。
类型转换 (type conversion) 是指将一个数据类型的值转换为另一个数据类型的过程。C++ 中类型转换分为两种主要类型:隐式类型转换 (implicit type conversion) 和 显式类型转换 (explicit type conversion)。
2.4.1 隐式类型转换 (Implicit Type Conversion)
讲解C++编译器自动进行的类型转换,例如算术运算中的类型提升。
隐式类型转换 (implicit type conversion),也称为自动类型转换 (automatic type conversion) 或 强制类型转换 (coercion),是指编译器在某些情况下自动进行的类型转换,无需程序员显式指定。隐式类型转换通常发生在以下几种情况:
① 算术运算中的类型提升 (Arithmetic Conversion):
当二元算术运算符 (如 +
, -
, *
, /
等) 的操作数类型不一致时,编译器会进行类型提升 (type promotion),将较低精度或较小范围的类型自动转换为较高精度或较大范围的类型,然后再进行运算。类型提升的规则通常是:
⚝ 整型提升 (Integer Promotion): char
, short
, 枚举类型 (enumeration types)
和 bool
类型的值在参与运算时,会首先被提升为 int
类型 (如果 int
类型可以容纳原始类型的所有值,否则提升为 unsigned int
)。这种提升称为整型提升。
1
char ch = 'A';
2
int i = 10;
3
auto result1 = ch + i; // ch 被提升为 int 类型,然后与 i 相加,result1 的类型为 int
4
5
short s = 100;
6
unsigned int ui = 1000;
7
auto result2 = s + ui; // s 被提升为 int 类型,但 int 类型不能容纳 unsigned int 的所有值,所以 s 和 ui 都被提升为 unsigned int 类型,result2 的类型为 unsigned int
8
9
bool b = true;
10
auto result3 = b + 5; // b 被提升为 int 类型 (true 转换为 1, false 转换为 0),然后与 5 相加,result3 的类型为 int
⚝ 操作数类型转换规则: 在完成整型提升后,如果操作数类型仍然不一致,编译器会按照以下规则进行类型转换 (从低到高精度或范围):
1. 如果一个操作数是 long double
类型,则另一个操作数也会被转换为 long double
类型。
2. 否则,如果一个操作数是 double
类型,则另一个操作数也会被转换为 double
类型。
3. 否则,如果一个操作数是 float
类型,则另一个操作数也会被转换为 float
类型。
4. 否则,操作数都是整型,需要进行整型转换:
▮▮▮▮▮▮▮▮⚝ 如果一个操作数是 unsigned long long
类型,则另一个操作数也会被转换为 unsigned long long
类型。
▮▮▮▮▮▮▮▮⚝ 否则,如果一个操作数是 long long
类型,则另一个操作数会被转换为 long long
类型。
▮▮▮▮▮▮▮▮⚝ 否则,如果一个操作数是 unsigned long
类型,则另一个操作数会被转换为 unsigned long
类型。
▮▮▮▮▮▮▮▮⚝ 否则,如果一个操作数是 long
类型,则另一个操作数会被转换为 long
类型。
▮▮▮▮▮▮▮▮⚝ 否则,如果一个操作数是 unsigned int
类型,则另一个操作数会被转换为 unsigned int
类型。
▮▮▮▮▮▮▮▮⚝ 否则,两个操作数都将是 int
类型 (或经过整型提升后的类型)。
1
int intVar = 10;
2
double doubleVar = 3.14;
3
auto result4 = intVar + doubleVar; // intVar 被转换为 double 类型,然后与 doubleVar 相加,result4 的类型为 double
4
5
float floatVar = 2.5f;
6
auto result5 = floatVar * intVar; // intVar 被转换为 float 类型,然后与 floatVar 相乘,result5 的类型为 float
7
8
long long longLongVar = 10000000000LL;
9
unsigned int unsignedIntVar = 50000;
10
auto result6 = longLongVar - unsignedIntVar; // unsignedIntVar 被转换为 long long 类型,然后与 longLongVar 相减,result6 的类型为 long long
② 赋值运算中的类型转换 (Assignment Conversion):
在赋值运算中,如果赋值运算符右侧表达式的类型与左侧变量的类型不一致,编译器会尝试将右侧表达式的值隐式转换为左侧变量的类型,然后再进行赋值。赋值转换通常是将较高精度或较大范围的类型转换为较低精度或较小范围的类型,可能会导致数据丢失或精度损失 (窄化转换)。
1
double doubleValue = 3.1415926;
2
int intValue = doubleValue; // double 隐式转换为 int,小数部分被截断,intValue 的值为 3
3
4
float floatValue = 3.1415926f;
5
double anotherDoubleValue = floatValue; // float 隐式转换为 double,精度提升
6
7
int largeIntValue = 100000;
8
short shortValue = largeIntValue; // int 隐式转换为 short,如果 short 类型无法容纳 largeIntValue 的值,可能会发生溢出或数据截断
③ 函数参数传递和返回值类型转换 (Function Argument and Return Conversion):
在函数调用时,如果实参的类型与形参的类型不一致,编译器会尝试将实参的值隐式转换为形参的类型。函数返回值也可能发生隐式类型转换,将函数返回值的类型转换为函数声明的返回值类型。
1
void func(double d) { // 形参类型为 double
2
std::cout << "函数参数为: " << d << std::endl;
3
}
4
5
int main() {
6
int intArg = 10;
7
func(intArg); // int 实参隐式转换为 double 形参
8
9
return 0;
10
}
11
12
double func2() { // 函数声明返回值类型为 double
13
int intReturnValue = 20;
14
return intReturnValue; // int 返回值隐式转换为 double 返回值
15
}
④ 其他隐式类型转换:
⚝ 布尔转换 (Boolean Conversion): 在条件判断语句 (如 if
, while
等) 中,条件表达式的结果会被隐式转换为 bool
类型。非零值转换为 true
,零值转换为 false
。指针类型也可以隐式转换为 bool
类型,非空指针转换为 true
,空指针转换为 false
。
1
int num = 10;
2
if (num) { // int 隐式转换为 bool,非零值转换为 true
3
std::cout << "num is not zero" << std::endl;
4
}
5
6
int* ptr = nullptr;
7
if (!ptr) { // 指针隐式转换为 bool,空指针转换为 false,!ptr 为 true
8
std::cout << "ptr is null" << std::endl;
9
}
⚝ 派生类到基类的转换 (Derived-to-Base Conversion): 在面向对象编程中,派生类对象可以隐式转换为基类对象或基类指针/引用。
⑤ 隐式类型转换的潜在问题和注意事项:
⚝ 数据丢失和精度损失 (Narrowing Conversion): 将较高精度或较大范围的类型转换为较低精度或较小范围的类型时,可能会发生数据丢失或精度损失,例如 double
转换为 int
会截断小数部分,int
转换为 short
可能会发生溢出。
⚝ 类型转换的二义性 (Ambiguity): 在某些复杂的类型转换场景中,可能会存在多种可能的隐式类型转换路径,导致编译器无法确定选择哪种转换,从而产生编译错误。
⚝ 可读性和可维护性降低: 过多的隐式类型转换可能会降低代码的可读性和可维护性,使代码意图不明确。
为了避免隐式类型转换带来的潜在问题,建议:
⚝ 尽量保持操作数类型一致,避免不必要的隐式类型转换。
⚝ 在可能发生窄化转换时,使用显式类型转换,明确代码的类型转换意图。
⚝ 注意编译器发出的类型转换警告,及时检查和修正潜在的问题。
示例代码:
1
#include <iostream>
2
3
int main() {
4
int intValue = 10;
5
double doubleValue = 3.14;
6
7
auto sum = intValue + doubleValue; // 隐式类型转换,intValue 转换为 double
8
std::cout << "隐式类型转换: int + double = double, sum = " << sum << ", type = " << typeid(sum).name() << std::endl;
9
10
int truncatedIntValue = doubleValue; // 隐式类型转换,double 转换为 int,窄化转换,数据丢失
11
std::cout << "隐式窄化转换: double to int, truncatedIntValue = " << truncatedIntValue << std::endl;
12
13
bool boolValue = intValue; // 隐式类型转换,int 转换为 bool,非零值转换为 true
14
std::cout << "隐式 bool 转换: int to bool, boolValue = " << boolValue << std::endl;
15
16
return 0;
17
}
这段代码演示了隐式类型转换的几种常见情况,包括算术运算中的类型提升、赋值运算中的类型转换和布尔转换。代码展示了隐式类型转换的发生和结果,以及窄化转换可能导致的数据丢失问题。
2.4.2 显式类型转换 (Explicit Type Conversion):强制类型转换
介绍C风格强制类型转换和C++风格强制类型转换(static_cast
, dynamic_cast
, reinterpret_cast
, const_cast
)的用法和适用场景。
显式类型转换 (explicit type conversion),也称为强制类型转换 (type casting),是指程序员显式地使用类型转换运算符将一个类型的值转换为另一个类型的过程。显式类型转换可以明确代码的类型转换意图,避免隐式类型转换可能带来的潜在问题,并在某些情况下实现编译器无法自动进行的类型转换。
C++ 提供了两种风格的显式类型转换:C 风格强制类型转换 (C-style cast) 和 C++ 风格强制类型转换 (C++-style cast)。
① C 风格强制类型转换 (C-style Cast):
C 风格强制类型转换的语法形式为:
1
(目标类型) 表达式
或
1
目标类型 (表达式) // 函数式风格
C 风格强制类型转换非常简洁,但功能强大且灵活,可以执行各种类型的转换,包括:
⚝ 基本类型之间的转换: 例如 int
到 double
,double
到 int
,int
到 char
等。
⚝ 指针类型之间的转换: 例如 int*
到 void*
,void*
到 int*
,基类指针到派生类指针等。
⚝ const
和 volatile
限定符的转换: 例如移除 const
限定符。
1
double doubleValue = 3.1415926;
2
int intValue = (int)doubleValue; // C 风格强制类型转换,double 转换为 int
3
4
int* intPtr = new int(10);
5
void* voidPtr = (void*)intPtr; // C 风格强制类型转换,int* 转换为 void*
6
int* anotherCharPtr = (int*)voidPtr; // C 风格强制类型转换,void* 转换为 int*
7
8
const int constIntValue = 20;
9
// int* nonConstPtr = &constIntValue; // 错误!不能将 const int* 赋值给 int*
10
int* nonConstPtr = (int*)&constIntValue; // C 风格强制类型转换,移除 const 限定符
11
*nonConstPtr = 30; // 虽然移除了 const 限定符,但修改 const 对象的值是未定义行为,可能导致程序崩溃或其他问题
12
std::cout << "*nonConstPtr = " << *nonConstPtr << ", constIntValue = " << constIntValue << std::endl; // 输出结果可能不确定,取决于编译器优化和内存布局
C 风格强制类型转换的缺点:
⚝ 过于强大和灵活: C 风格强制类型转换可以执行各种类型的转换,包括一些不安全的转换,例如任意指针类型之间的转换,移除 const
限定符等,容易导致程序错误。
⚝ 语义不明确: C 风格强制类型转换的语法形式简单粗暴,没有明确表达类型转换的意图,代码可读性较差。
⚝ 不易于查找和维护: 在大型代码库中,C 风格强制类型转换难以查找和定位,不利于代码维护和错误排查。
② C++ 风格强制类型转换 (C++-style Cast):
为了克服 C 风格强制类型转换的缺点,C++ 引入了四种命名的强制类型转换运算符,也称为 C++ 风格强制类型转换:
⚝ static_cast
: 用于执行静态类型转换,主要用于基本类型之间的转换 (例如 int
到 double
,double
到 int
等) 和 具有继承关系的类类型之间的转换 (例如基类指针到派生类指针,但不进行运行时类型检查)。static_cast
不能用于不相关类型之间的转换 (例如 int*
到 float*
),也不能移除 const
或 volatile
限定符。
1
double doubleValue2 = 3.1415926;
2
int intValue2 = static_cast<int>(doubleValue2); // static_cast,double 转换为 int
3
4
int* intPtr2 = static_cast<int*>(voidPtr); // static_cast,void* 转换为 int*,但 voidPtr 必须指向 int 类型内存,否则可能导致未定义行为
5
// float* floatPtr = static_cast<float*>(intPtr2); // 错误!static_cast 不能用于不相关指针类型之间的转换
6
7
// const int constIntValue2 = 20;
8
// int* nonConstPtr2 = static_cast<int*>(&constIntValue2); // 错误!static_cast 不能移除 const 限定符
⚝ dynamic_cast
: 主要用于具有继承关系的类类型之间的指针或引用转换。dynamic_cast
会进行运行时类型检查 (run-time type checking),确保转换是安全的。如果转换是不安全的 (例如将基类指针转换为不属于基类派生类的派生类指针),dynamic_cast
会返回空指针 (对于指针转换) 或抛出 std::bad_cast
异常 (对于引用转换)。dynamic_cast
要求基类至少包含一个虚函数 (virtual function),以便进行运行时类型检查 (实现多态性)。dynamic_cast
的性能开销较大,因为需要进行运行时类型检查。
1
class Base { public: virtual ~Base() {} }; // 基类,包含虚函数
2
class Derived : public Base {}; // 派生类
3
4
Base* basePtr1 = new Derived(); // 基类指针指向派生类对象
5
Derived* derivedPtr1 = dynamic_cast<Derived*>(basePtr1); // dynamic_cast,基类指针转换为派生类指针,安全转换,derivedPtr1 指向派生类对象
6
if (derivedPtr1) {
7
std::cout << "dynamic_cast 成功,derivedPtr1 指向派生类对象" << std::endl;
8
}
9
10
Base* basePtr2 = new Base(); // 基类指针指向基类对象
11
Derived* derivedPtr2 = dynamic_cast<Derived*>(basePtr2); // dynamic_cast,基类指针转换为派生类指针,不安全转换,derivedPtr2 为空指针
12
if (!derivedPtr2) {
13
std::cout << "dynamic_cast 失败,derivedPtr2 为空指针" << std::endl;
14
}
15
16
Derived derivedObj;
17
Base& baseRef = derivedObj; // 基类引用绑定到派生类对象
18
try {
19
Derived& derivedRef = dynamic_cast<Derived&>(baseRef); // dynamic_cast,基类引用转换为派生类引用,安全转换
20
std::cout << "dynamic_cast 引用转换成功" << std::endl;
21
} catch (const std::bad_cast& e) {
22
std::cerr << "dynamic_cast 引用转换失败: " << e.what() << std::endl;
23
}
24
25
Base baseObj;
26
Base& baseRef2 = baseObj; // 基类引用绑定到基类对象
27
try {
28
Derived& derivedRef2 = dynamic_cast<Derived&>(baseRef2); // dynamic_cast,基类引用转换为派生类引用,不安全转换,抛出 std::bad_cast 异常
29
} catch (const std::bad_cast& e) {
30
std::cerr << "dynamic_cast 引用转换失败: " << e.what() << std::endl; // 输出异常信息
31
}
⚝ reinterpret_cast
: 用于执行底层的数据重新解释 (reinterpretation) 的类型转换。reinterpret_cast
可以将任意指针类型之间 (甚至指针和整数之间) 进行转换,不对类型进行任何检查或调整,只是简单地将内存中的二进制位重新解释为另一种类型。reinterpret_cast
是最不安全的类型转换运算符,容易导致程序错误,应谨慎使用。reinterpret_cast
的典型应用场景包括:
▮▮▮▮⚝ 指针类型之间的强制转换,例如 int*
到 char*
,void*
到 int*
等。
▮▮▮▮⚝ 将指针转换为整数类型,例如将指针地址转换为 unsigned int
或 uintptr_t
(C++11,定义在 <cstdint>
头文件中,用于存储指针的整数类型)。
▮▮▮▮⚝ 将整数类型转换为指针类型,例如将整数地址转换为指针。
1
int intValue3 = 123456789;
2
int* intPtr3 = &intValue3;
3
char* charPtr = reinterpret_cast<char*>(intPtr3); // reinterpret_cast,int* 转换为 char*,只是简单地重新解释内存
4
5
std::cout << "intPtr3 指向的 int 值: " << *intPtr3 << std::endl; // 输出 123456789
6
std::cout << "charPtr 指向的 char 值 (只取 int 的低 1 字节): " << static_cast<int>(*charPtr) << std::endl; // 输出 -27 (123456789 的低 1 字节的补码表示)
7
8
unsigned int address = reinterpret_cast<unsigned int>(intPtr3); // reinterpret_cast,指针转换为整数
9
std::cout << "指针地址转换为整数: " << address << std::endl; // 输出指针地址的整数表示
10
11
int* anotherIntPtr = reinterpret_cast<int*>(address); // reinterpret_cast,整数转换为指针
12
std::cout << "整数转换回指针后指向的 int 值: " << *anotherIntPtr << std::endl; // 输出 123456789,前提是地址仍然有效
⚝ const_cast
: 主要用于移除 const
、volatile
和 __unaligned
限定符。const_cast
只能用于修改类型的限定符,不能改变类型的基本类型。最常见的用途是移除 const
限定符,以便修改原本被声明为 const
的对象。使用 const_cast
移除 const
限定符并修改原本为 const
的对象的值是未定义行为,只有当对象本身不是 const
类型,但被声明为 const
类型时,才可以使用 const_cast
移除 const
限定符并进行修改。
1
const int constIntValue3 = 40;
2
// int* nonConstPtr3 = &constIntValue3; // 错误!不能将 const int* 赋值给 int*
3
int* nonConstPtr3 = const_cast<int*>(&constIntValue3); // const_cast,移除 const 限定符
4
// *nonConstPtr3 = 50; // 未定义行为!constIntValue3 本身是 const 对象,修改其值可能导致程序崩溃或其他问题
5
6
int nonConstIntValue = 60;
7
const int* constPtr = &nonConstIntValue; // 指向 const int 的指针,但 nonConstIntValue 本身不是 const
8
int* modifiablePtr = const_cast<int*>(constPtr); // const_cast,移除 const 限定符
9
*modifiablePtr = 70; // OK,nonConstIntValue 本身不是 const,可以修改
10
std::cout << "*modifiablePtr = " << *modifiablePtr << ", nonConstIntValue = " << nonConstIntValue << std::endl; // 输出 *modifiablePtr = 70, nonConstIntValue = 70
③ C++ 风格强制类型转换的优点:
⚝ 类型安全: C++ 风格强制类型转换运算符功能更明确,限制更严格,可以减少类型转换的错误。例如,static_cast
用于安全的类型转换,dynamic_cast
用于安全的运行时类型检查,reinterpret_cast
用于底层的位重新解释,const_cast
用于移除限定符。
⚝ 语义明确: C++ 风格强制类型转换运算符的名称明确表达了类型转换的意图,代码可读性更好。
⚝ 易于查找和维护: C++ 风格强制类型转换运算符在代码中更容易查找和定位,便于代码维护和错误排查。
C++ 风格强制类型转换的选择原则:
⚝ 优先使用 static_cast
: 在需要进行基本类型之间的转换,或具有继承关系的类类型之间的转换时,优先使用 static_cast
。
⚝ 在需要运行时类型检查的类类型指针或引用转换时,使用 dynamic_cast
。
⚝ 在需要进行底层的数据重新解释的类型转换时,谨慎使用 reinterpret_cast
。
⚝ 只有在需要移除 const
或 volatile
限定符时,才使用 const_cast
,并确保转换的安全性。
⚝ 尽量避免使用 C 风格强制类型转换,除非在某些特殊情况下 (例如 C 遗留代码)。
示例代码:
1
#include <iostream>
2
#include <typeinfo> // 需要包含 typeinfo 头文件来使用 typeid
3
4
class Base { public: virtual ~Base() {} };
5
class Derived : public Base {};
6
7
int main() {
8
double doubleValue = 3.14;
9
int intValue = static_cast<int>(doubleValue); // static_cast
10
11
Base* basePtr = new Derived();
12
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // dynamic_cast
13
14
int* intPtr = new int(100);
15
void* voidPtr = reinterpret_cast<void*>(intPtr); // reinterpret_cast
16
int* anotherIntPtr = static_cast<int*>(voidPtr); // static_cast,void* 转换为 int*
17
18
const int constValue = 200;
19
int* nonConstPtr = const_cast<int*>(&constValue); // const_cast
20
21
std::cout << "double to int (static_cast): " << intValue << ", type: " << typeid(intValue).name() << std::endl;
22
std::cout << "Base* to Derived* (dynamic_cast): " << derivedPtr << std::endl;
23
std::cout << "int* to void* (reinterpret_cast): " << voidPtr << std::endl;
24
std::cout << "void* to int* (static_cast): " << anotherIntPtr << std::endl;
25
std::cout << "const int* to int* (const_cast): " << nonConstPtr << std::endl;
26
27
return 0;
28
}
这段代码演示了 C++ 风格强制类型转换的四种运算符 static_cast
, dynamic_cast
, reinterpret_cast
和 const_cast
的用法和适用场景。代码展示了每种类型转换运算符的语法形式和转换结果,以及它们之间的区别和选择原则。
3. 程序流程控制:语句与结构
3.1 语句 (Statements) 概述
在C++编程中,语句 (Statements) 是构成程序的基本执行单元。程序是由一系列语句组成的,计算机按照语句的顺序逐条执行,完成特定的任务。C++中的语句可以分为以下几种类型:
① 声明语句 (Declaration Statements):用于引入程序中使用的各种实体,如变量、函数、类等,并赋予它们名字和类型。声明语句本身不执行任何操作,而是为编译器提供必要的信息。例如:
1
int age; // 声明一个整型变量 age (age)
2
double price = 99.9; // 声明并初始化一个双精度浮点型变量 price (price)
3
void myFunction(); // 声明一个函数 myFunction (myFunction)
② 表达式语句 (Expression Statements):由一个表达式加上分号 ;
构成。表达式语句的主要作用是执行表达式并产生副作用,例如赋值、函数调用、输入/输出操作等。例如:
1
x = y + 10; // 赋值表达式语句
2
std::cout << "Hello" << std::endl; // 输出表达式语句
3
increment(); // 函数调用表达式语句
③ 控制语句 (Control Statements):用于控制程序的执行流程,决定程序中哪些语句将被执行以及执行的顺序。控制语句是实现程序逻辑的关键,C++提供了多种控制语句,包括:
▮▮▮▮ⓑ 选择语句 (Selection Statements):if
语句和 switch
语句,用于根据条件选择不同的执行路径。
▮▮▮▮ⓒ 循环语句 (Iteration Statements):for
语句、while
语句和 do-while
语句,用于重复执行一段代码,直到满足特定条件为止。
▮▮▮▮ⓓ 跳转语句 (Jump Statements):break
语句、continue
语句和 goto
语句,用于改变程序的执行流程,跳转到程序的其他位置。
④ 复合语句 (Compound Statements),也称为语句块 (Statement Blocks):用一对花括号 {}
括起来的一系列语句。复合语句可以看作是一个整体语句,在语法上等同于单个语句。语句块通常用于在控制语句(如 if
、for
、while
)的控制范围内包含多条语句。例如:
1
if (x > 0) { // 复合语句作为 if 语句的执行体
2
std::cout << "x is positive" << std::endl;
3
y = x * 2;
4
}
语句块可以嵌套使用,形成更复杂的程序结构。语句块内部声明的变量具有块作用域 (Block Scope),只在语句块内部可见和有效。
3.2 选择结构:if
语句
选择结构 (Selection Structures) 允许程序在运行时根据不同的条件执行不同的代码分支。if
语句是C++中最基本和常用的选择结构,它提供了多种形式来实现条件判断和分支执行。
3.2.1 if
语句
if
语句 (if statement) 的基本语法结构如下:
1
if (condition)
2
statement;
或者使用语句块:
1
if (condition) {
2
statement1;
3
statement2;
4
// ...
5
}
执行流程:
① 首先计算 condition
(条件) 表达式的值。condition
可以是任何可以转换为布尔类型 (bool) 的表达式,例如关系表达式、逻辑表达式、变量或函数调用。
② 如果 condition
的值为 true
(真)或非零值,则执行 if
语句后面的 statement
(语句) 或语句块。
③ 如果 condition
的值为 false
(假) 或零值,则跳过 statement
或语句块,继续执行 if
语句之后的代码。
示例:判断一个数是否为正数
1
#include <iostream>
2
3
int main() {
4
int number = 10;
5
if (number > 0) {
6
std::cout << number << " 是正数 (is positive)." << std::endl;
7
}
8
return 0;
9
}
在这个例子中,条件 number > 0
为真,因此会执行 if
语句块中的输出语句。
3.2.2 if-else
语句
if-else
语句 (if-else statement) 在 if
语句的基础上增加了 else
(否则) 分支,用于在条件不成立时执行另一段代码。其语法结构如下:
1
if (condition) {
2
statement1; // 条件为真时执行
3
// ...
4
} else {
5
statement2; // 条件为假时执行
6
// ...
7
}
执行流程:
① 首先计算 condition
表达式的值。
② 如果 condition
的值为 true
,则执行 if
语句块 (statement1
)。
③ 如果 condition
的值为 false
,则执行 else
语句块 (statement2
)。
④ 执行完相应的语句块后,程序继续执行 if-else
语句之后的代码。
示例:判断一个数是奇数还是偶数
1
#include <iostream>
2
3
int main() {
4
int number = 7;
5
if (number % 2 == 0) {
6
std::cout << number << " 是偶数 (is even)." << std::endl;
7
} else {
8
std::cout << number << " 是奇数 (is odd)." << std::endl;
9
}
10
return 0;
11
}
在这个例子中,条件 number % 2 == 0
为假,因此会执行 else
语句块中的输出语句。
3.2.3 if-else if-else
语句
if-else if-else
语句 (if-else if-else statement) 用于处理多分支选择的情况,可以连续判断多个条件,并根据第一个成立的条件执行相应的代码块。其语法结构如下:
1
if (condition1) {
2
statement1; // 条件1为真时执行
3
// ...
4
} else if (condition2) {
5
statement2; // 条件1为假且条件2为真时执行
6
// ...
7
} else if (condition3) {
8
statement3; // 条件1和2为假且条件3为真时执行
9
// ...
10
}
11
// ... 可以有多个 else if 分支
12
else {
13
statement_n; // 所有条件都为假时执行 (可选)
14
// ...
15
}
执行流程:
① 依次计算 condition1
, condition2
, condition3
等条件表达式的值。
② 如果某个条件 condition_i
为 true
,则执行相应的 statement_i
语句块,并且跳过剩余的 else if
和 else
分支,程序继续执行整个 if-else if-else
结构之后的代码。
③ 如果所有 if
和 else if
的条件都为 false
,并且存在 else
分支,则执行 else
语句块 (statement_n
)。
④ 如果所有条件都为 false
,且没有 else
分支,则不执行任何语句块,程序直接执行 if-else if-else
结构之后的代码。
示例:根据分数等级输出评价
1
#include <iostream>
2
3
int main() {
4
int score = 85;
5
if (score >= 90) {
6
std::cout << "优秀 (Excellent)" << std::endl;
7
} else if (score >= 80) {
8
std::cout << "良好 (Good)" << std::endl;
9
} else if (score >= 70) {
10
std::cout << "中等 (Medium)" << std::endl;
11
} else if (score >= 60) {
12
std::cout << "及格 (Pass)" << std::endl;
13
} else {
14
std::cout << "不及格 (Fail)" << std::endl;
15
}
16
return 0;
17
}
在这个例子中,分数 score
为 85,满足条件 score >= 80
,因此输出 "良好 (Good)"。
3.2.4 嵌套 if
语句
嵌套 if
语句 (Nested if statements) 指的是在一个 if
语句或 else
语句块内部再包含其他的 if
语句。通过嵌套 if
语句,可以实现更复杂的条件判断逻辑。
示例:判断一个数是正数、负数还是零,并且进一步判断正数是偶数还是奇数
1
#include <iostream>
2
3
int main() {
4
int number = 12;
5
if (number > 0) {
6
std::cout << number << " 是正数 (is positive)." << std::endl;
7
if (number % 2 == 0) { // 嵌套的 if 语句
8
std::cout << number << " 是正偶数 (is positive and even)." << std::endl;
9
} else {
10
std::cout << number << " 是正奇数 (is positive and odd)." << std::endl;
11
}
12
} else if (number < 0) {
13
std::cout << number << " 是负数 (is negative)." << std::endl;
14
} else {
15
std::cout << number << " 是零 (is zero)." << std::endl;
16
}
17
return 0;
18
}
在这个例子中,外层的 if-else if-else
结构判断数的正负性,内层的 if-else
结构(嵌套在第一个 if
语句块中)进一步判断正数的奇偶性。
注意事项:
⚝ 嵌套 if
语句可以增加代码的复杂度,过度嵌套会降低代码的可读性和可维护性。
⚝ 在编写嵌套 if
语句时,要清晰地组织代码结构,使用适当的缩进,以提高代码的可读性。
⚝ 对于多分支选择的情况,可以考虑使用 if-else if-else
语句或 switch
语句,以简化代码结构。
3.3 选择结构:switch
语句
switch
语句 (switch statement) 是另一种选择结构,它提供了一种更简洁的方式来处理多分支选择,特别是当需要根据一个表达式的值与多个常量值进行比较时。
3.3.1 switch
语句的语法和执行流程
switch
语句的语法结构如下:
1
switch (expression) {
2
case constant-expression1:
3
statement1;
4
// ...
5
break; // 可选的 break 语句
6
case constant-expression2:
7
statement2;
8
// ...
9
break; // 可选的 break 语句
10
// ... 可以有多个 case 分支
11
default: // 可选的 default 分支
12
statement_default;
13
// ...
14
break; // 可选的 break 语句
15
}
语法解释:
⚝ switch (expression)
:expression
(表达式) 必须是整型、字符型、枚举类型或布尔类型。表达式的值将与各个 case
分支的常量表达式进行比较。
⚝ case constant-expression:
:constant-expression
(常量表达式) 必须是常量或常量表达式,且类型必须与 expression
的类型兼容。同一个 switch
语句中,所有 case
的常量表达式的值必须互不相同。
⚝ statement;
:每个 case
分支可以包含一条或多条语句。
⚝ break;
(break 语句):break
语句用于跳出 switch
语句。如果在一个 case
分支中没有 break
语句,程序会继续执行下一个 case
分支的语句,直到遇到 break
语句或 switch
语句结束。这种现象称为 case 穿透 (fall-through)。
⚝ default:
(default 分支):default
分支是可选的,用于处理当 expression
的值与所有 case
的常量表达式都不匹配的情况。如果存在 default
分支,则执行 default
分支的语句。
执行流程:
① 计算 switch
语句中 expression
的值。
② 将 expression
的值依次与每个 case
分支的 constant-expression
进行比较。
③ 如果 expression
的值与某个 case
的 constant-expression
相等,则从该 case
分支的语句开始执行,直到遇到 break
语句或 switch
语句结束。
④ 如果 expression
的值与所有 case
的 constant-expression
都不相等,且存在 default
分支,则执行 default
分支的语句。
⑤ 如果 expression
的值与所有 case
的 constant-expression
都不相等,且没有 default
分支,则 switch
语句不执行任何语句,程序直接执行 switch
语句之后的代码。
示例:根据数字输出星期几
1
#include <iostream>
2
3
int main() {
4
int day = 3;
5
switch (day) {
6
case 1:
7
std::cout << "星期一 (Monday)" << std::endl;
8
break;
9
case 2:
10
std::cout << "星期二 (Tuesday)" << std::endl;
11
break;
12
case 3:
13
std::cout << "星期三 (Wednesday)" << std::endl;
14
break;
15
case 4:
16
std::cout << "星期四 (Thursday)" << std::endl;
17
break;
18
case 5:
19
std::cout << "星期五 (Friday)" << std::endl;
20
break;
21
case 6:
22
std::cout << "星期六 (Saturday)" << std::endl;
23
break;
24
case 7:
25
std::cout << "星期日 (Sunday)" << std::endl;
26
break;
27
default:
28
std::cout << "无效的日期 (Invalid day)" << std::endl;
29
break;
30
}
31
return 0;
32
}
在这个例子中,day
的值为 3,与 case 3
匹配,因此输出 "星期三 (Wednesday)"。每个 case
分支都使用了 break
语句,以避免 case 穿透。
case 穿透示例:多个 case 执行相同的代码
1
#include <iostream>
2
3
int main() {
4
char grade = 'B';
5
switch (grade) {
6
case 'A':
7
case 'B':
8
case 'C':
9
std::cout << "通过 (Pass)" << std::endl;
10
break;
11
case 'D':
12
case 'F':
13
std::cout << "不通过 (Fail)" << std::endl;
14
break;
15
default:
16
std::cout << "无效的等级 (Invalid grade)" << std::endl;
17
break;
18
}
19
return 0;
20
}
在这个例子中,case 'A'
, case 'B'
, case 'C'
都没有 break
语句,因此当 grade
为 'A'、'B' 或 'C' 时,都会执行 std::cout << "通过 (Pass)" << std::endl;
语句。
3.3.2 switch
语句的应用场景和注意事项
应用场景:
⚝ 当需要根据一个表达式的值与多个常量值进行比较,并执行不同的操作时,switch
语句通常比 if-else if-else
语句更简洁和高效。
⚝ switch
语句特别适合处理枚举类型和字符类型的多分支选择。
注意事项:
⚝ switch
语句只能用于相等性比较,不能用于范围比较或其他复杂的条件判断。
⚝ case
分支的常量表达式必须是常量或常量表达式,不能是变量或非常量表达式。
⚝ 务必注意 break
语句的使用,避免不必要的 case 穿透。在大多数情况下,每个 case
分支都应该以 break
语句结束,除非有意利用 case 穿透实现多个 case 分支共享相同的代码。
⚝ default
分支虽然是可选的,但通常建议在 switch
语句中包含 default
分支,以处理未预期的输入值,提高程序的健壮性。
⚝ 对于分支较少的情况,if-else if-else
语句可能更清晰易懂。switch
语句更适合处理分支较多且条件是常量值的情况。
switch
语句 vs. if-else if-else
语句:
特性 | switch 语句 | if-else if-else 语句 |
---|---|---|
适用条件 | 基于离散常量值的选择 | 基于任意布尔表达式的选择 |
比较类型 | 相等性比较 (== ) | 任意关系或逻辑运算 (> , < , >= , <= , == , != , && , || , ! ) |
表达式类型 | 整型、字符型、枚举类型、布尔类型 | 任意可转换为布尔类型的表达式 |
常量表达式 | case 后必须是常量或常量表达式 | 无此限制 |
代码结构 | 通常更简洁,特别是当分支较多时 | 更灵活,可以处理更复杂的条件 |
执行效率 | 在某些情况下可能比 if-else if-else 语句效率更高 | 效率可能较低,特别是当条件判断较多时 |
可读性 | 对于基于常量值的多分支选择,可读性较好 | 更通用,对于复杂的条件判断,可读性可能更好 |
选择使用 switch
语句还是 if-else if-else
语句,应根据具体的应用场景和需求进行权衡。一般来说,当条件是基于离散常量值的相等性比较时,switch
语句是更好的选择;当条件是范围比较或其他复杂的条件判断时,if-else if-else
语句更适合。
3.4 循环结构:while
语句
循环结构 (Iteration Structures) 允许程序重复执行一段代码,直到满足特定条件为止。while
语句 (while statement) 是C++中最基本的循环结构之一,它实现条件循环,即在每次循环迭代前先判断条件是否成立,如果成立则执行循环体,否则结束循环。
3.4.1 while
循环的语法和执行流程
while
循环的语法结构如下:
1
while (condition) {
2
statement; // 循环体
3
// ...
4
}
执行流程:
① 首先计算 condition
(条件) 表达式的值。condition
可以是任何可以转换为布尔类型的表达式。
② 如果 condition
的值为 true
(真)或非零值,则执行 while
循环的循环体,即 {}
内的 statement
(语句) 或语句块。
③ 执行完循环体后,程序返回到循环的开始,再次计算 condition
的值。
④ 如果 condition
的值仍然为 true
,则重复步骤 ② 和 ③。
⑤ 如果 condition
的值为 false
(假) 或零值,则结束循环,程序继续执行 while
循环之后的代码。
关键点:
⚝ while
循环是先判断条件,后执行循环体的循环结构。这意味着循环体可能一次也不执行,如果初始条件为 false
。
⚝ 为了避免死循环 (infinite loop),循环体内部通常需要包含改变循环条件的语句,使得条件最终能够变为 false
,从而结束循环。
3.4.2 while
循环的应用示例
示例 1:计数循环 - 计算 1 到 10 的整数和
1
#include <iostream>
2
3
int main() {
4
int sum = 0;
5
int i = 1; // 初始化循环变量
6
while (i <= 10) { // 循环条件
7
sum += i; // 累加求和
8
i++; // 更新循环变量,使循环条件最终为假
9
}
10
std::cout << "1 到 10 的整数和为 (Sum of integers from 1 to 10): " << sum << std::endl;
11
return 0;
12
}
在这个例子中,循环变量 i
从 1 开始,每次循环迭代递增 1,直到 i
大于 10 时,循环条件 i <= 10
为假,循环结束。
示例 2:条件控制循环 - 读取用户输入,直到输入 0 为止
1
#include <iostream>
2
3
int main() {
4
int number;
5
std::cout << "请输入一些数字,输入 0 结束 (Enter numbers, enter 0 to end):" << std::endl;
6
std::cin >> number; // 首次读取输入
7
while (number != 0) { // 循环条件:输入的数不为 0
8
std::cout << "你输入了 (You entered): " << number << std::endl;
9
std::cin >> number; // 循环体内再次读取输入,更新循环条件
10
}
11
std::cout << "循环结束 (Loop ended)." << std::endl;
12
return 0;
13
}
在这个例子中,循环条件是用户输入的 number
不等于 0。每次循环迭代读取用户输入,如果输入不为 0,则继续循环;如果输入为 0,则循环条件 number != 0
为假,循环结束。
3.4.3 死循环 (Infinite Loop) 和循环控制
死循环 (Infinite Loop):如果 while
循环的条件永远为 true
,或者循环体内部没有改变循环条件的语句,就会导致循环无限执行下去,形成死循环。死循环通常是程序错误,应尽量避免。
示例:死循环
1
#include <iostream>
2
3
int main() {
4
int i = 1;
5
while (i <= 10) {
6
std::cout << i << std::endl;
7
// 缺少 i++,导致 i 的值永远为 1,循环条件永远为真,形成死循环
8
}
9
return 0; // 这行代码永远不会被执行到
10
}
要终止死循环运行的程序,通常需要手动强制结束程序进程(例如,在命令行窗口按 Ctrl+C
,或在IDE中点击停止按钮)。
循环控制语句:C++提供了 break
(break 语句) 和 continue
(continue 语句) 两种循环控制语句,用于更灵活地控制循环的执行流程。
⚝ break
语句:用于立即终止当前所在的循环(while
、for
、do-while
循环或 switch
语句),程序跳转到循环结构之后的语句继续执行。
⚝ continue
语句:用于跳过当前循环迭代的剩余语句,直接进入下一次循环迭代的条件判断。对于 while
循环,程序会立即判断循环条件;对于 for
循环,程序会先执行循环迭代语句,再判断循环条件。
示例:使用 break
语句提前终止循环
1
#include <iostream>
2
3
int main() {
4
int i = 1;
5
while (i <= 10) {
6
std::cout << i << std::endl;
7
if (i == 5) {
8
break; // 当 i 等于 5 时,跳出循环
9
}
10
i++;
11
}
12
std::cout << "循环在 i = " << i << " 时结束 (Loop ended when i = " << i << ")." << std::endl;
13
return 0;
14
}
在这个例子中,当 i
等于 5 时,break
语句被执行,循环立即终止。
示例:使用 continue
语句跳过当前迭代
1
#include <iostream>
2
3
int main() {
4
for (int i = 1; i <= 10; i++) {
5
if (i % 2 == 0) {
6
continue; // 当 i 为偶数时,跳过当前迭代,不执行后面的输出语句
7
}
8
std::cout << i << std::endl; // 只输出奇数
9
}
10
return 0;
11
}
在这个例子中,当 i
为偶数时,continue
语句被执行,跳过 std::cout << i << std::endl;
语句,直接进入下一次循环迭代。因此,程序只输出奇数。
注意事项:
⚝ break
和 continue
语句只能用于循环语句 (while
, for
, do-while
) 和 switch
语句。
⚝ 过度使用 break
和 continue
语句可能会降低代码的可读性和可维护性,应谨慎使用。
⚝ 在设计循环时,应优先考虑使用清晰的循环条件和循环体结构来控制循环流程,尽量避免过度依赖 break
和 continue
语句。
3.5 循环结构:for
语句
for
语句 (for statement) 是C++中另一种常用的循环结构,它提供了一种更紧凑和灵活的方式来实现计数循环和迭代循环。for
循环将循环的初始化、条件判断和循环迭代操作集中在一个语句中,使得循环结构更清晰易懂。
3.5.1 for
循环的语法和执行流程
for
循环的语法结构如下:
1
for (initialization; condition; iteration) {
2
statement; // 循环体
3
// ...
4
}
语法解释:
⚝ initialization
(初始化):用于初始化循环变量,通常在循环开始前执行一次。可以声明和初始化循环变量,也可以是其他类型的表达式。可以为空。
⚝ condition
(条件):循环条件表达式,在每次循环迭代之前进行判断。如果 condition
的值为 true
,则执行循环体;如果为 false
,则结束循环。可以为空,如果为空则默认为 true
(即无限循环,需要使用 break
语句终止)。
⚝ iteration
(迭代):用于在每次循环迭代之后更新循环变量,通常是对循环变量进行递增、递减或其他操作。可以为空。
⚝ statement
(语句):循环体,即需要重复执行的代码块。
执行流程:
① 执行 initialization
(初始化) 语句,只执行一次,用于设置循环的初始状态。
② 计算 condition
(条件) 表达式的值。
③ 如果 condition
的值为 true
(真)或非零值,则执行 for
循环的循环体 (statement
)。
④ 执行完循环体后,执行 iteration
(迭代) 语句,更新循环变量。
⑤ 返回到步骤 ②,再次计算 condition
的值。
⑥ 如果 condition
的值为 false
(假) 或零值,则结束循环,程序继续执行 for
循环之后的代码。
关键点:
⚝ for
循环是先判断条件,后执行循环体的循环结构,与 while
循环类似。
⚝ initialization
、condition
和 iteration
部分都是可选的,可以根据需要省略。但通常情况下,这三部分都会被使用,以实现计数循环或迭代循环。
⚝ for
循环的循环变量的作用域通常限制在 for
循环内部,即在 initialization
部分声明的变量只在 for
循环内部可见。
3.5.2 for
循环的应用示例
示例 1:计数循环 - 计算 1 到 10 的整数和 (使用 for
循环)
1
#include <iostream>
2
3
int main() {
4
int sum = 0;
5
for (int i = 1; i <= 10; i++) { // 初始化 i=1; 条件 i<=10; 迭代 i++
6
sum += i;
7
}
8
std::cout << "1 到 10 的整数和为 (Sum of integers from 1 to 10): " << sum << std::endl;
9
return 0;
10
}
在这个例子中,for
循环的初始化部分 int i = 1
声明并初始化循环变量 i
;条件部分 i <= 10
指定循环条件;迭代部分 i++
在每次循环迭代后递增 i
的值。
示例 2:数组遍历 (Array Traversal) - 遍历输出数组元素
1
#include <iostream>
2
3
int main() {
4
int numbers[] = {10, 20, 30, 40, 50};
5
int arraySize = sizeof(numbers) / sizeof(numbers[0]); // 计算数组大小
6
for (int i = 0; i < arraySize; i++) { // 循环遍历数组索引
7
std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl;
8
}
9
return 0;
10
}
在这个例子中,for
循环遍历数组 numbers
的索引,从 0 到 arraySize - 1
,并输出每个数组元素的值。
示例 3:无限循环 (Infinite Loop) (使用 for
循环)
1
#include <iostream>
2
3
int main() {
4
for (;;) { // 初始化、条件和迭代部分都为空,条件默认为 true,形成无限循环
5
std::cout << "这是一个无限循环 (This is an infinite loop)." << std::endl;
6
// 可以使用 break 语句在循环体内终止循环
7
}
8
return 0; // 这行代码永远不会被执行到
9
}
当 for
循环的条件部分为空时,条件默认为 true
,会形成无限循环。通常需要在循环体内使用 break
语句来控制循环的终止。
3.5.3 范围 for
循环 (Range-based for loop) (C++11)
范围 for
循环 (Range-based for loop) 是C++11标准引入的一种新的 for
循环语法,用于更简洁地遍历容器 (Containers) 和数组 (Arrays) 中的元素。范围 for
循环隐藏了循环变量和索引的细节,使得代码更加简洁易读。
语法结构:
1
for (declaration : range) {
2
statement; // 循环体
3
// ...
4
}
语法解释:
⚝ declaration
(声明):声明一个循环变量,用于接收 range
(范围) 中的每个元素的值。循环变量的类型通常使用 auto
关键字自动推导,也可以显式指定类型。
⚝ range
(范围):表示要遍历的容器或数组。可以是数组名、std::vector
、std::list
等容器对象,或者任何具有 begin()
和 end()
成员函数(或自由函数)的对象。
⚝ statement
(语句):循环体,对每个元素执行的操作。
执行流程:
① 范围 for
循环会自动迭代 range
中的每个元素。
② 在每次迭代中,将当前元素的值拷贝给 declaration
中声明的循环变量(默认情况下是值拷贝)。
③ 执行循环体 statement
,对当前元素进行操作。
④ 循环遍历完 range
中的所有元素后,循环结束。
示例 1:遍历数组 (使用范围 for
循环)
1
#include <iostream>
2
3
int main() {
4
int numbers[] = {10, 20, 30, 40, 50};
5
for (int number : numbers) { // 遍历数组 numbers 中的每个元素
6
std::cout << number << " "; // 输出当前元素的值
7
}
8
std::cout << std::endl;
9
return 0;
10
}
在这个例子中,for (int number : numbers)
范围 for
循环遍历数组 numbers
中的每个元素,并将元素的值依次赋值给循环变量 number
。
示例 2:遍历 std::vector
(使用范围 for
循环)
1
#include <iostream>
2
#include <vector>
3
4
int main() {
5
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
6
for (const auto& name : names) { // 遍历 vector names 中的每个元素,使用 const auto& 避免拷贝
7
std::cout << name << std::endl;
8
}
9
return 0;
10
}
在这个例子中,for (const auto& name : names)
范围 for
循环遍历 std::vector<std::string> names
中的每个元素。使用 const auto&
作为循环变量类型,可以避免不必要的字符串拷贝,提高效率,并且防止在循环体内意外修改元素的值。
注意事项:
⚝ 默认情况下,范围 for
循环使用值拷贝的方式将容器或数组中的元素赋值给循环变量。如果容器或数组中的元素类型是大型对象,值拷贝可能会带来性能开销。可以使用引用 (reference) 或常量引用 (const reference) 作为循环变量类型,以避免拷贝。例如 for (auto& element AlBeRt63EiNsTeIn container)
.
⚝ 范围 for
循环不能直接获取元素的索引。如果需要索引,仍然需要使用传统的 for
循环或结合其他方法(例如使用计数器)。
⚝ 范围 for
循环主要用于只读遍历容器或数组。如果在循环体内需要修改容器或数组的元素值,需要使用引用作为循环变量类型,例如 for (auto& element : container)
.
3.6 循环结构:do-while
语句
do-while
语句 (do-while statement) 是C++中另一种循环结构,它与 while
循环类似,也实现条件循环。do-while
循环与 while
循环的主要区别在于:do-while
循环是先执行循环体,后判断条件的循环结构,这意味着 do-while
循环的循环体至少会被执行一次。
3.6.1 do-while
循环的语法和执行流程
do-while
循环的语法结构如下:
1
do {
2
statement; // 循环体
3
// ...
4
} while (condition); // 注意分号 ; 结尾
执行流程:
① 首先执行 do-while
循环的循环体,即 {}
内的 statement
(语句) 或语句块。
② 执行完循环体后,计算 while (condition)
中的 condition
(条件) 表达式的值。
③ 如果 condition
的值为 true
(真)或非零值,则返回到步骤 ①,再次执行循环体。
④ 如果 condition
的值为 false
(假) 或零值,则结束循环,程序继续执行 do-while
循环之后的代码。
关键点:
⚝ do-while
循环是先执行循环体,后判断条件的循环结构。这保证了循环体至少执行一次,即使初始条件为 false
。
⚝ while (condition)
部分的末尾必须加分号 ;
,这是 do-while
循环语法的一部分,容易被初学者忽略。
⚝ 与 while
循环和 for
循环一样,为了避免死循环,循环体内部通常需要包含改变循环条件的语句,使得条件最终能够变为 false
,从而结束循环。
3.6.2 do-while
循环的应用场景
do-while
循环适用于那些循环体至少需要执行一次的场景。例如,需要先执行某些操作,然后再根据结果判断是否需要继续循环的情况。
示例 1:确保用户输入有效范围的数字
1
#include <iostream>
2
3
int main() {
4
int number;
5
do {
6
std::cout << "请输入一个 1 到 10 之间的数字 (Enter a number between 1 and 10): ";
7
std::cin >> number;
8
if (number < 1 || number > 10) {
9
std::cout << "输入无效,请重新输入 (Invalid input, please try again)." << std::endl;
10
}
11
} while (number < 1 || number > 10); // 循环条件:输入的数字不在 1 到 10 的范围内
12
13
std::cout << "你输入的有效数字是 (You entered a valid number): " << number << std::endl;
14
return 0;
15
}
在这个例子中,do-while
循环先执行循环体,提示用户输入数字并读取输入。然后判断输入的数字是否在 1 到 10 的范围内。如果不在范围内,循环条件 number < 1 || number > 10
为真,循环继续执行,再次提示用户输入;如果在范围内,循环条件为假,循环结束。do-while
循环保证了至少会提示用户输入一次数字。
示例 2:简单的菜单驱动程序
1
#include <iostream>
2
3
int main() {
4
char choice;
5
do {
6
std::cout << "请选择操作 (Please select an operation):" << std::endl;
7
std::cout << "A. 操作 A (Operation A)" << std::endl;
8
std::cout << "B. 操作 B (Operation B)" << std::endl;
9
std::cout << "Q. 退出 (Quit)" << std::endl;
10
std::cout << "你的选择是 (Your choice): ";
11
std::cin >> choice;
12
13
switch (choice) {
14
case 'A':
15
case 'a':
16
std::cout << "执行操作 A (Performing Operation A)..." << std::endl;
17
break;
18
case 'B':
19
case 'b':
20
std::cout << "执行操作 B (Performing Operation B)..." << std::endl;
21
break;
22
case 'Q':
23
case 'q':
24
std::cout << "退出程序 (Exiting program)." << std::endl;
25
break;
26
default:
27
std::cout << "无效的选择 (Invalid choice)." << std::endl;
28
}
29
} while (choice != 'Q' && choice != 'q'); // 循环条件:选择不是 'Q' 或 'q'
30
31
return 0;
32
}
在这个例子中,do-while
循环实现了一个简单的菜单驱动程序。程序首先显示菜单选项,并读取用户选择。然后根据用户选择执行相应的操作。循环会一直执行,直到用户选择 'Q' 或 'q' 退出程序。do-while
循环保证了菜单至少会显示一次。
do-while
循环 vs. while
循环:
特性 | do-while 循环 | while 循环 |
---|---|---|
条件判断时机 | 先执行循环体,后判断条件 | 先判断条件,后执行循环体 |
循环体执行次数 | 至少执行一次 | 可能一次也不执行 (如果初始条件为 false ) |
语法结构 | do { ... } while (condition); (注意分号 ; ) | while (condition) { ... } |
适用场景 | 循环体至少需要执行一次的情况 | 循环体可能不需要执行或执行多次的情况 |
选择使用 do-while
循环还是 while
循环,应根据具体的应用场景和需求进行选择。如果循环体必须至少执行一次,则 do-while
循环是更好的选择;如果循环体可能不需要执行,或者循环次数不确定,则 while
循环更适合。
3.7 跳转语句:break
、continue
和 goto
跳转语句 (Jump Statements) 用于改变程序的正常执行流程,使程序跳转到代码的特定位置执行。C++提供了 break
(break 语句)、 continue
(continue 语句) 和 goto
(goto 语句) 三种跳转语句。break
和 continue
语句主要用于循环结构和 switch
语句的控制,而 goto
语句则可以跳转到程序中的任意标记位置。
3.7.1 break
语句
break
语句 (break statement) 的作用是立即终止当前所在的循环结构(while
、for
、do-while
循环)或 switch
语句,程序跳转到循环或 switch
结构之后的语句继续执行。
应用场景:
⚝ 在循环中,当满足特定条件时,需要提前结束循环,可以使用 break
语句。
⚝ 在 switch
语句中,每个 case
分支通常以 break
语句结尾,以防止 case 穿透。
示例 1:在 for
循环中使用 break
语句提前结束循环
1
#include <iostream>
2
3
int main() {
4
for (int i = 1; i <= 10; i++) {
5
std::cout << i << std::endl;
6
if (i == 5) {
7
break; // 当 i 等于 5 时,跳出 for 循环
8
}
9
}
10
std::cout << "循环在 i = 5 时结束 (Loop ended when i = 5)." << std::endl;
11
return 0;
12
}
示例 2:在 switch
语句中使用 break
语句防止 case 穿透
1
#include <iostream>
2
3
int main() {
4
int choice = 2;
5
switch (choice) {
6
case 1:
7
std::cout << "选择了选项 1 (Option 1 selected)." << std::endl;
8
break;
9
case 2:
10
std::cout << "选择了选项 2 (Option 2 selected)." << std::endl;
11
break;
12
case 3:
13
std::cout << "选择了选项 3 (Option 3 selected)." << std::endl;
14
break;
15
default:
16
std::cout << "无效的选择 (Invalid choice)." << std::endl;
17
break;
18
}
19
return 0;
20
}
3.7.2 continue
语句
continue
语句 (continue statement) 的作用是跳过当前循环迭代的剩余语句,直接进入下一次循环迭代的条件判断。
应用场景:
⚝ 在循环中,当满足特定条件时,需要跳过当前迭代的剩余操作,直接开始下一次迭代,可以使用 continue
语句。
示例:在 for
循环中使用 continue
语句跳过偶数迭代
1
#include <iostream>
2
3
int main() {
4
for (int i = 1; i <= 10; i++) {
5
if (i % 2 == 0) {
6
continue; // 当 i 为偶数时,跳过当前迭代,不执行后面的输出语句
7
}
8
std::cout << i << std::endl; // 只输出奇数
9
}
10
return 0;
11
}
break
语句 vs. continue
语句:
语句 | 作用 | 应用范围 |
---|---|---|
break | 立即终止当前循环或 switch 结构 | 循环语句 (while , for , do-while )、switch 语句 |
continue | 跳过当前迭代的剩余语句,进入下一次迭代 | 循环语句 (while , for , do-while ) |
3.7.3 goto
语句 (慎用)
goto
语句 (goto statement) 是一种无条件跳转语句,它可以使程序无条件跳转到程序中预先标记的标签 (label) 位置执行。
语法结构:
1
goto label_name; // 跳转到标签 label_name 处
2
// ...
3
label_name: // 标签 label_name
4
statement; // 标签位置的语句
5
// ...
语法解释:
⚝ label_name
(标签名) 是一个标识符,用于标记程序中的一个位置。标签名的命名规则与变量名相同。
⚝ 标签的声明方式是在标签名后加一个冒号 :
。
⚝ goto label_name;
语句将使程序无条件跳转到名为 label_name
的标签位置,并从标签位置的语句开始执行。
示例:使用 goto
语句实现循环 (不推荐)
1
#include <iostream>
2
3
int main() {
4
int i = 1;
5
loop_start: // 标签 loop_start
6
std::cout << i << std::endl;
7
i++;
8
if (i <= 5) {
9
goto loop_start; // 跳转到标签 loop_start,实现循环
10
}
11
return 0;
12
}
虽然 goto
语句可以实现跳转,但过度使用或不恰当使用 goto
语句会导致程序流程混乱,代码可读性和可维护性极差,容易形成“意大利面条式代码” (spaghetti code)。因此,在现代编程实践中,强烈建议慎用或避免使用 goto
语句。
goto
语句的潜在问题:
⚝ 破坏结构化程序设计原则:goto
语句破坏了程序的结构化,使得程序流程难以跟踪和理解。
⚝ 降低代码可读性和可维护性:goto
语句容易导致程序逻辑混乱,使得代码难以阅读、调试和维护。
⚝ 容易引入错误:不恰当使用 goto
语句容易引入各种难以发现和调试的错误。
goto
语句的合理使用场景 (极少):
⚝ 多重循环的跳出:在嵌套很深的循环结构中,如果需要从最内层循环一次性跳出到最外层循环之外,使用 goto
语句可能比使用多层 break
语句更简洁。但这通常也可以通过函数封装和返回值等更结构化的方式来实现。
⚝ 错误处理:在某些错误处理场景中,可以使用 goto
语句跳转到统一的错误处理代码块。但这也可以通过异常处理 (Exception Handling) 等更现代和更安全的方式来实现。
替代方案:在绝大多数情况下,goto
语句的功能都可以通过更结构化的控制语句(如 if-else
、switch
、while
、for
、break
、continue
)、函数调用、异常处理等方式来实现,并且代码的可读性和可维护性会更好。因此,应尽量使用结构化的控制语句来替代 goto
语句。
总结:
⚝ break
语句用于立即终止循环或 switch
结构。
⚝ continue
语句用于跳过当前迭代的剩余语句,进入下一次迭代。
⚝ goto
语句是一种无条件跳转语句,应慎用或避免使用,因为它容易破坏程序结构,降低代码可读性和可维护性。在绝大多数情况下,可以使用更结构化的控制语句来替代 goto
语句。
4. 函数:模块化程序设计
本章深入讲解C++函数的定义、声明、调用、参数传递和返回值,以及函数重载、递归函数等高级特性,帮助读者掌握模块化程序设计的思想和方法。
4.1 函数的定义 (Function Definition) 和声明 (Function Declaration)
本节详细介绍函数的语法结构,包括函数名、参数列表、返回值类型和函数体,以及函数声明的作用和必要性。
4.1.1 函数的语法结构
函数是C++程序的基本组成模块,用于封装可重复使用的代码块。一个函数定义 (Function Definition) 包括以下几个关键部分:
① 返回值类型 (Return Type):指定函数执行结束后返回给调用者的值的类型。
▮ 例如,int
表示函数返回一个整数值,double
表示返回一个双精度浮点数值。
▮ 如果函数不返回任何值,则返回值类型声明为 void
。
② 函数名 (Function Name):是函数的标识符,用于在程序中调用该函数。
▮ 函数名应具有描述性,能够清晰表达函数的功能。
▮ 函数名需要遵循C++标识符的命名规则(例如,不能以数字开头,不能包含空格和特殊字符,等等)。
③ 参数列表 (Parameter List):位于函数名后的一对圆括号 ()
内,用于接收调用者传递给函数的数据。
▮ 参数列表中可以包含零个或多个参数,每个参数由参数类型 (Parameter Type) 和参数名 (Parameter Name) 组成,参数之间用逗号 ,
分隔。
▮ 例如,int a, double b
表示函数接受两个参数,一个整型参数 a
和一个双精度浮点型参数 b
。
▮ 如果函数没有参数,则参数列表为空,即 ()
。
④ 函数体 (Function Body):位于花括号 {}
内,包含函数实际执行的代码。
▮ 函数体由一系列语句组成,这些语句描述了函数要完成的具体操作。
▮ 函数体可以包含声明语句、表达式语句、控制语句(如 if
语句、循环语句)等。
▮ return
语句用于从函数中返回值,并结束函数的执行。如果函数返回值类型为 void
,则 return
语句是可选的,用于提前结束函数执行。
函数定义的通用语法结构如下:
1
返回值类型 函数名 (参数列表) {
2
// 函数体
3
// ... 函数的语句 ...
4
return 返回值; // 如果返回值类型不是 void,则需要 return 语句
5
}
示例:一个计算两个整数之和的函数
1
int add(int num1, int num2) {
2
int sum = num1 + num2;
3
return sum;
4
}
在这个例子中:
⚝ int
是返回值类型,表示函数返回一个整数。
⚝ add
是函数名。
⚝ (int num1, int num2)
是参数列表,表示函数接受两个整型参数 num1
和 num2
。
⚝ { ... }
内是函数体,计算 num1
和 num2
的和,并将结果存储在变量 sum
中,然后使用 return sum;
语句返回 sum
的值。
4.1.2 函数声明的作用和语法
函数声明 (Function Declaration),也称为函数原型 (Function Prototype),是在使用函数之前向编译器提供函数信息的一种方式。函数声明告诉编译器函数的名称、返回值类型和参数列表,但不包含函数体。
函数声明的主要作用包括:
① 类型检查 (Type Checking):函数声明允许编译器在函数调用之前检查函数的使用是否正确,例如,参数类型和数量是否匹配,返回值类型是否被正确使用。这有助于在编译时发现潜在的类型错误,提高程序的健壮性。
② 分离接口和实现 (Separation of Interface and Implementation):函数声明定义了函数的接口(即函数的“外部”可见部分:函数名、参数和返回值),而函数定义则包含了函数的实现(即函数的“内部”工作方式:函数体)。通过将声明和定义分离,可以更好地组织代码,提高代码的可读性和可维护性。例如,可以将函数声明放在头文件 (Header File) 中,而将函数定义放在源文件 (Source File) 中。
③ 支持前向声明 (Forward Declaration):在C++中,函数必须先声明后使用。函数声明允许在函数定义之前就调用该函数,这对于解决函数之间的相互调用或循环依赖问题非常有用。
函数声明的语法结构与函数定义非常相似,但有以下关键区别:
⚝ 没有函数体:函数声明只包含返回值类型、函数名和参数列表,以分号 ;
结尾,不包含花括号 {}
和函数体。
⚝ 参数名可以省略:在函数声明中,参数名可以省略,只保留参数类型即可。但这通常不推荐,因为包含参数名可以提高函数声明的可读性,使程序员更容易理解函数参数的含义。
函数声明的通用语法结构如下:
1
返回值类型 函数名 (参数列表); // 注意末尾的分号
或省略参数名:
1
返回值类型 函数名 (参数类型1, 参数类型2, ...);
示例: add
函数的声明
1
int add(int num1, int num2); // 包含参数名的声明
2
3
int add(int, int); // 省略参数名的声明 (不推荐,可读性较差)
函数声明的位置:
函数声明通常放在以下位置:
⚝ 头文件 (Header File):当函数需要在多个源文件中使用时,通常将函数声明放在头文件中。然后在需要使用该函数的源文件中包含 (include) 该头文件。这是C++程序组织和模块化的常用方法。
⚝ 源文件 (Source File) 的顶部:如果函数只在一个源文件中使用,可以将函数声明放在该源文件的顶部,在 main()
函数或其他调用该函数的函数之前。
⚝ 类定义 (Class Definition) 内部:作为成员函数 (Member Function) 声明时,函数声明位于类定义的内部。
函数声明与函数定义的区别总结:
特征 | 函数声明 (Declaration/Prototype) | 函数定义 (Definition) |
---|---|---|
目的 | 向编译器提供函数接口信息 | 提供函数的具体实现 |
是否包含函数体 | 否 (不包含) | 是 (包含) |
语法 | 返回值类型 函数名 (参数列表); | 返回值类型 函数名 (参数列表) { 函数体 } |
参数名 | 可选 (建议包含,提高可读性) | 必须包含 |
末尾符号 | 分号 ; | 花括号 {} |
位置 | 头文件、源文件顶部、类定义内 | 源文件 |
4.1.3 头文件 (Header Files) 和函数声明
头文件 (Header Files) 在C++程序中扮演着至关重要的角色,尤其在组织和管理大型项目时。头文件主要用于存放函数声明、类定义、宏定义、类型别名 (typedef) 和全局变量声明等信息,以便在多个源文件之间共享代码和接口。
头文件的主要作用:
① 代码共享和重用 (Code Sharing and Reuse):通过将函数声明、类定义等放在头文件中,可以在多个源文件中包含同一个头文件,从而实现代码的共享和重用。这避免了在每个源文件中重复编写相同的声明,提高了代码的效率和可维护性。
② 模块化 (Modularity):头文件有助于将程序划分为独立的模块。每个模块可以有自己的头文件,声明该模块提供的接口(函数、类等)。其他模块只需要包含相应的头文件就可以使用该模块的功能,而无需了解模块的内部实现细节。这符合模块化程序设计的原则,提高了代码的组织性和可管理性。
③ 接口与实现分离 (Interface and Implementation Separation):头文件通常只包含声明(接口),而函数的具体实现(定义)则放在源文件中。这种接口与实现分离的设计模式,使得代码的修改和维护更加方便。例如,如果只需要修改函数的实现,而函数接口保持不变,那么只需要重新编译包含函数定义的源文件,而无需重新编译所有使用该函数的源文件。
④ 提高编译效率 (Improve Compilation Efficiency):在大型项目中,如果所有代码都放在一个源文件中,每次修改都需要重新编译整个源文件,编译时间会很长。通过使用头文件和分离编译 (Separate Compilation) 技术,可以将程序划分为多个源文件,每个源文件可以独立编译。当修改一个源文件时,只需要重新编译该源文件和依赖于该源文件的其他源文件,而无需重新编译整个项目,从而大大提高了编译效率。
头文件的使用规范:
① 头文件的扩展名:C++头文件通常使用 .h
或 .hpp
作为扩展名。标准库头文件通常没有扩展名,例如 <iostream>
, <vector>
.
② #include
指令:要在一个源文件中使用头文件中声明的内容,需要使用 #include
预处理指令。#include
指令告诉预处理器将指定头文件的内容原样插入到当前源文件中。
▮▮▮▮⚝ 尖括号 <>
: #include <iostream>
用于包含系统头文件或标准库头文件。编译器会在系统头文件目录中搜索头文件。
▮▮▮▮⚝ 双引号 ""
: #include "my_header.h"
用于包含用户自定义头文件。编译器会首先在当前源文件所在目录中搜索头文件,如果没有找到,再到系统头文件目录中搜索。
③ 头文件内容:头文件通常包含以下内容:
▮▮▮▮⚝ 函数声明 (Function Declarations):声明函数的名字、返回值类型和参数列表。
▮▮▮▮⚝ 类定义 (Class Definitions):定义类的结构,包括成员变量和成员函数声明。
▮▮▮▮⚝ 宏定义 (Macro Definitions):使用 #define
定义的宏常量或宏函数。
▮▮▮▮⚝ 类型别名 (Type Aliases):使用 typedef
或 using
定义的类型别名。
▮▮▮▮⚝ 全局变量声明 (Global Variable Declarations) (谨慎使用):声明全局变量,但不建议在头文件中定义全局变量,以避免多重定义错误。
▮▮▮▮⚝ inline
函数定义 (Inline Function Definitions):对于简单的、频繁调用的函数,可以在头文件中直接定义为 inline
函数。
▮▮▮▮⚝ 包含其他头文件 (#include
directives):一个头文件可以包含其他头文件。
④ 头文件保护 (Header Guards) 或预编译指示 (Pragma once):为了防止头文件被重复包含 (Multiple Inclusion),导致编译错误(例如,重复定义),通常需要在头文件中使用头文件保护或 #pragma once
预编译指示。
▮▮▮▮⚝ 头文件保护 (Header Guards):使用预处理器宏定义和条件编译指令 #ifndef
, #define
, #endif
来实现。
1
#ifndef MY_HEADER_H // 如果 MY_HEADER_H 宏未定义
2
#define MY_HEADER_H // 定义 MY_HEADER_H 宏
3
4
// 头文件内容 ...
5
6
#endif // MY_HEADER_H
通常,宏名采用头文件名的大写形式,并将文件名中的 .
替换为 _
,并在末尾加上 _H
或 _HEADER
。
▮▮▮▮⚝ #pragma once
: 更简洁的头文件保护方法,只需在头文件开头添加 #pragma once
即可。但 #pragma once
不是所有编译器都支持,虽然现代主流编译器都支持。
1
#pragma once
2
3
// 头文件内容 ...
示例:创建和使用头文件
假设我们要创建一个名为 math_utils.h
的头文件,其中声明了一个计算平方的函数 square()
,并将函数定义放在 math_utils.cpp
源文件中。
math_utils.h
(头文件)
1
#ifndef MATH_UTILS_H
2
#define MATH_UTILS_H
3
4
// 函数声明
5
int square(int num);
6
7
#endif // MATH_UTILS_H
math_utils.cpp
(源文件,包含函数定义)
1
#include "math_utils.h" // 包含自定义头文件
2
3
// 函数定义
4
int square(int num) {
5
return num * num;
6
}
main.cpp
(主程序源文件,使用 square()
函数)
1
#include <iostream> // 包含标准库头文件
2
#include "math_utils.h" // 包含自定义头文件
3
4
int main() {
5
int number = 5;
6
int squared_number = square(number); // 调用 square() 函数
7
std::cout << number << " 的平方是 " << squared_number << std::endl;
8
return 0;
9
}
编译和运行:
需要将 math_utils.cpp
和 main.cpp
两个源文件一起编译链接。例如,使用 g++ 编译器:
1
g++ main.cpp math_utils.cpp -o main
2
./main
输出结果:
1
5 的平方是 25
通过使用头文件,我们将函数声明和定义分离,实现了代码的模块化和重用。main.cpp
只需要包含 math_utils.h
头文件就可以使用 square()
函数,而无需了解 square()
函数的具体实现细节。
4.2 函数的调用 (Function Call)
本节讲解如何调用函数,包括函数调用的一般形式、实参和形参的对应关系,以及函数调用的执行流程。
4.2.1 函数调用的一般形式
函数调用 (Function Call) 是指在程序中执行已定义的函数。要调用一个函数,需要使用函数名,后跟一对圆括号 ()
,并在圆括号内提供实参 (Arguments)(如果函数定义了形参)。
函数调用的一般形式如下:
1
函数名 (实参列表);
⚝ 函数名 (Function Name):要调用的函数的名称,必须与函数定义或声明时使用的名称完全一致。
⚝ 实参列表 (Argument List):位于圆括号 ()
内,用于向函数传递数据。实参是函数调用时提供给函数的具体数值或变量。
▮▮▮▮⚝ 如果函数定义时没有形参,则函数调用时实参列表为空,即 ()
。
▮▮▮▮⚝ 如果函数定义了形参,则函数调用时必须提供与形参数量、类型和顺序都匹配的实参。实参之间用逗号 ,
分隔。
函数调用可以出现在程序中的任何允许表达式出现的位置,例如:
⚝ 单独作为语句:function_name(arguments);
(例如,print_message();
)
⚝ 作为表达式的一部分:result = function_name(arguments) + 5;
(例如,sum = add(3, 4);
)
⚝ 作为其他函数的实参:another_function(function_name(arguments));
(例如,display_result(square(number));
)
示例:调用 add()
和 square()
函数
假设我们有之前定义的 add()
和 square()
函数:
1
// 函数定义 (假设已定义)
2
int add(int num1, int num2) {
3
return num1 + num2;
4
}
5
6
int square(int num) {
7
return num * num;
8
}
9
10
int main() {
11
int a = 10;
12
int b = 20;
13
14
// 调用 add() 函数,传递实参 a 和 b
15
int sum_result = add(a, b);
16
std::cout << "a + b = " << sum_result << std::endl; // 输出:a + b = 30
17
18
// 调用 square() 函数,传递实参 7
19
int square_result = square(7);
20
std::cout << "7 的平方 = " << square_result << std::endl; // 输出:7 的平方 = 49
21
22
// 函数调用作为表达式的一部分
23
int combined_result = add(square(3), 5); // 先调用 square(3) 得到 9,再调用 add(9, 5)
24
std::cout << "square(3) + 5 = " << combined_result << std::endl; // 输出:square(3) + 5 = 14
25
26
return 0;
27
}
在这些函数调用中:
⚝ add(a, b)
:a
和 b
是实参,它们的值分别传递给 add()
函数的形参 num1
和 num2
。
⚝ square(7)
:7
是实参,它的值传递给 square()
函数的形参 num
。
⚝ square(3)
:3
是实参,square()
函数的返回值 (9) 作为 add()
函数的第一个实参。
4.2.2 实参 (Arguments) 和形参 (Parameters) 的传递
在函数调用过程中,实参 (Arguments) 是在调用函数时提供给函数的具体数值或变量,而形参 (Parameters) 是在函数定义时声明的,用于接收从调用者传递过来的数据的变量。实参和形参之间的数据传递方式决定了函数如何处理传入的数据,以及函数对形参的修改是否会影响到实参。
C++ 中函数参数传递主要有三种方式:
① 值传递 (Pass-by-Value):
▮▮▮▮⚝ 机制:在值传递中,实参的值被复制一份,然后将副本传递给函数的形参。函数内部对形参的任何修改都只影响副本,不会影响到原始的实参。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 形参是实参的副本,两者存储在不同的内存位置。
▮▮▮▮▮▮▮▮⚝ 函数内部对形参的修改不会影响实参。
▮▮▮▮▮▮▮▮⚝ 适用于不需要函数修改实参值的情况。
▮▮▮▮▮▮▮▮⚝ 当实参是大型对象时,值传递会涉及大量的复制操作,可能效率较低。
② 引用传递 (Pass-by-Reference):
▮▮▮▮⚝ 机制:在引用传递中,形参成为实参的别名,它们指向相同的内存位置。函数内部对形参的任何修改都会直接影响到原始的实参。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 形参和实参是同一个变量的不同名称,它们共享相同的内存位置。
▮▮▮▮▮▮▮▮⚝ 函数内部对形参的修改会直接反映到实参。
▮▮▮▮▮▮▮▮⚝ 适用于需要函数修改实参值的情况。
▮▮▮▮▮▮▮▮⚝ 避免了值传递中不必要的复制操作,提高了效率,尤其适用于传递大型对象。
③ 指针传递 (Pass-by-Pointer):
▮▮▮▮⚝ 机制:在指针传递中,实参的地址被传递给函数的指针形参。函数内部通过解引用指针来访问和修改实参所指向的内存位置。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 形参是指针,存储的是实参的地址。
▮▮▮▮▮▮▮▮⚝ 函数内部通过解引用指针可以修改实参指向的值。
▮▮▮▮▮▮▮▮⚝ 适用于需要函数修改实参值的情况,以及处理动态内存分配等场景。
▮▮▮▮▮▮▮▮⚝ 与引用传递类似,避免了值传递的复制开销。
▮▮▮▮▮▮▮▮⚝ 需要显式使用指针操作(解引用 *
)。
示例代码:演示三种参数传递方式
1
#include <iostream>
2
3
// 值传递
4
void passByValue(int param) {
5
param = 100; // 修改形参
6
std::cout << "值传递函数内,形参 param = " << param << std::endl;
7
}
8
9
// 引用传递
10
void passByReference(int& param) { // 形参声明为引用类型 int&
11
param = 200; // 修改形参 (实际上是修改实参)
12
std::cout << "引用传递函数内,形参 param = " << param << std::endl;
13
}
14
15
// 指针传递
16
void passByPointer(int* param) { // 形参声明为指针类型 int*
17
*param = 300; // 解引用指针,修改指针指向的值 (实际上是修改实参)
18
std::cout << "指针传递函数内,形参 *param = " << *param << std::endl;
19
}
20
21
int main() {
22
int argument = 10;
23
24
std::cout << "调用函数前,实参 argument = " << argument << std::endl; // 输出:10
25
26
passByValue(argument);
27
std::cout << "值传递函数调用后,实参 argument = " << argument << std::endl; // 输出:10 (实参未被修改)
28
29
passByReference(argument);
30
std::cout << "引用传递函数调用后,实参 argument = " << argument << std::endl; // 输出:200 (实参被修改)
31
32
argument = 10; // 重新将 argument 设置为 10
33
34
passByPointer(&argument); // 传递实参的地址 &argument
35
std::cout << "指针传递函数调用后,实参 argument = " << argument << std::endl; // 输出:300 (实参被修改)
36
37
return 0;
38
}
输出结果:
1
调用函数前,实参 argument = 10
2
值传递函数内,形参 param = 100
3
值传递函数调用后,实参 argument = 10
4
引用传递函数内,形参 param = 200
5
引用传递函数调用后,实参 argument = 200
6
指针传递函数内,形参 *param = 300
7
指针传递函数调用后,实参 argument = 300
总结三种参数传递方式的特点和适用场景:
参数传递方式 | 机制 | 是否修改实参 | 适用场景 | 效率 |
---|---|---|---|---|
值传递 | 实参值的副本传递给形参 | 否 | 适用于函数不需要修改实参值的情况。实参是基本数据类型或小型对象时,代码简洁易懂。 | 较低 (大型对象) |
引用传递 | 形参作为实参的别名,共享内存位置 | 是 | 适用于函数需要修改实参值的情况。传递大型对象时,避免复制开销,提高效率。函数接口更清晰,调用方式与值传递类似。 | 较高 |
指针传递 | 实参的地址传递给指针形参,通过解引用修改实参指向的值 | 是 | 适用于函数需要修改实参值的情况。也常用于处理动态内存分配、数组和字符串等。在C语言遗留代码中常见。需要显式使用指针操作,代码稍复杂。 | 较高 |
在实际编程中,应根据函数的功能和需求选择合适的参数传递方式。
⚝ 优先考虑引用传递:如果函数需要修改实参值,或者需要传递大型对象以提高效率,引用传递通常是首选,因为它更安全、更简洁,且避免了空指针的风险。
⚝ 值传递适用于不需要修改实参的情况:当函数只需要使用实参的值,而不需要修改实参时,值传递是一个安全且易于理解的选择。
⚝ 指针传递在某些特定场景下仍然有用:例如,处理C风格的API,或者需要传递空指针的情况(引用不能为空)。但在现代C++编程中,应尽量使用引用和智能指针来替代原始指针。
4.2.3 函数调用的执行流程和栈帧 (Stack Frame)
函数调用涉及到程序的执行流程的跳转和返回,以及内存的分配和管理。理解函数调用的执行流程和栈帧 (Stack Frame) 的概念,有助于深入理解程序的运行机制。
函数调用的执行流程:
① 程序执行到函数调用语句:当程序执行到函数调用语句 function_name(arguments);
时,程序会暂停当前函数的执行。
② 参数传递 (Argument Passing):根据参数传递方式(值传递、引用传递或指针传递),将实参的值或地址传递给被调用函数的形参。
③ 控制权转移 (Control Transfer):程序的控制权 (Control Flow) 从调用函数转移到被调用函数。程序计数器 (Program Counter, PC) 指向被调用函数的入口地址,开始执行被调用函数的代码。
④ 栈帧创建 (Stack Frame Creation):在函数调用时,系统会在调用栈 (Call Stack) 上为被调用函数分配一块内存区域,称为栈帧 (Stack Frame) 或活动记录 (Activation Record)。栈帧用于存储:
▮▮▮▮⚝ 局部变量 (Local Variables):函数内部声明的局部变量。
▮▮▮▮⚝ 函数参数 (Function Parameters):形参的值。
▮▮▮▮⚝ 返回地址 (Return Address):函数执行结束后,程序需要返回到调用函数的位置。返回地址记录了调用函数中函数调用语句的下一条指令的地址。
▮▮▮▮⚝ 其他控制信息 (Control Information):例如,保存调用函数的栈帧基指针 (Base Pointer, BP) 和栈指针 (Stack Pointer, SP) 等。
⑤ 函数体执行 (Function Body Execution):被调用函数开始执行其函数体内的代码。函数体内的语句按照顺序依次执行。
⑥ 返回值 (Return Value):如果函数有返回值,return
语句会将返回值传递给调用函数。
⑦ 栈帧销毁 (Stack Frame Destruction):当被调用函数执行完毕(遇到 return
语句或执行到函数体末尾),系统会销毁被调用函数的栈帧,释放栈帧占用的内存空间。栈帧的销毁过程通常包括:
▮▮▮▮⚝ 弹出栈帧 (Popping the Stack Frame):栈指针 SP 恢复到函数调用前的状态,栈帧从调用栈中弹出。
▮▮▮▮⚝ 局部变量销毁 (Local Variable Destruction):栈帧中的局部变量超出作用域,被销毁(如果局部变量是对象,会调用其析构函数)。
⑧ 返回地址 (Return to Calling Function):程序根据栈帧中保存的返回地址,将控制权返回给调用函数。程序计数器 PC 设置为返回地址,从函数调用语句的下一条指令继续执行调用函数的代码。
调用栈 (Call Stack)
调用栈是一种栈 (Stack) 数据结构,用于管理函数调用和返回的执行过程。每当一个函数被调用时,一个新的栈帧被压入 (push) 调用栈;当函数执行完毕返回时,其栈帧从调用栈中弹出 (pop)。调用栈遵循后进先出 (Last-In, First-Out, LIFO) 的原则。
⚝ 栈顶 (Stack Top):调用栈的顶部始终是当前正在执行的函数的栈帧。
⚝ 栈底 (Stack Bottom):调用栈的底部是 main()
函数的栈帧(在程序启动时创建)。
栈帧 (Stack Frame) 内存布局 (示意图)
1
+-------------------+ <-- 栈顶 (SP)
2
| 被调用函数栈帧 |
3
| ----------------- |
4
| 局部变量 |
5
| 函数参数 |
6
| 返回地址 |
7
| ... |
8
| ----------------- |
9
| 调用函数栈帧 |
10
| ... |
11
| ----------------- |
12
| main() 函数栈帧 |
13
| ... |
14
+-------------------+ <-- 栈底
示例:函数调用执行流程
1
#include <iostream>
2
3
int square(int num) { // 函数定义
4
int result = num * num; // 局部变量 result
5
return result;
6
}
7
8
int main() { // main() 函数
9
int x = 5; // 局部变量 x
10
int y = square(x); // 函数调用
11
std::cout << "square(" << x << ") = " << y << std::endl;
12
return 0;
13
}
执行流程分析:
main()
函数开始执行,为main()
函数创建栈帧,局部变量x
在main()
栈帧中分配内存。- 执行
int y = square(x);
语句,准备调用square()
函数。 - 参数传递:将
x
的值 (5) 值传递给square()
函数的形参num
。 - 创建
square()
函数的栈帧,形参num
和局部变量result
在square()
栈帧中分配内存。返回地址 (指向main()
函数中int y = square(x);
语句的下一条指令) 保存在square()
栈帧中。 - 执行
square()
函数体:计算num * num
(5 * 5 = 25),将结果赋值给局部变量result
。 - 执行
return result;
语句,将result
的值 (25) 作为返回值返回。 - 销毁
square()
函数的栈帧,释放square()
栈帧占用的内存。 - 返回
main()
函数,程序控制权返回到main()
函数中int y = square(x);
语句的下一条指令。返回值 25 赋值给main()
函数的局部变量y
。 - 继续执行
main()
函数的代码,输出结果。 main()
函数执行完毕,main()
函数栈帧被销毁,程序结束。
栈溢出 (Stack Overflow)
当函数调用层级过深,导致调用栈空间被耗尽时,会发生栈溢出 (Stack Overflow) 错误。这通常发生在递归函数调用深度过大,或者嵌套调用函数过多时。
⚝ 递归函数栈溢出:如果递归函数没有正确的递归出口 (Base Case),或者递归调用的层数超过了调用栈的容量限制,就会导致栈溢出。
⚝ 避免栈溢出:
▮▮▮▮⚝ 优化递归算法:确保递归函数有明确的递归出口,并尽量减少递归深度。
▮▮▮▮⚝ 使用迭代 (Iteration) 替代递归:对于某些问题,可以使用循环迭代的方式来替代递归,从而避免栈溢出。
▮▮▮▮⚝ 增加栈空间 (Stack Size):在某些情况下,可以尝试增加程序的栈空间大小(但这通常不是根本的解决方案)。
理解函数调用的执行流程和栈帧机制,有助于编写更高效、更健壮的程序,并能更好地分析和解决程序运行时可能出现的问题,例如栈溢出。
5. 数组和字符串:批量数据处理
章节概要
本章系统讲解C++数组的定义、初始化、访问和使用,以及字符串的表示和操作,包括C风格字符串和std::string
类,为处理批量数据提供有效工具。
5.1 数组 (Arrays)
5.1.1 数组的定义和声明 (Definition and Declaration of Arrays)
数组 是一种存储相同类型元素集合的数据结构 (Data Structure)。这些元素在内存中是连续存储的,可以通过索引 (Index) 来访问。数组提供了一种便捷的方式来组织和管理批量数据。
① 定义数组
定义数组需要指定元素的数据类型 (Data Type)、数组名 (Array Name) 和数组的大小 (Size),即数组可以容纳多少个元素。语法格式如下:
1
数据类型 数组名[数组大小];
例如,定义一个可以存储 5 个整数的数组 numbers
:
1
int numbers[5];
⚝ int
:指定数组 numbers
中元素的数据类型为整型 (integer)。
⚝ numbers
:是数组的名称,遵循标识符的命名规则。
⚝ [5]
:方括号中的数字 5
表示数组 numbers
可以存储 5 个 int
类型的元素。数组的大小必须是编译时常量表达式 (Compile-time Constant Expression),这意味着在编译时就必须确定数组的大小。
② 数组的声明
与变量类似,数组也需要先声明后使用。数组的声明可以与定义同时进行,如上面的例子。如果数组在其他地方定义,只需声明数组的类型和名称,不需要再次指定大小,但需要使用方括号 []
表明它是一个数组。
例如,在一个源文件中定义了数组 scores
:
1
// file1.cpp
2
int scores[100];
在另一个源文件中如果想使用 scores
数组,需要进行声明:
1
// file2.cpp
2
extern int scores[]; // 声明 scores 是一个 int 类型的数组
⚝ extern
关键字表明 scores
数组是在其他地方定义的。
⚝ 声明时 scores[]
方括号内可以为空,因为编译器只需要知道 scores
是一个 int
类型的数组。如果需要在当前文件中访问数组的大小,则需要在定义数组的文件中提供相关信息,或者通过其他方式传递数组大小。
③ 注意事项
⚝ C++ 中数组的索引从 0
开始。对于大小为 n
的数组,有效的索引范围是 0
到 n-1
。
⚝ 数组在定义时必须指定大小,且大小在编译时 (Compile Time) 必须是已知的。C++ 标准数组的大小是固定的,一旦定义后就不能动态改变大小。如果需要动态大小的数组,可以使用 std::vector
容器 (Container),后续章节会详细介绍。
5.1.2 数组的初始化 (Initialization of Arrays)
数组可以在定义时进行初始化 (Initialization),为数组元素赋予初始值。C++ 提供了多种数组初始化的方式。
① 完全初始化 (Full Initialization)
在定义数组时,使用初始化列表 (Initializer List) {}
为数组的所有元素赋予初始值。
1
int numbers[5] = {1, 2, 3, 4, 5};
⚝ {1, 2, 3, 4, 5}
是一个初始化列表,列表中的值依次赋给数组 numbers
的元素:numbers[0] = 1
, numbers[1] = 2
, ..., numbers[4] = 5
。
⚝ 初始化列表中的值的数量必须等于或少于数组的大小。如果少于数组大小,剩余的元素将默认初始化 (Default Initialization)。对于 int
类型,默认初始化值为 0
。
② 部分初始化 (Partial Initialization)
如果初始化列表中的元素数量少于数组的大小,则只初始化数组的前几个元素,剩余的元素会被默认初始化为 0
(对于数值类型)。
1
int numbers[5] = {1, 2, 3};
⚝ 上述代码初始化了 numbers[0] = 1
, numbers[1] = 2
, numbers[2] = 3
。
⚝ numbers[3]
和 numbers[4]
将被默认初始化为 0
。
③ 省略数组大小的初始化
在完全初始化时,可以省略数组的大小,编译器会根据初始化列表中的元素数量自动推断数组的大小。
1
int numbers[] = {1, 2, 3, 4, 5}; // 编译器自动推断 numbers 的大小为 5
⚝ 这种方式更简洁,但也只能在定义时初始化,之后不能再用这种方式为数组重新赋值。
④ 字符数组的特殊初始化
字符数组 (Character Array) 可以使用字符串字面量 (String Literal) 进行初始化,更加方便。
1
char message[] = "Hello"; // 编译器自动在末尾添加空字符 '\0'
⚝ 字符串字面量 "Hello"
用于初始化字符数组 message
。
⚝ 编译器会自动在字符串末尾添加空字符 (Null Character) '\0'
,作为字符串的结束标志。因此,message
数组的大小实际上是 6,可以存储 'H', 'e', 'l', 'l', 'o', '\0'。
⚝ 等价于: char message[] = {'H', 'e', 'l', 'l', 'o', '\0'};
或者 char message[] = {'H', 'e', 'l', 'l', 'o'}; // 错误!缺少 '\0'
。注意: 使用字符数组存储字符串时,务必确保数组大小足够容纳字符串的所有字符以及结尾的空字符 '\0'
。
⑤ 默认初始化 (Default Initialization)
如果定义数组时没有提供初始化列表,则数组元素会被默认初始化。
⚝ 对于全局数组 (Global Array) 或 静态数组 (Static Array),数值类型的元素默认初始化为 0
,字符类型默认初始化为空字符 '\0'
,指针类型默认初始化为空指针 nullptr
。
⚝ 对于局部数组 (Local Array) (在函数内部定义的数组,不加 static
修饰),如果不显式初始化,则其元素的值是未定义的 (Undefined),包含了垃圾数据 (Garbage Data)。因此,建议总是显式地初始化局部数组。
1
int globalArray[5]; // 全局数组,元素默认初始化为 0
2
static int staticArray[5]; // 静态数组,元素默认初始化为 0
3
4
void func() {
5
int localArray[5]; // 局部数组,元素值未定义,是垃圾数据
6
int localInitializedArray[5] = {0}; // 局部数组,显式初始化为 0
7
}
5.1.3 数组元素的访问 (Accessing Array Elements)
数组元素通过数组名 (Array Name) 和 索引 (Index) 来访问。索引从 0
开始,到 数组大小 - 1
结束。
① 使用索引访问
使用下标运算符 (Subscript Operator) []
和索引值来访问数组中的元素。
1
int numbers[5] = {10, 20, 30, 40, 50};
2
3
int firstNumber = numbers[0]; // 访问第一个元素,值为 10
4
int thirdNumber = numbers[2]; // 访问第三个元素,值为 30
5
6
numbers[1] = 25; // 修改第二个元素的值为 25
⚝ numbers[0]
访问数组 numbers
的第一个元素(索引为 0)。
⚝ numbers[2]
访问数组 numbers
的第三个元素(索引为 2)。
⚝ numbers[1] = 25
修改数组 numbers
的第二个元素的值。
② 循环遍历数组
通常使用 for
循环 (for loop) 遍历数组的所有元素。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
3
for (int i = 0; i < 5; ++i) {
4
std::cout << "Element at index " << i << ": " << numbers[i] << std::endl;
5
}
⚝ 循环变量 i
从 0
递增到 4
,依次作为数组 numbers
的索引,访问并打印每个元素的值。
⚝ 循环条件 i < 5
确保了索引不会超出数组的有效范围。
③ 范围 for
循环 (Range-based for loop) (C++11)
C++11 引入了范围 for
循环 (Range-based for loop),可以更简洁地遍历数组或容器中的所有元素。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
3
for (int number : numbers) { // 依次将 numbers 中的元素赋值给 number
4
std::cout << "Element value: " << number << std::endl;
5
}
⚝ for (int number : numbers)
循环会自动遍历数组 numbers
中的每个元素,并将当前元素的值赋值给变量 number
。
⚝ 这种方式更简洁易读,但也只能访问数组元素的值,不能修改元素的值 (在上述例子中)。如果需要修改元素的值,可以使用引用 (Reference)。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
3
for (int& number : numbers) { // 使用 int& 类型,number 是数组元素的引用
4
number *= 2; // 修改 number 的值会影响数组元素的值
5
}
6
7
// 打印修改后的数组元素
8
for (int number : numbers) {
9
std::cout << "Modified element value: " << number << std::endl;
10
}
⚝ for (int& number : numbers)
中,int&
表明 number
是数组元素的引用 (Reference)。通过修改 number
的值,可以直接修改数组中对应元素的值。
④ 越界访问 (Out-of-bounds Access)
越界访问 (Out-of-bounds Access) 指的是访问数组时使用的索引超出了数组的有效索引范围(0
到 数组大小 - 1
)。C++ 不会对数组的越界访问进行自动检查 (Automatic Check),这意味着如果程序中出现数组越界访问,编译器 (Compiler) 不会报错,程序在运行时可能会产生未定义行为 (Undefined Behavior),例如程序崩溃、数据损坏或者得到意想不到的结果。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
3
int value = numbers[10]; // 越界访问,索引 10 超出有效范围 (0-4)
4
numbers[10] = 100; // 越界写入,同样是未定义行为
⚝ numbers[10]
试图访问索引为 10 的元素,但 numbers
数组的有效索引范围是 0 到 4,因此这是越界访问。
⚝ 越界访问是非常危险的,可能导致程序出现各种难以预料的错误。程序员需要手动确保数组访问的索引在有效范围内,避免越界访问。
5.1.4 多维数组 (Multidimensional Arrays)
C++ 支持多维数组 (Multidimensional Arrays),最常见的是二维数组 (Two-dimensional Arrays),可以看作是数组的数组,用于表示表格或矩阵等数据结构。
① 二维数组的定义和初始化
二维数组的定义需要指定行数 (Number of Rows) 和 列数 (Number of Columns)。
1
数据类型 数组名[行数][列数];
例如,定义一个 3 行 4 列的二维数组 matrix
:
1
int matrix[3][4];
⚝ matrix
是一个包含 3 个元素的数组,每个元素又是一个包含 4 个 int
元素的数组。可以将其看作一个 3 行 4 列的表格。
二维数组的初始化可以使用嵌套的初始化列表 (Nested Initializer Lists)。
1
int matrix[3][4] = {
2
{1, 2, 3, 4}, // 第一行
3
{5, 6, 7, 8}, // 第二行
4
{9, 10, 11, 12} // 第三行
5
};
⚝ 外层花括号 {}
包含所有行的初始化列表。
⚝ 内层花括号 {}
分别表示每一行的初始化列表。例如,{1, 2, 3, 4}
初始化 matrix
的第一行元素。
也可以部分初始化二维数组,规则与一维数组类似,未初始化的元素默认值为 0。
1
int matrix[3][4] = {
2
{1, 2}, // 第一行,前两个元素初始化,后两个默认 0
3
{5, 6, 7}, // 第二行,前三个元素初始化,最后一个默认 0
4
{9} // 第三行,第一个元素初始化,后三个默认 0
5
};
如果使用一维初始化列表来初始化二维数组,元素会按照行优先 (Row-major) 的顺序进行初始化。
1
int matrix[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; // 按行初始化
⚝ 初始化顺序为:matrix[0][0]=1
, matrix[0][1]=2
, matrix[0][2]=3
, matrix[0][3]=4
, matrix[1][0]=5
, ..., matrix[2][3]=12
。
② 二维数组元素的访问
使用两个索引来访问二维数组的元素,第一个索引表示行号 (Row Index),第二个索引表示列号 (Column Index),索引都从 0
开始。
1
int matrix[3][4] = {
2
{1, 2, 3, 4},
3
{5, 6, 7, 8},
4
{9, 10, 11, 12}
5
};
6
7
int element = matrix[1][2]; // 访问第二行第三列的元素,值为 7
8
9
matrix[0][0] = 100; // 修改第一行第一列的元素值为 100
⚝ matrix[1][2]
访问第 2 行第 3 列的元素(行索引为 1,列索引为 2)。
⚝ matrix[0][0] = 100
修改第 1 行第 1 列的元素值。
③ 遍历二维数组
通常使用嵌套循环 (Nested Loops) 遍历二维数组的所有元素。外层循环遍历行,内层循环遍历列。
1
int matrix[3][4] = {
2
{1, 2, 3, 4},
3
{5, 6, 7, 8},
4
{9, 10, 11, 12}
5
};
6
7
for (int i = 0; i < 3; ++i) { // 遍历行
8
for (int j = 0; j < 4; ++j) { // 遍历列
9
std::cout << "Element at row " << i << ", column " << j << ": " << matrix[i][j] << std::endl;
10
}
11
}
⚝ 外层循环控制行索引 i
从 0 到 2。
⚝ 内层循环控制列索引 j
从 0 到 3。
⚝ matrix[i][j]
访问当前行 i
和列 j
的元素。
④ 更高维度的数组
C++ 支持更高维度的数组,例如三维数组、四维数组等,定义和访问方式与二维数组类似,只需增加相应的维度索引。例如,定义一个三维数组:
1
int cube[3][4][5]; // 3x4x5 的三维数组
高维数组在处理多维数据时非常有用,例如在图像处理、科学计算等领域。但需要注意,数组的维度越高,占用的内存空间越大,访问效率可能会降低。
5.1.5 数组与指针 (Arrays and Pointers)
在 C++ 中,数组名在很多情况下可以退化 (Decay) 为指向数组首元素的指针 (Pointer)。理解数组与指针的关系是深入理解 C++ 数组的关键。
① 数组名是指针
当数组名单独使用时(除了作为 sizeof
运算符或取地址运算符 &
的操作数时),它会被解释为指向数组首元素的指针,即数组首元素的地址。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
int* ptr = numbers; // numbers 退化为指向 numbers[0] 的指针
3
4
std::cout << "Address of numbers[0]: " << &numbers[0] << std::endl;
5
std::cout << "Value of ptr: " << ptr << std::endl; // ptr 存储的是 numbers[0] 的地址
6
std::cout << "Value pointed to by ptr: " << *ptr << std::endl; // *ptr 解引用指针,得到 numbers[0] 的值 1
⚝ int* ptr = numbers;
将数组名 numbers
赋值给指针 ptr
。此时,numbers
退化为指向 numbers[0]
的指针,ptr
存储的是 numbers[0]
的内存地址。
⚝ *ptr
解引用 (Dereference) 指针 ptr
,得到指针指向的内存地址中存储的值,即 numbers[0]
的值 1。
② 指针的算术运算与数组访问
由于数组元素在内存中是连续存储的,可以通过指针的算术运算 (Pointer Arithmetic) 来访问数组中的其他元素。对于指向数组元素的指针 p
,p + n
(其中 n
是整数) 指向数组中距 p
所指元素后第 n
个元素的位置。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
int* ptr = numbers; // ptr 指向 numbers[0]
3
4
std::cout << "numbers[0]: " << *ptr << std::endl; // numbers[0]
5
std::cout << "numbers[1]: " << *(ptr + 1) << std::endl; // numbers[1]
6
std::cout << "numbers[2]: " << *(ptr + 2) << std::endl; // numbers[2]
7
std::cout << "numbers[3]: " << *(ptr + 3) << std::endl; // numbers[3]
8
std::cout << "numbers[4]: " << *(ptr + 4) << std::endl; // numbers[4]
⚝ ptr + 1
指针加 1,指针会移动到数组的下一个元素 numbers[1]
的地址。因为 ptr
是 int*
类型,所以指针加 1 实际上移动了 sizeof(int)
个字节。
⚝ *(ptr + 1)
解引用指针 ptr + 1
,得到 numbers[1]
的值。
⚝ *(ptr + n)
等价于 numbers[n]
,通过指针算术运算和解引用,可以访问数组的任意元素。
③ 使用指针遍历数组
可以使用指针来遍历数组,效率通常比使用索引略高,尤其是在循环次数很多的情况下。
1
int numbers[5] = {1, 2, 3, 4, 5};
2
int* ptr = numbers; // ptr 指向 numbers[0]
3
int* endPtr = numbers + 5; // endPtr 指向数组末尾元素的下一个位置
4
5
while (ptr < endPtr) {
6
std::cout << "Element value: " << *ptr << std::endl;
7
++ptr; // 指针递增,移动到下一个元素
8
}
⚝ int* endPtr = numbers + 5;
计算指向数组末尾元素的下一个位置的指针。numbers + 5
指向的是数组 numbers
之后 越界 (Out-of-bounds) 的位置,但作为循环结束的条件是安全的,因为循环条件 ptr < endPtr
会在 ptr
到达 endPtr
之前停止。
⚝ ++ptr
指针递增,使指针 ptr
指向数组的下一个元素。
⚝ 循环条件 ptr < endPtr
判断指针 ptr
是否到达数组的末尾。
④ 数组作为函数参数 (Arrays as Function Parameters)
当数组作为函数参数 (Function Parameter) 传递时,数组名会退化为指向数组首元素的指针。因此,在函数内部,无法直接获得数组的大小,需要显式地传递数组的大小。
1
void printArray(int arr[], int size) { // arr 实际上是指针 int* arr
2
for (int i = 0; i < size; ++i) {
3
std::cout << arr[i] << " "; // 可以像数组一样使用下标运算符访问
4
}
5
std::cout << std::endl;
6
}
7
8
int main() {
9
int numbers[5] = {1, 2, 3, 4, 5};
10
printArray(numbers, 5); // 传递数组名 numbers 和数组大小 5
11
return 0;
12
}
⚝ 函数 printArray
的参数 int arr[]
实际上会被编译器解释为 int* arr
,即指向 int
类型的指针。
⚝ 在 printArray
函数内部,arr
可以像数组名一样使用下标运算符 []
访问数组元素,例如 arr[i]
等价于 *(arr + i)
。
⚝ 需要在函数参数列表中额外传递一个参数 size
来表示数组的大小,因为通过指针 arr
无法直接获得数组的大小信息。
理解数组与指针的关系对于深入掌握 C++ 数组的使用至关重要,尤其是在动态内存分配、函数参数传递等方面。
5.2 字符串 (Strings)
在 C++ 中,字符串可以用两种主要方式表示:C 风格字符串 (C-style Strings) 和 std::string
类 (std::string Class)。
5.2.1 C 风格字符串 (C-style Strings)
C 风格字符串本质上是字符数组 (Character Arrays),以空字符 (Null Character) '\0'
结尾。空字符用于标记字符串的结束。C 风格字符串是 C 语言遗留下来的特性,在 C++ 中仍然广泛使用,尤其是在与 C 语言代码互操作时。
① C 风格字符串的定义和初始化
C 风格字符串就是字符数组,可以使用字符数组的定义和初始化方式。
1
char message1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式添加空字符
2
char message2[] = "Hello"; // 编译器自动添加空字符
3
char message3[20] = "World"; // 部分初始化,剩余位置未定义
⚝ message1
:显式初始化字符数组,并在末尾添加空字符 '\0'
。数组大小必须足够容纳所有字符和空字符。
⚝ message2
:使用字符串字面量 "Hello"
初始化字符数组,编译器会自动在末尾添加空字符。数组大小由字符串长度自动确定(包括空字符)。
⚝ message3
:定义大小为 20 的字符数组,并用 "World"
初始化。字符串 "World"
(包括空字符) 只占用数组的前 6 个位置,剩余位置的内容是未定义的。
② C 风格字符串的输入/输出
可以使用 std::cout
(输出流) 和 std::cin
(输入流) 进行 C 风格字符串的输入输出。
1
char name[20];
2
3
std::cout << "请输入你的名字:";
4
std::cin >> name; // 从标准输入读取字符串,直到遇到空格、制表符或换行符
5
6
std::cout << "你好," << name << "!" << std::endl;
⚝ std::cin >> name;
从标准输入读取字符串,并存储到字符数组 name
中。std::cin
在读取字符串时,遇到空格、制表符或换行符会停止读取,并在 name
数组末尾自动添加空字符 '\0'
。
⚝ std::cout << name;
输出 C 风格字符串 name
,std::cout
会从 name
数组的首地址开始输出字符,直到遇到空字符 '\0'
停止。
⚠️ 安全风险:缓冲区溢出 (Buffer Overflow)
使用 std::cin >> name;
读取 C 风格字符串时,存在缓冲区溢出 (Buffer Overflow) 的风险。如果用户输入的字符串长度超过 name
数组的大小(例如上述例子中超过 20 个字符),std::cin
仍然会继续写入数据,超出数组边界,覆盖相邻内存区域,可能导致程序崩溃或安全漏洞。
为了避免缓冲区溢出,可以使用 std::cin.getline()
函数 (std::cin.getline() Function) 来安全地读取一行字符串,并限制读取的字符数。
1
char address[100];
2
3
std::cout << "请输入你的地址:";
4
std::cin.getline(address, 100); // 最多读取 99 个字符,剩余位置留给 '\0'
5
6
std::cout << "你的地址是:" << address << std::endl;
⚝ std::cin.getline(address, 100);
从标准输入读取一行字符串,存储到 address
数组中,最多读取 99 个字符 (第 100 个位置留给空字符 '\0'
)。
⚝ std::cin.getline()
会读取整行输入,直到遇到换行符 \n
或达到指定的字符数限制。如果读取到换行符,std::cin.getline()
会丢弃换行符,不会将其存储到字符数组中。
③ C 风格字符串的常用函数
C 语言标准库 <cstring>
(在 C++ 中是 <cstring>
或 <string.h>
) 提供了一系列函数用于操作 C 风格字符串,例如:
⚝ strlen(str)
: 计算字符串 str
的长度(不包括空字符 '\0'
)。
⚝ strcpy(dest, src)
: 将字符串 src
复制到字符数组 dest
中。⚠️ 安全风险:缓冲区溢出,应使用更安全的 strncpy
。
⚝ strncpy(dest, src, n)
: 将字符串 src
的前 n
个字符复制到 dest
中。如果 src
的长度小于 n
,则 dest
的剩余部分会用空字符填充。如果 src
的长度大于等于 n
,则 dest
不会以空字符结尾,需要手动添加。
⚝ strcat(dest, src)
: 将字符串 src
连接到 dest
的末尾。⚠️ 安全风险:缓冲区溢出,应使用更安全的 strncat
。
⚝ strncat(dest, src, n)
: 将字符串 src
的前 n
个字符连接到 dest
的末尾,并在末尾添加空字符。
⚝ strcmp(str1, str2)
: 比较字符串 str1
和 str2
的大小。返回 0 表示相等,负数表示 str1 < str2
,正数表示 str1 > str2
。
⚝ strncmp(str1, str2, n)
: 比较字符串 str1
和 str2
的前 n
个字符。
⚝ strstr(haystack, needle)
: 在字符串 haystack
中查找子字符串 needle
,返回指向第一次出现 needle
的位置的指针,如果未找到则返回 nullptr
。
代码示例:
1
#include <iostream>
2
#include <cstring> // 引入 cstring 头文件
3
4
int main() {
5
char str1[20] = "Hello";
6
char str2[] = "World";
7
char str3[50];
8
9
// 计算字符串长度
10
std::cout << "Length of str1: " << strlen(str1) << std::endl; // 输出 5
11
12
// 字符串复制
13
strcpy(str3, str1); // 将 str1 复制到 str3
14
std::cout << "str3 after strcpy: " << str3 << std::endl; // 输出 Hello
15
16
// 字符串连接
17
strcat(str3, " "); // 在 str3 末尾连接空格
18
strcat(str3, str2); // 在 str3 末尾连接 str2
19
std::cout << "str3 after strcat: " << str3 << std::endl; // 输出 Hello World
20
21
// 字符串比较
22
if (strcmp(str1, str2) < 0) {
23
std::cout << "str1 is less than str2" << std::endl;
24
} else {
25
std::cout << "str1 is not less than str2" << std::endl;
26
} // 输出 str1 is less than str2
27
28
// 查找子字符串
29
char* ptr = strstr(str3, "World");
30
if (ptr != nullptr) {
31
std::cout << "Found 'World' in str3 at position: " << ptr - str3 << std::endl; // 输出 Found 'World' in str3 at position: 6
32
}
33
34
return 0;
35
}
⚠️ 安全风险:C 风格字符串的函数 (如 strcpy
, strcat
) 容易导致缓冲区溢出 (Buffer Overflow),因为它们不进行边界检查。在编写 C++ 程序时,应尽量使用更安全的函数 (如 strncpy
, strncat
) 或 std::string
类 来处理字符串,以提高程序的安全性和可靠性。
5.2.2 std::string
类 (std::string Class)
std::string
类是 C++ 标准库提供的字符串类 (String Class),定义在 <string>
头文件中。std::string
类封装了字符串的操作,提供了更安全、更方便、更强大的字符串处理功能,是 C++ 中处理字符串的首选方式。
① std::string
对象的定义和初始化
1
#include <string> // 引入 string 头文件
2
3
std::string str1; // 默认构造函数,创建空字符串
4
std::string str2 = "Hello"; // 使用字符串字面量初始化
5
std::string str3("World"); // 使用构造函数初始化
6
std::string str4 = str2; // 拷贝构造函数,使用 str2 初始化 str4
7
std::string str5(str3); // 拷贝构造函数,使用 str3 初始化 str5
8
std::string str6(10, 'a'); // 使用重复字符初始化,创建包含 10 个 'a' 的字符串
9
std::string str7(str2, 1, 3); // 使用子字符串初始化,从 str2 的索引 1 开始,取 3 个字符,str7 为 "ell"
10
std::string str8(str2.begin(), str2.end()); // 使用迭代器初始化,复制 str2 的内容
⚝ std::string str1;
:使用默认构造函数 (Default Constructor) 创建一个空字符串。
⚝ std::string str2 = "Hello";
:使用字符串字面量 (String Literal) 初始化 std::string
对象。
⚝ std::string str3("World");
:使用构造函数 (Constructor) 初始化 std::string
对象。
⚝ std::string str4 = str2;
和 std::string str5(str3);
:使用拷贝构造函数 (Copy Constructor),用已有的 std::string
对象初始化新的 std::string
对象。
⚝ std::string str6(10, 'a');
:使用重复字符构造函数,创建一个包含 10 个字符 'a'
的字符串。
⚝ std::string str7(str2, 1, 3);
:使用子字符串构造函数,从 str2
的索引 1 开始,取 3 个字符初始化 str7
。
⚝ std::string str8(str2.begin(), str2.end());
:使用迭代器 (Iterator) 初始化,复制 str2
的内容。
② std::string
对象的输入/输出
可以使用 std::cout
(输出流) 和 std::cin
(输入流) 进行 std::string
对象的输入输出,与 C 风格字符串类似。
1
std::string name;
2
3
std::cout << "请输入你的名字:";
4
std::cin >> name; // 从标准输入读取字符串,直到遇到空格、制表符或换行符
5
6
std::cout << "你好," << name << "!" << std::endl;
7
8
std::string line;
9
std::cout << "请输入一行文本:";
10
std::getline(std::cin, line); // 读取整行输入,包括空格
11
12
std::cout << "你输入的文本是:" << line << std::endl;
⚝ std::cin >> name;
:从标准输入读取字符串,存储到 std::string
对象 name
中。与 C 风格字符串类似,std::cin
在读取字符串时,遇到空格、制表符或换行符会停止读取。
⚝ std::cout << name;
:输出 std::string
对象 name
的内容。
⚝ std::getline(std::cin, line);
:读取整行输入,包括空格,直到遇到换行符 \n
。与 std::cin.getline()
不同,std::getline()
的第一个参数是输入流对象 std::cin
,第二个参数是 std::string
对象 line
。std::getline()
更常用于读取 std::string
类型的字符串。
安全:std::string
类自动管理内存 (Automatic Memory Management),不会发生缓冲区溢出 (Buffer Overflow)。std::string
对象的大小会根据存储的字符串内容动态调整,无需担心数组越界的问题。
③ std::string
类的常用操作
std::string
类提供了丰富的成员函数和运算符,用于字符串操作,例如:
⚝ 长度 (Length):
▮▮▮▮⚝ str.length()
或 str.size()
: 返回字符串的长度(字符个数),不包括空字符。
⚝ 访问字符 (Access Characters):
▮▮▮▮⚝ str[index]
: 使用下标运算符 (Subscript Operator) 访问指定索引位置的字符,不进行越界检查。如果索引越界,行为未定义。
▮▮▮▮⚝ str.at(index)
: 访问指定索引位置的字符,进行越界检查。如果索引越界,会抛出 std::out_of_range
异常。
⚝ 字符串连接 (String Concatenation):
▮▮▮▮⚝ str1 + str2
: 使用 +
运算符 (Operator) 连接两个字符串,返回一个新的 std::string
对象。
▮▮▮▮⚝ str1 += str2
: 使用 +=
运算符 (Operator) 将 str2
连接到 str1
的末尾,修改 str1
本身。
▮▮▮▮⚝ str.append(str2)
: 将 str2
连接到 str
的末尾,修改 str
本身。
⚝ 字符串比较 (String Comparison):
▮▮▮▮⚝ str1 == str2
, str1 != str2
, str1 < str2
, str1 <= str2
, str1 > str2
, str1 >= str2
: 使用关系运算符 (Relational Operators) 比较两个字符串的大小。按照字典序 (Lexicographical Order) 进行比较。
⚝ 查找子字符串 (Find Substring):
▮▮▮▮⚝ str.find(substring)
: 在字符串 str
中查找子字符串 substring
第一次出现的位置,返回子字符串首字符的索引。如果未找到,返回 std::string::npos
(一个静态成员常量,表示未找到)。
▮▮▮▮⚝ str.rfind(substring)
: 从字符串 str
的末尾开始向前查找子字符串 substring
第一次出现的位置,返回子字符串首字符的索引。如果未找到,返回 std::string::npos
。
▮▮▮▮⚝ str.find_first_of(chars)
: 在字符串 str
中查找字符集合 chars
中任意字符第一次出现的位置。
▮▮▮▮⚝ str.find_first_not_of(chars)
: 在字符串 str
中查找不在字符集合 chars
中的字符第一次出现的位置。
▮▮▮▮⚝ str.substr(pos, len)
: 提取字符串 str
中从索引 pos
开始,长度为 len
的子字符串,返回一个新的 std::string
对象。
⚝ 插入和删除 (Insert and Erase):
▮▮▮▮⚝ str.insert(pos, substring)
: 在字符串 str
的索引 pos
位置插入子字符串 substring
。
▮▮▮▮⚝ str.erase(pos, len)
: 删除字符串 str
中从索引 pos
开始,长度为 len
的子字符串。
⚝ 替换 (Replace):
▮▮▮▮⚝ str.replace(pos, len, new_substring)
: 将字符串 str
中从索引 pos
开始,长度为 len
的子字符串替换为 new_substring
。
⚝ 转换为 C 风格字符串 (Convert to C-style String):
▮▮▮▮⚝ str.c_str()
: 返回指向以空字符结尾的 C 风格字符串的常量指针 (Constant Pointer) (const char*
)。返回的指针指向的字符数组由 std::string
对象内部管理,生命周期与 std::string
对象相同。不要手动释放返回的指针。
代码示例:
1
#include <iostream>
2
#include <string>
3
4
int main() {
5
std::string str1 = "Hello";
6
std::string str2 = "World";
7
std::string str3;
8
9
// 字符串连接
10
str3 = str1 + " " + str2; // 使用 + 运算符连接
11
std::cout << "str3: " << str3 << std::endl; // 输出 Hello World
12
13
// 获取字符串长度
14
std::cout << "Length of str3: " << str3.length() << std::endl; // 输出 11
15
16
// 访问字符
17
std::cout << "First character of str3: " << str3[0] << std::endl; // 输出 H
18
std::cout << "Character at index 6 of str3: " << str3.at(6) << std::endl; // 输出 W
19
20
// 字符串比较
21
if (str1 == "Hello") {
22
std::cout << "str1 is equal to 'Hello'" << std::endl;
23
}
24
25
// 查找子字符串
26
size_t pos = str3.find("World");
27
if (pos != std::string::npos) {
28
std::cout << "'World' found at position: " << pos << std::endl; // 输出 'World' found at position: 6
29
}
30
31
// 提取子字符串
32
std::string sub = str3.substr(6, 5); // 从索引 6 开始,提取 5 个字符
33
std::cout << "Substring: " << sub << std::endl; // 输出 World
34
35
// 转换为 C 风格字符串
36
const char* cStr = str3.c_str();
37
std::cout << "C-style string: " << cStr << std::endl; // 输出 C-style string: Hello World
38
39
return 0;
40
}
std::string
类提供了强大而安全的字符串处理功能,建议在 C++ 程序中优先使用 std::string
类来处理字符串,以提高代码的可读性、可维护性和安全性。只有在需要与 C 语言代码或旧代码库兼容,或者对性能有极致要求时,才考虑使用 C 风格字符串。
6. 指针:内存的直接操纵
章节概要
本章深入讲解C++指针的概念、声明、初始化、解引用和运算,以及指针与数组、函数、动态内存分配的关系,帮助读者理解和掌握C++指针的精髓。指针是C++语言中一个强大而重要的特性,它允许程序直接访问和操纵内存,是理解C++内存管理和底层编程的关键。掌握指针对于深入学习C++,进行系统级编程、高性能计算以及复杂数据结构的设计至关重要。本章将通过清晰的解释、丰富的示例和实践指导,帮助读者克服指针学习的难点,真正理解和运用指针。
6.1 指针的概念:内存地址 (Memory Address)
6.1.1 内存与地址 (Memory and Address)
① 内存 (Memory) 的基本单元:
▮▮▮▮ⓑ 计算机的内存可以看作是一个连续的存储空间,由大量的存储单元 (Storage Unit) 组成。
▮▮▮▮ⓒ 每个存储单元都有一个唯一的地址 (Address),就像街道上的门牌号一样,用于标识和访问该单元。
▮▮▮▮ⓓ 最基本的存储单元是字节 (Byte),通常情况下,一个字节 (Byte) 由8个位 (Bit) 组成。
▮▮▮▮ⓔ 程序中的数据(如变量、常量、对象等)都存储在内存的这些存储单元中。
② 地址 (Address) 的作用:
▮▮▮▮ⓑ 地址是访问内存单元的唯一标识。
▮▮▮▮ⓒ 通过地址,程序可以读取 (Read) 存储单元中存储的数据,也可以写入 (Write) 数据到指定的存储单元。
▮▮▮▮ⓓ 可以将内存地址看作是数据在内存中的“门牌号”,程序通过门牌号找到数据并进行操作。
③ 内存地址的表示:
▮▮▮▮ⓑ 在C++中,内存地址通常用十六进制 (Hexadecimal) 数表示,例如 0x7ffd9b3c971c
。
▮▮▮▮ⓒ 不同的计算机体系结构 (Architecture) 可能使用不同长度的地址,例如32位系统和64位系统。
▮▮▮▮ⓓ 在C++程序中,可以使用指针 (Pointer) 来存储和操作内存地址。
1
#include <iostream>
2
3
int main() {
4
int number = 10;
5
// 使用 & 运算符获取变量 number 的内存地址
6
int* addressOfNumber = &number;
7
8
std::cout << "变量 number 的值: " << number << std::endl;
9
std::cout << "变量 number 的内存地址: " << addressOfNumber << std::endl;
10
11
return 0;
12
}
代码解释:
⚝ int number = 10;
:声明一个整型变量 number
并赋值为 10。
⚝ int* addressOfNumber = &number;
:
▮▮▮▮⚝ &
是取地址运算符 (Address-of Operator),&number
获取变量 number
的内存地址。
▮▮▮▮⚝ int*
声明了一个指向整型 (Integer) 的指针变量 (Pointer Variable) addressOfNumber
,用于存储整型变量的地址。
▮▮▮▮⚝ addressOfNumber = &number;
将变量 number
的内存地址赋值给指针变量 addressOfNumber
。
⚝ std::cout << "变量 number 的内存地址: " << addressOfNumber << std::endl;
:输出指针变量 addressOfNumber
中存储的内存地址。
6.1.2 为什么需要指针 (Why Pointers are Needed)
① 直接内存访问 (Direct Memory Access):
▮▮▮▮ⓑ 指针允许程序直接访问和操纵内存中的数据,提供了底层 (Low-level) 的控制能力。
▮▮▮▮ⓒ 这在某些场景下非常重要,例如:
▮▮▮▮⚝ 系统编程 (System Programming):操作系统、设备驱动程序等需要直接操作硬件资源,包括内存。
▮▮▮▮⚝ 嵌入式系统 (Embedded Systems):资源受限的嵌入式设备,需要精细的内存管理和控制。
▮▮▮▮⚝ 高性能计算 (High-Performance Computing):为了追求极致的性能,有时需要绕过高级抽象,直接操作内存。
② 高效的数据传递 (Efficient Data Passing):
▮▮▮▮ⓑ 在函数调用时,使用指针传递大型数据结构(如数组、对象)的地址,避免了值传递时的数据拷贝,提高了效率。
▮▮▮▮ⓒ 特别是在处理复杂对象或大数据量时,指针传递比值传递更高效。
③ 动态内存分配 (Dynamic Memory Allocation):
▮▮▮▮ⓑ 指针是实现动态内存分配的关键。通过 new
和 delete
运算符,程序可以在运行时动态地申请和释放内存,灵活地管理内存资源。
▮▮▮▮ⓒ 动态内存分配允许程序根据实际需求分配内存,而不是在编译时预先确定,提高了内存利用率。
④ 实现复杂数据结构 (Implementing Complex Data Structures):
▮▮▮▮ⓑ 指针是构建复杂数据结构(如链表、树、图等)的基础。
▮▮▮▮ⓒ 通过指针,可以灵活地组织和连接内存中的数据,实现各种复杂的数据关系。
⑤ 函数回调 (Function Callbacks):
▮▮▮▮ⓑ 指针可以指向函数,实现函数指针 (Function Pointer)。
▮▮▮▮ⓒ 函数指针可以作为参数传递给其他函数,实现回调 (Callback) 机制,增强程序的灵活性和可扩展性。
总结:指针是C++中一个强大而灵活的工具,它提供了直接内存访问的能力,可以用于提高程序效率、实现动态内存管理、构建复杂数据结构和实现函数回调等高级编程技巧。虽然指针学习曲线陡峭,容易出错,但掌握指针是成为C++高级程序员的必经之路。
6.2 指针的声明和初始化 (Pointer Declaration and Initialization)
6.2.1 指针变量的声明 (Declaring Pointer Variables)
① 声明指针变量的语法:
1
数据类型* 指针变量名;
2
数据类型 *指针变量名; // 推荐风格,* 更靠近类型,强调指针类型
3
数据类型*指针变量名;
⚝ 数据类型 (Data Type)
:指定指针所指向的数据类型。例如 int*
表示指向整型数据的指针,double*
表示指向双精度浮点型数据的指针。
⚝ *
:指针声明符 (Pointer Declarator),*
表明声明的变量是指针类型。
⚝ 指针变量名 (Pointer Variable Name)
:遵循变量命名规则,通常以 p
或 ptr
开头,以增强可读性,例如 pNumber
, ptrArray
。
② 示例:
1
int* pInteger; // 声明一个指向整型数据的指针 pInteger
2
double *pDouble; // 声明一个指向双精度浮点型数据的指针 pDouble
3
char* pCharacter; // 声明一个指向字符型数据的指针 pCharacter
4
float *ptrFloat; // 声明一个指向单精度浮点型数据的指针 ptrFloat
5
std::string* pString; // 声明一个指向字符串对象的指针 pString
6
void* pVoid; // 声明一个 void 指针 pVoid,可以指向任何数据类型
注意:
⚝ *
符号是类型声明的一部分,表示声明的是指针类型变量,而不是乘法运算符。
⚝ 指针变量本身也需要占用内存空间来存储地址值。指针变量的大小取决于计算机的体系结构(32位系统通常为4字节,64位系统通常为8字节),与指针指向的数据类型无关。
6.2.2 指针的初始化 (Initializing Pointers)
① 初始化为已存在的变量的地址:
⚝ 使用取地址运算符 &
获取已存在变量的内存地址,并赋值给指针变量。
1
int number = 100;
2
int* pNumber = &number; // 初始化 pNumber 指针,指向变量 number 的地址
3
4
double pi = 3.14159;
5
double* pPi = π // 初始化 pPi 指针,指向变量 pi 的地址
② 初始化为 nullptr
(空指针):
⚝ nullptr
是C++11引入的空指针常量 (Null Pointer Constant),用于表示指针不指向任何有效的内存地址。
⚝ 使用 nullptr
初始化指针是一种良好的编程习惯,可以避免野指针 (Wild Pointer) 问题。
1
int* pValue = nullptr; // 初始化 pValue 指针为空指针
2
double* ptrData = nullptr; // 初始化 ptrData 指针为空指针
③ 动态内存分配的地址:
⚝ 使用 new
运算符动态分配内存,new
运算符返回分配内存的首地址,可以将该地址赋值给指针变量。
1
int* pDynamicInt = new int; // 动态分配一个 int 类型大小的内存,并将地址赋值给 pDynamicInt
2
double* pDynamicArray = new double[10]; // 动态分配一个包含 10 个 double 类型元素的数组,并将首地址赋值给 pDynamicArray
④ 指针的赋值 (Pointer Assignment):
⚝ 可以将一个指针变量的值(即存储的地址)赋值给另一个指针变量,使两个指针指向相同的内存地址。
1
int number = 200;
2
int* pFirst = &number;
3
int* pSecond = pFirst; // pSecond 指针也指向 number 变量的地址
未初始化的指针 (Uninitialized Pointers):
⚝ 声明指针变量时,如果没有进行初始化,则指针变量的值是不确定的 (Undetermined)。
⚝ 未初始化的指针可能包含随机的内存地址,称为野指针 (Wild Pointer) 或悬空指针 (Dangling Pointer)。
⚝ 绝对避免使用未初始化的指针,因为对野指针进行解引用操作会导致未定义行为 (Undefined Behavior),可能导致程序崩溃、数据损坏或其他不可预测的错误。
1
int* pUninitialized; // 声明但未初始化的指针,pUninitialized 是野指针,不要使用!
2
// *pUninitialized = 5; // 错误!对野指针解引用,未定义行为!
最佳实践:
⚝ 始终初始化指针变量。
⚝ 如果指针在声明时没有明确的指向目标,初始化为 nullptr
。
⚝ 使用 new
分配内存后,立即将返回的地址赋值给指针。
⚝ 避免使用未初始化的指针,防止野指针错误。
6.3 指针的解引用 (Dereferencing Pointers)
6.3.1 解引用运算符 *
(Dereference Operator)
① 解引用运算符的作用:
⚝ 解引用运算符 *
(Dereference Operator) 用于访问指针所指向的内存单元中存储的值。
⚝ 如果指针 p
存储的是变量 v
的地址,那么 *p
就表示变量 v
本身。
⚝ 解引用操作是读取 (Read) 或修改 (Modify) 指针所指向的内存单元中数据的基础操作。
② 解引用运算符的语法:
1
*指针变量名
⚝ *
:解引用运算符,位于指针变量名前面。
⚝ 指针变量名
:已声明并初始化的指针变量。
③ 示例:
1
int number = 25;
2
int* pNumber = &number;
3
4
std::cout << "变量 number 的地址: " << pNumber << std::endl; // 输出地址
5
std::cout << "指针 pNumber 解引用的值: " << *pNumber << std::endl; // 输出 number 的值 (25)
6
7
*pNumber = 50; // 通过指针修改 number 的值
8
9
std::cout << "修改后变量 number 的值: " << number << std::endl; // 输出修改后的 number 的值 (50)
10
std::cout << "再次解引用指针 pNumber 的值: " << *pNumber << std::endl; // 再次解引用,输出修改后的值 (50)
代码解释:
⚝ int* pNumber = &number;
:指针 pNumber
指向变量 number
的地址。
⚝ std::cout << "指针 pNumber 解引用的值: " << *pNumber << std::endl;
:*pNumber
解引用指针 pNumber
,访问 pNumber
所指向的内存单元(即变量 number
的内存空间),并读取其中存储的值 (25)。
⚝ *pNumber = 50;
:*pNumber
位于赋值运算符的左侧,表示将值 50 写入到 pNumber
所指向的内存单元(即变量 number
的内存空间),从而修改了变量 number
的值。
⚝ 再次解引用 *pNumber
,输出的是修改后的值 (50)。
6.3.2 间接访问 (Indirect Access) 与直接访问 (Direct Access)
① 直接访问 (Direct Access):
⚝ 通过变量名 (Variable Name) 直接访问变量所代表的内存单元。
⚝ 例如 number = 10;
,直接修改变量 number
的值。
② 间接访问 (Indirect Access):
⚝ 通过指针 (Pointer) 间接地访问内存单元。
⚝ 首先,通过指针变量获取内存地址。
⚝ 然后,使用解引用运算符 *
访问该地址所指向的内存单元。
⚝ 例如 *pNumber = 10;
,通过指针 pNumber
间接修改变量 number
的值。
③ 对比:
特性 | 直接访问 (Direct Access) | 间接访问 (Indirect Access) |
---|---|---|
访问方式 | 通过变量名 | 通过指针 |
操作符 | 无操作符 | 解引用运算符 * |
灵活性 | 较低 | 较高 |
底层控制能力 | 较低 | 较高 |
适用场景 | 一般变量操作 | 需要底层内存操作、动态数据结构等 |
示例:
1
#include <iostream>
2
3
int main() {
4
int value = 100;
5
int* pValue = &value;
6
7
// 直接访问
8
std::cout << "直接访问 value 的值: " << value << std::endl; // 输出 100
9
value = 200; // 直接修改 value 的值
10
std::cout << "直接访问修改后 value 的值: " << value << std::endl; // 输出 200
11
12
// 间接访问
13
std::cout << "间接访问 pValue 解引用的值: " << *pValue << std::endl; // 输出 200 (与 value 的值相同)
14
*pValue = 300; // 通过指针间接修改 value 的值
15
std::cout << "间接访问修改后 pValue 解引用的值: " << *pValue << std::endl; // 输出 300
16
std::cout << "直接访问 value 的值: " << value << std::endl; // 输出 300 (value 的值也被修改了)
17
18
return 0;
19
}
总结:
⚝ 直接访问和间接访问都是访问内存中数据的手段。
⚝ 直接访问通过变量名,简单直观,适用于一般场景。
⚝ 间接访问通过指针,更灵活,提供了底层内存操作能力,适用于需要动态内存管理、复杂数据结构等高级编程场景。
⚝ 指针的解引用是实现间接访问的关键操作。
6.4 指针的运算 (Pointer Arithmetic)
6.4.1 指针的算术运算 (Arithmetic Operations on Pointers)
① 指针可以进行的算术运算:
⚝ 加法 (Addition):指针 +
整数
⚝ 减法 (Subtraction):指针 -
整数,指针 -
指针
⚝ 自增 (Increment):指针 ++
(前缀/后缀)
⚝ 自减 (Decrement):指针 --
(前缀/后缀)
② 指针算术运算的意义:
⚝ 指针的算术运算不是简单的数值运算,而是地址的偏移 (Address Offset) 运算。
⚝ 指针的加减运算是以指针所指向的数据类型的大小 (Size) 为单位进行的。
⚝ 例如,如果 p
是 int*
类型的指针,sizeof(int)
为 4 字节,那么 p + 1
不是将 p
的值加 1,而是将 p
的地址值增加 4 个字节,使其指向下一个 int
类型数据的位置。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
int array[5] = {10, 20, 30, 40, 50};
5
int* pArray = array; // 指针 pArray 指向数组 array 的首元素地址 (array[0])
6
7
std::cout << "数组首元素地址 (array[0]): " << pArray << ", 值: " << *pArray << std::endl; // 地址,10
8
9
pArray = pArray + 1; // 指针加 1,指向下一个 int 元素 (array[1])
10
std::cout << "指针 pArray + 1 后地址: " << pArray << ", 值: " << *pArray << std::endl; // 地址,20
11
12
pArray++; // 指针自增,指向再下一个 int 元素 (array[2])
13
std::cout << "指针 pArray++ 后地址: " << pArray << ", 值: " << *pArray << std::endl; // 地址,30
14
15
pArray = pArray - 2; // 指针减 2,指向上上个 int 元素 (array[0])
16
std::cout << "指针 pArray - 2 后地址: " << pArray << ", 值: " << *pArray << std::endl; // 地址,10
17
18
int* pEnd = array + 5; // 指向数组末尾元素的下一个位置 (越界)
19
std::cout << "数组末尾后一个位置地址: " << pEnd << std::endl;
20
21
// 指针相减:计算两个指针之间的元素个数 (以元素大小为单位)
22
ptrdiff_t diff = pEnd - array; // ptrdiff_t 是用于表示指针差值的带符号整型类型
23
std::cout << "指针 pEnd 和 array 首地址之差 (元素个数): " << diff << std::endl; // 输出 5
24
25
return 0;
26
}
代码解释:
⚝ int* pArray = array;
:数组名 array
本身就代表数组首元素的地址,因此可以直接赋值给指针 pArray
。
⚝ pArray = pArray + 1;
:指针 pArray
加 1,地址值增加 sizeof(int)
(通常为 4) 个字节,指向数组的下一个元素 array[1]
。
⚝ pArray++;
:指针自增,效果与 pArray = pArray + 1;
相同。
⚝ pArray = pArray - 2;
:指针 pArray
减 2,地址值减小 2 * sizeof(int)
个字节,指向数组的前两个元素 array[0]
。
⚝ int* pEnd = array + 5;
:指针 pEnd
指向数组 array
的越界 (Out-of-bounds) 位置,即数组最后一个元素的下一个位置。注意:解引用越界指针是未定义行为!
⚝ ptrdiff_t diff = pEnd - array;
:指针 pEnd
和 array
相减,得到的是两个指针之间相隔的元素个数 (以元素类型大小为单位)。
6.4.2 指针运算的有效性 (Validity of Pointer Arithmetic)
① 有效的指针运算:
⚝ 对指向数组元素 (Array Element) 的指针进行加减运算,使其在数组范围内移动是有效的。
⚝ 指针相减运算,计算同一数组内两个指针之间的元素个数是有效的。
② 无效的指针运算:
⚝ 对指向非数组元素 (Non-array Element) 的指针进行加减运算,其结果是未定义 (Undefined) 的。
⚝ 对不同数组 (Different Arrays) 的指针进行运算,结果通常是无意义 (Meaningless) 的。
⚝ 解引用越界指针 (Dereferencing Out-of-bounds Pointer) 是严重错误 (Serious Error),会导致未定义行为,例如程序崩溃、数据损坏等。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
int array1[5] = {1, 2, 3, 4, 5};
5
int array2[5] = {6, 7, 8, 9, 10};
6
int* p1 = array1;
7
int* p2 = array2;
8
9
// 有效的指针运算 (在同一数组内)
10
int* p1_end = array1 + 5;
11
ptrdiff_t diff1 = p1_end - p1; // 有效,计算 array1 的元素个数
12
13
// 无效的指针运算 (指向不同数组)
14
// ptrdiff_t diff2 = p2 - p1; // 通常无意义,结果取决于内存布局
15
16
// 越界访问 (错误!)
17
// int* p_out_of_bounds = array1 + 10; // 越界指针
18
// std::cout << *p_out_of_bounds << std::endl; // 错误!解引用越界指针,未定义行为!
19
20
return 0;
21
}
安全建议:
⚝ 进行指针运算时,要确保指针仍然指向有效的内存区域,特别是数组范围内。
⚝ 避免越界访问,防止未定义行为。
⚝ 在进行指针运算前,仔细考虑运算的意义和目的,确保逻辑正确。
⚝ 可以使用迭代器 (Iterator) 或范围 for
循环 (Range-based for loop) 等更安全的机制来遍历数组和容器,减少指针运算的错误风险。
6.5 指针与数组 (Pointers and Arrays)
6.5.1 数组名是指针 (Array Name as Pointer)
① 数组名 (Array Name) 的本质:
⚝ 在C++中,数组名 (Array Name) 在大多数情况下会被隐式转换为指向数组首元素 (First Element) 的指针 (Pointer)。
⚝ 当数组名单独使用时(例如赋值给指针、作为函数参数),它会被解释为数组首元素的地址。
② 数组名与指针的联系:
⚝ array
等价于 &array[0]
,都表示数组首元素的地址。
⚝ 可以通过数组名进行指针运算,例如 array + i
等价于 &array[i]
,表示数组第 i
个元素的地址。
⚝ 可以使用指针的方式访问数组元素,例如 *(array + i)
等价于 array[i]
,表示数组第 i
个元素的值。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
int array[5] = {100, 200, 300, 400, 500};
5
6
std::cout << "数组名 array 的值 (首元素地址): " << array << std::endl;
7
std::cout << "数组首元素地址 &array[0]: " << &array[0] << std::endl;
8
9
int* pArray = array; // 数组名赋值给指针
10
std::cout << "指针 pArray 的值: " << pArray << std::endl; // 与 array 的值相同
11
12
// 通过数组名和下标访问数组元素
13
std::cout << "array[2] 的值: " << array[2] << std::endl; // 300
14
15
// 通过指针和偏移量访问数组元素
16
std::cout << "*(array + 2) 的值: " << *(array + 2) << std::endl; // 300
17
std::cout << "pArray[2] 的值: " << pArray[2] << std::endl; // 300 (指针也可以像数组一样使用下标)
18
std::cout << "*(pArray + 2) 的值: " << *(pArray + 2) << std::endl; // 300
19
20
return 0;
21
}
代码解释:
⚝ std::cout << "数组名 array 的值 (首元素地址): " << array << std::endl;
:直接输出数组名 array
,输出的是数组首元素的地址。
⚝ int* pArray = array;
:将数组名 array
赋值给指针 pArray
,指针 pArray
指向数组首元素。
⚝ array[2]
和 *(array + 2)
、pArray[2]
和 *(pArray + 2)
都表示访问数组的第 3 个元素 (索引为 2),它们是等价的。
6.5.2 指针遍历数组 (Iterating Through Arrays with Pointers)
① 使用指针遍历数组的方法:
⚝ 可以使用指针的自增 (Increment) 运算 ++
,使指针依次指向数组的每个元素,从而遍历数组。
⚝ 通常结合循环 (Loop) 结构(如 for
循环、while
循环)来实现数组的遍历。
② 示例:
1
#include <iostream>
2
3
int main() {
4
int array[5] = {10, 20, 30, 40, 50};
5
int* pArray = array; // 指针指向数组首元素
6
7
std::cout << "使用指针遍历数组:" << std::endl;
8
9
// 方法一:使用 for 循环和指针自增
10
for (int i = 0; i < 5; ++i) {
11
std::cout << "元素 " << i << ": " << *pArray << std::endl;
12
pArray++; // 指针指向下一个元素
13
}
14
15
// 方法二:使用 while 循环和指针运算
16
pArray = array; // 指针重新指向数组首元素
17
int* pEnd = array + 5; // 指向数组末尾元素的下一个位置
18
while (pArray < pEnd) { // 当指针未越界时循环
19
std::cout << "元素: " << *pArray << std::endl;
20
pArray++; // 指针指向下一个元素
21
}
22
23
// 方法三:使用范围 for 循环 (更安全、更简洁)
24
std::cout << "使用范围 for 循环遍历数组:" << std::endl;
25
for (int element : array) { // 范围 for 循环自动遍历数组元素
26
std::cout << "元素: " << element << std::endl;
27
}
28
29
return 0;
30
}
代码解释:
⚝ 方法一和方法二:使用指针 pArray
遍历数组,通过指针自增 pArray++
使指针依次指向数组的每个元素,并使用解引用运算符 *
访问元素的值。
⚝ 方法三:使用C++11引入的范围 for
循环 (Range-based for loop),可以更简洁、更安全地遍历数组和容器,避免手动指针操作的错误。推荐使用范围 for
循环进行数组遍历。
6.5.3 数组作为函数参数 (Arrays as Function Arguments)
① 数组作为函数参数的传递方式:
⚝ 当数组作为函数参数传递时,实际上退化 (Decay) 为指向数组首元素的指针 (Pointer)。
⚝ 函数接收到的不是整个数组的拷贝,而是数组首元素的地址。
⚝ 因此,在函数内部,可以通过指针操作来访问和修改数组的元素。
② 函数参数声明形式:
⚝ 形式一:指针形式
1
void processArray(int* arr, int size); // arr 是指向 int 类型的指针
⚝ 形式二:数组形式
1
void processArray(int arr[], int size); // arr 看起来像数组,但本质上仍然是指针
2
void processArray(int arr[5], int size); // 指定数组大小,但大小信息在函数调用时会被忽略,仍然是指针
注意:在函数参数声明中,int arr[]
和 int arr[5]
等数组形式声明,本质上都会被编译器解释为 int* arr
指针声明。数组的大小信息在函数参数传递时会丢失,函数内部无法直接获取数组的长度。因此,通常需要额外传递一个参数 size
来表示数组的长度。
③ 示例:
1
#include <iostream>
2
3
// 函数:处理整型数组,计算数组元素之和
4
int sumArray(int arr[], int size) { // 数组参数,本质是指针 int* arr
5
int sum = 0;
6
for (int i = 0; i < size; ++i) {
7
sum += arr[i]; // 可以像数组一样使用下标访问元素,实际上是指针运算 *(arr + i)
8
}
9
return sum;
10
}
11
12
int main() {
13
int numbers[5] = {1, 2, 3, 4, 5};
14
int arraySum = sumArray(numbers, 5); // 数组名 numbers 作为参数传递,退化为指针
15
std::cout << "数组元素之和: " << arraySum << std::endl; // 输出 15
16
17
return 0;
18
}
代码解释:
⚝ sumArray(int arr[], int size)
函数,参数 int arr[]
本质上是指针 int* arr
,接收数组 numbers
的首元素地址。
⚝ 在函数内部,可以使用数组下标 arr[i]
访问数组元素,实际上是指针运算 *(arr + i)
的语法糖。
⚝ sumArray(numbers, 5)
调用函数时,数组名 numbers
作为参数传递,退化为指向数组首元素的指针。
总结:
⚝ 数组名在大多数情况下会被视为指向数组首元素的指针。
⚝ 指针和数组在很多方面可以互换使用,例如指针运算、下标访问等。
⚝ 数组作为函数参数传递时,会退化为指针,函数内部无法直接获取数组长度,需要额外传递长度参数。
⚝ 理解指针与数组的关系,是深入掌握C++内存管理和高效编程的关键。
6.6 指针与函数 (Pointers and Functions)
6.6.1 函数指针 (Function Pointers)
① 函数指针的概念:
⚝ 函数指针 (Function Pointer) 是指向函数 (Function) 的指针变量 (Pointer Variable)。
⚝ 与数据指针指向数据存储位置类似,函数指针指向函数在内存中的代码起始地址。
⚝ 通过函数指针,可以间接调用 (Indirectly Call) 函数,实现回调 (Callback)、策略模式 (Strategy Pattern) 等高级编程技巧。
② 函数指针的声明:
1
返回值类型 (*指针变量名)(参数列表);
⚝ 返回值类型 (Return Type)
:函数指针指向的函数的返回值类型。
⚝ (*指针变量名)
:指针变量名,*
表示是指针,()
括号不能省略,否则会变成返回指针的函数声明。
⚝ (参数列表)
:函数指针指向的函数的参数列表,包括参数类型和参数个数。
③ 示例:
1
#include <iostream>
2
3
// 定义一个整型加法函数
4
int add(int a, int b) {
5
return a + b;
6
}
7
8
// 定义一个整型减法函数
9
int subtract(int a, int b) {
10
return a - b;
11
}
12
13
int main() {
14
// 声明一个函数指针 pOperation,指向返回 int 类型、接受两个 int 类型参数的函数
15
int (*pOperation)(int, int);
16
17
// 将 add 函数的地址赋值给函数指针 pOperation
18
pOperation = add;
19
std::cout << "使用函数指针调用 add 函数: " << pOperation(5, 3) << std::endl; // 输出 8
20
21
// 将 subtract 函数的地址赋值给函数指针 pOperation
22
pOperation = subtract;
23
std::cout << "使用函数指针调用 subtract 函数: " << pOperation(5, 3) << std::endl; // 输出 2
24
25
return 0;
26
}
代码解释:
⚝ int (*pOperation)(int, int);
:声明一个函数指针 pOperation
,它可以指向任何返回值类型为 int
,参数列表为 (int, int)
的函数。
⚝ pOperation = add;
:将函数 add
的地址 (Address) 赋值给函数指针 pOperation
。函数名 (Function Name) 本身就代表函数的地址,类似于数组名代表数组首元素地址。
⚝ pOperation(5, 3)
:通过函数指针 pOperation
间接调用 (Indirectly Call) 函数,实际上调用的是 add(5, 3)
。
⚝ pOperation = subtract;
:将函数指针 pOperation
指向另一个函数 subtract
,后续通过 pOperation
调用的是 subtract
函数。
6.6.2 函数指针作为函数参数 (Function Pointers as Function Arguments)
① 函数指针作为函数参数的作用:
⚝ 函数指针可以作为参数传递给其他函数,实现回调机制 (Callback Mechanism)。
⚝ 回调函数 (Callback Function):将函数指针作为参数传递给另一个函数,在适当的时候,被调用的函数会通过函数指针反过来调用 (Call Back) 传递进来的函数。
⚝ 回调机制可以增强程序的灵活性 (Flexibility) 和可扩展性 (Extensibility),允许在运行时动态地指定函数的行为。
② 示例:
1
#include <iostream>
2
3
// 定义一个通用的计算函数,接受函数指针作为参数
4
int calculate(int a, int b, int (*operation)(int, int)) {
5
return operation(a, b); // 通过函数指针调用传递进来的函数
6
}
7
8
// 加法函数 (回调函数)
9
int add(int a, int b) {
10
return a + b;
11
}
12
13
// 乘法函数 (回调函数)
14
int multiply(int a, int b) {
15
return a * b;
16
}
17
18
int main() {
19
int x = 10, y = 5;
20
21
// 使用 add 函数进行计算
22
int sumResult = calculate(x, y, add); // 将 add 函数指针作为参数传递
23
std::cout << "加法结果: " << sumResult << std::endl; // 输出 15
24
25
// 使用 multiply 函数进行计算
26
int productResult = calculate(x, y, multiply); // 将 multiply 函数指针作为参数传递
27
std::cout << "乘法结果: " << productResult << std::endl; // 输出 50
28
29
return 0;
30
}
代码解释:
⚝ calculate(int a, int b, int (*operation)(int, int))
函数,第三个参数 int (*operation)(int, int)
是一个函数指针,用于接收一个函数地址。
⚝ calculate
函数内部,通过 operation(a, b)
回调 (Call Back) 传递进来的函数指针 operation
所指向的函数。
⚝ calculate(x, y, add)
调用时,将 add
函数的指针作为参数传递给 calculate
函数,calculate
函数会回调 add
函数进行加法计算。
⚝ calculate(x, y, multiply)
调用时,将 multiply
函数的指针作为参数传递给 calculate
函数,calculate
函数会回调 multiply
函数进行乘法计算。
6.6.3 返回指针的函数 (Functions Returning Pointers)
① 返回指针的函数 (Functions Returning Pointers):
⚝ 函数可以返回指针类型 (Pointer Type) 的值。
⚝ 返回指针的函数通常用于:
▮▮▮▮⚝ 动态内存分配 (Dynamic Memory Allocation):函数内部使用 new
分配内存,并将分配内存的地址作为指针返回。
▮▮▮▮⚝ 返回数据结构中的元素地址:例如,在数组或链表中查找元素,并返回指向该元素的指针。
② 示例:
1
#include <iostream>
2
3
// 函数:动态分配一个整型变量,并返回指向该变量的指针
4
int* createInteger(int initialValue) {
5
int* pInteger = new int; // 动态分配内存
6
*pInteger = initialValue; // 初始化值
7
return pInteger; // 返回指针
8
}
9
10
int main() {
11
int* ptr = createInteger(123); // 调用函数,接收返回的指针
12
13
if (ptr != nullptr) { // 检查指针是否有效 (是否分配成功)
14
std::cout << "动态分配的整数值: " << *ptr << std::endl; // 输出 123
15
delete ptr; // 释放动态分配的内存,防止内存泄漏 (Memory Leak)
16
ptr = nullptr; // 将指针置为空指针,防止悬空指针 (Dangling Pointer)
17
}
18
19
return 0;
20
}
代码解释:
⚝ int* createInteger(int initialValue)
函数,返回值类型为 int*
,表示函数返回一个指向 int
类型的指针。
⚝ 函数内部,使用 new int;
动态分配一个 int
类型大小的内存,并将分配内存的地址赋值给指针 pInteger
。
⚝ return pInteger;
将指针 pInteger
返回。
⚝ 在 main
函数中,int* ptr = createInteger(123);
调用 createInteger
函数,并将返回的指针赋值给 ptr
。
⚝ 重要:如果函数返回动态分配的内存的指针,调用者 (Caller) 必须负责释放 (Free) 这块内存,以防止内存泄漏 (Memory Leak)。使用 delete ptr;
释放动态分配的内存。
注意:
⚝ 避免返回指向局部变量 (Local Variable) 的指针。局部变量在函数执行结束后会被销毁,返回的指针会变成悬空指针 (Dangling Pointer),访问悬空指针是未定义行为。
1
int* incorrectFunction() {
2
int localVar = 10;
3
return &localVar; // 错误!返回局部变量的地址,localVar 在函数结束后销毁,返回的指针变成悬空指针!
4
}
⚝ 返回指针的函数需要明确内存管理责任 (Memory Management Responsibility)。如果函数负责动态分配内存并返回指针,则调用者需要负责释放内存。良好的文档和注释可以帮助明确内存管理责任。
6.7 动态内存分配 (Dynamic Memory Allocation)
6.7.1 new
运算符 (new Operator)
① new
运算符的作用:
⚝ new
运算符 (new Operator) 用于在堆 (Heap) 内存区域动态分配 (Dynamically Allocate) 内存。
⚝ 动态分配的内存在程序运行时 (Runtime) 分配,而不是在编译时 (Compile Time) 确定。
⚝ new
运算符返回分配内存的首地址 (Starting Address),通常需要使用指针来接收和管理这块内存。
② new
运算符的语法:
⚝ 分配单个对象:
1
指针变量 = new 数据类型;
⚝ 分配数组:
1
指针变量 = new 数据类型[数组大小];
⚝ 数据类型 (Data Type)
:指定要分配内存存储的数据类型。
⚝ 数组大小 (Array Size)
:指定要分配的数组元素的个数 (仅在分配数组时使用)。
⚝ 指针变量 (Pointer Variable)
:用于接收 new
运算符返回的内存地址。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
// 动态分配一个 int 类型大小的内存
5
int* pInt = new int;
6
if (pInt != nullptr) { // 检查内存分配是否成功
7
*pInt = 100;
8
std::cout << "动态分配的整数值: " << *pInt << std::endl; // 输出 100
9
delete pInt; // 释放动态分配的内存
10
pInt = nullptr;
11
} else {
12
std::cerr << "内存分配失败!" << std::endl;
13
}
14
15
// 动态分配一个包含 5 个 int 类型元素的数组
16
int* pArray = new int[5];
17
if (pArray != nullptr) { // 检查内存分配是否成功
18
for (int i = 0; i < 5; ++i) {
19
pArray[i] = i * 10;
20
}
21
std::cout << "动态分配的数组元素: ";
22
for (int i = 0; i < 5; ++i) {
23
std::cout << pArray[i] << " "; // 输出 0 10 20 30 40
24
}
25
std::cout << std::endl;
26
delete[] pArray; // 释放动态分配的数组内存,使用 delete[]
27
pArray = nullptr;
28
} else {
29
std::cerr << "数组内存分配失败!" << std::endl;
30
}
31
32
return 0;
33
}
代码解释:
⚝ int* pInt = new int;
:使用 new int
动态分配一个 int
类型大小的内存,返回的地址赋值给指针 pInt
。
⚝ int* pArray = new int[5];
:使用 new int[5]
动态分配一个包含 5 个 int
类型元素的数组,返回的地址赋值给指针 pArray
。
⚝ 内存分配失败处理:new
运算符在内存分配失败时,在较新的C++标准中会抛出 std::bad_alloc
异常 (Exception)。但旧的C++标准中,new
运算符在分配失败时可能返回空指针 nullptr
。为了兼容性和健壮性,通常需要检查 new
运算符的返回值是否为空指针,或者使用异常处理机制 (Exception Handling Mechanism) 来捕获 std::bad_alloc
异常。示例代码中使用了检查空指针的方式。
⚝ 释放内存:动态分配的内存必须手动释放 (Manually Free),使用 delete
运算符释放单个对象的内存,使用 delete[]
运算符释放数组的内存。不释放动态分配的内存会导致内存泄漏 (Memory Leak)。
⚝ 将指针置为空指针:释放内存后,将指针置为 nullptr
,防止悬空指针 (Dangling Pointer)。
6.7.2 delete
运算符 (delete Operator)
① delete
运算符的作用:
⚝ delete
运算符 (delete Operator) 用于释放 (Free) 由 new
运算符动态分配的内存,将内存归还给系统,使其可以被重新使用。
⚝ delete
运算符必须与 new
运算符配对使用,new
分配的内存必须使用 delete
释放。
② delete
运算符的语法:
⚝ 释放单个对象内存:
1
delete 指针变量;
⚝ 释放数组内存:
1
delete[] 指针变量; // 注意 [],用于释放数组内存
⚝ 指针变量 (Pointer Variable)
:指向要释放的动态分配内存的指针。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
int* pNumber = new int; // 动态分配单个 int 内存
5
double* pDataArray = new double[10]; // 动态分配 double 数组内存
6
7
// ... 使用动态分配的内存 ...
8
9
delete pNumber; // 释放单个 int 内存,使用 delete
10
pNumber = nullptr;
11
12
delete[] pDataArray; // 释放 double 数组内存,使用 delete[],注意 []
13
pDataArray = nullptr;
14
15
return 0;
16
}
使用 delete
运算符的注意事项:
⚝ 配对使用 new
和 delete
:new
分配的内存必须使用 delete
释放,new[]
分配的数组内存必须使用 delete[]
释放。不配对使用会导致未定义行为。
⚝ 只能释放动态分配的内存:delete
运算符只能用于释放由 new
运算符动态分配的内存。不能释放栈 (Stack) 内存或静态 (Static) 内存,否则会导致错误。
⚝ 释放后的指针置为 nullptr
:释放内存后,将指针置为 nullptr
,防止悬空指针 (Dangling Pointer)。
⚝ 避免重复释放 (Double Free):不要对同一块内存重复使用 delete
运算符,否则会导致程序崩溃 (Crash) 或其他严重错误。
6.7.3 动态内存分配的意义 (Significance of Dynamic Memory Allocation)
① 灵活的内存管理 (Flexible Memory Management):
⚝ 动态内存分配允许程序在运行时 (Runtime) 根据实际需求动态地 (Dynamically) 申请和释放内存。
⚝ 程序可以根据输入数据的大小、用户操作等运行时信息 (Runtime Information) 决定分配多少内存,提高了内存利用率。
⚝ 与静态内存分配 (Static Memory Allocation) 相比,动态内存分配更加灵活,可以适应程序运行时的各种变化。
② 创建动态数据结构 (Creating Dynamic Data Structures):
⚝ 动态内存分配是创建动态数据结构 (Dynamic Data Structures) 的基础,例如链表 (Linked List)、树 (Tree)、图 (Graph) 等。
⚝ 动态数据结构的大小可以在运行时动态调整,根据需要添加或删除节点,非常灵活。
⚝ 动态数据结构在处理不确定大小的数据集合、实现复杂算法时非常有用。
③ 示例:动态数组 (Dynamic Array):
⚝ 静态数组 (Static Array) 的大小在编译时 (Compile Time) 必须确定,大小固定,不能动态改变。
1
int staticArray[10]; // 静态数组,大小在编译时确定为 10
⚝ 动态数组 (Dynamic Array) 的大小可以在运行时 (Runtime) 动态指定,并可以根据需要调整大小。
1
int size;
2
std::cout << "请输入数组大小: ";
3
std::cin >> size;
4
5
int* dynamicArray = new int[size]; // 动态数组,大小在运行时指定
6
// ... 使用 dynamicArray ...
7
delete[] dynamicArray; // 释放动态数组内存
总结:
⚝ 动态内存分配通过 new
和 delete
运算符实现,允许程序在运行时动态地申请和释放堆内存。
⚝ 动态内存分配提供了灵活的内存管理能力,可以根据运行时需求动态调整内存大小。
⚝ 动态内存分配是创建动态数据结构的基础,是C++高级编程的重要特性。
⚝ 必须谨慎管理动态分配的内存,避免内存泄漏和悬空指针等问题。
6.8 空指针和悬空指针 (Null Pointers and Dangling Pointers)
6.8.1 空指针 nullptr
(Null Pointer)
① 空指针的概念:
⚝ 空指针 (Null Pointer) 是一个特殊的指针值,表示指针不指向任何有效的内存地址 (Valid Memory Address)。
⚝ C++11 标准引入了 nullptr
关键字 (nullptr Keyword) 作为空指针常量,替代了之前的 NULL
(宏定义,通常为整数 0)。
⚝ nullptr
是类型安全 (Type-safe) 的空指针常量,可以隐式转换为任何指针类型。
② 空指针的用途:
⚝ 初始化指针:将指针初始化为 nullptr
,表示指针当前没有指向任何有效的内存。
⚝ 错误检查:检查指针是否为空指针,判断指针是否有效,例如 new
运算符分配内存可能失败返回空指针,函数可能返回空指针表示错误或特殊情况。
⚝ 结束条件:在某些数据结构(如链表)中,使用空指针作为链表末尾的标记。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
int* pValue = nullptr; // 初始化为空指针
5
6
if (pValue == nullptr) {
7
std::cout << "指针 pValue 是空指针。" << std::endl;
8
}
9
10
int* pDynamic = new int;
11
if (pDynamic == nullptr) { // 内存分配失败,new 返回空指针 (旧标准)
12
std::cerr << "内存分配失败!" << std::endl;
13
} else {
14
*pDynamic = 100;
15
std::cout << "动态分配的整数值: " << *pDynamic << std::endl;
16
delete pDynamic;
17
pDynamic = nullptr; // 释放后置为空指针
18
}
19
20
return 0;
21
}
注意:
⚝ 不能对空指针进行解引用操作。解引用空指针会导致运行时错误 (Runtime Error),通常是段错误 (Segmentation Fault),程序会崩溃。
⚝ 在解引用指针之前,务必检查指针是否为空指针,以避免程序崩溃。
6.8.2 悬空指针 (Dangling Pointer)
① 悬空指针的概念:
⚝ 悬空指针 (Dangling Pointer) 是指指针指向的内存已经被释放 (Freed) 或销毁 (Destroyed),但指针本身仍然保存着该内存地址。
⚝ 悬空指针指向的内存已经无效,解引用悬空指针会导致未定义行为 (Undefined Behavior)。
② 悬空指针产生的原因:
⚝ 内存被释放后,指针没有被置为空指针:动态分配的内存被 delete
释放后,指针仍然指向原来的内存地址,但该地址的内存可能已经被系统回收或重新分配给其他程序。
⚝ 局部变量超出作用域:函数返回指向局部变量的指针,当函数执行结束后,局部变量被销毁,返回的指针变成悬空指针。
③ 示例:
1
#include <iostream>
2
3
int main() {
4
int* pDangling = new int;
5
*pDangling = 200;
6
std::cout << "原始值: " << *pDangling << std::endl; // 输出 200
7
8
delete pDangling; // 释放内存
9
// pDangling 现在是悬空指针,指向已释放的内存
10
11
// std::cout << "悬空指针解引用: " << *pDangling << std::endl; // 错误!解引用悬空指针,未定义行为!
12
13
pDangling = nullptr; // 将指针置为空指针,避免悬空指针问题
14
15
if (pDangling != nullptr) { // 安全检查
16
// std::cout << "尝试解引用空指针: " << *pDangling << std::endl; // 不会执行,因为 pDangling 是空指针
17
} else {
18
std::cout << "指针已置为空指针,避免悬空指针问题。" << std::endl;
19
}
20
21
return 0;
22
}
避免悬空指针的方法:
⚝ 及时置空指针:当动态分配的内存被 delete
释放后,立即将指向该内存的指针置为 nullptr
。
⚝ 避免返回指向局部变量的指针:不要返回指向局部变量的指针,局部变量超出作用域后会被销毁,返回的指针会变成悬空指针。
⚝ 智能指针 (Smart Pointers):使用C++智能指针(如 std::unique_ptr
, std::shared_ptr
)来管理动态分配的内存,智能指针可以自动管理内存的释放,减少内存泄漏和悬空指针的风险。
6.8.3 空指针与悬空指针的对比
特性 | 空指针 nullptr (Null Pointer) | 悬空指针 (Dangling Pointer) |
---|---|---|
指针值 | 特殊的地址值,表示不指向任何有效内存 | 指向已释放或销毁的内存地址 |
指向内存 | 不指向任何有效内存 | 指向无效内存 |
产生原因 | 初始化为 nullptr ,内存分配失败等 | 内存释放后指针未置空,局部变量超出作用域等 |
解引用行为 | 运行时错误 (段错误) | 未定义行为,可能导致程序崩溃、数据损坏等 |
避免方法 | 检查是否为空指针,避免解引用空指针 | 及时置空指针,避免返回局部变量指针,使用智能指针 |
危害程度 | 程序崩溃,相对容易调试 | 未定义行为,难以调试,可能造成更严重的问题 |
总结:
⚝ 空指针 nullptr
是一个安全的特殊指针值,用于表示指针不指向任何有效内存,可以用于初始化和错误检查。必须避免解引用空指针。
⚝ 悬空指针 指向已释放或销毁的内存,解引用悬空指针是未定义行为,非常危险。
⚝ 良好的编程习惯 (置空指针、避免返回局部变量指针)、智能指针 可以帮助避免悬空指针问题。
6.9 指针类型与 void
指针 (Pointer Types and void Pointers)
6.9.1 指针类型 (Pointer Types)
① 指针类型的概念:
⚝ 指针类型 (Pointer Type) 不仅表示指针本身是指针,还指定了指针所指向的数据类型 (Data Type)。
⚝ 例如 int*
是指向 int
类型数据的指针类型,double*
是指向 double
类型数据的指针类型。
⚝ 指针类型决定了:
▮▮▮▮⚝ 指针解引用时访问的内存大小和数据类型:int*
指针解引用访问 4 字节 (通常) 内存,并解释为整型数据;double*
指针解引用访问 8 字节 (通常) 内存,并解释为双精度浮点型数据。
▮▮▮▮⚝ 指针算术运算的步长 (Step Size):int*
指针加 1,地址值增加 sizeof(int)
字节;double*
指针加 1,地址值增加 sizeof(double)
字节。
▮▮▮▮⚝ 指针类型检查:C++是强类型语言 (Strongly-typed Language),指针类型需要匹配,不同类型的指针之间不能直接赋值或隐式转换 (除了 void*
指针)。
② 示例:
1
#include <iostream>
2
3
int main() {
4
int intValue = 10;
5
double doubleValue = 3.14;
6
7
int* pInt = &intValue; // int* 指针指向 int 数据
8
double* pDouble = &doubleValue; // double* 指针指向 double 数据
9
10
std::cout << "int* 指针解引用: " << *pInt << std::endl; // 正确,访问 int 数据
11
std::cout << "double* 指针解引用: " << *pDouble << std::endl; // 正确,访问 double 数据
12
13
// 类型不匹配的指针赋值 (编译错误!)
14
// pInt = pDouble; // 错误!不能将 double* 指针赋值给 int* 指针,类型不匹配
15
16
return 0;
17
}
强制类型转换 (Type Casting):
⚝ 如果需要将一种指针类型转换为另一种指针类型,可以使用强制类型转换 (Type Casting)。
⚝ C++ 提供了多种强制类型转换运算符,例如 static_cast
, reinterpret_cast
等。
⚝ 强制类型转换需要谨慎使用,不当的类型转换可能导致数据解释错误或未定义行为。
6.9.2 void
指针 (void Pointer)
① void
指针的概念:
⚝ void
指针 (void Pointer) 是一种特殊的指针类型,也称为通用指针 (Generic Pointer)。
⚝ void*
指针可以指向任何数据类型 (Any Data Type) 的内存地址。
⚝ void*
指针只保存内存地址,不包含任何类型信息。
② void
指针的特点:
⚝ 通用性:可以指向任何数据类型,例如 int
, double
, char
, 结构体, 类对象等。
⚝ 不能直接解引用:void*
指针不能直接使用解引用运算符 *
,因为编译器不知道 void*
指针指向的数据类型,无法确定解引用时访问的内存大小和数据类型。
⚝ 需要类型转换才能解引用:要解引用 void*
指针,必须先将其转换为具体的指针类型 (如 int*
, char*
等),然后再进行解引用操作。
⚝ 可以进行指针运算:void*
指针可以进行指针运算,但步长 (Step Size) 不确定,通常将 void*
指针的算术运算步长视为 1 字节。
③ void
指针的应用场景:
⚝ 通用数据处理:在某些场景下,需要处理不同数据类型的数据,可以使用 void*
指针作为通用接口,例如内存操作函数 (memcpy
, memset
)、通用数据容器等。
⚝ 类型擦除 (Type Erasure):在泛型编程 (Generic Programming) 中,可以使用 void*
指针实现类型擦除,隐藏具体类型信息,提高代码的通用性。
④ 示例:
1
#include <iostream>
2
3
int main() {
4
int intValue = 123;
5
double doubleValue = 3.14159;
6
7
void* pVoid; // 声明 void 指针
8
9
pVoid = &intValue; // void* 指针指向 int 数据
10
// std::cout << *pVoid << std::endl; // 错误!void* 指针不能直接解引用
11
12
// 将 void* 指针转换为 int* 指针,再解引用
13
int* pInt = static_cast<int*>(pVoid);
14
std::cout << "void* 指针转换为 int* 后解引用: " << *pInt << std::endl; // 输出 123
15
16
pVoid = &doubleValue; // void* 指针指向 double 数据
17
// 将 void* 指针转换为 double* 指针,再解引用
18
double* pDouble = static_cast<double*>(pVoid);
19
std::cout << "void* 指针转换为 double* 后解引用: " << *pDouble << std::endl; // 输出 3.14159
20
21
return 0;
22
}
代码解释:
⚝ void* pVoid;
:声明一个 void
指针 pVoid
。
⚝ pVoid = &intValue;
和 pVoid = &doubleValue;
:void*
指针可以指向 int
和 double
类型的数据。
⚝ int* pInt = static_cast<int*>(pVoid);
和 double* pDouble = static_cast<double*>(pVoid);
:使用 static_cast
将 void*
指针强制转换为具体的指针类型 (int*
和 double*
),才能进行解引用操作。
总结:
⚝ 指针类型 指定了指针指向的数据类型,影响指针的解引用和运算行为,C++是强类型语言,需要注意指针类型匹配。
⚝ void
指针 是一种通用指针,可以指向任何数据类型,但不包含类型信息,不能直接解引用,需要类型转换后才能使用。
⚝ void
指针在通用数据处理和类型擦除等场景中很有用,但也需要谨慎使用,避免类型安全问题。
7. 结构体、联合体和枚举:自定义数据类型
7.1 结构体 (Structures)
结构体 (Structures) 是 C++ 中一种用户自定义的复合数据类型,允许将多个不同类型的数据成员组合成一个单一的单元。结构体使得我们可以将相关的数据组织在一起,形成更有意义的数据结构,从而提高代码的可读性和可维护性。在面向对象编程 (Object-Oriented Programming, OOP) 中,结构体可以看作是类的基础,为构建更复杂的对象打下基础。
7.1.1 结构体的定义 (Structure Definition)
要定义一个结构体,需要使用关键字 struct
,后跟结构体的名称,并在花括号 {}
内声明结构体的成员。每个成员都有自己的类型和名称。结构体定义通常放在头文件或全局作用域中,以便在程序的多个地方使用。
1
struct Student {
2
char name[50]; // 姓名 (name)
3
int age; // 年龄 (age)
4
float gpa; // 平均绩点 (Grade Point Average)
5
char studentID[20]; // 学号 (Student ID)
6
};
上述代码定义了一个名为 Student
的结构体,它包含了四个成员:name
(姓名),类型为字符数组 char[50]
;age
(年龄),类型为整型 int
;gpa
(平均绩点),类型为浮点型 float
;studentID
(学号),类型为字符数组 char[20]
。结构体的定义以分号 ;
结尾。
结构体定义的语法格式:
1
struct 结构体名 (Structure Name) {
2
成员类型 (Member Type) 成员名1 (Member Name 1);
3
成员类型 (Member Type) 成员名2 (Member Name 2);
4
// ...
5
成员类型 (Member Type) 成员名n (Member Name n);
6
};
要点:
① struct
关键字用于声明结构体。
② 结构体名 (Structure Name)
是用户自定义的标识符,遵循标识符的命名规则。
③ 花括号 {}
内是结构体的成员列表,每个成员声明包括 成员类型 (Member Type)
和 成员名 (Member Name)
,以分号 ;
结尾。
④ 结构体定义以分号 ;
结尾,这是一个常见的语法错误点,需要注意。
7.1.2 结构体变量的声明和初始化 (Structure Variable Declaration and Initialization)
定义结构体类型后,就可以声明结构体变量。结构体变量的声明方式与普通变量类似,指定结构体类型名,后跟变量名。
1
Student student1; // 声明一个 Student 类型的结构体变量 student1
2
Student student2; // 声明另一个 Student 类型的结构体变量 student2
结构体变量的初始化可以在声明时进行,也可以在声明后赋值。初始化可以使用以下几种方式:
① 列表初始化 (List Initialization): 使用花括号 {}
将初始值列表放在结构体变量名后面。初始值列表中的值按照结构体成员的声明顺序排列。
1
Student student1 = {"张三", 20, 3.8, "2023001"}; // 列表初始化
2
Student student2 = {.name = "李四", .age = 21, .gpa = 3.5, .studentID = "2023002"}; // 成员初始化 (C++20 起支持)
② 逐成员赋值 (Member-wise Assignment): 在声明结构体变量后,通过成员访问运算符 .
逐个为结构体成员赋值。
1
Student student3;
2
strcpy(student3.name, "王五"); // 使用 strcpy 复制字符串到字符数组
3
student3.age = 22;
4
student3.gpa = 3.9;
5
strcpy(student3.studentID, "2023003");
③ 默认初始化 (Default Initialization): 如果在声明结构体变量时没有提供初始值,结构体成员将进行默认初始化。内置类型成员(如 int
, float
)的默认初始值是不确定的,字符数组的默认初始值也是不确定的,因此建议显式初始化结构体变量。
1
Student student4; // 默认初始化,成员的值是不确定的
要点:
① 结构体变量的声明使用 结构体名 变量名;
的形式。
② 列表初始化使用花括号 {}
,初始值与结构体成员顺序对应。
③ C++20 起支持使用成员初始化器 {.成员名 = 值}
,可以不按顺序初始化成员。
④ 逐成员赋值通过成员访问运算符 .
为每个成员赋值。
⑤ 默认初始化时,内置类型成员的值是不确定的,建议显式初始化。
⑥ 对于字符数组类型的成员,需要使用 strcpy
等字符串复制函数进行赋值,避免直接赋值导致错误。
7.1.3 结构体成员的访问 (Accessing Structure Members)
要访问结构体变量的成员,需要使用成员访问运算符 .
。运算符 .
连接结构体变量名和成员名。
1
Student student1 = {"张三", 20, 3.8, "2023001"};
2
3
// 访问 student1 的成员
4
std::cout << "姓名 (Name): " << student1.name << std::endl;
5
std::cout << "年龄 (Age): " << student1.age << std::endl;
6
std::cout << "平均绩点 (GPA): " << student1.gpa << std::endl;
7
std::cout << "学号 (Student ID): " << student1.studentID << std::endl;
8
9
// 修改 student1 的成员
10
student1.age = 21;
11
student1.gpa = 4.0;
12
std::cout << "修改后的年龄 (Age): " << student1.age << std::endl;
13
std::cout << "修改后的平均绩点 (GPA): " << student1.gpa << std::endl;
上述代码演示了如何使用成员访问运算符 .
访问和修改结构体 student1
的成员。
成员访问运算符 .
的语法格式:
1
结构体变量名.成员名
要点:
① 成员访问运算符 .
用于访问结构体变量的成员。
② 通过 结构体变量名.成员名
可以读取或修改结构体成员的值。
7.1.4 结构体数组 (Arrays of Structures)
可以声明结构体数组,即数组的每个元素都是结构体类型的变量。结构体数组可以用来存储和处理批量结构体数据。
1
Student students[3]; // 声明一个包含 3 个 Student 结构体变量的数组
2
3
// 初始化结构体数组
4
students[0] = {"张三", 20, 3.8, "2023001"};
5
students[1] = {"李四", 21, 3.5, "2023002"};
6
students[2] = {"王五", 22, 3.9, "2023003"};
7
8
// 遍历结构体数组并访问成员
9
for (int i = 0; i < 3; ++i) {
10
std::cout << "学生 " << i + 1 << ":" << std::endl;
11
std::cout << " 姓名 (Name): " << students[i].name << std::endl;
12
std::cout << " 年龄 (Age): " << students[i].age << std::endl;
13
std::cout << " 平均绩点 (GPA): " << students[i].gpa << std::endl;
14
std::cout << " 学号 (Student ID): " << students[i].studentID << std::endl;
15
}
上述代码声明了一个包含 3 个 Student
结构体变量的数组 students
,并对其进行了初始化和遍历访问。
结构体数组的声明格式:
1
结构体名 数组名[数组大小];
访问结构体数组元素的成员:
1
数组名[下标].成员名
要点:
① 结构体数组的声明方式与普通数组类似,类型为结构体类型。
② 可以使用下标访问结构体数组的元素,每个元素都是一个结构体变量。
③ 访问结构体数组元素的成员,需要结合数组下标和成员访问运算符 .
。
7.1.5 结构体指针 (Pointers to Structures)
可以声明指向结构体变量的指针,称为结构体指针。结构体指针存储的是结构体变量的地址,可以通过指针间接访问结构体成员。
1
Student student1 = {"张三", 20, 3.8, "2023001"};
2
Student* pStudent = &student1; // 声明一个指向 Student 结构体的指针 pStudent,并指向 student1
3
4
// 通过指针访问结构体成员
5
std::cout << "姓名 (Name): " << (*pStudent).name << std::endl; // 使用 (*pStudent). 访问成员
6
std::cout << "年龄 (Age): " << (*pStudent).age << std::endl;
7
std::cout << "平均绩点 (GPA): " << (*pStudent).gpa << std::endl;
8
std::cout << "学号 (Student ID): " << (*pStudent).studentID << std::endl;
9
10
// 使用箭头运算符 -> 访问结构体成员 (更简洁的方式)
11
std::cout << "姓名 (Name): " << pStudent->name << std::endl; // 使用 -> 运算符访问成员
12
std::cout << "年龄 (Age): " << pStudent->age << std::endl;
13
std::cout << "平均绩点 (GPA): " << pStudent->gpa << std::endl;
14
std::cout << "学号 (Student ID): " << pStudent->studentID << std::endl;
15
16
// 修改结构体成员的值
17
pStudent->age = 22;
18
std::cout << "修改后的年龄 (Age): " << pStudent->age << std::endl;
上述代码演示了如何声明和使用结构体指针 pStudent
,以及如何通过指针访问和修改结构体 student1
的成员。
结构体指针的声明格式:
1
结构体名 *指针名;
通过结构体指针访问成员:
① 使用 (*指针名).成员名
方式,先解引用指针,再使用成员访问运算符 .
。
② 使用 指针名->成员名
方式,箭头运算符 ->
是专门为结构体指针设计的,更简洁直观。
要点:
① 结构体指针存储结构体变量的地址。
② 可以通过解引用指针 (*指针名)
再使用 .
运算符访问成员。
③ 更常用和推荐的方式是使用箭头运算符 ->
直接通过指针访问成员。
④ 通过结构体指针可以间接修改结构体变量的成员值。
7.1.6 嵌套结构体 (Nested Structures)
结构体的成员也可以是另一个结构体类型,这就是嵌套结构体。嵌套结构体可以用来表示更复杂的数据关系。
1
struct Address {
2
char city[50]; // 城市 (city)
3
char street[100]; // 街道 (street)
4
int postalCode; // 邮政编码 (postal code)
5
};
6
7
struct Employee {
8
char name[50]; // 姓名 (name)
9
int employeeID; // 员工编号 (employee ID)
10
Address address; // 地址 (address),嵌套结构体
11
};
12
13
int main() {
14
Employee employee1;
15
strcpy(employee1.name, "赵六");
16
employee1.employeeID = 1001;
17
strcpy(employee1.address.city, "北京"); // 访问嵌套结构体的成员
18
strcpy(employee1.address.street, "中关村大街"); // 访问嵌套结构体的成员
19
employee1.address.postalCode = 100080; // 访问嵌套结构体的成员
20
21
std::cout << "员工姓名 (Employee Name): " << employee1.name << std::endl;
22
std::cout << "员工编号 (Employee ID): " << employee1.employeeID << std::endl;
23
std::cout << "城市 (City): " << employee1.address.city << std::endl; // 访问嵌套结构体的成员
24
std::cout << "街道 (Street): " << employee1.address.street << std::endl; // 访问嵌套结构体的成员
25
std::cout << "邮政编码 (Postal Code): " << employee1.address.postalCode << std::endl; // 访问嵌套结构体的成员
26
27
return 0;
28
}
上述代码定义了两个结构体 Address
和 Employee
,其中 Employee
结构体嵌套了 Address
结构体作为其成员。访问嵌套结构体的成员需要使用多个成员访问运算符 .
。
访问嵌套结构体成员的格式:
1
结构体变量名.嵌套结构体成员名.更深层成员名
要点:
① 结构体的成员可以是另一个结构体类型,形成嵌套结构体。
② 访问嵌套结构体的成员需要使用多个成员访问运算符 .
,逐层访问。
③ 嵌套结构体可以表示更复杂的数据结构和关系,例如地址、联系方式等。
7.1.7 结构体与函数 (Structures and Functions)
结构体可以作为函数的参数和返回值,使得函数可以处理和操作结构体类型的数据。
① 结构体作为函数参数:
结构体可以值传递、引用传递或指针传递给函数。
1
#include <iostream>
2
#include <cstring>
3
4
struct Student {
5
char name[50];
6
int age;
7
float gpa;
8
char studentID[20];
9
};
10
11
// 值传递结构体
12
void printStudentValue(Student s) {
13
std::cout << "姓名 (Name): " << s.name << std::endl;
14
std::cout << "年龄 (Age): " << s.age << std::endl;
15
std::cout << "平均绩点 (GPA): " << s.gpa << std::endl;
16
std::cout << "学号 (Student ID): " << s.studentID << std::endl;
17
}
18
19
// 引用传递结构体
20
void increaseStudentAgeReference(Student& s) {
21
s.age++; // 修改了原始结构体变量的 age 成员
22
}
23
24
// 指针传递结构体
25
void printStudentPointer(Student* p) {
26
std::cout << "姓名 (Name): " << p->name << std::endl;
27
std::cout << "年龄 (Age): " << p->age << std::endl;
28
std::cout << "平均绩点 (GPA): " << p->gpa << std::endl;
29
std::cout << "学号 (Student ID): " << p->studentID << std::endl;
30
}
31
32
int main() {
33
Student student1 = {"张三", 20, 3.8, "2023001"};
34
35
std::cout << "值传递 (Pass-by-Value):" << std::endl;
36
printStudentValue(student1); // 值传递,不会修改 student1
37
38
std::cout << "\n引用传递 (Pass-by-Reference) 前:" << std::endl;
39
printStudentValue(student1);
40
increaseStudentAgeReference(student1); // 引用传递,会修改 student1 的 age
41
std::cout << "\n引用传递 (Pass-by-Reference) 后:" << std::endl;
42
printStudentValue(student1); // student1 的 age 被修改
43
44
std::cout << "\n指针传递 (Pass-by-Pointer):" << std::endl;
45
printStudentPointer(&student1); // 指针传递,不会修改 student1 本身,但可以通过指针修改成员
46
47
return 0;
48
}
② 结构体作为函数返回值:
函数可以返回结构体类型的值或结构体指针。
1
#include <iostream>
2
#include <cstring>
3
4
struct Point {
5
int x;
6
int y;
7
};
8
9
// 返回结构体值
10
Point createPointValue(int x, int y) {
11
Point p = {x, y};
12
return p; // 返回结构体的值
13
}
14
15
// 返回结构体指针 (注意内存管理)
16
Point* createPointPointer(int x, int y) {
17
Point* p = new Point{x, y}; // 动态分配内存
18
return p; // 返回结构体指针
19
}
20
21
int main() {
22
Point p1 = createPointValue(10, 20); // 接收结构体返回值
23
std::cout << "点 (Point) p1: (" << p1.x << ", " << p1.y << ")" << std::endl;
24
25
Point* p2 = createPointPointer(30, 40); // 接收结构体指针返回值
26
std::cout << "点 (Point) p2: (" << p2->x << ", " << p2->y << ")" << std::endl;
27
delete p2; // 释放动态分配的内存,避免内存泄漏
28
29
return 0;
30
}
要点:
① 结构体可以作为函数的参数进行值传递、引用传递或指针传递,与基本数据类型类似。
② 值传递不会修改原始结构体变量,引用传递和指针传递可以修改原始结构体变量的成员。
③ 函数可以返回结构体类型的值或结构体指针。
④ 返回结构体指针时,需要注意内存管理,避免内存泄漏,特别是当结构体是在函数内部动态分配时。
7.2 联合体 (Unions)
联合体 (Unions) 是 C++ 中另一种用户自定义的数据类型,它允许在相同的内存位置存储不同的数据类型。与结构体不同,联合体的所有成员共享同一块内存空间,因此在任何时候,联合体中只有一个成员是有效的。联合体主要用于节省内存,在某些特定场景下非常有用。
7.2.1 联合体的定义 (Union Definition)
定义联合体与定义结构体类似,使用关键字 union
,后跟联合体的名称,并在花括号 {}
内声明联合体的成员。
1
union Data {
2
int i; // 整型 (integer) 成员
3
float f; // 浮点型 (floating-point) 成员
4
char str[20]; // 字符数组 (character array) 成员
5
};
上述代码定义了一个名为 Data
的联合体,它包含了三个成员:i
(整型),f
(浮点型),和 str
(字符数组)。所有成员共享同一块内存空间。
联合体定义的语法格式:
1
union 联合体名 (Union Name) {
2
成员类型 (Member Type) 成员名1 (Member Name 1);
3
成员类型 (Member Type) 成员名2 (Member Name 2);
4
// ...
5
成员类型 (Member Type) 成员名n (Member Name n);
6
};
要点:
① union
关键字用于声明联合体。
② 联合体名 (Union Name)
是用户自定义的标识符。
③ 花括号 {}
内是联合体的成员列表,每个成员声明包括 成员类型 (Member Type)
和 成员名 (Member Name)
。
④ 联合体的所有成员共享同一块内存空间。
⑤ 联合体的大小等于其最大成员的大小。
7.2.2 联合体变量的声明和初始化 (Union Variable Declaration and Initialization)
联合体变量的声明方式与结构体变量类似。初始化方式略有不同,因为在任何时候只有一个成员是有效的,所以初始化通常只针对第一个成员。
1
Data data1; // 声明一个 Data 类型的联合体变量 data1
2
Data data2 = {10}; // 初始化 data2 的第一个成员 i 为 10
3
// Data data3 = {10, 3.14}; // 错误!联合体只能初始化第一个成员
联合体变量的初始化方式:
① 列表初始化: 使用花括号 {}
初始化联合体变量。初始值只能用于初始化联合体的第一个成员。
1
Data data1 = {100}; // 初始化第一个成员 i 为 100
② 默认初始化: 如果没有显式初始化,联合体变量的成员将进行默认初始化。内置类型成员的默认初始值是不确定的。
1
Data data2; // 默认初始化,成员的值是不确定的
要点:
① 联合体变量的声明使用 联合体名 变量名;
的形式。
② 初始化联合体变量时,通常只初始化第一个成员。
③ 联合体在任何时候只有一个成员是有效的,因此不能同时初始化多个成员。
7.2.3 联合体成员的访问 (Accessing Union Members)
访问联合体成员的方式与访问结构体成员相同,使用成员访问运算符 .
和箭头运算符 ->
(对于联合体指针)。
1
Data data1;
2
data1.i = 10; // 为整型成员 i 赋值
3
std::cout << "data1.i: " << data1.i << std::endl;
4
5
data1.f = 3.14f; // 为浮点型成员 f 赋值
6
std::cout << "data1.f: " << data1.f << std::endl;
7
std::cout << "data1.i: " << data1.i << std::endl; // 此时 data1.i 的值可能已失效,因为内存被 f 覆盖
8
9
strcpy(data1.str, "Hello"); // 为字符数组成员 str 赋值
10
std::cout << "data1.str: " << data1.str << std::endl;
11
std::cout << "data1.f: " << data1.f << std::endl; // 此时 data1.f 的值可能已失效,因为内存被 str 覆盖
12
std::cout << "data1.i: " << data1.i << std::endl; // 此时 data1.i 的值可能已失效,因为内存被 str 覆盖
13
14
Data* pData = &data1;
15
std::cout << "pData->i: " << pData->i << std::endl; // 通过指针访问成员
上述代码演示了如何访问和修改联合体 data1
的成员。由于联合体成员共享内存,当为一个成员赋值时,之前存储在同一内存位置的其他成员的值可能会被覆盖或失效。
访问联合体成员的格式:
1
联合体变量名.成员名
2
// 或
3
联合体指针->成员名
要点:
① 访问联合体成员的方式与结构体相同,使用 .
和 ->
运算符。
② 联合体的成员共享同一块内存空间,修改一个成员的值可能会影响其他成员的值。
③ 在程序中使用联合体时,需要清楚地知道当前哪个成员是有效的,避免访问无效成员导致错误。
7.2.4 联合体的使用场景和注意事项 (Use Cases and Precautions for Unions)
使用场景:
① 节省内存: 当需要在同一内存位置存储不同类型的数据,但同一时间只使用其中一种类型时,可以使用联合体来节省内存空间。例如,表示一个数值,可能是整型、浮点型或字符型,但类型是互斥的。
② 类型转换的底层实现: 联合体可以用于某些底层操作,例如查看一个数据的不同类型表示。但这种用法通常不太安全,应谨慎使用。
③ 与结构体结合使用: 联合体常与结构体结合使用,结构体的一个成员可以是联合体,用于表示某个属性可能具有多种类型的情况。
注意事项:
① 内存共享: 联合体的所有成员共享同一块内存,因此在任何时候只有一个成员是有效的。
② 类型安全: 联合体不保证类型安全。程序员需要手动跟踪当前联合体中存储的是哪种类型的数据,并正确访问相应的成员。访问错误的成员可能导致数据解释错误或程序崩溃。
③ 大小: 联合体的大小等于其最大成员的大小。
④ 初始化: 联合体只能初始化第一个成员。
⑤ 不适用于面向对象: 联合体主要用于数据表示,不适合用于面向对象编程中的复杂对象表示,类 (class) 更适合用于表示具有复杂行为的对象。
示例:结构体中包含联合体,表示具有多种类型的属性
1
#include <iostream>
2
#include <string>
3
4
// 定义一个联合体,表示值可以是整型或浮点型
5
union Value {
6
int intValue;
7
float floatValue;
8
};
9
10
// 定义一个结构体,包含一个联合体成员,表示属性值
11
struct Attribute {
12
std::string name; // 属性名 (attribute name)
13
int type; // 属性类型 (attribute type): 0-整型, 1-浮点型
14
Value value; // 属性值 (attribute value),联合体
15
};
16
17
int main() {
18
Attribute attr1;
19
attr1.name = "数量"; // Quantity
20
attr1.type = 0; // 整型
21
attr1.value.intValue = 100; // 设置整型值
22
23
Attribute attr2;
24
attr2.name = "价格"; // Price
25
attr2.type = 1; // 浮点型
26
attr2.value.floatValue = 99.99f; // 设置浮点型值
27
28
std::cout << "属性 (Attribute) 1: " << attr1.name << ", 类型 (Type): 整型 (Integer), 值 (Value): " << attr1.value.intValue << std::endl;
29
std::cout << "属性 (Attribute) 2: " << attr2.name << ", 类型 (Type): 浮点型 (Float), 值 (Value): " << attr2.value.floatValue << std::endl;
30
31
return 0;
32
}
在这个示例中,Attribute
结构体使用 Value
联合体来表示属性值,属性值可以是整型或浮点型,通过 type
成员来指示当前 value
成员存储的是哪种类型的数据。这种结构在需要表示具有多种可能类型的数据时非常有用。
7.3 枚举 (Enumerations)
枚举 (Enumerations) 是 C++ 中用户自定义的数据类型,用于表示一组命名的整数常量。枚举类型可以提高代码的可读性和可维护性,特别是在需要使用一组具有特定含义的常量时。
7.3.1 枚举的定义 (Enumeration Definition)
定义枚举类型使用关键字 enum
,后跟枚举类型的名称,并在花括号 {}
内列出枚举常量 (枚举成员),枚举常量之间用逗号 ,
分隔。
1
enum Color {
2
RED, // 红色 (Red)
3
GREEN, // 绿色 (Green)
4
BLUE // 蓝色 (Blue)
5
};
上述代码定义了一个名为 Color
的枚举类型,它包含了三个枚举常量:RED
(红色), GREEN
(绿色), 和 BLUE
(蓝色)。默认情况下,枚举常量的值从 0 开始,依次递增 1。RED
的值为 0, GREEN
的值为 1, BLUE
的值为 2。
枚举定义的语法格式:
1
enum 枚举名 (Enumeration Name) {
2
枚举常量1 (Enumerator 1),
3
枚举常量2 (Enumerator 2),
4
// ...
5
枚举常量n (Enumerator n)
6
};
可以显式指定枚举常量的值:
1
enum Status {
2
PENDING = 1, // 待处理 (Pending)
3
PROCESSING, // 处理中 (Processing),值为 2 (默认递增)
4
COMPLETED = 10, // 已完成 (Completed)
5
FAILED // 失败 (Failed),值为 11 (默认递增)
6
};
在这个例子中,PENDING
的值为 1, PROCESSING
的值为 2, COMPLETED
的值为 10, FAILED
的值为 11。
要点:
① enum
关键字用于声明枚举类型。
② 枚举名 (Enumeration Name)
是用户自定义的标识符。
③ 花括号 {}
内是枚举常量列表,枚举常量之间用逗号 ,
分隔。
④ 枚举常量默认从 0 开始赋值,后续常量的值依次递增 1。
⑤ 可以显式为枚举常量指定整数值。
7.3.2 枚举变量的声明和初始化 (Enumeration Variable Declaration and Initialization)
声明枚举变量时,需要指定枚举类型名,后跟变量名。枚举变量只能被赋值为枚举类型中定义的枚举常量或其对应的整数值。
1
Color myColor; // 声明一个 Color 枚举类型的变量 myColor
2
3
// 初始化枚举变量
4
myColor = RED; // 使用枚举常量初始化
5
myColor = Color::GREEN; // 使用作用域解析运算符 :: 显式指定枚举类型 (推荐,更清晰)
6
// myColor = 0; // 隐式转换为枚举类型,但不推荐,可读性差
7
// myColor = 100; // 错误!超出枚举类型的取值范围 (默认情况下)
枚举变量的初始化方式:
① 使用枚举常量: 将枚举变量赋值为枚举类型中定义的枚举常量。推荐使用 枚举类型名::枚举常量
的形式,更清晰易懂。
② 隐式类型转换 (不推荐): 可以将整数值隐式转换为枚举类型,但不推荐这样做,因为会降低代码的可读性和类型安全性。
③ 显式类型转换 (谨慎使用): 可以使用显式类型转换将整数值转换为枚举类型,但需要确保整数值在枚举类型的有效范围内。
要点:
① 枚举变量的声明使用 枚举名 变量名;
的形式。
② 枚举变量应该被赋值为枚举类型中定义的枚举常量。
③ 推荐使用 枚举类型名::枚举常量
的形式访问枚举常量,提高代码可读性。
④ 应尽量避免将任意整数值直接赋值给枚举变量,以保持类型安全和代码清晰。
7.3.3 枚举的取值范围和底层实现 (Value Range and Underlying Implementation of Enumerations)
枚举类型在底层实际上是以整数类型实现的。默认情况下,枚举类型的底层类型是 int
,但编译器可以根据枚举常量的取值范围选择更小的整数类型,例如 unsigned int
, short
, unsigned short
, char
, unsigned char
等,以节省内存。
可以使用 sizeof
运算符查看枚举类型的大小。
1
enum SmallEnum { A, B, C }; // 默认底层类型可能是 char 或 unsigned char
2
enum LargeEnum { VAL_0 = 0, VAL_MAX = 100000 }; // 底层类型可能是 int 或 unsigned int
3
4
std::cout << "sizeof(SmallEnum): " << sizeof(SmallEnum) << std::endl; // 输出可能是 1 或 4
5
std::cout << "sizeof(LargeEnum): " << sizeof(LargeEnum) << std::endl; // 输出可能是 4
指定枚举的底层类型 (C++11):
C++11 引入了可以显式指定枚举底层类型的语法,使用 enum class
定义作用域枚举 (scoped enumeration),或者使用 enum : 底层类型
定义传统枚举并指定底层类型。
1
enum class ScopedEnum : unsigned char { // 作用域枚举,底层类型为 unsigned char
2
VAL1,
3
VAL2
4
};
5
6
enum OldEnum : short { // 传统枚举,底层类型为 short
7
E1,
8
E2
9
};
10
11
std::cout << "sizeof(ScopedEnum): " << sizeof(ScopedEnum) << std::endl; // 输出 1
12
std::cout << "sizeof(OldEnum): " << sizeof(OldEnum) << std::endl; // 输出 2
枚举的取值范围:
枚举类型的取值范围取决于其底层类型。对于传统的 enum
,其取值范围包括所有枚举常量的值,以及底层类型能够表示的最小值到最大值之间的所有整数值。对于 enum class
(作用域枚举),其取值范围更严格,仅限于枚举常量的值。
要点:
① 枚举类型在底层以整数类型实现,默认底层类型是 int
。
② 编译器可能根据枚举常量的取值范围选择更小的整数类型作为底层类型,以节省内存。
③ 可以使用 sizeof
运算符查看枚举类型的大小。
④ C++11 允许显式指定枚举的底层类型,可以使用 enum class
定义作用域枚举,或使用 enum : 底层类型
定义传统枚举并指定底层类型。
⑤ 枚举类型的取值范围取决于其底层类型和枚举类型的定义方式 (传统 enum
或 enum class
)。
7.3.4 作用域枚举 (Scoped Enumerations) (C++11)
C++11 引入了作用域枚举 (scoped enumerations),也称为枚举类 (enum classes),使用 enum class
关键字定义。作用域枚举解决了传统枚举的一些问题,例如命名空间污染和隐式类型转换。
1
enum class ColorClass { // 作用域枚举 ColorClass
2
RED,
3
GREEN,
4
BLUE
5
};
6
7
enum LegacyColor { // 传统枚举 LegacyColor
8
RED,
9
YELLOW,
10
BLUE
11
};
12
13
// ColorClass::RED; // 正确,需要使用作用域解析运算符访问
14
// RED; // 错误!作用域枚举的枚举常量不在枚举类型的作用域之外可见
15
16
// LegacyColor::RED; // 正确,可以使用作用域解析运算符访问
17
// RED; // 正确,传统枚举的枚举常量在枚举类型的作用域之外也可见,可能导致命名冲突
18
19
ColorClass color1 = ColorClass::RED; // 正确,作用域枚举需要显式指定枚举类型
20
// ColorClass color2 = RED; // 错误!作用域枚举需要显式指定枚举类型
21
22
LegacyColor color3 = LegacyColor::RED; // 正确,传统枚举可以使用作用域解析运算符
23
LegacyColor color4 = RED; // 正确,传统枚举可以直接使用枚举常量名
24
25
// int redValue = RED; // 正确!传统枚举常量可以隐式转换为 int
26
// int redClassValue = ColorClass::RED; // 错误!作用域枚举常量不能隐式转换为 int,更类型安全
27
int redClassValue = static_cast<int>(ColorClass::RED); // 需要显式类型转换
28
29
// if (color1 == RED) {} // 错误!作用域枚举需要显式指定枚举类型
30
if (color1 == ColorClass::RED) {} // 正确,作用域枚举需要显式指定枚举类型
31
if (color3 == RED) {} // 正确,传统枚举可以直接与枚举常量比较
32
if (color3 == LegacyColor::RED) {} // 正确,传统枚举可以使用作用域解析运算符
作用域枚举的特点:
① 作用域限制: 作用域枚举的枚举常量仅在其枚举类型的作用域内可见,必须使用 枚举类型名::枚举常量
的形式访问。避免了命名空间污染,减少了命名冲突的可能性。
② 类型安全: 作用域枚举常量不能隐式转换为整数类型,需要显式类型转换 (例如 static_cast<int>()
)。增强了类型安全性,避免了意外的类型转换错误。
③ 前向声明: 作用域枚举可以进行前向声明,而传统枚举在某些情况下不能直接前向声明。
要点:
① 作用域枚举使用 enum class
关键字定义。
② 作用域枚举的枚举常量具有作用域限制,只能通过 枚举类型名::枚举常量
访问。
③ 作用域枚举常量不能隐式转换为整数类型,需要显式类型转换。
④ 作用域枚举提供了更好的类型安全性和命名空间管理,推荐在现代 C++ 代码中使用作用域枚举。
7.3.5 枚举类的使用场景和优势 (Use Cases and Advantages of Enumeration Classes)
使用场景:
① 状态表示: 枚举类非常适合表示对象或系统的状态,例如订单状态 (PENDING, PROCESSING, SHIPPED, DELIVERED), 文件打开模式 (READ, WRITE, APPEND), 错误代码等。
② 选项和标志: 枚举类可以用于表示一组互斥的选项或标志,例如对齐方式 (LEFT, RIGHT, CENTER), 字体样式 (BOLD, ITALIC, UNDERLINE)。
③ 类型安全的常量集合: 当需要一组具有特定含义的常量,并且希望保证类型安全和避免命名冲突时,枚举类是理想的选择。
优势:
① 提高代码可读性: 使用有意义的枚举常量名代替数字,使代码更易于理解和维护。例如,使用 OrderStatus::PROCESSING
比使用数字 1
更清晰。
② 增强类型安全: 作用域枚举的类型安全特性避免了意外的类型转换和比较错误,减少了程序 bug 的可能性。
③ 命名空间管理: 作用域枚举的命名空间限制避免了枚举常量与程序中其他标识符的命名冲突,提高了代码的健壮性。
④ 易于扩展和修改: 当需要添加或修改枚举常量时,枚举类型的定义集中在一个地方,易于维护和修改。
示例:使用枚举类表示订单状态
1
#include <iostream>
2
#include <string>
3
4
enum class OrderStatus {
5
PENDING, // 待处理
6
PROCESSING, // 处理中
7
SHIPPED, // 已发货
8
DELIVERED, // 已送达
9
CANCELLED // 已取消
10
};
11
12
std::string getOrderStatusString(OrderStatus status) {
13
switch (status) {
14
case OrderStatus::PENDING: return "待处理 (Pending)";
15
case OrderStatus::PROCESSING: return "处理中 (Processing)";
16
case OrderStatus::SHIPPED: return "已发货 (Shipped)";
17
case OrderStatus::DELIVERED: return "已送达 (Delivered)";
18
case OrderStatus::CANCELLED: return "已取消 (Cancelled)";
19
default: return "未知状态 (Unknown Status)";
20
}
21
}
22
23
int main() {
24
OrderStatus order1Status = OrderStatus::PROCESSING;
25
OrderStatus order2Status = OrderStatus::SHIPPED;
26
27
std::cout << "订单 1 状态 (Order 1 Status): " << getOrderStatusString(order1Status) << std::endl;
28
std::cout << "订单 2 状态 (Order 2 Status): " << getOrderStatusString(order2Status) << std::endl;
29
30
return 0;
31
}
在这个示例中,OrderStatus
枚举类清晰地表示了订单的各种状态,提高了代码的可读性和可维护性。使用枚举类可以使代码更具表达力,减少错误,并提高程序质量。
7.4 总结与比较:结构体、联合体和枚举的选择 (Summary and Comparison: Choosing between Structures, Unions, and Enumerations)
特性 (Feature) | 结构体 (Structure) | 联合体 (Union) | 枚举 (Enumeration) |
---|---|---|---|
关键字 (Keyword) | struct | union | enum 或 enum class |
成员 (Members) | 多个成员,各自占用独立的内存空间 (Independent memory) | 多个成员,共享同一块内存空间 (Shared memory) | 一组命名的整数常量 (Named integer constants) |
内存布局 (Memory Layout) | 成员按声明顺序排列,总大小为各成员大小之和 (Sum of member sizes) | 大小等于最大成员的大小 (Size of largest member) | 底层以整数类型实现 (Implemented as integers) |
主要用途 (Main Use) | 组织不同类型的数据成一个逻辑单元 (Group different types of data) | 节省内存,在同一内存位置存储不同类型的数据 (Memory saving) | 表示一组命名的常量,提高代码可读性和类型安全 (Named constants) |
初始化 (Initialization) | 可以初始化所有成员 (Initialize all members) | 通常只初始化第一个成员 (Initialize first member only) | 初始化为枚举常量或对应的整数值 (Initialize with enumerators) |
访问 (Access) | 使用 . 和 -> 运算符访问成员 (Using . and -> operators) | 使用 . 和 -> 运算符访问成员 (Using . and -> operators) | 枚举常量通过名称访问 (Access by name) |
类型安全 (Type Safety) | 相对类型安全 (Relatively type-safe) | 类型安全较弱,需手动管理类型 (Less type-safe, manual type management) | 枚举类 (scoped enum) 类型安全高 (High type safety for scoped enum) |
适用场景 (Use Cases) | 表示复杂的数据结构,例如记录、对象 (Complex data structures) | 节省内存,表示互斥类型的数据 (Mutually exclusive data types) | 表示状态、选项、标志等常量集合 (Constants for states, options, flags) |
如何选择:
⚝ 当需要将多个不同类型的数据组织成一个逻辑单元,且每个成员都需要独立存储和访问时,应使用结构体 (Structures)。结构体是构建复杂数据结构的基础。
⚝ 当需要在同一内存位置存储不同类型的数据,但同一时间只需要使用其中一种类型,并且希望节省内存空间时,可以使用联合体 (Unions)。联合体适用于内存敏感的场景和某些底层操作。但需要注意联合体的类型安全问题。
⚝ 当需要表示一组具有特定含义的命名常量,例如状态、选项、标志等,并且希望提高代码的可读性和类型安全时,应使用枚举 (Enumerations),特别是 作用域枚举 (Scoped Enumerations/enum classes)。枚举可以使代码更清晰、更易于维护。
理解结构体、联合体和枚举的特点和适用场景,可以帮助开发者更有效地组织和管理数据,编写出更清晰、更高效、更健壮的 C++ 程序。
8. 类和对象:面向对象编程基础
8.1 面向对象编程 (OOP) 概述
8.1.1 什么是面向对象编程 (What is Object-Oriented Programming)
面向对象编程 (Object-Oriented Programming, OOP) 是一种重要的编程范式,它以“对象 (Object)”作为程序的基本单元,将数据和操作数据的方法封装在一起,旨在提高软件开发的模块化、可维护性和可复用性。与面向过程编程 (Procedural Programming) 关注执行步骤不同,面向对象编程更侧重于模拟现实世界中的事物及其相互关系。
面向对象编程主要有四大核心概念,构成了其理论基础和实践方法:
① 封装 (Encapsulation):
▮▮▮▮封装是将数据 (属性) 和操作数据的代码 (方法) 捆绑到一个单元 (即“对象”) 中。在C++中,类 (Class) 是实现封装的主要机制。通过封装,可以隐藏对象的内部实现细节,仅对外暴露必要的接口,从而提高代码的安全性和可维护性。
② 继承 (Inheritance):
▮▮▮▮继承允许创建新的类 (子类/派生类),这些新类可以继承现有类 (父类/基类) 的属性和方法。继承实现了代码的重用,并建立了类之间的层次关系,是实现多态性的基础。
③ 多态 (Polymorphism):
▮▮▮▮多态意味着“多种形态”,指允许使用一个接口来表示不同的底层数据类型或对象。在C++中,多态性主要通过虚函数 (Virtual Function) 和函数重载 (Function Overloading) 等机制实现,使得程序在运行时能够根据对象的实际类型动态地调用相应的方法。
④ 抽象 (Abstraction):
▮▮▮▮抽象是指关注事物的本质特征,忽略其非本质的细节。在面向对象编程中,抽象是通过类和接口 (Interface) 来实现的。类可以抽象出一类事物的共同属性和行为,而接口则定义了一组操作规范,使得不同的类可以实现相同的接口,从而实现更高层次的抽象和解耦。
8.1.2 面向对象编程的优势 (Advantages of Object-Oriented Programming)
采用面向对象编程范式,可以带来诸多优势,使其在现代软件开发中占据重要地位:
① 模块化 (Modularity):
▮▮▮▮面向对象编程将程序分解为相互独立的对象,每个对象负责特定的功能。这种模块化的设计方法使得程序结构更清晰,易于理解和维护。
② 代码重用 (Code Reusability):
▮▮▮▮通过继承和组合 (Composition) 等机制,面向对象编程可以有效地重用已有的代码,减少重复开发工作,提高开发效率。
③ 可维护性 (Maintainability):
▮▮▮▮封装和抽象等特性使得对象内部的实现细节与外部隔离,当需要修改内部实现时,不会影响到程序的其他部分,从而提高了代码的可维护性。
④ 可扩展性 (Extensibility):
▮▮▮▮面向对象编程支持通过继承和多态等机制来扩展程序的功能。当需要添加新功能时,可以通过创建新的类或修改现有类来实现,而无需修改程序的整体结构。
⑤ 易于理解 (Ease of Understanding):
▮▮▮▮面向对象编程以现实世界中的事物为模型,使得程序的设计更符合人类的思维方式,易于理解和分析。
8.2 类 (Class) 的定义和声明
8.2.1 类的基本概念 (Basic Concepts of Class)
在C++中,类 (Class) 是面向对象编程的基础,是创建对象的“蓝图 (Blueprint)”或“模板 (Template)”。类定义了一组具有相同属性 (Attributes) 和 行为 (Behaviors) 的对象的抽象描述。
⚝ 属性 (Attributes):描述对象的状态,在类中通常表现为数据成员 (Data Members) 或 成员变量 (Member Variables)。例如,对于一个 Car (汽车)
类,其属性可能包括 color (颜色)
、brand (品牌)
、speed (速度)
等。
⚝ 行为 (Behaviors):描述对象可以执行的操作,在类中通常表现为成员函数 (Member Functions) 或 方法 (Methods)。例如,对于 Car (汽车)
类,其行为可能包括 start (启动)
、accelerate (加速)
、brake (刹车)
等。
类本身并不是数据,而是一种类型定义 (Type Definition)。只有当根据类创建对象时,才会分配实际的内存空间来存储对象的数据。
8.2.2 类的声明 (Class Declaration)
在C++中,使用关键字 class
来声明一个类。类声明通常放在头文件 (.h
或 .hpp
) 中,以便在多个源文件之间共享类定义。
类声明的基本语法结构如下:
1
class ClassName {
2
public: // 公有访问说明符
3
// 公有成员 (数据成员和成员函数)
4
5
private: // 私有访问说明符
6
// 私有成员 (数据成员和成员函数)
7
8
protected: // 保护访问说明符 (将在后续章节介绍)
9
// 保护成员 (数据成员和成员函数)
10
}; // 注意类声明后的分号
⚝ class ClassName
: class
关键字后跟类名 ClassName
,类名通常采用驼峰命名法 (CamelCase) 或 帕斯卡命名法 (PascalCase),首字母大写。
⚝ 访问说明符 (Access Specifiers): public
, private
, protected
是访问说明符,用于控制类成员的访问权限。
▮▮▮▮⚝ public (公有)
: 公有成员可以在类的内部和外部被访问。
▮▮▮▮⚝ private (私有)
: 私有成员只能在类的内部被访问,外部无法直接访问。
▮▮▮▮⚝ protected (保护)
: 保护成员可以在类的内部以及派生类 (子类) 中被访问,外部无法直接访问。(将在后续章节介绍继承时详细讲解)
⚝ 类体 { ... }
: 类体包含类的成员声明,包括数据成员和成员函数。
⚝ 分号 ;
: 类声明的结尾必须以分号 ;
结束,这是一个常见的语法细节,容易被初学者忽略。
示例:声明一个简单的 Rectangle (矩形)
类
1
// rectangle.h
2
#ifndef RECTANGLE_H
3
#define RECTANGLE_H
4
5
class Rectangle {
6
public:
7
// 公有成员
8
double length; // 长度 (公有数据成员)
9
double width; // 宽度 (公有数据成员)
10
11
double getArea(); // 获取面积 (公有成员函数)
12
double getPerimeter(); // 获取周长 (公有成员函数)
13
14
private:
15
// 私有成员 (本例中暂无)
16
};
17
18
#endif
在这个 Rectangle (矩形)
类的声明中:
⚝ public:
部分声明了两个公有数据成员 length (长度)
和 width (宽度)
,以及两个公有成员函数 getArea() (获取面积)
和 getPerimeter() (获取周长)
。
⚝ private:
部分在本例中为空,表示没有私有成员。
注意:在类声明中,通常只进行成员的声明 (Declaration),而成员函数的定义 (Definition) 可以放在类声明之外的源文件 (.cpp
) 中,以实现声明与实现分离,提高代码的可读性和可维护性。
8.2.3 类的数据成员 (Data Members)
类的数据成员 (Data Members) 用于存储对象的状态信息,即对象的属性。数据成员可以是任何C++数据类型,包括基本数据类型 (如 int
, double
, char
)、指针、数组、甚至是其他类的对象。
在类声明中,数据成员的声明方式与普通变量的声明方式类似,但需要指定其访问权限 (通常是 public
, private
或 protected
)。
示例:Rectangle (矩形)
类的数据成员
在上面的 Rectangle (矩形)
类示例中,length (长度)
和 width (宽度)
就是数据成员,它们都是 double (双精度浮点型)
类型,用于存储矩形的长度和宽度。
1
class Rectangle {
2
public:
3
double length; // 长度 (公有数据成员)
4
double width; // 宽度 (公有数据成员)
5
// ...
6
};
访问数据成员:
⚝ 公有数据成员 (Public Data Members) 可以通过对象名直接访问,使用点运算符 .
。
⚝ 私有数据成员 (Private Data Members) 只能在类的内部被访问,外部无法直接访问。这是封装 (Encapsulation) 的重要体现,用于保护数据的安全性,防止外部随意修改对象的状态。
8.2.4 类的成员函数 (Member Functions)
类的成员函数 (Member Functions),也称为 方法 (Methods),用于定义对象的行为,即对象可以执行的操作。成员函数可以访问类的所有成员 (包括数据成员和函数成员),并且可以操作对象的状态。
在类声明中,成员函数的声明方式与普通函数的声明方式类似,但需要指定其访问权限,并且函数体可以定义在类声明内部 (内联函数,Inline Function) 或外部。
示例:Rectangle (矩形)
类的成员函数
在 Rectangle (矩形)
类示例中,getArea() (获取面积)
和 getPerimeter() (获取周长)
就是成员函数,它们分别用于计算矩形的面积和周长。
1
class Rectangle {
2
public:
3
double length;
4
double width;
5
6
double getArea(); // 获取面积 (公有成员函数声明)
7
double getPerimeter(); // 获取周长 (公有成员函数声明)
8
// ...
9
};
10
11
// 成员函数在类外部定义,需要使用作用域解析运算符 `::`
12
double Rectangle::getArea() {
13
return length * width;
14
}
15
16
double Rectangle::getPerimeter() {
17
return 2 * (length + width);
18
}
成员函数定义在类外部:
⚝ 当成员函数在类声明外部定义时,需要使用作用域解析运算符 ::
(Scope Resolution Operator) 来指明该函数属于哪个类。
⚝ 语法形式为:返回值类型 ClassName::FunctionName(参数列表) { 函数体 }
。
成员函数定义在类内部 (内联函数):
⚝ 当成员函数的函数体较短时,可以将其定义在类声明内部,此时编译器通常会将其作为内联函数 (Inline Function) 处理,以提高程序的执行效率。
⚝ 语法形式为:
1
class ClassName {
2
public:
3
返回值类型 FunctionName(参数列表) {
4
// 函数体
5
return 返回值;
6
}
7
// ...
8
};
示例:Rectangle (矩形)
类的内联成员函数
1
class Rectangle {
2
public:
3
double length;
4
double width;
5
6
// 内联成员函数定义在类内部
7
double getArea() {
8
return length * width;
9
}
10
11
double getPerimeter() {
12
return 2 * (length + width);
13
}
14
// ...
15
};
访问成员函数:
⚝ 公有成员函数 (Public Member Functions) 可以通过对象名直接访问,使用点运算符 .
。
⚝ 私有成员函数 (Private Member Functions) 只能在类的内部被调用,外部无法直接访问。私有成员函数通常用于辅助实现类的内部功能,对外部隐藏实现细节。
8.3 对象 (Object) 的创建和使用
8.3.1 对象的创建 (Object Creation / Instantiation)
对象 (Object) 是类的实例 (Instance)。类是抽象的模板,而对象是根据这个模板创建的具体实体。创建对象的过程也称为实例化 (Instantiation)。
在C++中,创建对象的基本语法形式如下:
1
ClassName objectName; // 创建对象 (栈对象)
2
ClassName *objectPtr = new ClassName; // 创建对象 (堆对象)
⚝ 栈对象 (Stack Object):
▮▮▮▮⚝ ClassName objectName;
这种方式创建的对象分配在栈 (Stack) 内存中。
▮▮▮▮⚝ 栈对象的生命周期与其作用域 (Scope) 相同,当对象所在的作用域结束时,栈对象会自动被销毁,并释放内存。
⚝ 堆对象 (Heap Object):
▮▮▮▮⚝ ClassName *objectPtr = new ClassName;
这种方式创建的对象分配在堆 (Heap) 内存中。
▮▮▮▮⚝ 堆对象的生命周期由程序员手动管理,需要使用 delete
运算符显式地销毁对象,并释放内存,否则可能导致内存泄漏 (Memory Leak)。
示例:创建 Rectangle (矩形)
对象
1
#include <iostream>
2
#include "rectangle.h" // 包含 Rectangle 类的声明
3
4
int main() {
5
// 创建栈对象
6
Rectangle rect1; // 调用默认构造函数 (如果存在)
7
8
// 设置栈对象的数据成员 (公有成员可以直接访问)
9
rect1.length = 10.0;
10
rect1.width = 5.0;
11
12
// 调用栈对象的成员函数
13
double area1 = rect1.getArea();
14
std::cout << "Rectangle 1 Area: " << area1 << std::endl; // 输出:Rectangle 1 Area: 50
15
16
// 创建堆对象
17
Rectangle *rect2Ptr = new Rectangle(); // 调用默认构造函数 (如果存在)
18
19
// 设置堆对象的数据成员 (通过指针访问公有成员)
20
rect2Ptr->length = 20.0;
21
rect2Ptr->width = 8.0;
22
23
// 调用堆对象的成员函数 (通过指针访问成员)
24
double area2 = rect2Ptr->getArea();
25
std::cout << "Rectangle 2 Area: " << area2 << std::endl; // 输出:Rectangle 2 Area: 160
26
27
// 销毁堆对象,释放内存 (重要!)
28
delete rect2Ptr;
29
rect2Ptr = nullptr; // 避免悬空指针
30
31
return 0;
32
}
注意:
⚝ 创建对象时,会调用类的构造函数 (Constructor) 进行初始化 (将在后续章节详细介绍)。
⚝ 对于堆对象,必须使用 delete
运算符手动释放内存,避免内存泄漏。良好的编程习惯是在 delete
之后将指针设置为 nullptr (空指针)
,以避免悬空指针 (Dangling Pointer) 的问题。
8.3.2 对象成员的访问 (Accessing Object Members)
创建对象后,可以使用点运算符 .
(Dot Operator) 或 箭头运算符 ->
(Arrow Operator) 来访问对象的公有成员 (数据成员和成员函数)。
⚝ 点运算符 .
: 用于访问栈对象或对象引用的成员。
▮▮▮▮⚝ 语法形式:objectName.memberName
⚝ 箭头运算符 ->
: 用于访问堆对象指针的成员。
▮▮▮▮⚝ 语法形式:objectPtr->memberName
▮▮▮▮⚝ objectPtr->memberName
等价于 (*objectPtr).memberName
,但箭头运算符更简洁直观。
示例:访问 Rectangle (矩形)
对象的成员
在上面的示例代码中,我们已经演示了如何使用点运算符 .
和箭头运算符 ->
来访问 Rectangle (矩形)
对象的公有数据成员 (length
, width
) 和成员函数 (getArea()
, getPerimeter()
)。
1
Rectangle rect1; // 栈对象
2
rect1.length = 10.0; // 使用点运算符访问数据成员
3
double area1 = rect1.getArea(); // 使用点运算符访问成员函数
4
5
Rectangle *rect2Ptr = new Rectangle(); // 堆对象
6
rect2Ptr->length = 20.0; // 使用箭头运算符访问数据成员
7
double area2 = rect2Ptr->getArea(); // 使用箭头运算符访问成员函数
访问权限控制:
⚝ 只能访问对象的公有成员 (Public Members)。
⚝ 无法直接访问对象的私有成员 (Private Members) 和 保护成员 (Protected Members) (在类外部)。如果需要访问或修改私有成员,通常需要通过公有的成员函数 (例如 getter (获取器)
和 setter (设置器)
方法) 来间接实现,这是封装 (Encapsulation) 的重要原则。
8.4 封装 (Encapsulation)
8.4.1 封装的概念 (Concept of Encapsulation)
封装 (Encapsulation) 是面向对象编程的核心概念之一。它指的是将数据 (属性) 和 操作数据的代码 (方法) 捆绑到一个单元 (即“对象”) 中,并对外部隐藏对象的内部实现细节,仅对外暴露必要的接口。
在C++中,类 (Class) 是实现封装的主要机制。通过使用访问说明符 (Access Specifiers) (public
, private
, protected
),可以控制类成员的访问权限,实现信息隐藏和数据保护。
⚝ 信息隐藏 (Information Hiding):封装的核心目标之一是实现信息隐藏,即隐藏对象的内部实现细节,只对外暴露必要的接口。这有助于降低程序的复杂性,提高代码的可维护性和安全性。
⚝ 数据保护 (Data Protection):通过将数据成员设置为 private (私有)
,可以防止外部代码直接访问和修改对象的数据,从而保护数据的完整性和一致性。外部代码只能通过公有的成员函数来间接访问和操作数据,这样可以对数据的访问进行控制和验证,确保数据的有效性。
8.4.2 访问说明符的作用 (Role of Access Specifiers)
C++提供了三种访问说明符,用于控制类成员的访问权限:
① public (公有)
:
▮▮▮▮公有成员可以在类的内部和外部被访问。公有成员构成了类的公共接口 (Public Interface),是对象与外部世界交互的通道。通常,将需要对外暴露的成员函数 (方法) 设置为 public
,以便外部代码可以调用对象的方法来操作对象。
② private (私有)
:
▮▮▮▮私有成员只能在类的内部被访问,外部无法直接访问。私有成员用于实现类的内部细节,对外隐藏。通常,将数据成员 (属性) 设置为 private
,以实现信息隐藏和数据保护。私有成员函数通常用于辅助实现类的内部功能,不对外暴露。
③ protected (保护)
:
▮▮▮▮保护成员可以在类的内部以及派生类 (子类) 中被访问,外部无法直接访问。保护成员主要用于继承 (Inheritance) 场景,将在后续章节详细介绍。在没有继承的情况下,protected
成员的行为与 private
成员类似。
默认访问权限:
⚝ 在类声明中,如果在访问说明符之前声明成员,则默认的访问权限是 private (私有)
。
示例:封装的 Rectangle (矩形)
类
1
// encapsulated_rectangle.h
2
#ifndef ENCAPSULATED_RECTANGLE_H
3
#define ENCAPSULATED_RECTANGLE_H
4
5
class EncapsulatedRectangle {
6
private: // 私有访问说明符
7
double m_length; // 私有数据成员 (长度)
8
double m_width; // 私有数据成员 (宽度)
9
10
public: // 公有访问说明符
11
// 构造函数 (将在后续章节介绍)
12
EncapsulatedRectangle(double length = 0.0, double width = 0.0);
13
14
// 公有成员函数 (getter 方法) 用于获取私有数据成员的值
15
double getLength() const;
16
double getWidth() const;
17
18
// 公有成员函数 (setter 方法) 用于设置私有数据成员的值 (可以进行数据验证)
19
void setLength(double length);
20
void setWidth(double width);
21
22
// 公有成员函数 (计算面积和周长)
23
double getArea() const;
24
double getPerimeter() const;
25
};
26
27
#endif
1
// encapsulated_rectangle.cpp
2
#include "encapsulated_rectangle.h"
3
4
// 构造函数定义
5
EncapsulatedRectangle::EncapsulatedRectangle(double length, double width)
6
: m_length(length), m_width(width) {
7
// 初始化列表 (更高效的初始化方式,将在后续章节介绍)
8
}
9
10
// getter 方法定义
11
double EncapsulatedRectangle::getLength() const {
12
return m_length;
13
}
14
15
double EncapsulatedRectangle::getWidth() const {
16
return m_width;
17
}
18
19
// setter 方法定义 (可以添加数据验证)
20
void EncapsulatedRectangle::setLength(double length) {
21
if (length >= 0) {
22
m_length = length;
23
} else {
24
// 可以抛出异常或进行其他错误处理 (将在后续章节介绍异常处理)
25
std::cerr << "Error: Invalid length value." << std::endl;
26
}
27
}
28
29
void EncapsulatedRectangle::setWidth(double width) {
30
if (width >= 0) {
31
m_width = width;
32
} else {
33
std::cerr << "Error: Invalid width value." << std::endl;
34
}
35
}
36
37
// 计算面积
38
double EncapsulatedRectangle::getArea() const {
39
return m_length * m_width;
40
}
41
42
// 计算周长
43
double EncapsulatedRectangle::getPerimeter() const {
44
return 2 * (m_length + m_width);
45
}
在这个封装的 EncapsulatedRectangle (矩形)
类中:
⚝ 私有数据成员 m_length
和 m_width
: 矩形的长度和宽度被设置为 private (私有)
,外部代码无法直接访问和修改。
⚝ 公有成员函数 (getter/setter 方法): 提供了公有的 getLength()
, getWidth()
, setLength()
, setWidth()
方法 (也称为 访问器 (Accessor) 和 修改器 (Mutator) 方法) 来间接访问和操作私有数据成员。setter
方法中可以添加数据验证逻辑,确保数据的有效性。
⚝ 公有成员函数 getArea()
和 getPerimeter()
: 提供公有的方法来计算矩形的面积和周长。
使用封装的 EncapsulatedRectangle (矩形)
类:
1
#include <iostream>
2
#include "encapsulated_rectangle.h"
3
4
int main() {
5
EncapsulatedRectangle rect(10.0, 5.0); // 使用构造函数初始化对象
6
7
// 无法直接访问私有数据成员
8
// rect.m_length = 20.0; // 错误: 'EncapsulatedRectangle::m_length' is private
9
10
// 通过公有的 getter 方法获取数据成员的值
11
std::cout << "Length: " << rect.getLength() << std::endl; // 输出:Length: 10
12
std::cout << "Width: " << rect.getWidth() << std::endl; // 输出:Width: 5
13
14
// 通过公有的 setter 方法设置数据成员的值 (可以进行数据验证)
15
rect.setLength(20.0);
16
rect.setWidth(8.0);
17
rect.setWidth(-2.0); // 设置无效的宽度值,setter 方法会输出错误信息
18
19
// 重新获取数据成员的值
20
std::cout << "New Length: " << rect.getLength() << std::endl; // 输出:New Length: 20
21
std::cout << "New Width: " << rect.getWidth() << std::endl; // 输出:New Width: 8
22
23
// 调用公有的成员函数计算面积和周长
24
std::cout << "Area: " << rect.getArea() << std::endl; // 输出:Area: 160
25
std::cout << "Perimeter: " << rect.getPerimeter() << std::endl; // 输出:Perimeter: 56
26
27
return 0;
28
}
通过封装,EncapsulatedRectangle (矩形)
类的内部实现细节被隐藏起来,外部代码只能通过公有的接口 (getter/setter 方法和功能方法) 来与对象交互,提高了代码的安全性 (Safety)、可维护性 (Maintainability) 和 灵活性 (Flexibility)。
8.4.3 封装的优势 (Advantages of Encapsulation)
封装作为面向对象编程的重要特性,带来了诸多优势:
① 信息隐藏 (Information Hiding):
▮▮▮▮封装隐藏了对象的内部实现细节,只对外暴露必要的接口。这降低了程序的复杂性,使得类的使用者只需要关注如何使用对象提供的接口,而无需了解其内部实现。
② 数据保护 (Data Protection):
▮▮▮▮通过将数据成员设置为 private (私有)
,可以防止外部代码直接访问和修改对象的数据,保护数据的完整性和一致性。通过公有的 setter
方法,可以对数据的设置进行验证和控制,确保数据的有效性。
③ 模块化 (Modularity):
▮▮▮▮封装将数据和操作数据的代码组织成独立的模块 (对象),提高了代码的模块化程度。每个对象负责特定的功能,易于理解、测试和维护。
④ 代码复用 (Code Reusability):
▮▮▮▮封装后的类可以被其他程序或模块复用,提高了代码的复用率,减少了重复开发工作。
⑤ 灵活性 (Flexibility):
▮▮▮▮封装使得类的内部实现可以灵活地修改和演化,而不会影响到外部代码。只要类的公共接口保持不变,类的内部实现可以自由地进行调整和优化。
⑥ 可维护性 (Maintainability):
▮▮▮▮封装提高了代码的可维护性。当需要修改类的内部实现时,由于外部代码不直接依赖于内部实现细节,因此修改类内部代码对外部代码的影响较小,降低了维护成本。
8.5 构造函数 (Constructor)
8.5.1 构造函数的概念和作用 (Concept and Purpose of Constructor)
构造函数 (Constructor) 是一种特殊的成员函数,它在创建对象时自动被调用,用于初始化对象的状态 (即数据成员的值)。构造函数的名字必须与类名完全相同,并且没有返回值类型 (甚至 void
也不行)。
构造函数的主要作用是:
① 对象初始化 (Object Initialization):
▮▮▮▮构造函数负责在对象创建时,将对象的数据成员初始化为合理的初始值,确保对象在创建后处于一个有效的状态。
② 资源分配 (Resource Allocation):
▮▮▮▮构造函数可以在对象创建时分配对象所需的资源,例如动态内存分配、打开文件、建立网络连接等。这些资源需要在对象销毁时通过析构函数 (Destructor) 进行释放。
构造函数的特点:
⚝ 函数名与类名相同。
⚝ 没有返回值类型 (包括 void
)。
⚝ 在对象创建时自动调用。
⚝ 可以重载 (Overloading),即可以定义多个参数列表不同的构造函数。
⚝ 如果没有显式定义构造函数,编译器会提供一个默认构造函数 (Default Constructor)。
8.5.2 默认构造函数 (Default Constructor)
默认构造函数 (Default Constructor) 是指没有参数或所有参数都有默认值的构造函数。如果一个类没有显式定义任何构造函数,C++编译器会自动为该类生成一个默认构造函数。
编译器生成的默认构造函数的行为:
⚝ 无参数。
⚝ 函数体为空 (通常)。
⚝ 对类的数据成员进行默认初始化:
▮▮▮▮⚝ 对于内置类型 (Built-in Types) (如 int
, double
, 指针
等) 的数据成员,不进行初始化,其值是不确定的 (取决于内存中的原始值)。
▮▮▮▮⚝ 对于类类型 (Class Types) 的数据成员,会调用其默认构造函数进行初始化 (如果存在)。
示例:没有显式构造函数的类
1
class NoConstructorClass {
2
public:
3
int value; // 内置类型数据成员,不会被默认构造函数初始化
4
// 没有显式定义构造函数,编译器会提供默认构造函数
5
};
6
7
int main() {
8
NoConstructorClass obj; // 调用编译器提供的默认构造函数
9
10
std::cout << "Value: " << obj.value << std::endl; // 输出的值是不确定的
11
return 0;
12
}
在这个例子中,NoConstructorClass
类没有显式定义构造函数,编译器提供了默认构造函数。但是默认构造函数不会初始化 value
数据成员,所以 obj.value
的值是不确定的。
何时需要显式定义构造函数:
⚝ 当需要对数据成员进行自定义初始化时 (例如,初始化为特定的初始值)。
⚝ 当需要在对象创建时执行某些操作时 (例如,资源分配、输出提示信息等)。
⚝ 当类的数据成员包含引用 (Reference) 或 const
成员时,必须在构造函数的初始化列表中进行初始化。
8.5.3 参数化构造函数 (Parameterized Constructor)
参数化构造函数 (Parameterized Constructor) 是指带有参数的构造函数。通过参数化构造函数,可以在创建对象时传递初始值,对对象的数据成员进行初始化。
示例:带有参数的 Rectangle (矩形)
构造函数
1
class Rectangle {
2
private:
3
double m_length;
4
double m_width;
5
6
public:
7
// 参数化构造函数,接受长度和宽度作为参数
8
Rectangle(double length, double width);
9
10
double getArea() const;
11
double getPerimeter() const;
12
// ...
13
};
14
15
// 构造函数在类外部定义
16
Rectangle::Rectangle(double length, double width) : m_length(length), m_width(width) {
17
// 构造函数体 (可以为空,初始化工作在初始化列表中完成)
18
std::cout << "Rectangle constructor called." << std::endl; // 输出提示信息
19
}
20
21
// ... (getArea() 和 getPerimeter() 函数的定义与之前相同)
构造函数的初始化列表 (Initializer List):
⚝ 在构造函数的定义中,可以使用初始化列表来初始化数据成员。
⚝ 初始化列表位于构造函数的参数列表之后,函数体 {}
之前,以冒号 :
开头,多个初始化项之间用逗号 ,
分隔。
⚝ 语法形式: ClassName::ClassName(参数列表) : 成员1(初始值1), 成员2(初始值2), ... { 函数体 }
⚝ 优点:
▮▮▮▮⚝ 效率更高:对于某些类型的数据成员 (尤其是类类型成员),使用初始化列表进行初始化比在构造函数体内部赋值效率更高。
▮▮▮▮⚝ 必须使用:对于 const
成员和引用类型成员,必须在初始化列表中进行初始化,因为 const
成员和引用类型成员在创建后就不能再被赋值。
使用参数化构造函数创建对象:
1
int main() {
2
// 使用参数化构造函数创建对象,并传递初始值
3
Rectangle rect1(10.0, 5.0); // 调用 Rectangle(double, double) 构造函数
4
std::cout << "Rectangle 1 Area: " << rect1.getArea() << std::endl;
5
6
Rectangle rect2(20.0, 8.0); // 调用 Rectangle(double, double) 构造函数
7
std::cout << "Rectangle 2 Perimeter: " << rect2.getPerimeter() << std::endl;
8
9
// Rectangle rect3(); // 错误! 这不是创建对象,而是声明一个返回 Rectangle 对象的函数
10
Rectangle rect3{}; // C++11 列表初始化,调用默认构造函数 (如果存在,本例中不存在默认构造函数,会报错)
11
12
return 0;
13
}
注意:
⚝ 如果类中定义了参数化构造函数,但没有定义默认构造函数,则无法使用不带参数的方式创建对象 (例如 Rectangle rect;
会报错)。
⚝ 如果需要支持不带参数创建对象,需要显式定义一个默认构造函数 (即使函数体为空)。
8.5.4 构造函数重载 (Constructor Overloading)
构造函数可以重载 (Overloading),即在一个类中可以定义多个参数列表不同的构造函数。构造函数重载使得可以在创建对象时,根据不同的需求选择不同的构造函数进行初始化,提供更灵活的对象创建方式。
示例:Rectangle (矩形)
类的构造函数重载
1
class Rectangle {
2
private:
3
double m_length;
4
double m_width;
5
6
public:
7
// 默认构造函数 (无参数)
8
Rectangle();
9
10
// 参数化构造函数 (接受长度和宽度)
11
Rectangle(double length, double width);
12
13
// 拷贝构造函数 (将在后续章节介绍)
14
Rectangle(const Rectangle& other);
15
16
double getArea() const;
17
double getPerimeter() const;
18
// ...
19
};
20
21
// 默认构造函数定义
22
Rectangle::Rectangle() : m_length(0.0), m_width(0.0) {
23
std::cout << "Default constructor called." << std::endl;
24
}
25
26
// 参数化构造函数定义 (与之前相同)
27
Rectangle::Rectangle(double length, double width) : m_length(length), m_width(width) {
28
std::cout << "Parameterized constructor called." << std::endl;
29
}
30
31
// 拷贝构造函数定义 (暂为空实现,将在后续章节介绍)
32
Rectangle::Rectangle(const Rectangle& other) : m_length(other.m_length), m_width(other.m_width) {
33
std::cout << "Copy constructor called." << std::endl;
34
}
35
36
// ... (getArea() 和 getPerimeter() 函数的定义与之前相同)
使用重载的构造函数创建对象:
1
int main() {
2
Rectangle rect1; // 调用默认构造函数
3
std::cout << "Rectangle 1 Area: " << rect1.getArea() << std::endl; // 输出 Area: 0
4
5
Rectangle rect2(10.0, 5.0); // 调用参数化构造函数 Rectangle(double, double)
6
std::cout << "Rectangle 2 Perimeter: " << rect2.getPerimeter() << std::endl;
7
8
Rectangle rect3 = rect2; // 调用拷贝构造函数 (将在后续章节详细介绍)
9
std::cout << "Rectangle 3 Area: " << rect3.getArea() << std::endl;
10
11
return 0;
12
}
编译器会根据创建对象时提供的参数类型和数量,自动选择匹配的构造函数进行调用,这就是构造函数重载的实现机制。
8.6 析构函数 (Destructor)
8.6.1 析构函数的概念和作用 (Concept and Purpose of Destructor)
析构函数 (Destructor) 也是一种特殊的成员函数,它在对象生命周期结束时自动被调用,用于执行清理 (Cleanup) 操作,释放对象占用的资源。析构函数的名字是在类名前面加上 波浪线 ~
(tilde),并且没有参数和返回值类型 (甚至 void
也不行)。
析构函数的主要作用是:
① 资源释放 (Resource Deallocation):
▮▮▮▮析构函数负责在对象销毁时释放对象占用的资源,例如动态内存释放 (delete
)、关闭文件、断开网络连接等。这些资源通常是在构造函数中分配的。
② 清理操作 (Cleanup Operations):
▮▮▮▮析构函数可以执行一些其他的清理操作,例如保存数据、记录日志、释放锁等,确保对象在销毁前完成必要的清理工作。
析构函数的特点:
⚝ 函数名是在类名前面加上波浪线 ~
。
⚝ 没有参数。
⚝ 没有返回值类型 (包括 void
)。
⚝ 在对象生命周期结束时自动调用。
⚝ 一个类只能有一个析构函数 (不能重载)。
⚝ 如果没有显式定义析构函数,编译器会提供一个默认析构函数 (Default Destructor)。
8.6.2 默认析构函数 (Default Destructor)
默认析构函数 (Default Destructor) 是指编译器自动生成的析构函数。如果一个类没有显式定义析构函数,C++编译器会自动为该类生成一个默认析构函数。
编译器生成的默认析构函数的行为:
⚝ 无参数。
⚝ 函数体为空 (通常)。
⚝ 负责调用类的数据成员的析构函数 (如果数据成员是类类型的对象)。
⚝ 对于内置类型的数据成员,不执行任何操作。
示例:没有显式析构函数的类
1
class NoDestructorClass {
2
public:
3
NoDestructorClass() {
4
std::cout << "Constructor called." << std::endl;
5
}
6
// 没有显式定义析构函数,编译器会提供默认析构函数
7
};
8
9
int main() {
10
NoDestructorClass obj; // 创建栈对象,构造函数被调用
11
// 对象生命周期结束,默认析构函数被自动调用 (但默认析构函数没有实际操作)
12
std::cout << "Object life cycle ends." << std::endl;
13
return 0;
14
} // 栈对象 obj 在 main 函数结束时销毁,析构函数被调用
在这个例子中,NoDestructorClass
类没有显式定义析构函数,编译器提供了默认析构函数。默认析构函数在对象 obj
生命周期结束时被自动调用,但它没有执行任何实际的清理操作。
何时需要显式定义析构函数:
⚝ 当需要在对象销毁时释放对象在构造函数中分配的资源时 (例如,动态内存释放、关闭文件等)。
⚝ 当需要在对象销毁时执行某些清理操作时 (例如,保存数据、记录日志等)。
8.6.3 析构函数的调用时机 (Timing of Destructor Call)
析构函数在以下情况下会被自动调用:
① 栈对象 (Stack Object):
▮▮▮▮当栈对象的生命周期结束时,析构函数会被自动调用。栈对象的生命周期通常由其作用域 (Scope) 决定,当对象所在的作用域结束时 (例如,函数执行结束、代码块执行结束),栈对象会被自动销毁,析构函数被调用。
② 堆对象 (Heap Object):
▮▮▮▮当使用 delete
运算符显式销毁堆对象时,析构函数会被调用。对于使用 new
运算符在堆内存中创建的对象,需要使用 delete
运算符手动释放内存,delete
运算符会先调用对象的析构函数,然后再释放对象占用的内存空间。
③ 临时对象 (Temporary Object):
▮▮▮▮当临时对象的生命周期结束时,析构函数会被自动调用。临时对象通常是在表达式求值过程中产生的,例如函数返回值、隐式类型转换等。临时对象的生命周期通常很短,在表达式求值结束后立即销毁。
④ 程序结束:
▮▮▮▮当程序正常结束时,所有全局对象 (Global Objects) 和 静态对象 (Static Objects) 的析构函数会被按照构造顺序的相反顺序依次调用。
示例:析构函数的调用时机
1
#include <iostream>
2
3
class MyClass {
4
public:
5
MyClass(int id) : m_id(id) {
6
std::cout << "Constructor for object " << m_id << " called." << std::endl;
7
}
8
~MyClass() {
9
std::cout << "Destructor for object " << m_id << " called." << std::endl;
10
}
11
12
private:
13
int m_id;
14
};
15
16
void myFunction() {
17
MyClass localObj(2); // 创建栈对象 localObj,构造函数被调用
18
std::cout << "Inside myFunction." << std::endl;
19
} // myFunction 函数结束,栈对象 localObj 生命周期结束,析构函数被调用
20
21
int main() {
22
MyClass obj1(1); // 创建栈对象 obj1,构造函数被调用
23
myFunction(); // 调用 myFunction 函数
24
25
MyClass *heapObjPtr = new MyClass(3); // 创建堆对象,构造函数被调用
26
delete heapObjPtr; // 显式销毁堆对象,析构函数被调用
27
heapObjPtr = nullptr;
28
29
std::cout << "End of main function." << std::endl;
30
return 0;
31
} // main 函数结束,栈对象 obj1 生命周期结束,析构函数被调用
输出结果 (大致):
1
Constructor for object 1 called.
2
Constructor for object 2 called.
3
Inside myFunction.
4
Destructor for object 2 called.
5
Constructor for object 3 called.
6
Destructor for object 3 called.
7
End of main function.
8
Destructor for object 1 called.
从输出结果可以看出析构函数的调用时机:
⚝ 栈对象 localObj
在 myFunction()
函数结束时销毁。
⚝ 堆对象通过 delete
显式销毁。
⚝ 栈对象 obj1
在 main()
函数结束时销毁。
显式定义析构函数释放资源:
1
class ResourceClass {
2
private:
3
int *m_data; // 指向动态分配的内存
4
5
public:
6
ResourceClass(int size) {
7
m_data = new int[size]; // 在构造函数中动态分配内存
8
std::cout << "ResourceClass constructor: Memory allocated." << std::endl;
9
}
10
11
~ResourceClass() {
12
delete[] m_data; // 在析构函数中释放动态分配的内存
13
m_data = nullptr;
14
std::cout << "ResourceClass destructor: Memory freed." << std::endl;
15
}
16
};
17
18
int main() {
19
ResourceClass obj(100); // 创建对象,分配内存
20
21
// ... 使用对象 ...
22
23
return 0;
24
} // 对象生命周期结束,析构函数被调用,释放内存
在这个例子中,ResourceClass
类在构造函数中动态分配了内存,并在析构函数中释放了内存,确保了资源在对象销毁时得到正确释放,避免了内存泄漏。
9. 深入类与对象:高级特性
9.1 静态成员 (Static Members)
9.1.1 静态成员变量 (Static Member Variables)
▮ 介绍静态成员变量的定义、声明和初始化方式。
▮ 阐述静态成员变量与普通成员变量的区别:
▮▮▮▮ⓐ 存储位置:静态成员变量存储在全局数据区,而非类的每个对象的内存空间中。
▮▮▮▮ⓑ 生命周期:静态成员变量的生命周期贯穿整个程序运行期间。
▮▮▮▮ⓒ 访问方式:可以通过类名直接访问静态成员变量,也可以通过对象访问。
▮ 讲解静态成员变量的共享性:所有类的对象共享同一个静态成员变量。
▮ 探讨静态成员变量的应用场景,例如:
▮▮▮▮▮▮▮▮❶ 统计类的对象数量。
▮▮▮▮▮▮▮▮❷ 在类的所有对象之间共享数据。
▮▮▮▮▮▮▮▮❸ 实现单例模式 (Singleton Pattern) 的基础。
▮ 代码示例:演示静态成员变量的声明、初始化、访问和应用。
9.1.2 静态成员函数 (Static Member Functions)
▮ 介绍静态成员函数的定义和声明方式。
▮ 阐述静态成员函数与普通成员函数的区别:
▮▮▮▮ⓐ this
指针:静态成员函数没有 this
指针,不能访问非静态成员变量和非静态成员函数。
▮▮▮▮ⓑ 访问权限:静态成员函数可以访问类的静态成员变量和其他静态成员函数,以及类的外部全局变量和函数。
▮▮▮▮ⓒ 调用方式:可以通过类名直接调用静态成员函数,也可以通过对象调用。
▮ 讲解静态成员函数的作用:主要用于执行与类相关的、但不依赖于特定对象的操作。
▮ 探讨静态成员函数的应用场景,例如:
▮▮▮▮▮▮▮▮❶ 创建工具函数或辅助函数,封装在类中,提高代码组织性。
▮▮▮▮▮▮▮▮❷ 访问和操作静态成员变量。
▮▮▮▮▮▮▮▮❸ 实现工厂模式 (Factory Pattern) 的一部分。
▮ 代码示例:演示静态成员函数的声明、定义、调用和应用。
9.1.3 静态成员的初始化时机和顺序
▮ 强调静态成员变量的初始化必须在类外部进行,通常在源文件 (.cpp) 中。
▮ 解释静态成员变量的初始化时机:在程序启动时,main()
函数执行之前完成初始化。
▮ 简要提及多个静态成员变量的初始化顺序,通常按照它们在类中声明的顺序进行,但具体顺序依赖于编译器实现,不应过度依赖。
▮ 静态成员函数没有初始化问题,因为函数定义即初始化。
9.2 友元 (Friends)
9.2.1 友元函数 (Friend Functions)
▮ 介绍友元函数的概念:友元函数是定义在类外部的普通函数,但被声明为类的友元后,可以访问该类的 private
和 protected
成员。
▮ 讲解友元函数的声明方式:在类定义中使用 friend
关键字声明友元函数。
▮ 阐述友元函数与成员函数的区别:
▮▮▮▮ⓐ 所属关系:友元函数不属于类,是定义在类外部的普通函数。
▮▮▮▮ⓑ this
指针:友元函数没有 this
指针。
▮▮▮▮ⓒ 访问权限:友元函数可以访问类的所有成员(包括 private
和 protected
),突破了类的封装性限制。
▮ 探讨友元函数的应用场景,例如:
▮▮▮▮▮▮▮▮❶ 运算符重载:某些运算符重载(如输入/输出运算符 <<
和 >>
)需要访问类的私有成员,通常使用友元函数实现。
▮▮▮▮▮▮▮▮❷ 跨类的数据共享:当多个类需要频繁访问彼此的私有成员时,可以声明友元关系,提高效率。
▮▮▮▮▮▮▮▮❸ 辅助函数:某些辅助函数可能需要访问类的内部细节,可以声明为友元函数。
▮ 强调友元关系的单向性:如果函数 f
是类 A
的友元,则 f
可以访问 A
的私有成员,但反之不成立。
▮ 强调友元关系的非传递性:如果函数 f
是类 A
的友元,类 B
是类 A
的友元,则 f
不是类 B
的友元。
▮ 讨论过度使用友元的弊端:破坏类的封装性,降低代码的可维护性,应谨慎使用。
▮ 代码示例:演示友元函数的声明、定义、调用和应用,例如重载输出运算符 <<
。
9.2.2 友元类 (Friend Classes)
▮ 介绍友元类的概念:如果一个类 B
被声明为类 A
的友元类,则类 B
的所有成员函数都可以访问类 A
的 private
和 protected
成员。
▮ 讲解友元类的声明方式:在类 A
的定义中使用 friend class B;
声明类 B
为友元类。
▮ 阐述友元类与普通类的关系:友元类是普通类,只是拥有访问其他类私有成员的特权。
▮ 探讨友元类的应用场景,例如:
▮▮▮▮▮▮▮▮❶ 管理器类 (Manager Class):一个管理器类需要管理多个相关类的对象,并需要访问这些对象的内部状态,可以将管理器类声明为这些类的友元类。
▮▮▮▮▮▮▮▮❷ 迭代器 (Iterator):迭代器类通常需要访问容器类的私有成员,以便遍历容器中的元素,可以将迭代器类声明为容器类的友元类。
▮▮▮▮▮▮▮▮❸ 测试类 (Test Class):在单元测试中,测试类可能需要访问被测试类的私有成员,以进行更全面的测试,可以将测试类声明为被测试类的友元类。
▮ 同样强调友元关系的单向性和非传递性,以及过度使用友元的弊端。
▮ 代码示例:演示友元类的声明、定义和应用,例如迭代器类作为容器类的友元类。
9.3 this
指针 (this
Pointer)
9.3.1 this
指针的基本概念
▮ 解释 this
指针的含义:this
是一个隐含的指针,它被传递给每一个非静态成员函数。
▮ 阐述 this
指针的类型:在成员函数中,this
指针的类型为 ClassName* const
(对于非 const
成员函数)或 const ClassName* const
(对于 const
成员函数),即指向当前对象的常量指针。
▮ 说明 this
指针指向:this
指针指向调用该成员函数的对象自身。
▮ 强调静态成员函数没有 this
指针。
9.3.2 this
指针的作用
▮ 讲解 this
指针的主要作用:
▮▮▮▮ⓐ 区分成员变量和局部变量:当成员函数的形式参数或局部变量与成员变量同名时,可以使用 this
指针显式访问成员变量。例如,this->variable = variable;
。
▮▮▮▮ⓑ 返回对象自身:在成员函数中,可以使用 *this
返回当前对象自身,实现链式操作 (Chaining)。例如,return *this;
。
▮▮▮▮ⓒ 在 const
成员函数中访问数据成员:即使在 const
成员函数中,也可以通过 this
指针访问和读取数据成员,但不能修改(除非数据成员被声明为 mutable
)。
▮ 代码示例:演示 this
指针在区分同名变量、返回对象自身和 const
成员函数中的应用。
9.3.3 this
指针的使用场景和注意事项
▮ 探讨 this
指针的常见使用场景:
▮▮▮▮▮▮▮▮❶ 在构造函数中初始化成员变量。
▮▮▮▮▮▮▮▮❷ 在赋值运算符重载函数中返回对象自身,实现链式赋值。
▮▮▮▮▮▮▮▮❸ 在需要返回对象自身的成员函数中。
▮▮▮▮▮▮▮▮❹ 在需要区分同名变量的成员函数中。
▮ 强调不要显式修改 this
指针的值,this
指针的值由编译器隐式传递和管理。
▮ 讨论在多线程环境中使用 this
指针的潜在风险,以及如何避免。
9.4 拷贝构造函数 (Copy Constructor)
9.4.1 拷贝构造函数的定义和作用
▮ 介绍拷贝构造函数的概念:拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为已存在对象的副本。
▮ 讲解拷贝构造函数的函数签名:拷贝构造函数的形参必须是对同类类型的常量引用,通常形式为 ClassName(const ClassName& other);
。
▮ 阐述拷贝构造函数的作用:
▮▮▮▮ⓐ 初始化新对象:当使用一个已存在对象初始化一个新对象时,拷贝构造函数会被自动调用。
▮▮▮▮ⓑ 函数参数按值传递:当对象作为函数参数按值传递时,拷贝构造函数会被调用,创建实参的副本。
▮▮▮▮ⓒ 函数返回值按值返回:当函数返回值是对象类型并按值返回时,拷贝构造函数会被调用,创建返回值的副本(在某些情况下会被返回值优化 (RVO) 优化掉)。
9.4.2 默认拷贝构造函数 (Default Copy Constructor)
▮ 解释默认拷贝构造函数:如果类中没有显式定义拷贝构造函数,编译器会自动生成一个默认拷贝构造函数。
▮ 阐述默认拷贝构造函数的行为:默认拷贝构造函数执行浅拷贝 (Shallow Copy),即逐位复制 (Memberwise Copy) 对象的所有成员变量的值。
▮ 浅拷贝的潜在问题:
▮▮▮▮ⓐ 对于包含指针成员变量的类,浅拷贝会导致多个对象共享同一块动态分配的内存,当其中一个对象释放内存后,其他对象的指针将变为悬 dangling 指针 (Dangling Pointer),引发错误。
9.4.3 自定义拷贝构造函数 (User-Defined Copy Constructor) 和深拷贝 (Deep Copy)
▮ 讲解自定义拷贝构造函数的必要性:当类中包含指针成员变量,且需要进行深拷贝 (Deep Copy) 时,必须自定义拷贝构造函数。
▮ 阐述深拷贝的概念:深拷贝不仅复制对象的值,还复制对象所指向的动态分配的内存,使得每个对象都拥有独立的内存副本,避免浅拷贝带来的问题。
▮ 实现深拷贝的步骤:
▮▮▮▮ⓐ 在自定义拷贝构造函数中,为新对象的指针成员变量重新动态分配内存。
▮▮▮▮ⓑ 将被拷贝对象指针成员变量所指向的内容复制到新分配的内存中。
▮ 代码示例:演示默认拷贝构造函数的浅拷贝问题,以及自定义拷贝构造函数实现深拷贝的例子。
9.4.4 拷贝构造函数的调用时机
▮ 总结拷贝构造函数被调用的三种主要情况:
① 对象初始化:ClassName obj2 = obj1;
或 ClassName obj2(obj1);
② 函数参数按值传递:void func(ClassName obj);
调用 func(obj1);
③ 函数返回值按值返回:ClassName func();
ClassName obj3 = func();
▮ 强调理解拷贝构造函数的调用时机对于理解对象生命周期和内存管理的重要性。
9.4.5 禁用拷贝构造函数
▮ 介绍禁用拷贝构造函数的方法:将拷贝构造函数声明为 private
或 delete
(C++11)。
▮ 讨论禁用拷贝构造函数的应用场景:
① 单例模式:为了保证单例类的唯一实例,通常需要禁用拷贝构造函数和赋值运算符。
② 某些资源管理类:例如,管理独占资源的类,可能不希望被拷贝。
▮ 代码示例:演示如何禁用拷贝构造函数。
9.5 赋值运算符重载 (Assignment Operator Overloading)
9.5.1 赋值运算符重载的定义和作用
▮ 介绍赋值运算符重载的概念:赋值运算符重载允许自定义类的赋值行为,即当使用赋值运算符 =
将一个对象赋值给另一个对象时,可以执行自定义的操作。
▮ 讲解赋值运算符重载函数的函数签名:赋值运算符重载函数通常作为成员函数,函数名为 operator=
,形参通常是对同类类型的常量引用,返回值通常是对当前对象的引用,以便支持链式赋值,函数签名形式为 ClassName& operator=(const ClassName& other);
。
▮ 阐述赋值运算符重载的作用:
▮▮▮▮ⓐ 自定义对象赋值行为:可以根据类的特性,自定义赋值操作,例如实现深拷贝。
▮▮▮▮ⓑ 支持链式赋值:通过返回 *this
引用,可以实现链式赋值,如 obj1 = obj2 = obj3;
。
9.5.2 默认赋值运算符 (Default Assignment Operator)
▮ 解释默认赋值运算符:如果类中没有显式定义赋值运算符重载函数,编译器会自动生成一个默认赋值运算符。
▮ 阐述默认赋值运算符的行为:默认赋值运算符执行浅拷贝 (Shallow Copy),即逐位复制 (Memberwise Copy) 对象的所有成员变量的值,与默认拷贝构造函数行为类似。
▮ 浅拷贝的潜在问题:同样存在与默认拷贝构造函数相同的浅拷贝问题,特别是对于包含指针成员变量的类。
9.5.3 自定义赋值运算符重载函数和深拷贝
▮ 讲解自定义赋值运算符重载函数的必要性:当类中包含指针成员变量,且赋值操作需要进行深拷贝 (Deep Copy) 时,必须自定义赋值运算符重载函数。
▮ 实现深拷贝的步骤(与拷贝构造函数类似,但需要考虑自赋值 (Self-Assignment) 的情况):
▮▮▮▮ⓐ 自赋值检查 (Self-Assignment Check):首先检查是否为自赋值,即 if (this == &other) return *this;
,避免不必要的资源释放和拷贝。
▮▮▮▮ⓑ 释放旧资源:释放当前对象已有的动态分配的内存资源,避免内存泄漏 (Memory Leak)。
▮▮▮▮ⓒ 分配新资源:为当前对象的指针成员变量重新动态分配内存,大小与被赋值对象相同。
▮▮▮▮ⓓ 复制数据:将被赋值对象指针成员变量所指向的内容复制到新分配的内存中。
▮▮▮▮ⓔ 返回对象自身引用:return *this;
,支持链式赋值。
▮ 代码示例:演示默认赋值运算符的浅拷贝问题,以及自定义赋值运算符重载函数实现深拷贝和处理自赋值的例子。
9.5.4 拷贝构造函数与赋值运算符重载的区别和联系
▮ 总结拷贝构造函数和赋值运算符重载的区别:
▮▮▮▮ⓐ 作用不同:拷贝构造函数用于初始化新对象,赋值运算符重载用于修改已存在对象的值。
▮▮▮▮ⓑ 调用时机不同:拷贝构造函数在对象初始化时调用,赋值运算符在对象赋值时调用。
▮ 阐述拷贝构造函数和赋值运算符重载的联系:
▮▮▮▮ⓐ 对于需要深拷贝的类,拷贝构造函数和赋值运算符重载函数通常都需要实现深拷贝逻辑。
▮▮▮▮ⓑ 可以通过代码复用,例如在一个函数中实现深拷贝逻辑,然后在拷贝构造函数和赋值运算符重载函数中调用该函数,减少代码重复。
▮ 强调在设计类时,如果需要自定义拷贝构造函数或赋值运算符重载函数,通常两者都需要同时提供,以保证对象拷贝和赋值行为的一致性和正确性,满足拷贝控制 (Rule of Five/Rule of Zero) 原则 (C++11/17)。
9.5.5 禁用赋值运算符重载
▮ 介绍禁用赋值运算符重载的方法:将赋值运算符重载函数声明为 private
或 delete
(C++11)。
▮ 讨论禁用赋值运算符重载的应用场景:与禁用拷贝构造函数类似,例如单例模式、某些资源管理类等。
▮ 代码示例:演示如何禁用赋值运算符重载函数。
10. 继承与多态:面向对象的核心机制
10.1 继承 (Inheritance) 概述
10.1.1 继承的概念和意义 (Concept and Significance of Inheritance)
继承 (Inheritance) 是面向对象编程 (Object-Oriented Programming, OOP) 的一项核心特性,它允许我们创建一个新的类 (class),这个新类继承已存在类 (基类/父类/超类 (base class/parent class/superclass)) 的属性和行为。继承的主要目的是实现代码的重用 (code reusability),并建立类之间的层次关系,从而更好地组织和管理复杂的软件系统。
通过继承,派生类 (derived class) (也称为子类 (subclass)) 可以获得基类的所有非私有 (non-private) 成员 (成员变量 (member variables) 和成员函数 (member functions))。这意味着派生类不必从头开始定义所有相同的属性和行为,而是可以专注于添加新的特性或修改已有的特性,以满足特定的需求。
继承的意义主要体现在以下几个方面:
① 代码重用 (Code Reusability):继承允许派生类重用基类的代码,避免了代码的重复编写,提高了开发效率,并降低了维护成本。基类中已经实现的功能可以在多个派生类中共享,减少了代码冗余。
② 扩展性 (Extensibility):通过继承,我们可以在不修改原有基类代码的基础上,扩展类的功能。派生类可以添加新的成员变量和成员函数,以适应新的需求,而不会影响基类的原有功能。这种开放-封闭原则 (Open/Closed Principle) 是面向对象设计的重要原则之一。
③ 可维护性 (Maintainability):继承使得类之间的关系更加清晰和结构化。通过继承层次结构,我们可以更容易地理解和维护代码。当需要修改或添加功能时,可以更容易地定位到相关的类,并进行修改或扩展。
④ 多态性 (Polymorphism) 的基础:继承是实现多态性 (Polymorphism) 的重要基础。多态性允许我们使用基类指针或引用来操作派生类对象,从而实现更加灵活和通用的代码设计。多态性是面向对象编程的核心特性之一,将在本章后续章节详细讨论。
简而言之,继承是一种强大的代码组织和重用机制,它不仅提高了代码的效率和质量,也为构建可扩展、可维护的软件系统奠定了基础。
10.1.2 继承的类型 (Types of Inheritance)
C++ 支持多种类型的继承,根据派生类继承自多少个基类,以及继承的方式,可以分为不同的类型。主要的继承类型包括:
10.1.2.1 单继承 (Single Inheritance)
单继承 (Single Inheritance) 是指一个派生类只从一个基类继承。这是最简单和最常见的继承形式。在单继承中,派生类直接继承基类的特性,并可以扩展或修改这些特性。
例如,我们可以创建一个 Animal
类作为基类,然后派生出 Dog
类和 Cat
类。Dog
和 Cat
类都继承了 Animal
类的通用属性和行为,例如 name
(名字) 和 eat()
(吃) 函数,同时它们也可以有自己特有的属性和行为,例如 Dog
类的 bark()
(吠叫) 函数和 Cat
类的 meow()
(喵喵叫) 函数。
单继承的优点是结构简单,易于理解和实现。它适用于类之间存在清晰的 "is-a" (是一个) 关系的情况,例如 "Dog is an Animal" (狗是一种动物)。
10.1.2.2 多继承 (Multiple Inheritance)
多继承 (Multiple Inheritance) 是指一个派生类可以从多个基类继承。在多继承中,派生类将获得所有基类的特性,并可以将它们组合起来。
例如,我们可以创建 Flyable
(可飞行的) 类和 Mammal
(哺乳动物) 类,然后派生出 Bat
(蝙蝠) 类,它同时继承了 Flyable
类和 Mammal
类的特性。Bat
类既具有飞行能力,又具有哺乳动物的特征。
多继承提供了更强大的代码重用能力,但同时也带来了更高的复杂性。例如,当多个基类具有同名成员时,派生类需要明确指定要访问哪个基类的成员,这可能会导致命名冲突和歧义。此外,多继承还可能引发菱形继承 (Diamond Inheritance) 问题,需要通过虚继承 (Virtual Inheritance) 等机制来解决。
10.1.2.3 其他继承类型 (Other Inheritance Types)
除了单继承和多继承之外,根据继承的层次结构,还可以将继承分为以下类型,但这些类型更多的是描述继承的结构,而不是 C++ 语言层面直接区分的继承类型:
① 层次继承 (Hierarchical Inheritance):多个派生类继承自同一个基类。例如,Dog
类、 Cat
类和 Bird
类都继承自 Animal
类。
② 多层继承 (Multilevel Inheritance):一个派生类继承自另一个派生类,形成多层继承链。例如,Animal
类 -> Mammal
类 -> Dog
类。Dog
类间接继承了 Animal
类的特性。
③ 混合继承 (Hybrid Inheritance):是以上各种继承类型的组合。例如,一个类可能既参与多层继承,又参与多继承。
这些继承类型描述了类之间复杂的继承关系,在实际的软件设计中,可以根据需求灵活选择和组合使用。
10.1.3 继承的访问修饰符 (Access Modifiers in Inheritance)
在 C++ 中,继承方式通过访问修饰符 (access modifiers) 来控制,主要有三种访问修饰符用于继承:public
(公有的)、protected
(受保护的) 和 private
(私有的)。继承的访问修饰符决定了基类成员在派生类中的访问权限,以及派生类对象对基类成员的访问权限。
10.1.3.1 public
继承 (Public Inheritance)
public
继承是最常用的继承方式。当使用 public
继承时,基类的 public
成员和 protected
成员在派生类中保持原有的访问权限,而基类的 private
成员在派生类中仍然是不可访问的。
具体来说:
⚝ 基类的 public
成员在派生类中仍然是 public
,可以被派生类的成员函数和派生类对象在外部访问。
⚝ 基类的 protected
成员在派生类中仍然是 protected
,可以被派生类的成员函数访问,但不能被派生类对象在外部直接访问。
⚝ 基类的 private
成员在派生类中是不可访问的,即使在派生类的成员函数中也不能直接访问。
public
继承体现了 "is-a" 关系,派生类对象可以被视为基类对象的一种特殊类型。例如,如果 Dog
public
继承自 Animal
,那么 Dog
对象就可以被当作 Animal
对象来使用。
10.1.3.2 protected
继承 (Protected Inheritance)
当使用 protected
继承时,基类的 public
成员和 protected
成员在派生类中都变为 protected
成员,而基类的 private
成员在派生类中仍然是不可访问的。
具体来说:
⚝ 基类的 public
成员在派生类中变为 protected
,可以被派生类的成员函数访问,但不能被派生类对象在外部直接访问。
⚝ 基类的 protected
成员在派生类中仍然是 protected
,可以被派生类的成员函数访问,但不能被派生类对象在外部直接访问。
⚝ 基类的 private
成员在派生类中是不可访问的。
protected
继承通常用于实现 "is-implemented-in-terms-of" (用...来实现) 关系,派生类只是使用了基类的实现,但并不想对外暴露基类的接口。
10.1.3.3 private
继承 (Private Inheritance)
当使用 private
继承时,基类的 public
成员和 protected
成员在派生类中都变为 private
成员,而基类的 private
成员在派生类中仍然是不可访问的。
具体来说:
⚝ 基类的 public
成员在派生类中变为 private
,只能被派生类的成员函数访问,不能被派生类对象在外部访问,也不能被再派生的类的成员函数访问。
⚝ 基类的 protected
成员在派生类中变为 private
,只能被派生类的成员函数访问,不能被派生类对象在外部访问,也不能被再派生的类的成员函数访问。
⚝ 基类的 private
成员在派生类中是不可访问的。
private
继承也常用于实现 "is-implemented-in-terms-of" 关系,并且比 protected
继承更加严格地限制了基类接口的暴露。private
继承可以看作是 "has-a" (有一个) 关系的一种实现方式,即派生类 "has-a" 基类的实现,但并不对外宣称 "is-a" 基类。
总结:继承访问修饰符的影响
基类成员访问权限 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可访问 | 不可访问 | 不可访问 |
选择哪种继承方式取决于类之间的关系以及设计意图。public
继承是最常见的,用于表示 "is-a" 关系;protected
和 private
继承则更多用于实现代码重用和封装,表示 "is-implemented-in-terms-of" 或 "has-a" 关系。
10.2 单继承 (Single Inheritance) 详解
10.2.1 单继承的语法和基本用法 (Syntax and Basic Usage of Single Inheritance)
单继承 (Single Inheritance) 的语法非常简洁明了。在 C++ 中,声明一个派生类并指定其基类,只需在派生类名后使用冒号 :
,然后指定继承的访问修饰符和基类名。
语法格式:
1
class DerivedClassName : access-modifier BaseClassName {
2
// 派生类成员 (derived class members)
3
};
其中:
⚝ DerivedClassName
(派生类名) 是要定义的派生类的名称。
⚝ access-modifier
(访问修饰符) 是继承的访问修饰符,可以是 public
、protected
或 private
。
⚝ BaseClassName
(基类名) 是要继承的基类的名称。
示例:
1
#include <iostream>
2
#include <string>
3
4
// 基类 (Base class)
5
class Animal {
6
public:
7
Animal(std::string name) : name_(name) {}
8
void eat() {
9
std::cout << name_ << " is eating." << std::endl;
10
}
11
protected:
12
std::string name_; // 受保护的成员,派生类可以访问
13
};
14
15
// 派生类 (Derived class) - public 继承
16
class Dog : public Animal {
17
public:
18
Dog(std::string name, std::string breed) : Animal(name), breed_(breed) {}
19
void bark() {
20
std::cout << name_ << " the " << breed_ << " is barking: Woof!" << std::endl;
21
}
22
private:
23
std::string breed_;
24
};
25
26
int main() {
27
Dog dog("Buddy", "Golden Retriever");
28
dog.eat(); // 调用基类的成员函数 (Call base class member function)
29
dog.bark(); // 调用派生类的成员函数 (Call derived class member function)
30
// std::cout << dog.name_ << std::endl; // 错误:name_ 是 protected 成员,不能在类外部直接访问 (Error: name_ is protected, cannot access directly outside the class)
31
return 0;
32
}
代码解析:
① 定义了基类 Animal
,包含构造函数、eat()
函数和一个受保护的成员变量 name_
。
② 定义了派生类 Dog
,使用 public
继承自 Animal
。Dog
类有自己的构造函数、bark()
函数和一个私有成员变量 breed_
。
③ 在 main()
函数中,创建了一个 Dog
对象 dog
。
④ dog.eat()
调用了从基类 Animal
继承来的 eat()
函数。由于是 public
继承,Animal
类的 public
成员在 Dog
类中仍然是 public
。
⑤ dog.bark()
调用了 Dog
类自身定义的 bark()
函数。
⑥ 尝试在 main()
函数中直接访问 dog.name_
会报错,因为 name_
在 Animal
类中是 protected
成员,虽然派生类 Dog
可以访问,但在类外部 (包括 main()
函数) 不能直接访问。
这个示例展示了单继承的基本语法和用法。派生类 Dog
继承了基类 Animal
的 eat()
行为和 name_
属性,并扩展了 bark()
行为和 breed_
属性,体现了代码重用和扩展性。
10.2.2 派生类 (Derived Class) 的构造函数和析构函数 (Constructors and Destructors of Derived Class)
在继承关系中,构造函数 (constructor) 和析构函数 (destructor) 的调用顺序和规则比较特殊,需要特别注意。
构造函数的调用顺序:
当创建派生类对象时,构造函数的调用顺序是:
① 基类构造函数 (Base class constructor):首先调用基类的构造函数。如果有多个基类 (多继承),则按照继承列表中基类的顺序依次调用。
② 派生类成员对象构造函数 (Member object constructors of derived class):然后调用派生类中成员对象的构造函数 (如果派生类有成员对象)。按照成员对象在类中声明的顺序依次调用。
③ 派生类自身构造函数 (Derived class constructor itself):最后调用派生类自身的构造函数。
析构函数的调用顺序:
当销毁派生类对象时,析构函数的调用顺序与构造函数的调用顺序相反:
① 派生类自身析构函数 (Derived class destructor itself):首先调用派生类自身的析构函数。
② 派生类成员对象析构函数 (Member object destructors of derived class):然后调用派生类中成员对象的析构函数 (如果派生类有成员对象)。按照成员对象在类中声明顺序的逆序依次调用。
③ 基类析构函数 (Base class destructor):最后调用基类的析构函数。如果有多个基类 (多继承),则按照继承列表中基类的逆序依次调用。
派生类构造函数职责:
派生类的构造函数除了要初始化自身新增的成员变量外,还需要负责调用基类的构造函数,以初始化从基类继承来的成员变量。
显式调用基类构造函数:
在派生类的构造函数的初始化列表中,可以显式调用基类的构造函数。如果没有显式调用,编译器会默认调用基类的默认构造函数 (default constructor,即不带参数的构造函数)。如果基类没有默认构造函数,则派生类的构造函数必须显式调用基类的带参数的构造函数。
示例:
1
#include <iostream>
2
3
class Base {
4
public:
5
Base() { std::cout << "Base constructor called" << std::endl; }
6
Base(int value) : baseValue(value) { std::cout << "Base constructor with value " << baseValue << " called" << std::endl; }
7
~Base() { std::cout << "Base destructor called" << std::endl; }
8
protected:
9
int baseValue;
10
};
11
12
class Derived : public Base {
13
public:
14
Derived() : Base(10) { // 显式调用基类的带参数构造函数 (Explicitly call base class constructor with parameter)
15
std::cout << "Derived constructor called" << std::endl;
16
}
17
~Derived() { std::cout << "Derived destructor called" << std::endl; }
18
};
19
20
int main() {
21
Derived d;
22
return 0;
23
}
输出结果:
1
Base constructor with value 10 called
2
Derived constructor called
3
Derived destructor called
4
Base destructor called
代码解析:
① Derived
类的构造函数在初始化列表中使用了 Base(10)
,显式调用了 Base
类的带一个 int
参数的构造函数,并传递了参数 10
。
② 构造函数的调用顺序是:先基类 Base
的构造函数,后派生类 Derived
的构造函数。
③ 析构函数的调用顺序是:先派生类 Derived
的析构函数,后基类 Base
的析构函数。
注意:
⚝ 构造函数不能被继承。派生类需要定义自己的构造函数来完成自身的初始化,并负责调用基类的构造函数。
⚝ 析构函数可以被继承,但通常情况下,派生类需要定义自己的析构函数来释放派生类特有的资源,并在析构函数中隐式或显式地调用基类的析构函数 (编译器会自动处理基类析构函数的调用)。
⚝ 如果基类的析构函数不是虚函数 (virtual function),则通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,可能导致内存泄漏等问题。因此,当基类可能被用作多态类型时 (即基类指针可能指向派生类对象时),应该将基类的析构函数声明为虚函数,确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数 (虚析构函数将在后续章节详细讨论)。
10.2.3 派生类的成员访问 (Member Access in Derived Class)
在 public
继承中,派生类可以访问基类的 public
和 protected
成员。对于基类的 public
成员,派生类可以像访问自己的成员一样直接访问,也可以通过派生类对象在外部访问。对于基类的 protected
成员,派生类只能在派生类的成员函数中访问,不能通过派生类对象在外部直接访问。基类的 private
成员在派生类中始终是不可访问的。
示例:
1
#include <iostream>
2
#include <string>
3
4
class Base {
5
public:
6
int publicVar;
7
Base() : publicVar(1), protectedVar(2), privateVar(3) {}
8
void printBaseVars() {
9
std::cout << "Base publicVar: " << publicVar << std::endl;
10
std::cout << "Base protectedVar: " << protectedVar << std::endl;
11
std::cout << "Base privateVar: " << privateVar << std::endl; // 基类内部可以访问 private 成员 (Base class can access private members)
12
}
13
protected:
14
int protectedVar;
15
private:
16
int privateVar;
17
};
18
19
class Derived : public Base {
20
public:
21
void printDerivedVars() {
22
std::cout << "Derived publicVar (inherited): " << publicVar << std::endl; // 派生类可以访问 public 成员 (Derived class can access public members)
23
std::cout << "Derived protectedVar (inherited): " << protectedVar << std::endl; // 派生类可以访问 protected 成员 (Derived class can access protected members)
24
// std::cout << "Derived privateVar (inherited): " << privateVar << std::endl; // 错误:派生类不能访问 private 成员 (Error: Derived class cannot access private members)
25
}
26
};
27
28
int main() {
29
Derived d;
30
d.printBaseVars();
31
d.printDerivedVars();
32
std::cout << "Main publicVar (via Derived object): " << d.publicVar << std::endl; // 可以通过派生类对象访问 public 成员 (Can access public members via derived class object)
33
// std::cout << "Main protectedVar (via Derived object): " << d.protectedVar << std::endl; // 错误:不能通过派生类对象访问 protected 成员 (Error: Cannot access protected members via derived class object)
34
// std::cout << "Main privateVar (via Derived object): " << d.privateVar << std::endl; // 错误:不能通过派生类对象访问 private 成员 (Error: Cannot access private members via derived class object)
35
return 0;
36
}
输出结果:
1
Base publicVar: 1
2
Base protectedVar: 2
3
Base privateVar: 3
4
Derived publicVar (inherited): 1
5
Derived protectedVar (inherited): 2
6
Main publicVar (via Derived object): 1
代码解析:
① 基类 Base
定义了 public
、protected
和 private
三种访问权限的成员变量。
② 派生类 Derived
public
继承自 Base
。
③ 在 Derived
类的 printDerivedVars()
函数中,可以访问从 Base
类继承来的 publicVar
和 protectedVar
,但不能访问 privateVar
。
④ 在 main()
函数中,可以通过 Derived
类对象 d
访问 publicVar
,但不能访问 protectedVar
和 privateVar
。
总结:派生类成员访问权限
⚝ 派生类成员函数可以访问基类的 public
和 protected
成员。
⚝ 派生类成员函数不能访问基类的 private
成员。
⚝ 派生类对象在外部可以访问从基类继承来的 public
成员 (如果继承方式是 public
继承)。
⚝ 派生类对象在外部不能访问从基类继承来的 protected
和 private
成员。
10.2.4 单继承的应用场景和优势 (Application Scenarios and Advantages of Single Inheritance)
单继承作为最基本的继承形式,在面向对象设计中有着广泛的应用场景和显著的优势。
应用场景:
① 类别的细化和特化 (Specialization):当存在 "is-a" 关系时,可以使用单继承来创建更具体的类。例如,Car
(汽车)、 Truck
(卡车)、 Bus
(公交车) 都可以继承自 Vehicle
(交通工具) 基类,它们都是交通工具的特殊类型,并在此基础上添加各自特有的属性和行为。
② 代码的组织和模块化 (Organization and Modularization):单继承可以帮助组织代码,将通用的功能放在基类中,将特定的功能放在派生类中,使得代码结构更加清晰和模块化。例如,GUI (图形用户界面) 框架中,各种控件 (button, text box, window 等) 可以继承自一个通用的 Widget
基类。
③ 框架和库的扩展 (Framework and Library Extension):单继承是扩展框架和库功能的常用手段。用户可以通过继承框架或库提供的基类,创建自定义的类,并重用框架或库提供的通用功能,同时添加自己的特定功能。例如,很多测试框架允许用户通过继承特定的基类来定义测试用例。
优势:
① 简单易用 (Simplicity and Ease of Use):单继承语法简单,易于理解和使用。类之间的继承关系清晰明了,降低了代码的复杂性。
② 降低代码冗余 (Reduced Code Redundancy):单继承实现了代码的重用,避免了在多个类中重复编写相同的代码,提高了代码的效率和质量。
③ 易于维护和扩展 (Easy Maintenance and Extension):通过继承层次结构,可以更容易地定位和修改代码。当需要添加新功能或修改现有功能时,可以在不影响基类代码的情况下,通过派生类进行扩展或修改,提高了代码的可维护性和可扩展性。
④ 支持多态性 (Polymorphism Support):单继承是实现多态性的基础。通过结合虚函数,可以实现运行时多态性,提高代码的灵活性和通用性。
总结:
单继承是面向对象编程的重要基石,它通过代码重用和类层次结构,提高了代码的效率、质量、可维护性和可扩展性。在实际的软件开发中,合理地运用单继承,可以构建出结构清晰、功能完善的面向对象系统。
10.3 多继承 (Multiple Inheritance) 详解
10.3.1 多继承的语法和基本用法 (Syntax and Basic Usage of Multiple Inheritance)
多继承 (Multiple Inheritance) 允许一个派生类从多个基类继承特性。在 C++ 中,多继承的语法也很简单,只需要在派生类声明时,列出所有要继承的基类,并用逗号 ,
分隔。
语法格式:
1
class DerivedClassName : access-modifier1 BaseClassName1, access-modifier2 BaseClassName2, ... {
2
// 派生类成员 (derived class members)
3
};
其中:
⚝ DerivedClassName
(派生类名) 是要定义的派生类的名称。
⚝ access-modifier1
, access-modifier2
, ... (访问修饰符) 分别是继承各个基类的访问修饰符,可以是 public
、protected
或 private
。
⚝ BaseClassName1
, BaseClassName2
, ... (基类名) 分别是要继承的基类的名称。
示例:
1
#include <iostream>
2
3
// 基类 1 (Base class 1)
4
class Flyer {
5
public:
6
void fly() {
7
std::cout << "I can fly!" << std::endl;
8
}
9
};
10
11
// 基类 2 (Base class 2)
12
class Swimmer {
13
public:
14
void swim() {
15
std::cout << "I can swim!" << std::endl;
16
}
17
};
18
19
// 派生类 (Derived class) - 多继承 (multiple inheritance)
20
class Duck : public Flyer, public Swimmer {
21
public:
22
void quack() {
23
std::cout << "Quack!" << std::endl;
24
}
25
};
26
27
int main() {
28
Duck duck;
29
duck.fly(); // 调用 Flyer 类的成员函数 (Call Flyer class member function)
30
duck.swim(); // 调用 Swimmer 类的成员函数 (Call Swimmer class member function)
31
duck.quack(); // 调用 Duck 类自身的成员函数 (Call Duck class own member function)
32
return 0;
33
}
代码解析:
① 定义了两个基类 Flyer
和 Swimmer
,分别具有 fly()
和 swim()
函数。
② 定义了派生类 Duck
,使用 public
继承自 Flyer
和 Swimmer
。Duck
类有自己的 quack()
函数。
③ 在 main()
函数中,创建了一个 Duck
对象 duck
。
④ duck.fly()
和 duck.swim()
分别调用了从 Flyer
和 Swimmer
类继承来的 fly()
和 swim()
函数。
⑤ duck.quack()
调用了 Duck
类自身定义的 quack()
函数。
这个示例展示了多继承的基本语法和用法。派生类 Duck
同时拥有了 Flyer
类的飞行能力和 Swimmer
类的游泳能力,体现了多继承的代码重用能力。
10.3.2 多继承的构造函数和析构函数 (Constructors and Destructors in Multiple Inheritance)
多继承下的构造函数和析构函数的调用顺序与单继承类似,但需要考虑多个基类的情况。
构造函数的调用顺序:
当创建多继承的派生类对象时,构造函数的调用顺序是:
① 基类构造函数 (Base class constructors):按照派生类继承列表中基类的顺序依次调用基类的构造函数。
② 派生类成员对象构造函数 (Member object constructors of derived class):然后调用派生类中成员对象的构造函数 (如果派生类有成员对象)。按照成员对象在类中声明的顺序依次调用。
③ 派生类自身构造函数 (Derived class constructor itself):最后调用派生类自身的构造函数。
析构函数的调用顺序:
当销毁多继承的派生类对象时,析构函数的调用顺序与构造函数调用顺序相反:
① 派生类自身析构函数 (Derived class destructor itself):首先调用派生类自身的析构函数。
② 派生类成员对象析构函数 (Member object destructors of derived class):然后调用派生类中成员对象的析构函数 (如果派生类有成员对象)。按照成员对象在类中声明顺序的逆序依次调用。
③ 基类析构函数 (Base class destructors):按照派生类继承列表中基类的逆序依次调用基类的析构函数。
派生类构造函数职责:
多继承的派生类的构造函数需要负责调用所有基类的构造函数,以初始化从各个基类继承来的成员变量。在派生类的构造函数的初始化列表中,可以显式调用多个基类的构造函数,用逗号分隔。
示例:
1
#include <iostream>
2
3
class Base1 {
4
public:
5
Base1() { std::cout << "Base1 constructor called" << std::endl; }
6
~Base1() { std::cout << "Base1 destructor called" << std::endl; }
7
};
8
9
class Base2 {
10
public:
11
Base2() { std::cout << "Base2 constructor called" << std::endl; }
12
~Base2() { std::cout << "Base2 destructor called" << std::endl; }
13
};
14
15
class Derived : public Base1, public Base2 { // 多继承 (Multiple inheritance)
16
public:
17
Derived() : Base1(), Base2() { // 显式调用 Base1 和 Base2 的构造函数 (Explicitly call Base1 and Base2 constructors)
18
std::cout << "Derived constructor called" << std::endl;
19
}
20
~Derived() { std::cout << "Derived destructor called" << std::endl; }
21
};
22
23
int main() {
24
Derived d;
25
return 0;
26
}
输出结果:
1
Base1 constructor called
2
Base2 constructor called
3
Derived constructor called
4
Derived destructor called
5
Base2 destructor called
6
Base1 destructor called
代码解析:
① Derived
类多继承自 Base1
和 Base2
。
② Derived
类的构造函数在初始化列表中使用了 Base1()
和 Base2()
,显式调用了 Base1
和 Base2
的构造函数。
③ 构造函数的调用顺序是:先按照继承列表顺序调用基类构造函数 (Base1
, 然后 Base2
),最后调用派生类 Derived
的构造函数。
④ 析构函数的调用顺序是:先派生类 Derived
的析构函数,然后按照继承列表逆序调用基类析构函数 (Base2
, 然后 Base1
)。
注意:
⚝ 多继承的构造函数和析构函数的调用顺序需要严格按照继承列表的顺序和逆序。
⚝ 派生类构造函数必须负责调用所有直接基类的构造函数。
10.3.3 多继承的菱形继承问题 (Diamond Problem in Multiple Inheritance)
菱形继承 (Diamond Inheritance) 是多继承中一个经典且复杂的问题。当一个派生类从两个或更多个基类继承,而这些基类又直接或间接地继承自同一个更高级别的基类时,就会形成菱形继承结构。
菱形继承结构示意图:
1
A
2
/ / B C
3
\ /
4
\ /
5
D
在这个结构中:
⚝ 类 A
是最顶层的基类。
⚝ 类 B
和 类 C
都直接继承自类 A
。
⚝ 类 D
同时继承自类 B
和 类 C
。
菱形继承问题主要出现在以下两个方面:
① 数据冗余 (Data Redundancy):类 D
会从类 B
和 类 C
各自继承一份类 A
的成员变量,导致数据冗余,浪费内存空间。如果类 A
的成员变量被修改,可能导致数据不一致。
② 命名冲突和歧义 (Name Collision and Ambiguity):如果类 A
中有一个成员函数 foo()
,并且类 B
和 类 C
都没有重写 foo()
函数,那么类 D
继承自类 B
和 类 C
后,就有了两份 foo()
函数的继承路径。当类 D
的对象调用 foo()
函数时,编译器无法确定应该调用哪个版本的 foo()
函数,从而产生歧义。
示例:菱形继承导致的数据冗余和歧义
1
#include <iostream>
2
3
class A {
4
public:
5
A(int v) : value(v) {}
6
int value;
7
void printValue() {
8
std::cout << "Value in A: " << value << std::endl;
9
}
10
};
11
12
class B : public A { // 继承自 A (Inherit from A)
13
public:
14
B(int v) : A(v) {}
15
};
16
17
class C : public A { // 继承自 A (Inherit from A)
18
public:
19
C(int v) : A(v) {}
20
};
21
22
class D : public B, public C { // 多继承自 B 和 C (Multiple inherit from B and C)
23
public:
24
D(int v) : B(v), C(v) {} // B 和 C 的构造函数都调用了 A 的构造函数 (Constructors of B and C both call constructor of A)
25
void printDValue() {
26
// printValue(); // 错误:歧义,不知道调用 B::printValue 还是 C::printValue (Error: Ambiguous, unclear whether to call B::printValue or C::printValue)
27
B::printValue(); // 明确指定调用 B 的 printValue (Explicitly specify to call B's printValue)
28
C::printValue(); // 明确指定调用 C 的 printValue (Explicitly specify to call C's printValue)
29
std::cout << "Value in B's A: " << B::value << std::endl; // 访问 B 继承的 A 的 value (Access value of A inherited by B)
30
std::cout << "Value in C's A: " << C::value << std::endl; // 访问 C 继承的 A 的 value (Access value of A inherited by C)
31
}
32
};
33
34
int main() {
35
D d(10);
36
d.printDValue();
37
return 0;
38
}
输出结果:
1
Value in A: 10
2
Value in A: 10
3
Value in B's A: 10
4
Value in C's A: 10
代码解析:
① 类 D
多继承自 B
和 C
,而 B
和 C
又都继承自 A
,形成了菱形继承结构。
② D
的构造函数同时调用了 B(v)
和 C(v)
,而 B
和 C
的构造函数又都调用了 A(v)
。结果是类 A
的构造函数被调用了两次,D
对象中包含了两份 A
的成员变量 value
。
③ 在 D
类的 printDValue()
函数中,直接调用 printValue()
会产生歧义,因为编译器不知道应该调用 B::printValue()
还是 C::printValue()
。需要使用作用域解析运算符 ::
明确指定要调用的版本。
④ 可以通过 B::value
和 C::value
分别访问 D
对象中从 B
和 C
继承来的两份 value
成员变量,证明了数据冗余的存在。
菱形继承问题是多继承的一个主要缺点,需要谨慎处理。C++ 提供了虚继承 (Virtual Inheritance) 机制来解决菱形继承问题。
10.3.4 虚继承 (Virtual Inheritance) 解决菱形继承问题 (Virtual Inheritance to Solve Diamond Problem)
虚继承 (Virtual Inheritance) 是 C++ 为了解决菱形继承问题而引入的一种机制。当使用虚继承时,菱形继承结构中的派生类只会继承一份共同基类的成员。
虚继承的语法:
在派生类继承基类时,在继承访问修饰符前加上关键字 virtual
,即可声明为虚继承。
1
class B : virtual public A { ... }; // B 虚继承自 A (B virtually inherits from A)
2
class C : virtual public A { ... }; // C 虚继承自 A (C virtually inherits from A)
3
class D : public B, public C { ... }; // D 多继承自 B 和 C (D multiple inherits from B and C)
在上述代码中,B
和 C
都虚继承自 A
。这样,当 D
继承自 B
和 C
时,D
将只会继承一份 A
的成员。这种继承方式被称为虚继承。
修改菱形继承示例,使用虚继承解决数据冗余和歧义问题:
1
#include <iostream>
2
3
class A {
4
public:
5
A(int v) : value(v) { std::cout << "A constructor with value " << value << " called" << std::endl; }
6
int value;
7
void printValue() {
8
std::cout << "Value in A: " << value << std::endl;
9
}
10
};
11
12
class B : virtual public A { // 虚继承自 A (Virtually inherit from A)
13
public:
14
B(int v) : A(v) { std::cout << "B constructor with value " << v << " called" << std::endl; }
15
};
16
17
class C : virtual public A { // 虚继承自 A (Virtually inherit from A)
18
public:
19
C(int v) : A(v) { std::cout << "C constructor with value " << v << " called" << std::endl; }
20
};
21
22
class D : public B, public C { // 多继承自 B 和 C (Multiple inherit from B and C)
23
public:
24
D(int v) : A(v), B(v), C(v) { // D 的构造函数直接初始化 A (D's constructor directly initializes A)
25
std::cout << "D constructor with value " << v << " called" << std::endl;
26
}
27
void printDValue() {
28
printValue(); // 不再歧义,只会继承一份 A 的 printValue (No longer ambiguous, only one copy of A's printValue is inherited)
29
std::cout << "Value in D's A: " << value << std::endl; // 直接访问 value,不再需要 B::value 或 C::value (Directly access value, no need for B::value or C::value)
30
}
31
};
32
33
int main() {
34
D d(10);
35
d.printDValue();
36
return 0;
37
}
输出结果:
1
A constructor with value 10 called
2
B constructor with value 10 called
3
C constructor with value 10 called
4
D constructor with value 10 called
5
Value in A: 10
6
Value in D's A: 10
代码解析:
① B
和 C
都使用 virtual public
虚继承自 A
。
② D
多继承自 B
和 C
。
③ D
的构造函数直接在初始化列表中调用 A(v)
,并且 B
和 C
的构造函数也保留了 A(v)
的调用。但由于是虚继承,实际上 A
的构造函数只会被调用一次,由最底层的派生类 D
的构造函数负责初始化。
④ 在 D
类的 printDValue()
函数中,可以直接调用 printValue()
和访问 value
,不再存在歧义,因为 D
只继承了一份 A
的成员。
⑤ 输出结果显示,A
的构造函数只被调用了一次,证明了虚继承解决了数据冗余问题。
虚继承的原理:
虚继承的实现原理相对复杂。简单来说,虚继承通过引入虚基类指针 (virtual base class pointer),使得派生类共享同一个虚基类实例。编译器会为虚继承的派生类对象添加一个隐藏的虚基类指针,指向虚基类实例的地址。当派生类对象访问虚基类成员时,会通过虚基类指针间接访问共享的虚基类实例。
虚继承的构造函数调用:
虚继承的构造函数调用规则也比较特殊。在菱形继承结构中,虚基类的构造函数由最底层的派生类直接调用,而中间层的派生类构造函数对虚基类的构造函数的调用会被忽略。在上述示例中,D
的构造函数直接调用了 A(v)
,而 B
和 C
的构造函数中的 A(v)
调用实际上不起作用。
总结:
虚继承是解决菱形继承问题的有效机制,它可以消除数据冗余和命名冲突,使得多继承在某些场景下更加实用。但是,虚继承的实现原理比较复杂,会增加一定的运行时开销。因此,在设计多继承结构时,应该谨慎评估是否需要使用虚继承,避免过度使用。
10.3.5 多继承的应用场景和注意事项 (Application Scenarios and Considerations of Multiple Inheritance)
多继承虽然提供了强大的代码重用能力,但也带来了更高的复杂性,容易引发菱形继承等问题。因此,多继承的使用需要谨慎,并遵循一些设计原则。
应用场景:
① 接口继承 (Interface Inheritance) 的组合:当需要组合多个接口时,多继承可以发挥作用。例如,一个类可能需要同时实现多个接口,如 Serializable
(可序列化的)、 Cloneable
(可克隆的)、 Comparable
(可比较的) 等。在 C++ 中,可以使用纯虚函数 (pure virtual functions) 和抽象类 (abstract classes) 来模拟接口,并通过多继承组合多个接口。
② 某些特定的设计模式 (Specific Design Patterns):在某些特定的设计模式中,例如 Mixin 模式,可以使用多继承来组合多个类的功能。Mixin 类通常是一些小的、独立的、提供特定功能的类,通过多继承将 Mixin 类的功能 "混入" 到其他类中,实现功能的复用和组合。
注意事项和最佳实践:
① 谨慎使用多继承 (Use Multiple Inheritance Judiciously):多继承会增加代码的复杂性,容易引发菱形继承等问题。在设计类继承结构时,应该优先考虑使用单继承和组合 (composition) 的方式来解决问题。只有在确实需要组合多个基类的接口或功能,并且单继承和组合无法有效解决时,才考虑使用多继承。
② 避免菱形继承 (Avoid Diamond Inheritance):尽量避免菱形继承结构。如果不可避免,必须使用虚继承来解决菱形继承问题,消除数据冗余和命名冲突。
③ 明确继承关系 (Clarify Inheritance Relationships):在使用多继承时,要清晰地理解派生类与各个基类之间的关系,明确派生类需要从哪些基类继承哪些功能。避免滥用多继承,导致继承结构混乱和难以维护。
④ 优先使用组合 (Prefer Composition over Inheritance):在很多情况下,组合可以替代继承,并且更加灵活和解耦。组合是指在一个类中包含其他类的对象作为成员变量,通过对象之间的协作来实现功能复用。组合通常比继承更加简单、灵活和易于维护。在设计类结构时,应该优先考虑使用组合,只有在确实需要 "is-a" 关系,并且继承能够带来明显优势时,才考虑使用继承。
⑤ 合理使用访问修饰符 (Use Access Modifiers Properly):在多继承中,要合理使用继承访问修饰符 (public
, protected
, private
),控制基类成员在派生类中的访问权限,以及派生类对象对基类成员的访问权限,确保代码的封装性和安全性。
总结:
多继承是一种强大的代码重用机制,但同时也具有较高的复杂性和潜在的风险。在使用多继承时,需要谨慎评估其必要性,遵循最佳实践,并充分理解其原理和限制,才能有效地利用多继承,构建出高质量的面向对象系统。在很多情况下,单继承和组合是更加简单、安全和灵活的选择。
11. 模板:泛型编程
11.1 泛型编程 (Generic Programming) 思想
泛型编程 (Generic Programming) 是一种编程范式,旨在编写不依赖于具体数据类型的代码。其核心思想是将算法从特定的数据类型中抽象出来,使其能够应用于多种数据类型,从而提高代码的复用性 (reusability) 和灵活性 (flexibility)。在传统的面向对象编程 (Object-Oriented Programming, OOP) 中,我们通常通过继承 (inheritance) 和多态 (polymorphism) 来实现一定程度的代码复用,但泛型编程提供了更为强大的抽象能力。
泛型编程的关键在于参数化类型 (parameterized types)。这意味着我们可以编写模板 (templates),这些模板在定义时使用类型参数 (type parameters) 或非类型参数 (non-type parameters) 来代替实际的数据类型。当使用模板时,编译器会根据实际提供的类型参数或非类型参数,生成特定类型的代码,这个过程称为模板实例化 (template instantiation)。
泛型编程的主要优势包括:
① 代码复用 (Code Reusability):
▮ 使用模板可以编写通用的算法和数据结构,无需为每种数据类型都编写重复的代码。例如,可以编写一个通用的排序算法模板,它可以对 int
数组、float
数组、甚至自定义类型的数组进行排序。
② 类型安全 (Type Safety):
▮ 泛型编程在编译时进行类型检查,可以避免在运行时出现类型错误。模板实例化生成的代码是类型安全的,因为编译器会确保模板代码与实际使用的类型参数兼容。
③ 性能优化 (Performance Optimization):
▮ 泛型编程通常能够提供与特定类型代码相近的性能,甚至在某些情况下可以获得更好的性能。这是因为模板在编译时生成特定类型的代码,避免了运行时的类型判断和转换开销。
④ 提高抽象层次 (Increased Abstraction Level):
▮ 泛型编程允许程序员在更高的抽象层次上思考问题,专注于算法的逻辑,而无需过多关注具体的数据类型。这有助于提高开发效率和代码的可维护性 (maintainability)。
C++ 语言通过模板 (templates) 机制来支持泛型编程。C++ 模板可以分为两类:函数模板 (function templates) 和类模板 (class templates)。
11.2 函数模板 (Function Templates)
函数模板 (Function Templates) 是一种用于创建泛型函数 (generic functions) 的蓝图或公式。它允许我们编写一个函数,使其可以操作多种数据类型,而无需为每种类型都重载函数。
11.2.1 函数模板的定义 (Function Template Definition)
函数模板的定义以关键字 template
开头,后跟尖括号 <>
包含的模板参数列表 (template parameter list)。模板参数列表可以包含一个或多个模板参数 (template parameters),用于表示类型或非类型参数。类型参数使用关键字 typename
或 class
声明,非类型参数声明方式与普通变量类似,但类型必须是整型 (integral type)、枚举类型 (enumeration type)、指针类型 (pointer type)、引用类型 (reference type) 或 std::nullptr_t
。
函数模板的语法结构如下:
1
template <模板参数列表>
2
返回类型 函数名(参数列表) {
3
// 函数体
4
}
例如,下面是一个简单的函数模板,用于交换两个变量的值:
1
template <typename T>
2
void swap_values(T& a, T& b) {
3
T temp = a;
4
a = b;
5
b = temp;
6
}
在这个例子中,template <typename T>
声明了一个函数模板,typename T
表示 T
是一个类型参数,它可以代表任何数据类型。函数 swap_values
接受两个类型为 T
的引用参数 a
和 b
,并交换它们的值。
11.2.2 函数模板的实例化 (Function Template Instantiation)
函数模板本身不是一个真正的函数,而是一个蓝图 (blueprint)。只有当函数模板被调用 (called) 时,编译器才会根据实际提供的模板实参 (template arguments) 生成特定类型的函数代码,这个过程称为模板实例化 (template instantiation)。
函数模板的实例化可以分为隐式实例化 (implicit instantiation) 和显式实例化 (explicit instantiation) 两种方式。
① 隐式实例化 (Implicit Instantiation):
▮ 隐式实例化是指编译器根据函数调用时提供的实参类型,自动推导模板参数的类型,并生成相应的函数代码。
例如,当我们调用 swap_values
函数模板时,编译器会根据实参的类型进行隐式实例化:
1
int main() {
2
int x = 10, y = 20;
3
swap_values(x, y); // 隐式实例化 swap_values<int>
4
std::cout << "x = " << x << ", y = " << y << std::endl; // 输出: x = 20, y = 10
5
6
double a = 3.14, b = 1.59;
7
swap_values(a, b); // 隐式实例化 swap_values<double>
8
std::cout << "a = " << a << ", b = " << b << std::endl; // 输出: a = 1.59, b = 3.14
9
10
return 0;
11
}
在第一次调用 swap_values(x, y)
时,由于实参 x
和 y
的类型是 int
,编译器会隐式实例化 swap_values<int>
函数。在第二次调用 swap_values(a, b)
时,由于实参 a
和 b
的类型是 double
,编译器会隐式实例化 swap_values<double>
函数。
② 显式实例化 (Explicit Instantiation):
▮ 显式实例化是指程序员显式地指定模板参数的类型,强制编译器生成特定类型的函数代码。
函数模板的显式实例化语法如下:
1
template 返回类型 函数名<模板实参列表>(参数列表);
例如,我们可以显式实例化 swap_values<int>
函数:
1
template void swap_values<int>(int& a, int& b); // 显式实例化 swap_values<int>
显式实例化通常用于提前生成特定类型的模板代码,或者在分离编译 (separate compilation) 的情况下,确保模板代码的链接 (linking) 正确。
11.2.3 函数模板的重载 (Function Template Overloading)
函数模板也可以像普通函数一样进行重载 (overloading)。函数模板的重载可以通过以下方式实现:
① 参数列表不同 (Different Parameter Lists):
▮ 可以定义多个函数模板,它们的函数名相同,但参数列表的数量 (number) 或类型 (type) 不同。
② 模板参数列表不同 (Different Template Parameter Lists):
▮ 可以定义多个函数模板,它们的函数名和参数列表相同,但模板参数列表的数量 (number) 或类型 (type) 不同。
例如,我们可以重载 max
函数模板,使其可以比较两个值,也可以比较三个值:
1
template <typename T>
2
T max(T a, T b) {
3
return (a > b) ? a : b;
4
}
5
6
template <typename T>
7
T max(T a, T b, T c) {
8
return max(max(a, b), c); // 调用 max(T, T) 函数模板
9
}
当我们调用 max
函数模板时,编译器会根据提供的实参数量和类型,选择最匹配的函数模板进行实例化和调用:
1
int main() {
2
int x = 10, y = 20, z = 15;
3
int m1 = max(x, y); // 调用 max(T, T) 函数模板,实例化 max<int>(int, int)
4
int m2 = max(x, y, z); // 调用 max(T, T, T) 函数模板,实例化 max<int>(int, int, int)
5
std::cout << "max(x, y) = " << m1 << std::endl; // 输出: max(x, y) = 20
6
std::cout << "max(x, y, z) = " << m2 << std::endl; // 输出: max(x, y, z) = 20
7
8
return 0;
9
}
11.2.4 函数模板的模板参数 (Template Parameters of Function Templates)
函数模板的模板参数列表可以包含多种类型的参数,包括类型参数 (type parameters) 和非类型参数 (non-type parameters)。
① 类型参数 (Type Parameters):
▮ 类型参数用于表示类型,通常使用关键字 typename
或 class
声明。类型参数在函数模板实例化时会被实际的数据类型替换。
例如,在 swap_values
函数模板中,typename T
声明了一个类型参数 T
。
② 非类型参数 (Non-type Parameters):
▮ 非类型参数用于表示值,其声明方式与普通变量类似,但类型必须是整型 (integral type)、枚举类型 (enumeration type)、指针类型 (pointer type)、引用类型 (reference type) 或 std::nullptr_t
。非类型参数在函数模板实例化时会被实际的值替换。
例如,下面是一个使用非类型参数的函数模板,用于创建静态数组:
1
template <typename T, int size>
2
class StaticArray {
3
private:
4
T data[size]; // 使用非类型参数 size 定义数组大小
5
public:
6
// ...
7
};
在这个例子中,int size
声明了一个非类型参数 size
,它表示数组的大小。当我们实例化 StaticArray
类模板时,需要为 size
提供一个整数值作为实参:
1
StaticArray<int, 10> arr1; // size = 10
2
StaticArray<double, 20> arr2; // size = 20
③ 默认模板实参 (Default Template Arguments) (C++11 起):
▮ 从 C++11 标准开始,函数模板和类模板的模板参数可以指定默认模板实参 (default template arguments)。如果在模板实例化时没有显式提供模板实参,将使用默认值。
例如,我们可以为 StaticArray
类模板的类型参数 T
指定默认模板实参 int
:
1
template <typename T = int, int size> // 为类型参数 T 指定默认模板实参 int
2
class StaticArray {
3
// ...
4
};
5
6
StaticArray< > arr3; // 使用默认模板实参 T = int,size 需要显式指定
7
StaticArray<int, 30> arr4; // 显式指定 T = int,size = 30
④ 可变参数模板 (Variadic Templates) (C++11 起):
▮ 从 C++11 标准开始,C++ 引入了可变参数模板 (variadic templates),允许模板参数列表接受可变数量 (variable number) 的模板参数。可变参数模板使用省略号 ...
来表示,可以用于函数模板和类模板。
例如,下面是一个使用可变参数模板的函数模板,用于打印可变数量的参数:
1
template <typename T>
2
void print(T arg) {
3
std::cout << arg << std::endl;
4
}
5
6
template <typename T, typename... Args> // Args 是一个模板参数包 (template parameter pack)
7
void print(T first_arg, Args... args) { // args 是一个函数参数包 (function parameter pack)
8
std::cout << first_arg << ", ";
9
print(args...); // 递归调用 print 函数模板,展开参数包
10
}
在这个例子中,typename... Args
声明了一个模板参数包 (template parameter pack) Args
,它可以接受零个或多个类型参数。Args... args
声明了一个函数参数包 (function parameter pack) args
,它可以接受与 Args
参数包中类型参数数量相同的函数实参。
当我们调用 print
函数模板时,可以传递可变数量的实参:
1
int main() {
2
print(1); // 调用 print(T)
3
print(1, 2.5); // 调用 print(T, Args...),实例化 print<int, double>
4
print("hello", 10, 3.14); // 调用 print(T, Args...),实例化 print<const char*, int, double>
5
6
return 0;
7
}
可变参数模板在实现元组 (tuples)、变参函数 (variadic functions) 等高级特性时非常有用。
11.3 类模板 (Class Templates)
类模板 (Class Templates) 是一种用于创建泛型类 (generic classes) 的蓝图或公式。它允许我们定义一个类,使其可以操作多种数据类型,而无需为每种类型都定义一个类。类模板是 C++ 泛型编程的重要组成部分,是实现容器 (containers) 和算法 (algorithms) 等通用数据结构和算法的基础。
11.3.1 类模板的定义 (Class Template Definition)
类模板的定义与函数模板类似,以关键字 template
开头,后跟尖括号 <>
包含的模板参数列表 (template parameter list)。模板参数列表可以包含一个或多个模板参数 (template parameters),用于表示类型或非类型参数。
类模板的语法结构如下:
1
template <模板参数列表>
2
class 类名 {
3
// 类体
4
};
例如,下面是一个简单的类模板,用于表示一个动态数组:
1
template <typename T>
2
class DynamicArray {
3
private:
4
T* data; // 指向动态分配的数组
5
int capacity; // 数组容量
6
int size; // 当前元素个数
7
public:
8
DynamicArray(int capacity); // 构造函数
9
~DynamicArray(); // 析构函数
10
void push_back(const T& value); // 添加元素
11
T& operator[](int index); // 访问元素
12
int getSize() const; // 获取元素个数
13
int getCapacity() const; // 获取数组容量
14
};
在这个例子中,template <typename T>
声明了一个类模板 DynamicArray
,typename T
表示 T
是一个类型参数,它可以代表数组中元素的类型。类 DynamicArray
包含动态分配的数组 data
,容量 capacity
和元素个数 size
等成员变量,以及构造函数、析构函数、添加元素和访问元素等成员函数。
11.3.2 类模板的实例化 (Class Template Instantiation)
与函数模板类似,类模板本身也不是一个真正的类,而是一个蓝图 (blueprint)。只有当类模板被使用 (used) 时,编译器才会根据实际提供的模板实参 (template arguments) 生成特定类型的类代码,这个过程称为类模板实例化 (class template instantiation)。
类模板的实例化也分为隐式实例化 (implicit instantiation) 和显式实例化 (explicit instantiation) 两种方式。
① 隐式实例化 (Implicit Instantiation):
▮ 隐式实例化是指编译器根据类模板的使用场景,自动推导模板参数的类型,并生成相应的类代码。
例如,当我们创建 DynamicArray
类模板的对象时,编译器会根据对象声明时指定的类型进行隐式实例化:
1
int main() {
2
DynamicArray<int> intArray(10); // 隐式实例化 DynamicArray<int>
3
DynamicArray<double> doubleArray(20); // 隐式实例化 DynamicArray<double>
4
5
intArray.push_back(100);
6
doubleArray.push_back(3.14);
7
8
std::cout << "intArray size = " << intArray.getSize() << std::endl; // 输出: intArray size = 1
9
std::cout << "doubleArray size = " << doubleArray.getSize() << std::endl; // 输出: doubleArray size = 1
10
11
return 0;
12
}
在创建 DynamicArray<int> intArray(10)
对象时,编译器会隐式实例化 DynamicArray<int>
类。在创建 DynamicArray<double> doubleArray(20)
对象时,编译器会隐式实例化 DynamicArray<double>
类。
② 显式实例化 (Explicit Instantiation):
▮ 类模板也支持显式实例化,语法与函数模板类似:
1
template class 类名<模板实参列表>;
例如,我们可以显式实例化 DynamicArray<int>
类:
1
template class DynamicArray<int>; // 显式实例化 DynamicArray<int>
类模板的显式实例化用途与函数模板类似,通常用于提前生成特定类型的模板代码,或者在分离编译的情况下,确保模板代码的链接正确。
11.3.3 类模板的成员函数 (Member Functions of Class Templates)
类模板的成员函数本身也是函数模板。类模板的成员函数可以在类模板的类体内 (in-class) 定义,也可以在类模板的类体外 (out-of-class) 定义。
① 类体内定义 (In-class Definition):
▮ 在类模板的类体内定义的成员函数,编译器会自动将其视为函数模板。
例如,DynamicArray
类模板的构造函数和析构函数可以在类体内定义:
1
template <typename T>
2
class DynamicArray {
3
// ...
4
public:
5
DynamicArray(int capacity) : capacity(capacity), size(0) { // 类体内定义构造函数
6
data = new T[capacity];
7
}
8
~DynamicArray() { // 类体内定义析构函数
9
delete[] data;
10
}
11
// ...
12
};
② 类体外定义 (Out-of-class Definition):
▮ 在类模板的类体外定义成员函数时,需要使用限定名 (qualified name),并在函数定义前加上 template <模板参数列表>
声明。
例如,DynamicArray
类模板的 push_back
成员函数可以在类体外定义:
1
template <typename T>
2
void DynamicArray<T>::push_back(const T& value) { // 类体外定义 push_back 成员函数
3
if (size == capacity) {
4
// 扩容操作
5
capacity *= 2;
6
T* newData = new T[capacity];
7
for (int i = 0; i < size; ++i) {
8
newData[i] = data[i];
9
}
10
delete[] data;
11
data = newData;
12
}
13
data[size++] = value;
14
}
注意,在类体外定义类模板的成员函数时,需要使用 DynamicArray<T>::
这样的限定名,并在函数名前面加上 template <typename T>
声明,以表明这是一个类模板的成员函数模板。
11.3.4 类模板的模板特化 (Template Specialization of Class Templates)
类模板也支持模板特化 (template specialization),包括全特化 (full specialization) 和偏特化 (partial specialization)。模板特化允许我们为类模板的特定类型参数组合提供不同的实现。
① 全特化 (Full Specialization):
▮ 全特化是指为类模板的所有模板参数都指定具体的类型,从而创建一个完全特化的类。全特化类与原始类模板是完全独立的,可以有不同的成员变量和成员函数。
例如,我们可以为 DynamicArray<char*>
提供全特化版本,用于专门处理字符指针数组:
1
template <> // 全特化版本,模板参数列表为空
2
class DynamicArray<char*> { // 特化 DynamicArray<char*>
3
private:
4
char** data;
5
int capacity;
6
int size;
7
public:
8
DynamicArray(int capacity);
9
~DynamicArray();
10
void push_back(char* value); // 注意参数类型变为 char*
11
char*& operator[](int index);
12
int getSize() const;
13
int getCapacity() const;
14
};
全特化版本的类定义以 template <>
开头,表示这是一个全特化版本,模板参数列表为空。类名后面跟上特化的模板参数列表 DynamicArray<char*>
。
② 偏特化 (Partial Specialization):
▮ 偏特化是指只为类模板的部分模板参数指定具体的类型,或者对模板参数的类型进行某种约束 (constraint)。偏特化类仍然是模板,需要根据剩余的模板参数进行实例化。
偏特化只能针对类模板进行,函数模板不支持偏特化。
偏特化又可以分为两种形式:
▮▮▮▮ⓐ 数量偏特化 (Partial Specialization by Number of Parameters):
▮ 减少模板参数的数量。
例如,假设我们有一个接受两个类型参数的类模板 MyTemplate<T, U>
,我们可以偏特化为一个只接受一个类型参数的版本 MyTemplate<T, int>
,将第二个类型参数固定为 int
:
1
template <typename T, typename U> // 原始类模板
2
class MyTemplate {
3
// ...
4
};
5
6
template <typename T> // 偏特化版本,只接受一个类型参数
7
class MyTemplate<T, int> { // 偏特化 MyTemplate<T, int>
8
// ...
9
};
▮▮▮▮ⓑ 类型偏特化 (Partial Specialization by Type):
▮ 对模板参数的类型进行约束,例如将类型参数约束为指针类型、引用类型等。
例如,我们可以为 DynamicArray<T*>
提供偏特化版本,用于专门处理指针类型数组:
1
template <typename T> // 原始类模板
2
class DynamicArray {
3
// ...
4
};
5
6
template <typename T> // 偏特化版本,约束类型参数为指针类型
7
class DynamicArray<T*> { // 偏特化 DynamicArray<T*>
8
// ...
9
};
当编译器遇到类模板的实例化时,会按照以下顺序进行匹配:
- 全特化版本 (Full Specialization):如果存在与模板实参完全匹配的全特化版本,则选择全特化版本。
- 偏特化版本 (Partial Specialization):如果不存在全特化版本,但存在与模板实参部分匹配的偏特化版本,则选择最匹配的偏特化版本。如果有多个偏特化版本匹配,则选择最特化 (most specialized) 的版本。
- 原始类模板 (Primary Class Template):如果既不存在全特化版本,也不存在偏特化版本,则选择原始类模板进行实例化。
模板特化机制使得我们可以为特定的类型参数组合提供定制化的实现,以优化性能或处理特殊情况。
11.4 模板参数 (Template Parameters) 详解
模板参数 (Template Parameters) 是模板定义中用于表示类型或值的占位符。模板参数列表可以包含多种类型的参数,包括类型参数、非类型参数、以及从 C++11 开始引入的模板模板参数 (template template parameters) 和可变参数模板。
11.4.1 类型参数 (Type Parameters)
类型参数 (Type Parameters) 用于表示类型,通常使用关键字 typename
或 class
声明。类型参数在模板实例化时会被实际的数据类型替换。
类型参数的声明语法如下:
1
typename 类型参数名
2
class 类型参数名 // 与 typename 等价,但 class 容易引起混淆,建议使用 typename
例如,在函数模板 template <typename T> void swap_values(T& a, T& b)
中,T
就是一个类型参数。
11.4.2 非类型参数 (Non-type Parameters)
非类型参数 (Non-type Parameters) 用于表示值,其声明方式与普通变量类似,但类型受到限制,必须是以下类型之一:
⚝ 整型 (Integral Types):int
、char
、short
、long
、long long
、unsigned int
等。
⚝ 枚举类型 (Enumeration Types):enum class Color { Red, Green, Blue };
⚝ 指针类型 (Pointer Types):int*
、char*
、函数指针等。
⚝ 引用类型 (Reference Types):int&
、const double&
等。
⚝ std::nullptr_t
(C++11 起):空指针类型。
非类型参数在模板实例化时会被实际的值替换,这些值必须是编译时常量表达式 (compile-time constant expressions),例如字面量、const
或 constexpr
修饰的变量、枚举常量等。
非类型参数的声明语法如下:
1
类型 非类型参数名
例如,在类模板 template <typename T, int size> class StaticArray
中,size
就是一个非类型参数。
11.4.3 默认模板实参 (Default Template Arguments)
从 C++11 标准开始,函数模板和类模板的模板参数可以指定默认模板实参 (default template arguments)。如果在模板实例化时没有显式提供模板实参,将使用默认值。
默认模板实参的指定语法如下:
1
template <typename T = 默认类型, int size = 默认值>
2
// ...
例如,我们可以为 StaticArray
类模板的类型参数 T
指定默认模板实参 int
,为非类型参数 size
指定默认值 10
:
1
template <typename T = int, int size = 10>
2
class StaticArray {
3
// ...
4
};
5
6
StaticArray< > arr5; // 使用默认模板实参 T = int, size = 10,相当于 StaticArray<int, 10>
7
StaticArray<double> arr6; // 使用默认模板实参 size = 10,相当于 StaticArray<double, 10>
8
StaticArray<float, 20> arr7; // 显式指定 T = float, size = 20
默认模板实参可以简化模板的使用,并提高代码的灵活性。
11.4.4 模板模板参数 (Template Template Parameters) (高级主题)
模板模板参数 (template template parameters) 是一种高级的模板参数,它允许我们将类模板 (class templates) 作为模板参数传递给另一个类模板或函数模板。模板模板参数主要用于处理容器 (containers) 和分配器 (allocators) 等需要嵌套模板的情况。
模板模板参数的声明语法如下:
1
template <template <typename ...> class 模板模板参数名>
2
// ...
例如,假设我们有一个容器类模板 Container
,它需要接受一个分配器类模板作为模板参数:
1
template <typename T, template <typename U> class Allocator = std::allocator>
2
class Container {
3
private:
4
Allocator<T> allocator; // 使用分配器模板
5
// ...
6
public:
7
// ...
8
};
在这个例子中,template <typename U> class Allocator = std::allocator
声明了一个模板模板参数 Allocator
,它期望接收一个类模板作为实参,并且该类模板需要接受一个类型参数。默认情况下,Allocator
的默认模板实参是 std::allocator
,它是 C++ 标准库提供的默认分配器。
当我们实例化 Container
类模板时,可以传递不同的分配器类模板作为模板实参:
1
Container<int> container1; // 使用默认分配器 std::allocator
2
Container<int, MyAllocator> container2; // 使用自定义分配器 MyAllocator
3
Container<int, std::pmr::polymorphic_allocator> container3; // 使用多态内存资源分配器
模板模板参数的应用场景相对高级,主要用于构建复杂的泛型库和框架。
11.5 模板特化 (Template Specialization) 深入
模板特化 (Template Specialization) 允许我们为模板的特定类型参数组合提供定制化的实现。模板特化可以提高特定场景下的性能,或者处理某些类型不适用通用模板实现的情况。模板特化分为全特化和偏特化两种。
11.5.1 全特化 (Full Specialization) 详解
全特化 (Full Specialization) 是指为模板的所有模板参数都指定具体的类型,从而创建一个完全特化的版本。全特化版本与原始模板是完全独立的,可以有不同的成员变量、成员函数和实现逻辑。
全特化的声明语法如下:
1
template <> // 模板参数列表为空
2
返回类型 函数名<模板实参列表>(参数列表) { // 指定所有模板参数的类型
3
// 特化版本的函数体
4
}
5
6
template <> // 模板参数列表为空
7
class 类名<模板实参列表> { // 指定所有模板参数的类型
8
// 特化版本的类体
9
};
例如,对于 swap_values
函数模板,我们可以为 char*
类型提供全特化版本,以处理字符指针的交换(通常需要交换指针指向的内容,而不是指针本身):
1
template <typename T> // 原始函数模板
2
void swap_values(T& a, T& b) {
3
// 通用实现
4
}
5
6
template <> // 全特化版本,模板参数列表为空
7
void swap_values<char*>(char*& a, char*& b) { // 特化 swap_values<char*>
8
// char* 类型的特化实现,可能需要交换指向的字符串内容
9
std::cout << "swap_values<char*> 特化版本被调用" << std::endl;
10
char* temp = a;
11
a = b;
12
b = temp; // 这里仍然只是交换指针,实际应用中可能需要更复杂的字符串交换逻辑
13
}
当我们调用 swap_values
函数模板,并传入 char*
类型的实参时,编译器会选择全特化版本 swap_values<char*>
,而不是原始的函数模板:
1
int main() {
2
char* str1 = "hello";
3
char* str2 = "world";
4
swap_values(str1, str2); // 调用全特化版本 swap_values<char*>
5
std::cout << "str1 = " << str1 << ", str2 = " << str2 << std::endl; // 输出: str1 = world, str2 = hello
6
7
int x = 10, y = 20;
8
swap_values(x, y); // 调用原始函数模板 swap_values<T>
9
std::cout << "x = " << x << ", y = " << y << std::endl; // 输出: x = 20, y = 10
10
11
return 0;
12
}
11.5.2 偏特化 (Partial Specialization) 详解
偏特化 (Partial Specialization) 是指只为类模板的部分模板参数指定具体的类型,或者对模板参数的类型进行某种约束。偏特化版本仍然是模板,需要根据剩余的模板参数进行实例化。偏特化只能针对类模板进行,函数模板不支持偏特化。
偏特化的声明语法如下:
1
template <偏特化模板参数列表> // 偏特化版本的模板参数列表,数量可能少于原始模板
2
class 类名<偏特化模板实参列表> { // 指定部分模板参数的类型或约束
3
// 偏特化版本的类体
4
};
例如,对于类模板 MyTemplate<T, U, V>
,我们可以偏特化为 MyTemplate<T*, U, V>
,将第一个模板参数约束为指针类型:
1
template <typename T, typename U, typename V> // 原始类模板
2
class MyTemplate {
3
// ...
4
};
5
6
template <typename U, typename V> // 偏特化版本,模板参数列表减少了一个
7
class MyTemplate<int*, U, V> { // 偏特化 MyTemplate<int*, U, V>,第一个模板参数固定为 int*
8
// ...
9
};
10
11
template <typename T, typename V> // 偏特化版本,模板参数列表减少了一个
12
class MyTemplate<T, int, V> { // 偏特化 MyTemplate<T, int, V>,第二个模板参数固定为 int
13
// ...
14
};
15
16
template <typename T, typename U> // 偏特化版本,模板参数列表减少了一个
17
class MyTemplate<T, U, int> { // 偏特化 MyTemplate<T, U, int>,第三个模板参数固定为 int
18
// ...
19
};
20
21
template <typename T> // 偏特化版本,模板参数列表减少了两个
22
class MyTemplate<T, int, float> { // 偏特化 MyTemplate<T, int, float>,第二和第三个模板参数固定为 int 和 float
23
// ...
24
};
当编译器遇到类模板的实例化时,会按照最特化 (most specialized) 原则选择最匹配的版本。例如,当我们实例化 MyTemplate<int*, double, char>
时,编译器会选择 MyTemplate<int*, U, V>
偏特化版本,因为它比原始类模板更特化。当我们实例化 MyTemplate<int*, int, float>
时,编译器会选择 MyTemplate<T, int, float>
偏特化版本,因为它比 MyTemplate<int*, U, V>
更特化。
模板特化提供了一种强大的机制,用于在泛型编程中处理特殊情况,并优化特定类型的性能。但过度使用模板特化可能会导致代码复杂性增加,维护困难,因此需要谨慎使用。
11.6 模板元编程 (Template Metaprogramming) 简介 (高级主题)
模板元编程 (Template Metaprogramming, TMP) 是一种利用 C++ 模板机制在编译时 (compile time) 进行计算和代码生成的编程技术。模板元编程可以将一些原本需要在运行时 (runtime) 完成的计算,提前到编译时进行,从而提高程序的性能和灵活性。
模板元编程的核心思想是:模板实例化 (template instantiation) 过程本身就是一个编译时的计算过程。我们可以利用模板的递归实例化 (recursive instantiation)、特化 (specialization) 和类型推导 (type deduction) 等特性,在编译时执行复杂的逻辑运算,并生成不同的代码。
模板元编程的主要应用场景包括:
① 编译时计算 (Compile-time Computation):
▮ 将一些常量计算、类型计算、代码生成等操作提前到编译时进行,减少运行时开销。例如,可以使用模板元编程计算阶乘、斐波那契数列、类型转换、类型检查等。
② 静态多态 (Static Polymorphism):
▮ 利用模板实现静态多态,也称为编译时多态 (compile-time polymorphism) 或 CRTP (Curiously Recurring Template Pattern)。静态多态相比于运行时多态 (虚函数),具有更高的性能和灵活性。
③ 代码优化和定制 (Code Optimization and Customization):
▮ 根据编译时已知的类型信息或配置信息,生成高度优化的代码。例如,可以根据循环展开因子、SIMD 指令集等编译时常量,生成不同的循环代码。
模板元编程的实现方式主要依赖于以下 C++ 模板特性:
⚝ 模板递归实例化 (Template Recursive Instantiation):模板可以在其自身定义中递归地使用自身,实现循环或递归的编译时计算。
⚝ 模板特化 (Template Specialization):可以通过模板特化为不同的类型参数组合提供不同的实现,实现编译时的条件分支。
⚝ **constexpr**
函数 (constexpr Functions) (C++11 起):constexpr
函数可以在编译时或运行时求值,为模板元编程提供了编译时常量的计算能力。
⚝ **static_assert**
(C++11 起):static_assert
可以在编译时进行断言检查,用于在编译时发现错误。
⚝ **decltype**
(C++11 起):decltype
可以推导表达式的类型,用于获取类型信息。
⚝ **std::enable_if**
和 **std::conditional**
(C++11 起):std::enable_if
和 std::conditional
可以实现编译时的条件选择,根据条件选择不同的类型或代码分支。
⚝ 折叠表达式 (Fold Expressions) (C++17 起):折叠表达式可以简化可变参数模板的编译时计算。
⚝ Concepts (概念) (C++20 起):Concepts 可以对模板参数进行类型约束,提高模板代码的可读性和错误提示信息。
模板元编程是一种高级且复杂的编程技术,需要深入理解 C++ 模板机制和编译原理。模板元编程代码通常具有较高的抽象层次,可读性较差,调试困难,因此需要谨慎使用。
11.7 模板的最佳实践和常见错误
11.7.1 模板的最佳实践 (Best Practices for Templates)
① 保持模板代码简洁和通用 (Keep Templates Simple and Generic):
▮ 模板的主要目的是提高代码的复用性和灵活性。应该尽量编写简洁、通用的模板代码,避免过度特化和复杂的模板逻辑。
② 使用清晰的模板参数命名 (Use Clear Template Parameter Names):
▮ 模板参数的命名应该具有描述性,能够清晰地表达模板参数的用途。例如,使用 typename T
表示类型参数,int Size
表示大小参数。
③ 提供充分的文档和注释 (Provide Sufficient Documentation and Comments):
▮ 模板代码通常比普通代码更难理解,应该提供充分的文档和注释,解释模板的用途、参数、使用方法和注意事项。
④ 避免过度使用模板特化 (Avoid Overusing Template Specialization):
▮ 模板特化可以提高特定场景下的性能,但也可能增加代码的复杂性和维护难度。应该谨慎使用模板特化,只在必要时进行特化。
⑤ 考虑使用 Concepts (概念) 进行类型约束 (Consider Using Concepts for Type Constraints) (C++20 起):
▮ Concepts 可以对模板参数进行类型约束,提高模板代码的可读性和错误提示信息。在 C++20 及以上版本中,应该优先考虑使用 Concepts 来约束模板参数的类型。
⑥ 进行充分的测试 (Perform Thorough Testing):
▮ 模板代码需要进行充分的测试,确保在各种类型参数组合下都能正常工作。可以使用单元测试框架 (如 Google Test, Catch2) 对模板代码进行全面的测试。
11.7.2 模板的常见错误 (Common Errors with Templates)
① 模板编译错误 (Template Compilation Errors):
▮ 模板编译错误通常比较难以理解,错误信息可能很长且指向不明。应该仔细阅读编译错误信息,并使用调试工具 (如编译器提供的模板实例化信息) 定位错误。
② 模板链接错误 (Template Linking Errors):
▮ 在分离编译模式下,如果模板的定义和声明没有放在头文件中,或者没有进行显式实例化,可能会导致链接错误。应该将模板的定义和声明放在头文件中,或者在源文件中进行显式实例化。
③ 模板代码膨胀 (Template Code Bloat):
▮ 模板实例化会生成多份代码,如果过度使用模板,可能会导致可执行文件体积增大,甚至影响性能。应该尽量编写通用的模板代码,避免不必要的模板实例化。
④ 模板代码可读性差 (Template Code Readability Issues):
▮ 复杂的模板代码可能难以理解和维护。应该尽量保持模板代码简洁清晰,并提供充分的文档和注释。
⑤ 模板特化歧义 (Template Specialization Ambiguity):
▮ 当存在多个模板特化版本时,编译器可能会无法确定选择哪个版本,导致编译错误。应该仔细设计模板特化,避免特化版本之间的歧义。
理解和掌握模板的这些最佳实践和常见错误,可以帮助我们更好地使用 C++ 模板进行泛型编程,提高代码的质量和效率。
12. 第12章 异常处理:增强程序的健壮性
12.1 异常处理概述 (Overview of Exception Handling)
异常处理 (Exception Handling) 是一种程序设计中的重要机制,用于处理程序运行时出现的异常情况,例如:文件未找到、内存不足、网络连接中断、除零错误等。在没有异常处理机制的情况下,这些错误通常会导致程序崩溃或产生不可预测的行为。C++ 提供了强大的异常处理机制,允许程序员优雅地处理这些运行时错误,增强程序的健壮性 (Robustness) 和可靠性 (Reliability)。
12.1.1 什么是异常 (What is an Exception)
在程序执行过程中,如果发生某些非预期的事件,导致程序无法按照正常的控制流程继续执行,这种情况就被称为异常 (Exception)。异常不等于错误 (Error),错误通常指的是程序代码中的逻辑或语法错误,可以在编译阶段或测试阶段被发现和修复。而异常则是在程序运行时才可能发生的,例如,用户输入了错误格式的数据,或者程序试图访问一个不存在的文件。
异常是运行时发生的事件,它会中断程序的正常流程。为了应对这些异常情况,我们需要使用异常处理机制。C++ 中的异常可以是任何类型的数据,通常是表示错误信息的对象。
12.1.2 异常处理的目的 (Purpose of Exception Handling)
异常处理的主要目的是提高程序的健壮性和可靠性。具体来说,异常处理机制可以实现以下目标:
① 错误隔离 (Error Isolation):将错误处理代码与正常的程序逻辑代码分离,使得代码结构更清晰,更易于维护。
② 程序恢复 (Program Recovery):在程序发生异常时,允许程序尝试恢复到正常状态,而不是立即终止程序。例如,当文件打开失败时,可以提示用户检查文件路径,而不是直接崩溃。
③ 资源清理 (Resource Cleanup):确保在异常发生时,已经分配的资源(如内存、文件句柄、网络连接等)能够被正确地释放,避免资源泄漏 (Resource Leak)。
④ 错误信息传递 (Error Information Propagation):提供一种标准的机制来传递错误信息,使得错误信息能够被传递到合适的处理代码,方便调试和问题定位。
12.1.3 C++ 异常处理机制 (Exception Handling Mechanism in C++)
C++ 异常处理机制主要依赖于三个关键字:try
(尝试块)、catch
(捕获块) 和 throw
(抛出)。
① try
块 (Try Block):try
块用于包裹可能会抛出异常的代码段。程序首先尝试执行 try
块中的代码。
② catch
块 (Catch Block):catch
块紧跟在 try
块之后,用于捕获并处理特定类型的异常。一个 try
块可以跟多个 catch
块,每个 catch
块处理不同类型的异常。
③ throw
语句 (Throw Statement):当在 try
块的代码执行过程中检测到异常情况时,可以使用 throw
语句抛出一个异常。抛出的异常会被相应的 catch
块捕获并处理。
当 try
块中的代码抛出异常时,程序的执行流程会立即跳转到与抛出异常类型匹配的 catch
块中。如果在当前的 try
块中没有找到匹配的 catch
块,异常会沿着函数调用栈向上传播,直到找到能够处理该异常的 catch
块,或者最终导致程序终止(如果没有任何 catch
块处理该异常)。这个过程称为栈展开 (Stack Unwinding)。
12.2 try-catch
块:捕获和处理异常 (try-catch
Block: Catching and Handling Exceptions)
try-catch
块是 C++ 异常处理的核心结构。它由 try
关键字开始,后跟一个代码块,这个代码块是我们需要监控异常的代码区域。紧接着 try
块的是一个或多个 catch
块,每个 catch
块用于处理特定类型的异常。
12.2.1 try
块的语法和作用 (Syntax and Function of try
Block)
try
块的语法结构如下:
1
try {
2
// 可能会抛出异常的代码
3
// statements that might throw exceptions
4
}
try
块的作用是界定一个受保护的代码区域。在这个区域内的代码执行过程中,如果抛出了异常,程序会尝试找到合适的 catch
块来处理这个异常。
12.2.2 catch
块的语法和作用 (Syntax and Function of catch
Block)
catch
块的语法结构如下:
1
catch (异常类型 异常对象名) {
2
// 异常处理代码
3
// exception handling code
4
}
或者,可以不指定异常对象名,只指定异常类型:
1
catch (异常类型) {
2
// 异常处理代码
3
// exception handling code
4
}
catch
块紧跟在 try
块之后,并且必须指定要捕获的异常类型。当 try
块中的代码抛出一个异常时,C++ 运行时系统会查找与抛出异常类型匹配的 catch
块。匹配规则是类型兼容 (Type Compatibility)。例如,如果抛出一个 int
类型的异常,那么 catch(int e)
或 catch(int)
可以捕获它。如果抛出一个派生类 (Derived Class) 对象的异常,那么 catch(BaseClass& e)
或 catch(BaseClass)
也可以捕获它,这体现了多态 (Polymorphism) 的特性。
一个 try
块可以跟多个 catch
块,每个 catch
块处理不同类型的异常。catch
块的顺序很重要,通常应该将处理派生类异常的 catch
块放在处理基类 (Base Class) 异常的 catch
块之前,以避免捕获类型不正确的问题。
12.2.3 捕获多种类型的异常 (Catching Multiple Exception Types)
一个 try
块后面可以跟多个 catch
块,以便处理不同类型的异常。例如:
1
#include <iostream>
2
#include <stdexcept> // for std::runtime_error
3
4
int divide(int a, int b) {
5
if (b == 0) {
6
throw std::runtime_error("除数不能为零 (Divisor cannot be zero)");
7
}
8
return a / b;
9
}
10
11
int main() {
12
int x, y;
13
std::cout << "请输入两个整数 (Enter two integers): ";
14
std::cin >> x >> y;
15
16
try {
17
int result = divide(x, y);
18
std::cout << "结果是 (Result is): " << result << std::endl;
19
} catch (std::runtime_error& error) {
20
std::cerr << "运行时错误 (Runtime error): " << error.what() << std::endl;
21
} catch (std::bad_alloc& error) {
22
std::cerr << "内存分配错误 (Memory allocation error): " << error.what() << std::endl;
23
} catch (...) { // 捕获所有其他类型的异常 (Catch all other exception types)
24
std::cerr << "未知异常 (Unknown exception)!" << std::endl;
25
}
26
27
std::cout << "程序继续执行 (Program continues to execute)..." << std::endl;
28
return 0;
29
}
在这个例子中,try
块包裹了 divide
函数的调用。如果 divide
函数抛出 std::runtime_error
类型的异常(例如,除数为零),第一个 catch
块会捕获并处理它。如果 try
块中的代码抛出 std::bad_alloc
类型的异常(例如,内存分配失败),第二个 catch
块会捕获并处理它。最后的 catch(...)
是一个通配符 catch 块 (Catch-all block),它可以捕获任何类型的异常,包括前面 catch
块没有处理的异常。通常,通配符 catch
块应该放在所有其他 catch
块的最后,作为最后的异常处理手段。
12.2.4 异常对象 (Exception Object)
在 catch
块的括号中,可以声明一个异常对象,用于访问抛出的异常的详细信息。例如,在上面的例子中,catch (std::runtime_error& error)
声明了一个名为 error
的 std::runtime_error
类型的引用,通过 error.what()
可以获取异常的描述信息。
异常对象可以是任何类型,通常是类对象。C++ 标准库提供了一系列标准的异常类,可以用于表示不同类型的异常情况。
12.2.5 重新抛出异常 (Re-throwing Exception)
在 catch
块中,有时可能需要执行一些清理操作,然后将异常重新抛出,传递给更外层的异常处理代码。可以使用 throw;
语句在 catch
块中重新抛出当前捕获的异常。
1
try {
2
// ...
3
if (/* 发生错误 */) {
4
throw std::runtime_error("内部错误 (Internal error)");
5
}
6
// ...
7
} catch (std::runtime_error& error) {
8
std::cerr << "捕获到运行时错误 (Runtime error caught): " << error.what() << std::endl;
9
// 执行一些清理操作 (Perform some cleanup operations)
10
// ...
11
std::cout << "重新抛出异常 (Re-throwing exception)..." << std::endl;
12
throw; // 重新抛出当前异常 (Re-throw the current exception)
13
}
重新抛出的异常会沿着调用栈继续向上传播,寻找外层的 try-catch
块来处理。
12.3 throw
语句:抛出异常 (throw
Statement: Throwing Exceptions)
throw
语句用于显式地抛出一个异常。当程序检测到异常情况,并且当前代码无法处理时,可以使用 throw
语句将异常抛出。
12.3.1 throw
语句的语法 (Syntax of throw
Statement)
throw
语句的语法结构如下:
1
throw 异常对象;
或者,可以直接抛出一个字面值 (Literal Value):
1
throw 字符串字面值;
2
throw 数字字面值;
throw
语句后面跟的是要抛出的异常对象。异常对象可以是任何类型的数据,通常是类对象,字符串,或基本数据类型。
12.3.2 抛出异常的类型 (Types of Exceptions to Throw)
C++ 中可以抛出任何类型的异常,但通常建议抛出类对象,特别是从 std::exception
或其派生类继承的异常类对象。这样做的好处是:
① 类型信息 (Type Information):类对象可以携带更丰富的类型信息,方便 catch
块根据异常类型进行精确处理。
② 错误信息 (Error Message):自定义异常类可以包含详细的错误描述信息,例如错误代码、错误位置等,方便错误诊断和调试。
③ 继承和多态 (Inheritance and Polymorphism):使用类继承结构可以组织异常类型,利用多态性实现更灵活的异常处理。
12.3.3 标准异常类 (Standard Exception Classes)
C++ 标准库在 <stdexcept>
头文件中定义了一系列标准的异常类,构成了一个异常类的继承体系。这些标准异常类可以覆盖常见的运行时错误情况。常用的标准异常类包括:
① std::exception
: 所有标准异常类的基类。
② std::runtime_error
: 表示运行时错误,例如逻辑错误、范围错误等。
③ std::logic_error
: 表示程序逻辑错误。
▮▮▮▮⚝ std::domain_error
: 定义域错误。
▮▮▮▮⚝ std::invalid_argument
: 无效参数。
▮▮▮▮⚝ std::length_error
: 长度超出限制。
▮▮▮▮⚝ std::out_of_range
: 范围外访问。
③ std::bad_alloc
: 内存分配失败(new
运算符抛出)。
④ std::bad_cast
: 类型转换失败(dynamic_cast
运算符抛出)。
⑤ std::bad_typeid
: typeid
运算符用于空指针 (Null Pointer) 时抛出。
⑥ std::bad_exception
: 用于异常规范 (Exception Specification) 中,表示抛出了未声明的异常。
这些标准异常类都继承自 std::exception
类,并且都重载了 what()
成员函数,用于返回异常的描述信息。
12.3.4 自定义异常类 (Custom Exception Classes)
除了使用标准异常类,也可以根据需要自定义异常类。自定义异常类通常应该继承自 std::exception
或其派生类,并重载 what()
成员函数,提供自定义的错误描述信息。
1
#include <iostream>
2
#include <stdexcept>
3
#include <string>
4
5
class FileOpenError : public std::runtime_error {
6
public:
7
FileOpenError(const std::string& filename)
8
: std::runtime_error("无法打开文件 (Failed to open file): " + filename), filename_(filename) {}
9
10
const std::string& getFilename() const {
11
return filename_;
12
}
13
14
private:
15
std::string filename_;
16
};
17
18
void openFile(const std::string& filename) {
19
// 模拟文件打开失败的情况 (Simulate file open failure)
20
if (filename == "bad_file.txt") {
21
throw FileOpenError(filename);
22
}
23
std::cout << "成功打开文件 (File opened successfully): " << filename << std::endl;
24
// ... 文件操作 (File operations) ...
25
}
26
27
int main() {
28
try {
29
openFile("good_file.txt");
30
openFile("bad_file.txt");
31
} catch (const FileOpenError& error) {
32
std::cerr << "文件打开错误 (File open error): " << error.what() << std::endl;
33
std::cerr << "文件名 (Filename): " << error.getFilename() << std::endl;
34
} catch (const std::runtime_error& error) {
35
std::cerr << "运行时错误 (Runtime error): " << error.what() << std::endl;
36
}
37
38
return 0;
39
}
在这个例子中,FileOpenError
是一个自定义的异常类,它继承自 std::runtime_error
,并添加了一个 filename_
成员变量用于存储文件名。在 openFile
函数中,如果尝试打开 "bad_file.txt" 文件,就会抛出 FileOpenError
异常。在 main
函数的 catch
块中,可以捕获 FileOpenError
异常,并访问文件名等详细信息。
12.4 栈展开 (Stack Unwinding)
当 try
块中的代码抛出异常时,如果在当前的函数中没有找到匹配的 catch
块,C++ 运行时系统会开始栈展开 (Stack Unwinding) 的过程。栈展开是指沿着函数调用栈,从异常抛出点开始,逐层向上查找能够处理该异常的 catch
块。
12.4.1 栈展开的过程 (Process of Stack Unwinding)
① 查找 catch
块 (Searching for catch
Block):当异常在某个函数中抛出后,运行时系统首先在该函数的作用域内查找与异常类型匹配的 catch
块。
② 逐层向上 (Going Up the Call Stack):如果在当前函数中没有找到匹配的 catch
块,运行时系统会沿着函数调用栈向上回溯,回到调用当前函数的函数,并在调用函数的作用域内继续查找 catch
块,依此类推。
③ 析构局部对象 (Destruction of Local Objects):在栈展开的过程中,每当从一个函数的作用域退出时,该函数作用域内的所有局部对象 (Local Objects) (包括自动存储期对象和临时对象) 都会按照它们创建顺序的逆序被销毁,即调用它们的析构函数 (Destructor)。这保证了在异常发生时,资源能够被正确地释放,避免资源泄漏。
④ 找到 catch
块或程序终止 (Catch Block Found or Program Termination):栈展开过程会一直持续,直到找到一个能够处理该异常的 catch
块。如果找到了匹配的 catch
块,程序的执行流程会跳转到该 catch
块中,执行异常处理代码。如果在栈展开到 main
函数后仍然没有找到任何 catch
块来处理该异常,运行时系统会调用 std::terminate()
函数,默认情况下,std::terminate()
会调用 std::abort()
终止程序。
12.4.2 资源管理与 RAII (Resource Management and RAII)
栈展开过程中局部对象的自动析构机制是 C++ 中实现资源管理的重要手段。RAII (Resource Acquisition Is Initialization),即“资源获取即初始化”,是一种 C++ 编程技术,它利用对象的生命周期来管理资源。RAII 的核心思想是将资源的获取和管理与对象的生命周期绑定在一起。资源在对象创建时获取 (初始化),在对象销毁时释放 (析构)。由于栈展开过程中局部对象会被自动析构,因此,只要将资源封装在 RAII 对象中,就可以确保在异常发生时,资源能够被正确地释放,即使异常发生在资源获取之后,资源释放之前。
例如,可以使用 RAII 技术来管理文件句柄、内存、锁等资源。C++ 标准库中的智能指针 (Smart Pointers)(如 std::unique_ptr
, std::shared_ptr
)和文件流 (File Streams)(如 std::ifstream
, std::ofstream
)都是 RAII 技术的典型应用。
12.5 异常规范 (Exception Specification) (C++11 已弃用,C++17 已移除)
在早期的 C++ 标准中(C++11 之前),C++ 允许为函数声明异常规范 (Exception Specification),也称为throw 规范 (Throw Specification) 或异常列表 (Exception List)。异常规范用于声明一个函数可能抛出的异常类型列表。其语法形式是在函数声明的参数列表后面添加 throw(异常类型列表)
。
例如:
1
void func() throw(int, std::bad_alloc); // 声明 func 函数可能抛出 int 类型或 std::bad_alloc 类型的异常
2
void func2() throw(); // 声明 func2 函数不抛出任何异常 (noexcept 的前身)
3
void func3(); // 没有异常规范,表示 func3 函数可能抛出任何类型的异常
异常规范的作用是提供关于函数可能抛出异常的信息,帮助程序员理解和处理异常。然而,异常规范在实际应用中存在一些问题,例如运行时检查开销、与模板和泛型编程的兼容性问题等。因此,C++11 标准已经弃用 (Deprecated) 了动态异常规范,并在 C++17 标准中移除 (Removed) 了动态异常规范。
C++11 引入了 noexcept
规范 (Noexcept Specification) 作为替代方案。noexcept
规范用于声明函数是否会抛出异常。noexcept
是一个运算符,而不是一个异常类型列表。
1
void func() noexcept; // 声明 func 函数不抛出任何异常
2
void func2() noexcept(true); // 等价于 noexcept
3
void func3() noexcept(false); // 声明 func3 函数可能抛出异常 (与不使用 noexcept 效果相同)
noexcept
规范的主要优点是编译时检查,以及在某些情况下可以进行性能优化。如果一个声明为 noexcept
的函数抛出了异常,程序会立即调用 std::terminate()
终止。
在现代 C++ 编程中,推荐使用 noexcept
规范来明确声明函数是否会抛出异常,而不是使用已被弃用的动态异常规范。对于可能抛出异常的函数,则不需要显式声明异常规范(或者隐式地理解为可能抛出任何异常)。
12.6 异常处理的最佳实践 (Best Practices for Exception Handling)
合理的异常处理可以提高程序的健壮性和可维护性。以下是一些异常处理的最佳实践建议:
① 只为异常情况使用异常处理 (Use Exceptions for Exceptional Cases):异常处理机制应该用于处理真正的异常情况,即非预期的运行时错误。不要将异常处理用于正常的程序流程控制,例如代替条件判断或循环。
② 抛出有意义的异常 (Throw Meaningful Exceptions):抛出的异常应该能够清晰地描述错误情况,提供足够的信息帮助诊断和解决问题。建议使用标准异常类或自定义异常类,并提供详细的错误信息。
③ 尽早捕获,延迟处理 (Catch Early, Handle Late):在能够处理异常的地方尽早捕获异常,避免异常无限传播导致程序终止。但是,异常处理代码应该放在合适的位置,通常是在程序的较高层次,例如用户界面层、事务处理层等,而不是在底层库函数中。
④ 避免在析构函数中抛出异常 (Avoid Throwing Exceptions in Destructors):在栈展开过程中,如果析构函数本身也抛出了异常,并且此时没有 catch
块来处理这个异常,程序会调用 std::terminate()
终止。因此,应该尽量避免在析构函数中抛出异常。如果析构函数中可能发生异常,应该在析构函数内部处理,例如捕获并记录异常,或者使用 noexcept
声明析构函数。
⑤ 异常安全的代码 (Exception-Safe Code):编写异常安全的代码,保证在异常发生时,程序的状态仍然是有效的,资源不会泄漏。异常安全通常有三个级别:
▮▮▮▮⚝ 基本保证 (Basic Guarantee):即使在异常发生时,程序的状态仍然是有效的(例如,没有内存泄漏),但程序的状态可能与异常发生前不同。
▮▮▮▮⚝ 强异常安全保证 (Strong Exception Safety):如果操作成功,则操作完成;如果操作失败,程序状态回滚到操作开始之前的状态,并且不会有副作用。
▮▮▮▮⚝ 无抛出保证 (No-throw Guarantee):操作永远不会抛出异常,通常使用 noexcept
声明。
通过遵循这些最佳实践,可以编写出更健壮、更可靠、更易于维护的 C++ 程序。
总而言之,C++ 的异常处理机制是一个强大的工具,可以帮助程序员优雅地处理运行时错误,提高程序的健壮性。合理地使用 try-catch
块、throw
语句和标准异常类,并遵循异常处理的最佳实践,是编写高质量 C++ 代码的关键。
13. 输入/输出流:文件和控制台操作
13.1 C++ 输入/输出流库概述 (Overview of C++ Input/Output Stream Library)
13.2 标准输出流:控制台输出 (Standard Output Stream: Console Output)
13.3 标准输入流:控制台输入 (Standard Input Stream: Console Input)
13.4 标准错误流:错误信息输出 (Standard Error Stream: Error Message Output)
13.5 文件流:文件输入/输出 (File Streams: File Input/Output)
13.6 文件打开模式 (File Open Modes)
13.7 文本文件和二进制文件 (Text Files and Binary Files)
13.8 文件定位 (File Positioning)
13.9 格式化输入/输出 (Formatted Input/Output)
13.10 操纵算子 (Manipulators)
13.11 字符串流 (String Streams)
13.12 自定义输入/输出 (Custom Input/Output)
13.1 C++ 输入/输出流库概述 (Overview of C++ Input/Output Stream Library)
13.1.1 I/O 流的概念 (Concept of I/O Streams)
① 什么是流 (Stream)
▮▮▮▮▮▮▮▮❷ 数据流动的抽象 (Abstraction of data flow)
▮▮▮▮▮▮▮▮❸ 字节序列 (Sequence of bytes)
④ C++ I/O 流库的设计思想 (Design philosophy of C++ I/O stream library)
▮▮▮▮ⓔ 面向对象 (Object-Oriented)
▮▮▮▮ⓕ 类型安全 (Type-safe)
▮▮▮▮ⓖ 可扩展性 (Extensibility)
13.1.2 I/O 流库的头文件 (Header Files of I/O Stream Library)
① <iostream>
: 标准输入/输出流 (Standard input/output streams)
▮▮▮▮▮▮▮▮⚝ std::cin
(标准输入流, Standard input stream)
▮▮▮▮▮▮▮▮⚝ std::cout
(标准输出流, Standard output stream)
▮▮▮▮▮▮▮▮⚝ std::cerr
(标准错误流, Standard error stream, 无缓冲, unbuffered)
▮▮▮▮▮▮▮▮⚝ std::clog
(标准日志流, Standard log stream, 缓冲, buffered)
② <fstream>
: 文件输入/输出流 (File input/output streams)
▮▮▮▮▮▮▮▮⚝ std::ifstream
(文件输入流, File input stream)
▮▮▮▮▮▮▮▮⚝ std::ofstream
(文件输出流, File output stream)
▮▮▮▮▮▮▮▮⚝ std::fstream
(文件输入/输出流, File input/output stream)
③ <sstream>
: 字符串输入/输出流 (String input/output streams)
▮▮▮▮▮▮▮▮⚝ std::istringstream
(字符串输入流, String input stream)
▮▮▮▮▮▮▮▮⚝ std::ostringstream
(字符串输出流, String output stream)
▮▮▮▮▮▮▮▮⚝ std::stringstream
(字符串输入/输出流, String input/output stream)
④ <iomanip>
: I/O 操纵算子 (I/O manipulators)
▮▮▮▮▮▮▮▮⚝ 格式化输出 (Formatted output)
13.1.3 流类继承体系 (Stream Class Inheritance Hierarchy)
① 基类 (Base classes)
▮▮▮▮▮▮▮▮❷ std::ios_base
: 基本输入/输出设置 (Basic input/output settings), 如格式标志 (format flags), 异常处理 (exception handling), 本地化 (locale) 等。
▮▮▮▮▮▮▮▮❸ std::ios
: std::ios_base
的派生类, 提供格式化和错误状态管理 (formatting and error state management)。
④ 输入流类 (Input stream classes)
▮▮▮▮ⓔ std::istream
: 用于字符输入和格式化提取操作 (character input and formatted extraction operations)。
▮▮▮▮ⓕ std::ifstream
: 从文件读取数据 (read data from files)。
▮▮▮▮ⓖ std::istringstream
: 从字符串读取数据 (read data from strings)。
⑧ 输出流类 (Output stream classes)
▮▮▮▮ⓘ std::ostream
: 用于字符输出和格式化插入操作 (character output and formatted insertion operations)。
▮▮▮▮ⓙ std::ofstream
: 向文件写入数据 (write data to files)。
▮▮▮▮ⓚ std::ostringstream
: 向字符串写入数据 (write data to strings)。
⑫ 输入/输出流类 (Input/output stream classes)
▮▮▮▮▮▮▮▮❶ std::iostream
: 同时支持输入和输出 (supports both input and output), 继承自 std::istream
和 std::ostream
。
▮▮▮▮▮▮▮▮❷ std::fstream
: 读写文件 (read and write files)。
▮▮▮▮▮▮▮▮❸ std::stringstream
: 读写字符串 (read and write strings)。
13.2 标准输出流:控制台输出 (Standard Output Stream: Console Output)
13.2.1 std::cout
对象 (The std::cout
Object)
① 标准输出流的全局对象 (Global object for standard output stream)
② 关联到标准输出设备 (通常是控制台, Associated with the standard output device, usually the console)
13.2.2 使用插入运算符 <<
(Using the Insertion Operator <<
)
① 插入运算符的重载 (Overloading of the insertion operator)
▮▮▮▮▮▮▮▮❷ 将数据 "插入" 到输出流 (“Insert” data into the output stream)
③ 链式输出 (Chained output)
④ 输出不同数据类型 (Outputting different data types)
▮▮▮▮ⓔ 基本数据类型 (Basic data types): int
, float
, char
, bool
等
▮▮▮▮ⓕ 字符串 (Strings): C 风格字符串和 std::string
13.2.3 控制输出格式 (Controlling Output Format)
① 使用操纵算子 (Using manipulators)
▮▮▮▮▮▮▮▮❷ <iomanip>
头文件 (Header file <iomanip>
)
▮▮▮▮▮▮▮▮❸ 常用操纵算子 (Common manipulators):
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::endl
: 插入换行符并刷新流 (insert newline and flush stream)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::setw(int n)
: 设置字段宽度为 n
(set field width to n
)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::setprecision(int n)
: 设置浮点数精度为 n
位 (set floating-point precision to n
digits)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::fixed
: 以定点表示法显示浮点数 (display floating-point numbers in fixed-point notation)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::scientific
: 以科学计数法显示浮点数 (display floating-point numbers in scientific notation)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::left
: 左对齐 (left-align)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::right
: 右对齐 (right-align)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::internal
: 内部对齐 (符号或基数前缀左对齐,值右对齐, internal alignment, sign or base prefix left-justified, value right-justified)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::hex
, std::dec
, std::oct
: 十六进制、十进制、八进制输出 (hexadecimal, decimal, octal output)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::showbase
: 显示进制前缀 (如 0x
, 0
, show base prefix, e.g., 0x
, 0
)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::noshowbase
: 不显示进制前缀 (do not show base prefix)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::boolalpha
: 以 true
或 false
输出布尔值 (output boolean values as true
or false
)
▮▮▮▮▮▮▮▮▮▮▮▮⚝ std::noboolalpha
: 以 1
或 0
输出布尔值 (output boolean values as 1
or 0
)
② 使用 std::cout.flags()
, std::cout.setf()
, std::cout.unsetf()
, std::cout.precision()
, std::cout.width()
等成员函数 (Using member functions like std::cout.flags()
, std::cout.setf()
, std::cout.unsetf()
, std::cout.precision()
, std::cout.width()
)
13.2.4 输出缓冲区和刷新 (Output Buffer and Flushing)
① 输出缓冲区 (Output buffer)
▮▮▮▮▮▮▮▮❷ 提高 I/O 效率 (Improve I/O efficiency)
③ 刷新缓冲区 (Flushing the buffer)
▮▮▮▮ⓓ 自动刷新 (Automatic flushing): 程序结束, 缓冲区满, std::endl
等
▮▮▮▮ⓔ 手动刷新 (Manual flushing): std::cout << std::flush;
或 std::cout << std::endl;
13.3 标准输入流:控制台输入 (Standard Input Stream: Console Input)
13.3.1 std::cin
对象 (The std::cin
Object)
① 标准输入流的全局对象 (Global object for standard input stream)
② 关联到标准输入设备 (通常是键盘, Associated with the standard input device, usually the keyboard)
13.3.2 使用提取运算符 >>
(Using the Extraction Operator >>
)
① 提取运算符的重载 (Overloading of the extraction operator)
▮▮▮▮▮▮▮▮❷ 从输入流 "提取" 数据 (“Extract” data from the input stream)
③ 链式输入 (Chained input)
④ 输入不同数据类型 (Inputting different data types)
▮▮▮▮ⓔ 基本数据类型 (Basic data types): int
, float
, char
, bool
等
▮▮▮▮ⓕ 字符串 (Strings): C 风格字符串和 std::string
13.3.3 输入中的空格和换行符 (Whitespace and Newlines in Input)
① 提取运算符默认跳过空白字符 (Extraction operator skips whitespace by default)
② 读取一行输入 (Reading a line of input)
▮▮▮▮▮▮▮▮❸ std::getline(std::cin, string)
函数 (Function std::getline(std::cin, string)
)
④ 读取单个字符 (Reading a single character)
▮▮▮▮ⓔ std::cin >> char
(使用提取运算符, Using extraction operator)
▮▮▮▮ⓕ std::cin.get()
函数 (Function std::cin.get()
)
13.3.4 输入错误处理 (Input Error Handling)
① 输入错误状态标志 (Input error state flags)
▮▮▮▮▮▮▮▮❷ std::ios::failbit
: 格式错误 (Format error), 如读取整数时输入了字符
▮▮▮▮▮▮▮▮❸ std::ios::badbit
: 严重的错误 (Serious error), 如读取文件时发生 I/O 错误
▮▮▮▮▮▮▮▮❹ std::ios::eofbit
: 到达文件末尾 (End-of-file)
⑤ 检查错误状态 (Checking error state)
▮▮▮▮ⓕ std::cin.fail()
: 检查 failbit
或 badbit
是否被设置 (check if failbit
or badbit
is set)
▮▮▮▮ⓖ std::cin.bad()
: 检查 badbit
是否被设置 (check if badbit
is set)
▮▮▮▮ⓗ std::cin.eof()
: 检查 eofbit
是否被设置 (check if eofbit
is set)
▮▮▮▮ⓘ std::cin.good()
: 检查是否没有错误标志被设置 (check if no error flags are set)
⑩ 清除错误状态 (Clearing error state)
▮▮▮▮▮▮▮▮❶ std::cin.clear()
函数 (Function std::cin.clear()
)
⑫ 忽略输入缓冲区中的字符 (Ignoring characters in the input buffer)
▮▮▮▮ⓜ std::cin.ignore(streamsize n, char delim)
函数 (Function std::cin.ignore(streamsize n, char delim)
)
13.4 标准错误流:错误信息输出 (Standard Error Stream: Error Message Output)
13.4.1 std::cerr
对象 (The std::cerr
Object)
① 标准错误流的全局对象 (Global object for standard error stream)
② 关联到标准错误输出设备 (通常也是控制台, Associated with the standard error output device, usually also the console)
③ 无缓冲输出 (Unbuffered output)
▮▮▮▮▮▮▮▮❹ 错误信息立即输出 (Error messages are output immediately)
13.4.2 std::clog
对象 (The std::clog
Object)
① 标准日志流的全局对象 (Global object for standard log stream)
② 关联到标准错误输出设备 (通常也是控制台, Associated with the standard error output device, usually also the console)
③ 缓冲输出 (Buffered output)
▮▮▮▮▮▮▮▮❹ 用于程序日志记录 (Used for program logging)
13.4.3 何时使用 std::cerr
和 std::clog
(When to Use std::cerr
and std::clog
)
① std::cerr
: 用于紧急错误信息,需要立即显示 (for urgent error messages that need to be displayed immediately)
② std::clog
: 用于程序运行日志,可以缓冲提高效率 (for program logs, buffering can improve efficiency)
13.5 文件流:文件输入/输出 (File Streams: File Input/Output)
13.5.1 文件流类 (File Stream Classes)
① std::ofstream
: 文件输出流 (File output stream), 用于写文件 (for writing to files)
② std::ifstream
: 文件输入流 (File input stream), 用于读文件 (for reading from files)
③ std::fstream
: 文件输入/输出流 (File input/output stream), 用于读写文件 (for reading and writing to files)
13.5.2 打开文件 (Opening Files)
① 使用构造函数打开文件 (Opening files using constructors)
▮▮▮▮▮▮▮▮❷ std::ofstream outfile("filename", openmode);
▮▮▮▮▮▮▮▮❸ std::ifstream infile("filename", openmode);
▮▮▮▮▮▮▮▮❹ std::fstream iostreamfile("filename", openmode);
⑤ 使用 open()
函数打开文件 (Opening files using the open()
function)
⑥ 检查文件是否成功打开 (Checking if a file was opened successfully)
▮▮▮▮ⓖ 使用 is_open()
成员函数 (Using the is_open()
member function)
▮▮▮▮ⓗ 检查流对象的布尔值 (Checking the boolean value of the stream object): if (outfile)
...
13.5.3 关闭文件 (Closing Files)
① 自动关闭 (Automatic closing): 文件流对象析构时自动关闭 (files are closed automatically when the file stream object is destructed)
② 手动关闭 (Manual closing): 使用 close()
成员函数 (using the close()
member function): outfile.close();
③ 建议显式关闭文件 (It is recommended to explicitly close files)
13.5.4 文件的写入操作 (File Writing Operations)
① 使用插入运算符 <<
写入 (Writing using the insertion operator <<
)
② 写入文本数据和二进制数据 (Writing text data and binary data)
③ 格式化输出到文件 (Formatted output to files)
13.5.5 文件的读取操作 (File Reading Operations)
① 使用提取运算符 >>
读取 (Reading using the extraction operator >>
)
② 逐行读取文件 (Reading a file line by line)
▮▮▮▮▮▮▮▮❸ std::getline(infile, string)
函数 (Function std::getline(infile, string)
)
④ 逐字符读取文件 (Reading a file character by character)
▮▮▮▮ⓔ infile >> char
(使用提取运算符, Using extraction operator)
▮▮▮▮ⓕ infile.get()
函数 (Function infile.get()
)
⑦ 读取二进制数据 (Reading binary data)
⑧ 文件末尾 (End-of-File, EOF) 检测 (EOF detection)
▮▮▮▮ⓘ infile.eof()
函数 (Function infile.eof()
)
13.6 文件打开模式 (File Open Modes)
13.6.1 打开模式标志 (Open Mode Flags)
① 定义在 std::ios_base
类中 (Defined in the std::ios_base
class)
② 可以使用 |
运算符组合 (Can be combined using the |
operator)
13.6.2 常用打开模式 (Common Open Modes)
① std::ios::in
: 输入模式 (Input mode), 用于读取 (for reading), ifstream
和 fstream
默认模式
② std::ios::out
: 输出模式 (Output mode), 用于写入 (for writing), ofstream
和 fstream
默认模式 (会截断文件, truncates file by default)
③ std::ios::binary
: 二进制模式 (Binary mode), 以二进制方式读写文件 (read and write files in binary mode)
④ std::ios::app
: 追加模式 (Append mode), 输出添加到文件末尾 (output is appended to the end of the file)
⑤ std::ios::ate
: 文件打开后定位到文件末尾 (at-end mode, file position is set to the end of the file after opening)
⑥ std::ios::trunc
: 截断模式 (Truncate mode), 打开文件时清空文件内容 (file content is truncated when opening), ofstream
默认模式
⑦ std::ios::noreplace
(C++23): 如果文件已存在则打开失败 (open fails if the file already exists)
⑧ std::ios::nocreate
(C++23): 如果文件不存在则打开失败 (open fails if the file does not exist)
13.6.3 文本模式和二进制模式的区别 (Difference between Text Mode and Binary Mode)
① 文本模式 (Text mode):
▮▮▮▮▮▮▮▮❷ 行尾转换 (End-of-line translation): 不同平台的行尾符 (如 \r\n
和 \n
) 会被转换为统一的换行符 \n
③ 二进制模式 (Binary mode):
▮▮▮▮▮▮▮▮❹ 原始字节流 (Raw byte stream): 不进行任何转换 (no translation is performed)
13.7 文本文件和二进制文件 (Text Files and Binary Files)
13.7.1 文本文件 (Text Files)
① 存储可读字符 (Stores readable characters)
② 可以用文本编辑器打开 (Can be opened with a text editor)
③ 适合存储文本数据 (Suitable for storing text data)
13.7.2 二进制文件 (Binary Files)
① 存储原始字节数据 (Stores raw byte data)
② 需要特定的程序解析 (Requires specific programs to parse)
③ 适合存储非文本数据 (如图像、音频、视频、程序数据, Suitable for storing non-text data, such as images, audio, video, program data)
13.7.3 如何选择文本文件或二进制文件 (How to Choose Between Text Files and Binary Files)
① 数据类型 (Data type): 文本数据选择文本文件, 非文本数据选择二进制文件 (text data for text files, non-text data for binary files)
② 存储空间 (Storage space): 二进制文件通常更节省空间 (binary files are usually more space-efficient)
③ 读写效率 (Read/write efficiency): 二进制文件通常读写效率更高 (binary files usually have higher read/write efficiency)
④ 可读性 (Readability): 文本文件具有更好的可读性 (text files have better readability)
13.8 文件定位 (File Positioning)
13.8.1 文件位置指针 (File Position Pointers)
① 输入位置指针 (Get pointer, 用于读取, for reading): seekg()
函数操作
② 输出位置指针 (Put pointer, 用于写入, for writing): seekp()
函数操作
13.8.2 定位函数 (Positioning Functions)
① seekg(streampos pos)
和 seekp(streampos pos)
: 定位到绝对位置 pos
(position to absolute position pos
)
② seekg(streamoff off, ios::seekdir way)
和 seekp(streamoff off, ios::seekdir way)
: 相对于 way
偏移 off
个位置 (offset by off
positions relative to way
)
13.8.3 定位方向 (Seek Directions, ios::seekdir
)
① std::ios::beg
: 文件开始位置 (Beginning of the file)
② std::ios::cur
: 当前位置 (Current position)
③ std::ios::end
: 文件末尾位置 (End of the file)
13.8.4 获取当前位置 (Getting Current Position)
① tellg()
: 返回输入位置指针的当前位置 (returns the current position of the get pointer)
② tellp()
: 返回输出位置指针的当前位置 (returns the current position of the put pointer)
13.9 格式化输入/输出 (Formatted Input/Output)
13.9.1 格式化标志 (Format Flags)
① 控制输出格式的标志 (Flags that control output format)
② 可以使用 setf()
和 unsetf()
函数设置和清除 (Can be set and cleared using setf()
and unsetf()
functions)
13.9.2 常用格式化标志 (Common Format Flags)
① std::ios::boolalpha
: 以 true
或 false
输出布尔值 (output boolean values as true
or false
)
② std::ios::noboolalpha
: 以 1
或 0
输出布尔值 (output boolean values as 1
or 0
)
③ std::ios::showbase
: 显示进制前缀 (如 0x
, 0
, show base prefix, e.g., 0x
, 0
)
④ std::ios::noshowbase
: 不显示进制前缀 (do not show base prefix)
⑤ std::ios::showpos
: 显示正号 +
(show plus sign +
for positive numbers)
⑥ std::ios::noshowpos
: 不显示正号 +
(do not show plus sign +
)
⑦ std::ios::uppercase
: 十六进制输出中使用大写字母 (use uppercase letters in hexadecimal output)
⑧ std::ios::nouppercase
: 十六进制输出中使用小写字母 (use lowercase letters in hexadecimal output)
⑨ std::ios::fixed
: 以定点表示法显示浮点数 (display floating-point numbers in fixed-point notation)
⑩ std::ios::scientific
: 以科学计数法显示浮点数 (display floating-point numbers in scientific notation)
⑪ std::ios::hexfloat
(C++11): 以十六进制浮点数格式输出 (output floating-point numbers in hexadecimal format)
⑫ std::ios::defaultfloat
(C++11): 恢复默认浮点数格式 (restore default floating-point format)
⑬ std::ios::left
: 左对齐 (left-align)
⑭ std::ios::right
: 右对齐 (right-align)
⑮ std::ios::internal
: 内部对齐 (internal alignment)
⑯ std::ios::dec
, std::ios::hex
, std::ios::oct
: 设置进制 (set base to decimal, hexadecimal, octal)
13.9.3 字段宽度、填充和精度 (Field Width, Fill, and Precision)
① width(int n)
: 设置字段宽度为 n
(set field width to n
)
② fill(char c)
: 设置填充字符为 c
(set fill character to c
)
③ precision(int n)
: 设置浮点数精度为 n
位 (set floating-point precision to n
digits)
13.10 操纵算子 (Manipulators)
13.10.1 操纵算子的概念 (Concept of Manipulators)
① 插入或提取操作符可以与操纵算子一起使用 (Manipulators can be used with insertion or extraction operators)
② 用于格式化输入/输出 (Used for formatting input/output)
13.10.2 常用操纵算子 (Common Manipulators)
① 格式操纵算子 (Format Manipulators) (位于 <iomanip>
和 <iostream>
)
▮▮▮▮▮▮▮▮⚝ std::setw(int n)
▮▮▮▮▮▮▮▮⚝ std::setprecision(int n)
▮▮▮▮▮▮▮▮⚝ std::setfill(char c)
▮▮▮▮▮▮▮▮⚝ std::setbase(int base)
▮▮▮▮▮▮▮▮⚝ std::left
, std::right
, std::internal
▮▮▮▮▮▮▮▮⚝ std::fixed
, std::scientific
, std::hexfloat
, std::defaultfloat
▮▮▮▮▮▮▮▮⚝ std::hex
, std::dec
, std::oct
▮▮▮▮▮▮▮▮⚝ std::showbase
, std::noshowbase
▮▮▮▮▮▮▮▮⚝ std::showpos
, std::noshowpos
▮▮▮▮▮▮▮▮⚝ std::uppercase
, std::nouppercase
▮▮▮▮▮▮▮▮⚝ std::boolalpha
, std::noboolalpha
② 其他操纵算子 (Other Manipulators) (位于 <iostream>
)
▮▮▮▮▮▮▮▮⚝ std::endl
: 插入换行符并刷新流 (insert newline and flush stream)
▮▮▮▮▮▮▮▮⚝ std::flush
: 刷新流 (flush stream)
▮▮▮▮▮▮▮▮⚝ std::ends
: 插入空字符 (insert null character)
▮▮▮▮▮▮▮▮⚝ std::ws
: 提取空白字符 (提取输入流中的空白字符, extract whitespace characters from input stream)
▮▮▮▮▮▮▮▮⚝ std::skipws
: 跳过空白字符 (skip whitespace characters before extraction, 默认行为, default behavior)
▮▮▮▮▮▮▮▮⚝ std::noskipws
: 不跳过空白字符 (do not skip whitespace characters before extraction)
▮▮▮▮▮▮▮▮⚝ std::unitbuf
: 每次输出后刷新缓冲区 (flush buffer after each output operation)
▮▮▮▮▮▮▮▮⚝ std::nounitbuf
: 恢复默认缓冲行为 (restore default buffering behavior)
13.10.3 自定义操纵算子 (Custom Manipulators)
① 创建无参数操纵算子 (Creating parameterless manipulators)
② 创建带参数操纵算子 (Creating parameterized manipulators)
13.11 字符串流 (String Streams)
13.11.1 字符串流类 (String Stream Classes)
① std::istringstream
: 字符串输入流 (String input stream), 从字符串读取数据 (read data from strings)
② std::ostringstream
: 字符串输出流 (String output stream), 向字符串写入数据 (write data to strings)
③ std::stringstream
: 字符串输入/输出流 (String input/output stream), 读写字符串 (read and write strings)
13.11.2 std::ostringstream
的应用 (Applications of std::ostringstream
)
① 格式化字符串 (Formatting strings)
② 类型转换 (Type conversion)
③ 构建复杂的字符串 (Building complex strings)
13.11.3 std::istringstream
的应用 (Applications of std::istringstream
)
① 解析字符串 (Parsing strings)
② 从字符串中提取数据 (Extracting data from strings)
13.11.4 std::stringstream
的应用 (Applications of std::stringstream
)
① 在字符串和不同数据类型之间进行转换 (Converting between strings and different data types)
② 同时进行字符串的读写操作 (Performing both read and write operations on strings)
13.12 自定义输入/输出 (Custom Input/Output)
13.12.1 重载提取和插入运算符 (Overloading Extraction and Insertion Operators)
① 为自定义类重载 <<
运算符 (Overloading <<
operator for custom classes)
② 为自定义类重载 >>
运算符 (Overloading >>
operator for custom classes)
13.12.2 自定义格式化输出 (Custom Formatted Output)
① 实现自定义的输出格式 (Implementing custom output formats)
② 考虑国际化和本地化 (Considering internationalization and localization)
14. 标准模板库 (STL):C++的强大工具库
14.1 STL 概述 (Overview of STL)
14.1.1 STL 的起源与设计思想 (Origin and Design Philosophy of STL)
标准模板库 (STL, Standard Template Library) 是 C++ 标准库的核心组成部分之一,被誉为 C++ 最强大的工具库。STL 的设计始于 Alexander Stepanov 和 Meng Lee 在 1970 年代末和 1980 年代初期的研究工作,最初的目标是开发一套通用的算法库,能够应用于不同的数据结构。后来,他们在惠普实验室 (HP Labs) 与 David Musser 等人合作,最终形成了 STL 的雏形。STL 在 1994 年被正式纳入 C++ 标准库,并随着 C++ 标准的不断发展而演进。
STL 的设计思想主要基于泛型编程 (Generic Programming)。泛型编程的核心理念是将算法从特定的数据类型中解耦出来,使得算法可以尽可能地通用。在 STL 中,算法不是直接作用于特定的容器,而是通过迭代器 (Iterators) 间接地操作容器中的元素。这种设计使得 STL 的组件可以高度地复用和组合,极大地提高了 C++ 程序开发的效率和灵活性。
STL 的主要设计原则包括:
① 通用性 (Generality):STL 组件被设计成尽可能通用,能够适用于各种不同的数据类型和应用场景。
② 效率 (Efficiency):STL 组件在性能上进行了高度优化,力求在通用性的基础上保持卓越的效率。
③ 可扩展性 (Extensibility):STL 提供了良好的扩展机制,用户可以根据自身需求自定义容器、迭代器、算法和函数对象,并与 STL 提供的组件无缝集成。
④ 类型安全 (Type Safety):STL 充分利用 C++ 的模板机制,在编译时进行类型检查,保证了类型安全,减少了运行时错误。
⑤ 互操作性 (Interoperability):STL 组件之间可以方便地组合和协作,例如可以将算法应用于不同的容器,或者将不同的函数对象与算法结合使用。
14.1.2 STL 的主要组件 (Main Components of STL)
STL 主要由以下六个核心组件构成,但通常我们更关注前四个:
① 容器 (Containers):容器是 STL 中用于存储数据的对象。STL 提供了多种类型的容器,例如 vector
(向量), list
(列表), deque
(双端队列), set
(集合), map
(映射) 等。每种容器都有其特定的数据结构和适用场景。容器负责管理对象的内存分配和释放,并提供访问和操作容器内元素的方法。
② 迭代器 (Iterators):迭代器是 STL 中用于遍历容器中元素的通用接口。迭代器类似于指针,但功能更加强大和安全。通过迭代器,算法可以独立于容器的类型来访问和操作容器中的元素。STL 定义了不同类型的迭代器,例如输入迭代器 (Input Iterator), 输出迭代器 (Output Iterator), 前向迭代器 (Forward Iterator), 双向迭代器 (Bidirectional Iterator) 和随机访问迭代器 (Random Access Iterator),每种迭代器都支持不同的操作。
③ 算法 (Algorithms):算法是 STL 中用于执行各种操作(例如排序、搜索、拷贝、转换等)的函数模板。STL 提供了大量的通用算法,这些算法可以应用于各种容器,只要容器提供了相应的迭代器。算法通过迭代器来操作容器中的元素,实现了算法与容器的解耦。
④ 函数对象 (Function Objects):函数对象,也称为仿函数 (Functors),是行为类似于函数的对象。在 C++ 中,任何重载了函数调用运算符 operator()
的类对象都可以被视为函数对象。STL 中广泛使用函数对象来实现算法的自定义操作,例如自定义排序规则、自定义谓词条件等。函数对象比普通函数更加灵活,因为它们可以携带状态。
⑤ 分配器 (Allocators)(较少直接使用):分配器负责容器的内存分配和释放。STL 容器默认使用标准分配器 std::allocator
,用户也可以自定义分配器来满足特定的内存管理需求。分配器允许用户更精细地控制内存的分配策略,例如使用内存池、定制内存对齐等。
⑥ 适配器 (Adapters)(例如容器适配器、迭代器适配器、函数对象适配器):适配器是一种设计模式,用于将一个接口转换成另一个接口,使得原本不兼容的组件可以协同工作。在 STL 中,适配器被用于改造已有的组件,例如容器适配器 stack
(栈) 和 queue
(队列) 是基于现有容器实现的,迭代器适配器可以修改迭代器的行为,函数对象适配器可以组合或修改函数对象的行为。
在实际应用中,我们通常使用容器来存储数据,使用迭代器来访问容器中的元素,使用算法来操作容器中的数据,并使用函数对象来定制算法的行为。这四个组件协同工作,构成了 STL 的核心框架,为 C++ 程序开发提供了强大的支持。
14.1.3 STL 的优势 (Advantages of STL)
使用 STL 具有诸多优势,可以显著提高 C++ 程序开发的效率、质量和性能:
① 代码复用性高 (High Code Reusability):STL 组件(容器、迭代器、算法、函数对象)都是高度通用的,可以在不同的项目和场景中复用,减少了重复开发的工作量。
② 开发效率高 (High Development Efficiency):STL 提供了大量的现成组件,程序员可以直接使用这些组件来构建复杂的程序,而无需从零开始实现数据结构和算法,从而大大提高了开发效率。
③ 性能优越 (Excellent Performance):STL 组件在设计和实现上都经过了精心的优化,通常能够提供接近甚至超过手写代码的性能。例如,STL 的排序算法通常比手写的快速排序或归并排序更高效。
④ 可靠性高 (High Reliability):STL 是经过长时间广泛测试和使用的成熟库,其代码质量和稳定性都非常高,使用 STL 可以减少程序中的错误和 Bug。
⑤ 可维护性好 (Good Maintainability):使用 STL 可以使代码结构更加清晰和规范,提高了代码的可读性和可维护性。STL 的泛型编程思想使得代码更加灵活和易于扩展。
⑥ 标准化 (Standardization):STL 是 C++ 标准库的一部分,这意味着使用 STL 的代码具有良好的跨平台性和兼容性,可以在不同的编译器和操作系统上编译和运行。
⑦ 学习曲线平缓 (Relatively Gentle Learning Curve for Basic Usage):虽然 STL 的内部机制比较复杂,但是其基本使用方法相对简单易学。初学者可以快速上手使用 STL 提供的常用容器和算法,随着经验的积累再逐步深入学习其高级特性和原理。
总而言之,STL 是 C++ 程序员必备的工具库,掌握 STL 的使用是成为一名高效的 C++ 程序员的关键。通过学习和使用 STL,可以编写出更简洁、更高效、更可靠的 C++ 程序。
14.2 容器 (Containers)
容器是 STL 的核心组件之一,用于存储和管理数据集合。STL 提供了多种类型的容器,可以根据不同的数据组织方式和访问需求进行选择。根据数据结构和功能特点,STL 容器可以分为以下几类:
⚝ 序列容器 (Sequence Containers):以线性的方式存储元素,元素之间有严格的顺序关系。例如 vector
, deque
, list
, forward_list
, array
。
⚝ 关联容器 (Associative Containers):以键值对 (key-value pairs) 的形式存储元素,并根据键 (key) 进行快速检索。关联容器通常基于某种树形结构或哈希表实现,可以提供高效的查找、插入和删除操作。例如 set
, multiset
, map
, multimap
。
⚝ 容器适配器 (Container Adapters):基于现有的序列容器实现,提供了特定的访问接口,例如栈 stack
, 队列 queue
, 优先队列 priority_queue
。容器适配器并没有改变底层容器的存储方式,而是限制了对容器的访问方式,使其符合特定的数据结构特性。
14.2.1 序列容器 (Sequence Containers)
序列容器按照元素插入的顺序存储元素,并允许程序员控制元素的顺序。STL 提供了以下几种序列容器:
① vector
(向量):vector
是最常用的序列容器之一,它实际上是一个动态数组。vector
中的元素在内存中是连续存储的,这意味着可以像访问普通数组一样通过索引 (index) 快速访问 vector
中的元素。vector
具有以下特点:
⚝ 动态大小 (Dynamic Size):vector
的大小可以动态增长,当元素数量超过预分配的容量时,vector
会自动重新分配更大的内存空间,并将原有元素复制到新的内存空间。
⚝ 随机访问 (Random Access):由于元素在内存中连续存储,vector
支持通过索引进行随机访问,访问元素的平均时间复杂度为 \(O(1)\)。
⚝ 尾部插入和删除效率高 (Efficient Insertion and Deletion at the End):在 vector
的尾部插入和删除元素的时间复杂度为均摊 \(O(1)\)。当在 vector
的中部或头部插入或删除元素时,需要移动后续元素,时间复杂度为 \(O(n)\),其中 \(n\) 是元素数量。
⚝ 适用场景 (Use Cases):vector
适用于需要频繁随机访问元素,且主要在尾部进行插入和删除操作的场景。例如,存储动态数组、实现栈等。
1
#include <iostream>
2
#include <vector>
3
4
int main() {
5
std::vector<int> vec; // 声明一个存储 int 类型的 vector
6
7
// 尾部插入元素
8
vec.push_back(10);
9
vec.push_back(20);
10
vec.push_back(30);
11
12
// 随机访问元素
13
std::cout << "vec[0]: " << vec[0] << std::endl; // 输出:vec[0]: 10
14
std::cout << "vec[1]: " << vec[1] << std::endl; // 输出:vec[1]: 20
15
std::cout << "vec[2]: " << vec[2] << std::endl; // 输出:vec[2]: 30
16
17
// 遍历 vector
18
std::cout << "Vector elements: ";
19
for (int i = 0; i < vec.size(); ++i) {
20
std::cout << vec[i] << " ";
21
}
22
std::cout << std::endl; // 输出:Vector elements: 10 20 30
23
24
return 0;
25
}
② deque
(双端队列):deque
(double-ended queue) 是一种双端队列,允许在头部和尾部进行快速插入和删除操作。deque
的内部实现通常采用分段连续存储的方式,逻辑上是连续的,但物理上可能分散在多个内存块中。deque
具有以下特点:
⚝ 动态大小 (Dynamic Size):deque
的大小可以动态增长,可以高效地在头部和尾部扩展内存空间。
⚝ 随机访问 (Random Access):deque
支持通过索引进行随机访问,访问元素的平均时间复杂度为 \(O(1)\),但常数因子可能比 vector
略大,因为需要额外的索引计算。
⚝ 头部和尾部插入和删除效率高 (Efficient Insertion and Deletion at Both Ends):在 deque
的头部和尾部插入和删除元素的时间复杂度均为均摊 \(O(1)\)。在 deque
的中部插入或删除元素时,时间复杂度为 \(O(n)\)。
⚝ 适用场景 (Use Cases):deque
适用于需要在头部和尾部频繁进行插入和删除操作的场景,例如实现队列、双端队列等。当需要在头部插入元素时,deque
比 vector
更高效。
1
#include <iostream>
2
#include <deque>
3
4
int main() {
5
std::deque<int> deq; // 声明一个存储 int 类型的 deque
6
7
// 头部和尾部插入元素
8
deq.push_back(10); // 尾部插入
9
deq.push_front(20); // 头部插入
10
deq.push_back(30); // 尾部插入
11
deq.push_front(40); // 头部插入
12
13
// 随机访问元素
14
std::cout << "deq[0]: " << deq[0] << std::endl; // 输出:deq[0]: 40
15
std::cout << "deq[1]: " << deq[1] << std::endl; // 输出:deq[1]: 20
16
std::cout << "deq[2]: " << deq[2] << std::endl; // 输出:deq[2]: 10
17
std::cout << "deq[3]: " << deq[3] << std::endl; // 输出:deq[3]: 30
18
19
// 遍历 deque
20
std::cout << "Deque elements: ";
21
for (int i = 0; i < deq.size(); ++i) {
22
std::cout << deq[i] << " ";
23
}
24
std::cout << std::endl; // 输出:Deque elements: 40 20 10 30
25
26
return 0;
27
}
③ list
(列表):list
是一个双向链表,其中的元素在内存中不是连续存储的,而是通过指针链接在一起。list
具有以下特点:
⚝ 动态大小 (Dynamic Size):list
的大小可以动态增长,可以高效地插入和删除元素,无需重新分配内存。
⚝ 不支持随机访问 (No Random Access):list
不支持通过索引进行随机访问,只能通过迭代器顺序访问元素。访问特定位置元素的时间复杂度为 \(O(n)\)。
⚝ 任意位置插入和删除效率高 (Efficient Insertion and Deletion at Any Position):在 list
的任意位置插入和删除元素的时间复杂度均为 \(O(1)\),只需要修改指针的指向,无需移动元素。
⚝ 适用场景 (Use Cases):list
适用于需要频繁在任意位置进行插入和删除操作,而对随机访问需求不高的场景。例如,实现文本编辑器、管理动态数据集合等。
1
#include <iostream>
2
#include <list>
3
4
int main() {
5
std::list<int> lst; // 声明一个存储 int 类型的 list
6
7
// 插入元素
8
lst.push_back(10); // 尾部插入
9
lst.push_front(20); // 头部插入
10
lst.push_back(30); // 尾部插入
11
lst.push_front(40); // 头部插入
12
13
// 不支持随机访问,不能使用 lst[i]
14
15
// 使用迭代器遍历 list
16
std::cout << "List elements: ";
17
for (std::list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
18
std::cout << *it << " "; // 使用 *it 解引用迭代器,访问元素值
19
}
20
std::cout << std::endl; // 输出:List elements: 40 20 10 30
21
22
// 在 list 中间插入元素
23
std::list<int>::iterator it_insert = lst.begin();
24
std::advance(it_insert, 2); // 迭代器移动到第 3 个元素位置
25
lst.insert(it_insert, 50); // 在第 3 个元素前插入 50
26
27
std::cout << "List elements after insertion: ";
28
for (int val : lst) { // C++11 范围 for 循环遍历
29
std::cout << val << " ";
30
}
31
std::cout << std::endl; // 输出:List elements after insertion: 40 20 50 10 30
32
33
return 0;
34
}
④ forward_list
(前向列表):forward_list
是一个单向链表,只支持向前遍历。相比于 list
,forward_list
更加节省内存,且在某些操作上性能更高,但功能相对受限。forward_list
具有以下特点:
⚝ 动态大小 (Dynamic Size):forward_list
的大小可以动态增长。
⚝ 只支持前向迭代器 (Forward Iterators Only):forward_list
只能使用前向迭代器进行遍历,不支持反向迭代器。
⚝ 在指定位置之后插入和删除效率高 (Efficient Insertion and Deletion After a Given Position):在 forward_list
的指定位置之后插入和删除元素的时间复杂度为 \(O(1)\),但需要在指定位置之前插入或删除元素时,需要先找到指定位置的前一个位置,操作略微复杂。
⚝ 内存占用更小 (Smaller Memory Footprint):由于是单向链表,forward_list
每个节点只需要保存一个指向下一个节点的指针,内存占用比 list
更小。
⚝ 适用场景 (Use Cases):forward_list
适用于对内存占用有严格要求,且主要进行前向遍历和在指定位置之后进行插入删除操作的场景。
1
#include <iostream>
2
#include <forward_list>
3
4
int main() {
5
std::forward_list<int> flst; // 声明一个存储 int 类型的 forward_list
6
7
// 插入元素 (forward_list 没有 push_back,只有 push_front)
8
flst.push_front(10); // 头部插入
9
flst.push_front(20); // 头部插入
10
flst.push_front(30); // 头部插入
11
12
// 使用迭代器遍历 forward_list
13
std::cout << "Forward List elements: ";
14
for (std::forward_list<int>::iterator it = flst.begin(); it != flst.end(); ++it) {
15
std::cout << *it << " ";
16
}
17
std::cout << std::endl; // 输出:Forward List elements: 30 20 10
18
19
// 在 forward_list 中间插入元素 (需要在指定位置之前的位置操作)
20
std::forward_list<int>::iterator it_insert = flst.begin();
21
std::advance(it_insert, 1); // 迭代器移动到第 2 个元素位置
22
flst.insert_after(it_insert, 50); // 在第 2 个元素之后插入 50
23
24
std::cout << "Forward List elements after insertion: ";
25
for (int val : flst) {
26
std::cout << val << " ";
27
}
28
std::cout << std::endl; // 输出:Forward List elements after insertion: 30 20 50 10
29
30
return 0;
31
}
⑤ array
(数组):array
是 C++11 标准引入的固定大小的数组容器。与内置数组相比,array
提供了更多的成员函数和类型安全保证,但大小在编译时就确定了,不能动态改变。array
具有以下特点:
⚝ 固定大小 (Fixed Size):array
的大小在声明时就必须确定,且在运行时不能改变。
⚝ 随机访问 (Random Access):array
支持通过索引进行随机访问,访问元素的平均时间复杂度为 \(O(1)\),与内置数组性能相当。
⚝ 栈上分配 (Stack Allocation by Default):array
对象通常在栈上分配内存,与内置数组类似,但也可以作为类成员在堆上分配。
⚝ 提供容器接口 (Container Interface):array
提供了与 STL 容器类似的接口,例如 begin()
, end()
, size()
, empty()
等,可以方便地与 STL 算法和迭代器配合使用。
⚝ 适用场景 (Use Cases):array
适用于需要固定大小数组,且希望利用 STL 容器接口和类型安全特性的场景。在性能要求极高,且数组大小已知的情况下,array
是一个比 vector
更轻量级的选择。
1
#include <iostream>
2
#include <array>
3
4
int main() {
5
std::array<int, 5> arr; // 声明一个存储 5 个 int 元素的 array
6
7
// 初始化 array 元素
8
for (int i = 0; i < arr.size(); ++i) {
9
arr[i] = (i + 1) * 10;
10
}
11
12
// 随机访问元素
13
std::cout << "arr[0]: " << arr[0] << std::endl; // 输出:arr[0]: 10
14
std::cout << "arr[2]: " << arr[2] << std::endl; // 输出:arr[2]: 30
15
std::cout << "arr[4]: " << arr[4] << std::endl; // 输出:arr[4]: 50
16
17
// 遍历 array
18
std::cout << "Array elements: ";
19
for (int i = 0; i < arr.size(); ++i) {
20
std::cout << arr[i] << " ";
21
}
22
std::cout << std::endl; // 输出:Array elements: 10 20 30 40 50
23
24
// 使用范围 for 循环遍历 array
25
std::cout << "Array elements (range-based for loop): ";
26
for (int val : arr) {
27
std::cout << val << " ";
28
}
29
std::cout << std::endl; // 输出:Array elements (range-based for loop): 10 20 30 40 50
30
31
return 0;
32
}
序列容器的选择取决于具体的应用需求。vector
是最常用的通用序列容器,deque
适用于需要在两端进行高效插入删除的场景,list
适用于频繁在任意位置插入删除的场景,forward_list
适用于内存受限的单向链表场景,array
适用于固定大小数组的场景。
14.2.2 关联容器 (Associative Containers)
关联容器存储键值对 (key-value pairs),并允许通过键 (key) 快速访问元素。关联容器通常基于平衡二叉搜索树 (Balanced Binary Search Tree) 或哈希表 (Hash Table) 实现,能够提供高效的查找、插入和删除操作。STL 提供了以下几种关联容器:
① set
(集合):set
是一种有序集合,存储唯一 (unique) 的元素,并自动根据元素的值进行排序。set
通常基于红黑树 (Red-Black Tree) 实现,具有以下特点:
⚝ 有序性 (Ordered):set
中的元素按照从小到大的顺序排列(默认使用 <
运算符进行比较,可以自定义比较函数对象)。
⚝ 唯一性 (Uniqueness):set
中不允许存储重复的元素,如果尝试插入已存在的元素,插入操作会被忽略。
⚝ 高效查找 (Efficient Search):set
提供了高效的查找操作,例如 find()
, count()
, lower_bound()
, upper_bound()
等,查找元素的平均时间复杂度为 \(O(\log n)\),其中 \(n\) 是元素数量。
⚝ 高效插入和删除 (Efficient Insertion and Deletion):在 set
中插入和删除元素的时间复杂度也为 \(O(\log n)\)。
⚝ 适用场景 (Use Cases):set
适用于需要存储唯一元素,并需要快速查找、有序遍历的场景。例如,去重、统计词频、实现字典等。
1
#include <iostream>
2
#include <set>
3
4
int main() {
5
std::set<int> s; // 声明一个存储 int 类型的 set
6
7
// 插入元素 (set 会自动排序和去重)
8
s.insert(30);
9
s.insert(10);
10
s.insert(20);
11
s.insert(10); // 重复插入,会被忽略
12
13
// 遍历 set (元素已排序)
14
std::cout << "Set elements: ";
15
for (int val : s) {
16
std::cout << val << " ";
17
}
18
std::cout << std::endl; // 输出:Set elements: 10 20 30
19
20
// 查找元素
21
if (s.find(20) != s.end()) {
22
std::cout << "20 is found in the set." << std::endl; // 输出:20 is found in the set.
23
} else {
24
std::cout << "20 is not found in the set." << std::endl;
25
}
26
27
if (s.count(10)) { // count 返回元素出现的次数,set 中元素唯一,所以 count 返回 0 或 1
28
std::cout << "10 is in the set." << std::endl; // 输出:10 is in the set.
29
}
30
31
return 0;
32
}
② multiset
(多重集合):multiset
与 set
类似,也是一种有序集合,但允许存储重复 (duplicate) 的元素。multiset
也通常基于红黑树实现,具有以下特点:
⚝ 有序性 (Ordered):multiset
中的元素同样按照从小到大的顺序排列。
⚝ 允许重复 (Duplicates Allowed):multiset
允许存储重复的元素,相同值的元素会相邻存储。
⚝ 高效查找 (Efficient Search):multiset
也提供了高效的查找操作,时间复杂度为 \(O(\log n)\)。
⚝ 高效插入和删除 (Efficient Insertion and Deletion):插入和删除元素的时间复杂度也为 \(O(\log n)\)。
⚝ 适用场景 (Use Cases):multiset
适用于需要存储有序元素集合,且允许重复元素的场景。例如,统计频率、实现计数器等。
1
#include <iostream>
2
#include <set>
3
4
int main() {
5
std::multiset<int> ms; // 声明一个存储 int 类型的 multiset
6
7
// 插入元素 (multiset 允许重复元素)
8
ms.insert(30);
9
ms.insert(10);
10
ms.insert(20);
11
ms.insert(10); // 再次插入 10,允许重复
12
13
// 遍历 multiset (元素已排序,重复元素相邻)
14
std::cout << "Multiset elements: ";
15
for (int val : ms) {
16
std::cout << val << " ";
17
}
18
std::cout << std::endl; // 输出:Multiset elements: 10 10 20 30
19
20
// 查找元素
21
if (ms.find(10) != ms.end()) { // find 返回指向第一个找到的元素的迭代器
22
std::cout << "10 is found in the multiset." << std::endl; // 输出:10 is found in the multiset.
23
}
24
25
std::cout << "Count of 10 in multiset: " << ms.count(10) << std::endl; // 输出:Count of 10 in multiset: 2
26
27
return 0;
28
}
③ map
(映射):map
是一种有序键值对集合,存储 (键, 值) (key-value) 对,并根据键 (key) 进行排序。map
要求键是唯一 (unique) 的,值可以重复。map
通常基于红黑树实现,具有以下特点:
⚝ 有序键 (Ordered Keys):map
中的键按照从小到大的顺序排列(默认使用键的 <
运算符进行比较,可以自定义比较函数对象)。
⚝ 唯一键 (Unique Keys):map
中不允许键重复,如果尝试插入已存在的键,新的键值对会覆盖 (overwrite) 原有的键值对(对于 operator[]
插入)或插入操作会被忽略(对于 insert()
插入)。
⚝ 高效查找 (Efficient Search):map
提供了高效的通过键查找值的操作,例如 find()
, count()
, lower_bound()
, upper_bound()
以及 operator[]
等,查找的平均时间复杂度为 \(O(\log n)\),其中 \(n\) 是键值对数量。
⚝ 高效插入和删除 (Efficient Insertion and Deletion):插入和删除键值对的时间复杂度也为 \(O(\log n)\)。
⚝ 适用场景 (Use Cases):map
适用于需要存储键值对,并需要通过键快速查找值的场景。例如,字典、索引、配置管理等。
1
#include <iostream>
2
#include <map>
3
#include <string>
4
5
int main() {
6
std::map<std::string, int> age_map; // 声明一个键为 string 类型,值为 int 类型的 map
7
8
// 插入键值对
9
age_map["Alice"] = 30; // 使用 operator[] 插入
10
age_map["Bob"] = 25;
11
age_map["Charlie"] = 35;
12
13
// 遍历 map (键已排序)
14
std::cout << "Age map elements: " << std::endl;
15
for (auto const& [name, age] : age_map) { // C++17 结构化绑定,方便遍历 map
16
std::cout << name << ": " << age << std::endl;
17
}
18
// 输出:
19
// Age map elements:
20
// Alice: 30
21
// Bob: 25
22
// Charlie: 35
23
24
// 通过键查找值
25
std::cout << "Age of Bob: " << age_map["Bob"] << std::endl; // 输出:Age of Bob: 25
26
27
// 查找不存在的键,使用 operator[] 会插入新的键值对,值为默认值 (对于 int 是 0)
28
std::cout << "Age of David: " << age_map["David"] << std::endl; // 输出:Age of David: 0
29
std::cout << "Size of map after accessing 'David': " << age_map.size() << std::endl; // 输出:Size of map after accessing 'David': 4
30
31
// 使用 find 查找,不会插入新的键值对
32
if (age_map.find("Eve") != age_map.end()) {
33
std::cout << "Age of Eve: " << age_map["Eve"] << std::endl;
34
} else {
35
std::cout << "Eve is not found in the map." << std::endl; // 输出:Eve is not found in the map.
36
}
37
std::cout << "Size of map after searching 'Eve': " << age_map.size() << std::endl; // 输出:Size of map after searching 'Eve': 4
38
39
return 0;
40
}
④ multimap
(多重映射):multimap
与 map
类似,也是一种有序键值对集合,但允许存储键重复 (duplicate keys) 的键值对。multimap
也通常基于红黑树实现,具有以下特点:
⚝ 有序键 (Ordered Keys):multimap
中的键同样按照从小到大的顺序排列。
⚝ 允许键重复 (Duplicate Keys Allowed):multimap
允许存储键重复的键值对,相同键的键值对会相邻存储。
⚝ 高效查找 (Efficient Search):multimap
也提供了高效的通过键查找值的操作,例如 find()
, count()
, lower_bound()
, upper_bound()
, equal_range()
等,时间复杂度为 \(O(\log n)\)。
⚝ 高效插入和删除 (Efficient Insertion and Deletion):插入和删除键值对的时间复杂度也为 \(O(\log n)\)。
⚝ 适用场景 (Use Cases):multimap
适用于需要存储有序键值对集合,且允许键重复的场景。例如,索引表、日志记录等。
1
#include <iostream>
2
#include <map>
3
#include <string>
4
5
int main() {
6
std::multimap<std::string, int> score_map; // 声明一个键为 string 类型,值为 int 类型的 multimap
7
8
// 插入键值对 (multimap 允许键重复)
9
score_map.insert({"Alice", 90});
10
score_map.insert({"Bob", 85});
11
score_map.insert({"Alice", 95}); // 再次插入 "Alice" 键,允许重复
12
score_map.insert({"Charlie", 92});
13
14
// 遍历 multimap (键已排序,相同键的键值对相邻)
15
std::cout << "Score map elements: " << std::endl;
16
for (auto const& [name, score] : score_map) {
17
std::cout << name << ": " << score << std::endl;
18
}
19
// 输出:
20
// Score map elements:
21
// Alice: 90
22
// Alice: 95
23
// Bob: 85
24
// Charlie: 92
25
26
// 查找特定键的所有值
27
std::cout << "Scores of Alice: " << std::endl;
28
auto range = score_map.equal_range("Alice"); // equal_range 返回一个 pair,包含指向第一个和最后一个匹配元素的迭代器
29
for (auto it = range.first; it != range.second; ++it) {
30
std::cout << it->second << std::endl; // 访问值部分
31
}
32
// 输出:
33
// Scores of Alice:
34
// 90
35
// 95
36
37
std::cout << "Count of 'Alice' key: " << score_map.count("Alice") << std::endl; // 输出:Count of 'Alice' key: 2
38
39
return 0;
40
}
关联容器的选择取决于是否需要有序存储、是否允许重复元素/键、以及对查找效率的要求。set
和 map
存储唯一元素/键,multiset
和 multimap
允许重复元素/键。基于红黑树实现的关联容器提供了有序性和 \(O(\log n)\) 的查找、插入、删除效率。C++11 标准还引入了基于哈希表实现的无序关联容器 (Unordered Associative Containers),例如 unordered_set
, unordered_multiset
, unordered_map
, unordered_multimap
,它们提供了平均 \(O(1)\) 时间复杂度的查找、插入、删除操作,但在最坏情况下可能退化为 \(O(n)\)。无序关联容器将在后续章节中介绍。
14.2.3 容器适配器 (Container Adapters)
容器适配器是基于现有的序列容器构建的,提供了特定的接口,使其表现出栈、队列或优先队列等数据结构的特性。STL 提供了以下几种容器适配器:
① stack
(栈):stack
适配器基于 deque
(默认情况下) 或 vector
或 list
序列容器实现,提供了后进先出 (LIFO, Last-In-First-Out) 的访问接口。stack
具有以下特点:
⚝ LIFO 结构 (LIFO Structure):只能访问栈顶元素,不支持随机访问。
⚝ 基本操作 (Basic Operations):push()
(入栈), pop()
(出栈), top()
(访问栈顶元素), empty()
(判断栈是否为空), size()
(获取栈中元素个数)。
⚝ 默认基于 deque
实现 (Default Implementation Based on deque
):可以使用 deque
, vector
, list
作为底层容器。
⚝ 适用场景 (Use Cases):stack
适用于需要 LIFO 访问模式的场景,例如函数调用栈、表达式求值、回溯算法等。
1
#include <iostream>
2
#include <stack>
3
#include <vector>
4
#include <list>
5
6
int main() {
7
std::stack<int> stk_deque; // 默认基于 deque 的栈
8
std::stack<int, std::vector<int>> stk_vector; // 基于 vector 的栈
9
std::stack<int, std::list<int>> stk_list; // 基于 list 的栈
10
11
// 入栈
12
stk_deque.push(10);
13
stk_deque.push(20);
14
stk_deque.push(30);
15
16
// 访问栈顶元素
17
std::cout << "Top element of stack: " << stk_deque.top() << std::endl; // 输出:Top element of stack: 30
18
19
// 出栈
20
stk_deque.pop();
21
std::cout << "Top element after pop: " << stk_deque.top() << std::endl; // 输出:Top element after pop: 20
22
23
// 栈的大小和判空
24
std::cout << "Size of stack: " << stk_deque.size() << std::endl; // 输出:Size of stack: 2
25
std::cout << "Is stack empty? " << (stk_deque.empty() ? "Yes" : "No") << std::endl; // 输出:Is stack empty? No
26
27
return 0;
28
}
② queue
(队列):queue
适配器基于 deque
(默认情况下) 或 list
序列容器实现,提供了先进先出 (FIFO, First-In-First-Out) 的访问接口。queue
具有以下特点:
⚝ FIFO 结构 (FIFO Structure):只能访问队首和队尾元素,不支持随机访问。
⚝ 基本操作 (Basic Operations):push()
(入队,在队尾插入), pop()
(出队,移除队首元素), front()
(访问队首元素), back()
(访问队尾元素), empty()
(判断队列是否为空), size()
(获取队列中元素个数)。
⚝ 默认基于 deque
实现 (Default Implementation Based on deque
):可以使用 deque
, list
作为底层容器(vector
不适合作为 queue
的底层容器,因为 vector
不支持高效的头部删除操作)。
⚝ 适用场景 (Use Cases):queue
适用于需要 FIFO 访问模式的场景,例如任务队列、消息队列、广度优先搜索等。
1
#include <iostream>
2
#include <queue>
3
#include <list>
4
5
int main() {
6
std::queue<int> que_deque; // 默认基于 deque 的队列
7
std::queue<int, std::list<int>> que_list; // 基于 list 的队列
8
9
// 入队
10
que_deque.push(10);
11
que_deque.push(20);
12
que_deque.push(30);
13
14
// 访问队首和队尾元素
15
std::cout << "Front element of queue: " << que_deque.front() << std::endl; // 输出:Front element of queue: 10
16
std::cout << "Back element of queue: " << que_deque.back() << std::endl; // 输出:Back element of queue: 30
17
18
// 出队
19
que_deque.pop();
20
std::cout << "Front element after pop: " << que_deque.front() << std::endl; // 输出:Front element after pop: 20
21
22
// 队列的大小和判空
23
std::cout << "Size of queue: " << que_deque.size() << std::endl; // 输出:Size of queue: 2
24
std::cout << "Is queue empty? " << (que_deque.empty() ? "Yes" : "No") << std::endl; // 输出:Is queue empty? No
25
26
return 0;
27
}
③ priority_queue
(优先队列):priority_queue
适配器基于 vector
(默认情况下) 或 deque
序列容器实现,提供了优先级队列的访问接口。priority_queue
中的元素按照优先级排序,每次访问队首元素时,都会返回优先级最高的元素(默认是最大值,可以自定义比较函数对象来改变优先级规则)。priority_queue
通常基于堆 (Heap) 数据结构实现,具有以下特点:
⚝ 优先级排序 (Priority Ordering):元素按照优先级排序,优先级最高的元素始终位于队首。
⚝ 基本操作 (Basic Operations):push()
(入队,插入元素并维护堆结构), pop()
(出队,移除队首元素), top()
(访问队首元素,即优先级最高的元素), empty()
(判断优先队列是否为空), size()
(获取优先队列中元素个数)。
⚝ 默认基于 vector
和最大堆实现 (Default Implementation Based on vector
and Max Heap):可以使用 vector
, deque
作为底层容器。默认情况下,priority_queue
使用最大堆 (Max Heap) 实现,即优先级最高的元素是最大值。可以通过自定义比较函数对象来使用最小堆 (Min Heap) 或其他优先级规则。
⚝ 适用场景 (Use Cases):priority_queue
适用于需要按优先级处理元素的场景,例如任务调度、事件处理、Dijkstra 算法、堆排序等。
1
#include <iostream>
2
#include <queue>
3
#include <vector>
4
#include <functional> // std::greater
5
6
int main() {
7
std::priority_queue<int> pq_max; // 默认最大堆优先队列
8
std::priority_queue<int, std::vector<int>, std::greater<int>> pq_min; // 最小堆优先队列 (使用 std::greater<int> 作为比较函数对象)
9
10
// 最大堆优先队列
11
pq_max.push(10);
12
pq_max.push(30);
13
pq_max.push(20);
14
15
std::cout << "Top element of max priority queue: " << pq_max.top() << std::endl; // 输出:Top element of max priority queue: 30
16
pq_max.pop();
17
std::cout << "Top element after pop: " << pq_max.top() << std::endl; // 输出:Top element after pop: 20
18
19
// 最小堆优先队列
20
pq_min.push(10);
21
pq_min.push(30);
22
pq_min.push(20);
23
24
std::cout << "Top element of min priority queue: " << pq_min.top() << std::endl; // 输出:Top element of min priority queue: 10
25
pq_min.pop();
26
std::cout << "Top element after pop: " << pq_min.top() << std::endl; // 输出:Top element after pop: 20
27
28
return 0;
29
}
容器适配器通过限制底层容器的接口,提供了更高级、更专门化的数据结构。stack
提供了 LIFO 访问模式,queue
提供了 FIFO 访问模式,priority_queue
提供了优先级访问模式。容器适配器使得程序员可以更方便地使用这些常用的数据结构,而无需从零开始实现。
14.3 迭代器 (Iterators)
迭代器是 STL 中用于遍历容器中元素的通用接口。迭代器类似于指针,但功能更加强大和安全。通过迭代器,算法可以独立于容器的类型来访问和操作容器中的元素。迭代器提供了一种统一的方式来访问不同容器中的元素,是 STL 泛型编程的核心概念之一。
14.3.1 迭代器的概念和作用 (Concept and Role of Iterators)
迭代器本质上是一种抽象的指针 (Abstract Pointer)。它封装了访问容器中元素的方式,使得算法可以以统一的方式处理不同类型的容器。迭代器主要具有以下作用:
① 提供统一的访问接口 (Provide a Unified Access Interface):不同的容器内部数据结构可能不同(例如,vector
是连续存储,list
是链式存储),访问元素的方式也不同。迭代器抽象了这些差异,为所有容器提供了一套统一的访问接口。算法只需要通过迭代器来访问容器中的元素,而无需关心容器的具体类型和内部实现。
② 连接算法和容器 (Connect Algorithms and Containers):STL 算法不直接操作容器,而是通过迭代器来间接操作容器中的元素。迭代器充当了算法和容器之间的桥梁,使得算法可以独立于容器类型而工作。只要容器提供了符合要求的迭代器,算法就可以应用于该容器。
③ 支持多种遍历方式 (Support Multiple Traversal Methods):迭代器支持不同的遍历方式,例如前向遍历、双向遍历、随机访问等。不同类型的迭代器支持不同的操作,以满足不同算法的需求。
14.3.2 迭代器的类型 (Types of Iterators)
STL 定义了五种类型的迭代器,按照功能由弱到强依次为:
① 输入迭代器 (Input Iterator):输入迭代器是最基本的迭代器类型,只支持单向读取容器中的元素。输入迭代器支持以下操作:
⚝ *it
(解引用,读取迭代器指向的元素值,只读访问)
⚝ ++it
, it++
(前缀和后缀递增,迭代器向前移动到下一个位置)
⚝ it1 == it2
, it1 != it2
(比较两个迭代器是否相等或不等)
输入迭代器只能用于单趟遍历 (Single-Pass Traversal),即只能从头到尾遍历一次容器,不能重复遍历或反向遍历。例如,用于读取输入流的迭代器就是输入迭代器。
② 输出迭代器 (Output Iterator):输出迭代器也只支持单向写入容器中的元素。输出迭代器支持以下操作:
⚝ *it = value
(解引用赋值,将值写入迭代器指向的位置,只写访问)
⚝ ++it
, it++
(前缀和后缀递增,迭代器向前移动到下一个位置)
输出迭代器也只能用于单趟遍历,只能从头到尾写入一次容器,不能重复写入或反向写入。例如,用于写入输出流的迭代器就是输出迭代器。
③ 前向迭代器 (Forward Iterator):前向迭代器继承了输入迭代器和输出迭代器的所有功能,并且支持多次遍历 (Multi-Pass Traversal)。前向迭代器可以多次从头到尾遍历容器,并保持迭代器的状态。前向迭代器支持以下操作:
⚝ 输入迭代器和输出迭代器的所有操作
⚝ 可以多次解引用同一个迭代器,读取或写入元素值
forward_list
, unordered_set
, unordered_multiset
, unordered_map
, unordered_multimap
等容器提供前向迭代器。
④ 双向迭代器 (Bidirectional Iterator):双向迭代器在前向迭代器的基础上,增加了反向遍历 (Bidirectional Traversal) 的能力。双向迭代器支持以下操作:
⚝ 前向迭代器的所有操作
⚝ --it
, it--
(前缀和后缀递减,迭代器向后移动到前一个位置)
list
, set
, multiset
, map
, multimap
等容器提供双向迭代器。
⑤ 随机访问迭代器 (Random Access Iterator):随机访问迭代器是功能最强大的迭代器类型,在前向迭代器和双向迭代器的基础上,增加了随机访问 (Random Access) 的能力。随机访问迭代器支持以下操作:
⚝ 双向迭代器的所有操作
⚝ it += n
, it -= n
, it + n
, it - n
(迭代器向前或向后移动 n 个位置)
⚝ it[n]
(索引访问,访问迭代器当前位置之后第 n 个位置的元素)
⚝ it1 - it2
(计算两个迭代器之间的距离)
⚝ it1 < it2
, it1 <= it2
, it1 > it2
, it1 >= it2
(比较两个迭代器的大小关系)
vector
, deque
, array
, string
等容器提供随机访问迭代器。
各种迭代器类型的功能关系可以用下图表示:
1
Input Iterator --> Forward Iterator --> Bidirectional Iterator --> Random Access Iterator
2
Output Iterator ----^
功能更强的迭代器类型兼容功能较弱的迭代器类型。例如,随机访问迭代器可以当作双向迭代器、前向迭代器、输入迭代器或输出迭代器使用。算法会根据迭代器类型来选择最优的操作方式。例如,对于随机访问迭代器,排序算法可以使用快速排序等高效算法;对于双向迭代器或前向迭代器,可能只能使用归并排序等算法。
14.3.3 迭代器的使用 (Usage of Iterators)
每个 STL 容器都定义了自身的迭代器类型,通常通过容器的 begin()
和 end()
成员函数获取迭代器。begin()
返回指向容器第一个元素的迭代器,end()
返回指向容器尾后 (past-the-end) 位置的迭代器,即最后一个元素的下一个位置。迭代器通常与循环结合使用,遍历容器中的元素。
以下是一些迭代器的常用操作示例:
① 遍历 vector
(使用随机访问迭代器)
1
#include <iostream>
2
#include <vector>
3
4
int main() {
5
std::vector<int> vec = {10, 20, 30, 40, 50};
6
7
// 使用迭代器遍历 vector
8
std::cout << "Vector elements (using iterator): ";
9
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
10
std::cout << *it << " ";
11
}
12
std::cout << std::endl; // 输出:Vector elements (using iterator): 10 20 30 40 50
13
14
// 使用 const_iterator 遍历 const vector 或只读访问
15
std::cout << "Vector elements (using const_iterator): ";
16
for (std::vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it) {
17
std::cout << *it << " "; // 只能读取,不能修改 *it 的值
18
}
19
std::cout << std::endl; // 输出:Vector elements (using const_iterator): 10 20 30 40 50
20
21
// 使用 auto 关键字简化迭代器类型声明 (C++11)
22
std::cout << "Vector elements (using auto iterator): ";
23
for (auto it = vec.begin(); it != vec.end(); ++it) {
24
std::cout << *it << " ";
25
}
26
std::cout << std::endl; // 输出:Vector elements (using auto iterator): 10 20 30 40 50
27
28
// 使用范围 for 循环遍历 (C++11,底层也是使用迭代器)
29
std::cout << "Vector elements (using range-based for loop): ";
30
for (int val : vec) {
31
std::cout << val << " ";
32
}
33
std::cout << std::endl; // 输出:Vector elements (using range-based for loop): 10 20 30 40 50
34
35
return 0;
36
}
② 遍历 list
(使用双向迭代器)
1
#include <iostream>
2
#include <list>
3
4
int main() {
5
std::list<int> lst = {10, 20, 30, 40, 50};
6
7
// 使用迭代器遍历 list
8
std::cout << "List elements (using iterator): ";
9
for (std::list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
10
std::cout << *it << " ";
11
}
12
std::cout << std::endl; // 输出:List elements (using iterator): 10 20 30 40 50
13
14
// 反向遍历 list (使用 reverse_iterator 和 rbegin(), rend())
15
std::cout << "List elements in reverse order: ";
16
for (std::list<int>::reverse_iterator rit = lst.rbegin(); rit != lst.rend(); ++rit) {
17
std::cout << *rit << " ";
18
}
19
std::cout << std::endl; // 输出:List elements in reverse order: 50 40 30 20 10
20
21
return 0;
22
}
③ 遍历 map
(使用双向迭代器)
1
#include <iostream>
2
#include <map>
3
#include <string>
4
5
int main() {
6
std::map<std::string, int> age_map = {
7
{"Alice", 30},
8
{"Bob", 25},
9
{"Charlie", 35}
10
};
11
12
// 使用迭代器遍历 map
13
std::cout << "Age map elements (using iterator): " << std::endl;
14
for (std::map<std::string, int>::iterator it = age_map.begin(); it != age_map.end(); ++it) {
15
std::cout << it->first << ": " << it->second << std::endl; // it->first 获取键,it->second 获取值
16
}
17
// 输出:
18
// Age map elements (using iterator):
19
// Alice: 30
20
// Bob: 25
21
// Charlie: 35
22
23
// 使用 auto 和结构化绑定遍历 map (C++17)
24
std::cout << "Age map elements (using auto and structured binding): " << std::endl;
25
for (auto const& [name, age] : age_map) {
26
std::cout << name << ": " << age << std::endl;
27
}
28
// 输出同上
29
30
return 0;
31
}
迭代器是 STL 算法与容器交互的桥梁,掌握迭代器的使用是理解和使用 STL 的关键。在实际编程中,应根据容器类型和算法需求选择合适的迭代器类型,并熟练运用迭代器的各种操作。
14.4 算法 (Algorithms)
算法是 STL 的重要组成部分,提供了一系列通用的函数模板,用于执行各种数据处理操作,例如排序、搜索、拷贝、转换、数值计算等。STL 算法独立于容器类型,通过迭代器来操作容器中的元素,实现了算法与容器的解耦,体现了泛型编程的思想。
14.4.1 STL 算法的分类 (Classification of STL Algorithms)
STL 算法库提供了大量的算法,可以根据功能进行分类,常见的分类方式包括:
① 非修改性序列操作 (Non-modifying Sequence Operations):这类算法不修改容器中元素的顺序或值,只进行读取操作。例如:
⚝ for_each()
:对序列中的每个元素执行指定的操作。
⚝ find()
, find_if()
, find_if_not()
:在序列中查找特定元素或满足条件的元素。
⚝ count()
, count_if()
:统计序列中特定元素或满足条件的元素的个数。
⚝ equal()
, mismatch()
:比较两个序列是否相等或找出第一个不匹配的位置。
⚝ all_of()
, any_of()
, none_of()
:检查序列中是否所有、任意或没有元素满足特定条件。
⚝ search()
, find_end()
, find_first_of()
, adjacent_find()
:在序列中搜索子序列或特定模式。
② 修改性序列操作 (Modifying Sequence Operations):这类算法修改容器中元素的顺序或值。例如:
⚝ copy()
, copy_backward()
:复制序列中的元素到另一个位置。
⚝ move()
, move_backward()
:移动序列中的元素到另一个位置(用于移动语义,提高效率)。
⚝ transform()
:对序列中的每个元素应用指定的操作,并将结果存储到另一个位置或原位置。
⚝ replace()
, replace_if()
, replace_copy()
, replace_copy_if()
:替换序列中的特定元素或满足条件的元素。
⚝ fill()
, fill_n()
:用指定的值填充序列。
⚝ generate()
, generate_n()
:用生成器函数生成的值填充序列。
⚝ remove()
, remove_if()
, remove_copy()
, remove_copy_if()
:移除序列中的特定元素或满足条件的元素(实际上是将不移除的元素前移,返回新逻辑结尾的迭代器,需要配合 erase()
成员函数真正删除元素)。
⚝ unique()
, unique_copy()
:移除序列中相邻的重复元素(需要序列已排序,同样返回新逻辑结尾迭代器)。
⚝ reverse()
, reverse_copy()
:反转序列中的元素顺序。
⚝ rotate()
, rotate_copy()
:循环旋转序列中的元素。
⚝ random_shuffle()
, shuffle()
:随机打乱序列中的元素顺序。
⚝ partition()
, stable_partition()
:根据条件将序列划分为两个部分。
③ 排序算法 (Sorting Algorithms):这类算法用于对序列中的元素进行排序。例如:
⚝ sort()
, stable_sort()
:对序列进行排序,stable_sort()
保持相等元素的相对顺序。
⚝ partial_sort()
, partial_sort_copy()
:对序列进行部分排序,只排序前 k 个元素。
⚝ nth_element()
:找到序列中第 n 小(或第 n 大)的元素,并将该元素放到正确的位置上。
⚝ is_sorted()
, is_sorted_until()
:检查序列是否已排序或找到第一个未排序的位置。
④ 二分搜索算法 (Binary Search Algorithms):这类算法用于在已排序的序列中进行高效的搜索。例如:
⚝ binary_search()
:判断序列中是否包含特定元素。
⚝ lower_bound()
:在序列中查找第一个不小于(大于等于)给定值的元素的位置。
⚝ upper_bound()
:在序列中查找第一个大于给定值的元素的位置。
⚝ equal_range()
:在序列中查找与给定值相等的元素组成的区间。
⑤ 堆算法 (Heap Algorithms):这类算法用于操作堆 (heap) 数据结构。例如:
⚝ make_heap()
:将序列构建成堆。
⚝ push_heap()
:向堆中插入元素。
⚝ pop_heap()
:从堆中移除堆顶元素(最大值或最小值)。
⚝ sort_heap()
:将堆排序成有序序列。
⚝ is_heap()
, is_heap_until()
:检查序列是否是堆或找到第一个不满足堆性质的位置。
⑥ 数值算法 (Numeric Algorithms):这类算法用于进行数值计算。例如:
⚝ accumulate()
:计算序列中元素的累加和。
⚝ inner_product()
:计算两个序列的内积。
⚝ partial_sum()
:计算序列的部分和 (partial sums)。
⚝ adjacent_difference()
:计算序列中相邻元素的差值。
⚝ iota()
:生成一个递增的数值序列。
⑦ 生成器和变异算法 (Generating and Mutating Algorithms):这类算法用于生成新的序列或对现有序列进行变异操作。例如:
⚝ generate()
, generate_n()
:使用生成器函数生成值填充序列。
⚝ transform()
:对序列元素进行转换。
⚝ replace()
, remove()
, unique()
等修改性序列操作也可以看作是变异算法。
⑧ 关系算法 (Relational Algorithms):这类算法用于比较两个序列的关系。例如:
⚝ equal()
, mismatch()
:比较两个序列是否相等或找出第一个不匹配的位置。
⚝ lexicographical_compare()
:按字典序比较两个序列的大小。
⑨ 集合算法 (Set Algorithms):这类算法用于进行集合操作,要求序列已排序。例如:
⚝ includes()
:判断一个序列是否是另一个序列的子集。
⚝ set_union()
:计算两个集合的并集。
⚝ set_intersection()
:计算两个集合的交集。
⚝ set_difference()
:计算两个集合的差集。
⚝ set_symmetric_difference()
:计算两个集合的对称差集。
14.4.2 常用 STL 算法示例 (Examples of Common STL Algorithms)
以下是一些常用 STL 算法的使用示例:
① 排序算法 sort()
(Sorting Algorithm sort()
)
sort()
算法用于对序列进行升序排序(默认使用 <
运算符进行比较,可以自定义比较函数对象)。sort()
通常使用内省式快速排序 (Introsort) 算法,平均时间复杂度为 \(O(n \log n)\),最坏情况时间复杂度也为 \(O(n \log n)\)。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm> // 包含 sort 算法
4
5
int main() {
6
std::vector<int> vec = {30, 10, 50, 20, 40};
7
8
std::cout << "Before sort: ";
9
for (int val : vec) {
10
std::cout << val << " ";
11
}
12
std::cout << std::endl; // 输出:Before sort: 30 10 50 20 40
13
14
std::sort(vec.begin(), vec.end()); // 使用 sort 算法对 vector 排序
15
16
std::cout << "After sort: ";
17
for (int val : vec) {
18
std::cout << val << " ";
19
}
20
std::cout << std::endl; // 输出:After sort: 10 20 30 40 50
21
22
// 自定义比较函数对象,实现降序排序
23
std::sort(vec.begin(), vec.end(), std::greater<int>()); // 使用 std::greater<int> 函数对象
24
25
std::cout << "After sort (descending order): ";
26
for (int val : vec) {
27
std::cout << val << " ";
28
}
29
std::cout << std::endl; // 输出:After sort (descending order): 50 40 30 20 10
30
31
return 0;
32
}
② 查找算法 find()
(Searching Algorithm find()
)
find()
算法用于在序列中查找第一个与给定值相等的元素。如果找到,返回指向该元素的迭代器;如果未找到,返回尾后迭代器 end()
。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm> // 包含 find 算法
4
5
int main() {
6
std::vector<int> vec = {10, 20, 30, 40, 50};
7
8
int target = 30;
9
std::vector<int>::iterator it = std::find(vec.begin(), vec.end(), target); // 使用 find 算法查找元素 30
10
11
if (it != vec.end()) {
12
std::cout << target << " is found at position: " << std::distance(vec.begin(), it) << std::endl; // 输出:30 is found at position: 2
13
} else {
14
std::cout << target << " is not found in the vector." << std::endl;
15
}
16
17
target = 60;
18
it = std::find(vec.begin(), vec.end(), target); // 查找元素 60
19
if (it != vec.end()) {
20
std::cout << target << " is found." << std::endl;
21
} else {
22
std::cout << target << " is not found in the vector." << std::endl; // 输出:60 is not found in the vector.
23
}
24
25
return 0;
26
}
③ 拷贝算法 copy()
(Copying Algorithm copy()
)
copy()
算法用于将一个序列的元素复制到另一个序列的起始位置。目标序列需要有足够的空间容纳复制的元素。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm> // 包含 copy 算法
4
5
int main() {
6
std::vector<int> source = {10, 20, 30, 40, 50};
7
std::vector<int> destination(source.size()); // 目标 vector 需要预先分配空间
8
9
std::copy(source.begin(), source.end(), destination.begin()); // 使用 copy 算法复制元素
10
11
std::cout << "Source vector: ";
12
for (int val : source) {
13
std::cout << val << " ";
14
}
15
std::cout << std::endl; // 输出:Source vector: 10 20 30 40 50
16
17
std::cout << "Destination vector (after copy): ";
18
for (int val : destination) {
19
std::cout << val << " ";
20
}
21
std::cout << std::endl; // 输出:Destination vector (after copy): 10 20 30 40 50
22
23
return 0;
24
}
④ 数值算法 accumulate()
(Numeric Algorithm accumulate()
)
accumulate()
算法用于计算序列中元素的累加和。可以指定初始值,也可以自定义累加操作(默认使用加法)。
1
#include <iostream>
2
#include <vector>
3
#include <numeric> // 包含 accumulate 算法
4
5
int main() {
6
std::vector<int> vec = {1, 2, 3, 4, 5};
7
8
int sum = std::accumulate(vec.begin(), vec.end(), 0); // 计算累加和,初始值为 0
9
10
std::cout << "Sum of vector elements: " << sum << std::endl; // 输出:Sum of vector elements: 15
11
12
// 自定义累加操作,计算乘积
13
int product = std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>()); // 初始值为 1,使用 std::multiplies<int> 函数对象进行乘法累积
14
15
std::cout << "Product of vector elements: " << product << std::endl; // 输出:Product of vector elements: 120
16
17
return 0;
18
}
STL 算法库提供了丰富的算法,可以满足各种常见的算法需求。熟练掌握 STL 算法的使用,可以大大提高 C++ 程序开发的效率和代码质量。在实际编程中,应优先考虑使用 STL 提供的算法,而不是自己手动实现,以充分利用 STL 的优势。
14.5 函数对象 (Function Objects)
函数对象,也称为仿函数 (Functors),是行为类似于函数的对象。在 C++ 中,任何重载了函数调用运算符 operator()
的类对象都可以被视为函数对象。STL 中广泛使用函数对象来实现算法的自定义操作,例如自定义排序规则、自定义谓词条件等。函数对象比普通函数更加灵活,因为它们可以携带状态。
14.5.1 函数对象的概念和优势 (Concept and Advantages of Function Objects)
函数对象本质上是一个类 (Class) 的实例,但它具有函数 (Function) 的行为。通过重载函数调用运算符 operator()
,使得类对象可以像函数一样被调用。函数对象的主要优势在于:
① 可以携带状态 (Stateful):函数对象可以像普通对象一样拥有成员变量,用于存储状态信息。每次函数调用可以根据当前状态执行不同的操作。普通函数则不具备状态,每次调用行为都是固定的。
② 类型安全 (Type Safety):函数对象是对象,具有明确的类型,可以在编译时进行类型检查,提高了类型安全性。函数指针的类型检查相对较弱。
③ 可以作为模板参数 (Template Parameters):函数对象可以作为模板参数传递给算法,实现更灵活的定制。普通函数指针则不能直接作为模板参数。
④ 通常比函数指针更高效 (Potentially More Efficient than Function Pointers):编译器可以对函数对象进行内联优化,提高性能。函数指针的调用通常会产生额外的开销,内联优化的机会较少。
14.5.2 STL 预定义的函数对象 (Predefined Function Objects in STL)
STL 提供了大量的预定义函数对象,定义在 <functional>
头文件中,可以满足各种常见的操作需求。STL 预定义的函数对象主要分为以下几类:
① 算术运算函数对象 (Arithmetic Function Objects):用于执行基本的算术运算,例如:
⚝ std::plus<T>
:加法
⚝ std::minus<T>
:减法
⚝ std::multiplies<T>
:乘法
⚝ std::divides<T>
:除法
⚝ std::modulus<T>
:取模
⚝ std::negate<T>
:取反
② 比较运算函数对象 (Comparison Function Objects):用于执行比较运算,例如:
⚝ std::equal_to<T>
:等于
⚝ std::not_equal_to<T>
:不等于
⚝ std::greater<T>
:大于
⚝ std::less<T>
:小于
⚝ std::greater_equal<T>
:大于等于
⚝ std::less_equal<T>
:小于等于
③ 逻辑运算函数对象 (Logical Function Objects):用于执行逻辑运算,例如:
⚝ std::logical_and<T>
:逻辑与
⚝ std::logical_or<T>
:逻辑或
⚝ std::logical_not<T>
:逻辑非
④ 位运算函数对象 (Bitwise Function Objects) (C++11):用于执行位运算,例如:
⚝ std::bit_and<T>
:按位与
⚝ std::bit_or<T>
:按位或
⚝ std::bit_xor<T>
:按位异或
⚝ std::bit_not<T>
:按位取反 (C++20)
这些预定义函数对象都是类模板,可以根据需要指定模板参数类型 T
。例如,std::plus<int>()
表示一个执行整数加法的函数对象。
以下是一些使用预定义函数对象的示例:
① 使用 std::greater<int>()
进行降序排序
1
#include <iostream>
2
#include <vector>
3
#include <algorithm>
4
#include <functional> // 包含 std::greater
5
6
int main() {
7
std::vector<int> vec = {30, 10, 50, 20, 40};
8
9
std::sort(vec.begin(), vec.end(), std::greater<int>()); // 使用 std::greater<int>() 函数对象进行降序排序
10
11
std::cout << "Vector elements (descending order): ";
12
for (int val : vec) {
13
std::cout << val << " ";
14
}
15
std::cout << std::endl; // 输出:Vector elements (descending order): 50 40 30 20 10
16
17
return 0;
18
}
② 使用 std::plus<int>()
计算累加和
1
#include <iostream>
2
#include <vector>
3
#include <numeric>
4
#include <functional> // 包含 std::plus
5
6
int main() {
7
std::vector<int> vec = {1, 2, 3, 4, 5};
8
9
int sum = std::accumulate(vec.begin(), vec.end(), 0, std::plus<int>()); // 使用 std::plus<int>() 函数对象进行加法累积
10
11
std::cout << "Sum of vector elements: " << sum << std::endl; // 输出:Sum of vector elements: 15
12
13
return 0;
14
}
③ 使用 std::logical_not<bool>()
和 std::not1()
进行逻辑非操作
1
#include <iostream>
2
#include <vector>
3
#include <algorithm>
4
#include <functional> // 包含 std::logical_not, std::not1
5
6
int main() {
7
std::vector<bool> bool_vec = {true, false, true, false, true};
8
std::vector<bool> not_bool_vec(bool_vec.size());
9
10
// 使用 std::transform 和 std::logical_not<bool>() 进行逻辑非转换
11
std::transform(bool_vec.begin(), bool_vec.end(), not_bool_vec.begin(), std::logical_not<bool>());
12
13
std::cout << "Original bool vector: ";
14
for (bool val : bool_vec) {
15
std::cout << std::boolalpha << val << " ";
16
}
17
std::cout << std::endl; // 输出:Original bool vector: true false true false true
18
19
std::cout << "Not bool vector (using logical_not): ";
20
for (bool val : not_bool_vec) {
21
std::cout << std::boolalpha << val << " ";
22
}
23
std::cout << std::endl; // 输出:Not bool vector (using logical_not): false true false true false
24
25
// 使用 std::not1 和 lambda 表达式进行逻辑非转换 (lambda 表达式将在后续介绍)
26
std::transform(bool_vec.begin(), bool_vec.end(), not_bool_vec.begin(), std::not1(std::logical_and<bool>())); // 错误用法示例,std::not1 只能用于一元谓词
27
28
return 0;
29
}
STL 预定义的函数对象为常见的运算和比较操作提供了方便的工具,可以直接使用,无需手动编写简单的函数对象类。
14.5.3 自定义函数对象 (Custom Function Objects)
除了使用 STL 预定义的函数对象外,用户还可以根据需要自定义函数对象类,以实现更复杂或特定的操作。自定义函数对象类需要重载函数调用运算符 operator()
。
以下是一个自定义函数对象的示例:
① 自定义比较函数对象,忽略大小写比较字符串
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <algorithm>
5
#include <cctype> // std::tolower
6
7
// 自定义函数对象类,忽略大小写比较字符串
8
class CaseInsensitiveCompare {
9
public:
10
bool operator()(const std::string& str1, const std::string& str2) const {
11
std::string lower_str1 = str1;
12
std::string lower_str2 = str2;
13
std::transform(lower_str1.begin(), lower_str1.end(), lower_str1.begin(), ::tolower); // 转换为小写
14
std::transform(lower_str2.begin(), lower_str2.end(), lower_str2.begin(), ::tolower); // 转换为小写
15
return lower_str1 < lower_str2; // 比较小写字符串
16
}
17
};
18
19
int main() {
20
std::vector<std::string> str_vec = {"Apple", "banana", "Orange", "grape"};
21
22
std::cout << "Before sort: ";
23
for (const std::string& str : str_vec) {
24
std::cout << str << " ";
25
}
26
std::cout << std::endl; // 输出:Before sort: Apple banana Orange grape
27
28
// 使用自定义函数对象进行排序
29
std::sort(str_vec.begin(), str_vec.end(), CaseInsensitiveCompare()); // 使用 CaseInsensitiveCompare 函数对象
30
31
std::cout << "After sort (case-insensitive): ";
32
for (const std::string& str : str_vec) {
33
std::cout << str << " ";
34
}
35
std::cout << std::endl; // 输出:After sort (case-insensitive): Apple banana grape Orange
36
37
return 0;
38
}
② 自定义函数对象,统计字符串长度大于指定值的个数
1
#include <iostream>
2
#include <string>
3
#include <vector>
4
#include <algorithm>
5
6
// 自定义函数对象类,统计字符串长度大于指定值的个数
7
class StringLengthCounter {
8
private:
9
size_t min_length;
10
size_t count;
11
12
public:
13
StringLengthCounter(size_t length) : min_length(length), count(0) {}
14
15
void operator()(const std::string& str) {
16
if (str.length() > min_length) {
17
++count; // 统计长度大于 min_length 的字符串个数
18
}
19
}
20
21
size_t getCount() const {
22
return count;
23
}
24
};
25
26
int main() {
27
std::vector<std::string> str_vec = {"apple", "banana", "kiwi", "orange", "grape"};
28
size_t min_len = 5;
29
30
StringLengthCounter counter(min_len); // 创建 StringLengthCounter 函数对象,最小长度为 5
31
32
std::for_each(str_vec.begin(), str_vec.end(), counter); // 使用 for_each 算法和函数对象进行统计
33
34
std::cout << "Number of strings with length greater than " << min_len << ": " << counter.getCount() << std::endl; // 输出:Number of strings with length greater than 5: 2
35
36
return 0;
37
}
自定义函数对象提供了更强大的定制能力,可以根据具体需求实现各种复杂的操作。函数对象可以携带状态,可以作为模板参数,可以进行内联优化,相比普通函数和函数指针具有诸多优势,是 STL 泛型编程的重要组成部分。C++11 标准引入的 lambda 表达式 (Lambda Expressions) 提供了一种更简洁的方式来创建函数对象,将在后续章节中介绍。
15. 现代C++特性:C++11/14/17/20 新特性概览
15.1 C++11 核心特性
15.1.1 auto
类型推导 (Type Deduction)
概要: auto
关键字允许编译器自动推导变量的类型,简化代码并提高代码的可读性和维护性。
深度解析:
① 类型推导规则: auto
声明的变量类型,由初始化表达式的类型决定。编译器会根据初始化表达式的类型,自动推导出 auto
变量的实际类型。
② 简化复杂类型声明: 对于复杂的类型,例如迭代器 (iterators)、Lambda 表达式的返回类型或模板表达式的类型,使用 auto
可以避免冗长和容易出错的类型声明。
1
// 传统方式:类型声明繁琐
2
std::vector<int> numbers = {1, 2, 3, 4, 5};
3
std::vector<int>::iterator it = numbers.begin();
4
5
// 使用 auto:类型声明简洁
6
auto numbers = std::vector<int>{1, 2, 3, 4, 5};
7
auto it = numbers.begin();
③ 与模板和泛型编程 (Generic Programming) 结合: 在模板编程中,auto
尤其有用,因为模板参数的类型可能在编译时才能确定。auto
可以方便地处理各种模板类型。
1
template <typename T1, typename T2>
2
auto add(T1 a, T2 b) -> decltype(a + b) { // 使用 decltype 确保返回类型正确
3
return a + b;
4
}
5
6
int main() {
7
auto result1 = add(10, 20); // result1 被推导为 int
8
auto result2 = add(3.14, 2); // result2 被推导为 double
9
return 0;
10
}
④ 注意事项:
▮▮▮▮⚝ auto
必须初始化:auto
声明的变量必须在声明时初始化,因为类型推导依赖于初始化表达式。
▮▮▮▮⚝ auto
不会进行类型转换:auto
推导出的类型是精确的类型,不会发生隐式类型转换。
▮▮▮▮⚝ auto
的推导结果可能不是引用类型:如果初始化表达式是引用类型,auto
推导出的类型会是非引用类型。如果需要推导为引用类型,需要使用 auto&
或 auto&&
。
1
int x = 10;
2
int& ref_x = x;
3
auto y = ref_x; // y 的类型是 int (非引用)
4
auto& z = ref_x; // z 的类型是 int& (引用)
⑤ 最佳实践: 在类型显而易见或类型名称非常冗长时,优先使用 auto
,以提高代码的清晰度和简洁性。但对于需要强调变量类型的场合,或者类型不明确时,显式声明类型可能更佳。
15.1.2 范围 for
循环 (Range-based for loop)
概要: 范围 for
循环提供了一种简洁、易读的方式来遍历容器 (containers) 和数组 (arrays) 中的元素,避免了传统 for
循环中索引和迭代器的复杂性。
深度解析:
① 语法结构: 范围 for
循环的语法形式为:
1
for (declaration : range) {
2
// 循环体
3
}
▮▮▮▮⚝ declaration
: 声明一个变量,用于接收 range
中每个元素的副本或引用。
▮▮▮▮⚝ range
: 表示要遍历的范围,可以是容器、数组、或任何支持迭代器的对象。
② 遍历容器: 可以方便地遍历标准库容器,例如 std::vector
, std::list
, std::set
, std::map
等。
1
std::vector<int> numbers = {1, 2, 3, 4, 5};
2
3
// 遍历容器元素并打印
4
for (int number : numbers) { // 值拷贝
5
std::cout << number << " ";
6
}
7
std::cout << std::endl;
8
9
// 使用引用避免拷贝,并修改元素
10
for (int& number : numbers) { // 引用
11
number *= 2;
12
}
13
14
// 遍历修改后的容器
15
for (const auto& number : numbers) { // 常量引用,避免拷贝,只读
16
std::cout << number << " ";
17
}
18
std::cout << std::endl;
③ 遍历数组: 同样适用于遍历 C 风格的数组。
1
int array[] = {10, 20, 30, 40, 50};
2
3
for (int element : array) {
4
std::cout << element << " ";
5
}
6
std::cout << std::endl;
④ 自定义范围: 范围 for
循环可以用于任何支持 begin()
和 end()
函数(返回迭代器)的对象,或者具有 begin()
和 end()
自由函数 (free functions) 的对象。这允许用户自定义范围 for
循环的行为。
⑤ 迭代器要求: 范围 for
循环依赖于迭代器 (iterators) 的支持。被遍历的范围需要提供符合要求的迭代器,通常是指向元素开始和结束位置的迭代器。
⑥ 与 auto
结合: 通常与 auto
关键字结合使用,以简化变量声明,尤其是在遍历容器时,可以自动推导元素类型。
1
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
2
for (const auto& pair : ages) {
3
std::cout << pair.first << ": " << pair.second << std::endl;
4
}
⑦ 优势:
▮▮▮▮⚝ 代码简洁: 相比传统的 for
循环,代码更简洁易读,减少了循环控制代码的编写。
▮▮▮▮⚝ 安全性提高: 避免了手动管理索引或迭代器可能导致的越界访问等错误。
▮▮▮▮⚝ 更专注于业务逻辑: 开发者可以更专注于循环体内的业务逻辑,而不是循环控制的细节。
15.1.3 Lambda 表达式 (Lambda Expressions)
概要: Lambda 表达式允许在代码中定义匿名函数 (anonymous functions),可以方便地创建函数对象 (function objects),用于算法 (algorithms)、回调函数 (callback functions) 等场景。
深度解析:
① 语法结构: Lambda 表达式的基本语法结构如下:
1
[capture list](parameter list) -> return type {
2
// 函数体
3
}
▮▮▮▮⚝ capture list
(捕获列表): 指定 Lambda 表达式可以访问的外部变量。
▮▮▮▮⚝ parameter list
(参数列表): Lambda 表达式的参数列表,类似于普通函数的参数列表。
▮▮▮▮⚝ return type
(返回类型): Lambda 表达式的返回类型。可以显式指定,也可以由编译器自动推导 (trailing return type syntax)。
▮▮▮▮⚝ function body
(函数体): Lambda 表达式的函数体,包含具体的执行代码。
② 捕获列表 (Capture List): 捕获列表决定了 Lambda 表达式如何访问外部作用域的变量。
▮▮▮▮⚝ []
: 不捕获任何外部变量。Lambda 表达式体内部不能访问外部变量。
▮▮▮▮⚝ [=]
: 值捕获 (capture by value) 所有外部变量。Lambda 表达式体内部可以访问外部变量的副本,但不能修改它们。
▮▮▮▮⚝ [&]
: 引用捕获 (capture by reference) 所有外部变量。Lambda 表达式体内部可以访问和修改外部变量。
▮▮▮▮⚝ [var]
: 值捕获指定的外部变量 var
。
▮▮▮▮⚝ [&var]
: 引用捕获指定的外部变量 var
。
▮▮▮▮⚝ [=, &var]
: 值捕获所有外部变量,但引用捕获变量 var
。
▮▮▮▮⚝ [&, var]
: 引用捕获所有外部变量,但值捕获变量 var
。
1
int x = 10;
2
int y = 20;
3
4
auto lambda1 = []() { std::cout << "Hello from lambda1" << std::endl; }; // 不捕获任何变量
5
auto lambda2 = [=]() { std::cout << "x = " << x << ", y = " << y << std::endl; }; // 值捕获 x 和 y
6
auto lambda3 = [&]() { x++; y++; std::cout << "x = " << x << ", y = " << y << std::endl; }; // 引用捕获 x 和 y
7
8
lambda1();
9
lambda2(); // 输出 x = 10, y = 20
10
lambda3(); // 输出 x = 11, y = 21 (并修改了外部 x 和 y)
11
lambda2(); // 再次输出 x = 11, y = 21 (因为 lambda2 是值捕获,不受 lambda3 修改的影响)
③ 参数列表 (Parameter List) 和返回类型 (Return Type): Lambda 表达式的参数列表和返回类型与普通函数类似。如果函数体只包含一个 return
语句,或者没有 return
语句,返回类型可以省略,编译器会自动推导。
1
auto add = [](int a, int b) -> int { return a + b; }; // 显式指定返回类型
2
auto multiply = [](double a, double b) { return a * b; }; // 自动推导返回类型为 double
3
4
std::cout << "add(5, 3) = " << add(5, 3) << std::endl;
5
std::cout << "multiply(2.5, 4) = " << multiply(2.5, 4) << std::endl;
④ 应用场景:
▮▮▮▮⚝ 算法 (Algorithms): 作为算法的谓词 (predicate) 或操作 (operation),例如 std::sort
, std::for_each
, std::transform
等。
1
std::vector<int> numbers = {5, 2, 8, 1, 9};
2
std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; }); // 使用 Lambda 表达式自定义排序规则
3
std::for_each(numbers.begin(), numbers.end(), [](int n) { std::cout << n << " "; });
4
std::cout << std::endl;
▮▮▮▮⚝ 回调函数 (Callback Functions): 用于事件处理、异步操作等回调机制。
▮▮▮▮⚝ 函数对象 (Function Objects) 替代: Lambda 表达式可以替代传统的手动编写函数对象,使代码更简洁。
⑤ 优点:
▮▮▮▮⚝ 简洁性: 减少了编写独立函数或函数对象的代码量,使代码更紧凑。
▮▮▮▮⚝ 局部性: Lambda 表达式的定义位置通常靠近其使用位置,提高了代码的局部性和可读性。
▮▮▮▮⚝ 灵活性: 可以方便地捕获外部变量,适应不同的上下文环境。
15.1.4 右值引用 (Rvalue References) 和移动语义 (Move Semantics)
概要: 右值引用和移动语义是 C++11 中引入的重要特性,旨在提高程序性能,尤其是在处理临时对象和资源管理时,通过移动而非复制来避免不必要的开销。
深度解析:
① 左值 (Lvalue) 和右值 (Rvalue):
▮▮▮▮⚝ 左值 (Lvalue): 表示一个持久的对象,有地址,可以出现在赋值运算符的左边和右边。例如,变量名、解引用的指针等。
▮▮▮▮⚝ 右值 (Rvalue): 表示一个临时对象或字面值,即将销毁的对象,没有持久地址,通常只能出现在赋值运算符的右边。例如,函数返回值 (非引用返回)、字面常量、临时对象等。
1
int x = 10; // x 是左值
2
int* p = &x; // x 是左值
3
4
int y = x + 5; // x + 5 的结果是右值
5
int z = 20; // 20 是右值
6
int square() { return 5 * 5; } // square() 的返回值是右值
② 右值引用 (Rvalue Reference): 使用 &&
声明的引用,绑定到右值的引用。右值引用延长了临时对象的生命周期,并允许对其进行修改。
1
int&& rref = 25; // rref 绑定到右值 25,延长了 25 的生命周期
2
rref = 30; // 可以修改右值引用的值 (实际上是修改了延长生命周期的临时对象)
3
std::cout << rref << std::endl; // 输出 30
③ 移动语义 (Move Semantics): 允许将资源 (例如,动态分配的内存) 从一个对象 "移动" 到另一个对象,而不是进行深拷贝。移动语义通常用于构造函数 (move constructor) 和赋值运算符 (move assignment operator)。
▮▮▮▮⚝ 移动构造函数 (Move Constructor): 用于从一个右值对象构造新对象。移动构造函数接管源对象的资源,并将源对象置于有效但未指定的状态。
▮▮▮▮⚝ 移动赋值运算符 (Move Assignment Operator): 用于将一个右值对象赋值给已存在的对象。移动赋值运算符释放目标对象原有的资源,然后接管源对象的资源,并将源对象置于有效但未指定的状态。
1
class MyString {
2
private:
3
char* data;
4
size_t length;
5
public:
6
// 构造函数
7
MyString(const char* str) {
8
length = std::strlen(str);
9
data = new char[length + 1];
10
std::strcpy(data, str);
11
std::cout << "Constructor called" << std::endl;
12
}
13
14
// 拷贝构造函数 (Deep copy)
15
MyString(const MyString& other) {
16
length = other.length;
17
data = new char[length + 1];
18
std::strcpy(data, other.data);
19
std::cout << "Copy constructor called" << std::endl;
20
}
21
22
// 移动构造函数 (Move constructor)
23
MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
24
other.data = nullptr; // 源对象资源置空
25
other.length = 0;
26
std::cout << "Move constructor called" << std::endl;
27
}
28
29
// 析构函数
30
~MyString() {
31
delete[] data;
32
std::cout << "Destructor called" << std::endl;
33
}
34
35
// ... (其他成员函数)
36
};
37
38
MyString getString() {
39
return MyString("Hello, Move Semantics!"); // 返回右值
40
}
41
42
int main() {
43
MyString str1 = "Initial String"; // 构造函数
44
MyString str2 = str1; // 拷贝构造函数 (深拷贝)
45
MyString str3 = getString(); // 移动构造函数 (移动) - 避免深拷贝
46
47
return 0;
48
}
④ std::move
: std::move
是一个类型转换 (type cast) 函数,将左值转换为右值,使得可以对左值对象应用移动语义。但 std::move
并不真正移动任何东西,它只是一个类型转换,实际的移动操作发生在移动构造函数或移动赋值运算符中。
1
std::vector<int> vec1 = {1, 2, 3, 4, 5};
2
std::vector<int> vec2 = std::move(vec1); // vec1 的内容被移动到 vec2,vec1 变为有效但未指定状态
3
4
std::cout << "vec2 size: " << vec2.size() << std::endl; // 输出 vec2 的大小
5
std::cout << "vec1 size: " << vec1.size() << std::endl; // 输出 vec1 的大小 (可能为 0 或很小)
⑤ noexcept
: noexcept
关键字用于声明函数不会抛出异常 (exceptions)。移动构造函数和移动赋值运算符通常应声明为 noexcept
,以便标准库容器和算法在需要移动对象时,能够安全地使用移动语义,提高效率。
⑥ 完美转发 (Perfect Forwarding) 和 std::forward
: 完美转发是指在模板函数中,将参数以原始类型 (左值或右值) 转发给另一个函数。std::forward
用于实现完美转发,它根据参数的原始类型,将其转换为相应的右值或左值引用。
1
template <typename T>
2
void processValue(T&& value) { // 接受通用引用 (Universal Reference)
3
actualProcess(std::forward<T>(value)); // 完美转发到 actualProcess
4
}
5
6
void actualProcess(int& val) {
7
std::cout << "Lvalue reference version" << std::endl;
8
}
9
10
void actualProcess(int&& val) {
11
std::cout << "Rvalue reference version" << std::endl;
12
}
13
14
int main() {
15
int x = 10;
16
processValue(x); // 传递左值,调用 actualProcess(int&)
17
processValue(20); // 传递右值,调用 actualProcess(int&&)
18
return 0;
19
}
⑦ 优势:
▮▮▮▮⚝ 性能提升: 避免了不必要的深拷贝操作,尤其是在处理大型对象或资源密集型对象时,显著提高性能。
▮▮▮▮⚝ 资源高效管理: 更好地管理动态分配的资源,减少内存分配和释放的开销。
▮▮▮▮⚝ 支持移动容器: 标准库容器 (例如 std::vector
, std::string
) 广泛使用了移动语义,使得容器操作更加高效。
15.1.5 智能指针 (Smart Pointers)
概要: 智能指针是 C++11 引入的用于自动管理动态内存的类模板,旨在解决手动内存管理 (使用 new
和 delete
) 容易导致的内存泄漏 (memory leaks) 和悬挂指针 (dangling pointers) 等问题。
深度解析:
① 内存管理问题: 手动内存管理的常见问题包括:
▮▮▮▮⚝ 内存泄漏: new
分配的内存没有被 delete
释放,导致内存资源浪费。
▮▮▮▮⚝ 悬挂指针: delete
释放内存后,指针仍然指向已释放的内存,成为悬挂指针,访问悬挂指针会导致未定义行为。
▮▮▮▮⚝ 异常安全问题: 如果在 new
和 delete
之间抛出异常,delete
可能不会被执行,导致内存泄漏。
② 智能指针类型: C++11 标准库提供了三种主要的智能指针类型,定义在 <memory>
头文件中:
▮▮▮▮⚝ std::unique_ptr
: 独占所有权的智能指针。一个 unique_ptr
独占地拥有它指向的对象,不允许其他智能指针共享所有权。当 unique_ptr
销毁时,会自动释放它所拥有的对象。
▮▮▮▮⚝ std::shared_ptr
: 共享所有权的智能指针。多个 shared_ptr
可以共享同一个对象的所有权。使用引用计数 (reference counting) 来跟踪对象被多少个 shared_ptr
共享。当最后一个 shared_ptr
销毁时,才会释放对象。
▮▮▮▮⚝ std::weak_ptr
: 弱引用智能指针。weak_ptr
不拥有对象的所有权,只是观察 shared_ptr
管理的对象。weak_ptr
不会增加对象的引用计数。可以用于解决 shared_ptr
循环引用 (cyclic references) 的问题。
③ std::unique_ptr
:
▮▮▮▮⚝ 独占所有权: unique_ptr
是轻量级的智能指针,提供了独占所有权语义。适用于明确对象所有权归属的情况。
▮▮▮▮⚝ 构造和初始化: 可以使用 std::make_unique
(C++14 引入) 安全地创建 unique_ptr
,避免异常安全问题。
1
std::unique_ptr<int> ptr1 = std::make_unique<int>(10); // 推荐方式 (C++14)
2
std::unique_ptr<int> ptr2(new int(20)); // 也可以使用构造函数,但 new 可能抛出异常
3
4
// unique_ptr 不支持拷贝构造和拷贝赋值 (因为独占所有权)
5
// std::unique_ptr<int> ptr3 = ptr1; // 错误:拷贝构造函数被删除
6
// ptr3 = ptr1; // 错误:拷贝赋值运算符被删除
7
8
// 可以使用移动构造和移动赋值
9
std::unique_ptr<int> ptr4 = std::move(ptr1); // ptr1 的所有权转移到 ptr4,ptr1 变为 nullptr
▮▮▮▮⚝ 使用和访问: 使用 *
运算符解引用访问对象,使用 ->
运算符访问成员。
1
if (ptr4) { // 检查 ptr4 是否为空
2
std::cout << "*ptr4 = " << *ptr4 << std::endl;
3
}
▮▮▮▮⚝ 自定义删除器 (Custom Deleter): 可以为 unique_ptr
提供自定义删除器,用于在对象销毁时执行特定的清理操作,例如释放文件句柄、关闭网络连接等。
1
// 自定义删除器函数
2
void fileDeleter(FILE* file) {
3
if (file) {
4
std::fclose(file);
5
std::cout << "File closed by custom deleter" << std::endl;
6
}
7
}
8
9
int main() {
10
std::unique_ptr<FILE, decltype(&fileDeleter)> filePtr(std::fopen("test.txt", "r"), fileDeleter);
11
if (filePtr) {
12
// ... 使用文件 ...
13
} // fileDeleter 会在 filePtr 销毁时自动调用
14
return 0;
15
}
④ std::shared_ptr
:
▮▮▮▮⚝ 共享所有权: shared_ptr
允许多个智能指针共享同一个对象的所有权。适用于多个组件需要共享对象生命周期的情况。
▮▮▮▮⚝ 引用计数: shared_ptr
使用引用计数来跟踪共享对象的 shared_ptr
数量。每次拷贝 shared_ptr
,引用计数增加;每次 shared_ptr
销毁,引用计数减少。当引用计数变为 0 时,对象被释放。
▮▮▮▮⚝ 构造和初始化: 可以使用 std::make_shared
(C++11 引入) 安全地创建 shared_ptr
,提高效率并避免异常安全问题。
1
std::shared_ptr<int> ptr5 = std::make_shared<int>(30); // 推荐方式
2
std::shared_ptr<int> ptr6(new int(40)); // 也可以使用构造函数
3
4
std::shared_ptr<int> ptr7 = ptr5; // 拷贝构造,ptr5 和 ptr7 共享所有权,引用计数增加
5
std::shared_ptr<int> ptr8;
6
ptr8 = ptr6; // 拷贝赋值,ptr6 和 ptr8 共享所有权,ptr8 原有所有权释放
▮▮▮▮⚝ 循环引用问题: shared_ptr
循环引用会导致内存泄漏,因为循环引用的对象永远不会被释放,即使它们已经不再被程序使用。weak_ptr
可以用于解决循环引用问题。
⑤ std::weak_ptr
:
▮▮▮▮⚝ 弱引用,不拥有所有权: weak_ptr
是 shared_ptr
的辅助指针,不增加对象的引用计数,不拥有对象的所有权。
▮▮▮▮⚝ 解决循环引用: 在循环引用场景中,可以使用 weak_ptr
打破循环引用,避免内存泄漏。
▮▮▮▮⚝ lock()
方法: weak_ptr
提供 lock()
方法,尝试获取其观察的 shared_ptr
的所有权。如果对象仍然存在 (引用计数大于 0),lock()
返回一个有效的 shared_ptr
;如果对象已被释放,lock()
返回一个空的 shared_ptr
。
1
class ClassB; // 前向声明
2
3
class ClassA {
4
public:
5
std::shared_ptr<ClassB> b_ptr; // 使用 shared_ptr 可能导致循环引用
6
~ClassA() { std::cout << "ClassA destructor" << std::endl; }
7
};
8
9
class ClassB {
10
public:
11
std::weak_ptr<ClassA> a_ptr; // 使用 weak_ptr 打破循环引用
12
~ClassB() { std::cout << "ClassB destructor" << std::endl; }
13
};
14
15
int main() {
16
std::shared_ptr<ClassA> a = std::make_shared<ClassA>();
17
std::shared_ptr<ClassB> b = std::make_shared<ClassB>();
18
19
a->b_ptr = b;
20
b->a_ptr = a; // weak_ptr 不增加引用计数,打破循环引用
21
22
return 0; // a 和 b 能够被正确析构,避免内存泄漏
23
}
⑥ 选择智能指针:
▮▮▮▮⚝ unique_ptr
: 当需要独占对象所有权,且所有权转移明确时,使用 unique_ptr
。
▮▮▮▮⚝ shared_ptr
: 当多个组件需要共享对象所有权,且对象的生命周期需要被多个组件共同管理时,使用 shared_ptr
。
▮▮▮▮⚝ weak_ptr
: 当需要观察 shared_ptr
管理的对象,但不拥有所有权,且需要打破循环引用时,使用 weak_ptr
。
⑦ 优势:
▮▮▮▮⚝ 自动内存管理: 避免手动 new
和 delete
,减少内存泄漏和悬挂指针的风险。
▮▮▮▮⚝ 异常安全: 即使在异常情况下,智能指针也能确保资源被正确释放。
▮▮▮▮⚝ 代码简洁: 减少了内存管理代码的编写,使代码更简洁易读。
▮▮▮▮⚝ 资源管理 RAII (Resource Acquisition Is Initialization) 原则的体现。
15.1.6 constexpr
(常量表达式)
概要: constexpr
关键字用于声明常量表达式 (constant expressions),指示值或函数的结果可以在编译时计算出来,从而提高程序性能,并支持在编译时进行更多计算。
深度解析:
① 常量表达式 (Constant Expressions): 常量表达式是在编译时就能确定值的表达式。常量表达式可以用于:
▮▮▮▮⚝ 定义常量 (constants)。
▮▮▮▮⚝ 初始化全局变量、静态变量和线程局部变量。
▮▮▮▮⚝ 作为数组的边界。
▮▮▮▮⚝ 作为模板参数。
▮▮▮▮⚝ 在 switch
语句的 case
标签中。
② constexpr
变量: 使用 constexpr
声明的变量必须是常量表达式,在编译时求值。constexpr
变量一定是 const
的,但 const
变量不一定是 constexpr
的 (即 const
变量可以在运行时初始化)。
1
constexpr int compileTimeConstant = 10; // 编译时常量
2
const int runtimeConstant = getRuntimeValue(); // 运行时常量 (假设 getRuntimeValue() 在运行时确定值)
3
4
int array1[compileTimeConstant]; // 正确:编译时常量可以作为数组边界
5
// int array2[runtimeConstant]; // 错误:运行时常量不能作为数组边界
6
7
enum class Color {
8
Red,
9
Green,
10
Blue,
11
Count = compileTimeConstant // 正确:编译时常量可以作为枚举值
12
};
③ constexpr
函数: constexpr
函数是可以在编译时求值的函数。constexpr
函数的返回值在编译时或运行时都可以计算。如果所有参数都是常量表达式,且满足 constexpr
函数的限制,则 constexpr
函数的结果在编译时计算;否则,在运行时计算。
▮▮▮▮⚝ constexpr
函数的限制:
▮▮▮▮⚝ 函数体只能包含单一的 return
语句 (C++11)。在 C++14 之后,限制放宽,constexpr
函数可以包含更复杂的语句,例如 if
、switch
、循环等,但仍然需要保证在编译时可以求值。
▮▮▮▮⚝ 函数不能有副作用 (side effects),例如修改全局变量、执行 I/O 操作等。
▮▮▮▮⚝ 函数必须是字面值类型 (literal type),即类型在编译时是已知的。
▮▮▮▮⚝ 在 C++11 中,constexpr
函数只能调用其他 constexpr
函数。在 C++14 之后,constexpr
函数可以调用非 constexpr
函数,但如果调用的非 constexpr
函数需要在运行时才能求值,则整个 constexpr
函数也只能在运行时求值。
1
// C++11 constexpr 函数示例
2
constexpr int square(int n) {
3
return n * n;
4
}
5
6
// C++14 constexpr 函数示例 (可以使用更复杂的语句)
7
constexpr int factorial(int n) {
8
int result = 1;
9
for (int i = 1; i <= n; ++i) {
10
result *= i;
11
}
12
return result;
13
}
14
15
int main() {
16
constexpr int compileTimeSquare = square(5); // 编译时计算 square(5)
17
int runtimeValue = 3;
18
int runtimeSquare = square(runtimeValue); // 运行时计算 square(runtimeValue)
19
20
constexpr int compileTimeFactorial = factorial(5); // 编译时计算 factorial(5)
21
22
return 0;
23
}
④ constexpr
构造函数: constexpr
构造函数可以创建 constexpr
对象。如果构造函数的所有参数都是常量表达式,且满足 constexpr
函数的限制,则 constexpr
构造函数可以在编译时构造对象。
1
class Point {
2
private:
3
int x, y;
4
public:
5
constexpr Point(int x_val, int y_val) : x(x_val), y(y_val) {} // constexpr 构造函数
6
constexpr int getX() const { return x; } // constexpr 成员函数
7
constexpr int getY() const { return y; } // constexpr 成员函数
8
};
9
10
int main() {
11
constexpr Point compileTimePoint(10, 20); // 编译时构造 Point 对象
12
constexpr int x_coord = compileTimePoint.getX(); // 编译时调用 constexpr 成员函数
13
14
return 0;
15
}
⑤ 优势:
▮▮▮▮⚝ 性能提升: 将计算从运行时提前到编译时,减少了运行时的计算开销,提高程序性能。
▮▮▮▮⚝ 编译时检查: constexpr
变量和函数在编译时进行检查,可以尽早发现错误。
▮▮▮▮⚝ 代码优化: 编译器可以更好地优化使用 constexpr
的代码,例如进行常量折叠 (constant folding)、内联 (inlining) 等。
▮▮▮▮⚝ 模板元编程 (Template Metaprogramming) 的基础: constexpr
是现代 C++ 模板元编程的重要组成部分,使得可以在编译时进行更复杂的计算和逻辑处理。
15.1.7 初始化列表 (Initializer Lists)
概要: 初始化列表提供了一种统一的、通用的初始化语法,可以用于初始化各种类型的对象,包括内置类型、数组、结构体、类对象以及标准库容器。
深度解析:
① 统一初始化语法: 初始化列表使用花括号 {}
来表示,可以用于各种初始化场景。
1
int x = {10}; // 初始化内置类型
2
int array[5] = {1, 2, 3, 4, 5}; // 初始化数组
3
struct MyStruct { int a; double b; };
4
MyStruct s = {100, 3.14}; // 初始化结构体
5
6
std::vector<int> vec = {10, 20, 30}; // 初始化标准库容器
7
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}}; // 初始化 map
② 聚合初始化 (Aggregate Initialization): 对于聚合类型 (aggregates),初始化列表提供了直接初始化成员变量的方式。聚合类型包括数组、结构体和类 (满足特定条件,例如没有用户定义的构造函数、没有私有或受保护的非静态成员等)。
1
struct Point {
2
int x;
3
int y;
4
};
5
6
Point p1 = {1, 2}; // 聚合初始化,直接初始化成员 x 和 y
7
Point p2 = {.y = 2, .x = 1}; // C++20 指定初始化器 (designated initializers)
③ std::initializer_list
: 标准库提供了 std::initializer_list
类模板,用于支持接受初始化列表作为参数的构造函数和函数。std::initializer_list
允许函数接受可变数量的同类型元素。
1
class MyVector {
2
private:
3
std::vector<int> data;
4
public:
5
// 接受 initializer_list 的构造函数
6
MyVector(std::initializer_list<int> list) : data(list) {
7
std::cout << "Initializer list constructor called" << std::endl;
8
}
9
10
void print() const {
11
for (int val : data) {
12
std::cout << val << " ";
13
}
14
std::cout << std::endl;
15
}
16
};
17
18
int main() {
19
MyVector v1 = {1, 2, 3, 4, 5}; // 使用 initializer list 初始化 MyVector
20
v1.print();
21
22
MyVector v2{10, 20, 30}; // 也可以省略等号
23
v2.print();
24
25
return 0;
26
}
④ 构造函数初始化列表 (Constructor Initializer List): 类成员变量的初始化可以使用构造函数初始化列表,效率更高,特别是对于类类型的成员变量。
1
class MyClass {
2
private:
3
int x;
4
std::string name;
5
public:
6
// 构造函数初始化列表
7
MyClass(int x_val, const std::string& name_val) : x(x_val), name(name_val) {
8
std::cout << "Constructor called" << std::endl;
9
}
10
};
⑤ 窄化转换 (Narrowing Conversion) 预防: 使用初始化列表进行初始化时,编译器会阻止窄化转换,即从宽类型到窄类型的隐式转换,例如从 double
到 int
的转换,有助于避免潜在的数据丢失或精度损失。
1
int x1 = 3.14; // 警告:隐式窄化转换
2
// int x2 = {3.14}; // 错误:初始化列表阻止窄化转换
3
int x3 = static_cast<int>(3.14); // 显式窄化转换 (允许)
⑥ 优势:
▮▮▮▮⚝ 统一性: 提供了一种通用的初始化语法,适用于各种类型。
▮▮▮▮⚝ 简洁性: 使初始化代码更简洁易读。
▮▮▮▮⚝ 安全性: 初始化列表阻止窄化转换,提高类型安全。
▮▮▮▮⚝ 支持自定义类型: 通过 std::initializer_list
,用户可以为自定义类型添加接受初始化列表的构造函数,提供更灵活的初始化方式。
15.2 C++14 增强特性
15.2.1 泛型 Lambda 表达式 (Generic Lambdas)
概要: C++14 允许 Lambda 表达式的参数类型使用 auto
关键字,使其成为泛型 Lambda 表达式,可以接受不同类型的参数,增强了 Lambda 表达式的灵活性和通用性。
深度解析:
① auto
参数类型: 在 C++14 中,Lambda 表达式的参数列表可以使用 auto
关键字声明参数类型,编译器会根据 Lambda 表达式的实际调用推导参数类型。
1
auto genericLambda = [](auto x, auto y) { // 使用 auto 声明参数类型
2
return x + y;
3
};
4
5
int result1 = genericLambda(10, 20); // 推导为 int + int
6
double result2 = genericLambda(3.14, 2.0); // 推导为 double + double
7
std::string result3 = genericLambda(std::string("Hello"), std::string(", Generic Lambda!")); // 推导为 string + string
② 模板函数对象: 泛型 Lambda 表达式本质上是创建了一个模板函数对象。编译器会为每个不同的参数类型组合生成不同的 Lambda 表达式实例。
③ 与标准库算法结合: 泛型 Lambda 表达式可以更方便地与标准库算法结合使用,处理不同类型的数据。
1
std::vector<int> numbers1 = {1, 2, 3, 4, 5};
2
std::vector<double> numbers2 = {1.1, 2.2, 3.3, 4.4, 5.5};
3
4
// 使用泛型 Lambda 表达式计算向量元素的平方和
5
auto sumOfSquares = [](const auto& vec) {
6
double sum = 0;
7
for (const auto& val : vec) {
8
sum += val * val;
9
}
10
return sum;
11
};
12
13
std::cout << "Sum of squares of numbers1: " << sumOfSquares(numbers1) << std::endl;
14
std::cout << "Sum of squares of numbers2: " << sumOfSquares(numbers2) << std::endl;
④ 优势:
▮▮▮▮⚝ 泛型编程能力: 使 Lambda 表达式具备泛型编程能力,可以处理多种类型的数据,提高代码的复用性。
▮▮▮▮⚝ 代码简洁: 进一步简化 Lambda 表达式的语法,减少类型声明的冗余。
▮▮▮▮⚝ 灵活性: 增强了 Lambda 表达式的灵活性,使其能够适应更广泛的应用场景。
15.2.2 std::make_unique
概要: std::make_unique
是 C++14 引入的函数模板,用于安全、高效地创建 std::unique_ptr
对象。
深度解析:
① 异常安全问题: 在 C++11 中,创建 std::unique_ptr
的常见方式是直接使用 new
运算符,例如 std::unique_ptr<int> ptr(new int(10));
。但这种方式存在异常安全问题。考虑以下代码:
1
void process(std::unique_ptr<ClassA> ptr1, std::unique_ptr<ClassB> ptr2);
2
3
// 可能存在异常安全问题
4
process(std::unique_ptr<ClassA>(new ClassA()), std::unique_ptr<ClassB>(new ClassB()));
在 process
函数调用中,new ClassA()
、new ClassB()
和 std::unique_ptr
的构造函数可能会按任意顺序执行。如果 new ClassA()
执行成功,但 new ClassB()
执行失败并抛出异常,那么 new ClassA()
分配的内存将无法被 std::unique_ptr
管理,导致内存泄漏。
② std::make_unique
的优势: std::make_unique
函数模板将对象分配和 std::unique_ptr
的构造操作合并为一个原子操作,避免了上述异常安全问题。
1
// 使用 std::make_unique,异常安全
2
process(std::make_unique<ClassA>(), std::make_unique<ClassB>());
std::make_unique<T>(args...)
的作用相当于:
1
template <typename T, typename... Args>
2
std::unique_ptr<T> make_unique(Args&&... args) {
3
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
4
}
但 std::make_unique
的实现细节更复杂,以确保异常安全和效率。
③ 用法: std::make_unique
的用法非常简单,类似于 std::make_shared
。
1
std::unique_ptr<int> ptr1 = std::make_unique<int>(100); // 创建 unique_ptr<int>,初始化为 100
2
std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("Hello, unique_ptr!"); // 创建 unique_ptr<string>
3
4
auto ptr3 = std::make_unique<std::vector<int>>(5, 0); // 创建 unique_ptr<vector<int>>,包含 5 个 0
④ 不支持自定义删除器: std::make_unique
不直接支持自定义删除器。如果需要为 std::unique_ptr
指定自定义删除器,仍然需要使用 std::unique_ptr
的构造函数。
⑤ 优势:
▮▮▮▮⚝ 异常安全: 避免了使用 new
运算符直接创建 std::unique_ptr
时的异常安全问题。
▮▮▮▮⚝ 代码简洁: 使 std::unique_ptr
的创建代码更简洁。
▮▮▮▮⚝ 潜在的性能优化: 在某些情况下,std::make_unique
可能比直接使用 new
运算符创建 std::unique_ptr
更高效。
15.2.3 返回值类型推导 (Return Type Deduction for Functions)
概要: C++14 允许普通函数 (不仅仅是 Lambda 表达式) 的返回类型使用 auto
关键字进行推导,简化函数声明,但需要确保返回类型能够被编译器正确推导。
深度解析:
① auto
返回类型: 在 C++14 之前,只有 Lambda 表达式可以使用 auto
推导返回类型。C++14 将这一特性扩展到普通函数。
1
// C++14 返回类型推导
2
auto add(int a, int b) { // 返回类型由 return 语句推导
3
return a + b; // 返回类型推导为 int
4
}
5
6
auto multiply(double x, double y) {
7
return x * y; // 返回类型推导为 double
8
}
② 尾置返回类型 (Trailing Return Type) 仍然可用: C++11 引入的尾置返回类型语法 auto function(...) -> return_type
仍然可以使用,并且在某些情况下是必要的,例如返回类型依赖于函数参数时,或者返回类型比较复杂时。
1
// 尾置返回类型 (C++11 就已支持)
2
template <typename T1, typename T2>
3
auto subtract(T1 a, T2 b) -> decltype(a - b) { // 使用 decltype 推导返回类型,避免隐式转换问题
4
return a - b;
5
}
③ 返回类型推导的限制:
▮▮▮▮⚝ 函数必须有明确的 return
语句:编译器需要通过 return
语句推导返回类型。如果函数没有 return
语句 (例如,void
返回类型),则不能使用 auto
推导返回类型。
▮▮▮▮⚝ 函数体中必须有返回语句,且所有 return
语句返回的类型必须一致:如果函数有多个 return
语句,所有 return
语句返回的表达式类型必须能够统一推导为同一种类型。
▮▮▮▮⚝ 递归函数不能使用返回类型推导:递归函数的返回类型推导会产生循环依赖,编译器无法推导。
1
// 错误:无法推导递归函数的返回类型
2
// auto recursiveFunction(int n) {
3
// if (n <= 1) return 1;
4
// return n * recursiveFunction(n - 1); // 循环依赖
5
// }
④ 优势:
▮▮▮▮⚝ 代码简洁: 简化函数声明,尤其是在返回类型显而易见或类型名称冗长时,提高代码可读性。
▮▮▮▮⚝ 减少模板编程中的冗余: 在模板编程中,返回类型推导可以减少 decltype
的使用,使代码更简洁。
15.2.4 数字分隔符 (Digit Separators)
概要: C++14 引入了数字分隔符 '
(单引号),可以用于在数字字面量中分隔数字,提高数字字面量的可读性,尤其是在表示长数字时。
深度解析:
① 分隔符 '
的使用: 可以在整型字面量和浮点型字面量中使用单引号 '
作为数字分隔符,将数字分组,提高可读性。分隔符的位置没有限制,可以根据需要自由放置。
1
int population = 1'000'000'000; // 十亿
2
double pi = 3.141'592'653'589'793; // 圆周率
3
unsigned long long creditCardNumber = 1234'5678'9012'3456ULL; // 信用卡号
4
int binaryValue = 0b1010'0110'1100'1001; // 二进制数
② 编译时忽略: 数字分隔符 '
在编译时会被编译器忽略,不会影响数值的大小。它仅仅是为了提高代码的可读性。
③ 适用范围: 数字分隔符 '
可以用于:
▮▮▮▮⚝ 整型字面量 (十进制、二进制、八进制、十六进制)。
▮▮▮▮⚝ 浮点型字面量。
▮▮▮▮⚝ 用户自定义字面量 (user-defined literals)。
④ 优势:
▮▮▮▮⚝ 提高可读性: 对于长数字字面量,使用数字分隔符可以更清晰地表示数值,减少阅读错误。
▮▮▮▮⚝ 代码维护性: 提高代码的可维护性,使代码更易于理解和修改。
15.3 C++17 主要特性
15.3.1 内联变量 (Inline Variables)
概要: C++17 引入了内联变量的概念,允许在头文件中定义 inline
的静态成员变量和全局变量,解决了在多个编译单元 (translation units) 中重复定义内联变量导致的链接错误问题。
深度解析:
① 头文件定义变量的问题: 在 C++17 之前,在头文件中定义非 const
的静态成员变量或全局变量会导致链接错误 (multiple definition error),因为头文件会被包含到多个源文件中,导致变量在多个编译单元中重复定义。
1
// my_header.h (C++17 之前,可能导致链接错误)
2
static int globalCounter = 0; // 静态全局变量
为了避免链接错误,通常的做法是将变量声明放在头文件中,将定义放在一个源文件中。
1
// my_header.h (C++17 之前,声明)
2
extern int globalCounter;
3
4
// my_source.cpp (C++17 之前,定义)
5
int globalCounter = 0;
② 内联变量的解决方案: C++17 引入了内联变量,使用 inline
关键字声明的变量可以在头文件中定义,并且不会导致链接错误。编译器会负责在多个编译单元中只保留一个变量的实例。
1
// my_header.h (C++17,内联变量)
2
inline static int globalCounter = 0; // 内联静态全局变量
③ 适用场景: 内联变量适用于以下场景:
▮▮▮▮⚝ 静态成员变量: 在类中定义的静态成员变量,可以使用 inline
在类定义内部初始化。
1
class MyClass {
2
public:
3
inline static int staticMember = 100; // 内联静态成员变量
4
};
▮▮▮▮⚝ 全局变量: 全局变量可以使用 inline
在头文件中定义。
1
// my_header.h
2
inline int globalVariable = 50; // 内联全局变量
▮▮▮▮⚝ const
变量和 constexpr
变量: const
变量和 constexpr
变量在 C++11 中已经是隐式内联的,C++17 之后,inline
关键字可以显式地声明它们为内联变量。
④ 优势:
▮▮▮▮⚝ 代码简洁: 允许在头文件中直接定义静态成员变量和全局变量,无需额外的源文件定义,简化代码结构。
▮▮▮▮⚝ 提高头文件的自包含性: 头文件可以包含变量的声明和定义,提高头文件的自包含性,减少头文件之间的依赖。
▮▮▮▮⚝ 模板代码的简化: 在模板代码中,内联变量尤其有用,可以避免模板代码在多个编译单元中实例化时导致的链接错误。
15.3.2 结构化绑定 (Structured Bindings)
概要: C++17 引入了结构化绑定,允许使用类似解包 (unpacking) 的语法,将结构体、数组或元组 (tuples) 中的元素绑定到独立的变量名,简化了从复杂数据结构中提取数据的操作。
深度解析:
① 结构化绑定语法: 结构化绑定的基本语法形式为:
1
auto [v1, v2, ..., vn] = expression;
其中 expression
可以是:
▮▮▮▮⚝ std::pair
, std::tuple
, std::array
等标准库类型: 结构化绑定可以将 pair
, tuple
, array
中的元素分别绑定到 v1
, v2
, ... vn
变量。
1
std::pair<int, std::string> myPair = {10, "Hello"};
2
auto [id, message] = myPair; // 结构化绑定 pair 的元素
3
4
std::tuple<int, double, std::string> myTuple = {1, 3.14, "Tuple"};
5
auto [index, value, text] = myTuple; // 结构化绑定 tuple 的元素
6
7
std::array<int, 3> myArray = {100, 200, 300};
8
auto [x, y, z] = myArray; // 结构化绑定 array 的元素
▮▮▮▮⚝ 数组: 结构化绑定可以用于 C 风格的数组。
1
int coordinates[3] = {10, 20, 30};
2
auto [x_coord, y_coord, z_coord] = coordinates; // 结构化绑定数组元素
▮▮▮▮⚝ 结构体和类 (满足特定条件): 对于聚合类型的结构体和类,结构化绑定可以绑定到其非静态数据成员。
1
struct Point {
2
int x;
3
int y;
4
int z;
5
};
6
7
Point myPoint = {1, 2, 3};
8
auto [p_x, p_y, p_z] = myPoint; // 结构化绑定结构体成员
② 绑定方式 (引用或值拷贝): 结构化绑定可以使用 auto
或 auto&
或 const auto&
来声明绑定变量,决定绑定方式是值拷贝还是引用。
1
Point myPoint = {1, 2, 3};
2
auto [x1, y1, z1] = myPoint; // 值拷贝
3
auto& [x2, y2, z2] = myPoint; // 引用
4
const auto& [x3, y3, z3] = myPoint; // 常量引用
③ 忽略不需要的元素: 可以使用 std::ignore
忽略结构化绑定中不需要的元素。
1
std::tuple<int, double, std::string> myTuple = {1, 3.14, "Tuple"};
2
auto [index, std::ignore, text] = myTuple; // 忽略 tuple 的第二个元素 (double 值)
④ 优势:
▮▮▮▮⚝ 代码简洁: 简化了从 pair
, tuple
, array
, 结构体等复杂数据结构中提取元素的代码,使代码更简洁易读。
▮▮▮▮⚝ 提高代码可读性: 使用有意义的变量名绑定结构化数据,提高了代码的可读性和可维护性。
▮▮▮▮⚝ 避免手动索引或成员访问: 减少了手动使用索引 (例如 tuple.get<0>()
, array[0]
) 或成员访问 (例如 struct.member
) 的代码,使代码更清晰。
15.3.3 if
和 switch
语句的初始化器 (Initializer in if
and switch
statements)
概要: C++17 允许在 if
语句和 switch
语句的条件表达式之前添加初始化器,在语句的作用域内初始化变量,提高了代码的可读性和作用域的局部性。
深度解析:
① if
语句初始化器: if
语句的初始化器语法形式为:
1
if (initializer; condition) {
2
// 代码块
3
}
initializer
部分在条件表达式 condition
求值之前执行,通常用于声明和初始化只在 if
语句作用域内使用的变量。
1
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
2
3
if (auto it = ages.find("Alice"); it != ages.end()) { // 在 if 语句中初始化迭代器 it
4
std::cout << "Age of Alice: " << it->second << std::endl;
5
} else {
6
std::cout << "Alice not found" << std::endl;
7
}
8
// it 的作用域仅限于 if 语句,在 if 语句外部不可见
② switch
语句初始化器: switch
语句的初始化器语法形式类似:
1
switch (initializer; condition) {
2
case value1:
3
// 代码块 1
4
break;
5
case value2:
6
// 代码块 2
7
break;
8
// ...
9
default:
10
// 默认代码块
11
}
initializer
部分在 switch
语句的条件表达式 condition
求值之前执行。
1
enum class ErrorCode {
2
NoError,
3
FileNotFound,
4
AccessDenied,
5
OutOfMemory
6
};
7
8
ErrorCode getErrorCode() {
9
// ... 获取错误代码 ...
10
return ErrorCode::FileNotFound;
11
}
12
13
switch (ErrorCode error = getErrorCode(); error) { // 在 switch 语句中初始化 error 变量
14
case ErrorCode::NoError:
15
std::cout << "No error" << std::endl;
16
break;
17
case ErrorCode::FileNotFound:
18
std::cout << "File not found error" << std::endl;
19
break;
20
case ErrorCode::AccessDenied:
21
std::cout << "Access denied error" << std::endl;
22
break;
23
default:
24
std::cout << "Unknown error" << std::endl;
25
break;
26
}
27
// error 的作用域仅限于 switch 语句
③ 作用域限制: 在 if
和 switch
语句初始化器中声明的变量,其作用域仅限于 if
或 switch
语句本身,包括 if
或 switch
语句的条件表达式和代码块。
④ 优势:
▮▮▮▮⚝ 作用域局部化: 将变量的作用域限制在 if
或 switch
语句内部,提高了代码的封装性和可读性,避免变量名冲突。
▮▮▮▮⚝ 代码简洁: 可以在 if
或 switch
语句中直接初始化变量,减少了在语句外部声明变量的代码。
▮▮▮▮⚝ 提高代码可维护性: 作用域的局部化使代码更易于理解和维护,减少了变量作用域扩散导致的潜在问题。
15.3.4 折叠表达式 (Fold Expressions)
概要: C++17 引入了折叠表达式,用于简化对参数包 (parameter packs) 的操作,可以方便地对参数包中的所有参数进行二元运算 (binary operations),例如求和、求积、逻辑与、逻辑或等。
深度解析:
① 参数包 (Parameter Packs): 参数包是模板编程中的概念,用于表示可变数量的模板参数。例如,在可变参数模板函数中,参数包可以接受任意数量的参数。
1
template <typename... Args>
2
void variadicFunction(Args... args) {
3
// args 是参数包
4
}
② 折叠表达式语法: 折叠表达式的基本语法形式有以下几种:
▮▮▮▮⚝ 一元右折叠 (Unary Right Fold): ( ... op pack )
等价于 ( pack1 op ( pack2 op ( ... op packN ) ) )
▮▮▮▮⚝ 一元左折叠 (Unary Left Fold): ( pack op ... )
等价于 ( ( ( pack1 op pack2 ) op ... ) op packN )
▮▮▮▮⚝ 二元右折叠 (Binary Right Fold): ( pack op ... op init )
等价于 ( pack1 op ( pack2 op ( ... op ( packN op init ) ) ) )
▮▮▮▮⚝ 二元左折叠 (Binary Left Fold): ( init op ... op pack )
等价于 ( ( ( ( init op pack1 ) op pack2 ) op ... ) op packN )
其中:
▮▮▮▮⚝ op
是二元运算符,例如 +
, -
, *
, /
, &
, |
, &&
, ||
, ,
等。
▮▮▮▮⚝ pack
是参数包。
▮▮▮▮⚝ init
是初始值 (仅用于二元折叠)。
③ 应用示例:
▮▮▮▮⚝ 参数包求和:
1
template <typename... Args>
2
auto sum(Args... args) {
3
return (args + ... + 0); // 二元右折叠,初始值为 0
4
}
5
6
int result1 = sum(1, 2, 3, 4, 5); // 计算 1 + 2 + 3 + 4 + 5 + 0
▮▮▮▮⚝ 参数包求积:
1
template <typename... Args>
2
auto product(Args... args) {
3
return (args * ... * 1); // 二元右折叠,初始值为 1
4
}
5
6
int result2 = product(1, 2, 3, 4, 5); // 计算 1 * 2 * 3 * 4 * 5 * 1
▮▮▮▮⚝ 参数包逻辑与:
1
template <typename... Args>
2
bool allTrue(Args... args) {
3
return (args && ... && true); // 二元右折叠,初始值为 true
4
}
5
6
bool result3 = allTrue(true, true, false, true); // 计算 true && true && false && true && true
▮▮▮▮⚝ 参数包打印:
1
template <typename... Args>
2
void printAll(Args... args) {
3
(std::cout << ... << args) << std::endl; // 一元左折叠,使用逗号运算符
4
}
5
6
printAll("Value 1: ", 10, ", Value 2: ", 20, ", Value 3: ", 30);
④ 运算符限制: 并非所有二元运算符都可以用于折叠表达式。常用的运算符包括算术运算符、逻辑运算符、位运算符、逗号运算符等。具体支持的运算符列表请参考 C++ 标准文档。
⑤ 优势:
▮▮▮▮⚝ 代码简洁: 简化了对参数包进行二元运算的代码,避免了手动编写递归模板或循环展开的代码。
▮▮▮▮⚝ 提高代码可读性: 折叠表达式的语法简洁明了,更直观地表达了对参数包的聚合操作。
▮▮▮▮⚝ 编译时性能优化: 折叠表达式通常在编译时展开,可以提高运行时性能。
15.4 C++20 最新特性
15.4.1 概念 (Concepts)
概要: C++20 引入了概念 (concepts),用于在模板编程中对模板参数进行约束和限制,提高模板代码的类型安全性和编译时错误信息的可读性。
深度解析:
① 模板约束问题: 在 C++20 之前,模板代码的类型约束是隐式的,依赖于模板代码中对模板参数的操作是否有效。如果模板参数类型不满足模板代码的要求,编译错误通常会出现在模板实例化的代码中,错误信息冗长且难以理解。
1
template <typename T>
2
T add(T a, T b) {
3
return a + b; // 隐式要求 T 类型支持 operator+
4
}
5
6
int main() {
7
add(std::string("Hello"), std::string("World")); // 正确:string 类型支持 operator+
8
// add(std::vector<int>{1}, std::vector<int>{2}); // 编译错误:vector<int> 类型不支持 operator+,错误信息复杂
9
return 0;
10
}
② 概念的定义: 概念 (concept) 是一种具名的类型约束,用于描述类型需要满足的条件。概念使用 concept
关键字定义,本质上是一个返回布尔值的编译时谓词 (predicate)。
1
// 定义一个概念:要求类型 T 支持 operator+
2
template <typename T>
3
concept Addable = requires(T a, T b) {
4
a + b; // 检查表达式 a + b 是否合法
5
};
requires
子句用于定义概念的约束条件。在 requires
子句中,可以检查:
▮▮▮▮⚝ 有效表达式 (Valid Expressions): 例如 a + b
是否是合法的表达式。
▮▮▮▮⚝ 返回类型约束 (Return Type Constraints): 使用 ->
符号约束表达式的返回类型,例如 a + b -> std::same_as<int>
要求 a + b
的返回类型必须是 int
。
▮▮▮▮⚝ 嵌套需求 (Nested Requirements): 在概念内部嵌套其他概念或 requires
子句。
▮▮▮▮⚝ 类型需求 (Type Requirements): 使用 typename
关键字检查类型是否有效,例如 typename T::value_type
。
③ 概念的使用: 概念可以用于约束模板参数,在模板声明中使用 concept
关键字或 requires
子句。
▮▮▮▮⚝ 简写语法 (Concept Syntax): 直接在模板参数列表中使用概念名。
1
// 使用 Addable 概念约束模板参数 T
2
template <Addable T>
3
T add_with_concept(T a, T b) {
4
return a + b;
5
}
▮▮▮▮⚝ requires
子句语法 (Requires Clause Syntax): 在模板声明中使用 requires
子句,可以更灵活地组合多个概念或定义更复杂的约束条件。
1
// 使用 requires 子句约束模板参数 T 和 U,要求 T 是 Addable,U 是 Copyable (假设已定义)
2
template <typename T, typename U>
3
requires Addable<T> && Copyable<U>
4
auto process(T a, U b) {
5
// ...
6
}
④ 标准库概念: C++20 标准库提供了丰富的概念库,定义在 <concepts>
头文件中,例如:
▮▮▮▮⚝ std::integral
: 要求类型是整型。
▮▮▮▮⚝ std::floating_point
: 要求类型是浮点型。
▮▮▮▮⚝ std::copyable
: 要求类型是可拷贝的。
▮▮▮▮⚝ std::moveable
: 要求类型是可移动的。
▮▮▮▮⚝ std::equality_comparable
: 要求类型是可进行相等比较的 (支持 ==
和 !=
运算符)。
▮▮▮▮⚝ std::totally_ordered
: 要求类型是全序关系的 (支持 <
, >
, <=
, >=
运算符)。
▮▮▮▮⚝ std::range
: 要求类型是范围 (range),可以用于范围 for
循环。
▮▮▮▮⚝ std::input_iterator
: 要求类型是输入迭代器。
▮▮▮▮⚝ std::output_iterator
: 要求类型是输出迭代器。
1
#include <concepts>
2
#include <vector>
3
#include <numeric>
4
5
// 使用 std::integral 概念约束模板参数 T
6
template <std::integral T>
7
T accumulate_integral(const std::vector<T>& vec) {
8
return std::accumulate(vec.begin(), vec.end(), static_cast<T>(0));
9
}
10
11
int main() {
12
std::vector<int> intVec = {1, 2, 3, 4, 5};
13
int sum1 = accumulate_integral(intVec); // 正确:int 是 std::integral
14
15
std::vector<double> doubleVec = {1.1, 2.2, 3.3};
16
// double sum2 = accumulate_integral(doubleVec); // 编译错误:double 不是 std::integral,概念约束生效
17
return 0;
18
}
⑤ 概念和重载 (Overloading): 概念可以用于函数重载决议 (overload resolution),编译器会根据概念约束选择最匹配的重载函数。
1
template <typename T>
2
requires std::integral<T>
3
std::string formatValue(T value) {
4
return std::string("Integral: ") + std::to_string(value);
5
}
6
7
template <typename T>
8
requires std::floating_point<T>
9
std::string formatValue(T value) {
10
return std::string("Floating-point: ") + std::to_string(value);
11
}
12
13
template <typename T>
14
std::string formatValue(T value) { // 通用版本,没有概念约束
15
return std::string("Other type");
16
}
17
18
int main() {
19
std::cout << formatValue(10) << std::endl; // 调用 integral 版本
20
std::cout << formatValue(3.14) << std::endl; // 调用 floating_point 版本
21
std::cout << formatValue(std::string("text")) << std::endl; // 调用通用版本
22
return 0;
23
}
⑥ 优势:
▮▮▮▮⚝ 提高类型安全: 通过概念对模板参数进行显式约束,增强了模板代码的类型安全性,尽早发现类型错误。
▮▮▮▮⚝ 改善编译时错误信息: 当模板参数不满足概念约束时,编译器会生成更清晰、更易于理解的错误信息,帮助开发者快速定位问题。
▮▮▮▮⚝ 增强代码可读性: 概念的使用使模板代码的意图更清晰,提高了代码的可读性和可维护性。
▮▮▮▮⚝ 支持静态多态 (Static Polymorphism): 概念是实现静态多态的重要工具,可以编写更灵活、更高效的泛型代码。
15.4.2 范围 (Ranges)
概要: C++20 引入了范围 (ranges) 库,提供了一种新的、更高级的抽象来处理数据序列,简化了对容器、数组等数据结构的操作,并支持链式操作 (pipelining),提高了代码的表达能力和效率。
深度解析:
① 范围的概念: 范围 (range) 是对序列 (sequence) 的抽象,表示一组可以迭代的元素。范围可以是容器、数组、或任何可以产生迭代器的对象。范围库基于迭代器 (iterators) 和视图 (views) 构建。
② 视图 (Views): 视图 (view) 是范围库的核心概念,表示对底层数据序列的转换或过滤操作,但视图本身不拥有数据,也不修改原始数据。视图是轻量级的、延迟求值 (lazy evaluation) 的。
▮▮▮▮⚝ 转换视图 (Transformation Views): 例如 std::views::transform
,将一个范围的元素经过函数转换后生成新的范围。
▮▮▮▮⚝ 过滤视图 (Filtering Views): 例如 std::views::filter
,根据谓词 (predicate) 过滤范围的元素,只保留满足条件的元素。
▮▮▮▮⚝ 取子范围视图 (Subrange Views): 例如 std::views::take
, std::views::drop
, std::views::slice
,从原始范围中取出一部分子范围。
▮▮▮▮⚝ 组合视图 (Combining Views): 例如 std::views::zip
, std::views::concat
,将多个范围组合成新的范围。
▮▮▮▮⚝ 自定义视图: 用户可以自定义视图,实现特定的数据处理逻辑。
③ 范围适配器 (Range Adaptors): 范围适配器是用于创建和组合视图的函数对象。范围适配器可以使用管道运算符 |
进行链式调用,将多个视图操作组合成一个复杂的数据处理流程。
1
#include <ranges>
2
#include <vector>
3
#include <iostream>
4
5
int main() {
6
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
7
8
// 链式范围操作:
9
// 1. 过滤偶数 (filter)
10
// 2. 每个元素平方 (transform)
11
// 3. 取前 5 个元素 (take)
12
auto processedNumbers = numbers
13
| std::views::filter([](int n){ return n % 2 == 0; }) // 过滤偶数
14
| std::views::transform([](int n){ return n * n; }) // 平方
15
| std::views::take(5); // 取前 5 个
16
17
// 遍历处理后的范围
18
for (int n : processedNumbers) {
19
std::cout << n << " "; // 输出:4 16 36 64 100
20
}
21
std::cout << std::endl;
22
23
return 0;
24
}
④ 范围算法 (Range Algorithms): 范围库提供了新的范围算法,例如 std::ranges::for_each
, std::ranges::transform
, std::ranges::filter
, std::ranges::sort
, std::ranges::copy
等,这些算法可以直接接受范围作为参数,而不需要传递迭代器对。
1
#include <ranges>
2
#include <vector>
3
#include <algorithm>
4
#include <iostream>
5
6
int main() {
7
std::vector<int> numbers = {5, 2, 8, 1, 9};
8
9
// 使用范围算法排序
10
std::ranges::sort(numbers); // 直接对范围 numbers 排序
11
12
// 使用范围算法打印
13
std::ranges::for_each(numbers, [](int n){ std::cout << n << " "; }); // 直接对范围 numbers 遍历
14
std::cout << std::endl;
15
return 0;
16
}
⑤ 延迟求值 (Lazy Evaluation): 范围视图是延迟求值的,只有在实际迭代范围元素时,才会进行计算。这可以提高效率,尤其是在处理大型数据集或复杂视图链时,可以避免不必要的计算。
⑥ 组合性和可读性: 范围库通过范围适配器和管道运算符,可以将多个数据处理操作组合成简洁、易读的链式表达式,提高了代码的表达能力和可维护性。
⑦ 优势:
▮▮▮▮⚝ 代码简洁: 简化了对数据序列的操作,使用范围算法和视图可以减少迭代器和循环控制代码的编写。
▮▮▮▮⚝ 提高代码可读性: 链式范围操作使数据处理流程更清晰、更易于理解。
▮▮▮▮⚝ 效率提升: 延迟求值避免了不必要的计算,提高了处理大型数据集的效率。
▮▮▮▮⚝ 更高级的抽象: 范围库提供了更高级的数据序列抽象,使开发者可以更专注于数据处理逻辑,而不是迭代器和循环的细节。
15.4.3 协程 (Coroutines)
概要: C++20 引入了协程 (coroutines),提供了一种实现协作式多任务 (cooperative multitasking) 的方式,可以编写异步 (asynchronous) 和非阻塞 (non-blocking) 的代码,简化了异步编程的复杂性。
深度解析:
① 协程的概念: 协程是一种可以暂停 (suspend) 和恢复 (resume) 执行的函数。与线程 (threads) 不同,协程的暂停和恢复是由程序员显式控制的,而不是由操作系统调度器 (scheduler) 抢占式 (preemptive) 调度。协程是轻量级的,上下文切换开销远小于线程。
② 协程的三个关键操作:
▮▮▮▮⚝ co_await
: 暂停协程的执行,等待异步操作完成。co_await
表达式会暂停当前协程,并将控制权返回给调用者或调度器,直到等待的条件满足 (例如,异步操作完成)。当条件满足时,协程从暂停点恢复执行。
▮▮▮▮⚝ co_yield
: 暂停协程的执行,并产生一个值。co_yield
表达式用于生成一个序列的值,每次 co_yield
都会暂停协程,并返回一个值给调用者。协程可以在下次调用时从暂停点恢复执行,生成下一个值。
▮▮▮▮⚝ co_return
: 完成协程的执行,并返回值 (可选)。co_return
表达式用于正常结束协程的执行,并可以返回一个值给调用者。
③ 协程的返回类型: 协程的返回类型需要满足特定的要求,通常需要返回以下类型之一:
▮▮▮▮⚝ std::future<T>
: 用于返回一个异步计算的结果,适用于启动异步任务并等待结果的场景。
▮▮▮▮⚝ std::task<T>
(或自定义的 task 类型): 类似于 std::future<T>
,但可以提供更灵活的控制和调度机制。
▮▮▮▮⚝ 范围 (ranges) 或迭代器 (iterators): 用于生成一个值序列,适用于生成器 (generator) 协程。
▮▮▮▮⚝ void
或自定义的 void-like
类型: 用于不返回值的协程,适用于启动异步操作但不关心结果的场景。
④ 协程示例:异步任务:
1
#include <iostream>
2
#include <future>
3
#include <chrono>
4
5
std::future<int> asyncTask() {
6
std::cout << "Async task started" << std::endl;
7
co_await std::chrono::seconds(2); // 模拟异步操作,暂停 2 秒
8
std::cout << "Async task resumed" << std::endl;
9
co_return 42; // 返回结果
10
}
11
12
int main() {
13
std::future<int> resultFuture = asyncTask(); // 启动协程
14
std::cout << "Waiting for result..." << std::endl;
15
int result = resultFuture.get(); // 等待协程完成并获取结果
16
std::cout << "Result from async task: " << result << std::endl;
17
return 0;
18
}
⑤ 协程示例:生成器 (Generator):
1
#include <iostream>
2
#include <coroutine>
3
4
struct Generator {
5
struct promise_type {
6
int current_value;
7
std::suspend_always yield_value(int value) {
8
current_value = value;
9
return {};
10
}
11
std::suspend_always initial_suspend() noexcept { return {}; }
12
std::suspend_always final_suspend() noexcept { return {}; }
13
Generator get_return_object() { return Generator{this}; }
14
void unhandled_exception() {}
15
void return_void() {}
16
};
17
18
using handle_type = std::coroutine_handle<promise_type>;
19
handle_type coroutine_handle;
20
21
Generator(promise_type* p) : coroutine_handle(handle_type::from_promise(*p)) {}
22
~Generator() { if (coroutine_handle) coroutine_handle.destroy(); }
23
24
struct iterator {
25
handle_type handle;
26
void operator++() { handle.resume(); }
27
int operator*() const { return handle.promise().current_value; }
28
bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
29
};
30
31
iterator begin() { return {coroutine_handle}; }
32
std::default_sentinel_t end() { return {}; }
33
};
34
35
Generator generateNumbers() {
36
for (int i = 0; i < 5; ++i) {
37
co_yield i; // 产生值 i
38
}
39
co_return;
40
}
41
42
int main() {
43
for (int number : generateNumbers()) { // 迭代生成器
44
std::cout << number << " "; // 输出:0 1 2 3 4
45
}
46
std::cout << std::endl;
47
return 0;
48
}
⑥ 异步编程简化: 协程可以显著简化异步编程的复杂性,避免传统回调函数 (callbacks) 或 future
/promise
模式中代码分散、难以维护的问题,使异步代码更接近同步代码的编写风格。
⑦ 性能优势: 协程的上下文切换开销远小于线程,可以提高并发性能,尤其是在 I/O 密集型应用中,可以更高效地处理大量并发请求。
⑧ 优势:
▮▮▮▮⚝ 简化异步编程: 使异步代码更易于编写和理解,减少异步编程的复杂性。
▮▮▮▮⚝ 提高并发性能: 协程的轻量级特性使其在并发场景下比线程更高效。
▮▮▮▮⚝ 代码可读性提升: 异步代码更接近同步代码的风格,提高了代码的可读性和可维护性。
▮▮▮▮⚝ 更好的资源利用: 协程可以更有效地利用系统资源,提高程序的整体效率。
15.4.4 模块 (Modules)
概要: C++20 引入了模块 (modules) 系统,旨在替代传统的头文件 (header files) 包含机制,解决头文件包含导致的编译时间长、宏污染 (macro pollution)、命名冲突 (name clashes) 等问题,提高代码的模块化和编译效率。
深度解析:
① 头文件包含机制的问题: 传统的头文件包含机制存在以下问题:
▮▮▮▮⚝ 编译时间长: 头文件会被重复包含到多个源文件中,导致编译器需要重复解析和编译头文件内容,增加编译时间。
▮▮▮▮⚝ 宏污染: 头文件中定义的宏 (macros) 会影响包含该头文件的所有源文件,可能导致意外的宏替换和命名冲突。
▮▮▮▮⚝ 命名冲突: 头文件中声明的全局命名空间中的名称可能与其他头文件或源文件中的名称冲突。
▮▮▮▮⚝ 脆弱的包含依赖: 头文件之间的包含关系复杂,修改一个头文件可能导致大量源文件需要重新编译。
② 模块的引入: 模块 (module) 是一种新的代码组织和编译单元,提供了更好的代码封装和编译隔离。模块可以导出 (export) 一部分声明,供其他模块或源文件使用,同时隐藏模块的内部实现细节。
③ 模块的定义和使用:
▮▮▮▮⚝ 模块接口单元 (Module Interface Unit): 定义模块的接口,声明模块导出的内容。模块接口单元的文件扩展名通常是 .ixx
或 .cppm
。
1
// my_module.ixx (模块接口单元)
2
export module my_module; // 声明模块名
3
4
export int add(int a, int b); // 导出函数声明
5
6
namespace my_module {
7
export const double pi = 3.1415926; // 导出命名空间中的常量
8
}
▮▮▮▮⚝ 模块实现单元 (Module Implementation Unit): 实现模块接口单元中声明的导出内容,以及模块的内部实现细节。模块实现单元的文件扩展名通常是 .cpp
。
1
// my_module.cpp (模块实现单元)
2
module my_module; // 声明模块名
3
4
int add(int a, int b) { // 实现导出的函数
5
return a + b;
6
}
7
8
namespace my_module {
9
// 模块内部实现细节,不导出
10
int internalFunction() { return 10; }
11
}
▮▮▮▮⚝ 模块导入 (Module Import): 使用 import
声明导入模块,替代传统的 #include
指令。
1
// main.cpp
2
import my_module; // 导入模块
3
4
int main() {
5
int result = my_module::add(5, 3); // 使用模块导出的函数
6
double piValue = my_module::pi; // 使用模块导出的常量
7
// my_module::internalFunction(); // 错误:无法访问模块内部实现细节
8
9
return 0;
10
}
④ 模块编译和链接: 编译器需要支持模块的编译和链接。模块编译通常会生成模块接口文件 (module interface file),用于描述模块的导出接口。在编译导入模块的源文件时,编译器会读取模块接口文件,进行类型检查和代码生成。链接器 (linker) 需要支持模块的链接,将模块编译生成的目标文件链接到最终的可执行文件或库中。
⑤ 模块和命名空间 (Namespaces): 模块和命名空间是正交 (orthogonal) 的概念,模块用于代码组织和编译隔离,命名空间用于避免命名冲突。模块可以导出命名空间中的内容,也可以在模块内部使用命名空间。
⑥ 模块和宏 (Macros): 模块系统可以有效地解决宏污染问题。模块内部定义的宏不会泄漏到模块外部,模块导入不会引入模块导出接口之外的宏定义。
⑦ 模块和模板 (Templates): 模块可以导出模板,但模板的实例化仍然需要在使用模板的代码中进行。模块接口单元需要包含模板的完整定义,以便编译器在模块外部实例化模板。
⑧ 模块的优势:
▮▮▮▮⚝ 提高编译速度: 模块的编译隔离和模块接口文件的使用可以减少重复编译,提高编译速度,尤其是在大型项目中效果显著。
▮▮▮▮⚝ 解决宏污染和命名冲突: 模块系统可以有效地解决头文件包含导致的宏污染和命名冲突问题,提高代码的可靠性和可维护性。
▮▮▮▮⚝ 增强代码封装性: 模块可以更好地封装代码,隐藏内部实现细节,只导出必要的接口,提高代码的模块化程度。
▮▮▮▮⚝ 改善代码组织结构: 模块系统提供了一种更清晰、更结构化的代码组织方式,使大型项目更易于管理和维护。
15.4.5 三路比较运算符 (Spaceship Operator) <=>
概要: C++20 引入了三路比较运算符 <=>
(也称为 spaceship operator),用于简化类型的比较操作,可以一次性完成小于、等于和大于三种比较,并支持自动生成各种比较运算符 (例如 <
, >
, <=
, >=
, ==
, !=
)。
深度解析:
① 传统比较运算符的问题: 在 C++20 之前,需要为自定义类型手动重载 (overload) 各种比较运算符 (<
, >
, <=
, >=
, ==
, !=
),代码冗余且容易出错。例如,为了支持类型的全序关系 (total ordering),通常需要重载所有六个比较运算符。
② 三路比较运算符 <=>
的引入: 三路比较运算符 <=>
可以一次性完成小于、等于和大于三种比较,并返回一个比较结果类别 (comparison category) 对象,表示比较结果是小于、等于、大于还是无法比较。
③ 比较结果类别 (Comparison Category Types): C++20 标准库定义了以下比较结果类别类型,定义在 <compare>
头文件中:
▮▮▮▮⚝ std::strong_ordering
: 表示强序关系 (strong ordering)。如果 a <=> b
返回 std::strong_ordering::equal
,则 a == b
为真;如果 a <=> b
返回 std::strong_ordering::less
,则 a < b
为真;如果 a <=> b
返回 std::strong_ordering::greater
,则 a > b
为真。强序关系要求如果 a == b
,则 a
和 b
在所有方面都相等,可以互相替换。例如,整数、浮点数通常具有强序关系。
▮▮▮▮⚝ std::weak_ordering
: 表示弱序关系 (weak ordering)。弱序关系允许即使 a <=> b
返回 std::weak_ordering::equivalent
,a
和 b
仍然可能不完全相等。例如,浮点数的 NaN (Not-a-Number) 值,字符串的大小写不敏感比较等。
▮▮▮▮⚝ std::partial_ordering
: 表示偏序关系 (partial ordering)。偏序关系允许比较结果是不确定的 (unordered)。例如,浮点数包含 NaN 值时,NaN <=> x
的结果可能是 std::partial_ordering::unordered
。
▮▮▮▮⚝ std::equal_ordering
: 表示相等关系 (equality ordering),只区分相等和不相等,不区分小于和大于。例如,只关心对象是否相等,而不需要排序的场景。
④ <=>
运算符的重载: 可以为自定义类型重载 <=>
运算符,并指定返回的比较结果类别。编译器可以根据 <=>
运算符的重载自动生成其他比较运算符 (<
, >
, <=
, >=
, ==
, !=
)。
1
#include <compare>
2
3
class Point {
4
public:
5
int x;
6
int y;
7
8
// 重载三路比较运算符 <=>,返回 strong_ordering
9
auto operator<=>(const Point& other) const = default; // 使用 = default 自动生成 <=> 运算符
10
11
// 如果不使用 = default,可以手动实现 <=> 运算符
12
// std::strong_ordering operator<=>(const Point& other) const {
13
// if (x != other.x) return x <=> other.x;
14
// return y <=> other.y;
15
// }
16
};
17
18
int main() {
19
Point p1{1, 2};
20
Point p2{1, 3};
21
Point p3{2, 2};
22
23
std::cout << (p1 < p2) << std::endl; // 自动生成 < 运算符
24
std::cout << (p1 <= p2) << std::endl; // 自动生成 <= 运算符
25
std::cout << (p1 == p2) << std::endl; // 自动生成 == 运算符
26
std::cout << (p1 != p2) << std::endl; // 自动生成 != 运算符
27
std::cout << (p3 > p2) << std::endl; // 自动生成 > 运算符
28
std::cout << (p3 >= p2) << std::endl; // 自动生成 >= 运算符
29
30
return 0;
31
}
使用 = default
可以让编译器自动生成 <=>
运算符的默认实现,默认实现会按照类成员的声明顺序进行字典序比较。
⑤ 默认比较运算符生成: 当重载了 <=>
运算符后,编译器可以根据 <=>
运算符的返回类型自动生成以下比较运算符:
▮▮▮▮⚝ 如果 <=>
返回 std::strong_ordering
或 std::weak_ordering
: 自动生成 <
, >
, <=
, >=
, ==
, !=
运算符。
▮▮▮▮⚝ 如果 <=>
返回 std::partial_ordering
: 自动生成 <
, >
, <=
, >=
, ==
, !=
运算符。
▮▮▮▮⚝ 如果 <=>
返回 std::equal_ordering
: 自动生成 ==
, !=
运算符。
⑥ 优势:
▮▮▮▮⚝ 代码简洁: 只需重载一个三路比较运算符 <=>
,即可自动生成其他比较运算符,减少了代码冗余。
▮▮▮▮⚝ 提高代码可读性: 使用 <=>
运算符可以更清晰地表达类型的比较语义。
▮▮▮▮⚝ 减少错误: 自动生成比较运算符可以减少手动重载比较运算符时可能出现的错误。
▮▮▮▮⚝ 支持自定义比较逻辑: 可以根据需要自定义 <=>
运算符的实现,并选择合适的比较结果类别,灵活地控制类型的比较行为。
16. 第16章的标题
16.1 项目实战的重要性 (Importance of Project Practice)
16.2 项目案例选择 (Project Case Selection)
16.3 案例一:简易文本编辑器 (Case Study 1: Simple Text Editor)
16.3.1 项目描述与需求分析 (Project Description and Requirements Analysis)
16.3.2 系统设计 (System Design)
16.3.3 核心功能模块设计 (Core Function Module Design)
16.3.4 编码实现 (Coding Implementation)
16.3.5 测试与调试 (Testing and Debugging)
16.3.6 案例总结与扩展 (Case Summary and Extensions)
16.4 案例二:学生信息管理系统 (Case Study 2: Student Information Management System)
16.4.1 项目描述与需求分析 (Project Description and Requirements Analysis)
16.4.2 系统设计 (System Design)
16.4.3 核心功能模块设计 (Core Function Module Design)
16.4.4 编码实现 (Coding Implementation)
16.4.5 测试与调试 (Testing and Debugging)
16.4.6 案例总结与扩展 (Case Summary and Extensions)
16.5 通用项目开发最佳实践 (General Project Development Best Practices)
16.6 更多项目实践建议 (Further Project Practice Suggestions)
16.1 项目实战的重要性 (Importance of Project Practice)
① 理论与实践结合:强调学习编程语言不仅仅是掌握语法 (grammar),更重要的是能够运用所学知识解决实际问题。通过项目实战,可以将书本上学到的理论知识 (theoretical knowledge) 应用到实际编码中,加深理解和记忆。
② 提升问题解决能力 (Problem-Solving Skills):实际项目开发往往充满挑战,需要程序员独立分析问题、设计解决方案、并将其转化为可执行的代码。这个过程能够有效锻炼和提升问题解决能力,培养编程思维 (programming thinking)。
③ 掌握软件开发流程 (Software Development Process):项目实战能够让学习者亲身体验软件开发的完整流程,包括需求分析 (requirements analysis)、系统设计 (system design)、编码实现 (coding implementation)、测试 (testing) 与调试 (debugging)、以及项目维护 (project maintenance) 等环节,为未来参与实际项目开发打下基础。
④ 增强代码编写能力 (Coding Skills):通过大量的编码实践,可以提高代码编写的熟练度和效率,掌握良好的编程习惯 (programming habits) 和代码风格 (coding style),编写出更规范、更高效、更易于维护的代码。
⑤ 积累项目经验 (Project Experience):完成项目实战是积累项目经验最直接的方式。拥有一两个完整的项目经验,能够显著提升个人竞争力,在求职 (job seeking) 或职业发展 (career development) 中更具优势。
⑥ 激发学习兴趣 (Stimulate Learning Interest):相较于枯燥的语法学习,项目实战往往更具趣味性和挑战性。成功完成一个项目,能够带来成就感,激发学习热情,形成正向循环。
16.2 项目案例选择 (Project Case Selection)
① 案例选择原则 (Principles for Case Selection):
▮▮▮▮ⓑ 目标明确 (Clear Objectives):项目案例应具有明确的学习目标,例如,练习特定C++语法特性、掌握某种编程技巧、或解决特定领域的问题。目标明确的项目案例能够帮助学习者更Focused地进行实践。
▮▮▮▮ⓒ 难度适中 (Moderate Difficulty):项目案例的难度应适中,既不能过于简单,缺乏挑战性,也不能过于复杂,超出学习者当前的能力范围。合适的难度能够保持学习者的学习兴趣和信心。
▮▮▮▮ⓓ 实用性强 (Practicality):选择具有一定实用价值或贴近实际应用场景的案例,能够提高学习者的学习兴趣和动力。例如,开发一个实用的工具软件或小游戏等。
▮▮▮▮ⓔ 可扩展性 (Scalability):优秀的项目案例应具有一定的可扩展性,方便学习者在完成基本功能后,根据自身兴趣和能力,进行功能扩展和优化,持续深入学习。
▮▮▮▮ⓕ 代表性 (Representativeness):案例应具有一定的代表性,能够涵盖C++语言的核心知识点和常用编程技巧,例如,面向对象编程 (Object-Oriented Programming, OOP)、数据结构 (data structures)、算法 (algorithms)、文件操作 (file operations)、异常处理 (exception handling) 等。
② 案例类型建议 (Suggestions for Case Types):
▮▮▮▮ⓑ 控制台应用程序 (Console Applications):适合初学者入门,例如,文本处理工具、命令行计算器、简单游戏等。控制台程序开发周期短, focus在逻辑实现上,无需过多关注图形界面 (Graphical User Interface, GUI) 设计。
▮▮▮▮ⓒ 桌面应用程序 (Desktop Applications):可以选用一些轻量级的GUI框架 (GUI frameworks) (例如,Qt, wxWidgets) 进行开发,例如,简单的记事本、图片浏览器、音乐播放器等。桌面应用程序能够让学习者接触到事件驱动编程 (event-driven programming) 和用户界面设计 (User Interface design)。
▮▮▮▮ⓓ 小型游戏 (Small Games):例如,猜数字游戏、扫雷 (Minesweeper)、俄罗斯方块 (Tetris) 等。游戏开发能够锻炼逻辑思维 (logical thinking) 和算法设计能力,同时具有较高的趣味性。
▮▮▮▮ⓔ 实用工具 (Practical Tools):例如,文件批量重命名工具、日志分析工具、简易数据库 (simple database) 等。开发实用工具能够将编程知识应用于解决实际问题,提高学习的应用价值感。
▮▮▮▮ⓕ 网络编程 (Network Programming) 案例:例如,简单的客户端/服务器 (client/server) 程序、网络聊天程序等。网络编程案例能够让学习者了解网络通信 (network communication) 的基本原理和技术。
③ 本书案例选择 (Case Selection in This Book):
▮▮▮▮本书选择简易文本编辑器和学生信息管理系统两个案例,旨在覆盖C++基础语法 (basic syntax)、面向对象编程 (OOP) 核心概念、文件操作、以及标准模板库 (Standard Template Library, STL) 的应用。这两个案例难度适中,实用性较强,且具有一定的扩展性,适合不同阶段的学习者进行实践。
16.3 案例一:简易文本编辑器 (Case Study 1: Simple Text Editor)
16.3.1 项目描述与需求分析 (Project Description and Requirements Analysis)
① 项目描述 (Project Description):
▮▮▮▮开发一个简易的文本编辑器,该编辑器应具备基本的文本编辑功能,例如,新建、打开、保存文件,文本输入、编辑、查找、替换,以及简单的格式设置 (例如,字体大小、颜色等)。该编辑器将以控制台应用程序的形式实现,重点在于实现核心的文本处理逻辑。
② 需求分析 (Requirements Analysis):
▮▮▮▮ⓑ 基本功能需求 (Basic Functionality Requirements):
▮▮▮▮▮▮▮▮❸ 新建文件 (New File):允许用户创建一个新的空白文本文件。
▮▮▮▮▮▮▮▮❹ 打开文件 (Open File):允许用户打开已存在的文本文件 (支持常见文本文件格式,例如,.txt
)。
▮▮▮▮▮▮▮▮❺ 保存文件 (Save File):允许用户保存当前编辑的文件 (支持保存为 .txt
格式)。
▮▮▮▮▮▮▮▮❻ 另存为 (Save As):允许用户将当前编辑的文件另存为新的文件。
▮▮▮▮▮▮▮▮❼ 文本输入与编辑 (Text Input and Editing):允许用户在编辑器中输入和编辑文本内容,包括字符输入、删除、插入、换行等基本编辑操作。
▮▮▮▮▮▮▮▮❽ 查找 (Find):允许用户在当前文本中查找指定的字符串 (string)。
▮▮▮▮▮▮▮▮❾ 替换 (Replace):允许用户将当前文本中指定的字符串替换为新的字符串。
▮▮▮▮▮▮▮▮❿ 退出 (Exit):允许用户安全退出文本编辑器程序。
▮▮▮▮ⓑ 可选功能需求 (Optional Functionality Requirements) (可以作为扩展功能):
▮▮▮▮▮▮▮▮❷ 撤销 (Undo) / 重做 (Redo):支持文本编辑操作的撤销和重做功能。
▮▮▮▮▮▮▮▮❸ 剪切 (Cut) / 复制 (Copy) / 粘贴 (Paste):支持文本的剪切、复制和粘贴操作。
▮▮▮▮▮▮▮▮❹ 字体 (Font) 和颜色 (Color) 设置:允许用户设置文本的字体和颜色 (在控制台环境下,可能仅限简单的颜色设置)。
▮▮▮▮▮▮▮▮❺ 行号显示 (Line Number Display):在文本编辑器中显示行号。
▮▮▮▮▮▮▮▮❻ 字数统计 (Word Count):统计当前文本的字数、字符数等信息。
▮▮▮▮ⓒ 非功能需求 (Non-functional Requirements):
▮▮▮▮▮▮▮▮❷ 易用性 (Usability):程序操作应简单直观,用户界面友好 (即使是控制台程序)。
▮▮▮▮▮▮▮▮❸ 稳定性 (Stability):程序运行稳定可靠,不易崩溃 (crash)。
▮▮▮▮▮▮▮▮❹ 效率 (Efficiency):程序运行效率较高,文本处理速度快。
▮▮▮▮▮▮▮▮❺ 可维护性 (Maintainability):代码结构清晰,模块化 (modularized) 设计,易于维护和扩展。
16.3.2 系统设计 (System Design)
① 系统架构设计 (System Architecture Design):
▮▮▮▮采用模块化设计思想 (modular design),将文本编辑器系统划分为若干个功能模块,例如:
▮▮▮▮ⓐ 用户界面模块 (User Interface Module):负责接收用户输入,显示程序输出,与用户进行交互。对于控制台程序,用户界面主要通过命令行 (command line) 菜单和提示信息实现。
▮▮▮▮ⓑ 文件操作模块 (File Operation Module):负责文件的新建、打开、保存、另存为等文件操作。
▮▮▮▮ⓒ 文本编辑模块 (Text Editing Module):负责文本的输入、编辑、查找、替换等核心文本处理功能。
▮▮▮▮ⓓ 数据存储模块 (Data Storage Module):负责在内存中存储和管理文本数据。可以使用 std::vector<std::string>
或 std::list<std::string>
等容器 (containers) 来存储文本行。
\[ \text{简易文本编辑器系统架构} \]
\[ \begin{tikzcd} \text{用户} \arrow[r, "\text{用户交互}"] & \text{用户界面模块} \arrow[r, "\text{命令调用}"] \arrow[d, "\text{数据传递}"] & \text{文件操作模块} \arrow[d, "\text{数据传递}"] \\ & \text{文本编辑模块} \arrow[r, "\text{数据操作}"] & \text{数据存储模块} \end{tikzcd} \]
② 类设计 (Class Design) (如果采用面向对象方法):
▮▮▮▮可以考虑设计以下几个类 (classes) 来组织代码:
▮▮▮▮ⓐ TextEditor
类:核心编辑器类,负责协调各个模块,实现文本编辑器的主要功能。包含用户界面模块、文件操作模块、文本编辑模块的实例 (instances)。
▮▮▮▮ⓑ Document
类:文档类,负责存储和管理文本数据。可以使用 std::vector<std::string>
来存储文本内容,并提供访问和修改文本的方法。
▮▮▮▮ⓒ FileManager
类:文件管理类,负责文件的读写操作。封装文件操作的具体实现,例如,使用 std::ifstream
和 std::ofstream
进行文件读写。
▮▮▮▮ⓓ UserInterface
类:用户界面类,负责处理用户输入和程序输出。提供菜单显示、命令解析、以及信息提示等功能。
③ 数据结构选择 (Data Structure Selection):
▮▮▮▮ⓑ 文本数据存储:使用 std::vector<std::string>
或 std::list<std::string>
来存储文本内容。std::vector
适合随机访问 (random access) 文本行,std::list
适合频繁的插入和删除操作。由于文本编辑器可能需要频繁地插入和删除行,std::list
可能是更合适的选择,但 std::vector
在内存连续性方面具有优势,可以根据具体需求权衡选择。
▮▮▮▮ⓒ 文件名存储:使用 std::string
存储当前打开的文件名和路径。
16.3.3 核心功能模块设计 (Core Function Module Design)
① 文件操作模块设计 (File Operation Module Design):
▮▮▮▮ⓑ 新建文件功能 (New File Function):
▮▮▮▮▮▮▮▮❸ 清空 (clear) 当前文档 (Document
) 对象中的文本数据。
▮▮▮▮▮▮▮▮❹ 将当前文件名设置为空字符串 (empty string),表示当前为新文件,尚未保存。
▮▮▮▮ⓔ 打开文件功能 (Open File Function):
▮▮▮▮▮▮▮▮❻ 接收用户输入的文件路径。
▮▮▮▮▮▮▮▮❼ 使用 std::ifstream
打开指定文件。
▮▮▮▮▮▮▮▮❽ 逐行读取文件内容,将每行文本存储到 Document
对象中的文本数据容器中 (std::vector<std::string>
或 std::list<std::string>
)。
▮▮▮▮▮▮▮▮❾ 如果文件打开失败,提示用户错误信息 (error message)。
▮▮▮▮▮▮▮▮❿ 将当前文件名设置为打开文件的路径。
▮▮▮▮ⓚ 保存文件功能 (Save File Function):
▮▮▮▮▮▮▮▮❶ 判断当前是否已经打开或新建了文件 (文件名是否为空)。
▮▮▮▮▮▮▮▮❷ 如果是新文件,提示用户输入文件名,并调用 “另存为” 功能。
▮▮▮▮▮▮▮▮❸ 如果是已打开的文件,使用 std::ofstream
以覆盖模式 (overwrite mode) 打开当前文件。
▮▮▮▮▮▮▮▮❹ 将 Document
对象中的文本数据逐行写入文件。
▮▮▮▮▮▮▮▮❺ 如果文件保存失败,提示用户错误信息。
▮▮▮▮ⓠ 另存为功能 (Save As Function):
▮▮▮▮▮▮▮▮❶ 接收用户输入的新文件名和路径。
▮▮▮▮▮▮▮▮❷ 使用 std::ofstream
打开指定文件。
▮▮▮▮▮▮▮▮❸ 将 Document
对象中的文本数据逐行写入文件。
▮▮▮▮▮▮▮▮❹ 如果文件保存失败,提示用户错误信息。
▮▮▮▮▮▮▮▮❺ 将当前文件名更新为新保存的文件路径。
② 文本编辑模块设计 (Text Editing Module Design):
▮▮▮▮ⓑ 文本输入与编辑功能 (Text Input and Editing Function):
▮▮▮▮▮▮▮▮❸ 用户在控制台输入文本时,将输入的字符 (characters) 追加到当前行的字符串 (string) 中。
▮▮▮▮▮▮▮▮❹ 当用户按下回车键 (Enter key) 时,表示当前行输入结束,将当前行字符串添加到 Document
对象的文本数据容器中,并开始新的行输入。
▮▮▮▮▮▮▮▮❺ 支持退格键 (Backspace key) 删除字符,删除当前行末尾的字符。如果当前行已为空,且不是第一行,则删除当前行,并将光标 (cursor) 移动到上一行末尾。
▮▮▮▮▮▮▮▮❻ 支持删除键 (Delete key) 删除字符 (可选功能)。
▮▮▮▮▮▮▮▮❼ 支持方向键 (arrow keys) 移动光标 (可选功能,控制台程序实现较为复杂)。
▮▮▮▮ⓗ 查找功能 (Find Function):
▮▮▮▮▮▮▮▮❾ 接收用户输入的查找字符串。
▮▮▮▮▮▮▮▮❿ 遍历 Document
对象中的每一行文本。
▮▮▮▮▮▮▮▮❸ 在每一行中使用字符串查找算法 (例如,std::string::find()
) 查找目标字符串。
▮▮▮▮▮▮▮▮❹ 如果找到目标字符串,在用户界面中高亮显示 (console程序高亮显示较为困难,可以简单地输出包含目标字符串的行号和内容)。
▮▮▮▮▮▮▮▮❺ 可以支持查找下一个、查找上一个功能 (可选功能)。
▮▮▮▮ⓝ 替换功能 (Replace Function):
▮▮▮▮▮▮▮▮❶ 接收用户输入的查找字符串和替换字符串。
▮▮▮▮▮▮▮▮❷ 遍历 Document
对象中的每一行文本。
▮▮▮▮▮▮▮▮❸ 在每一行中使用字符串替换算法 (例如,std::string::replace()
或 std::regex_replace()
) 将所有匹配的查找字符串替换为替换字符串。
▮▮▮▮▮▮▮▮❹ 可以支持只替换第一个匹配项、替换所有匹配项、替换前询问等选项 (可选功能)。
③ 用户界面模块设计 (User Interface Module Design):
▮▮▮▮ⓑ 主菜单 (Main Menu):
1
****************************
2
* 简易文本编辑器 *
3
****************************
4
1. 新建 (New)
5
2. 打开 (Open)
6
3. 保存 (Save)
7
4. 另存为 (Save As)
8
5. 查找 (Find)
9
6. 替换 (Replace)
10
7. 退出 (Exit)
11
****************************
12
请选择操作 (Please select operation):
▮▮▮▮ⓑ 命令处理 (Command Handling):
▮▮▮▮▮▮▮▮❷ 接收用户输入的菜单选项 (1-7)。
▮▮▮▮▮▮▮▮❸ 使用 switch
语句或 if-else if-else
语句根据用户选择调用相应的处理函数 (例如,选择 1 调用新建文件函数,选择 2 调用打开文件函数等)。
▮▮▮▮▮▮▮▮❹ 对于需要用户进一步输入的命令 (例如,打开、保存、查找、替换),提示用户输入必要的信息 (例如,文件名、查找字符串等)。
▮▮▮▮ⓔ 信息提示 (Information Prompts):
▮▮▮▮▮▮▮▮❻ 在文件操作成功或失败时,给出相应的提示信息 (例如,"文件保存成功!", "文件打开失败!")。
▮▮▮▮▮▮▮▮❼ 在查找或替换操作后,给出操作结果的提示信息 (例如,"找到 3 处匹配!", "替换完成!")。
▮▮▮▮▮▮▮▮❽ 在程序运行过程中,给出必要的帮助信息和操作指南。
16.3.4 编码实现 (Coding Implementation)
① 开发环境准备 (Development Environment Preparation):
▮▮▮▮确保已安装C++编译器 (Compiler) (例如,GCC, Clang, MSVC) 和集成开发环境 (Integrated Development Environment, IDE) (例如,VS Code, Visual Studio, CLion)。配置好C++开发环境。
② 创建项目 (Create Project):
▮▮▮▮在IDE中创建一个新的C++控制台应用程序项目 (Console Application Project)。
③ 编写代码 (Write Code):
▮▮▮▮按照系统设计和模块设计,逐步实现各个功能模块的代码。
▮▮▮▮代码示例 (核心代码片段,完整代码请参考附录或示例代码):
1
#include <iostream>
2
#include <fstream>
3
#include <string>
4
#include <vector>
5
#include <list>
6
7
class Document {
8
public:
9
std::list<std::string> lines; // 使用 std::list 存储文本行
10
std::string filename;
11
12
Document() : filename("") {}
13
14
void clear() {
15
lines.clear();
16
filename = "";
17
}
18
19
bool loadFromFile(const std::string& filePath) {
20
std::ifstream inputFile(filePath);
21
if (!inputFile.is_open()) {
22
return false; // 文件打开失败
23
}
24
lines.clear();
25
std::string line;
26
while (std::getline(inputFile, line)) {
27
lines.push_back(line);
28
}
29
filename = filePath;
30
return true; // 文件加载成功
31
}
32
33
bool saveToFile(const std::string& filePath) {
34
std::ofstream outputFile(filePath);
35
if (!outputFile.is_open()) {
36
return false; // 文件打开失败
37
}
38
for (const auto& line : lines) {
39
outputFile << line << std::endl;
40
}
41
filename = filePath;
42
return true; // 文件保存成功
43
}
44
45
// ... 其他文本编辑操作方法 (例如,查找、替换、插入、删除行等) ...
46
47
};
48
49
class TextEditor {
50
public:
51
Document currentDocument;
52
UserInterface ui; // 假设存在 UserInterface 类
53
54
void run() {
55
while (true) {
56
ui.showMainMenu(); // 显示主菜单
57
int choice = ui.getMenuChoice(); // 获取用户选择
58
59
switch (choice) {
60
case 1: // 新建
61
currentDocument.clear();
62
ui.showMessage("新建文件成功!");
63
break;
64
case 2: // 打开
65
{
66
std::string filePath = ui.getInput("请输入文件路径: ");
67
if (currentDocument.loadFromFile(filePath)) {
68
ui.showMessage("文件打开成功!");
69
} else {
70
ui.showError("文件打开失败!");
71
}
72
}
73
break;
74
case 3: // 保存
75
if (currentDocument.filename.empty()) {
76
std::string filePath = ui.getInput("请输入保存文件路径: ");
77
if (currentDocument.saveToFile(filePath)) {
78
ui.showMessage("文件保存成功!");
79
} else {
80
ui.showError("文件保存失败!");
81
}
82
} else {
83
if (currentDocument.saveToFile(currentDocument.filename)) {
84
ui.showMessage("文件保存成功!");
85
} else {
86
ui.showError("文件保存失败!");
87
}
88
}
89
break;
90
case 4: // 另存为
91
{
92
std::string filePath = ui.getInput("请输入另存为文件路径: ");
93
if (currentDocument.saveToFile(filePath)) {
94
ui.showMessage("文件另存为成功!");
95
} else {
96
ui.showError("文件另存为失败!");
97
}
98
}
99
break;
100
case 5: // 查找
101
{
102
std::string searchText = ui.getInput("请输入查找内容: ");
103
// ... 实现查找功能 ...
104
}
105
break;
106
case 6: // 替换
107
{
108
std::string searchText = ui.getInput("请输入要替换的内容: ");
109
std::string replaceText = ui.getInput("请输入替换为的内容: ");
110
// ... 实现替换功能 ...
111
}
112
break;
113
case 7: // 退出
114
ui.showMessage("感谢使用,再见!");
115
return;
116
default:
117
ui.showError("无效的选项,请重新选择!");
118
break;
119
}
120
// ... 显示当前文档内容 (可选) ...
121
}
122
}
123
};
124
125
int main() {
126
TextEditor editor;
127
editor.run();
128
return 0;
129
}
④ 代码组织 (Code Organization):
▮▮▮▮将代码按照模块 (modules) 分文件组织,例如,document.h/cpp
(Document 类), file_manager.h/cpp
(FileManager 类), user_interface.h/cpp
(UserInterface 类), text_editor.h/cpp
(TextEditor 类), main.cpp
(主程序入口)。 使用头文件 (header files) 声明类和函数 (functions),在源文件 (source files) 中实现具体功能。
16.3.5 测试与调试 (Testing and Debugging)
① 单元测试 (Unit Testing):
▮▮▮▮对各个功能模块 (例如,文件操作模块、文本编辑模块) 进行单元测试,确保每个模块的功能正确性 (correctness)。可以使用C++的单元测试框架 (unit testing frameworks) (例如,Google Test, Catch2),编写测试用例 (test cases) 进行自动化测试 (automated testing)。
② 集成测试 (Integration Testing):
▮▮▮▮将各个模块集成起来进行集成测试,测试模块之间的协同工作是否正常,数据传递是否正确。
③ 系统测试 (System Testing):
▮▮▮▮进行全面的系统测试,模拟用户实际使用场景,测试文本编辑器的所有功能是否符合需求,程序运行是否稳定可靠。
④ 调试技巧 (Debugging Techniques):
▮▮▮▮使用IDE的调试器 (debugger) 进行代码调试,设置断点 (breakpoints),单步执行 (step-by-step execution),观察变量 (variables) 值,定位和解决程序错误 (bugs)。
▮▮▮▮使用日志 (logging) 输出调试信息,例如,在关键代码段 (critical code sections) 输出变量值、函数调用信息等,辅助bug定位。
▮▮▮▮学习使用代码静态分析工具 (static code analysis tools) (例如,clang-tidy, cppcheck) 检查代码潜在的错误和代码风格问题。
16.3.6 案例总结与扩展 (Case Summary and Extensions)
① 案例总结 (Case Summary):
▮▮▮▮本案例通过开发一个简易文本编辑器,综合运用了C++基础语法、面向对象编程思想 (如果采用面向对象设计)、文件操作、字符串处理、以及控制台输入输出等知识。学习者通过完成本案例,可以巩固C++语言基础,提升项目开发能力,了解软件开发的基本流程。
② 扩展功能建议 (Extension Functionality Suggestions):
▮▮▮▮ⓑ 更完善的文本编辑功能:例如,实现撤销/重做、剪切/复制/粘贴、更丰富的文本格式设置 (字体、颜色、对齐方式等)、行号显示、字数统计等功能。
▮▮▮▮ⓒ 图形用户界面 (GUI):将控制台程序升级为图形界面程序,使用GUI框架 (例如,Qt, wxWidgets) 开发更友好的用户界面,提供更丰富的交互方式。
▮▮▮▮ⓓ 语法高亮 (Syntax Highlighting):为不同的编程语言实现语法高亮显示功能,将文本编辑器扩展为代码编辑器。
▮▮▮▮ⓔ 自动完成 (Auto-completion):实现代码自动完成功能,提高代码编辑效率 (代码编辑器扩展功能)。
▮▮▮▮ⓕ 插件扩展 (Plug-in Extension):设计插件架构 (plug-in architecture),允许用户通过插件扩展编辑器的功能。
▮▮▮▮ⓖ 网络协作 (Network Collaboration):实现多人在线协作编辑功能,将单机文本编辑器扩展为网络协作编辑器。
16.4 案例二:学生信息管理系统 (Case Study 2: Student Information Management System)
16.4.1 项目描述与需求分析 (Project Description and Requirements Analysis)
① 项目描述 (Project Description):
▮▮▮▮开发一个学生信息管理系统,用于管理学生的基本信息。该系统应具备学生信息录入、查询、修改、删除功能,以及学生成绩管理、信息统计等功能。系统数据可以持久化存储到文件中。
② 需求分析 (Requirements Analysis):
▮▮▮▮ⓑ 基本功能需求 (Basic Functionality Requirements):
▮▮▮▮▮▮▮▮❸ 学生信息录入 (Student Information Input):录入学生的基本信息,包括学号 (student ID)、姓名 (name)、性别 (gender)、年龄 (age)、班级 (class)、联系方式 (contact information) 等。
▮▮▮▮▮▮▮▮❹ 学生信息查询 (Student Information Query):根据学号或姓名查询学生信息。支持精确查询和模糊查询 (例如,姓名模糊查询)。
▮▮▮▮▮▮▮▮❺ 学生信息修改 (Student Information Modification):修改已录入的学生信息。
▮▮▮▮▮▮▮▮❻ 学生信息删除 (Student Information Deletion):根据学号删除学生信息。
▮▮▮▮▮▮▮▮❼ 学生信息列表显示 (Student Information List Display):显示所有学生的信息列表。
▮▮▮▮▮▮▮▮❽ 学生成绩录入 (Student Grade Input):录入学生的课程 (course) 成绩 (grade)。一个学生可以有多门课程成绩。
▮▮▮▮▮▮▮▮❾ 学生成绩查询 (Student Grade Query):根据学号查询学生的课程成绩。
▮▮▮▮▮▮▮▮❿ 学生成绩修改 (Student Grade Modification):修改学生的课程成绩。
▮▮▮▮▮▮▮▮❾ 学生成绩删除 (Student Grade Deletion):删除学生的课程成绩。
▮▮▮▮▮▮▮▮❿ 学生成绩统计 (Student Grade Statistics):统计学生的总成绩 (total score)、平均成绩 (average score)、最高分 (highest score)、最低分 (lowest score) 等信息。
▮▮▮▮▮▮▮▮⓫ 数据持久化存储 (Data Persistence Storage):将学生信息和成绩数据保存到文件中 (例如,文本文件或CSV文件),实现数据持久化存储,程序退出后数据不丢失。
▮▮▮▮▮▮▮▮⓬ 数据加载 (Data Loading):程序启动时,从文件中加载学生信息和成绩数据。
▮▮▮▮▮▮▮▮⓭ 退出系统 (Exit System):安全退出学生信息管理系统。
▮▮▮▮ⓑ 可选功能需求 (Optional Functionality Requirements) (可以作为扩展功能):
▮▮▮▮▮▮▮▮❷ 数据排序 (Data Sorting):按照学号、姓名、成绩等字段对学生信息进行排序。
▮▮▮▮▮▮▮▮❸ 数据导出 (Data Export):将学生信息和成绩数据导出到文件 (例如,CSV文件, Excel文件)。
▮▮▮▮▮▮▮▮❹ 数据导入 (Data Import):从文件 (例如,CSV文件) 导入学生信息和成绩数据。
▮▮▮▮▮▮▮▮❺ 用户权限管理 (User Permission Management):实现用户登录 (login) 功能,不同用户角色 (例如,管理员、教师、学生) 具有不同的操作权限。
▮▮▮▮▮▮▮▮❻ 图形用户界面 (GUI):使用GUI框架开发图形界面,提升用户体验。
▮▮▮▮▮▮▮▮❼ 数据库存储 (Database Storage):使用数据库 (例如,MySQL, SQLite) 存储学生信息和成绩数据,提高数据管理效率和可靠性。
▮▮▮▮▮▮▮▮❽ 网络化 (Networking):将学生信息管理系统部署到Web服务器 (web server) 上,实现网络访问和管理。
▮▮▮▮ⓒ 非功能需求 (Non-functional Requirements):
▮▮▮▮与简易文本编辑器案例的非功能需求类似,包括易用性、稳定性、效率、可维护性等。数据安全性 (data security) 和数据完整性 (data integrity) 在学生信息管理系统中尤为重要。
16.4.2 系统设计 (System Design)
① 系统架构设计 (System Architecture Design):
▮▮▮▮同样采用模块化设计思想,将学生信息管理系统划分为若干个功能模块:
▮▮▮▮ⓐ 用户界面模块 (User Interface Module):负责用户交互,接收用户输入,显示程序输出。
▮▮▮▮ⓑ 学生信息管理模块 (Student Information Management Module):负责学生信息的增 (add)、删 (delete)、改 (modify)、查 (query)、列表显示等操作。
▮▮▮▮ⓒ 成绩管理模块 (Grade Management Module):负责学生成绩的录入、查询、修改、删除、统计等操作。
▮▮▮▮ⓓ 数据存储模块 (Data Storage Module):负责数据的持久化存储和加载。可以将学生信息和成绩数据存储到文件中。
\[ \text{学生信息管理系统架构} \]
\[ \begin{tikzcd} \text{用户} \arrow[r, "\text{用户交互}"] & \text{用户界面模块} \arrow[r, "\text{命令调用}"] \arrow[d, "\text{数据传递}"] & \text{学生信息管理模块} \arrow[d, "\text{数据传递}"] \\ & \text{成绩管理模块} \arrow[r, "\text{数据操作}"] & \text{数据存储模块} \end{tikzcd} \]
② 类设计 (Class Design) (面向对象方法):
▮▮▮▮可以设计以下类:
▮▮▮▮ⓐ Student
类:学生类,封装学生的基本信息 (学号、姓名、性别、年龄、班级、联系方式)。
▮▮▮▮ⓑ CourseGrade
类:课程成绩类,封装课程名称和成绩信息。一个学生可以有多个 CourseGrade
对象。
▮▮▮▮ⓒ StudentManager
类:学生信息管理类,负责管理 Student
对象集合。提供学生信息的增删改查、列表显示、数据持久化存储和加载等功能。
▮▮▮▮ⓓ GradeManager
类:成绩管理类,负责管理学生成绩信息。提供成绩录入、查询、修改、删除、统计等功能。可以作为 StudentManager
类的成员,或独立存在。
▮▮▮▮ⓔ DataManager
类:数据管理类,负责数据的持久化存储和加载。封装文件读写操作,例如,使用文本文件或CSV文件存储学生信息和成绩数据。
▮▮▮▮ⓕ UserInterface
类:用户界面类,负责用户交互。提供菜单显示、命令解析、信息提示等功能。
③ 数据结构选择 (Data Structure Selection):
▮▮▮▮ⓑ 学生信息存储:可以使用 std::vector<Student>
或 std::map<std::string, Student>
存储学生对象。std::vector
适合列表显示和遍历,std::map
适合根据学号快速查找学生信息 (学号作为键 (key))。考虑到需要根据学号快速查询学生信息,std::map
可能是更合适的选择。
▮▮▮▮ⓒ 成绩信息存储:可以在 Student
类中,使用 std::vector<CourseGrade>
或 std::map<std::string, double>
存储学生的课程成绩。std::vector
适合存储课程成绩列表,std::map
适合根据课程名称快速查找成绩 (课程名称作为键)。
▮▮▮▮ⓓ 数据持久化存储格式:可以使用文本文件或CSV文件存储学生信息和成绩数据。CSV文件格式更结构化,易于解析和导入导出。也可以考虑使用JSON或XML格式,但解析和生成相对复杂。
16.4.3 核心功能模块设计 (Core Function Module Design)
① 学生信息管理模块设计 (Student Information Management Module Design):
▮▮▮▮ⓑ 学生信息录入功能 (Add Student Function):
▮▮▮▮▮▮▮▮❸ 提示用户逐项输入学生信息 (学号、姓名、性别、年龄、班级、联系方式)。
▮▮▮▮▮▮▮▮❹ 创建 Student
对象,并将用户输入的信息赋值给 Student
对象的成员变量 (member variables)。
▮▮▮▮▮▮▮▮❺ 将新的 Student
对象添加到 StudentManager
管理的学生集合中 (std::map
或 std::vector
)。
▮▮▮▮▮▮▮▮❻ 可以进行学号查重 (check for duplicate student ID) 检查,避免录入重复学号的学生信息。
▮▮▮▮ⓖ 学生信息查询功能 (Query Student Function):
▮▮▮▮▮▮▮▮❽ 提示用户输入查询条件 (学号或姓名)。
▮▮▮▮▮▮▮▮❾ 根据用户选择的查询条件,在 StudentManager
管理的学生集合中查找匹配的学生信息。
▮▮▮▮▮▮▮▮❿ 如果找到匹配的学生,显示学生信息。如果没有找到,提示用户未找到。
▮▮▮▮▮▮▮▮❹ 可以支持精确查询 (exact match) 和模糊查询 (fuzzy match) (例如,姓名模糊查询)。模糊查询可以使用字符串查找算法 (例如,std::string::find()
) 实现。
▮▮▮▮ⓛ 学生信息修改功能 (Modify Student Function):
▮▮▮▮▮▮▮▮❶ 提示用户输入要修改的学生学号。
▮▮▮▮▮▮▮▮❷ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❸ 如果找到学生,显示当前学生信息,并提示用户逐项输入新的学生信息。
▮▮▮▮▮▮▮▮❹ 更新 Student
对象的成员变量为用户输入的新信息。
▮▮▮▮▮▮▮▮❺ 如果未找到学生,提示用户未找到。
▮▮▮▮ⓡ 学生信息删除功能 (Delete Student Function):
▮▮▮▮▮▮▮▮❶ 提示用户输入要删除的学生学号。
▮▮▮▮▮▮▮▮❷ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❸ 如果找到学生,确认用户是否要删除,避免误操作。
▮▮▮▮▮▮▮▮❹ 从 StudentManager
管理的学生集合中删除该学生对象。
▮▮▮▮▮▮▮▮❺ 如果未找到学生,提示用户未找到。
▮▮▮▮ⓧ 学生信息列表显示功能 (List Students Function):
▮▮▮▮▮▮▮▮❶ 遍历 StudentManager
管理的学生集合。
▮▮▮▮▮▮▮▮❷ 逐个显示每个学生的详细信息。
▮▮▮▮▮▮▮▮❸ 可以支持分页显示 (pagination) (如果学生数量很多)。
② 成绩管理模块设计 (Grade Management Module Design):
▮▮▮▮ⓑ 学生成绩录入功能 (Input Grade Function):
▮▮▮▮▮▮▮▮❸ 提示用户输入学生学号。
▮▮▮▮▮▮▮▮❹ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❺ 如果找到学生,提示用户输入课程名称和成绩。
▮▮▮▮▮▮▮▮❻ 创建 CourseGrade
对象,并将课程名称和成绩赋值给 CourseGrade
对象的成员变量。
▮▮▮▮▮▮▮▮❼ 将 CourseGrade
对象添加到 Student
对象的课程成绩列表中 (std::vector<CourseGrade>
或 std::map<std::string, double>
)。
▮▮▮▮▮▮▮▮❽ 可以进行课程名称查重检查,避免重复录入同一课程的成绩。
▮▮▮▮ⓘ 学生成绩查询功能 (Query Grade Function):
▮▮▮▮▮▮▮▮❿ 提示用户输入学生学号。
▮▮▮▮▮▮▮▮❷ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❸ 如果找到学生,显示学生的课程成绩列表。包括课程名称和成绩。
▮▮▮▮▮▮▮▮❹ 如果未找到学生,提示用户未找到。
▮▮▮▮ⓝ 学生成绩修改功能 (Modify Grade Function):
▮▮▮▮▮▮▮▮❶ 提示用户输入学生学号和要修改的课程名称。
▮▮▮▮▮▮▮▮❷ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❸ 如果找到学生,根据课程名称在学生的课程成绩列表中查找成绩记录。
▮▮▮▮▮▮▮▮❹ 如果找到成绩记录,提示用户输入新的成绩。
▮▮▮▮▮▮▮▮❺ 更新 CourseGrade
对象的成绩为用户输入的新成绩。
▮▮▮▮▮▮▮▮❻ 如果未找到学生或成绩记录,提示用户未找到。
▮▮▮▮ⓤ 学生成绩删除功能 (Delete Grade Function):
▮▮▮▮▮▮▮▮❶ 提示用户输入学生学号和要删除成绩的课程名称。
▮▮▮▮▮▮▮▮❷ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❸ 如果找到学生,根据课程名称在学生的课程成绩列表中查找成绩记录。
▮▮▮▮▮▮▮▮❹ 如果找到成绩记录,确认用户是否要删除,避免误操作。
▮▮▮▮▮▮▮▮❺ 从学生的课程成绩列表中删除该 CourseGrade
对象。
▮▮▮▮▮▮▮▮❻ 如果未找到学生或成绩记录,提示用户未找到。
▮▮▮▮ⓩ 学生成绩统计功能 (Grade Statistics Function):
▮▮▮▮▮▮▮▮❶ 提示用户输入学生学号。
▮▮▮▮▮▮▮▮❷ 根据学号在 StudentManager
管理的学生集合中查找学生。
▮▮▮▮▮▮▮▮❸ 如果找到学生,计算学生的总成绩、平均成绩、最高分、最低分等信息。
▮▮▮▮▮▮▮▮ - 总成绩:所有课程成绩之和。
▮▮▮▮▮▮▮▮ - 平均成绩:总成绩除以课程数量。
▮▮▮▮▮▮▮▮ - 最高分:课程成绩中的最高分。
▮▮▮▮▮▮▮▮ - 最低分:课程成绩中的最低分。
▮▮▮▮▮▮▮▮❹ 显示统计结果。
▮▮▮▮▮▮▮▮❺ 如果未找到学生,提示用户未找到。
③ 数据存储模块设计 (Data Storage Module Design):
▮▮▮▮ⓑ 数据持久化存储 (Save Data):
▮▮▮▮▮▮▮▮❸ 将 StudentManager
管理的学生集合中的所有 Student
对象,以及每个 Student
对象的课程成绩信息,写入到文件中。
▮▮▮▮▮▮▮▮❹ 可以选择文本文件或CSV文件格式存储数据。
▮▮▮▮▮▮▮▮ - 文本文件:可以将每个 Student
对象的信息格式化为文本行,逐行写入文件。课程成绩可以作为学生信息的一部分存储,或者单独存储到另一个文件中,并通过学号关联。
▮▮▮▮▮▮▮▮ - CSV文件:可以使用CSV格式存储学生信息和成绩数据,每行表示一个学生或一条成绩记录,字段之间用逗号分隔。可以使用C++的CSV库 (CSV library) (例如,cpp-csv-parser) 简化CSV文件的读写操作。
▮▮▮▮▮▮▮▮❸ 文件保存路径可以硬编码 (hardcoded) 在程序中,或者从配置文件 (configuration file) 中读取,或者让用户在程序运行时指定。
▮▮▮▮ⓑ 数据加载 (Load Data):
▮▮▮▮▮▮▮▮❸ 程序启动时,从指定的文件中读取学生信息和成绩数据。
▮▮▮▮▮▮▮▮❹ 根据文件格式 (文本文件或CSV文件) 解析文件内容,创建 Student
对象和 CourseGrade
对象,并将数据加载到 StudentManager
管理的学生集合中。
▮▮▮▮▮▮▮▮❺ 如果文件不存在或加载失败,可以提示用户,或者创建一个空的 StudentManager
对象,从零开始录入数据。
④ 用户界面模块设计 (User Interface Module Design):
▮▮▮▮与简易文本编辑器案例的用户界面模块设计类似,提供主菜单,处理用户命令,显示信息提示。主菜单可以包含以下选项:
1
****************************
2
* 学生信息管理系统 *
3
****************************
4
1. 录入学生信息 (Add Student)
5
2. 查询学生信息 (Query Student)
6
3. 修改学生信息 (Modify Student)
7
4. 删除学生信息 (Delete Student)
8
5. 显示学生列表 (List Students)
9
6. 录入学生成绩 (Input Grade)
10
7. 查询学生成绩 (Query Grade)
11
8. 修改学生成绩 (Modify Grade)
12
9. 删除学生成绩 (Delete Grade)
13
10. 统计学生成绩 (Grade Statistics)
14
11. 保存数据 (Save Data)
15
12. 退出系统 (Exit)
16
****************************
17
请选择操作 (Please select operation):
16.4.4 编码实现 (Coding Implementation)
① 开发环境准备和创建项目 (Development Environment Preparation and Create Project):
▮▮▮▮与简易文本编辑器案例相同。
② 编写代码 (Write Code):
▮▮▮▮按照系统设计和模块设计,逐步实现各个功能模块的代码。
▮▮▮▮代码示例 (核心代码框架,具体实现细节较为复杂,需要根据选择的数据存储格式和用户界面实现方式进行编写):
1
#include <iostream>
2
#include <fstream>
3
#include <string>
4
#include <vector>
5
#include <map>
6
7
// Student 类, CourseGrade 类, StudentManager 类, GradeManager 类, DataManager 类, UserInterface 类 的定义和实现 ...
8
9
class StudentManagerSystem {
10
public:
11
StudentManager studentManager;
12
GradeManager gradeManager;
13
DataManager dataManager;
14
UserInterface ui;
15
16
StudentManagerSystem() : studentManager(), gradeManager(studentManager), dataManager(), ui() {
17
dataManager.loadData(studentManager); // 加载数据
18
}
19
20
~StudentManagerSystem() {
21
dataManager.saveData(studentManager); // 保存数据
22
}
23
24
void run() {
25
while (true) {
26
ui.showMainMenu();
27
int choice = ui.getMenuChoice();
28
29
switch (choice) {
30
case 1: // 录入学生信息
31
// ... 调用 studentManager.addStudent() ...
32
break;
33
case 2: // 查询学生信息
34
// ... 调用 studentManager.queryStudent() ...
35
break;
36
// ... 其他功能选项 ...
37
case 11: // 保存数据
38
dataManager.saveData(studentManager);
39
ui.showMessage("数据保存成功!");
40
break;
41
case 12: // 退出系统
42
ui.showMessage("感谢使用,再见!");
43
return;
44
default:
45
ui.showError("无效的选项,请重新选择!");
46
break;
47
}
48
}
49
}
50
};
51
52
int main() {
53
StudentManagerSystem system;
54
system.run();
55
return 0;
56
}
③ 代码组织 (Code Organization):
▮▮▮▮与简易文本编辑器案例类似,按照模块分文件组织代码,例如,student.h/cpp
, course_grade.h/cpp
, student_manager.h/cpp
, grade_manager.h/cpp
, data_manager.h/cpp
, user_interface.h/cpp
, main.cpp
。
16.4.5 测试与调试 (Testing and Debugging)
① 测试类型 (Testing Types):
▮▮▮▮与简易文本编辑器案例类似,需要进行单元测试、集成测试和系统测试。学生信息管理系统的测试重点在于数据管理的正确性 (data management correctness) 和数据持久化 (data persistence) 的可靠性。
② 测试重点 (Testing Focus):
▮▮▮▮ⓑ 数据录入测试:测试学生信息和成绩录入功能的正确性,包括各种数据类型 (data types) 和边界条件 (boundary conditions) 的测试。
▮▮▮▮ⓒ 数据查询测试:测试各种查询条件下的查询结果是否正确,包括精确查询、模糊查询、不存在的数据查询等。
▮▮▮▮ⓓ 数据修改测试:测试数据修改功能的正确性,修改后数据是否正确更新。
▮▮▮▮ⓔ 数据删除测试:测试数据删除功能的正确性,删除后数据是否被正确移除。
▮▮▮▮ⓕ 数据持久化测试:测试数据保存和加载功能的可靠性,程序重启后数据是否能够正确加载,数据是否丢失或损坏。
▮▮▮▮ⓖ 数据统计测试:测试成绩统计功能的正确性,统计结果是否准确。
▮▮▮▮ⓗ 边界条件测试:测试各种边界条件下的程序行为,例如,输入空数据、非法数据、超长数据等。
▮▮▮▮ⓘ 错误处理测试:测试程序在各种错误情况下的处理能力,例如,文件读写错误、数据格式错误、用户输入错误等。
③ 调试技巧 (Debugging Techniques):
▮▮▮▮与简易文本编辑器案例的调试技巧类似。
16.4.6 案例总结与扩展 (Case Summary and Extensions)
① 案例总结 (Case Summary):
▮▮▮▮本案例通过开发学生信息管理系统,进一步巩固C++面向对象编程思想、数据结构 (例如,std::map
, std::vector
) 的应用、文件操作、以及系统模块化设计等知识。学习者通过完成本案例,可以提升面向对象系统设计和开发能力,掌握数据管理和持久化存储的基本技术。
② 扩展功能建议 (Extension Functionality Suggestions):
▮▮▮▮ⓑ 更完善的数据管理功能:例如,数据排序、数据导出/导入、数据备份/恢复、数据校验 (data validation) 等。
▮▮▮▮ⓒ 用户权限管理:实现用户登录功能,不同用户角色具有不同的操作权限。
▮▮▮▮ⓓ 图形用户界面 (GUI):使用GUI框架开发图形界面,提升用户体验。
▮▮▮▮ⓔ 数据库存储:使用数据库 (例如,SQLite, MySQL) 存储数据,提高数据管理效率和可靠性,为后续扩展为网络化应用打下基础。
▮▮▮▮ⓕ 网络化应用:将学生信息管理系统部署到Web服务器上,实现网络访问和管理,扩展为Web应用程序 (web application)。
▮▮▮▮ⓖ 报表生成 (Report Generation):生成各种学生信息和成绩报表,例如,学生成绩单、班级成绩汇总表、学生信息统计报表等。
16.5 通用项目开发最佳实践 (General Project Development Best Practices)
① 需求分析优先 (Requirements Analysis First):
▮▮▮▮在项目开发初期,务必进行充分的需求分析,明确项目的功能需求和非功能需求。良好的需求分析是项目成功的基石。
② 系统设计先行 (System Design First):
▮▮▮▮在编码实现之前,进行系统设计,包括系统架构设计、模块划分、类设计、数据结构选择、算法设计等。良好的系统设计可以提高代码的可维护性、可扩展性和可重用性。
③ 模块化编程 (Modular Programming):
▮▮▮▮采用模块化编程思想,将系统划分为独立的模块,每个模块负责特定的功能。模块之间通过接口 (interfaces) 进行交互。模块化编程可以降低代码复杂度,提高代码可读性和可维护性,方便团队协作开发。
④ 面向对象编程 (Object-Oriented Programming, OOP):
▮▮▮▮在C++项目开发中,充分利用面向对象编程的特性,例如,封装 (encapsulation)、继承 (inheritance)、多态 (polymorphism)。面向对象编程可以提高代码的抽象程度,增强代码的复用性和可扩展性。
⑤ 代码规范 (Coding Conventions):
▮▮▮▮遵循统一的代码规范,包括命名规范 (naming conventions)、代码缩进 (code indentation)、注释规范 (commenting conventions) 等。良好的代码规范可以提高代码可读性和可维护性,方便团队协作开发和代码 review (代码审查)。
⑥ 版本控制 (Version Control):
▮▮▮▮使用版本控制系统 (例如,Git) 管理项目代码。版本控制可以记录代码的修改历史,方便代码回溯 (code rollback) 和版本管理,支持多人协同开发。
⑦ 测试驱动开发 (Test-Driven Development, TDD) (可选):
▮▮▮▮采用测试驱动开发模式,先编写测试用例,再编写代码实现功能。测试驱动开发可以提高代码质量,保证代码的功能正确性,降低bug率 (bug rate)。
⑧ 持续集成 (Continuous Integration, CI) (可选):
▮▮▮▮使用持续集成工具 (例如,Jenkins, GitLab CI) 实现代码的自动构建 (automatic build)、自动测试 (automatic testing) 和自动部署 (automatic deployment)。持续集成可以尽早发现和解决代码集成问题,提高软件开发效率和质量。
⑨ 代码审查 (Code Review):
▮▮▮▮进行代码审查,由团队成员互相审查代码,检查代码质量、代码风格、潜在的bug和安全漏洞 (security vulnerabilities)。代码审查可以提高代码质量,促进团队知识共享 (knowledge sharing)。
⑩ 文档编写 (Documentation):
▮▮▮▮编写项目文档,包括需求文档 (requirements document)、设计文档 (design document)、用户手册 (user manual)、API文档 (API documentation) 等。文档可以帮助理解项目,方便维护和扩展,方便用户使用。
16.6 更多项目实践建议 (Further Project Practice Suggestions)
① 图书管理系统 (Library Management System):
▮▮▮▮实现图书的入库、借阅、归还、查询、统计等功能。可以扩展实现读者管理、图书分类管理、预约管理、逾期管理等功能。
② 学生选课系统 (Student Course Selection System):
▮▮▮▮实现学生的选课、退课、课程查询、课表生成、选课结果管理等功能。可以扩展实现教师管理、课程管理、班级管理、选课人数统计、冲突检测等功能。
③ 简易计算器 (Simple Calculator):
▮▮▮▮实现基本算术运算 (加、减、乘、除)、科学计算 (三角函数、指数函数、对数函数等)、表达式求值等功能。可以扩展实现图形界面、历史记录、单位转换、自定义函数等功能。
④ 通讯录 (Address Book):
▮▮▮▮管理联系人的姓名、电话、地址、邮箱等信息。实现联系人添加、删除、修改、查询、分组、导入导出等功能。可以扩展实现网络同步、云存储、智能搜索等功能。
⑤ 任务管理工具 (Task Management Tool):
▮▮▮▮管理任务的标题、描述、优先级、截止日期、状态等信息。实现任务添加、删除、修改、查询、分类、提醒、统计等功能。可以扩展实现团队协作、项目管理、甘特图、日历视图等功能。
⑥ 简易Web服务器 (Simple Web Server):
▮▮▮▮实现一个简单的Web服务器,能够处理HTTP请求 (HTTP requests),返回HTML页面 (HTML pages)、图片 (images)、CSS文件 (CSS files)、JavaScript文件 (JavaScript files) 等静态资源 (static resources)。可以扩展实现动态内容生成 (dynamic content generation)、CGI支持 (CGI support)、HTTPS支持 (HTTPS support)、负载均衡 (load balancing) 等功能。
⑦ 网络聊天程序 (Network Chat Program):
▮▮▮▮实现客户端 (client) 和服务器 (server) 端的网络聊天程序。客户端可以发送消息、接收消息、显示聊天记录。服务器端可以接收客户端连接、转发消息、管理用户。可以扩展实现群聊、私聊、文件传输、语音聊天、视频聊天、加密通信等功能。
⑧ 游戏开发 (Game Development):
▮▮▮▮使用C++和游戏开发库 (game development libraries) (例如,SDL, SFML, OpenGL, DirectX) 开发各种类型的游戏,例如,2D游戏 (例如,平台跳跃游戏 (platformer game)、射击游戏 (shooter game)、益智游戏 (puzzle game))、3D游戏 (例如,第一人称射击游戏 (FPS game)、角色扮演游戏 (RPG game)、策略游戏 (strategy game))。游戏开发可以综合运用C++的各种知识,锻炼逻辑思维、算法设计、图形编程、音效处理、用户交互等技能。
选择合适的项目案例进行实践,是巩固C++知识,提升编程能力,积累项目经验的有效途径。希望读者能够通过项目实战,不断提升C++编程水平,成为优秀的C++程序员。 🚀
17. C++性能优化和最佳实践
本章将深入探讨C++程序在不同层面的性能优化策略和技术,并介绍在日常开发中应遵循的编程最佳实践和代码规范。性能优化是提升软件响应速度、降低资源消耗的关键环节,而良好的编程实践则是保证代码质量、提高开发效率和降低维护成本的基础。本章旨在为读者提供一套系统性的指导,帮助读者写出既高效又易于维护的C++代码。
17.1 性能优化的目标与原则
在编写C++程序时,实现正确的功能是首要目标。然而,对于许多应用场景,如游戏开发、高性能计算、嵌入式系统等,程序的执行效率和资源消耗同样至关重要。性能优化旨在通过改进代码、算法或系统交互方式,使得程序运行更快、占用内存更少或消耗更少的CPU资源。
17.1.1 为什么需要性能优化?
① 提升用户体验:在交互式应用中,更快的响应速度直接提升用户满意度。
② 降低硬件成本:高效的程序可以在较低配置的硬件上良好运行,或在相同硬件上处理更多任务。
③ 提高处理能力:对于批处理或计算密集型任务,性能优化意味着能在相同时间内处理更多数据。
④ 节约能源:尤其在移动设备或大型服务器集群中,降低CPU和内存使用可以显著节约能源。
⑤ 满足实时性要求:某些应用(如实时控制系统)对响应时间有严格限制,必须通过优化来满足。
17.1.2 性能优化的基本原则
性能优化并非盲目进行的,需要遵循一些核心原则,以确保优化工作的有效性和方向性。
① 测量 (Measure) 先于优化:在着手优化之前,必须准确地测量程序的当前性能,找出真正的瓶颈 (bottleneck)。凭直觉进行的优化往往事倍功半,甚至引入新的问题。使用性能分析工具 (profilers) 是这一阶段的关键。
② 优化关键路径 (Critical Path):大多数程序的性能问题集中在少数代码区域(通常是热点代码,hotspot)。应将优化精力集中在这些对整体性能影响最大的部分。
③ 先保证正确性:优化可能会使代码更复杂,更容易引入错误。因此,任何优化都必须在确保程序功能正确的前提下进行。优化后的代码需要进行充分的测试。
④ 循序渐进:一次只进行一项优化,并测量其效果。这样可以更容易地确定优化的影响,并在发现问题时快速回滚。
⑤ 理解底层原理:深入了解C++语言特性、编译器工作原理、操作系统调度、内存访问模式以及硬件架构(如CPU缓存、指令流水线)有助于做出更有效的优化决策。
17.2 性能分析与度量工具
性能优化的第一步是测量和分析。缺乏准确的数据,任何优化都只能是猜测。性能分析工具(Profilers)和基准测试(Benchmarking)是获取这些数据的关键手段。
17.2.1 代码性能分析工具 (Profilers)
性能分析工具能够帮助开发者了解程序在运行时哪些部分消耗了最多的时间或资源。
① CPU Profilers (CPU 分析器):
▮▮▮▮⚝ 采样分析器 (Sampling Profilers):以固定的时间间隔(例如每毫秒)中断程序,记录程序计数器 (Program Counter),从而统计程序大部分时间花费在哪些函数或代码行上。代表工具有 Linux perf、gprof (GNU Profiler)、VTune Amplifier (Intel)、Visual Studio Profiler。
▮▮▮▮⚝ 插桩分析器 (Instrumenting Profilers):在函数入口和出口插入代码,精确测量每个函数的执行时间。这种方法可能对程序性能有一定影响。代表工具有 gprof (部分模式)、一些商业分析器。
▮▮▮▮⚝ 动态分析器 (Dynamic Analysis Tools):如 Valgrind (尤其是 Callgrind 工具),可以在运行时模拟CPU,提供详细的函数调用图和缓存使用信息。
② 内存分析工具 (Memory Profilers):
▮▮▮▮⚝ 用于检测内存泄漏 (memory leaks)、过度分配 (excessive allocations)、不合理的内存使用模式等。代表工具有 Valgrind (Memcheck 工具)、asan (AddressSanitizer)、Visual Studio Memory Profiler。
③ 其他专业分析器:
▮▮▮▮⚝ 针对特定领域,如并行计算 (parallel computing)、GPU 性能分析等。
使用 Profiler 的一般流程:
① 选择合适的分析器。
② 使用调试信息 (debugging symbols) 编译程序,以便分析器能将性能数据映射回源代码。
③ 运行分析器并收集数据。
④ 分析报告,找出热点代码或资源消耗异常的部分。
17.2.2 基准测试 (Benchmarking)
基准测试是设计一套测试用例,用于衡量程序在特定条件下的性能。这通常涉及对一段代码执行多次,并测量其平均执行时间。
① 设计基准测试:
▮▮▮▮⚝ 明确测试目标:要测量什么?(例如,某个函数的执行时间,特定数据结构的操作速度)。
▮▮▮▮⚝ 设计代表性输入:使用与实际应用场景相似的数据量和数据特征。
▮▮▮▮⚝ 避免测量开销:确保测量代码本身对被测代码的影响最小。
▮▮▮▮⚝ 重复测量:多次运行测试并取平均值,忽略首次运行的启动开销,减少随机因素(如操作系统调度、缓存状态)的影响。
▮▮▮▮⚝ 控制测试环境:尽量在稳定、可重复的环境中运行测试,减少其他进程的干扰。
② 常用的基准测试库:
▮▮▮▮⚝ Google Benchmark:一个广泛使用的C++基准测试库,提供了方便的API来测量代码片段的执行时间,并能自动处理重复测量和统计。
1
#include <benchmark/benchmark.h>
2
#include <vector>
3
#include <numeric>
4
5
void BM_VectorSum(benchmark::State& state) {
6
std::vector<int> v(state.range(0));
7
std::iota(v.begin(), v.end(), 0); // Fill with 0, 1, 2, ...
8
9
for (auto _ : state) {
10
long long sum = 0;
11
for (int x : v) {
12
sum += x;
13
}
14
// Prevent compiler from optimizing the loop away
15
benchmark::DoNotOptimize(sum);
16
}
17
}
18
19
// Register the function as a benchmark
20
BENCHMARK(BM_VectorSum)->Range(8, 8<<10); // Test with vector sizes 8 to 8192
21
22
// Run the benchmark
23
BENCHMARK_MAIN();
(注意:使用 Google Benchmark 需要先安装库并正确配置构建系统)
17.2.3 衡量指标 (Metrics)
衡量性能的常见指标包括:
① 执行时间 (Execution Time):CPU时间 (CPU time)(程序实际花费在CPU上的时间)和墙钟时间 (wall-clock time)(从开始到结束的总时间)。对于单线程程序,两者接近;对于多线程程序,墙钟时间通常更短。
② 内存使用 (Memory Usage):程序占用的内存峰值或平均值。
③ CPU利用率 (CPU Utilization):程序使用CPU核心的百分比。
④ I/O 操作次数/速度:文件读写、网络通信等操作的效率。
⑤ 缓存命中率 (Cache Hit Rate):程序访问内存时命中CPU缓存的比例,高命中率通常意味着更好的性能。
17.3 算法与数据结构优化
性能优化的最大潜力往往在于选择正确的算法和数据结构。一个具有较高时间复杂度或空间复杂度的算法,即使代码层面优化到极致,也很难比得上一个复杂度更优的算法。
17.3.1 选择合适的算法
理解算法的时间复杂度 (Time Complexity) 和空间复杂度 (Space Complexity),并根据问题规模选择最优的算法是性能优化的基础。
① 时间复杂度:描述算法执行时间与输入规模的关系,通常用大O符号 (Big O notation) 表示,例如 \(O(n)\)、\(O(n \log n)\)、\(O(n^2)\)、\(O(2^n)\) 等。应优先选择复杂度较低的算法。例如,在大型数据集上查找元素,二分查找 \(O(\log n)\) 远优于线性查找 \(O(n)\)。
② 空间复杂度:描述算法占用内存空间与输入规模的关系。虽然本章侧重时间性能,但空间效率低也可能间接影响时间性能(例如,导致频繁的内存分配/释放或缓存颠倒)。
考虑实际场景:
⚝ 对于小规模问题,简单算法可能因常数因子较小而表现更好。
⚝ 考虑算法的平均情况和最坏情况复杂度。
⚝ 有时可以通过预处理 (preprocessing) 或额外空间来换取更低的时间复杂度。
17.3.2 选择合适的数据结构
不同的数据结构在存储方式、访问方式和支持的操作上各有优劣,直接影响算法的效率。
① 数组 (Arrays) 和 std::vector
:
▮▮▮▮⚝ 优势:随机访问 (random access) 效率高 \(O(1)\),内存连续,利于缓存。
▮▮▮▮⚝ 劣势:插入和删除元素(尤其是中间位置)效率低 \(O(n)\)。动态数组 (std::vector
) 扩容时可能产生较大开销。
② 链表 (Linked Lists) 和 std::list
:
▮▮▮▮⚝ 优势:插入和删除元素效率高 \(O(1)\)。
▮▮▮▮⚝ 劣势:随机访问效率低 \(O(n)\),内存不连续,缓存不友好。
③ 树 (Trees) 和 std::set
, std::map
:
▮▮▮▮⚝ std::set
(红黑树实现):有序存储,插入、删除、查找效率均为 \(O(\log n)\)。
▮▮▮▮⚝ std::map
(红黑树实现):键值对存储,有序,插入、删除、查找效率均为 \(O(\log n)\)。
④ 哈希表 (Hash Tables) 和 std::unordered_set
, std::unordered_map
:
▮▮▮▮⚝ 优势:平均情况下,插入、删除、查找效率接近 \(O(1)\)。
▮▮▮▮⚝ 劣势:最坏情况下效率可能退化到 \(O(n)\)(哈希冲突),不保证有序,内存开销可能较大。
选择原则:
⚝ 如果需要频繁随机访问和遍历,且插入/删除不频繁,考虑 std::vector
。
⚝ 如果需要频繁在任意位置插入/删除,且随机访问需求低,考虑 std::list
。
⚝ 如果需要快速查找、插入、删除并保持元素有序,考虑 std::set
或 std::map
。
⚝ 如果需要最快的查找、插入、删除(平均情况),且不关心有序性,考虑 std::unordered_set
或 std::unordered_map
。
17.4 代码层面的性能优化
在确定了合适的算法和数据结构后,对代码本身进行优化也是提升性能的重要手段。这部分优化通常涉及减少不必要的开销、改进内存访问模式等。
17.4.1 避免不必要的计算与临时对象
冗余的计算和临时对象的创建/销毁会带来额外的CPU和内存开销。
① 消除公共子表达式 (Common Subexpression Elimination):避免重复计算同一个表达式的值。
1
// 低效
2
double result = (a * b) + std::sqrt(a * b + c);
3
4
// 优化
5
double temp = a * b;
6
double result = temp + std::sqrt(temp + c);
② 利用移动语义 (Move Semantics) (C++11及以后):对于资源密集型对象(如大型容器),避免昂贵的拷贝操作,转而使用移动操作。
1
std::vector<int> create_large_vector() {
2
std::vector<int> v(1000000);
3
// ... fill v ...
4
return v; // RVO/NRVO might apply here
5
}
6
7
std::vector<int> vec = create_large_vector(); // 如果支持RVO/NRVO,没有拷贝/移动开销
8
std::vector<int> vec2 = std::move(vec); // 强制移动,vec变为空/无效状态
返回值优化 (Return Value Optimization, RVO) 和命名返回值优化 (Named Return Value Optimization, NRVO) 是编译器自动进行的优化,可以消除函数返回局部对象时的拷贝/移动开销。
③ 避免在循环中创建临时对象:尽量在循环外部声明对象,然后在循环内部重用或修改。
1
// 低效:每次循环都创建新的 stringstream 对象
2
for (...) {
3
std::stringstream ss;
4
ss << ...;
5
std::string s = ss.str();
6
}
7
8
// 优化:在循环外部创建 stringstream 对象
9
std::stringstream ss;
10
for (...) {
11
ss.str(""); // 清空缓冲区
12
ss.clear(); // 清空状态标志
13
ss << ...;
14
std::string s = ss.str();
15
}
17.4.2 循环优化
循环是程序中经常执行的部分,对循环进行优化往往能带来显著的性能提升。
① 减少循环内部的计算:将与循环变量无关的计算移到循环外部(循环不变量外提,Loop-Invariant Code Motion)。
1
// 低效
2
for (int i = 0; i < n; ++i) {
3
result[i] = data[i] * std::sqrt(constant_value); // std::sqrt 在每次循环都计算
4
}
5
6
// 优化
7
double sqrt_const = std::sqrt(constant_value);
8
for (int i = 0; i < n; ++i) {
9
result[i] = data[i] * sqrt_const; // std::sqrt 只计算一次
10
}
② 循环展开 (Loop Unrolling):减少循环控制(递增、条件判断、跳转)的开销,增加并行执行的可能性。但可能导致代码体积增大、缓存局部性下降。现代编译器通常能自动进行有效的循环展开。
1
// 简单循环
2
for (int i = 0; i < n; ++i) {
3
process(i);
4
}
5
6
// 部分展开 (展开因子为2)
7
for (int i = 0; i < n - 1; i += 2) {
8
process(i);
9
process(i + 1);
10
}
11
if (n % 2 != 0) { // 处理剩余元素
12
process(n - 1);
13
}
③ 向量化 (Vectorization):利用CPU的SIMD (Single Instruction, Multiple Data) 指令同时处理多个数据。这通常依赖于编译器自动完成(通过 -O3
等优化级别),但也可能需要程序员以特定的方式组织代码(例如,使用连续内存的数组,避免数据依赖)。
17.4.3 函数调用开销
函数调用涉及参数传递、跳转、创建/销毁栈帧等开销。对于非常小的函数,这些开销可能大于函数体本身的执行时间。
① 内联函数 (Inline Functions):编译器尝试将函数体直接插入到调用点,避免函数调用开销。对于频繁调用的小函数,使用 inline
关键字可以建议编译器进行内联。但滥用 inline
可能导致代码膨胀 (code bloat),反而降低性能。
1
inline int add(int a, int b) {
2
return a + b;
3
}
4
5
int sum = add(x, y); // 编译器可能直接替换成 int sum = x + y;
(注意:inline
只是一个建议,编译器有最终决定权。)
② 避免过度分解 (Over-decomposition):虽然函数有助于模块化和可读性,但将极小的操作分解成独立函数可能会引入不必要的调用开销。在性能关键区域,可以考虑将一些小型函数的内容直接合并到调用处。
17.4.4 使用 const
和引用
正确使用 const
和引用 (references) 不仅提高代码安全性 (safety) 和可读性 (readability),还能带来性能优势。
① 通过 const
引用传递大型对象:避免拷贝大型对象作为函数参数。
1
// 低效:拷贝整个 vector
2
void process_vector_copy(std::vector<int> v) { ... }
3
4
// 优化:通过 const 引用传递,避免拷贝
5
void process_vector_ref(const std::vector<int>& v) { ... }
6
7
std::vector<int> my_vec(1000000);
8
process_vector_copy(my_vec); // 昂贵的拷贝
9
process_vector_ref(my_vec); // 无拷贝开销
② const
正确性:使用 const
标记不应被修改的变量、函数参数和成员函数,这有助于编译器进行更积极的优化。
17.4.5 分支预测 (Branch Prediction)
现代CPU使用分支预测来预测条件跳转(如 if
语句、循环)的结果,并提前加载和处理指令。预测错误会导致流水线停顿,影响性能。编写可预测性高的代码有助于提升性能。
① 使条件更可预测:如果知道某个条件更可能为真或为假,可以尝试以提高预测成功率的方式组织代码(尽管这通常依赖于编译器和CPU,并且过度追求可能降低代码可读性)。
② 避免不必要的条件分支:有时可以通过数学运算或查表来替代条件分支。
③ 利用编译器内置函数:某些编译器提供 __builtin_expect
(GCC/Clang) 等扩展,允许程序员向编译器提供分支预测提示。
1
// 示例:使用 __builtin_expect 提示编译器 unlikely
2
if (__builtin_expect(condition, 0)) { // 提示 condition 很可能为假
3
// 处理低概率事件
4
} else {
5
// 处理高概率事件
6
}
(注意:这是编译器扩展,非标准C++。)
17.4.6 内存访问模式
CPU访问内存的速度远低于其执行指令的速度。缓存 (Cache) 的存在是为了弥补这个差距。优化内存访问模式,提高缓存命中率,对性能至关重要。
① 局部性原理 (Principle of Locality):
▮▮▮▮⚝ 时间局部性 (Temporal Locality):如果一个数据项被访问,它在不久的将来很可能再次被访问。
▮▮▮▮⚝ 空间局部性 (Spatial Locality):如果一个数据项被访问,其附近的(内存地址上相邻的)数据项在不久的将来也很可能被访问。
② 优化数组和向量的遍历:顺序访问数组或 std::vector
等连续内存的数据结构具有很好的空间局部性,利于缓存。
1
// 缓存友好:顺序访问
2
std::vector<int> data(N);
3
long long sum = 0;
4
for (int i = 0; i < N; ++i) {
5
sum += data[i]; // 顺序访问 data
6
}
7
8
// 缓存不友好:跳跃访问 (如果 N 很大,步长很大)
9
int matrix[1000][1000];
10
long long sum_col = 0;
11
for (int j = 0; j < 1000; ++j) {
12
for (int i = 0; i < 1000; ++i) {
13
sum_col += matrix[i][j]; // 按列访问,内存不连续
14
}
15
}
16
// 优化:按行访问,内存连续
17
for (int i = 0; i < 1000; ++i) {
18
for (int j = 0; j < 1000; ++j) {
19
sum_col += matrix[i][j]; // 按行访问,内存连续
20
}
21
}
③ 结构体成员排序 (Struct Member Ordering):调整结构体成员的顺序,将经常一起访问的成员放在一起,或者将占用字节数较小的成员放在一起,有时可以提高缓存效率(考虑数据对齐的影响)。
17.5 内存管理优化
高效的内存管理是C++性能优化的重要组成部分。不当的内存使用可能导致性能瓶颈,如频繁的内存分配/释放、内存碎片 (fragmentation)、缓存颠倒等。
17.5.1 栈内存 (Stack) 与堆内存 (Heap)
理解栈内存和堆内存的区别是高效内存使用的基础。
⚝ 栈内存:由编译器自动管理,用于存储局部变量、函数参数、函数返回地址等。分配和释放速度极快(通过移动栈指针),局部性好,不会产生碎片。但空间有限,生命周期与函数调用栈绑定。
⚝ 堆内存:通过 new
/delete
或 malloc
/free
等动态分配和释放。空间较大,生命周期由程序员控制,可以在函数调用结束后依然存在。但分配和释放速度相对较慢,可能产生碎片,且容易发生内存泄漏 (memory leak) 或野指针 (dangling pointer) 问题。
优化原则:
① 优先使用栈内存存放小型、生命周期与函数调用绑定的对象。
② 尽量避免在性能关键的循环中频繁进行堆内存分配和释放。
17.5.2 减少动态内存分配次数
频繁的 new
/delete
调用会产生系统调用开销和堆管理开销。减少分配次数可以显著提高性能。
① 预分配 (Pre-allocation):如果知道需要存储的对象数量或总内存需求,可以在程序开始时或批量地分配一块大内存,然后从中划分给单个对象使用,而不是逐个分配。std::vector
通过 reserve()
函数提供了一种预分配机制。
1
std::vector<MyObject> objects;
2
objects.reserve(1000); // 预留 1000 个 MyObject 的空间
3
for (int i = 0; i < 1000; ++i) {
4
objects.emplace_back(...); // 直接在预留空间构造对象,避免多次分配和拷贝/移动
5
} // 如果没有 reserve,emplace_back 可能会触发多次扩容和元素拷贝/移动
② 对象池 (Object Pool):对于需要频繁创建和销毁同类型对象的场景,可以实现一个对象池。池中维护一组预先分配好的对象,需要时从池中“借用”,用完后“归还”回池中,避免实际的内存分配/释放操作。
17.5.3 智能指针 (Smart Pointers) 与资源管理
虽然智能指针 (如 std::unique_ptr
, std::shared_ptr
) 主要用于自动管理内存,防止内存泄漏,但它们对性能也有影响。
① std::unique_ptr
:拥有独占所有权,开销与裸指针接近,是优先选择的智能指针。支持移动语义,可以高效地转移资源所有权。
② std::shared_ptr
:基于引用计数 (reference counting),多个 shared_ptr
可以共享同一个对象。引用计数的增减操作是原子操作(为了线程安全),这会带来一定的开销。只在真正需要共享所有权时使用 shared_ptr
。
③ 避免不必要的智能指针使用:如果一个对象的生命周期非常简单,完全可以在栈上管理,就没有必要使用智能指针。如果只是传递一个对象而不转移所有权,优先使用引用或 const
引用,而不是 shared_ptr
或拷贝 unique_ptr
(后者需要移动)。
17.5.4 数据对齐 (Data Alignment)
数据对齐是指数据存储在内存中的地址满足特定约束(通常是其大小的倍数)。CPU访问对齐的数据通常更高效,尤其是在进行向量化操作时。编译器会自动为基本类型和结构体成员进行填充 (padding) 以保证对齐,但有时手动控制对齐(如使用 alignas
(C++11))或考虑结构体成员的排序可以进一步优化。
17.6 编译器优化 (Compiler Optimizations)
现代C++编译器(如GCC, Clang, MSVC)是极其复杂的程序,它们能够对源代码进行各种复杂的转换和优化,以生成更高效的机器码。了解并利用编译器的优化能力是性能调优的重要环节。
17.6.1 了解编译器优化级别和标志
编译器通过优化标志控制优化程度。常见的优化级别包括:
⚝ -O0
:无优化,用于快速编译和调试。
⚝ -O1
:进行少量优化,例如消除冗余代码、简单的函数内联等。
⚝ -O2
:进行更全面的优化,包括循环优化、寄存器分配优化等,通常是生产环境的默认推荐级别。
⚝ -O3
:更激进的优化,可能包括自动向量化、更大量的函数内联等。某些激进优化可能增加代码体积,甚至在极少数情况下改变程序的行为(如果代码存在未定义行为)。
⚝ -Os
:优化代码大小,适用于资源受限的环境。
⚝ -Ofast
(GCC/Clang):基于 -O3
,并开启一些可能违反标准C++行为的优化(如浮点数运算顺序的改变),慎用。
应根据项目需求选择合适的优化级别。通常 -O2
是一个很好的起点,如果需要更高的性能可以尝试 -O3
,并通过性能测试确认其效果。
17.6.2 链接时优化 (Link-Time Optimization, LTO)
LTO (GCC/Clang 使用 -flto
标志) 允许编译器在链接整个程序时进行跨模块的优化。这使得编译器能够看到所有代码,从而进行更全面的内联、死代码消除等优化,生成更高效的最终可执行文件。LTO 会显著增加编译时间。
17.6.3 Profile-Guided Optimization (PGO)
PGO 是一种高级优化技术。它分两个阶段:
① 插桩 (Instrumentation):使用特定的编译标志(如 GCC/Clang 的 -fprofile-generate
)编译程序,生成一个带有性能监测代码的可执行文件。
② 运行与收集数据:运行插桩后的程序,使用代表性输入数据集。程序会记录代码执行频率、分支预测结果等信息到数据文件中。
③ 优化编译:使用收集到的数据文件和特定的编译标志(如 GCC/Clang 的 -fprofile-use
)重新编译程序。编译器利用这些真实世界的性能数据,对热点代码进行更有针对性的优化(例如,更精确的内联、更好的代码布局、优化的分支预测等)。
PGO 可以带来显著的性能提升,但需要额外的构建步骤和有代表性的训练数据。
17.7 C++编程最佳实践 (Best Practices)
除了直接的性能优化技巧,遵循良好的编程实践对于编写高质量、易于维护、健壮且间接有助于性能优化的代码至关重要。
17.7.1 RAII (Resource Acquisition Is Initialization)
RAII 是C++中管理资源(如内存、文件句柄、锁等)的核心原则。它将资源的生命周期与对象的生命周期绑定,在对象构造时获取资源,在对象析构时自动释放资源。
例子:使用智能指针管理堆内存。
1
#include <memory>
2
3
void foo() {
4
// 使用 unique_ptr,内存由 unique_ptr 对象管理
5
std::unique_ptr<int> ptr(new int(10));
6
7
// ... 使用 ptr ...
8
9
// 函数结束时,ptr 对象自动销毁,其析构函数释放了 new 出来的内存
10
} // 不会发生内存泄漏
遵循 RAII 可以极大地减少资源泄漏(包括内存泄漏)和其他资源管理错误,使代码更安全、更可靠。
17.7.2 异常安全 (Exception Safety)
异常安全是指程序在抛出异常时能够保持某种程度的正确状态。不同的异常安全级别:
① 基本保证 (Basic Guarantee):即使发生异常,程序状态仍然有效(没有资源泄漏),但具体状态不可预测。
② 强保证 (Strong Guarantee):如果操作失败并抛出异常,程序状态回滚到操作开始之前的状态(如同操作从未发生)。
③ 无抛出保证 (No-Throw Guarantee):函数承诺永远不会抛出异常(通过 noexcept
标记)。
编写异常安全的代码需要仔细考虑资源管理和状态回滚。RAII 是实现异常安全的基石。
17.7.3 使用现代 C++ 特性
现代C++ (C++11, 14, 17, 20及更高版本) 引入了许多新特性,这些特性不仅提高了开发效率,也往往能够写出更安全、更高效的代码。
⚝ auto
类型推导:简化代码,减少冗余,有时能更好地表达意图(如迭代器类型)。
⚝ 范围 for
循环 (Range-based for loop):简化容器或数组的遍历,减少出错机会。
⚝ Lambda 表达式 (Lambda Expressions):方便创建匿名函数对象,常用于算法或并发编程。
⚝ 智能指针 (Smart Pointers):如前所述,自动管理内存。
⚝ 右值引用 (Rvalue References) 和移动语义 (Move Semantics):实现高效的资源转移,避免不必要的拷贝。
⚝ 并发编程库:std::thread
, std::mutex
, std::atomic
等,提供标准的并发编程工具。
⚝ 标准库算法与容器:优先使用标准库提供的算法和容器,它们经过高度优化和充分测试。
⚝ Concepts (C++20):提高模板编程的可用性和错误信息友好度。
拥抱现代C++,利用其提供的工具和抽象,通常能写出更清晰、更安全、更易于维护的代码,这间接促进了性能优化。
17.7.4 代码可读性与可维护性
高性能的代码也应该是易于理解和修改的。晦涩难懂、结构混乱的代码很难维护和进一步优化。
⚝ 清晰的命名 (Clear Naming):变量、函数、类等的名称应准确反映其用途和含义。
⚝ 适当的注释 (Appropriate Comments):解释代码的意图、复杂逻辑、重要决策等。
⚝ 一致的代码风格 (Consistent Code Style):遵循统一的格式化规则,使代码看起来整洁有序。
⚝ 模块化设计 (Modular Design):将程序分解为小的、独立的模块(函数、类、组件),降低复杂度。
⚝ 避免过度复杂化:不要为了所谓的“优化”而使代码变得难以理解和调试。简单的代码通常更容易优化且不易出错。
17.7.5 单元测试 (Unit Testing) 和集成测试 (Integration Testing)
测试是保证代码正确性的关键。特别是在进行性能优化时,测试可以帮助验证优化是否引入了功能性回归 (functional regression)。
⚝ 单元测试:测试程序中最小的可测试单元(通常是函数或方法)。确保每个单元独立地按预期工作。
⚝ 集成测试:测试不同模块或组件组合在一起时是否能正确工作。
编写自动化测试用例,并在每次修改代码(包括性能优化)后运行测试,是确保软件质量的重要流程。
17.8 代码规范与风格指南
遵循统一的代码规范和风格指南对于团队协作和代码可维护性至关重要。虽然代码风格本身不直接影响性能,但一致的风格提高了代码可读性,降低了理解和修改代码的成本,从而间接帮助进行性能分析和优化。
17.8.1 选择和遵循代码风格指南
许多大型组织和社区发布了自己的C++代码风格指南,例如:
⚝ Google C++ Style Guide
⚝ LLVM Coding Standards
⚝ C++ Core Guidelines (由C++标准委员会成员维护,强调现代C++的最佳实践)
选择一个适合团队或项目的风格指南,并确保所有成员都遵循它。
风格指南通常涵盖以下方面:
⚝ 命名约定 (Naming Conventions) (变量、函数、类、宏等)
⚝ 格式化规则 (Formatting Rules) (缩进、空格、换行、括号位置等)
⚝ 注释规范 (Commenting Conventions)
⚝ 文件组织结构 (File Organization) (头文件、源文件、命名空间等)
⚝ 特定的语言特性使用建议 (例如,何时使用 auto
,何时避免宏)
17.8.2 格式化工具
使用自动化工具来强制执行代码格式化规则可以大大提高效率和一致性。
⚝ clang-format
:一个流行的跨平台工具,可以根据多种预设或自定义风格规则自动格式化C++代码。
⚝ astyle
(Artistic Style):另一个代码格式化工具。
将格式化工具集成到开发工作流程中(例如,在提交代码前自动运行)是确保风格一致性的有效方法。
17.8.3 代码审查 (Code Review)
代码审查是团队成员互相检查代码的过程。它是发现错误、改进设计、分享知识和确保遵循代码规范的有效手段。在代码审查中,除了功能正确性和设计合理性,也可以关注性能问题和是否遵循了团队的代码规范和最佳实践。
17.9 总结与展望
本章系统地探讨了C++性能优化的方法和最佳实践。我们首先强调了性能优化的重要性以及“测量先于优化”的基本原则。接着,我们介绍了用于性能分析和度量的工具,如 Profilers 和基准测试。然后,我们深入讲解了在算法、数据结构、代码层面和内存管理方面的具体优化技巧,包括选择高效算法、利用现代C++特性、减少不必要的开销、优化内存访问模式等。最后,我们讨论了C++编程的最佳实践和代码规范,它们是编写高质量、可维护、且有助于性能优化的代码的基础。
性能优化是一个持续的过程,需要在开发周期的不同阶段加以考虑。从最初的设计选择(算法、数据结构)到具体的代码实现,再到最后的性能测试和调优,每一步都可能影响程序的最终性能。掌握本章介绍的知识和技术,并将其应用于实际开发中,将帮助读者编写出更强大、更高效的C++应用程序。
未来的C++标准将继续引入更多有助于性能和效率的特性,例如更强大的并行计算支持、更好的硬件交互能力等。持续学习和实践,结合对底层系统的理解,将是成为一名优秀的C++性能专家的必由之路。
18. C++语言与其他语言的互操作:混合编程
混合编程(Mixed Programming)是指在一个软件项目中同时使用多种编程语言进行开发。这种方式允许开发者充分利用不同语言的优势,例如C++的高性能、Python的快速开发和丰富的库、Java的跨平台能力等。本章将深入探讨C++如何与其他主流语言进行互操作,包括与C语言的天然兼容性、与Python、Java等语言通过特定机制进行通信,并讨论混合编程的应用场景、优缺点以及需要注意的事项。掌握混合编程技术,能够帮助您构建更强大、更灵活的软件系统。
18.1 混合编程概述
尽管C++是一种功能强大且应用广泛的语言,但在实际项目中,我们经常需要与其他语言编写的代码进行交互。这种互操作性(Interoperability)需求催生了混合编程。
18.1.1 混合编程的动机 (Motivations for Mixed Programming)
① 发挥不同语言的优势 (Leveraging Strengths of Different Languages):
▮▮▮▮ⓑ C++擅长系统编程、性能敏感应用(如游戏引擎、高性能计算),能够提供底层控制和极致性能。
▮▮▮▮ⓒ Python适合快速原型开发、脚本编写、数据分析、机器学习等,拥有庞大的库生态系统和简洁的语法。
▮▮▮▮ⓓ Java以其跨平台特性、成熟的企业级应用框架而闻名。
▮▮▮▮结合这些语言,可以在同一个项目中实现不同模块的最佳效率和开发速度。
② 复用现有代码 (Reusing Existing Code): 很多成熟的库、框架或遗留系统可能由特定语言编写。通过混合编程,可以直接调用这些现有代码,避免重复开发。
③ 满足特定平台或领域需求 (Meeting Specific Platform or Domain Requirements): 某些平台或领域(如操作系统调用、特定硬件交互)可能需要使用C/C++;某些领域(如Web开发、大数据)可能更倾向于使用Python或Java。
18.1.2 混合编程的挑战 (Challenges of Mixed Programming)
① 语言间的类型系统差异 (Differences in Type Systems): 不同语言有不同的数据类型表示和内存管理方式,需要在语言边界进行类型转换和数据结构映射。
② 函数调用和参数传递 (Function Calls and Parameter Passing): 不同语言的函数调用约定可能不同,参数传递方式(值传递、引用传递)也需要兼容或转换。
③ 错误处理和异常传播 (Error Handling and Exception Propagation): 一种语言中的错误或异常如何传递到另一种语言中进行处理是一个复杂的问题。
④ 内存管理 (Memory Management): 托管语言(如Python、Java)有垃圾回收机制,而非托管语言(如C++)需要手动或智能指针管理内存。混合编程需要协调不同语言的内存管理策略,避免内存泄漏或悬垂指针(Dangling Pointer)。
⑤ 构建和部署复杂性 (Build and Deployment Complexity): 混合语言项目需要配置多种语言的编译器、解释器、依赖库等,构建过程可能比单一语言项目更复杂。
18.2 C++与C的互操作 (Interoperability between C++ and C)
C++是C语言的超集,这意味着C++程序可以直接包含C语言的头文件并调用C函数。C语言是结构化编程的典范,而C++在其基础上引入了面向对象、模板、异常处理等特性。在很多底层库、操作系统接口等场景中,C API(Application Programming Interface)仍然是标准。C++与C的互操作是最常见和基础的混合编程形式。
18.2.1 extern "C"
的作用 (Role of extern "C"
)
C++为了支持函数重载(Function Overloading)和类型安全链接(Type-safe Linking),引入了名称修饰(Name Mangling)机制。编译器会在函数名后面添加额外的信息(如参数类型),生成一个唯一的内部名称。而C语言没有函数重载,因此没有名称修饰。
当C++代码需要调用C函数,或者C代码需要调用C++函数时,就需要告诉C++编译器不要对特定的函数或变量进行名称修饰,使其遵循C语言的链接约定。这就是 extern "C"
的作用。
⚝ extern "C"
块内的声明或定义,C++编译器会按照C语言的规则处理其名称,避免名称修饰。
例如,一个C头文件 my_c_lib.h
声明了一个C函数:
1
// my_c_lib.h
2
void c_function(int value);
在C++代码中调用这个C函数时,应该这样包含头文件或声明函数:
1
// my_cpp_code.cpp
2
#include <iostream>
3
4
extern "C" {
5
#include "my_c_lib.h"
6
}
7
8
int main() {
9
std::cout << "Calling C function from C++" << std::endl;
10
c_function(123); // 正确调用C函数
11
return 0;
12
}
通常,C库的头文件会使用预处理器指令 __cplusplus
来判断当前是否在C++环境下被编译,并自动添加 extern "C"
:
1
// my_c_lib.h (通常写成这样以便在C++中使用)
2
#ifndef MY_C_LIB_H
3
#define MY_C_LIB_H
4
5
#ifdef __cplusplus
6
extern "C" {
7
#endif
8
9
void c_function(int value);
10
// 其他 C 函数声明...
11
12
#ifdef __cplusplus
13
}
14
#endif
15
16
#endif // MY_C_LIB_H
这样,无论是C编译器还是C++编译器包含这个头文件,都能得到正确的声明。
18.2.2 C++ 调用 C 函数 (C++ Calling C Functions)
这是最直接的互操作方式。只需要确保C++编译器知道C函数的链接方式即可,这通过 extern "C"
实现。
1
// c_source.c
2
#include <stdio.h>
3
4
void greet_from_c(const char* name) {
5
printf("Hello, %s! - from C\n", name);
6
}
1
// cpp_main.cpp
2
#include <iostream>
3
4
extern "C" {
5
// 声明C函数,告诉C++编译器使用C链接
6
void greet_from_c(const char* name);
7
}
8
9
int main() {
10
std::cout << "Calling C function..." << std::endl;
11
greet_from_c("World");
12
return 0;
13
}
编译时需要分别编译C文件和C++文件,然后链接生成可执行文件:
1
gcc -c c_source.c -o c_source.o
2
g++ -c cpp_main.cpp -o cpp_main.o
3
g++ c_source.o cpp_main.o -o mixed_program
4
./mixed_program
输出:
1
Calling C function...
2
Hello, World! - from C
18.2.3 C 调用 C++ 函数 (C Calling C++ Functions)
相比之下,C调用C++函数稍微复杂一些,因为C语言不理解C++特有的特性,如类成员函数、函数重载、异常等。为了让C能够调用C++代码,通常需要创建一个C风格的接口(Wrapper Functions)。
⚝ 这些C风格的包装函数是在C++代码中实现的,并使用 extern "C"
声明,以便C编译器能够正确链接和调用。
⚝ 这些包装函数内部再调用实际的C++函数或操作C++对象。
示例:假设有一个C++类 MyClass
。
1
// cpp_library.h
2
#include <string>
3
4
class MyClass {
5
private:
6
std::string message;
7
public:
8
MyClass(const std::string& msg);
9
void display_message() const;
10
};
11
12
// C风格的接口声明,使用 extern "C"
13
#ifdef __cplusplus
14
extern "C" {
15
#endif
16
17
// C风格的“构造函数”:返回一个指向C++对象的 void* 指针
18
void* MyClass_new(const char* msg);
19
20
// C风格的“成员函数”:接收 void* 指针和参数
21
void MyClass_display_message(void* obj);
22
23
// C风格的“析构函数”:释放C++对象
24
void MyClass_delete(void* obj);
25
26
#ifdef __cplusplus
27
}
28
#endif
1
// cpp_library.cpp
2
#include "cpp_library.h"
3
#include <iostream>
4
#include <string>
5
6
MyClass::MyClass(const std::string& msg) : message(msg) {}
7
8
void MyClass::display_message() const {
9
std::cout << "MyClass message: " << message << " - from C++\n";
10
}
11
12
// C风格的包装函数实现
13
extern "C" {
14
void* MyClass_new(const char* msg) {
15
// 在堆上创建 C++ 对象,并返回其地址
16
return new MyClass(msg);
17
}
18
19
void MyClass_display_message(void* obj) {
20
// 将 void* 指针转换回 C++ 对象指针
21
MyClass* my_obj = static_cast<MyClass*>(obj);
22
if (my_obj) {
23
my_obj->display_message();
24
}
25
}
26
27
void MyClass_delete(void* obj) {
28
// 将 void* 指针转换回 C++ 对象指针并释放内存
29
MyClass* my_obj = static_cast<MyClass*>(obj);
30
if (my_obj) {
31
delete my_obj;
32
}
33
}
34
}
1
// c_main.c
2
#include <stdio.h>
3
#include "cpp_library.h" // 包含C风格接口头文件
4
5
int main() {
6
printf("Calling C++ functions via C wrappers...\n");
7
8
// 使用C风格接口创建 C++ 对象
9
void* my_instance = MyClass_new("Hello from C via C++");
10
11
// 调用C++对象的方法
12
if (my_instance) {
13
MyClass_display_message(my_instance);
14
15
// 释放C++对象内存
16
MyClass_delete(my_instance);
17
my_instance = NULL; // 防止悬垂指针
18
} else {
19
printf("Failed to create MyClass instance.\n");
20
}
21
22
return 0;
23
}
编译和链接:
1
g++ -c cpp_library.cpp -o cpp_library.o
2
gcc -c c_main.c -o c_main.o
3
g++ cpp_library.o c_main.o -o mixed_program
4
./mixed_program
输出:
1
Calling C++ functions via C wrappers...
2
MyClass message: Hello from C via C++ - from C++
这种方式通过显式的C风格接口,屏蔽了C++的复杂性,使得C代码能够安全地与C++代码交互。
18.2.4 结构体和数据类型兼容性 (Struct and Data Type Compatibility)
C++中的基本数据类型(如 int
, char
, float
, double
等)与C语言是兼容的。结构体(struct
)在C++中是类的特例,但其在内存布局上与C语言的结构体是兼容的,只要不使用虚函数(Virtual Functions)或其他C++特有的特性。
⚝ 基本数据类型 (Basic Data Types): C++和C的大部分基本数据类型内存布局相同,可以直接传递。
⚝ 结构体 (Structs): C++的 POD (Plain Old Data) 结构体与C结构体兼容。不要在需要在C和C++之间传递的结构体中使用构造函数、析构函数、虚函数、非POD成员等C++特有特性。
⚝ 指针 (Pointers): 指针类型(如 int*
, void*
)在C和C++中兼容。
⚝ 数组 (Arrays): 数组在内存中是连续存储的,C和C++兼容其内存布局。
⚝ 类 (Classes): C语言无法直接理解C++类及其成员函数。通常需要通过上面提到的C风格接口来操作C++对象,将对象指针作为 void*
传递。
总结来说,C++与C的互操作是天然且高效的,主要通过 extern "C"
解决名称修饰问题,并通过C风格的接口处理C++特有的复杂类型和特性。
18.3 C++与Python的互操作 (Interoperability between C++ and Python)
Python是一种解释型、动态类型的语言,常用于快速开发、数据科学等领域。当Python程序需要执行计算密集型任务或利用已有的C++高性能库时,就需要C++与Python进行互操作。常见的互操作方式有两种:扩展Python(使用C++编写模块供Python调用)和嵌入Python(在C++程序中运行Python解释器)。
18.3.1 概述:扩展 (Extending) 和嵌入 (Embedding)
⚝ 扩展 Python (Extending Python): 使用C或C++编写一个模块,然后将这个模块导入到Python脚本中像使用普通Python模块一样调用其中的函数或类。这主要用于为Python添加高性能功能或访问底层系统资源。
⚝ 嵌入 Python (Embedding Python): 在C++程序中初始化并运行Python解释器,从而在C++代码中执行Python脚本、调用Python函数、访问Python对象等。这主要用于在C++应用中集成脚本能力或利用Python丰富的库。
18.3.2 使用 C API 扩展 Python (Extending Python using C API)
Python解释器本身是用C语言实现的,提供了一套C API供开发者编写扩展模块。通过这套API,可以直接操作Python对象、调用Python函数、定义新的模块、类型等。
⚝ 优点:提供了最底层的控制和最高的灵活性。
⚝ 缺点:学习曲线陡峭,需要手动管理引用计数(Reference Counting),容易出错且开发效率较低。
示例(概念性代码,实际实现更复杂):
1
// mymodule.c (编译为Python模块)
2
#include <Python.h> // 包含Python C API头文件
3
4
// C函数,实现模块功能
5
static PyObject* mymodule_add(PyObject* self, PyObject* args) {
6
int a, b;
7
// 解析Python传递的参数
8
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
9
return NULL; // 解析失败,设置异常
10
}
11
return PyLong_FromLong(a + b); // 返回Python整数对象
12
}
13
14
// 定义模块方法表
15
static PyMethodDef MymoduleMethods[] = {
16
{"add", mymodule_add, METH_VARARGS, "Add two integers."},
17
{NULL, NULL, 0, NULL} // 哨兵值
18
};
19
20
// 定义模块结构体
21
static struct PyModuleDef mymodule = {
22
PyModuleDef_HEAD_INIT,
23
"mymodule", // 模块名称
24
"A simple module.", // 模块文档字符串
25
-1, // 模块状态大小,-1 表示模块不保持状态
26
MymoduleMethods
27
};
28
29
// 模块初始化函数
30
PyMODINIT_FUNC PyInit_mymodule(void) {
31
return PyModule_Create(&mymodule);
32
}
使用C++实现时,需要处理名称修饰问题,通常整个扩展模块的代码会包裹在 extern "C"
中,并使用C++对象通过 void*
或其他方式传递。手动管理Python对象的引用计数是使用C API的最大挑战,必须小心地调用 Py_INCREF()
增加引用和 Py_DECREF()
减少引用。
18.3.3 使用工具库:Boost.Python 或 PyBind11 (Using Toolkits: Boost.Python or PyBind11)
直接使用Python C API开发效率低且容易出错。因此,出现了许多库来简化C++与Python的互操作,其中最流行的是 Boost.Python 和 PyBind11。
⚝ Boost.Python: 是Boost库的一部分,功能强大但依赖Boost,编译过程相对复杂。
⚝ PyBind11: 专注于Python和C++之间的互操作,轻量级,仅依赖C++11(或更高版本)标准库,易于使用和编译。PyBind11是目前更推荐的选择。
这些库通过C++模板元编程(Template Metaprogramming)和一些宏,自动化了Python C API 的大部分繁琐工作,包括:
⚝ 自动生成C++到Python的类型转换代码。
⚝ 自动管理引用计数。
⚝ 轻松地将C++类、函数、枚举、变量等暴露给Python。
⚝ 支持C++异常转换为Python异常。
⚝ 支持函数重载的自动绑定。
示例 (使用 PyBind11):
1
// example.cpp (使用 PyBind11)
2
#include <pybind11/pybind11.h>
3
#include <string>
4
5
namespace py = pybind11;
6
7
// C++ 函数
8
int add(int i, int j) {
9
return i + j;
10
}
11
12
// C++ 类
13
class Pet {
14
public:
15
Pet(const std::string &name) : name(name) { }
16
void setName(const std::string &name_) { name = name_; }
17
const std::string &getName() const { return name; }
18
19
std::string name;
20
};
21
22
// 模块定义 (使用 PYBIND11_MODULE 宏)
23
PYBIND11_MODULE(example, m) {
24
m.doc() = "pybind11 example plugin"; // 可选的模块文档字符串
25
26
// 绑定函数
27
m.def("add", &add, "A function which adds two numbers");
28
29
// 绑定类
30
py::class_<Pet>(m, "Pet")
31
.def(py::init<const std::string &>()) // 绑定构造函数
32
.def("setName", &Pet::setName) // 绑定成员函数
33
.def("getName", &Pet::getName) // 绑定成员函数
34
.def_readwrite("name", &Pet::name); // 绑定成员变量
35
}
编译这个C++文件(需要配置PyBind11库和Python头文件,通常使用CMake):
1
# CMakeLists.txt
2
cmake_minimum_required(VERSION 3.4)
3
project(example)
4
5
# 查找 Python 安装路径和库
6
find_package(PythonInterp 3)
7
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} REQUIRED)
8
9
# 包含 PyBind11
10
add_subdirectory(pybind11) # 假设 PyBind11 位于项目子目录
11
12
add_library(example MODULE example.cpp) # 创建一个Python模块
13
14
# 链接 Python 库并包含头文件
15
target_link_libraries(example ${PYTHON_LIBRARIES})
16
target_include_directories(example PRIVATE ${PYTHON_INCLUDE_DIRS} pybind11/include)
构建后会生成一个名为 example.so
(Linux/macOS) 或 example.pyd
(Windows) 的文件,这就是Python可以导入的模块。
在Python中调用:
1
# test_example.py
2
import example
3
4
# 调用 C++ 函数
5
result = example.add(1, 2)
6
print(f"example.add(1, 2) = {result}")
7
8
# 使用 C++ 类
9
my_pet = example.Pet("Buddy")
10
print(f"Pet name: {my_pet.getName()}")
11
my_pet.setName("Lucy")
12
print(f"New pet name: {my_pet.name}") # 直接访问成员变量
运行 python test_example.py
即可看到结果。
使用PyBind11或Boost.Python极大地提高了C++扩展Python的开发效率,是目前主流的方案。
18.3.4 C++ 嵌入 Python (Embedding Python in C++)
在C++程序中嵌入Python意味着C++代码作为宿主,负责初始化Python解释器,加载Python脚本或模块,并执行Python代码。
⚝ 应用场景:在C++应用中加入脚本控制能力、利用Python的数据处理或AI库、实现插件系统等。
⚝ 实现方式:主要使用Python C API提供的函数,如 Py_Initialize()
初始化解释器, PyRun_SimpleString()
执行简单脚本, PyImport_ImportModule()
导入模块, PyObject_CallObject()
调用函数等。
示例 (概念性代码):
1
// cpp_embed.cpp
2
#include <Python.h>
3
#include <iostream>
4
5
int main() {
6
// 初始化 Python 解释器
7
Py_Initialize();
8
if (!Py_IsInitialized()) {
9
std::cerr << "Failed to initialize Python interpreter" << std::endl;
10
return 1;
11
}
12
13
std::cout << "Python interpreter initialized." << std::endl;
14
15
// 执行简单的 Python 脚本
16
PyRun_SimpleString("print('Hello from embedded Python!')");
17
18
// 尝试导入一个 Python 模块并调用函数 (假设有一个名为 my_python_script.py 的文件)
19
// my_python_script.py 包含 def greet(name): print(f'Greeting from Python: {name}')
20
PyObject* pName = PyUnicode_DecodeFSDefault("my_python_script");
21
PyObject* pModule = PyImport_Import(pName);
22
Py_XDECREF(pName);
23
24
if (pModule != NULL) {
25
PyObject* pFunc = PyObject_GetAttrString(pModule, "greet"); // 获取函数对象
26
27
if (pFunc && PyCallable_Check(pFunc)) {
28
PyObject* pArgs = PyTuple_New(1); // 创建一个参数元组
29
PyObject* pValue = PyUnicode_FromString("C++ Program"); // 创建一个字符串参数
30
PyTuple_SetItem(pArgs, 0, pValue); // 设置元组元素
31
32
PyObject* pResult = PyObject_CallObject(pFunc, pArgs); // 调用函数
33
34
Py_XDECREF(pArgs);
35
Py_XDECREF(pValue); // PyTuple_SetItem 已经增加了引用,这里释放局部引用
36
37
if (pResult != NULL) {
38
// 处理返回值 (如果需要)
39
Py_XDECREF(pResult);
40
} else {
41
PyErr_Print(); // 打印 Python 异常信息
42
}
43
} else {
44
if (PyErr_Occurred()) PyErr_Print();
45
std::cerr << "Cannot find function \"greet\"" << std::endl;
46
}
47
Py_XDECREF(pFunc);
48
Py_XDECREF(pModule);
49
} else {
50
PyErr_Print(); // 打印导入错误
51
std::cerr << "Failed to load \"my_python_script\"" << std::endl;
52
}
53
54
// 清理 Python 解释器
55
Py_Finalize();
56
std::cout << "Python interpreter finalized." << std::endl;
57
58
return 0;
59
}
嵌入Python需要更深入地理解Python对象的生命周期和引用计数,并且需要处理Python的异常。虽然可以使用Boost.Python或PyBind11来简化嵌入过程,但相比扩展Python,嵌入Python通常涉及更多的Python C API调用。
18.4 C++与Java的互操作:JNI (Interoperability between C++ and Java: JNI)
Java是一种广泛用于企业级应用开发的跨平台语言。为了利用C++在性能、底层访问或现有库方面的优势,Java提供了 JNI(Java Native Interface)机制,允许Java代码调用本地(Native)代码(通常是C或C++),反之亦然。
18.4.1 JNI 概述 (JNI Overview)
JNI是Java虚拟机(JVM)定义的一套编程接口,用于实现Java代码与运行在JVM外部的本地应用和库的交互。通过JNI,可以在Java程序中声明本地方法(Native Methods),然后用C/C++实现这些方法。同时,本地代码也可以通过JNI接口访问Java对象、调用Java方法、创建Java线程等。
18.4.2 JNI 基本概念 (JNI Basic Concepts)
⚝ 本地方法 (Native Method): 在Java代码中声明时带有 native
关键字的方法,其实现由C/C++等本地语言提供。
⚝ JVM 调用接口 (Invocation API): 允许C/C++程序创建、加载并嵌入JVM。
⚝ JNI 环境接口 (JNI Environment Interface): JNI 函数表,包含了本地代码与JVM交互所需的所有函数指针。本地方法通过 JNIEnv*
指针访问这些函数。
⚝ JNI 接口指针 (JNI Interface Pointer): 指向JNI环境接口的指针,作为本地方法的第一个参数传递。
⚝ 本地引用 (Local References) 和全局引用 (Global References): JNI对象引用类型。本地引用仅在本地方法调用期间有效,垃圾回收器不会回收被全局引用持有的对象。需要手动管理这些引用。
18.4.3 C++ 调用 Java (C++ Calling Java) (via JNI)
在C++(本地代码)中调用Java代码(如创建Java对象、调用Java方法)通常发生在以下场景:C++程序嵌入了JVM,或者Java本地方法实现中需要回调(Callback)Java代码。这需要使用JVM调用接口来启动JVM(如果尚未启动),然后使用JNI环境接口提供的函数。
示例(概念性代码,省略了大量错误检查):
1
// cpp_embed_java.cpp
2
#include <jni.h> // 包含 JNI 头文件
3
#include <iostream>
4
5
int main() {
6
JavaVM *jvm; // Java虚拟机
7
JNIEnv *env; // JNI环境指针
8
JavaVMInitArgs vm_args; // 初始化参数
9
JavaVMOption options[1]; // JVM选项
10
11
// 设置 classpath
12
options[0].optionString = (char*)"-Djava.class.path=."; // 指定当前目录为 classpath
13
vm_args.version = JNI_VERSION_1_8; // 指定 JNI 版本
14
vm_args.nOptions = 1;
15
vm_args.options = options;
16
vm_args.ignoreUnrecognized = JNI_FALSE;
17
18
// 创建 JVM
19
jint res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
20
if (res != JNI_OK) {
21
std::cerr << "Failed to create JVM" << std::endl;
22
return 1;
23
}
24
25
std::cout << "JVM created." << std::endl;
26
27
// 查找 Java 类 (使用全限定名,斜杠分隔)
28
jclass stringClass = env->FindClass("java/lang/String");
29
if (stringClass == NULL) {
30
std::cerr << "Failed to find String class" << std::endl;
31
// 销毁 JVM
32
jvm->DestroyJavaVM();
33
return 1;
34
}
35
36
// 创建一个 Java 字符串对象
37
jstring message = env->NewStringUTF("Hello from C++ via JNI!");
38
if (message == NULL) {
39
std::cerr << "Failed to create Java string" << std::endl;
40
env->DeleteLocalRef(stringClass);
41
jvm->DestroyJavaVM();
42
return 1;
43
}
44
45
// 查找 System.out 字段 (静态字段)
46
jclass systemClass = env->FindClass("java/lang/System");
47
if (systemClass == NULL) { /* Error handling */ }
48
jfieldID outFieldID = env->GetStaticFieldID(systemClass, "out", "Ljava/io/PrintStream;");
49
if (outFieldID == NULL) { /* Error handling */ }
50
jobject printStreamObj = env->GetStaticObjectField(systemClass, outFieldID);
51
if (printStreamObj == NULL) { /* Error handling */ }
52
53
// 查找 PrintStream.println(String) 方法
54
jclass printStreamClass = env->GetObjectClass(printStreamObj);
55
if (printStreamClass == NULL) { /* Error handling */ }
56
jmethodID printlnMethodID = env->GetMethodID(printStreamClass, "println", "(Ljava/lang/String;)V");
57
if (printlnMethodID == NULL) { /* Error handling */ }
58
59
// 调用 PrintStream.println 方法
60
env->CallVoidMethod(printStreamObj, printlnMethodID, message);
61
62
// 释放局部引用
63
env->DeleteLocalRef(message);
64
env->DeleteLocalRef(stringClass);
65
env->DeleteLocalRef(systemClass);
66
env->DeleteLocalRef(printStreamObj);
67
env->DeleteLocalRef(printStreamClass);
68
69
70
// 销毁 JVM
71
jvm->DestroyJavaVM();
72
std::cout << "JVM destroyed." << std::endl;
73
74
return 0;
75
}
这种方式涉及大量的JNI函数调用,代码相对繁琐,并且需要手动管理JNI引用。
18.4.4 Java 调用 C++ (Java Calling C++) (via JNI)
这是更常见的JNI使用场景:在Java代码中声明本地方法,然后在C/C++中实现它。
① 在 Java 中声明本地方法:
1
// MyNativeClass.java
2
public class MyNativeClass {
3
// 声明一个本地方法
4
public native void displayMessage(String message);
5
6
// 用于加载本地库
7
static {
8
// 加载名为 "mynativelib" 的库 (对应 libmynativelib.so 或 mynativelib.dll)
9
System.loadLibrary("mynativelib");
10
}
11
12
public static void main(String[] args) {
13
MyNativeClass obj = new MyNativeClass();
14
obj.displayMessage("Hello from Java!");
15
}
16
}
② 生成 JNI 头文件: 使用 javac
编译Java文件,然后使用 javah
工具(在较新JDK版本中已集成到 javac
)生成对应的C/C++头文件。
1
javac MyNativeClass.java
2
# For modern JDKs (9+):
3
javac -h . MyNativeClass.java
4
# For older JDKs (up to 8):
5
# javah MyNativeClass
这会生成一个类似 MyNativeClass.h
的头文件:
1
/* DO NOT EDIT THIS FILE - it is machine generated */
2
#include <jni.h>
3
/* Header for class MyNativeClass */
4
5
#ifndef _Included_MyNativeClass
6
#define _Included_MyNativeClass
7
#ifdef __cplusplus
8
extern "C" {
9
#endif
10
/*
11
* Class: MyNativeClass
12
* Method: displayMessage
13
* Signature: (Ljava/lang/String;)V
14
*/
15
JNIEXPORT void JNICALL Java_MyNativeClass_displayMessage
16
(JNIEnv *, jobject, jstring);
17
18
#ifdef __cplusplus
19
}
20
#endif
21
#endif
注意函数名 Java_MyNativeClass_displayMessage
的命名规则以及 extern "C"
的使用。
③ 在 C++ 中实现本地方法: 根据生成的头文件,编写C++代码实现本地方法。
1
// MyNativeClass.cpp
2
#include "MyNativeClass.h"
3
#include <iostream>
4
#include <string>
5
6
JNIEXPORT void JNICALL Java_MyNativeClass_displayMessage
7
(JNIEnv *env, jobject obj, jstring message) // env 是 JNI 环境指针,obj 是 Java 对象实例,message 是 Java 字符串参数
8
{
9
// 将 Java 字符串转换为 C 风格字符串
10
const char *message_chars = env->GetStringUTFChars(message, 0);
11
if (message_chars == NULL) {
12
// 获取字符串失败,处理错误
13
return;
14
}
15
16
std::cout << "Message from Java: " << message_chars << " - from C++ native method\n";
17
18
// 释放 C 风格字符串,非常重要!
19
env->ReleaseStringUTFChars(message, message_chars);
20
}
注意:在JNI中传递Java对象(如 jstring
, jobject
等)实际上是传递了Java对象的引用。这些引用是本地引用,在本地方法返回后会自动失效。如果需要在多次本地方法调用之间或异步操作中持有Java对象,需要创建全局引用(env->NewGlobalRef()
)。
④ 编译 C++ 代码并生成本地库: 将C++文件编译为动态链接库(.so
, .dll
, .dylib
),命名需要与Java代码中 System.loadLibrary()
指定的名称对应。
1
# Linux (using g++)
2
g++ -shared -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" MyNativeClass.cpp -o libmynativelib.so
3
4
# Windows (using g++)
5
# g++ -shared -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" MyNativeClass.cpp -o mynativelib.dll
⑤ 运行 Java 程序: 将编译好的本地库放在Java程序的库路径(java.library.path
)下,然后运行Java类。
1
# Linux
2
java -Djava.library.path=. MyNativeClass
3
4
# Windows
5
# java -Djava.library.path=. MyNativeClass
输出:
1
Message from Java: Hello from Java! - from C++ native method
JNI提供了强大的互操作能力,但也带来了复杂性,包括手动管理引用、平台相关的库编译和部署、以及错误处理(本地代码中的崩溃可能导致JVM崩溃)。因此,JNI通常用于性能瓶颈模块或必须调用本地API的场景。
18.5 混合编程的应用场景和考虑事项 (Application Scenarios and Considerations for Mixed Programming)
混合编程虽然复杂,但在特定场景下能带来显著优势。
18.5.1 性能敏感模块 (Performance-Sensitive Modules)
⚝ 将算法复杂、计算量大、对响应时间要求高的部分用C++实现,然后在其他语言(如Python、Java)中调用。例如,图像处理、科学计算、金融建模、游戏物理引擎等。
18.5.2 利用现有库和生态系统 (Leveraging Existing Libraries and Ecosystems)
⚝ 如果存在一个成熟、高效且难以用当前语言重写的库(例如,很多操作系统API是C/C++实现的,很多科学计算库如LAPACK、BLAS是Fortran/C实现的),可以通过混合编程的方式直接利用它们。
⚝ 在C++中使用Python的科学计算库(NumPy, SciPy)或机器学习库(TensorFlow, PyTorch),或在Python中使用C++图形库、网络库等。
18.5.3 UI 与后端分离 (Separation of UI and Backend)
⚝ 使用Python、Java等语言快速开发用户界面(UI),而将核心逻辑、数据处理或与硬件交互的部分用C++实现。
18.5.4 数据类型映射和内存管理 (Data Type Mapping and Memory Management)
⚝ 数据类型 (Data Types): 在语言边界传递数据时,需要小心处理不同语言的数据类型差异。简单类型通常可以直接映射,复杂类型(如对象、数组、容器)需要进行序列化/反序列化或通过指针/引用进行操作,确保内存布局和访问方式兼容。
⚝ 内存管理 (Memory Management): 托管语言(带垃圾回收)和非托管语言混合时,必须协调内存所有权。通常,谁分配的内存就由谁释放。例如,C++分配的内存不能直接交给Python或Java的垃圾回收器管理,需要在C++侧提供相应的释放接口。使用智能指针(Smart Pointers)可以简化C++侧的内存管理,但在跨语言传递时需要特殊处理。
18.5.5 错误处理和异常传播 (Error Handling and Exception Propagation)
⚝ 异常处理机制在不同语言中差异很大。C++使用 try-catch
,Python使用 try-except
,Java使用 try-catch-finally
。直接跨越语言边界传播异常通常是不可行的。
⚝ 常见的做法是在语言边界捕获异常,并将其转换为对方语言能够理解的错误表示(如错误码、特定的错误对象、或将C++异常映射到Python/Java异常),然后在对方语言中重新抛出或处理。
18.5.6 构建和部署复杂性 (Build and Deployment Complexity)
⚝ 混合项目需要配置多种语言的编译器/解释器、头文件、库依赖等,构建过程比单一语言复杂。
⚝ 部署时需要确保所有依赖的语言运行时、库文件都正确安装并能被找到。
总体而言,混合编程是一项强大的技术,可以帮助开发者构建更高效、更灵活的系统。但在采用混合编程方案之前,应充分评估其带来的额外复杂性,并仔细设计语言间的接口和数据交换方式。对于C++与其他语言的互操作,C++与C的互操作是基础,而与Python、Java等语言的互操作则依赖于特定的绑定工具或原生接口(如JNI),需要对相应语言的特性和交互机制有深入理解。
<END_OF_CHAPTER/>
Appendix A: 附录A:C++关键字 (Keywords) 列表
本附录列出了C++语言的所有关键字 (Keywords),并简要解释其在C++程序中的用途和含义。理解关键字是掌握C++语法的基础。
⚝ alignas
(对齐声明): 用于指定一个对象的对齐方式。
⚝ alignof
(对齐查询): 用于查询一个类型或对象的对齐要求。
⚝ and
(逻辑与): 替代符号 &&
。
⚝ and_eq
(按位与赋值): 替代符号 &=
。
⚝ asm
(汇编): 用于在C++代码中嵌入汇编代码块。现代C++中较少直接使用。
⚝ atomic_cancel
(原子取消): 与事务内存 (Transactional Memory) 相关,用于取消事务。
⚝ atomic_commit
(原子提交): 与事务内存 (Transactional Memory) 相关,用于提交事务。
⚝ atomic_noexcept
(原子无异常): 与事务内存 (Transactional Memory) 相关,指示在事务中不抛出异常。
⚝ auto
(自动类型推导): (C++11起) 用于自动推导变量的类型。
⚝ bitand
(按位与): 替代符号 &
。
⚝ bitor
(按位或): 替代符号 |
。
⚝ bool
(布尔类型): 表示布尔值,取值为 true
或 false
。
⚝ break
(跳出): 用于跳出最内层的循环 (for
, while
, do-while
) 或 switch
语句。
⚝ case
(情况): switch
语句中的一个分支标签。
⚝ catch
(捕获异常): 异常处理机制的一部分,用于捕获抛出的异常。
⚝ char
(字符类型): 表示一个字符,通常为一个字节。
⚝ char8_t
(UTF-8字符类型): (C++20起) 表示一个UTF-8编码的字符。
⚝ char16_t
(UTF-16字符类型): (C++11起) 表示一个UTF-16编码的字符。
⚝ char32_t
(UTF-32字符类型): (C++11起) 表示一个UTF-32编码的字符。
⚝ class
(类): 用于定义用户自定义的数据类型,是面向对象编程的基础。
⚝ compl
(按位取反): 替代符号 ~
。
⚝ concept
(概念): (C++20起) 用于约束模板参数的类型,提高模板代码的可读性和错误提示。
⚝ const
(常量): 用于声明常量或指定对象不可修改。
⚝ consteval
(立即求值函数): (C++20起) 指定函数必须在编译时求值。
⚝ constexpr
(常量表达式): (C++11起) 指定变量、函数或构造函数可以在编译时求值。
⚝ constinit
(常量初始化): (C++20起) 指定静态或线程局部变量必须由常量表达式进行初始化。
⚝ const_cast
(常量转换): 用于移除表达式的 const
或 volatile
属性。
⚝ continue
(继续): 用于跳过当前循环迭代的剩余部分,进入下一次迭代。
⚝ co_await
(协同等待): (C++20起) 用于暂停协程执行直到等待的对象完成。
⚝ co_return
(协同返回): (C++20起) 用于从协程返回结果或结束协程。
⚝ co_yield
(协同生成): (C++20起) 用于在协程中生成一个值并暂停执行。
⚝ decltype
(声明类型): (C++11起) 用于查询表达式的类型。
⚝ default
(默认):
▮▮▮▮⚝ 在 switch
语句中,表示当没有 case
匹配时的默认分支。
▮▮▮▮⚝ 用于指定类的特殊成员函数 (如构造函数、析构函数、赋值运算符) 使用编译器生成的默认实现。
⚝ delete
(删除):
▮▮▮▮⚝ 用于释放动态分配的内存。
▮▮▮▮⚝ 用于阻止类的特殊成员函数被使用。
⚝ do
(做): do-while
循环的一部分,表示循环体。
⚝ double
(双精度浮点型): 表示双精度浮点数。
⚝ dynamic_cast
(动态转换): 用于在运行时安全地进行多态类型的向下转换。
⚝ else
(否则): if
语句的一部分,表示条件不成立时执行的代码块。
⚝ enum
(枚举): 用于定义一组命名的整数常量。
⚝ explicit
(显式): 用于阻止构造函数或转换函数进行隐式类型转换。
⚝ export
(导出): 用于导出模板定义,在C++11后已移除,C++20重新引入但用途有限。
⚝ extern
(外部链接): 用于声明变量或函数具有外部链接,可以在其他翻译单元中访问。
⚝ false
(假): 布尔常量,表示逻辑假。
⚝ float
(单精度浮点型): 表示单精度浮点数。
⚝ for
(循环): 用于创建循环结构,常用于计数循环。
⚝ friend
(友元): 用于声明函数或类为当前类的友元,允许访问其私有和保护成员。
⚝ goto
(跳转): 用于无条件跳转到程序中的标签位置。通常不推荐使用。
⚝ if
(如果): 用于创建条件判断结构。
⚝ inline
(内联): 建议编译器将函数体直接嵌入到调用点,以减少函数调用开销。
⚝ int
(整型): 表示整数类型。
⚝ long
(长整型): 表示长度不小于 int
的整型。可以与 int
结合使用 (long int
)。
⚝ long long
(特长整型): (C++11起) 表示长度不小于 long
的整型。
⚝ mutable
(可变): 用于在 const
成员函数中修改类的成员变量。
⚝ namespace
(命名空间): 用于组织代码,避免命名冲突。
⚝ new
(新建): 用于在堆上动态分配内存。
⚝ noexcept
(无异常): (C++11起) 指定函数是否可能抛出异常。
⚝ not
(逻辑非): 替代符号 !
。
⚝ not_eq
(不等于): 替代符号 !=
。
⚝ nullptr
(空指针常量): (C++11起) 表示空指针。
⚝ operator
(运算符): 用于定义或重载运算符。
⚝ or
(逻辑或): 替代符号 ||
。
⚝ or_eq
(按位或赋值): 替代符号 |=
。
⚝ private
(私有): 类成员访问修饰符,表示成员只能在类内部访问。
⚝ protected
(保护): 类成员访问修饰符,表示成员可以在类内部及其派生类中访问。
⚝ public
(公有): 类成员访问修饰符,表示成员可以在任何地方访问。
⚝ reflexpr
(反射): (C++23) 与元编程和反射相关,用于编译时获取程序结构信息。
⚝ register
(寄存器): 建议编译器将变量存储在寄存器中以提高访问速度。现代编译器通常会自行优化,此关键字作用有限。
⚝ reinterpret_cast
(重解释转换): 用于进行低级别的、位模式的类型转换,通常用于指针类型之间。
⚝ requires
(要求): (C++20起) 用于概念 (Concepts) 中,指定模板参数必须满足的约束。
⚝ return
(返回): 用于从函数返回一个值或结束函数执行。
⚝ short
(短整型): 表示长度不大于 int
的整型。可以与 int
结合使用 (short int
)。
⚝ signed
(有符号): 用于指定整型类型是有符号的。通常可以省略,因为整型默认为有符号。
⚝ sizeof
(大小): 用于获取类型或对象在内存中占用的字节数。
⚝ static
(静态):
▮▮▮▮⚝ 在函数内部,声明静态局部变量,其生命周期贯穿整个程序。
▮▮▮▮⚝ 在类内部,声明静态成员变量或静态成员函数,它们属于类而不是类的具体对象。
▮▮▮▮⚝ 在全局或命名空间作用域,声明静态变量或函数,使其仅在当前翻译单元内可见 (内部链接)。
⚝ static_assert
(静态断言): (C++11起) 用于在编译时检查条件,如果条件不满足,则产生编译错误。
⚝ static_cast
(静态转换): 用于进行类型之间的显式转换,例如基本类型之间、派生类和基类指针/引用之间。
⚝ struct
(结构体): 用户自定义数据类型,与 class
类似,但在C++中默认成员访问权限为 public
。
⚝ switch
(分支选择): 用于创建多分支选择结构。
⚝ synchronized
(同步): (C++11事务内存) 与事务内存相关,用于标记一个同步代码块。
⚝ template
(模板): 用于定义函数模板或类模板,实现泛型编程。
⚝ this
(当前对象指针): 在类的成员函数中,指向调用该成员函数的当前对象。
⚝ thread_local
(线程局部存储): (C++11起) 指定变量具有线程局部存储期,每个线程都有其独立的变量副本。
⚝ throw
(抛出异常): 用于抛出一个异常,启动异常处理过程。
⚝ true
(真): 布尔常量,表示逻辑真。
⚝ try
(尝试): 异常处理机制的一部分,用于标记可能抛出异常的代码块。
⚝ typedef
(类型定义): 用于为现有类型创建新的别名。在现代C++中, using
通常更推荐。
⚝ typeid
(类型标识): 用于在运行时获取对象的动态类型信息。
⚝ typename
(类型名): 用于在模板声明中指定一个依赖名称是一个类型。
⚝ union
(联合体): 用户自定义数据类型,其成员共享同一块内存区域。
⚝ unsigned
(无符号): 用于指定整型类型是无符号的,只能表示非负值。
⚝ using
(使用):
▮▮▮▮⚝ 引入命名空间中的名称。
▮▮▮▮⚝ (C++11起) 用于定义类型别名,替代 typedef
。
▮▮▮▮⚝ (C++11起) 引入基类成员名称。
⚝ virtual
(虚函数): 用于在基类中声明虚函数,实现多态性。
⚝ void
(无类型): 表示空类型,常用于指定函数没有返回值或参数。
⚝ volatile
(易失的): 建议编译器不要对变量进行优化,每次访问都从内存中读取,适用于多线程或硬件相关的变量。
⚝ wchar_t
(宽字符类型): 表示一个宽字符,用于处理Unicode等字符集。
⚝ while
(循环): 用于创建条件循环结构。
⚝ xor
(按位异或): 替代符号 ^
。
⚝ xor_eq
(按位异或赋值): 替代符号 ^=
。
Appendix B: 附录B:运算符优先级 (Operator Precedence) 表
在C++语言中,表达式的求值顺序很大程度上取决于运算符的优先级 (Operator Precedence) 和结合性 (Associativity)。优先级决定了在没有括号的情况下,哪个运算符先被计算。结合性则决定了当多个同等优先级的运算符出现在同一个表达式中时,它们的分组顺序(是从左往右还是从右往左)。📘 正确理解和运用运算符优先级和结合性对于编写正确且可读的C++代码至关重要。
下表列出了C++中常用运算符的优先级和结合性。优先级级别从高到低排列,级别1最高,级别18最低。在同一个优先级级别内的运算符,其求值顺序由结合性决定。
优先级级别 (Precedence Level) | 运算符 (Operators) | 描述 (Description) | 结合性 (Associativity) |
---|---|---|---|
1 | :: | 作用域解析 (Scope resolution) | 左到右 (Left-to-right) |
2 | () [] . -> | 后缀形式的自增/自减 (Postfix increment/decrement) 函数调用 (Function call) 数组下标 (Array subscript) 成员访问 (Member access) | 左到右 (Left-to-right) |
3 | ++ -- + - ! ~ (type) * & sizeof alignof new delete noexcept await | 前缀形式的自增/自减 (Prefix increment/decrement) 一元加/减 (Unary plus/minus) 逻辑非 (Logical NOT) 位非 (Bitwise NOT) 类型转换 (Type cast) 解引用 (Dereference) 取地址 (Address-of) 对象大小 (Size of object) 对齐要求 (Alignment requirement) 动态内存分配 (Dynamic memory allocation) 动态内存释放 (Dynamic memory deallocation) noexcept 运算符 (noexcept operator) await 表达式 (await expression) | 右到左 (Right-to-left) |
4 | .* ->* | 成员指针访问 (Member pointer access) | 左到右 (Left-to-right) |
5 | * / % | 乘法 (Multiplication) 除法 (Division) 取余 (Modulo) | 左到右 (Left-to-right) |
6 | + - | 加法 (Addition) 减法 (Subtraction) | 左到右 (Left-to-right) |
7 | << >> | 位左移 (Bitwise left shift) 位右移 (Bitwise right shift) | 左到右 (Left-to-right) |
8 | < <= > >= | 关系运算符 (Relational operators) | 左到右 (Left-to-right) |
9 | == != | 相等性运算符 (Equality operators) | 左到右 (Left-to-right) |
10 | & | 位与 (Bitwise AND) | 左到右 (Left-to-right) |
11 | ^ | 位异或 (Bitwise XOR) | 左到右 (Left-to-right) |
12 | | | 位或 (Bitwise OR) | 左到右 (Left-to-right) |
13 | && | 逻辑与 (Logical AND) | 左到右 (Left-to-right) |
14 | || | 逻辑或 (Logical OR) | 左到右 (Left-to-right) |
15 | ?: | 三元条件运算符 (Ternary conditional operator) | 右到左 (Right-to-left) |
16 | = += -= *= /= %= <<= >>= &= ^= |= | 赋值运算符 (Assignment operators) | 右到左 (Right-to-left) |
17 | throw | 抛出异常 (Throw exception) | 右到左 (Right-to-left) |
18 | , | 逗号运算符 (Comma operator) | 左到右 (Left-to-right) |
✨ 如何使用此表:
当一个表达式中包含多个不同的运算符时,优先级高的运算符会先于优先级低的运算符进行求值。例如,在表达式 a + b * c
中,乘法运算符 *
的优先级高于加法运算符 +
,因此 b * c
会先被计算,然后再与 a
相加。
如果表达式中包含多个具有相同优先级的运算符,它们的求值顺序则由其结合性决定。例如,在表达式 a - b - c
中,减法运算符 -
的结合性是左到右,因此 a - b
会先被计算,然后再减去 c
。而在表达式 a = b = c
中,赋值运算符 =
的结合性是右到左,因此 b = c
会先被计算(并将 c
的值赋给 b
),然后再将 b
的新值赋给 a
。
重要提示: 虽然了解运算符优先级和结合性有助于理解表达式,但在实际编程中,为了提高代码的可读性和避免潜在的错误,强烈建议使用括号 ()
来明确指定求值顺序,即使默认的优先级和结合性已经满足要求。这能使代码意图更清晰,也更容易被其他开发者理解。📝
Appendix C: 附录C:ASCII码表
欢迎来到本书的附录部分。在本附录中,我们将探讨一个在计算机科学和编程中基础且重要的概念——ASCII码(美国标准信息交换码)。理解字符是如何在计算机内部表示和处理的,对于深入学习C++及其他编程语言都至关重要。本附录将为您提供一份标准的ASCII码表,并简要解释其结构和意义,方便您在学习和实践中查阅。
什么是ASCII码?
ASCII(American Standard Code for Information Interchange,美国标准信息交换码)是一种基于拉丁字母的字符编码系统。它最初设计为7位编码,用于在不同计算机和设备之间交换文本信息。标准的ASCII码定义了128个字符,包括大写和小写字母、数字0-9、标点符号以及一些控制字符。每个字符都被赋予一个唯一的0到127之间的十进制数值,这个数值就是该字符的ASCII码。
在C++中,char
类型通常用来表示一个字符。在大多数现代系统上,char
类型的大小是1个字节(8位),它可以存储0到255之间的值。标准的ASCII码只使用了7位(即0-127),最高位通常为0。扩展ASCII码(Extended ASCII)则利用了第8位,将编码范围扩展到0-255,增加了额外的字符,如带有变音符号的字母、图形符号等,但这些扩展码在不同系统和编码页中可能不兼容。本书主要关注标准的ASCII码(0-127)。
理解ASCII码有助于我们理解字符在内存中的存储方式,以及进行字符与整数之间的转换。例如,在C++中,一个 char
类型的变量实际上存储的是其对应的ASCII数值。当我们打印一个 char
变量时,输出流会根据这个数值将其解释为相应的字符进行显示。
下面是标准的ASCII码表(0-127),表中通常会列出字符的十进制 (Decimal)、十六进制 (Hexadecimal) 和对应的字符或描述。
ASCII码表
▮▮▮▮ 控制字符 (Control Characters) (0-31)
这些字符通常用于控制设备或传输信息,例如换行、回车、响铃等,它们大多数是不可打印的。
Dec | Hex | 缩写 | 描述(英文) | 描述(中文) |
---|---|---|---|---|
0 | 00 | NUL | Null | 空字符 |
1 | 01 | SOH | Start of Heading | 标题开始 |
2 | 02 | STX | Start of Text | 文本开始 |
3 | 03 | ETX | End of Text | 文本结束 |
4 | 04 | EOT | End of Transmission | 传输结束 |
5 | 05 | ENQ | Enquiry | 查询 |
6 | 06 | ACK | Acknowledge | 肯定应答 |
7 | 07 | BEL | Bell | 响铃 |
8 | 08 | BS | Backspace | 退格 |
9 | 09 | HT | Horizontal Tab | 水平制表符 |
10 | 0A | LF | Line Feed | 换行 |
11 | 0B | VT | Vertical Tab | 垂直制表符 |
12 | 0C | FF | Form Feed | 换页 |
13 | 0D | CR | Carriage Return | 回车 |
14 | 0E | SO | Shift Out | 切换出 |
15 | 0F | SI | Shift In | 切换进 |
16 | 10 | DLE | Data Link Escape | 数据链路转义 |
17 | 11 | DC1 | Device Control 1 | 设备控制 1 |
18 | 12 | DC2 | Device Control 2 | 设备控制 2 |
19 | 13 | DC3 | Device Control 3 | 设备控制 3 |
20 | 14 | DC4 | Device Control 4 | 设备控制 4 |
21 | 15 | NAK | Negative Acknowledge | 否定应答 |
22 | 16 | SYN | Synchronous Idle | 同步空闲 |
23 | 17 | ETB | End of Transmission Block | 传输块结束 |
24 | 18 | CAN | Cancel | 作废 |
25 | 19 | EM | End of Medium | 介质结束 |
26 | 1A | SUB | Substitute | 替换 |
27 | 1B | ESC | Escape | 退出 |
28 | 1C | FS | File Separator | 文件分隔符 |
29 | 1D | GS | Group Separator | 组分隔符 |
30 | 1E | RS | Record Separator | 记录分隔符 |
31 | 1F | US | Unit Separator | 单元分隔符 |
▮▮▮▮ 可打印字符 (Printable Characters) (32-127)
这些字符是可见的,包括空格、标点符号、数字和英文字母。
Dec | Hex | Char | Dec | Hex | Char | Dec | Hex | Char | Dec | Hex | Char |
---|---|---|---|---|---|---|---|---|---|---|---|
32 | 20 | 64 | 40 | @ | 96 | 60 | ` | 128 | 80 | DEL | |
33 | 21 | ! | 65 | 41 | A | 97 | 61 | a | |||
34 | 22 | " | 66 | 42 | B | 98 | 62 | b | |||
35 | 23 | # | 67 | 43 | C | 99 | 63 | c | |||
36 | 24 | $ | 68 | 44 | D | 100 | 64 | d | |||
37 | 25 | % | 69 | 45 | E | 101 | 65 | e | |||
38 | 26 | & | 70 | 46 | F | 102 | 66 | f | |||
39 | 27 | ' | 71 | 47 | G | 103 | 67 | g | |||
40 | 28 | ( | 72 | 48 | H | 104 | 68 | h | |||
41 | 29 | ) | 73 | 49 | I | 105 | 69 | i | |||
42 | 2A | * | 74 | 4A | J | 106 | 6A | j | |||
43 | 2B | + | 75 | 4B | K | 107 | 6B | k | |||
44 | 2C | , | 76 | 4C | L | 108 | 6C | l | |||
45 | 2D | - | 77 | 4D | M | 109 | 6D | m | |||
46 | 2E | . | 78 | 4E | N | 110 | 6E | n | |||
47 | 2F | / | 79 | 4F | O | 111 | 6F | o | |||
48 | 30 | 0 | 80 | 50 | P | 112 | 70 | p | |||
49 | 31 | 1 | 81 | 51 | Q | 113 | 71 | q | |||
50 | 32 | 2 | 82 | 52 | R | 114 | 72 | r | |||
51 | 33 | 3 | 83 | 53 | S | 115 | 73 | s | |||
52 | 34 | 4 | 84 | 54 | T | 116 | 74 | t | |||
53 | 35 | 5 | 85 | 55 | U | 117 | 75 | u | |||
54 | 36 | 6 | 86 | 56 | V | 118 | 76 | v | |||
55 | 37 | 7 | 87 | 57 | W | 119 | 77 | w | |||
56 | 38 | 8 | 88 | 58 | X | 120 | 78 | x | |||
57 | 39 | 9 | 89 | 59 | Y | 121 | 79 | y | |||
58 | 3A | : | 90 | 5A | Z | 122 | 7A | z | |||
59 | 3B | ; | 91 | 5B | [ | 123 | 7B | { | |||
60 | 3C | < | 92 | 5C | \ | 124 | 7C | ||||
61 | 3D | = | 93 | 5D | ] | 125 | 7D | } | |||
62 | 3E | > | 94 | 5E | ^ | 126 | 7E | ~ | |||
63 | 3F | ? | 95 | 5F | _ | 127 | 7F | DEL |
注:表中 Dec 127 对应的字符 DEL (Delete) 也是一个控制字符,常被包含在可打印字符的范围表格中。Dec 32 是空格字符 (Space)。
通过查阅此表,您可以方便地找到任何标准ASCII字符对应的十进制或十六进制数值,这对于理解字符编码、进行底层操作或解析特定格式数据时非常有用。😊
Appendix D: 附录D:常用C++开发工具和资源
Appendix D1: C++开发工具 (Development Tools)
编写和运行C++程序需要一系列工具。本节将介绍一些常用的C++开发工具,包括编译器(Compiler)、集成开发环境(IDE)、代码编辑器(Code Editor)、构建工具(Build Tool)、调试器(Debugger)和代码分析工具(Code Analysis Tool)。选择合适的工具能够极大地提高开发效率和代码质量。
Appendix D1.1: 编译器 (Compilers)
编译器是将C++源代码转换成可执行机器代码的关键工具。选择合适的编译器通常取决于你的操作系统和开发需求。
⚝ GCC (GNU Compiler Collection): 这是一个开源的、跨平台的编译器集合,支持多种编程语言,包括C++。GCC是Linux系统上最常用的C++编译器,也支持Windows(通过MinGW或Cygwin)和macOS。GCC的C++编译器通常被称为g++
。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 开源免费。
▮▮▮▮▮▮▮▮⚝ 支持最新的C++标准(C++11, C++14, C++17, C++20等)。
▮▮▮▮▮▮▮▮⚝ 广泛应用于各种平台。
▮▮▮▮⚝ 获取: 在Linux上通常通过包管理器安装(如sudo apt-get install g++
);在Windows上可以安装MinGW或Cygwin;在macOS上安装Xcode命令行工具即可获得。
⚝ Clang: 这是一个基于LLVM (Low Level Virtual Machine) 的编译器前端,支持C、C++、Objective-C等语言。Clang以其优秀的错误提示信息和快速编译速度而闻名。它是macOS和iOS开发的首选编译器,在Linux和Windows上也越来越流行。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 开源免费。
▮▮▮▮▮▮▮▮⚝ 错误提示友好,易于理解。
▮▮▮▮▮▮▮▮⚝ 编译速度通常比GCC快。
▮▮▮▮▮▮▮▮⚝ 生成的代码质量高。
▮▮▮▮⚝ 获取: 在macOS上安装Xcode命令行工具;在Linux上通常通过包管理器安装(如sudo apt-get install clang
);在Windows上可以通过LLVM官网下载安装程序。
⚝ MSVC (Microsoft Visual C++): 这是Microsoft为Windows平台开发的C++编译器,是Visual Studio集成开发环境的一部分。MSVC是Windows平台上进行C++开发的主流选择,尤其适用于开发Windows应用程序和游戏。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ tightly集成于Visual Studio IDE,提供丰富的开发工具。
▮▮▮▮▮▮▮▮⚝ 对Windows平台的支持非常好。
▮▮▮▮▮▮▮▮⚝ 支持最新的C++标准。
▮▮▮▮⚝ 获取: 安装Visual Studio(提供社区版、专业版、企业版,社区版免费供个人和小型团队使用)。
Appendix D1.2: 集成开发环境 (IDEs - Integrated Development Environment)
IDE集成了代码编辑、编译、链接、调试等多种功能,为开发者提供一站式服务,极大地提高了开发效率。
⚝ Visual Studio: Microsoft出品的强大IDE,主要用于Windows平台开发,也支持跨平台开发(如使用CMake或特定插件)。功能强大,社区版免费,适合各种规模的项目。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 功能全面,集成了编辑器、编译器、调试器、项目管理等。
▮▮▮▮▮▮▮▮⚝ 强大的调试功能。
▮▮▮▮▮▮▮▮⚝ 丰富的插件生态系统。
▮▮▮▮⚝ 平台: Windows为主,有限支持macOS(Visual Studio for Mac,主要用于.NET和移动开发,非原生C++ IDE)。
⚝ VS Code (Visual Studio Code): Microsoft出品的轻量级但功能强大的代码编辑器,通过安装C++扩展可以成为一个优秀的C++ IDE。跨平台支持(Windows, macOS, Linux),免费开源。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 轻量级,启动速度快。
▮▮▮▮▮▮▮▮⚝ 丰富的扩展市场,可定制性极高。
▮▮▮▮▮▮▮▮⚝ 内置Git支持。
▮▮▮▮▮▮▮▮⚝ 跨平台。
▮▮▮▮⚝ 平台: Windows, macOS, Linux。需要配合GCC, Clang, MSVC等编译器和CMake, Make等构建工具使用。
⚝ CLion: JetBrains公司出品的商业C++ IDE。功能强大,智能代码分析、重构和调试功能非常出色,对CMake项目支持良好。适合专业的C++开发者。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 智能代码补全、分析和重构功能。
▮▮▮▮▮▮▮▮⚝ 强大的调试器界面。
▮▮▮▮▮▮▮▮⚝ 内置版本控制工具集成。
▮▮▮▮▮▮▮▮⚝ 跨平台。
▮▮▮▮⚝ 平台: Windows, macOS, Linux。商业软件(提供学生和教师免费许可证)。
⚝ Code::Blocks: 开源的、跨平台的免费C++ IDE。支持多种编译器,界面简洁,适合初学者使用。
▮▮▮▮⚝ 特点:
▮▮▮▮▮▮▮▮⚝ 开源免费。
▮▮▮▮▮▮▮▮⚝ 跨平台。
▮▮▮▮▮▮▮▮⚝ 界面相对简单,易于上手。
▮▮▮▮⚝ 平台: Windows, macOS, Linux。
Appendix D1.3: 代码编辑器 (Code Editors)
如果你不需要一个完整的IDE,或者偏好轻量级的工具,代码编辑器是很好的选择。它们通常提供语法高亮、代码补全等功能,可以通过插件扩展功能。
⚝ VS Code: 如上所述,功能强大的编辑器,通过扩展可作为IDE。
⚝ Sublime Text: 流行的商业代码编辑器,速度快,功能强大,支持多种语言,通过插件可增强C++开发体验。
⚝ Atom: GitHub开发的免费开源代码编辑器,可高度定制,插件丰富。
⚝ Vim / Emacs: 历史悠久、功能极度强大的文本编辑器,面向命令行用户,学习曲线较陡峭,但一旦掌握效率极高。
Appendix D1.4: 构建工具 (Build Tools)
构建工具用于自动化编译、链接等过程,特别是在项目包含多个源文件或依赖复杂时非常有用。
⚝ Make / GNU Make: 最传统的构建工具之一,通过Makefile文件描述项目构建规则。广泛应用于Linux和macOS。
⚝ CMake: 跨平台的构建系统生成工具。它不直接构建项目,而是根据CMakeLists.txt文件生成特定平台的构建文件(如Makefile, Visual Studio项目文件等),然后再调用相应的原生构建工具进行实际构建。现代C++项目常用。
⚝ Ninja: 注重构建速度的构建系统。通常与CMake等工具配合使用,由CMake生成Ninja文件,然后Ninja执行构建。
Appendix D1.5: 调试器 (Debuggers)
调试器用于帮助开发者查找和修复程序中的错误。常见的调试器包括:
⚝ GDB (GNU Debugger): 广泛用于Linux和macOS的命令行调试器,支持多种语言。功能强大,但需要熟悉命令行操作。
⚝ LLDB: 苹果公司开发的调试器,也是基于LLVM项目。通常与Clang配合使用,是Xcode和Clion等IDE的默认调试器。
⚝ Visual Studio Debugger: 集成在Visual Studio中的图形化调试器,功能强大,易于使用,尤其适合Windows平台开发。
Appendix D1.6: 代码分析工具 (Code Analysis Tools)
代码分析工具可以帮助发现代码中的潜在问题、风格不一致或可能的错误。
⚝ Clang-Tidy: 基于Clang的静态分析工具,用于强制执行编码风格、查找bug、进行现代C++迁移等。
⚝ Cppcheck: 另一个开源的静态分析工具,可以检测许多类型的错误,包括内存泄漏、越界访问等。
⚝ Valgrind: 主要用于Linux平台的内存调试、内存泄漏检测和性能分析工具。
Appendix D2: 常用C++库 (Common C++ Libraries)
C++拥有丰富的库生态系统,可以极大地简化开发任务。
Appendix D2.1: 标准库 (Standard Library)
C++标准库是C++语言规范的一部分,提供了大量的通用功能,例如输入/输出(iostream)、字符串(string)、容器(vector, list, map等)、算法(sort, find等)、智能指针(smart pointers)、线程(thread)等。这些库在本书的各个章节中都有详细介绍(特别是在第13章和第14章)。
Appendix D2.2: 其他常用库 (Other Common Libraries)
除了标准库,还有许多广泛使用的第三方C++库。
⚝ Boost: 一个高质量、 peer-reviewed、可移植的C++库集合,涵盖了众多领域,如智能指针、线程、文件系统、正则表达式、数学函数等。许多Boost库后来被采纳进C++标准库。学习Boost有助于深入理解现代C++的高级特性和编程范式。
⚝ Qt: 一个流行的跨平台应用程序开发框架,特别是用于开发图形用户界面 (GUI) 应用程序。使用Qt可以方便地创建美观且功能丰富的桌面、移动和嵌入式应用。
⚝ Eigen: 高性能的C++模板库,用于线性代数运算(矩阵、向量等)。在科学计算、机器学习等领域应用广泛。
⚝ OpenCV: 开源的计算机视觉库,提供了丰富的图像处理、目标检测、特征提取等算法。
⚝ asio: 用于网络编程的跨平台库,支持TCP/IP、UDP等协议,提供了同步和异步编程接口。Boost.Asio是其早期版本,现在asio是一个独立的库。
Appendix D3: 在线资源和学习网站 (Online Resources and Learning Websites)
丰富的在线资源是学习和解决C++开发问题的重要途径。
Appendix D3.1: 官方文档和标准 (Official Documentation and Standards)
⚝ cppreference.com: 可能是最全面的C++语言和标准库参考网站。提供了详细的语法、关键字、标准库组件的介绍和示例。遇到语法或标准库用法问题时,这里是首选的查询站点。
⚝ C++ 标准文档 (C++ Standards Papers): 对于希望深入了解C++语言规范细节的高级开发者,可以查找ISO C++标准的公开草案或最终文档(通常需要付费)。
Appendix D3.2: 教程和博客 (Tutorials and Blogs)
⚝ Learn C++ (learncpp.com): 一个非常受欢迎的免费在线C++教程,内容系统且详细,适合初学者入门。
⚝ C++ Primer 系列书籍配套网站/资源: 如果你购买了经典的C++ Primer书籍,可以关注作者或出版商提供的配套资源。
⚝ Bjarne Stroustrup 的个人网站: C++语言创始人Bjarne Stroustrup的网站包含了他对C++的看法、常见问题解答、书籍信息等。
⚝ 各类C++专家和社区的博客: 许多C++领域的专家会分享他们的经验、新技术解析等,例如Scott Meyers (Effective C++作者), Herb Sutter等。通过搜索引擎查找“C++ blogs”可以找到很多优秀的资源。
Appendix D3.3: 在线编程平台和社区 (Online Coding Platforms and Communities)
⚝ Stack Overflow (stackoverflow.com): 程序员问答社区。遇到编程问题时,很可能在这里能找到答案或提问寻求帮助。
⚝ GitHub (github.com): 全球最大的代码托管平台。可以找到大量的开源C++项目,学习他人的代码,参与开源贡献。
⚝ LeetCode (leetcode.com), HackerRank (hackerrank.com), Codeforces (codeforces.com) 等: 在线编程练习平台,提供了大量C++算法和数据结构练习题,有助于提高编程实战能力。
⚝ Reddit r/cpp: 活跃的C++社区论坛,可以获取C++最新资讯,参与技术讨论。
Appendix D4: 优秀C++书籍推荐 (Recommended Excellent C++ Books)
除了本书,还有一些经典的C++书籍值得参考,它们通常提供更深入或特定角度的讲解。
⚝ 《C++ Primer》 (包括第5版及更高版本): C++入门和进阶的经典教材,内容全面且深入,适合系统学习。
⚝ 《Effective C++》系列 (Effective C++, More Effective C++, Effective Modern C++): Scott Meyers撰写的系列书籍,以条款(Item)的形式介绍C++编程中的重要概念、陷阱和最佳实践,是提高C++编程水平的必备书籍。
⚝ 《The C++ Programming Language》: C++语言创始人Bjarne Stroustrup撰写的权威著作,详细描述了C++语言的各个方面。
⚝ 《深入理解C++11/14/17/20新特性》或类似的现代C++书籍: 专注于介绍各个C++标准版本引入的新特性,帮助开发者跟上语言的发展。
选择合适的工具和资源,并结合系统的学习,将有助于你更高效地掌握C++语言。祝你学习愉快!
Appendix E: C++常见错误和调试技巧
作为一名学习C++的开发者,无论是初学者还是有经验的程序员,都会遇到各种各样的错误。理解这些常见的错误类型以及掌握有效的调试技巧,是提高编程效率和编写健壮程序不可或缺的能力。本附录旨在总结C++中常见的错误类型,并提供系统化的调试方法和工具,帮助读者更好地诊断和解决问题。
Appendix E1: C++常见错误类型 (Common Error Types)
C++程序中的错误可以大致分为三类:编译时错误、链接时错误和运行时错误(包括逻辑错误)。理解每种错误的特点有助于快速定位问题所在。
Appendix E1.1: 编译时错误 (Compile-time Errors)
编译时错误是在程序被编译时由编译器(Compiler)检测到的错误。这些错误通常是语法或类型不匹配引起的。编译器会输出错误消息,指示错误发生的文件、行号和错误类型。这是最容易发现和修复的错误类型。
① 语法错误 (Syntax Errors)
▮▮▮▮ⓑ 缺少分号 (;): C++中大多数语句需要以分号结尾。忘记在语句末尾添加分号是最常见的初级错误之一。
▮▮▮▮ⓒ 括号不匹配 ({}, [], ()): 函数体、代码块、表达式中的各种括号必须成对出现且正确嵌套。
▮▮▮▮ⓓ 关键字拼写错误或误用: 例如将 int
写成 it
,或者在不适当的地方使用关键字。
▮▮▮▮ⓔ 遗漏或放置错误的预处理器指令: 例如 #include
、#define
等指令语法错误。
② 类型错误 (Type Errors)
▮▮▮▮ⓑ 类型不匹配的操作: 将 incompatible 的类型用于赋值、运算或函数参数。
▮▮▮▮ⓒ 声明或定义错误: 例如多次声明同一个变量或函数,或者声明与定义不一致。
▮▮▮▮ⓓ 未声明的标识符 (Undeclared Identifiers): 使用了未声明的变量、函数或类型名。
③ 其他编译错误
▮▮▮▮ⓑ 函数签名不匹配: 调用函数时,提供的参数类型或数量与函数声明/定义不符。
▮▮▮▮ⓒ const
正确性问题: 违反 const
对象的修改规则,或传递非 const
对象给需要 const
引用的函数。
▮▮▮▮ⓓ 访问权限问题: 尝试访问类的 private
或 protected
成员。
编译时错误通常会阻止生成可执行文件 (Executable File)。根据编译器提供的错误信息仔细检查代码,对照语法规则通常可以解决这些问题。
Appendix E1.2: 链接时错误 (Link-time Errors)
链接时错误发生在编译过程之后,由链接器 (Linker) 检测到。链接器的任务是将编译生成的各个目标文件 (Object Files) 以及库文件 (Libraries) 组合成最终的可执行文件。链接错误通常与符号(Symbol,如变量名、函数名)的查找和匹配有关。
① 未定义的符号 (Undefined Symbols)
▮▮▮▮ⓑ 缺少函数定义: 声明了函数但在任何地方都没有提供其实现(定义)。这可能是忘记编写函数体,或者函数定义所在的源文件没有被编译或链接。
▮▮▮▮ⓒ 缺少全局变量定义: 声明了 extern
全局变量但未在任何地方定义它。
▮▮▮▮ⓓ 缺少库文件: 程序中使用了某个库(例如数学库 cmath
或线程库 pthread
)中的函数,但在链接时没有指定对应的库文件。
② 多重定义符号 (Multiple Definitions)
▮▮▮▮ⓑ 函数或全局变量被定义了不止一次。这通常发生在同一个函数或变量在多个源文件中都被定义(而不是在一个头文件中声明,在某个源文件中定义)。
▮▮▮▮ⓒ C++的单一定义规则 (One Definition Rule, ODR) 要求非内联函数、非模板类、非模板变量等在整个程序中只能有一个定义。违反ODR会导致链接错误。
解决链接错误通常需要检查项目的编译和链接设置,确保所有必要的源文件被编译,所有必需的库被正确链接,并且没有违反ODR的定义。
Appendix E1.3: 运行时错误 (Runtime Errors)
运行时错误是在程序成功编译和链接后,执行过程中发生的错误。这类错误通常是逻辑问题、资源问题或外部因素引起的。它们可能导致程序崩溃 (Crash)、产生错误的结果,或者进入无限循环 (Infinite Loop)。
① 崩溃类错误
▮▮▮▮ⓑ 野指针 (Wild Pointers) 或空指针 (Null Pointers) 解引用: 访问未初始化、已释放或值为 nullptr
的指针指向的内存。这会导致段错误 (Segmentation Fault) 或访问冲突。
▮▮▮▮ⓒ 数组越界访问 (Array Out-of-Bounds Access): 访问数组时使用了超出其有效索引范围的下标。
▮▮▮▮ⓓ 除以零 (Division by Zero): 算术运算中出现除以零的情况。
▮▮▮▮ⓔ 栈溢出 (Stack Overflow): 函数调用层级过深(例如递归没有正确的终止条件),导致程序使用的调用栈超出其分配的内存空间。
▮▮▮▮ⓕ 堆溢出 (Heap Overflow) 或缓冲区溢出 (Buffer Overflow): 向固定大小的缓冲区写入的数据超过了其容量,覆盖了相邻的内存区域。
② 逻辑错误 (Logical Errors)
逻辑错误是程序按照语法和运行时规则执行,但结果与预期不符的错误。这类错误最难发现和调试,因为它不会导致程序崩溃,只是结果不正确。
▮▮▮▮ⓐ 条件判断错误: if
、while
、for
等语句中的条件表达式写错,导致分支或循环执行不符合逻辑。
▮▮▮▮ⓑ 循环边界错误 (Off-by-one Errors): 循环次数多一次或少一次,例如在遍历数组时,循环范围是 0
到 n
而不是 0
到 n-1
。
▮▮▮▮ⓒ 运算符优先级或结合性理解错误: 表达式的计算顺序与预期不符。
▮▮▮▮ⓓ 值传递与引用传递混淆: 误以为通过值传递可以修改函数外部变量的值。
▮▮▮▮ⓔ 函数调用或参数传递错误: 传递了错误的值给函数,或者函数返回的结果被错误地使用。
③ 资源管理错误
▮▮▮▮ⓑ 内存泄漏 (Memory Leak): 使用 new
或 malloc
分配了内存,但在不再需要时没有使用 delete
或 free
释放,导致内存持续占用,长期运行可能耗尽系统资源。
▮▮▮▮ⓒ 重复释放 (Double Free): 多次释放同一块已分配的内存,可能导致程序崩溃或数据损坏。
▮▮▮▮ⓓ 使用已释放的内存 (Use-After-Free): 在内存释放后仍然访问该内存区域。
运行时错误需要通过调试技术来定位问题发生的位置和原因。逻辑错误尤其需要仔细的代码审查和测试用例来发现。
Appendix E2: 调试技巧和工具 (Debugging Techniques and Tools)
调试 (Debugging) 是识别、定位和修复程序错误的过程。掌握有效的调试技巧和使用合适的工具能极大地提高问题解决效率。
Appendix E2.1: 手动调试技巧 (Manual Debugging Techniques)
这些技巧不需要特定的调试器,可以随时使用。
① 代码审查 (Code Inspection) 🧐
▮▮▮▮⚝ 仔细阅读代码: 从程序入口开始,逐步跟踪代码执行流程,检查变量的值如何在不同步骤变化,以及条件判断是否如预期工作。
▮▮▮▮⚝ 对比预期行为: 将代码的实际逻辑与你想要它实现的功能进行对比,寻找差异。
▮▮▮▮⚝ 特殊输入测试: 考虑边界条件(Boundary Conditions)、无效输入或极端情况,看程序是否能正确处理。
▮▮▮▮⚝ 请人代码审查: 让其他有经验的开发者阅读你的代码,他们可能会发现你忽略的问题。
② 打印调试 (Print Debugging) 🖨️
▮▮▮▮⚝ 在关键位置插入输出语句: 使用 std::cout
(或C中的 printf
) 打印变量的值、程序执行到某个位置的信息、或者条件判断的结果。
1
#include <iostream>
2
3
int main() {
4
int x = 10;
5
int y = 0;
6
std::cout << "Before division: x = " << x << ", y = " << y << std::endl; // 打印变量值
7
if (y != 0) {
8
int result = x / y;
9
std::cout << "Result: " << result << std::endl;
10
} else {
11
std::cout << "Error: Division by zero prevented." << std::endl; // 打印执行路径
12
}
13
return 0;
14
}
▮▮▮▮⚝ 缩小问题范围: 通过在不同位置打印信息,可以逐步确定错误发生在哪个代码段。
③ 简化问题 (Simplifying the Problem) ✂️
▮▮▮▮⚝ 隔离错误: 如果一个复杂的程序出现错误,尝试创建一个最小化的、能够重现该错误的代码片段。这有助于排除其他代码的干扰。
▮▮▮▮⚝ 注释掉可疑代码: 暂时移除部分代码,看错误是否消失。如果是,则问题可能出在被移除的代码段。
④ 橡皮鸭调试 (Rubber Duck Debugging) 🦆
▮▮▮▮⚝ 向一个物体(例如橡皮鸭)或假想的听众解释你的代码和问题。在解释的过程中,你可能会自己发现逻辑上的漏洞或错误。
Appendix E2.2: 使用调试器 (Using a Debugger)
调试器是专门用于程序调试的工具,提供了强大的功能来控制程序执行和检查程序状态。常见的C++调试器有GDB (GNU Debugger)、LLDB (LLVM Debugger),以及集成在各种IDE中的调试器(如Visual Studio Debugger, CLion Debugger等)。
① 设置断点 (Setting Breakpoints) 📍
▮▮▮▮⚝ 断点是程序中你希望暂停执行的位置。当程序运行到断点时,会暂停下来,允许你检查当前状态。
▮▮▮▮⚝ 可以在可疑的代码行设置断点。
② 逐步执行代码 (Stepping Through Code) 🚶
程序暂停在断点后,可以使用以下命令控制执行流程:
▮▮▮▮ⓐ 步过 (Step Over): 执行当前行代码,如果当前行是一个函数调用,则整个函数作为一个整体执行,不会进入函数内部。常用于跳过你确定没有问题的函数。
▮▮▮▮ⓑ 步入 (Step Into): 执行当前行代码。如果当前行是一个函数调用,则进入该函数的第一行执行。用于深入检查函数内部的逻辑。
▮▮▮▮ⓒ 步出 (Step Out): 从当前函数中执行剩余代码,直到函数返回,然后暂停在调用该函数的下一行。
③ 查看变量和内存 (Inspecting Variables and Memory) 👀
▮▮▮▮⚝ 在程序暂停时,可以查看任何可访问的变量的当前值。这对于理解程序状态和发现变量值是否符合预期至关重要。
▮▮▮▮⚝ 调试器通常也允许查看特定内存地址的内容。
④ 查看调用栈 (Call Stack) 📜
▮▮▮▮⚝ 调用栈显示了当前函数是如何被调用的,以及一系列未完成的函数调用。它能帮助你理解程序的执行路径和函数的嵌套关系,尤其在处理递归或复杂的函数调用链时非常有用。
⑤ 条件断点 (Conditional Breakpoints) 🚦
▮▮▮▮⚝ 只在满足特定条件时才触发的断点。例如,在一个循环中,你可能只关心当某个变量的值达到特定数值时暂停执行。这在循环次数很多的情况下非常有用,避免每次迭代都暂停。
学习并熟练使用至少一种调试器是每个程序员的必备技能。
Appendix E2.3: 其他调试工具 (Other Debugging Tools)
除了交互式调试器,还有其他一些工具可以帮助发现特定类型的错误。
① 内存错误检测工具 (Memory Error Detectors) 🕵️
▮▮▮▮⚝ 例如 Valgrind (在Linux/macOS上常用)。这些工具可以检测内存泄漏、无效的读写、重复释放等内存相关的错误。
1
# 使用 Valgrind 检测程序 ./my_program 的内存问题
2
valgrind --leak-check=full ./my_program
② 性能分析工具 (Profilers) ⏱️
▮▮▮▮⚝ 例如 Gprof (GNU Profiler)、perf、Intel VTune Amplifier。这些工具可以帮助你了解程序在哪些函数或代码段花费了最多的时间,从而进行性能优化。有时性能瓶颈也可能是由逻辑错误或低效算法引起的。
③ 静态分析工具 (Static Analysis Tools) 🤖
▮▮▮▮⚝ 例如 Clang-Tidy, Cppcheck, SonarQube。这些工具在不运行程序的情况下检查源代码,发现潜在的错误(如未使用的变量、可能的空指针解引用、风格问题等)和违反编码规范的地方。
④ 单元测试框架 (Unit Testing Frameworks) 🧪
▮▮▮▮⚝ 例如 Google Test (GTest), Catch2, Boost.Test。编写单元测试用例可以验证代码的每个小部分(单元,通常是函数或方法)是否按照预期工作。这有助于在早期发现错误,并确保代码修改没有引入新的问题(回归测试)。
Appendix E3: 调试最佳实践 (Debugging Best Practices)
除了掌握技巧和工具,一些良好的编程习惯和实践也能帮助减少错误的发生并简化调试过程。
① 编写清晰、可读的代码 ✍️
▮▮▮▮⚝ 使用有意义的变量名和函数名。
▮▮▮▮⚝ 保持函数和代码块简短,专注单一功能。
▮▮▮▮⚝ 添加必要的注释 (Comments) 解释复杂逻辑。
▮▮▮▮⚝ 遵循一致的代码风格。
② 尽早并频繁地测试 🔄
▮▮▮▮⚝ 在编写代码时就进行测试,而不是等到所有代码写完。
▮▮▮▮⚝ 每完成一个小的功能单元就进行测试。
③ 理解错误消息 🤔
▮▮▮▮⚝ 不要忽略编译器或调试器输出的错误和警告信息。它们通常提供了关于问题原因和位置的重要线索。
▮▮▮▮⚝ 学习查阅文档或在线搜索错误消息。
④ 使用版本控制 (Version Control) 🕰️
▮▮▮▮⚝ 使用Git等版本控制系统,可以轻松回溯到之前的代码版本,帮助定位问题是什么时候引入的。
⑤ 解释问题时要清晰准确 🗣️
▮▮▮▮⚝ 如果向他人寻求帮助,清晰地描述你遇到的问题、你已经尝试过的解决方法、错误消息是什么以及如何重现问题。提供最小化的可重现代码示例。
⑥ 不要“修补”错误 🩹
▮▮▮▮⚝ 找到错误的根本原因并彻底修复它,而不是仅仅修补症状。治标不治本可能导致错误在其他地方或未来再次出现。
掌握C++编程中的错误和调试是提高编程技能的必经之路。通过不断实践、学习和运用上述技巧和工具,你将能够更有效地面对和解决编程中遇到的各种挑战,写出更稳定、可靠、高效的C++程序。 🚀