C++PPT

Introduction to C++

  • C++: A powerful programming language suitable for both beginners and experienced programmers.
  • Software and Hardware: Software (programs) controls hardware (computers).
  • Object-Oriented Programming (OOP): Key programming methodology that enhances productivity and reduces development costs.
  • C++: 一种功能强大的编程语言,适合初学者和有经验的程序员使用。
  • 软件和硬件: 软件(程序)控制硬件(计算机)。
  • 面向对象编程(OOP): 关键的编程方法,提高生产力并降低开发成本。

Importance of C++

  • Popularity: C++ is one of today's most popular programming languages.
  • Standardization: The latest version is C++11, standardized by ISO and IEC.
  • Widespread Use: C++ is used in general-purpose computers, smartphones, tablets, etc.
  • 受欢迎程度: C++是当今最受欢迎的编程语言之一。
  • 标准化: 最新版本是C++11,由ISO和IEC标准化。
  • 广泛使用: C++用于通用计算机、智能手机、平板电脑等。

Computing Power

  • Performance: Computers can perform billions of calculations per second.
  • Advancement: Supercomputers perform thousands of trillions (quadrillions) of instructions per second.
  • Programs: Sequences of instructions that guide the computer, created by programmers.
  • 性能: 计算机每秒钟可以执行数十亿次计算。
  • 进步: 超级计算机每秒钟可以执行数千万亿(千万亿次)指令。
  • 程序: 程序是由程序员编写的一系列指示计算机执行操作的指令。

Software Development

  • Costs: Rapid developments in technology have significantly reduced computing costs.
  • Moore's Law: Computing power doubles approximately every two years, leading to exponential growth in memory, storage, and processing speeds.
  • Communications: Similar growth has occurred in the communications field, fostering the Information Revolution.
  • 成本: 技术的快速发展显著降低了计算成本。
  • 摩尔定律: 计算能力大约每两年翻一番,导致内存、存储和处理速度的指数级增长。
  • 通信: 通信领域也经历了类似的增长,推动了信息革命。

Computer Organization

  • Logical Units: Computers are divided into various logical units or sections.
  • Data Hierarchy: Data processed by computers form a hierarchy from bits to complex structures.
  • 逻辑单元: 计算机分为多个逻辑单元或部分。
  • 数据层次结构: 计算机处理的数据形成一个从比特到复杂结构的层次结构。

Programming Languages

  • Types of Languages:

    • Machine Language: Directly understood by computers but cumbersome for humans.
    • Assembly Language: Uses English-like abbreviations, converted to machine language by assemblers.
    • High-Level Languages: Easier to use, translated into machine language by compilers. Examples include C++, JavaScript, and PHP.

    语言类型

    • 机器语言:计算机直接理解的语言,但对人类来说不方便。
    • 汇编语言:使用类似英语的缩写,由汇编器转换为机器语言。
    • 高级语言:更易于使用,由编译器转换为机器语言。示例包括C ++、JavaScript和PHP。

Evolution of C++

  • C Language: Developed by Dennis Ritchie, standardized as ANSI/ISO 9899: 1990.
  • C++: Developed by Bjarne Stroustrup, an extension of C with added features for OOP.
  • C++11: Latest standard to keep pace with powerful hardware and user demands.
  • C 语言:由丹尼斯·里奇(Dennis Ritchie)开发,被标准化为 ANSI/ISO 9899:1990。
  • C++:由比雅尼·斯特劳斯特鲁普(Bjarne Stroustrup)开发,是 C 语言的扩展,增加了面向对象编程的特性。
  • C++11:最新的标准,以适应强大的硬件和用户需求。

C++ Standard Library

  • Classes and Functions: Core components of C++ programs.
  • Rich Library: Offers a collection of classes and functions to simplify programming tasks.
  • 类和函数:C++程序的核心组件。
  • 丰富的库:提供了一系列类和函数,以简化编程任务。

Object-Oriented Programming Concepts

  • Objects: Represent real-world entities with attributes and behaviors.
  • Classes: Blueprints for creating objects.
  • Encapsulation: Wrapping data and functions into a single unit (object).
  • Inheritance: Creating new classes from existing ones, inheriting attributes and behaviors.
  • Modularity: Breaking down programs into smaller, manageable parts.
  • 对象:用属性和行为表示现实世界的实体。
  • :创建对象的蓝图。
  • 封装:将数据和函数封装到单个单元(对象)中。
  • 继承:从现有类创建新类,继承属性和行为。
  • 模块化:将程序分解为更小、可管理的部分。

Programming Phases in C++

  • Editing: Writing and saving the program using an editor.
  • Preprocessing: Executing commands before compilation.
  • Compiling: Translating the program into machine language.
  • Linking: Combining object code with libraries to create an executable.
  • Loading: Transferring the executable to memory for execution.
  • Execution: Running the program under CPU control.
  • 编辑:使用编辑器编写和保存程序。
  • 预处理:在编译之前执行命令。
  • 编译:将程序翻译成机器语言。
  • 链接:将目标代码与库结合起来,创建可执行文件。
  • 加载:将可执行文件传输到内存中以进行执行。
  • 执行:在 CPU 控制下运行程序。

Integrated Development Environments (IDEs)

  • Tools: Provide editors and debuggers to aid in software development.
  • Popular IDEs: Microsoft Visual Studio, Dev C++, NetBeans, Eclipse, Apple's Xcode, CodeLite.
  • 工具:提供编辑器和调试器,以帮助软件开发。
  • 流行的集成开发环境(IDE):Microsoft Visual Studio、Dev C++、NetBeans、Eclipse、Apple 的 Xcode、CodeLite。

Challenges

  • Debugging: Programs may not work on the first try, requiring debugging to resolve issues.

调试:程序可能在第一次尝试时无法正常工作,需要进行调试以解决问题。

Key Takeaways

  • Understanding C++: Essential for creating robust and efficient software.
  • OOP Methodology: Facilitates the development of modular, reusable, and maintainable code.
  • Standard Library: Provides a vast array of tools to streamline the development process.
  • Development Phases: Following a structured approach ensures efficient and error-free programming.
  • 理解 C++:对于创建稳健高效的软件至关重要。
  • 面向对象编程方法论:有助于开发模块化、可重用和易维护的代码。
  • 标准库:提供了大量工具,以简化开发过程。
  • 开发阶段:遵循结构化的方法确保高效且无错误的编程。

这一部分都是理论知识,看看就行。


C++ Programming Basics: Key Concepts from the Presentation

  • Discipline in Development: C++ programming encourages a disciplined approach to developing programs.
  • Data Processing and Display: Most C++ programs process data and display results.
  • 开发中的纪律性:C++ 编程鼓励采用纪律性的方法来开发程序。
  • 数据处理与显示:大多数 C++ 程序处理数据并显示结果。

Comments

  • Single-Line Comments: Begin with // and extend to the end of the line.
  • Multi-Line Comments: Enclosed between /* and */.
  • 单行注释:以 // 开始,并延伸至行尾。
  • 多行注释:被包裹在 /**/ 之间。

Preprocessing Directives

  • #include <iostream>: Instructs the preprocessor to include the input/output stream header file for data input and output operations.

#include <iostream>:指示预处理器包含输入/输出流头文件,用于数据输入和输出操作。

Whitespace

  • Whitespace Characters: Include blank lines, space characters, and tabs, which are ignored by the compiler but help in making the code more readable.

空白字符:包括空行、空格和制表符,编译器会忽略这些字符,但有助于使代码更易读。

The main Function

  • Function Definition: Every C++ program must have a main function where execution begins.
  • Integer Return Type: The main function returns an integer value, typically 0, to indicate successful program termination.
  • 函数定义:每个 C++ 程序必须有一个 main 函数,程序从这里开始执行。
  • 整数返回类型main 函数返回一个整数值,通常是 0,表示程序成功终止。

Basic Syntax

  • Braces {}: Used to delimit the body of functions.
  • String Literals: Characters enclosed in double quotes are considered string literals.
  • Semicolon ;: Used to terminate most C++ statements.

基本语法

  • 大括号 {}: 用于限定函数的主体部分。
  • 字符串字面值: 用双引号括起来的字符被视为字符串字面值。
  • 分号 ;: 用于终止大多数C++语句。

Input and Output

  • Streams: C++ uses streams for input (cin) and output (cout).
  • Namespaces: std::cout and std::cin belong to the std namespace, which can be brought into scope with using namespace std;.
  • Stream Operators: The << operator inserts data into the output stream, and the >> operator extracts data from the input stream.
  • :C++ 使用流进行输入(cin)和输出(cout)。
  • 命名空间std::coutstd::cin 属于 std 命名空间,可以通过 using namespace std; 将其引入作用域。
  • 流运算符<< 运算符将数据插入输出流,>> 运算符从输入流中提取数据。

Variables and Data Types

  • Declaration: Variables must be declared before use with a name and data type.
  • Types: Common data types include int (integer), double (floating-point), and char (character).
  • Identifiers: Names for variables, which must start with a letter or underscore and are case-sensitive.
  • 声明:变量在使用前必须用名称和数据类型声明。
  • 类型:常见的数据类型包括 int(整数)、double(浮点数)和 char(字符)。
  • 标识符:变量的名称,必须以字母或下划线开头,且区分大小写。

Arithmetic Operations

  • Operators: Include +, -, *, /, and % for addition, subtraction, multiplication, division, and modulus operations, respectively.
  • Operator Precedence: Operators are applied in a specific order, similar to algebraic expressions.
  • 运算符:包括 +-*/%,分别用于加法、减法、乘法、除法和取模运算。
  • 运算符优先级:运算符按特定顺序应用,类似于代数表达式。

Control Structures

  • if Statements: Allow conditional execution of code based on boolean expressions.
  • Relational and Equality Operators: Used to form conditions in if statements, such as ==, !=, <, >, <=, and >=.
  • if 语句:根据布尔表达式的条件执行代码。
  • 关系运算符和相等运算符:用于形成 if 语句中的条件,如 ==!=<><=>=

Example Programs

  • Basic I/O Program: Reads two integers from the keyboard, computes their sum, and outputs the result.
  • Escape Sequences: \n for newline and \t for tab are examples of escape sequences used in output.
  • 基本输入/输出程序:从键盘读取两个整数,计算它们的和,并输出结果。
  • 转义序列\n 表示换行,\t 表示制表符,是用于输出的转义序列的示例。

Additional Concepts

  • Using Declarations: To avoid repeatedly using std::, you can include using namespace std; to simplify the code.
  • Compound Statements: Multiple statements can be grouped using braces {} to form a compound statement or block.
  • Syntax Errors: Occur when the code does not conform to the grammatical rules of C++.
  • 使用声明:为了避免反复使用 std::,可以包含 using namespace std; 来简化代码。
  • 复合语句:多个语句可以用大括号 {} 分组形成复合语句或块。
  • 语法错误:当代码不符合 C++ 的语法规则时发生。

Introduction to Classes Objects

Class Definitions and Member Functions:

  • Classes are defined using the class keyword, with names typically following PascalCase convention for readability.

  • Class bodies are enclosed in curly braces {} and terminate with a semicolon.

  • Member functions manipulate data and perform tasks related to objects of the class. They are declared within the class and defined outside of it.

  • Access specifiers (public, private, protected) determine the accessibility of class members. public members are accessible outside the class, while private members are only accessible within the class.

    • 使用 class 关键字定义类,类名通常遵循 PascalCase 约定以提高可读性。
    • 类主体被大括号 {} 包围,并以分号结束。
    • 成员函数操作数据并执行与类对象相关的任务。它们在类内部声明,在外部定义。
    • 访问修饰符(publicprivateprotected)确定类成员的可访问性。public 成员可以在类外部访问,而 private 成员只能在类内部访问。

Object Creation and Usage:

  • Objects are instances of classes and are created using the class name followed by parentheses to call the constructor.
  • Constructors are special member functions with the same name as the class. They initialize object attributes and are called automatically when an object is created.
  • Member functions can be called on objects using the dot operator (.). They may take parameters, which are provided as arguments in the function call.
  • 对象是类的实例,通过使用类名后跟括号来创建,以调用构造函数。
  • 构造函数是与类同名的特殊成员函数。它们初始化对象的属性,并在对象创建时自动调用。
  • 成员函数可以使用点运算符(.)在对象上调用。它们可以接受参数,在函数调用中作为参数提供。

Data Hiding and Encapsulation:

  • Data members are variables declared within a class, representing the object's attributes. They are often declared as private to enforce encapsulation, hiding implementation details from external code.
  • Member functions provide an interface for interacting with data members, enforcing controlled access to the object's state.
  • 数据成员是在类内部声明的变量,代表对象的属性。它们通常声明为 private,以强制封装,隐藏实现细节不让外部代码访问。
  • 成员函数提供了与数据成员交互的接口,强制控制对对象状态的访问。

Code Reusability and Modularization:

  • By encapsulating related data and behavior within classes, code can be modularized and reused across different parts of a program or in other programs.
  • Header files (.h) contain class declarations and are included in source files using #include directives. They facilitate code reuse and modularity by providing a clear interface to external code.
  • 通过将相关数据和行为封装在类内部,代码可以被模块化,并在程序的不同部分或其他程序中重用。
  • 头文件(.h)包含类的声明,并通过 #include 指令包含在源文件中。它们通过为外部代码提供清晰的接口,促进了代码重用和模块化。

Separation of Header and Source Files:

  • In C++ programming, it's common practice to separate the declaration of a class from its implementation.
  • Declarations are placed in header files (.h), while definitions are placed in source code files (.cpp).
  • Header files contain the class declaration and any dependencies on other header files, but do not include the actual function implementations.
  • Source code files contain the concrete implementation of the class, including the definitions of member functions and other implementation details.
  • 在C++编程中,通常会将类的声明和定义分开放置。声明放在头文件(.h)中,而定义放在源代码文件(.cpp)中。
  • 头文件包含类的声明和可能依赖的其他头文件,但不包含具体的函数实现。
  • 源代码文件包含类的具体实现,包括成员函数的定义和其他实现细节。

Including Header Files:

  • Header files are included using the #include preprocessor directive to make class declarations available in a program.
  • Header files should be enclosed in double quotes to allow the preprocessor to search for them in the current directory.
  • 通过使用#include预处理指令来包含头文件,以便在程序中使用类的声明。
  • 头文件应该使用双引号括起来,以便预处理器在当前目录中查找头文件。

Public Interface of a Class:

  • The public interface of a class includes its public member functions, defining the services that clients of the class can use and how to request those services without revealing the implementation details.
  • By declaring member function prototypes (function declarations) in the header file, the public interface of the class is exposed to client code.
  • 类的公共接口包括类的公共成员函数,定义了类的客户端可以使用的服务及如何请求这些服务,但不包括具体实现。
  • 通过在头文件中定义类的成员函数原型(函数声明),可以向客户端公开类的公共接口。

Class Implementation:

  • The concrete implementation of a class should be hidden in the source code file to prevent direct access to the internal details by client code.
  • In the source code file, member functions' definitions are linked to the class declaration using the scope resolution operator ::.
  • 类的具体实现应该隐藏在源代码文件中,以防止客户端代码直接访问类的内部细节。
  • 在源代码文件中,使用范围解析运算符::来连接成员函数的定义与类的声明。

Compilation and Linking:

  • Before executing a program, all source code files need to be compiled into object files, which are then linked together to generate an executable file.
  • The compiler checks for correct function calls based on the declarations in header files to ensure that the code can be linked and executed properly.
  • 在编译和链接程序之前,需要将所有的源代码文件编译成目标文件,然后将这些目标文件链接在一起生成可执行文件。
  • 编译器根据头文件中的声明来检查函数调用是否正确,确保代码能够正确链接并运行。

Modifying Member Functions of a Class:

  • If changes are made to a class's member functions, client code doesn't need to be modified as long as the class's interface remains unchanged.
  • After modifying a class's member functions, the program needs to be recompiled and linked to ensure that the new implementation is correctly invoked.
  • 如果需要修改类的成员函数,只要类的接口保持不变,就无需修改客户端代码。
  • 修改类的成员函数后,需要重新编译并链接程序,以确保新的实现被正确地调用

Control Statements

  1. Problem Understanding and Program Planning:
    • Before writing a program, understanding the problem thoroughly is essential.(在编写程序之前,深入了解问题至关重要。)
    • A well-planned approach is crucial for problem-solving.(对问题的解决方案进行精心规划至关重要。)
    • Employing proven program construction techniques enhances program development.(采用经过验证的程序构建技术可以增强程序开发。)
  2. Algorithm Definition:
    • An algorithm is a step-by-step procedure for solving a problem, specifying actions to execute and their order.(算法是解决问题的逐步过程,指定要执行的操作及其顺序。)
    • It defines program control by specifying the sequence of actions to execute.(它通过指定要执行的操作顺序来定义程序控制。)
  3. Pseudocode:
    • Pseudocode is an informal language resembling everyday English, aiding algorithm development.(伪代码是一种类似于日常英语的非正式语言,有助于算法的开发。)
    • It's user-friendly, helps in visualizing a program's logic, and can be easily translated into code.(它用户友好,有助于可视化程序的逻辑,并且可以轻松转换为代码。)
    • Typically, pseudocode describes only executable statements, excluding declarations without initializers or constructor calls.(通常,伪代码仅描述可执行语句,不包括没有初始化程序或构造函数调用的声明。)
  4. Control Structures:
    • Programs execute a series of actions, typically in sequence, but control structures allow deviations.(程序执行一系列操作,通常按顺序执行,但控制结构允许偏差。)
    • Three fundamental control structures include sequence, selection, and repetition.(三种基本的控制结构包括顺序,选择和重复。)
  5. Sequence Structure:
    • In sequence structure, statements execute one after another in the order they are written.(在序列结构中,语句按照编写顺序依次执行。)
    • Various actions can be placed in sequence, facilitating program flow.(各种动作可以按顺序排列,以促进程序流程。)
  6. Selection Structures:
    • Selection structures enable choosing alternative actions based on conditions.(选择结构使得可以根据条件选择替代动作。)
    • C++ provides if, if...else, and switch selection statements.(C++提供了ifif...elseswitch选择语句。)
    • if performs an action if a condition is true, if...else selects between two actions, and switch selects among many actions based on an integer expression.(if在条件为真时执行动作,if...else在两个动作之间进行选择,switch根据整数表达式选择多个动作。)
  7. Repetition Structures:
    • Repetition structures allow executing statements repeatedly based on a condition.(重复结构允许根据条件重复执行语句。)
    • C++ provides while, do...while, and for loops.(C++提供whiledo...whilefor循环。)
    • while and for perform actions zero or more times, while do...while executes at least once.(whilefor零次或多次执行动作,而do...while至少执行一次。)
  8. Keywords:
    • C++ keywords like if, else, switch, while, do, and for are reserved and used to implement control structures.(C++关键字如ifelseswitchwhiledofor是保留的,用于实现控制结构。)
    • Keywords cannot be used as identifiers (e.g., variable names).(关键字不能用作标识符(例如,变量名)。)
  9. Control Statement Modeling:
    • Control statements can be modeled as activity diagrams, illustrating their entry and exit points.(控制语句可以建模为活动图,说明它们的输入和输出点。)
    • Statements can be connected via control-statement stacking or nesting.(语句可以通过控制语句堆叠或嵌套连接。)
  10. Nested Control Statements:
    • Nested control statements enable testing multiple cases by embedding one inside another.(嵌套的控制语句通过将一个语句嵌套到另一个语句中来测试多个情况。)
    • Proper indentation and brace placement are crucial to avoid ambiguity, such as the dangling-else problem.(适当的缩进和括号放置至关重要,以避免歧义,例如悬挂else问题。)
  11. Compound Statements and Null Statements:
    • Compound statements (blocks) allow grouping multiple statements.(复合语句(块)允许将多个语句分组。)
    • Null statements, represented by a semicolon, are useful when no action is required.(空语句,由分号表示,在不需要执行动作时很有用。)

好的,我会按照您的要求重新生成笔记,确保英文在外面,中文在括号里,并且提供更详细的解释。

  1. Repetition Statement (循环语句):
  • A repetition statement allows you to specify that a program should repeat an action while some condition remains true.(循环语句允许指定程序在某个条件保持为真时重复执行某个动作。)
  • While there are more items on my shopping list, purchase the next item and cross it off my list.(当我的购物清单上还有更多物品时,执行“购买下一个物品并在清单上划掉它”。)
  • “There are more items on my shopping list” is true or false. If true, “Purchase next item and cross it off my list” is performed. Performed repeatedly while the condition remains true.(“还有更多物品在我的购物清单上”是真还是假。如果是真,则执行“购买下一个物品并在清单上划掉它”。此过程重复执行,直到条件为假为止。)
  • The statement contained in the repetition statement constitutes the body of the loop, which can be a single statement or a block.(循环语句中包含的语句构成了循环体,它可以是单个语句或一个代码块。)
1
2
3
4
5
// Find the first power of 3 larger than 100
int product = 3;
while (product <= 100)
product = 3 * product;
// At the end of the loop, 'product' contains the result
  1. Class Average Problem (班级平均分问题):
  • A class of ten students took a quiz. The grades (0 to 100) for this quiz are available to you. Calculate and display the total of the grades and the class average.(一个由十名学生组成的班级参加了一次测验。这次测验的分数(0到100)可供您使用。计算并显示所有分数的总和和班级平均分。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Counters and variables initialization
int total = 0;
int gradeCounter = 0;
int grade;

// Input grades and calculate total
while (gradeCounter < 10) {
// Input grade
cout << "Enter grade: ";
cin >> grade;
// Add grade to total
total += grade;
// Increment grade counter
gradeCounter++;
}

// Calculate and print total and average
if (gradeCounter != 0) {
double average = static_cast<double>(total) / gradeCounter;
cout << "Total of all grades: " << total << endl;
cout << "Class average: " << average << endl;
} else {
cout << "No grades were entered." << endl;
}
  1. Additional Notes (其他说明):
  • Use sentinel-controlled repetition to process grades for an arbitrary number of students.(使用标记控制的循环处理任意数量的学生的成绩。)
  • Use list initialization (C++11) to initialize variables, avoiding data loss.(使用列表初始化(C++11)初始化变量,避免数据损失。)
  • Use assignment operators to abbreviate assignment expressions, e.g., use += operator.(使用赋值运算符简化赋值表达式,例如,使用+=运算符。)
  1. Counter-Controlled Repetition (计数控制循环):
  • Counter-controlled repetition uses a variable called a counter to control the number of times a group of statements will execute.(计数控制循环使用一个叫做计数器的变量来控制一组语句执行的次数。)
  • It's often called definite repetition because the number of repetitions is known before the loop begins executing.(通常被称为明确的重复,因为在循环开始执行之前,重复的次数是已知的。)
1
2
3
4
5
6
// Counter-controlled repetition example
int count = 0;
while (count < 10) {
cout << "Count: " << count << endl;
count++; // Increment counter
}
  1. Sentinel-Controlled Repetition (标记控制循环):
  • Sentinel-controlled repetition uses a special value called a sentinel value to indicate "end of data entry".(标记控制循环使用一个特殊值称为标记值来指示“数据输入结束”。)
  • The number of repetitions is not known in advance.(重复的次数事先是不知道的。)
  • The sentinel value must not be an acceptable input value.(标记值不能是可接受的输入值。)
1
2
3
4
5
6
// Sentinel-controlled repetition example
int number;
while (number != -1) {
cout << "Enter a number (-1 to quit): ";
cin >> number;
}
  1. Preventing Arithmetic Overflow (防止算术溢出):
  • Arithmetic overflow can occur when adding integers, resulting in a value too large to store in an int variable.(在加法中,可能会发生算术溢出,导致一个值太大而无法存储在int变量中。)
  • To prevent overflow, ensure that arithmetic calculations do not exceed the maximum value that can be stored in the data type.(为了防止溢出,确保算术计算不超过数据类型可以存储的最大值。)
1
2
3
4
// Example of preventing arithmetic overflow
int total = 0;
int grade = 90;
total += grade; // Add grade to total, ensuring no overflow
  1. <climits> 头文件:
  • <climits> is a header file in the C++ Standard Library that provides specific information about data types, such as the maximum and minimum values of integer types.(<climits> 是 C++ 标准库中的一个头文件,用于提供有关数据类型的特定信息,如整数类型的最大值和最小值等。)
  1. 常用的 <climits> 常量:
  • INT_MAX: int 类型的最大值。(INT_MAX: Maximum value for the int type.)
  • INT_MIN: int 类型的最小值。(INT_MIN: Minimum value for the int type.)
  • UINT_MAX: unsigned int 类型的最大值。(UINT_MAX: Maximum value for the unsigned int type.)
  • 其他类型(如 short、long、long long 等)也有相应的常量。(Other types such as short, long, long long, etc., have corresponding constants as well.)
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <climits>

using namespace std;

int main() {
cout << "Maximum value for int: " << INT_MAX << endl;
cout << "Minimum value for int: " << INT_MIN << endl;
cout << "Maximum value for unsigned int: " << UINT_MAX << endl;
return 0;
}
  1. for循环:Regarding counter-controlled repetition, the for loop statement is commonly used. This type of loop requires four elements:(关于计数器控制的循环重复,通常使用 for 循环语句。这种循环需要四个要素:)

    • The name of the control variable (or loop counter) 控制变量的名称(或循环计数器)

    • The initial value of the control variable 控制变量的初始值

    • The loop-continuation condition (i.e., whether looping should continue) 循环继续条件(即循环是否应该继续执行的条件)

    • The increment (or decrement) by which the control variable is modified each time through the loop 控制变量每次循环中的增量(或减量)接下来是 for 循环语句头部的组成部分:

1
for ( 初始化; 循环继续条件; 增量 )    语句

这里的初始化表达式初始化循环的控制变量,循环继续条件确定循环是否应该继续执行,增量则是控制变量的增加量。

for 循环语句的一般形式如下:

1
初始化;while ( 循环继续条件 ){   语句   增量;}

接下来是关于 for 循环语句头部中的表达式的一些注意事项:

  • for 循环语句头部中的三个表达式是可选的,但是两个分号分隔符是必需的。
  • 如果省略了循环继续条件,C++ 将假定条件为真,从而创建一个无限循环。
  • 如果控制变量的初始化表达式被省略,那么如果在程序的早期已经对控制变量进行了初始化,就可以省略它。
  • 如果增量表达式被省略,那么增量可以通过 for 循环体内的语句计算,或者如果不需要增量,就可以省略它。

接下来是关于增量表达式的一些注意事项:

  • for 循环语句中,增量表达式 acts like a standalone statement at the end of the for statement’s body。

  • 在没有其他代码的情况下,counter = counter + 1, counter += 1, ++counter, counter++ 这些表达式在增量表达式中都是等价的。

  • for 循环语句的初始化、循环继续条件和增量表达式都可以包含算术表达式。

  • for 循环语句的“增量”可以是负数,这样它实际上是一个递减,并且循环实际上是向下计数的。

  • 如果循环继续条件最初为假,则不执行 for 循环语句的主体。

21.do while

  • do...while 循环是一种后测试循环,它先执行循环体中的代码,然后再检查循环条件是否为真。因此,即使循环条件一开始就为假,循环体也至少会被执行一次。

  • do...while 循环的语法格式如下:

1
2
3
do {
// 循环体代码
} while (循环条件);
  • do...while 循环中,循环条件通常是一个布尔表达式,它决定是否继续执行循环。如果循环条件为真,则继续执行循环;如果为假,则退出循环。

  • do...while 循环的主要特点是它至少会执行一次循环体中的代码,即使循环条件一开始就为假。

  • 使用 do...while 循环时要特别注意循环条件的更新,以避免出现无限循环的情况。通常在循环体内部更新循环条件。

  • do...while 循环中,如果循环体内部使用了 break 语句,它会导致立即退出循环,并且循环条件在下一次迭代开始之前不会被重新检查。

  • 可以在 do...while 循环中使用 continue 语句来跳过循环的剩余部分并立即执行下一次迭代。

  • do...while 循环通常用于需要至少执行一次循环体代码的情况,或者在循环体执行之前无法确定循环条件的情况下使用。

  1. break 语句:
  • break 语句用于立即终止当前所在的循环(例如 forwhiledo...while 循环)或 switch 语句,并将控制转移到循环或 switch 语句后面的下一个语句。
  • 使用 break 语句可提前退出循环,即使循环条件仍然为真。
  • 在嵌套循环中,break 语句只会中断最内层的循环,并跳出该循环的代码块。
  • break 语句通常用于跳出循环的情况,例如当达到特定条件时,不再需要执行循环的剩余代码。
  • break语句记住一点就是终止当前循环,是这一层都跳掉了。
  1. continue 语句:
  • continue 语句用于跳过循环体中剩余的代码,并立即开始下一次循环迭代。

  • 当遇到 continue 语句时,循环会立即转到下一次迭代的开始处,而不执行循环体内 continue 语句之后的任何代码。

  • continue 语句通常用于跳过特定迭代或条件下的代码执行,而不中断整个循环的执行。

  • forwhiledo...while 循环中,continuebreak 语句通常与条件语句一起使用,以便在特定条件满足时提前退出循环,或者跳过特定条件下的代码执行。

  • switch 语句中,break 语句用于退出 switch 语句块,防止执行下一个 case 标签后面的语句。

  • continue就是跳出当前的循环一次,直接进入当前循环的下一次迭代。

Logic Operations

Stream manipulator boolalpha (a sticky manipulator)

有一个要点就是boolalpha

boolalpha 是一个流操纵器,用于指定布尔表达式的输出格式。它使得布尔值 truefalse 被输出为字符串 "true""false",而不是默认的 10。它的作用是将布尔值转换为对应的文字形式,使输出更易读。

  1. 作用

    • boolalpha 用于更改流的状态,以便布尔值按照文字形式输出。
  2. 用法

    • 要激活 boolalpha,只需在流输出之前使用 boolalpha 操纵器。
    • 可以通过插入 std::boolalphastd::coutstd::ostream 对象中来激活 boolalpha
    • 可以通过插入 std::noboolalpha 来取消 boolalpha 的激活状态,使布尔值按照默认的数字形式输出。
  3. 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream>
    using namespace std;

    int main() {
    bool value = true;
    cout << boolalpha << value << endl; // 输出 "true"
    cout << noboolalpha << value << endl; // 输出 "1"
    return 0;
    }
  4. 注意事项

    • boolalpha 是一个“粘性”操纵器,一旦激活,它会影响流中之后所有的布尔值输出,直到被取消激活。
    • 在取消激活 boolalpha 后,后续的布尔值输出会恢复为默认的数字形式。

Functions and an Introduction to Recursion

C++ programs are typically written by combining new functions and classes you write with “prepackaged” functions and classes available in the C++ Standard Library. The C++ Standard Library provides a rich collection of functions. Functions allow you to modularize a program by separating its tasks into self-contained units. Functions you write are referred to as user-defined functions. The statements in function bodies are written only once, are reused from perhaps several locations in a program and are hidden from other functions.(C++ 程序通常是通过将您编写的新函数和类与 C++ 标准库中提供的“预打包”函数和类组合而成的。C++ 标准库提供了丰富的函数集合。函数允许您通过将其任务分解为自包含单元来模块化程序。您编写的函数被称为用户定义函数。函数体中的语句只需编写一次,可以从程序中的多个位置重用,并且对其他函数隐藏起来。)

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

// User-defined function
void myFunction() {
std::cout << "This is a user-defined function" << std::endl;
}

int main() {
myFunction(); // Function call
return 0;
}

A function is invoked by a function call, and when the called function completes its task, it either returns a result or simply returns control to the caller. An analogy to this program structure is the hierarchical form of management (Figure 6.1). A boss (similar to the calling function) asks a worker (similar to the called function) to perform a task and report back (i.e., return) the results after completing the task. The boss function does not know how the worker function performs its designated tasks. The worker may also call other worker functions, unknown to the boss. This hiding of implementation details promotes good software engineering.

(函数通过函数调用进行调用,当被调用的函数完成其任务时,它要么返回一个结果,要么只是将控制返回给调用者。这种程序结构的类比是管理的分层形式(见图 6.1)。老板(类似于调用函数)要求工人(类似于被调用函数)执行一个任务,并在完成任务后报告(即返回)结果。老板函数不知道工人函数如何执行其指定的任务。工人还可以调用其他工人函数,老板不知道这些函数。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

// User-defined function
void workerFunction() {
std::cout << "Worker function performing a task" << std::endl;
}

// Another user-defined function
void bossFunction() {
std::cout << "Boss function assigning task" << std::endl;
workerFunction(); // Call to another user-defined function
}

int main() {
bossFunction(); // Call to user-defined function
return 0;
}

Sometimes functions are not members of a class. Called global functions. Function prototypes for global functions are placed in header files, so that the global functions can be reused in any program that includes the header file and that can link to the function’s object code.

(有时函数不是类的成员。它们被称为全局函数。全局函数的函数原型放在头文件中,以便全局函数可以在包含头文件的任何程序中重用,并且可以链接到函数的目标代码。)

1
2
3
4
5
6
7
// Header file with function prototype
#ifndef MY_FUNCTIONS_H
#define MY_FUNCTIONS_H

void myGlobalFunction(); // Function prototype for a global function

#endif
1
2
3
4
5
6
7
8
// Implementation file with function definition
#include "my_functions.h"
#include <iostream>

// Definition of the global function
void myGlobalFunction() {
std::cout << "This is a global function" << std::endl;
}

The <cmath> header file provides a collection of functions that enable you to perform common mathematical calculations. All functions in the <cmath> header file are global functions—therefore, each is called simply by specifying the name of the function followed by parentheses containing the function’s arguments.

(<cmath> 头文件提供了一组函数,使您能够执行常见的数学计算。<cmath> 头文件中的所有函数都是全局函数,因此,可以通过简单指定函数名称后跟包含函数参数的括号来调用每个函数。)

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <cmath> // Include the <cmath> header file

int main() {
double x = 4.0;
double result = sqrt(x); // Call to a global function from <cmath>
std::cout << "Square root of " << x << " is " << result << std::endl;
return 0;
}

Functions often require more than one piece of information to perform their tasks. Such functions have multiple parameters.

(函数通常需要多个信息来执行其任务。这样的函数具有多个参数。)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

// Function with multiple parameters
int sum(int a, int b) {
return a + b;
}

int main() {
int x = 5, y = 3;
int total = sum(x, y); // Call to a function with multiple parameters
std::cout << "Sum of " << x << " and " << y << " is " << total << std::endl;
return 0;
}

The compiler refers to the function prototype to check that calls to sum contain the correct number and types of arguments and that the types of the arguments are in the correct order. In addition, the compiler uses the prototype to ensure that the value returned by the function can be used correctly in the expression that called the function (e.g., a function call that returns void cannot be used as the right side of an assignment statement). Each argument must be consistent with the type of the corresponding parameter. If the arguments passed to a function do not match the types specified in the function’s prototype, the compiler attempts to convert the arguments to those types.

(编译器参考函数原型来检查对 sum 的调用是否包含正确数量和类型的参数,以及参数的类型是否按正确的顺序排列。此外,编译器使用函数原型来确保函数返回的值可以在调用函数的表达式中正确使用(例如,返回 void 的函数调用不能作为赋值语句的右侧)。每个参数必须与相应参数的类型一致。如果传递给函数的参数与函数原型中指定的类型不匹配,则编译器尝试将参数转换为这些类型。)

函数原型的一个重要特性是参数强制转换,强制将参数转换为参数声明中指定的适当类型。这些转换按照 C++ 的提升规则进行。提升规则指示如何在不丢失数据的情况下在类型之间进行转换。提升规则适用于包含两种或多种数据类型值的表达式,也称为混合类型表达式。混合类型表达式中每个值的类型将提升到表达式中的“最高”类型。

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
int a = 5;
double b = 3.5;
double result = a + b; // Implicit type promotion
std::cout << "Result: " << result << std::endl;
return 0;
}

当函数参数的类型与函数定义或原型中指定的参数类型不匹配时,也会发生提升。图 6.6 列出了算术数据类型,从“最高类型”到“最低类型”的顺序。将值转换为较低的基本类型可能导致不正确的值。因此,只有通过将值显式赋值给较低类型的变量或使用转换运算符,才能将值转换为较低的基本类型。

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
int a = 5;
double b = 3.5;
int result = a + b; // Implicit type promotion (may lead to loss of data)
std::cout << "Result: " << result << std::endl;
return 0;
}

C++ 标准库被划分为许多部分,每个部分都有自己的头文件。头文件包含相关函数的函数原型,这些函数形成库的每个部分。头文件还包含各种类类型和函数的定义,以及这些函数所需的常量。头文件“指示”编译器如何与库和用户编写的组件进行交互。

1
2
3
4
#include <iostream> // Standard input/output stream objects
#include <cmath> // Mathematical functions
#include <cstdlib> // Standard general utilities library
#include <ctime> // Date and time functions

可以通过使用 C++ 标准库函数 rand 在计算机应用程序中引入机会元素。函数 rand 生成一个介于 0 和 RAND_MAX(在 <cstdlib>头文件中定义的符号常量)之间的无符号整数。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <cstdlib> // For rand function
#include <ctime> // For time function

int main() {
srand(time(0)); // Seed the random number generator
int randomNumber = rand(); // Generate a random number
std::cout << "Random number: " << randomNumber << std::endl;
return 0;
}

为了在特定范围内产生随机数,可以使用以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <cstdlib> // For rand function
#include <ctime> // For time function

int main() {
srand(time(0)); // Seed the random number generator
int shiftingValue = 1;
int scalingFactor = 6;
int randomNumber = shiftingValue + rand() % scalingFactor; // Generate a random number in the range 1 to 6
std::cout << "Random number: " << randomNumber << std::endl;
return 0;
}

C++ 中的枚举

基本枚举

枚举(enumerations)是由用户定义的一组命名整型常量。它们通过为一组值赋予有意义的名称来提高代码的可读性和可维护性。

基本枚举示例

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

enum Months {
JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
};

int main() {
Months month = FEB;
std::cout << "Month: " << month << std::endl; // 输出: Month: 2
return 0;
}

在上面的例子中,Months 是一个枚举,包含表示一年中月份的常量。第一个值 JAN 被显式设置为 1,其余常量自动递增,因此 FEB 为 2,MAR 为 3,依此类推。

赋予自定义值

你可以为任意枚举常量赋予自定义整数值:

1
2
3
4
5
enum ErrorCodes {
SUCCESS = 0,
ERROR_NOT_FOUND = 404,
ERROR_ACCESS_DENIED = 403
};

作用域枚举(C++11)

未限定作用域的枚举存在一个缺点,即多个枚举可能包含相同的标识符,导致命名冲突。C++11 引入了作用域枚举来解决这个问题。作用域枚举使用 enum class(或 enum struct)语法声明。

作用域枚举示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

enum class Status {
CONTINUE,
WON,
LOST
};

int main() {
Status gameStatus = Status::CONTINUE;

if (gameStatus == Status::CONTINUE) {
std::cout << "Game is ongoing." << std::endl; // 输出: Game is ongoing.
}
return 0;
}

在这个例子中,Status::CONTINUE 明确地将 CONTINUE 标识为 Status 枚举类中的常量,从而避免了与其他枚举的命名冲突。

指定枚举常量的类型

默认情况下,未限定作用域枚举的基础类型取决于其常量值,保证类型足够大以存储这些常量值。对于作用域枚举,默认的基础类型是 int。然而,C++11 允许明确指定枚举的基础类型。

带指定类型的作用域枚举示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

enum class Status : unsigned int {
CONTINUE,
WON,
LOST
};

int main() {
Status gameStatus = Status::CONTINUE;
std::cout << "Game status: " << static_cast<unsigned int>(gameStatus) << std::endl; // 输出: Game status: 0
return 0;
}

在这个例子中,Status 枚举类的基础类型被指定为 unsigned int,这意味着 Status 中的每个常量都将是 unsigned int 类型。

总结

C++ 中的枚举提供了一种定义一组命名整型常量的方法。未限定作用域的枚举可能会导致命名冲突,而 C++11 引入的作用域枚举解决了这个问题。作用域枚举要求使用其类型名和作用域解析运算符进行限定。此外,C++11 允许指定枚举常量的基础类型,从而更好地控制常量的大小和表示方式。

带有混合类型表达式的枚举示例

1
2
3
4
5
6
7
8
9
10
#include <iostream>

enum class Color : char { RED = 'R', GREEN = 'G', BLUE = 'B' };

int main() {
Color myColor = Color::RED;
char colorChar = static_cast<char>(myColor); // 类型转换
std::cout << "Color: " << colorChar << std::endl; // 输出: Color: R
return 0;
}

毫无疑问,random的随机性是不够强的,因此需要其他的随机数生成。

根据CERT的指导原则MSC30-CPP,函数rand不具有“良好的统计属性”,且可能是可预测的,这使得使用rand的程序安全性较差。为了解决这一问题,C++11提供了一个新的、更安全的随机数库,可以生成非确定性的随机数,适用于模拟和需要不可预测性的安全场景。新的随机数功能位于C++标准库的<random>头文件中。

随机数生成的引擎和分布

C++11提供了许多类来表示各种随机数生成引擎和分布,以根据程序中随机数的使用方式提供灵活性。

  • 引擎(Engine):实现随机数生成算法,生成伪随机数。
  • 分布(Distribution):控制引擎生成的值的范围,这些值的类型(如intdouble等)以及这些值的统计属性。

例如,uniform_int_distribution可以在指定范围内均匀地分布伪随机整数。

示例代码

以下是一个使用C++11随机数库生成均匀分布的整数随机数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <random> // 包含随机数库
#include <ctime> // 包含时间函数

int main() {
// 创建一个随机数引擎
std::default_random_engine engine(static_cast<unsigned int>(std::time(0)));

// 定义一个均匀分布的随机数生成器,范围为1到6
std::uniform_int_distribution<int> distribution(1, 6);

// 生成并打印10个随机数
for (int i = 0; i < 10; ++i) {
int random_number = distribution(engine);
std::cout << "Random number: " << random_number << std::endl;
}

return 0;
}

代码解释

  1. 包含头文件

    1
    2
    3
    #include <iostream>
    #include <random>
    #include <ctime>

    包含必要的头文件以使用随机数库和时间函数。

  2. 创建随机数引擎

    1
    std::default_random_engine engine(static_cast<unsigned int>(std::time(0)));

    创建一个随机数引擎,使用当前时间作为种子,以确保每次运行程序时生成不同的随机数序列。

  3. 定义均匀分布的随机数生成器

    1
    std::uniform_int_distribution<int> distribution(1, 6);

    定义一个生成1到6之间均匀分布整数的分布对象。

  4. 生成并打印随机数

    1
    2
    3
    4
    for (int i = 0; i < 10; ++i) {
    int random_number = distribution(engine);
    std::cout << "Random number: " << random_number << std::endl;
    }

    使用分布对象生成10个随机数,并打印出来。

C++中的存储持续时间、作用域和链接

在C++中,每个标识符还有其他属性,包括存储持续时间、作用域和链接。C++提供了几个存储类说明符来确定变量的存储持续时间:registerexternmutablestatic

存储持续时间(Storage Duration)

标识符的存储持续时间决定了该标识符在内存中存在的时间段。有些标识符存在时间较短,有些则会在程序的整个执行期间一直存在。C++中可以将存储类说明符分为四种存储持续时间:自动、静态、动态和线程。

局部变量和自动存储持续时间(Local Variables and Automatic Storage Duration)

自动存储持续时间的变量包括:

  • 在函数中声明的局部变量
  • 函数参数
  • 使用register声明的局部变量或函数参数

这些变量在程序执行进入其定义所在的代码块时被创建,在该代码块活动期间存在,并在程序退出该代码块时被销毁。自动变量仅存在于定义它们的函数体内的最近的花括号内,或者在函数参数的情况下存在于整个函数体内。局部变量默认具有自动存储持续时间。我们通常称具有自动存储持续时间的变量为自动变量。

示例代码
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

void automaticStorage() {
int a = 10; // 自动变量
std::cout << "Automatic variable a = " << a << std::endl;
}

int main() {
automaticStorage();
return 0;
}

寄存器变量(Register Variables)

在程序的机器语言版本中,数据通常会加载到寄存器中进行计算和其他处理。编译器可能会忽略寄存器声明。register关键字只能用于局部变量和函数参数。

示例代码
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

void registerStorage() {
register int a = 20; // 寄存器变量
std::cout << "Register variable a = " << a << std::endl;
}

int main() {
registerStorage();
return 0;
}

静态存储持续时间(Static Storage Duration)

关键字externstatic用于声明具有静态存储持续时间的变量和函数。此类变量从程序开始执行时存在,直到程序终止。这样的变量在其声明被遇到时初始化一次。

具有静态存储持续时间的标识符(Identifiers with Static Storage Duration)

具有静态存储持续时间的标识符有两种类型:

  • 外部标识符(如全局变量和全局函数名)
  • 使用存储类说明符static声明的局部变量

静态局部变量(static Local Variables)

使用static声明的局部变量仍然只能在其声明所在的函数中访问,但与自动变量不同,静态局部变量在函数返回给调用者时保留其值。下次调用该函数时,静态局部变量包含上次执行函数完成时的值。

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

void staticStorage() {
static int count = 0; // 静态局部变量
count++;
std::cout << "Static variable count = " << count << std::endl;
}

int main() {
for (int i = 0; i < 5; ++i) {
staticStorage();
}
return 0;
}

作用域(Scope)

标识符的作用域是指在程序中可以引用该标识符的区域。有些标识符可以在整个程序中引用,而另一些标识符只能在程序的某些部分引用。

作用域分为以下几种:

  • 块作用域(Block Scope)
  • 函数作用域(Function Scope)
  • 全局命名空间作用域(Global Namespace Scope)
  • 函数原型作用域(Function-Prototype Scope)

块作用域(Block Scope)

在块内部声明的标识符具有块作用域。块作用域从标识符声明开始,到块的结束右花括号}结束。局部变量和函数参数都具有块作用域。任何块都可以包含变量声明。当块嵌套时,如果外部块的标识符与内部块的标识符同名,外部块的标识符会被“隐藏”,直到内部块结束。内部块“看到”的是它自己的局部标识符值,而不是外部块的同名标识符值。

即使声明为静态的局部变量也具有块作用域,尽管它们从程序开始执行时就存在。存储持续时间不影响标识符的作用域。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

void blockScopeExample() {
int x = 10; // 外部块的局部变量
std::cout << "Outer block x = " << x << std::endl;

{
int x = 20; // 内部块的局部变量,隐藏外部块的x
std::cout << "Inner block x = " << x << std::endl;
}

std::cout << "Outer block x = " << x << std::endl; // 再次引用外部块的x
}

int main() {
blockScopeExample();
return 0;
}

函数作用域(Function Scope)

标签(标识符后跟一个冒号,例如start:switch语句中的case标签)是唯一具有函数作用域的标识符。标签可以在它们出现的函数的任何地方使用,但不能在函数体外部引用。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

void functionScopeExample() {
int x = 0;

start:
x++;
std::cout << "Label 'start' x = " << x << std::endl;

if (x < 3) {
goto start;
}
}

int main() {
functionScopeExample();
return 0;
}

全局命名空间作用域(Global Namespace Scope)

在任何函数或类外部声明的标识符具有全局命名空间作用域。全局变量、函数定义和放在函数外部的函数原型都具有全局命名空间作用域。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int globalVar = 100; // 全局变量

void globalNamespaceScopeExample() {
std::cout << "Global variable globalVar = " << globalVar << std::endl;
}

int main() {
globalNamespaceScopeExample();
return 0;
}

函数原型作用域(Function-Prototype Scope)

唯一具有函数原型作用域的标识符是函数原型参数列表中使用的标识符。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

// 函数原型
void functionPrototypeScopeExample(int x);

int main() {
functionPrototypeScopeExample(10);
return 0;
}

void functionPrototypeScopeExample(int x) { // 参数x具有函数原型作用域
std::cout << "Function parameter x = " << x << std::endl;
}

作用域演示

下面的程序演示了全局变量、自动局部变量和静态局部变量的作用域问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

// 全局变量
int globalVar = 1;

void scopeDemonstration() {
// 静态局部变量
static int staticVar = 2;

// 自动局部变量
int localVar = 3;

std::cout << "Inside function scopeDemonstration:\n";
std::cout << "Global variable: " << globalVar << std::endl;
std::cout << "Static local variable: " << staticVar << std::endl;
std::cout << "Automatic local variable: " << localVar << std::endl;

globalVar++;
staticVar++;
localVar++;
}

int main() {
std::cout << "First call to scopeDemonstration:\n";
scopeDemonstration();

std::cout << "\nSecond call to scopeDemonstration:\n";
scopeDemonstration();

return 0;
}

代码解释

  • 全局变量globalVar在整个程序执行期间都存在,并在两个函数调用中保持其值。
  • 静态局部变量staticVar在第一次函数调用时初始化,并在函数返回后保留其值。第二次调用该函数时,它将保持第一次调用后的值。
  • 自动局部变量localVar在每次函数调用时都会重新创建,并在函数返回后销毁。因此,它在每次调用时都重新初始化为3。

链接(Linkage)

标识符的链接决定了它是否仅在声明它的源文件中可见,还是在编译并链接在一起的多个文件中可见。

标识符的链接有三种类型:

  1. 无链接(No Linkage):标识符只能在声明它的作用域中使用。
  2. 内部链接(Internal Linkage):标识符在声明它的源文件中可见,但不能在其他源文件中访问。
  3. 外部链接(External Linkage):标识符可以在多个源文件中共享和使用。

链接类型的具体解释

无链接(No Linkage)

局部变量和函数参数具有无链接。它们只能在声明它们的块中使用。

1
2
3
4
void function() {
int localVar = 10; // localVar 具有无链接
// localVar 只能在这个函数中使用
}

内部链接(Internal Linkage)

通过在变量或函数前加上 static 关键字,可以将它们的链接类型设为内部链接。这意味着它们只能在声明它们的源文件中使用,不能在其他源文件中访问。

1
2
3
4
5
6
7
8
9
10
11
// file1.cpp
static int internalVar = 10; // internalVar 具有内部链接
static void internalFunction() {
// 这个函数只能在 file1.cpp 中使用
}

// file2.cpp
extern int internalVar; // 错误,internalVar 只能在 file1.cpp 中使用
void function() {
internalFunction(); // 错误,internalFunction 只能在 file1.cpp 中使用
}

外部链接(External Linkage)

全局变量和函数默认具有外部链接。这意味着它们可以在多个源文件中共享和使用。通过使用 extern 关键字,我们可以在一个源文件中声明在另一个源文件中定义的变量或函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// file1.cpp
int externalVar = 10; // externalVar 具有外部链接
void externalFunction() {
// 这个函数可以在其他源文件中使用
}

// file2.cpp
extern int externalVar; // 声明 externalVar,它在 file1.cpp 中定义
extern void externalFunction(); // 声明 externalFunction,它在 file1.cpp 中定义

void anotherFunction() {
externalVar = 20; // 可以访问并修改 externalVar
externalFunction(); // 可以调用 externalFunction
}

链接类型的总结

  • 无链接:标识符只能在声明它的块中使用(局部变量和函数参数)。
  • 内部链接:标识符只能在声明它的源文件中使用(通过 static 关键字)。
  • 外部链接:标识符可以在多个源文件中共享和使用(全局变量和函数默认具有外部链接,可以用 extern 声明)。

理解C++中的函数调用机制:函数调用栈

为了理解C++如何执行函数调用,我们需要首先了解一种数据结构,即栈(stack)。栈类似于一堆盘子,当一个盘子放在堆上时,它通常放在顶部——这称为“推入”(pushing)。同样,当一个盘子从堆上取出时,它通常从顶部取出——这称为“弹出”(popping)。栈是一种后进先出(LIFO, Last-In, First-Out)数据结构——最后推入的项是第一个弹出的项。

函数调用栈(Function-Call Stack)

函数调用栈(或程序执行栈)是计算机科学中一个非常重要的机制,支持函数调用/返回机制,并且支持每个被调用函数的自动变量的创建、维护和销毁。

栈帧(Stack Frames)

每个函数最终必须将控制返回给调用它的函数。每当一个函数调用另一个函数时,一个条目被推入函数调用栈。这个条目被称为栈帧或活动记录,包含被调用函数需要返回到调用函数的返回地址。

自动变量和栈帧(Automatic Variables and Stack Frames)

当一个函数调用返回时,该函数调用的栈帧被弹出,控制转移到弹出的栈帧中的返回地址。

栈溢出(Stack Overflow)

计算机中的内存是有限的,所以只有一定量的内存可以用来在函数调用栈上存储活动记录。如果发生的函数调用超过了函数调用栈可以存储的活动记录的数量,就会发生一个称为栈溢出的致命错误。

示例代码和解释

以下是一个简单的示例程序,展示了函数调用栈的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

// 函数原型
int square(int);

int main() {
int a = 10;
int result = square(a);
std::cout << "The square of " << a << " is " << result << std::endl;
return 0;
}

int square(int x) {
return x * x;
}

代码解释

  1. 操作系统调用main函数

    • 一个活动记录被推入栈,包含main的返回地址(R1)和自动变量a的内存。

    • 此时,栈看起来像这样:

      1
      | R1 | a=10 |
  2. main调用square函数

    • 一个square的栈帧被推入栈,包含square的返回地址(R2)和自动变量x的内存。

    • 栈现在看起来像这样:

      1
      2
      | R2 | x=10 |
      | R1 | a=10 |
  3. square函数计算结果并返回

    • square的栈帧被弹出栈,返回地址R2被使用以返回到main

    • 栈恢复为:

      1
      | R1 | a=10 |
  4. main函数完成执行

    • main的栈帧被弹出,控制返回给操作系统。

C++ 函数与参数

空参数列表

在 C++ 中,可以通过以下两种方式指定空参数列表:

  1. 在括号中使用 void
  2. 直接留空括号。

这两种方式都表示该函数不接收任何参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

// 使用 void 作为参数列表
void functionWithVoid(void) {
std::cout << "使用 void 参数列表的函数\n";
}

// 空参数列表
void functionWithoutParams() {
std::cout << "空参数列表的函数\n";
}

int main() {
functionWithVoid();
functionWithoutParams();
return 0;
}

内联函数

内联函数通过建议编译器在每个调用点生成函数代码的副本来减少函数调用的开销。这可能会使程序变大,因为函数代码被重复了。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

// 内联函数定义
inline int cube(const int side) {
return side * side * side;
}

int main() {
int sideLength = 3;
std::cout << "立方体的体积: " << cube(sideLength) << std::endl;
return 0;
}
  • const 关键字:在函数 cube 中,const 确保变量 side 在计算过程中不会被修改。

向函数传递参数

  1. 值传递
    • 将参数的值复制一份传递给被调用函数。
    • 对副本的修改不会影响调用者中的原始变量。
  2. 引用传递
    • 调用者允许被调用函数直接访问和修改调用者的数据。
    • 使用 & 符号在函数原型和定义中指示引用传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

// 值传递
void modifyByValue(int number) {
number = 10; // 不会影响原始变量
}

// 引用传递
void modifyByReference(int &number) {
number = 10; // 会影响原始变量
}

int main() {
int value = 5;
modifyByValue(value);
std::cout << "经过 modifyByValue 后: " << value << std::endl; // 输出: 5

modifyByReference(value);
std::cout << "经过 modifyByReference 后: " << value << std::endl; // 输出: 10

return 0;
}

函数内作为别名的引用

引用可以作为函数内其他变量的别名。引用在声明时必须初始化,且不能重新赋值为其他变量的别名。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
int original = 10;
int &alias = original; // alias 是 original 的引用

alias = 20; // 修改 original
std::cout << "Original: " << original << std::endl; // 输出: 20

return 0;
}

从函数返回引用

从函数返回引用可能是危险的,因为它可能会导致悬空引用(dangling reference),如果引用指向的是局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

// 危险示例:返回局部变量的引用
int& dangerousFunction() {
int localVar = 10;
return localVar; // 局部变量在函数返回后超出作用域
}

// 安全示例:返回静态变量的引用
int& safeFunction() {
static int localVar = 10;
return localVar; // 静态变量在程序生命周期内一直存在
}

int main() {
int &ref = safeFunction();
std::cout << "安全引用: " << ref << std::endl; // 输出: 10

return 0;
}

总结

  • 空参数列表:可以通过 void 或空括号来指定。
  • 内联函数:使用 inline 关键字减少函数调用开销。
  • 值传递:传递参数值的副本。
  • 引用传递:直接修改原始参数。
  • 函数内引用:必须初始化且不可重新赋值。
  • 返回引用:避免返回局部变量的引用,以防悬空引用。

默认参数和作用域解析运算符

默认参数

在 C++ 中,可以为函数的某些参数指定默认值。这样,当函数调用省略这些参数时,编译器会自动使用默认值。这对于减少重复代码非常有用,特别是当多次调用函数时传递相同的参数值。

使用默认参数的规则
  • 默认参数必须是参数列表中最右边(最后)的参数。
  • 可以在函数声明或定义时指定默认参数,但不能同时在两处都指定。
示例:计算盒子的体积
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

// 函数声明带有默认参数
double boxVolume(double length = 1.0, double width = 1.0, double height = 1.0);

int main() {
// 使用所有默认参数
std::cout << "体积1: " << boxVolume() << std::endl;

// 使用一个默认参数
std::cout << "体积2: " << boxVolume(2.0) << std::endl;

// 使用两个默认参数
std::cout << "体积3: " << boxVolume(2.0, 3.0) << std::endl;

// 使用所有参数
std::cout << "体积4: " << boxVolume(2.0, 3.0, 4.0) << std::endl;

return 0;
}

// 函数定义
double boxVolume(double length, double width, double height) {
return length * width * height;
}

输出结果:

1
2
3
4
体积1: 1
体积2: 2
体积3: 6
体积4: 24

作用域解析运算符

在 C++ 中,可能会出现局部变量和全局变量同名的情况。此时,可以使用一元作用域解析运算符 :: 来访问全局变量。

这是一个陌生知识点,::可以访问全局变量,在此之前并不知道。

使用 :: 访问全局变量

当局部变量和全局变量同名时,默认情况下局部变量会隐藏全局变量。使用 :: 可以显式访问全局变量。

示例:局部变量和全局变量同名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int x = 1; // 全局变量

void useLocalVariable() {
int x = 2; // 局部变量
std::cout << "局部变量 x = " << x << std::endl;
std::cout << "全局变量 x = " << ::x << std::endl; // 使用 :: 访问全局变量
}

int main() {
std::cout << "全局变量 x = " << x << std::endl; // 默认访问全局变量
useLocalVariable();
return 0;
}

输出结果:

1
2
3
全局变量 x = 1
局部变量 x = 2
全局变量 x = 1

总结

  • 默认参数:在函数调用省略参数时,编译器会使用默认值。默认参数必须是参数列表中的最后几个参数。
  • 作用域解析运算符 :::用于在局部变量和全局变量同名时访问全局变量。

函数重载

在 C++ 中,允许定义多个同名函数,只要它们的参数列表不同,这就是函数重载。函数重载允许创建一组函数,它们执行类似的任务,但操作的数据类型不同。

如何区分重载函数

重载函数通过它们的签名来区分。签名是函数名和参数类型(按顺序)的组合。编译器将每个函数标识符编码为其参数类型的组合,以便进行类型安全的链接。

示例:使用函数重载计算平方

下面的示例展示了如何使用函数重载计算整数和浮点数的平方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

// 重载函数:计算整数的平方
int square(int num) {
std::cout << "整数的平方:";
return num * num;
}

// 重载函数:计算浮点数的平方
double square(double num) {
std::cout << "浮点数的平方:";
return num * num;
}

int main() {
int intNum = 5;
double doubleNum = 3.5;

// 调用重载函数计算整数的平方
std::cout << square(intNum) << std::endl;

// 调用重载函数计算浮点数的平方
std::cout << square(doubleNum) << std::endl;

return 0;
}

输出结果:

1
2
整数的平方:25
浮点数的平方:12.25

总结

  • 函数重载:允许定义多个同名函数,通过参数列表的不同来区分。
  • 重载函数的区分:编译器根据函数的参数类型来区分重载函数。

函数重载使得代码更加灵活,可以根据不同的参数类型执行相似的操作。

函数模板

如果程序逻辑和操作对于每种数据类型都是相同的,可以使用函数模板来更加紧凑和方便地进行重载。

定义函数模板

下面是一个定义最大值函数模板的示例,它确定三个值中的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

// 函数模板定义
template <typename T>
T maximum(T x, T y, T z) {
T max = x; // 假设 x 为最大值
if (y > max) {
max = y;
}
if (z > max) {
max = z;
}
return max;
}

int main() {
int intMax = maximum(10, 20, 30); // 调用函数模板,传入三个整数
std::cout << "最大整数值:" << intMax << std::endl;

double doubleMax = maximum(3.5, 2.5, 4.5); // 调用函数模板,传入三个双精度浮点数
std::cout << "最大浮点数值:" << doubleMax << std::endl;

char charMax = maximum('a', 'A', 'b'); // 调用函数模板,传入三个字符
std::cout << "最大字符值:" << charMax << std::endl;

return 0;
}

输出结果:

1
2
3
最大整数值:30
最大浮点数值:4.5
最大字符值:b

返回类型后置(这个也是一个未知知识点)

C++11 引入了返回类型后置语法,用于指定函数的返回类型。示例如下:

1
2
3
4
5
// 返回类型后置的函数模板定义
template <typename T>
auto maximum(T x, T y, T z) -> T {
// 函数体
}

主要是用于外部不知道参数究竟应该怎么进行计算,因此也不知道参数究竟应该怎么进行推导,所以需要直接拿到返回值,直接进行推导。不一定总是a+b这种可以直接相同的类型,可能会需要进行自动类型推导。

总结

  • 函数模板:可以使用函数模板来定义一次性适用于多种数据类型的函数。
  • 返回类型后置:C++11 提供了返回类型后置的语法,使函数模板的返回类型更加灵活。

函数模板使得代码更加通用和可重用,可以方便地处理不同类型的数据。

递归是一种函数调用自身的编程技巧,通常用于解决可以被分解为相同问题的子问题的情况。在递归过程中,问题被分解为更小的子问题,直到达到某个基本条件,然后逐级返回解决方案。下面是关于递归的详细说明以及一个计算阶乘的示例代码:

递归的特点:

  1. 调用自身:递归函数会在其定义中调用自身,以处理较小的子问题。
  2. 基本条件:递归函数必须有一个基本条件,用于终止递归调用,防止无限循环。
  3. 递归步骤:递归函数通过调用自身来解决更小的子问题,直到达到基本条件为止。

阶乘的递归定义:

阶乘是一个典型的递归问题。正整数 n 的阶乘(表示为 n!)定义为从 1 到 n 的所有正整数的乘积。其递归定义如下:

  • 如果 n 为 0 或 1,则 n 的阶乘为 1。
  • 否则,n 的阶乘为 n 乘以 (n-1) 的阶乘。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

// 递归计算阶乘
unsigned long long factorial(unsigned int n) {
// 基本条件:n 为 0 或 1 时,阶乘为 1
if (n == 0 || n == 1) {
return 1;
}
// 递归步骤:n 的阶乘为 n 乘以 (n-1) 的阶乘
else {
return n * factorial(n - 1);
}
}

int main() {
unsigned int n = 5;
unsigned long long result = factorial(n);
std::cout << "Factorial of " << n << " is: " << result << std::endl;
return 0;
}

在上面的示例代码中,我们定义了一个名为 factorial 的递归函数来计算给定正整数的阶乘。递归函数通过不断调用自身来解决较小的子问题,直到达到基本条件(n 为 0 或 1)为止,然后逐级返回解决方案。

迭代和递归都是基于控制语句的编程技巧。迭代使用重复结构,而递归使用选择结构。

  • 迭代和递归都涉及重复:迭代明确使用重复结构;递归通过重复调用函数实现重复。
  • 迭代和递归都包含终止测试:迭代在循环继续条件失败时终止;递归在识别到基本情况时终止。
  • 迭代和递归都逐渐接近终止:迭代通过修改计数器直到计数器取得使循环继续条件失败的值;递归通过产生原始问题的简化版本,直到达到基本情况。

下面是计算阶乘问题的迭代解决方案示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

// 迭代计算阶乘
unsigned long long factorial(unsigned int n) {
unsigned long long result = 1;
// 从 1 到 n 逐个相乘
for (unsigned int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}

int main() {
unsigned int n = 5;
unsigned long long result = factorial(n);
std::cout << "Factorial of " << n << " is: " << result << std::endl;
return 0;
}

在上面的示例代码中,我们使用迭代方式计算了给定正整数的阶乘。迭代通过循环从 1 到 n 逐个相乘,直到达到 n。与递归不同,迭代直接使用循环结构来完成这一过程,而不需要通过函数调用来实现重复。


Class Templates array and vector; Catching Exceptions

数据结构:std::array

std::array 是一个容器类,它封装了原始数组,使其具有更方便的接口和更多的功能。它的大小是固定的,且在编译时就确定了。std::array 是在 C++11 中引入的,位于头文件 <array> 中。

定义和声明

要定义一个 std::array,你需要指定元素的类型和数组的大小:

1
2
3
#include <array>

std::array<int, 5> arr; // 声明一个包含 5 个整数的 std::array

初始化

可以使用列表初始化来初始化 std::array

1
2
3
4
5
6
7
8
9
10
11
12
#include <array>
#include <iostream>

int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};

for (const auto& element : arr) {
std::cout << element << " ";
}

return 0;
}

常用操作

  • 访问元素: 使用下标运算符 [] 或成员函数 at() 访问元素。at() 会进行边界检查。
  • 获取大小: 使用 size() 成员函数获取数组大小。
  • 填充: 使用 fill() 成员函数填充数组。

示例代码

下面是几个使用 std::array 的示例,展示了其常见操作。

  1. 声明并初始化数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <array>
#include <iostream>

int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};

for (const auto& element : arr) {
std::cout << element << " ";
}

std::cout << std::endl;

return 0;
}
  1. 使用 at() 访问元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <array>
#include <iostream>

int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};

try {
std::cout << "Element at index 2: " << arr.at(2) << std::endl;
// 试图访问越界元素,会抛出 std::out_of_range 异常
std::cout << "Element at index 10: " << arr.at(10) << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Out of range error: " << e.what() << std::endl;
}

return 0;
}
  1. 填充数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <array>
#include <iostream>

int main() {
std::array<int, 5> arr;
arr.fill(10);

for (const auto& element : arr) {
std::cout << element << " ";
}

std::cout << std::endl;

return 0;
}
  1. 求和数组元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <array>
#include <iostream>

int main() {
std::array<int, 4> arr = {1, 2, 3, 4};
int sum = 0;

for (const auto& element : arr) {
sum += element;
}

std::cout << "Sum of array elements: " << sum << std::endl;

return 0;
}

静态局部数组的初始化

  1. 静态局部数组的声明和初始化
    • 静态局部数组是在函数内声明的,但是它们在整个程序执行期间只初始化一次,并且在整个程序的生命周期内保持其值。
    • 如果没有显式初始化,编译器会将静态局部数组的每个元素初始化为零。
  2. 首次遇到声明时初始化
    • 当程序首次遇到静态局部数组的声明时,会进行初始化。之后每次进入该函数时,静态数组保持上一次的状态,而不会重新初始化。
  3. 零初始化
    • 如果没有提供显式初始化器,静态局部数组的所有元素会被自动初始化为零。

示例代码

下面是一些示例代码,展示了静态局部数组的声明、初始化和使用。

示例 1: 静态局部数组的零初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void printStaticArray() {
static int arr[5]; // 声明一个静态局部数组,但没有显式初始化

std::cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

int main() {
printStaticArray();
return 0;
}

在这个例子中,arr 是一个静态局部数组。因为它没有显式初始化,所以编译器会将它的每个元素初始化为零。程序输出:

1
Array elements: 0 0 0 0 0

示例 2: 静态局部数组的显式初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void initializeAndPrintStaticArray() {
static int arr[5] = {1, 2, 3}; // 显式初始化前三个元素,其余元素自动初始化为零

std::cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

int main() {
initializeAndPrintStaticArray();
return 0;
}

在这个例子中,arr 的前三个元素显式初始化为 1, 2, 3,其余元素自动初始化为零。程序输出:

1
Array elements: 1 2 3 0 0

示例 3: 静态局部数组的值保持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

void modifyAndPrintStaticArray() {
static int arr[5] = {1, 2, 3};

arr[0] += 1;
arr[1] += 1;
arr[2] += 1;

std::cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

int main() {
modifyAndPrintStaticArray();
modifyAndPrintStaticArray();
return 0;
}

在这个例子中,arr 在第一次调用 modifyAndPrintStaticArray 时被修改,然后在第二次调用时保留了这些修改。程序输出:

1
2
Array elements: 2 3 4 0 0
Array elements: 3 4 5 0 0

范围基于范围的for语句和数组处理(Range-Based for Statement )

知识点

  1. 范围基于for语句

    • C++11引入了范围基于for语句,使得迭代数组元素更简单且避免越界错误。

    • 语法格式:

      1
      2
      for ( rangeVariableDeclaration : expression )
      statement
    • rangeVariableDeclaration 包含类型和标识符,如 int item

    • expression 是要迭代的数组或容器。

    • rangeVariableDeclaration 中的类型必须与数组元素的类型一致。

  2. 迭代和修改数组元素

    • 可以使用范围基于for语句来显示数组内容或修改数组内容。
    • 通过引用类型(如 int& itemRef)可以修改数组元素值。
  3. C++标准库容器支持

    • 范围基于for语句可以用于C++标准库的大多数预构建数据结构(容器),如 arrayvector
  4. 当需要访问元素下标时

    • 范围基于for语句不提供元素下标访问。如果需要下标,仍需使用计数器控制的for语句。

示例代码

示例 1:使用范围基于for语句显示数组内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <array>
using namespace std;

int main() {
array<int, 5> items = {1, 2, 3, 4, 5};

// 使用范围基于for语句显示数组内容
cout << "Array items: ";
for (int item : items) {
cout << item << " ";
}
cout << endl;

return 0;
}

输出:

1
Array items: 1 2 3 4 5
示例 2:使用范围基于for语句修改数组内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <array>
using namespace std;

int main() {
array<int, 5> items = {1, 2, 3, 4, 5};

// 使用范围基于for语句将每个元素乘以2
for (int& itemRef : items) {
itemRef *= 2;
}

// 显示修改后的数组内容
cout << "Modified array items: ";
for (int item : items) {
cout << item << " ";
}
cout << endl;

return 0;
}

输出:

1
Modified array items: 2 4 6 8 10
示例 3:使用下标访问数组元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <array>
using namespace std;

int main() {
array<int, 5> items = {1, 2, 3, 4, 5};

// 使用计数器控制的for语句显示数组元素和它们的下标
cout << "Array items with indices: " << endl;
for (size_t i = 0; i < items.size(); ++i) {
cout << "Index " << i << ": " << items[i] << endl;
}

return 0;
}

输出:

1
2
3
4
5
6
Array items with indices: 
Index 0: 1
Index 1: 2
Index 2: 3
Index 3: 4
Index 4: 5

静态数据成员

知识点

  1. 静态数据成员
    • 静态数据成员使用 static 关键字声明,表示它是类级别的成员,而不是对象级别的成员。
    • 静态数据成员对类的所有对象共享,不会为每个对象单独存储一个副本。
    • 可以在类定义和成员函数定义中像访问其他数据成员一样访问静态数据成员。
  2. 常量静态数据成员
    • 使用 const 关键字将静态数据成员声明为常量,表示它的值在程序执行期间不会改变。
    • 常量静态数据成员通常用于定义类常量,如数组大小等。
  3. 访问静态数据成员
    • 可以通过类名和作用域解析运算符 :: 访问公共静态数据成员,即使没有创建类的对象。
    • 例如,ClassName::staticMemberName

示例代码

示例 1:静态常量数据成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

class Student {
public:
// 声明静态常量数据成员
static const int students = 30;

// 构造函数
Student(const string& name) : name(name) {
++count;
}

// 静态成员函数,用于访问静态数据成员
static int getStudentCount() {
return count;
}

private:
string name; // 学生姓名
static int count; // 静态数据成员,所有对象共享
};

// 定义并初始化静态数据成员
int Student::count = 0;

int main() {
cout << "Total students: " << Student::students << endl;

Student s1("Alice");
Student s2("Bob");

cout << "Number of Student objects created: " << Student::getStudentCount() << endl;

return 0;
}

输出:

1
2
Total students: 30
Number of Student objects created: 2
示例 2:公共静态数据成员的访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class Example {
public:
// 公共静态数据成员
static int value;

// 静态成员函数,用于修改静态数据成员
static void setValue(int v) {
value = v;
}
};

// 定义并初始化静态数据成员
int Example::value = 0;

int main() {
// 通过类名访问静态数据成员
cout << "Initial value: " << Example::value << endl;

// 修改静态数据成员
Example::setValue(10);
cout << "Modified value: " << Example::value << endl;

// 创建对象并访问静态数据成员
Example e;
cout << "Value accessed through object: " << e.value << endl;

return 0;
}

输出:

1
2
3
Initial value: 0
Modified value: 10
Value accessed through object: 10

总结

  • 静态数据成员对于需要在所有对象之间共享的数据非常有用。
  • 常量静态数据成员通常用于定义类常量。
  • 静态数据成员可以通过类名和作用域解析运算符 :: 访问,而无需创建类的对象。

使用C++标准库进行排序和二分查找

知识点

  1. 排序 (Sorting)
    • 排序是将数据按升序或降序排列的过程,是计算中非常重要的应用之一。
    • C++标准库提供了 std::sort 函数,可以方便地对数组或容器中的元素进行排序。
  2. 搜索 (Searching)
    • 搜索是确定数组中是否包含与某个键值匹配的值的过程。
    • 二分查找 (binary search) 是一种高效的搜索算法,要求数据序列是有序的。
    • C++标准库提供了 std::binary_search 函数来进行二分查找。
  3. std::sort
    • 用于对元素进行排序。
    • 语法:std::sort(begin, end),其中 beginend 是迭代器,表示要排序的范围。
  4. std::binary_search
    • 用于在有序范围内进行二分查找。
    • 语法:std::binary_search(begin, end, value),其中 beginend 是迭代器,表示要搜索的范围,value 是要查找的值。
    • 返回 bool 值,表示是否找到了该值。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <algorithm> // 包含sort和binary_search
#include <array> // 包含array容器
#include <string> // 包含string类型

int main() {
// 定义并初始化一个字符串数组
std::array<std::string, 7> colors = {"red", "orange", "yellow", "green", "blue", "indigo", "violet"};

// 显示未排序的数组
std::cout << "Unsorted array:\n";
for (const auto& color : colors) {
std::cout << color << " ";
}
std::cout << std::endl;

// 使用std::sort对数组进行升序排序
std::sort(colors.begin(), colors.end());

// 显示已排序的数组
std::cout << "Sorted array:\n";
for (const auto& color : colors) {
std::cout << color << " ";
}
std::cout << std::endl;

// 使用std::binary_search在数组中查找值
std::string searchKey1 = "yellow";
bool found1 = std::binary_search(colors.begin(), colors.end(), searchKey1);
std::cout << "The value \"" << searchKey1 << "\" is "
<< (found1 ? "found" : "not found") << " in the array." << std::endl;

// 再次查找一个不同的值
std::string searchKey2 = "purple";
bool found2 = std::binary_search(colors.begin(), colors.end(), searchKey2);
std::cout << "The value \"" << searchKey2 << "\" is "
<< (found2 ? "found" : "not found") << " in the array." << std::endl;

return 0;
}

输出

运行上述程序时,将输出以下内容:

1
2
3
4
5
6
Unsorted array:
red orange yellow green blue indigo violet
Sorted array:
blue green indigo orange red violet yellow
The value "yellow" is found in the array.
The value "purple" is not found in the array.

使用二维数组表示表格数据

知识点

  1. 二维数组 (Two-Dimensional Arrays)
    • 二维数组用于表示表格数据,这些数据由按行和列排列的信息组成。
    • 要标识特定的表格元素,必须指定两个下标——通常,第一个下标标识元素的行,第二个下标标识元素的列。
    • 二维数组也被称为2-D数组,多维数组指的是具有两个或更多维度的数组。
  2. 二维数组的初始化
    • 可以在声明时初始化二维数组。
    • 元素的类型由数组的定义指定。例如:array<int, columns> 表示每个元素都是包含三个 int 值的数组,其中 columns 是常量,值为3。
  3. 嵌套的范围基于的for语句 (Nested Range-Based for Statements)
    • 为了处理二维数组的元素,我们使用嵌套循环,其中外层循环遍历行,内层循环遍历给定行的列。
    • C++11引入的 auto 关键字可以让编译器根据变量的初始化值推断变量的数据类型。
  4. 嵌套的计数器控制的for语句 (Nested Counter-Controlled for Statements)
    • 可以使用计数器控制的嵌套循环来实现二维数组的遍历。

示例代码

使用 array 模板初始化二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <array>

int main() {
// 初始化一个3x4的二维数组
const size_t rows = 3;
const size_t columns = 4;
std::array<std::array<int, columns>, rows> a = {{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}};

// 使用嵌套的范围基于的for语句遍历二维数组
std::cout << "Using range-based for loop:" << std::endl;
for (const auto& row : a) {
for (const auto& element : row) {
std::cout << element << ' ';
}
std::cout << std::endl;
}

// 使用嵌套的计数器控制的for语句遍历二维数组
std::cout << "\nUsing counter-controlled for loop:" << std::endl;
for (size_t row = 0; row < a.size(); ++row) {
for (size_t column = 0; column < a[row].size(); ++column) {
std::cout << a[row][column] << ' ';
}
std::cout << std::endl;
}

return 0;
}

解释

  1. 初始化二维数组
    • 使用 std::array 模板初始化一个3x4的二维数组 a,其中包含整数值。
  2. 嵌套的范围基于的for语句
    • 使用范围基于的for语句遍历二维数组,auto 关键字用于推断变量的数据类型。
    • 外层循环遍历数组的每一行,内层循环遍历行中的每个元素。
  3. 嵌套的计数器控制的for语句
    • 使用计数器控制的for语句遍历二维数组。
    • 外层循环控制行索引,内层循环控制列索引,通过数组索引访问每个元素。

进一步的应用

假设在一个学期中,学生会参加多次考试,教授们可能希望分析整个学期的成绩,包括单个学生和整个班级的成绩。

示例代码:存储学生成绩的二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <array>

class GradeBook {
public:
static const size_t students = 10;
static const size_t exams = 3;

GradeBook(std::array<std::array<int, exams>, students>& gradesArray) : grades(gradesArray) {}

void displayGrades() const {
std::cout << "The grades are:\n";
for (size_t student = 0; student < grades.size(); ++student) {
std::cout << "Student " << student + 1 << ": ";
for (size_t exam = 0; exam < grades[student].size(); ++exam) {
std::cout << grades[student][exam] << ' ';
}
std::cout << std::endl;
}
}

private:
std::array<std::array<int, exams>, students> grades;
};

int main() {
std::array<std::array<int, GradeBook::exams>, GradeBook::students> studentGrades = {{
{87, 96, 70},
{68, 87, 90},
{94, 100, 90},
{100, 81, 82},
{83, 65, 85},
{78, 87, 65},
{85, 75, 83},
{91, 94, 100},
{76, 72, 84},
{87, 93, 73}
}};

GradeBook myGradeBook(studentGrades);
myGradeBook.displayGrades();

return 0;
}

解释

  1. 定义和初始化二维数组
    • GradeBook 类中包含一个静态常量数据成员 studentsexams,分别表示学生人数和考试次数。
    • 初始化一个包含10个学生和每个学生3次考试成绩的二维数组 studentGrades
  2. GradeBook 类
    • 构造函数接受一个二维数组并初始化成员变量 grades
    • displayGrades 函数用于显示所有学生的成绩,使用嵌套的计数器控制的for语句遍历二维数组并输出每个学生的成绩。

C++ 标准库模板类 vector

知识点

  1. vector 类模板
    • vector 类模板与 array 类模板类似,但支持动态调整大小。
    • 定义在头文件 <vector> 中,并属于 std 命名空间。
  2. 初始化和默认值
    • 默认情况下,vector 对象的所有元素都设置为0。
    • vector 可以定义来存储大多数数据类型。
  3. 获取元素数量
    • 可以使用 size 成员函数获取 vector 中元素的数量。
    • 也可以使用计数器控制的循环和下标 ([]) 操作符。
  4. 赋值操作
    • 可以使用赋值 (=) 操作符与 vector 对象。
    • 与数组一样,C++ 在使用方括号访问 vector 元素时不需要执行边界检查。
    • vector 类模板提供成员函数 at 来执行边界检查,如果参数是无效的下标,会抛出异常。
  5. 异常处理
    • 异常处理允许创建容错程序,解决执行期间出现的问题。
    • 当函数检测到问题时,例如无效的数组下标或无效的参数,会抛出异常。
    • 使用 try 语句包含可能抛出异常的代码,使用 catch 块处理异常。
    • vectorat 成员函数提供边界检查,并在无效下标时抛出异常,默认情况下这会导致 C++ 程序终止。
  6. 动态调整大小
    • vector 可以动态增长以容纳更多元素。
    • 使用 push_back 成员函数向 vector 末尾添加新元素。
  7. C++11 列表初始化
    • 可以使用列表初始化器为 vector(以及其他 C++ 标准库数据结构)指定初始元素值。

示例代码

使用 vector 的示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <vector>
#include <stdexcept> // For standard exception classes

int main() {
// 定义一个包含10个整数的 vector
std::vector<int> integers(10);

// 使用范围基于的for循环显示 vector 的内容
std::cout << "Initial vector contents:\n";
for (const auto& item : integers) {
std::cout << item << ' ';
}
std::cout << std::endl;

// 修改 vector 的元素
for (size_t i = 0; i < integers.size(); ++i) {
integers[i] = i * 2;
}

// 显示修改后的 vector 内容
std::cout << "Modified vector contents:\n";
for (const auto& item : integers) {
std::cout << item << ' ';
}
std::cout << std::endl;

// 使用 at 成员函数进行边界检查
try {
std::cout << "Accessing element at position 20: ";
std::cout << integers.at(20) << std::endl; // 这将抛出一个异常
} catch (const std::out_of_range& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}

// 动态调整 vector 的大小
std::cout << "Current size of vector: " << integers.size() << std::endl;
integers.push_back(1000);
std::cout << "New size of vector: " << integers.size() << std::endl;
std::cout << "Vector contents after push_back:\n";
for (const auto& item : integers) {
std::cout << item << ' ';
}
std::cout << std::endl;

// 使用 C++11 列表初始化 vector
std::vector<int> anotherVector = {1, 2, 3, 4, 5};
std::cout << "Another vector initialized with list initializer:\n";
for (const auto& item : anotherVector) {
std::cout << item << ' ';
}
std::cout << std::endl;

return 0;
}

解释

  1. 定义和初始化 vector
    • 定义一个包含10个整数的 vector,初始值为0。
    • 使用范围基于的 for 循环遍历 vector 并显示其内容。
  2. 修改 vector 元素
    • 使用下标访问和修改 vector 的元素。
    • 显示修改后的 vector 内容。
  3. 边界检查和异常处理
    • 使用 at 成员函数进行边界检查,并在尝试访问超出范围的元素时抛出异常。
    • 使用 trycatch 块处理异常并输出异常信息。
  4. 动态调整 vector 的大小
    • 显示 vector 的当前大小,使用 push_back 添加新元素,并显示新的大小和内容。
  5. C++11 列表初始化
    • 使用列表初始化器为另一个 vector 赋初始值,并显示其内容。

指针的基础知识

1. 指针简介

指针是C++中功能强大但使用起来具有挑战性的能力之一。指针可以包含变量的内存地址,通过指针可以间接引用该变量的值。正确和负责地使用指针是编程的重要技能之一。

2. 何时使用指针

  • 传递引用:指针使得传递引用(pass-by-reference)成为可能。
  • 动态数据结构:指针用于创建和操作可以动态增长和收缩的数据结构,如链表、队列、堆栈和树。

3. 间接引用(Indirection)

  • 定义:间接引用是通过指针引用变量的值。指针包含变量的内存地址,通过这个地址可以访问变量的值。
  • 图示:通常用箭头表示指针,从包含地址的变量指向内存中该地址所在的变量。

4. 空指针(Null Pointers)

  • C++11之前:空指针的值是0或NULL。NULL在几个标准库头文件中定义为0。
  • 初始化空指针:将指针初始化为NULL等价于将指针初始化为0。在C++11之前,约定使用0表示空指针。0是唯一可以直接赋值给指针变量的整数值,无需首先将整数转换为指针类型。

5. 地址运算符(&)

  • 定义:地址运算符(&)是一个一元运算符,用于获取其操作数的内存地址。

  • 示例声明

    1
    2
    3
    int y = 5; // 声明变量 y
    int *yPtr = nullptr; // 声明指针变量 yPtr
    yPtr = &y; // 将变量 y 的地址赋给指针变量 yPtr

示例代码

指针基础示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main() {
int y = 5; // 声明整型变量 y 并初始化为 5
int *yPtr = nullptr; // 声明指针变量 yPtr 并初始化为 nullptr

yPtr = &y; // 将变量 y 的地址赋给指针变量 yPtr

// 输出 y 的值和地址
std::cout << "Value of y: " << y << std::endl;
std::cout << "Address of y: " << &y << std::endl;

// 使用指针 yPtr 访问 y 的值和地址
std::cout << "Value pointed to by yPtr: " << *yPtr << std::endl;
std::cout << "Address stored in yPtr: " << yPtr << std::endl;

return 0;
}

空指针示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main() {
int *ptr = nullptr; // 将指针变量初始化为 nullptr

// 检查指针是否为 nullptr
if (ptr == nullptr) {
std::cout << "ptr is a null pointer" << std::endl;
} else {
std::cout << "ptr is not a null pointer" << std::endl;
}

return 0;
}

指针与数组的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main() {
int numbers[5] = {1, 2, 3, 4, 5}; // 声明并初始化数组

// 使用指针遍历数组
int *ptr = numbers; // 数组名本身是指向数组首元素的指针
for (int i = 0; i < 5; ++i) {
std::cout << "Value of numbers[" << i << "]: " << *(ptr + i) << std::endl;
}

return 0;
}

解释

  1. 基本指针操作
    • 通过地址运算符 & 获取变量的内存地址。
    • 通过指针访问和修改变量的值。
  2. 空指针
    • 指针初始化为 nullptr
    • 使用条件语句检查指针是否为空。
  3. 指针与数组
    • 数组名本身是指向数组首元素的指针。
    • 使用指针遍历和访问数组元素。

指针和地址运算符示例

间接引用运算符

  • 定义* 运算符,也称为间接引用或解引用运算符,用于返回指针操作数指向的对象的变量。
  • 解引用指针:称为对指针进行解引用,即获取指针指向的对象的值。
  • 左值:解引用指针后的结果可以作为赋值语句的左值使用。

示例程序

下面的示例程序演示了地址运算符 & 和间接引用运算符 * 的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main() {
int y = 10; // 声明并初始化整数变量 y
int *yPtr = nullptr; // 声明指针变量 yPtr 并初始化为 nullptr

yPtr = &y; // 将变量 y 的地址赋给指针变量 yPtr

std::cout << "Address of y: " << &y << std::endl; // 输出变量 y 的地址
std::cout << "Value of yPtr: " << yPtr << std::endl; // 输出指针变量 yPtr 存储的地址
std::cout << "Value pointed to by yPtr: " << *yPtr << std::endl; // 输出指针变量 yPtr 指向的值

*yPtr = 20; // 使用指针 yPtr 修改变量 y 的值

std::cout << "New value of y: " << y << std::endl; // 输出修改后的变量 y 的值

return 0;
}

解释

  1. 基本概念
    • 展示了整数变量 y 和指针变量 yPtr 的内存地址。
    • 介绍了地址运算符 & 和间接引用运算符 * 的概念。
  2. 示例程序
    • 使用地址运算符 & 获取变量的地址,并将其赋给指针变量。
    • 使用间接引用运算符 * 解引用指针,获取其指向的变量的值。
    • 示例程序还展示了如何修改指针指向的变量的值。

指针传递的引用传递

1. 引言

在C++中,有三种方式可以将参数传递给函数:按值传递、按引用传递(使用引用参数)和按引用传递(使用指针参数)。这里我们解释使用指针参数的引用传递。

2. 引用传递示例

  • 引用传递的基本概念:指针和引用一样,可以用于修改调用者的一个或多个变量,或者将指向大型数据对象的指针传递给函数,以避免通过值传递这些对象所带来的开销。
  • 通过指针进行引用传递:当调用需要修改的参数的函数时,传递该参数的地址。函数内部使用间接引用操作符 (*) 来访问该地址处的值,从而实现引用传递。

3. 示例代码

下面是一个使用指针参数进行引用传递的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

// Function prototype
void cubeByReference(int *nPtr);

int main() {
int number = 5;

std::cout << "Original value of number: " << number << std::endl;

// Pass the address of number to the function
cubeByReference(&number);

std::cout << "New value of number after cubeByReference: " << number << std::endl;

return 0;
}

// Function definition to cube a number by reference
void cubeByReference(int *nPtr) {
// Dereference the pointer and cube the value
*nPtr = *nPtr * *nPtr * *nPtr;
}

内置数组

1. 声明内置数组

  • 使用以下形式的声明来指定元素的类型和所需的元素数量:type arrayName[arraySize];
  • 编译器会为数组保留适当数量的内存空间。
  • arraySize 必须是大于零的整数常量。

2. 访问内置数组的元素

  • 与数组对象一样,使用下标([])运算符来访问内置数组的各个元素。

3. 初始化内置数组

  • 可以使用初始化器列表来初始化内置数组的元素。
  • 如果提供的初始化器数量少于数组元素的数量,则剩余元素会进行值初始化。
  • 如果提供的初始化器数量多于数组元素的数量,则会引发编译错误。

4. 内置数组的参数传递

  • 内置数组的名称的值隐式转换为内置数组第一个元素的地址。
  • 在将内置数组传递给函数时,不需要取内置数组的地址(&)—只需传递内置数组的名称。
  • 对于内置数组,被调用的函数可以修改调用者的所有元素,除非函数在相应的内置数组参数前面加上 const,以指示不应修改元素。

5. 声明内置数组参数

  • 可以在函数头中声明内置数组参数,如下所示:

    1
    int sumElements(const int values[], const size_t numberOfElements);

    这表明函数的第一个参数应该是一个一维的 int 类型的内置数组,并且该函数不应该修改这个数组。

6. C++11:标准库函数 beginend

  • C++11 引入了 beginend 函数(在头文件 <iterator> 中),用于返回指向内置数组开头和结尾的指针。
  • 这些函数可用于指定要在标准库函数(如 sort)中处理的元素范围。

7. 内置数组的局限性

  • 内置数组有一些限制:
    • 不能使用关系和相等运算符进行比较,必须使用循环逐个比较数组元素。
    • 不能相互赋值。
    • 无法确定自己的大小,通常需要将数组的名称和大小一起作为参数传递给处理数组的函数。
    • 不提供自动边界检查,必须确保数组访问表达式中的下标在数组的边界内。

使用类模板 arrayvector 的对象比内置数组更安全、更健壮,并提供更多的功能。

最小权限原则

最小权限原则建议给予函数访问其所需数据的最低权限,同时限制对不必要数据的访问。这有助于确保更好的封装性,减少潜在错误,并提高代码的可维护性。

向函数传递指针的四种方式

  1. 非常量指针指向非常量数据:这提供了最高级别的访问权限。指针可以修改其指向的数据和其自身持有的地址。示例:int *ptr

  2. 非常量指针指向常量数据:指针可以被修改以指向不同的数据,但它所指向的数据不能通过指针进行修改。示例:const int *ptr

  3. 常量指针指向非常量数据:指针始终指向相同的内存位置,但该位置的数据可以通过指针进行修改。示例:int *const ptr

  4. 常量指针指向常量数据:这提供了最低级别的访问权限。指针始终指向相同的内存位置,且该位置的数据不能通过指针进行修改。示例:const int *const ptr

示例演示

  • 非常量指针指向常量数据:尝试通过非常量指针访问常量数据,编译器报错示例。

  • 常量指针指向非常量数据:尝试修改常量指针的数据,导致编译器报错。

  • 常量指针指向常量数据:尝试修改常量指针指向的数据和地址,编译器报错示例。

sizeof 运算符

sizeof 运算符在程序编译期间确定内置数组或任何其他数据类型、变量或常量的字节大小。

  1. 对内置数组的大小计算:当应用于内置数组的名称时,sizeof 运算符返回内置数组中的总字节数,返回类型为 size_t

  2. 在函数参数中应用于指针参数:当应用于接收内置数组作为参数的函数中的指针参数时,sizeof 运算符返回指针的大小(以字节为单位),而不是内置数组的大小。

计算内置数组的元素数量

要确定内置数组 numbers 中的元素数量,可以使用以下表达式(在编译时计算):

1
sizeof numbers / sizeof(numbers[0])

该表达式将内置数组 numbers 中的字节数除以第一个元素的字节数,从而得到元素的数量。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main() {
int numbers[10];
std::cout << "Size of numbers array: " << sizeof(numbers) << " bytes\n";
std::cout << "Number of elements in numbers array: "
<< sizeof(numbers) / sizeof(numbers[0]) << std::endl;

std::cout << "Size of int: " << sizeof(int) << " bytes\n";
std::cout << "Size of char: " << sizeof(char) << " byte\n";
std::cout << "Size of double: " << sizeof(double) << " bytes\n";
std::cout << "Size of float: " << sizeof(float) << " bytes\n";
std::cout << "Size of bool: " << sizeof(bool) << " byte\n";

return 0;
}

示例输出

1
2
3
4
5
6
7
Size of numbers array: 40 bytes
Number of elements in numbers array: 10
Size of int: 4 bytes
Size of char: 1 byte
Size of double: 8 bytes
Size of float: 4 bytes
Size of bool: 1 byte

注意事项

  • sizeof 运算符可应用于任何表达式或类型名称。
  • 如果 sizeof 应用于变量名(而不是内置数组的名称)或其他表达式,则返回用于存储表达式特定类型的字节数。
  • sizeof 运算符的括号仅在其操作数是类型名时是必需的。(唯一一个不太懂的点,只有在操作数是类型名时候括号是必须的)

指针算术运算

指针是有效的操作数,可用于算术表达式、赋值表达式和比较表达式。C++允许进行指针算术,即对指针进行几种算术操作:

  1. 增加和减少操作:指针可以通过 ++-- 运算符进行递增和递减。
  2. 指针与整数相加或相减:可以将整数加到指针上(++=),或从指针中减去整数(--=)。
  3. 赋值操作:可以将一个指针赋值给另一个指针。
  4. 比较操作:可以使用等号和关系运算符对指针进行比较。

指针算术只有在指针指向内置数组时才有意义。

示例说明

假设已经声明了 int v[5],并且其第一个元素位于内存位置3000。假设指针 vPtr 已初始化为指向 v[0](即 vPtr 的值为3000)。

  1. vPtr 可以通过以下任一语句初始化为指向 v 的指针:

    1
    2
    int *vPtr = v;
    int *vPtr = &v[0];
  2. 将整数添加到指针或从指针中减去整数

    • 在常规算术中,加法 3000 + 2 的结果是3002。但在指针算术中,当整数添加到或从指针中减去时,指针不是简单地增加或减去该整数,而是增加或减去指针所指对象的大小乘以该整数。
    • 例如,语句 vPtr += 2; 将产生3008(从计算结果3000 + 2 * 4),假设一个 int 在四个字节的内存中存储。在内置数组 v 中,vPtr 现在将指向 v[2]
  3. 指针相减

    • 指向同一内置数组的指针变量可以相互减去。例如,如果 vPtr 包含地址3000,v2Ptr 包含地址3008,则语句 x = v2Ptr - vPtr; 将为 x 赋值为从 vPtrv2Ptr 的内置数组元素数量,此处为2。

指针赋值和void*指针

指针赋值

在C++中,可以将一个指针直接赋值给另一个相同类型的指针,而不需要进行显式的类型转换。这可以确保类型的一致性,并使代码更加清晰易读。

1
2
3
int x = 10;
int *ptr1 = &x;
int *ptr2 = ptr1; // 可以直接将ptr1的值赋给ptr2

强制转换操作符

但是,如果要将一个指针赋值给另一个不同类型的指针,就需要使用强制转换操作符。其中最常见的是 reinterpret_cast。这种情况下,需要确保类型转换是安全的,因为这种转换可能会导致未定义的行为或者错误。

1
2
3
double y = 5.5;
double *ptr3 = &y;
int *ptr4 = reinterpret_cast<int*>(ptr3); // 将double*转换为int*

指向void的指针

指向 void 的指针(void*)是一种特殊的指针类型,它可以指向任何类型的数据。这使得它非常灵活,特别是在与函数接口和动态内存分配等方面的使用。

1
2
int z = 20;
void *ptr5 = &z; // 将int*指针赋给void*指针,无需转换

示例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main() {
// 直接赋值
int x = 10;
int *ptr1 = &x;
int *ptr2 = ptr1;

// 强制转换
double y = 5.5;
double *ptr3 = &y;
int *ptr4 = reinterpret_cast<int*>(ptr3);

// void*指针
int z = 20;
void *ptr5 = &z;

return 0;
}

注意事项

  • 使用指针赋值时,确保类型一致,避免产生未定义的行为。
  • 使用强制转换时,需要慎重考虑类型转换的安全性。
  • void*指针是一种通用的指针类型,可以指向任何数据类型的地址。

一些更加深层次的考虑: 当然可以!在这里,我将深入探讨指针赋值和void*指针的更多细节和用途。

指针赋值的更多考虑

指针赋值的安全性

在进行指针赋值时,需要确保源指针和目标指针的生命周期和作用域是符合预期的。如果源指针在目标指针之后被销毁,目标指针可能会指向无效的内存地址,导致未定义的行为。因此,确保源指针的有效性至关重要。

指针赋值的语义

指针赋值不仅仅是将一个指针的值复制给另一个指针,还涉及到指针所指向的内存区域。如果源指针和目标指针指向相同的内存区域,那么它们将共享同一块内存,并且对其中的数据的修改将会影响到另一个指针。这可能会导致意外的副作用,因此需要谨慎使用指针赋值。

void*指针的更多用途

泛型编程

void*指针是C和C++中一种常用的技术,用于实现泛型编程。通过使用void*指针,可以编写与特定数据类型无关的通用代码,这样的代码可以处理各种类型的数据。

指针比较

  • 可以使用等号和关系运算符来比较指针。
  • 使用关系运算符进行比较时,除非指针指向同一内置数组的元素,否则比较无意义。
  • 指针比较比较指针中存储的地址。
  • 指针比较的常见用法是确定指针是否具有值 nullptr、0 或 NULL(即指针不指向任何东西)。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

int main() {
int v[5];
int *vPtr = v;

// Pointer arithmetic
vPtr += 2;
std::cout << "vPtr now points to v[2], address: " << vPtr << std::endl;

// Pointer subtraction
int x = 0;
int *v2Ptr = vPtr + 2;
x = v2Ptr - vPtr;
std::cout << "Number of elements between vPtr and v2Ptr: " << x << std::endl;

// Pointer assignment
int *ptr1 = v;
int *ptr2 = nullptr;
ptr2 = ptr1;

// Pointer comparison
if (ptr1 == ptr2) {
std::cout << "ptr1 and ptr2 point to the same location." << std::endl;
}

return 0;
}

注意事项

  • 指针算术仅在指向内置数组时才有意义。
  • 要小心指针的解引用,确保不会访问无效内存地址。

指针可以用于执行任何涉及数组下标的操作。假设有以下声明:

1
2
3
4
// 创建5个元素的int数组b;b是一个const指针
int b[5];
// 创建int指针bPtr,它不是一个const指针
int *bPtr;

我们可以用如下语句将bPtr设置为指向内置数组b的第一个元素的地址:

1
2
// 将内置数组b的地址赋给bPtr
bPtr = b;

这相当于将第一个元素的地址赋值给bPtr的另一种方式:

1
2
// 同样将内置数组b的地址赋给bPtr
bPtr = &b[0];

指针/偏移表示法

可以用指针表达式 *(bPtr + 3) 来引用内置数组元素 b[3]。这里的 3 是指针的偏移量。这种表示法被称为指针/偏移表示法。括号是必需的,因为 * 的优先级高于 +。

同样,内置数组元素的地址 &b[3] 可以用指针表达式 bPtr + 3 来表示。

使用内置数组的名称作为指针的偏移表示法

内置数组名称可以被视为指针,并用于指针算术运算。例如,表达式 *(b + 3) 也引用了元素 b[3]。通常来说,所有带有下标的内置数组表达式都可以用指针和偏移量来写。

指针/下标表示法

指针可以像内置数组一样进行下标操作。例如,表达式 bPtr[1] 引用了 b[1];这种表达方式使用了指针/下标表示法。

C字符串。

  • 字符和字符常量:字符是C++源程序的基本构建块。字符常量是以单引号表示的整数值。字符常量的值是机器字符集中字符的整数值。

  • 字符串:字符串是一系列字符,被视为单个单元。可以包括字母、数字和各种特殊字符,如+、-、*、/和$。在C++中,字符串文字或字符串常量用双引号括起来。

  • 基于指针的字符串:基于指针的字符串是以空字符('\0')结尾的字符数组。通过指向其第一个字符的指针来访问字符串。字符串文字的sizeof是字符串的长度,包括终止空字符。

  • 字符常量作为初始化器:当声明一个包含字符串的字符数组时,字符数组必须足够大,以存储字符串及其终止空字符。

  • 访问C字符串中的字符:由于C字符串是一个字符数组,因此可以直接使用数组下标表示法访问字符串中的单个字符。

  • 使用cin读取char类型的内置数组中的字符串:可以使用cin的流提取运算符将字符串读入char类型的内置数组中。setw流操作符可用于确保读入到word中的字符串不超过内置数组的大小。

  • 使用cin.getline读取char类型的内置数组中的文本行:在某些情况下,希望将整行文本输入到char类型的内置数组中。为此,cin对象提供了成员函数getline,它接受三个参数:将存储文本行的char类型的内置数组、长度和分隔符字符。当遇到分隔符字符''、输入结束符或已读取的字符数少于第二个参数中指定的长度减一时,该函数停止读取字符。cin.getline的第三个参数有''作为默认值。

  • 显示C字符串:用表示以空字符终止的字符串的char类型的内置数组可以用cout和<<输出。直到遇到终止空字符为止,字符都会被输出;终止空字符不会显示。cin和cout假定char类型的内置数组应被处理为以空字符终止的字符串;cin和cout不为其他类型的char类型的内置数组提供类似的输入和输出处理能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
// 字符串常量
const char* str = "Hello, world!";
cout << "字符串常量:" << str << endl;

// 字符串数组
char text[] = "C strings";
cout << "字符串数组:" << text << endl;

// 访问单个字符
char ch = text[2];
cout << "第三个字符是:" << ch << endl;

// 使用cin读取字符串
char word[20];
cout << "请输入一个单词:";
cin >> setw(20) >> word;
cout << "你输入的单词是:" << word << endl;

// 使用cin.getline读取整行文本
char line[100];
cout << "请输入一行文本:";
cin.ignore(); // 清除输入缓冲区
cin.getline(line, 100);
cout << "你输入的文本是:" << line << endl;

return 0;
}


Classes: A Deeper Look; Throwing Exceptions

这一章主要就是对类比较深入的剖析了。

防止头文件被重复包含

这是第一个知识点,经常在写头文件的时候加上

1. 包含保护的目的:

  • 包含保护防止头文件在同一编译单元中被多次包含,避免由于重复定义而产生错误。

2. 语法:

  • 包含保护通常由三个指令组成:#ifndef#define#endif
  • #ifndef:检查特定标识符是否未被定义。
  • #define:定义标识符以防止后续包含头文件。
  • #endif:关闭条件包含块。

3. 示例实现:

1
2
3
4
5
6
#ifndef TIME_H
#define TIME_H

// 头文件内容放在这里

#endif // TIME_H

4. 工作原理:

  • 当头文件首次包含在编译单元中时,TIME_H 标识符未定义。
  • 因此,预处理器使用 #define 定义 TIME_H
  • 然后包含头文件内容。
  • 如果相同的头文件再次在同一编译单元中被包含,#ifndef 指令检查是否已经定义了 TIME_H
  • 由于 TIME_H 已经定义,预处理器跳过头文件内容的包含。

5. 好处和重要性:

  • 防止多次定义类、函数或对象,从而避免多次包含头文件可能导致的问题。
  • 仅处理声明一次,提高编译效率。
  • 确保在不同编译单元中具有一致的行为。

6. 最佳实践:

  • 对于包含保护,始终使用唯一标识符,通常派生自头文件名。
  • 确保命名约定一致,以避免冲突和混淆。
  • 将包含保护放置在每个头文件的开头和结尾。

7. 高级考虑:

  • 现代编译器通常支持 #pragma once 指令,它更有效地实现了相同的目的。
  • #pragma once 并未标准化,但被主要编译器广泛支持。

一个比较坑的点:

在C++11之前,只有静态常量整型(static const int)数据成员可以在类主体中声明时进行初始化。这是因为C++03标准不允许对其他类型的数据成员在类主体中进行初始化。这意味着对于其他类型的数据成员(例如非静态数据成员),必须在类的构造函数中进行初始化,否则它们将保持未初始化状态。这对于类的设计和实现可能带来一些不便,特别是在需要在不同构造函数中使用相同的初始化值时。

C++11引入了一种新的特性,即类内初始值设定符(in-class initializer)。这使得我们可以在类的定义中对任何数据成员进行初始化,而不仅限于静态常量整型数据成员。这意味着我们可以直接在类主体中为非静态数据成员提供初始值,而无需依赖构造函数。这种特性的引入提高了代码的可读性和简洁性,使得类的定义更加直观和简单。

使用类内初始值设定符可以让我们更方便地初始化数据成员,并且可以确保对象在创建时具有合适的初始状态。这在实践中提高了代码的可维护性和可靠性,减少了错误的可能性。因此,尽管在C++11之前,我们必须使用构造函数来初始化非静态数据成员,但引入了类内初始值设定符后,我们可以更灵活地进行类的设计和实现。

流操纵符setfill用于指定在将整数输出到宽度大于值的数字字段时显示的填充字符。默认情况下,填充字符会显示在数字的左侧,因为数字是右对齐的——对于左对齐的值,填充字符将显示在右侧。

如果要输出的数字填满了指定的字段,填充字符将不会显示。一旦使用setfill指定了填充字符,它将应用于所有后续在比正在显示的值宽的字段中显示的值。

这种机制允许在输出中对数字进行格式化,并确保它们在字段中对齐。例如,可以使用setfill来填充数字左侧的空白,以确保数字在字段中右对齐。此外,一旦使用了setfill,它将适用于后续的所有输出,直到另一个setfill指令修改了填充字符。这种灵活性使得在输出中实现各种格式化效果变得更加简单和方便。 因此说他是sticky的

至于setw则是非粘性的

定义类外部的成员函数;类作用域

1. 成员函数的定义

成员函数可以在类内部进行声明,并且在类外部进行定义。例如:

1
2
3
4
5
6
7
8
9
class MyClass {
public:
void myFunction(); // 在类内声明
};

void MyClass::myFunction() {
// 在类外定义
// 函数体实现
}

2. 类作用域

无论成员函数是在类内部定义还是在外部定义,它们都属于类的作用域。这意味着在函数体内可以直接访问类的成员变量和其他成员函数,无需使用任何限定符。

3. 内联展开

如果成员函数是在类内部进行定义,编译器会尝试进行内联展开。内联展开是一种优化技术,它会将函数的代码直接插入到调用处,而不是通过函数调用的方式执行。这样可以减少函数调用的开销,提高程序的执行效率。内联展开的效果取决于编译器和函数的复杂性,因此并不是所有函数都会被内联展开。

4. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

class MyClass {
public:
void myFunction(); // 在类内声明
};

// 在类外定义成员函数
void MyClass::myFunction() {
std::cout << "Inside myFunction" << std::endl;
}

int main() {
MyClass obj;
obj.myFunction(); // 调用成员函数
return 0;
}

在上面的示例中,myFunction() 被定义在类外部,但它仍然属于 MyClass 类的作用域。在 main() 函数中,我们创建了一个 MyClass 对象 obj 并调用了 myFunction()

对象大小

初学面向对象编程的人常常认为对象的大小会很大,因为它们包含了数据成员和成员函数。

逻辑上的大小

从逻辑上讲,这种观点是正确的。我们可以将对象视为包含数据和函数的集合体,这样的观点也在一定程度上得到了鼓励。一个对象的大小确实包括了所有的数据成员和函数成员所占用的空间。

物理上的大小

然而,从物理上来看,这种观点并不完全正确。事实上,在大多数编译器中,对象的大小主要取决于其数据成员的大小和对齐方式,而与成员函数无关。成员函数的代码并不直接存储在对象中,而是存储在类的代码段中,所有对象共享这些函数的代码。

对象的内存布局

一个对象的内存布局通常包括:

  1. 数据成员:按照声明的顺序依次存储在内存中。
  2. 虚函数表指针(如果有虚函数):用于实现多态性,指向对象所属类的虚函数表。
  3. 其他的额外信息:如对齐字节等。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

class MyClass {
public:
int data;
void myFunction() {
std::cout << "Inside myFunction" << std::endl;
}
};

int main() {
MyClass obj;
std::cout << "Size of MyClass object: " << sizeof(obj) << " bytes" << std::endl;
return 0;
}

在上面的示例中,虽然 MyClass 包含了一个数据成员 data 和一个成员函数 myFunction,但对象 obj 的大小主要取决于数据成员 data 的大小和对齐方式,而与成员函数 myFunction 无关。

一些更加深入的细节

  1. 对齐方式:在某些情况下,编译器会根据平台的要求对数据成员进行对齐,以提高访问速度和效率。这可能会导致对象的大小超出数据成员本身所占用的空间。对齐方式可以通过编译器选项进行调整。
  1. 空类的大小:即使一个类没有任何数据成员,它的大小也不是零。这是因为每个对象都需要一些空间来存储对象的地址,以便在程序中能够引用它。此外,编译器可能会为了对齐而在对象的尾部添加一些额外的填充字节。
  1. 虚函数和多态性:如果一个类包含虚函数,那么它的对象在内存中通常会包含一个指向虚函数表的指针。这个指针的大小与平台有关,通常是一个指针的大小。
  1. 继承和派生类:派生类的对象的大小会包括它继承的基类部分和自身新增的数据成员。此外,如果派生类重写了基类的虚函数,它的虚函数表指针可能会指向自己的虚函数表,导致额外的内存开销。
  1. 内存布局的优化:一些编译器可能会对内存布局进行优化,以减小对象的大小。例如,通过调整数据成员的顺序或使用紧凑的数据类型,可以减少填充字节的数量,从而减小对象的总大小。

Class Scope and Block Scope:

  • 类的数据成员和成员函数属于该类的作用域。
  • 非成员函数默认在全局命名空间作用域内定义。
  • 在类的作用域内,类成员可以直接被所有该类的成员函数访问,并且可以通过名称引用。
  • 在类的作用域外,公共类成员通过对象的某种句柄进行引用—对象名、对象的引用或对象的指针。

Dot (.) and Arrow (->) Member Selection Operators:

  • 点号成员选择运算符 (.) 前面跟着对象的名称或对象的引用,用于访问对象的成员。
  • 箭头成员选择运算符 (->) 前面跟着对象的指针,用于访问对象的成员。

Accessing public Class Members Through Objects, References and Pointers:

考虑一个具有公共 setBalance 成员函数的 Account 类,给出以下声明:

1
2
3
4
5
Account account; // 一个 Account 对象
// accountRef 引用一个 Account 对象
Account &accountRef = account;
// accountPtr 指向一个 Account 对象
Account *accountPtr = &account;

你可以使用点号 (.) 和箭头 (->) 成员选择运算符调用 member function setBalance,如下所示:

1
2
3
4
5
6
// 通过 Account 对象调用 setBalance
account.setBalance(123.45);
// 通过 Account 对象的引用调用 setBalance
accountRef.setBalance(123.45);
// 通过 Account 对象的指针调用 setBalance
accountPtr->setBalance(123.45);

Access Functions:

  • 访问函数用于读取或显示数据。
  • 访问函数经常用于测试条件的真假,这些函数通常被称为谓词函数(predicate functions)。
  • 深入剖析: 访问函数是类的重要组成部分,它们允许对象的状态得以查看或获取。这些函数允许对类的内部数据进行安全访问,同时隐藏了对象的内部实现细节。通过使用访问函数,可以封装对象的内部状态,并确保数据的一致性和完整性。谓词函数是一种特殊类型的访问函数,它们用于检查对象的属性或状态,通常返回布尔值,用于测试特定条件的真假。

Utility Functions:

  • 实用函数(也称为辅助函数)是支持类的其他成员函数运行的私有成员函数。

深入剖析: 实用函数是用于实现类的其他成员函数的辅助函数。它们通常用于封装重复或共享的代码逻辑,并提供更高级别的抽象,以促进代码的可维护性和可重用性。由于实用函数是私有的,它们仅在类的内部使用,因此可以隐藏类的内部实现细节,并防止外部代码直接访问或修改对象的内部状态。通过将类的功能分解为多个小的实用函数,可以更轻松地管理和测试类的功能,提高代码的可读性和可维护性。

都有解释了,肯定要有例子!

访问函数:

示例:

考虑一个 Car 类,其中有一个私有成员变量 speed。我们可以定义一个访问函数 getSpeed() 来获取汽车的当前速度。

1
2
3
4
5
6
7
8
class Car {
private:
int speed;
public:
int getSpeed() const {
return speed;
}
};

在这个例子中,getSpeed() 是一个访问函数,允许外部代码获取私有成员变量 speed 的值。这个函数提供了对 Car 对象内部状态的受控访问,确保了数据封装和抽象。

实用函数:

示例:

我们考虑一个 Math 类,其中有私有实用函数 calculateSquare()calculateCube(),用于执行数学运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Math {
private:
int calculateSquare(int num) const {
return num * num;
}
int calculateCube(int num) const {
return num * num * num;
}
public:
int squareAndCubeSum(int num) const {
int square = calculateSquare(num);
int cube = calculateCube(num);
return square + cube;
}
};

在这个例子中,calculateSquare()calculateCube() 是实用函数,用于执行特定的数学运算。这些函数是私有的,只能在类内部访问。公共函数 squareAndCubeSum() 利用这些实用函数来计算给定数字的平方和立方的和。这展示了实用函数如何在内部使用,以协助实现其他成员函数的功能。

默认参数

  1. 函数声明中的默认参数:默认参数通常在函数声明中指定。当定义函数时,可以在参数列表中为某些参数提供默认值。例如:

    1
    void foo(int x, int y = 10);

    在这个示例中,函数 foo 有两个参数,xy,其中 y 的默认值为 10。因此,可以这样调用函数 foo

    1
    2
    foo(5); // 将使用默认值 10
    foo(5, 15); // 传递了明确的参数值 15,将忽略默认值
  2. 函数定义中的默认参数:在函数的定义中也可以提供默认参数的值。通常情况下,将默认参数的值放在函数声明中更具可读性,因为它们提供了函数的接口信息。

  3. 默认参数的规则

    • 如果函数有多个参数,只能从最后一个参数开始使用默认参数。
    • 一旦设置了默认参数,所有后续参数都必须有默认值,不能只给其中的一部分参数提供默认值。

下面是一个简单的示例,演示了如何在 C++ 函数中使用默认参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

// 带有默认参数的函数声明
void greet(std::string name = "World");

int main() {
greet(); // 使用默认参数调用函数
greet("Alice"); // 传递了自定义的参数值
return 0;
}

// 函数定义,这里也可以再次指定默认参数值
void greet(std::string name) {
std::cout << "Hello, " << name << "!" << std::endl;
}

在这个示例中,函数 greet 有一个默认参数 name,默认值为 "World"。在 main 函数中,第一个调用没有提供参数,因此将使用默认值 "World",而第二个调用提供了自定义的参数值 "Alice"

ppt小窗口有一个关于函数的默认参数值发生变化时候,客户端代码需要进行重新编译。

假设有一个函数 printMessage,它有一个默认参数 message,默认值为 "Hello, world!"。客户端代码调用这个函数来打印消息。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void printMessage(std::string message = "Hello, world!") {
std::cout << message << std::endl;
}

int main() {
// 调用 printMessage() 函数
printMessage();

return 0;
}

现在,假设我们想要更改 printMessage 函数的默认参数值为 "Welcome to the program!"。我们对函数进行了修改:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void printMessage(std::string message = "Welcome to the program!") {
std::cout << message << std::endl;
}

int main() {
// 调用 printMessage() 函数
printMessage();

return 0;
}

如果客户端代码未进行任何修改并重新编译,它仍然调用的是旧版本的 printMessage 函数,这可能导致输出结果与预期不符。为了确保客户端代码与更新后的函数接口相匹配,我们需要重新编译客户端代码:

1
g++ main.cpp -o main

通过重新编译客户端代码,它将会使用新版本的 printMessage 函数,从而确保程序的正确性。

列表初始化器:

  1. C++11 中的列表初始化器:
    • C++11 提供了一种统一的初始化语法,称为列表初始化器,可以用于初始化任何变量。
    • 使用列表初始化器可以更简洁地调用类的构造函数。
  2. C++11 中的构造函数重载和委托构造函数:
    • 类的构造函数和成员函数也可以进行重载。
    • 重载的构造函数通常允许使用不同类型和/或数量的参数来初始化对象。
    • 要重载构造函数,需要在类定义中为每个版本的构造函数提供原型,并为每个重载的版本提供单独的构造函数定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>

class Time {
private:
int hour;
int minute;
int second;

public:
// Constructor
Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}

// Set and Get Functions
void setHour(int h) { hour = h; }
void setMinute(int m) { minute = m; }
void setSecond(int s) { second = s; }

int getHour() const { return hour; }
int getMinute() const { return minute; }
int getSecond() const { return second; }

// Other Member Functions...
};

int main() {
// Using C++11 list initializers
Time t2{ 2 }; // hour specified; minute and second defaulted
Time t3{ 21, 34 }; // hour and minute specified; second defaulted
Time t4{ 12, 25, 42 }; // hour, minute and second specified

// Using overloaded constructors
Time t5(10, 20, 30);
Time t6(5, 15);
Time t7;

std::cout << "Time t5: " << t5.getHour() << ":" << t5.getMinute() << ":" << t5.getSecond() << std::endl;
std::cout << "Time t6: " << t6.getHour() << ":" << t6.getMinute() << ":" << t6.getSecond() << std::endl;
std::cout << "Time t7: " << t7.getHour() << ":" << t7.getMinute() << ":" << t7.getSecond() << std::endl;

return 0;
}
  1. 构造函数的重载:
1
2
3
4
5
Time(); // default hour, minute and second to 0
Time( int ); // initialize hour; default minute and second to 0
Time( int, int); // initialize hour and minute; default second to 0
Time( int, int, int ); // initialize hour, minute and second

  • 在示例代码中,构造函数 Time 被重载为四个不同的版本,每个版本具有不同数量的参数,并且每个参数都有默认值。
  • 在 C++11 中,构造函数可以相互调用,称为委托构造函数(delegating constructor)。这意味着某些构造函数可以委托其工作给另一个构造函数,以避免代码重复。

委托构造在此之前并不知道,可以知道一下。

  1. 使用成员初始化列表:
    • 在示例中,构造函数的委托过程使用了成员初始化列表的形式。这样的语法允许在构造函数体之前初始化类的成员变量,可以更加清晰和高效地编写代码。

当不使用成员初始化列表时,会导致以下情况发生:

  1. 隐式调用默认构造函数:如果一个成员对象没有通过成员初始化列表进行初始化,那么该成员对象的默认构造函数将会被隐式调用。这意味着对象会被用默认构造函数的默认值初始化。
  1. 默认构造函数的默认值:默认构造函数的默认值可能不符合需求,因此可能需要通过调用成员函数来修改这些默认值。这样的修改可能会在构造函数体内部进行,或者在对象创建后通过外部调用进行。
  1. 复杂初始化的额外工作和时间:对于复杂的初始化过程,如果不使用成员初始化列表,可能会导致代码变得复杂,并且需要更多的时间和精力来维护和调试。这是因为在构造函数体内部进行初始化可能会涉及到额外的逻辑和操作,而且可能会增加代码的复杂性。
  1. 析构函数:
    • 析构函数是在对象销毁时被隐式调用的特殊成员函数。
    • 它负责在对象的内存被回收之前执行终止清理工作,但实际上并不释放内存。因此,内存可以被重新用于分配给新对象。
    • 析构函数的名称由波浪符号(~)后跟类名组成。
    • 如果不显式定义析构函数,编译器会自动生成一个空的析构函数。
    • 析构函数必须是公有的,不能有返回类型(包括 void)。
  2. 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Time {
public:
// Constructor Overloading
Time() : Time(0, 0, 0) {}
Time(int hour) : Time(hour, 0, 0) {}
Time(int hour, int minute) : Time(hour, minute, 0) {}
Time(int hour, int minute, int second) : hour(hour), minute(minute), second(second) {}

// Destructor
~Time() {
// Perform termination housekeeping chores
// (if needed)
}

private:
int hour;
int minute;
int second;
};

一些构造函数和析构函数的知识点,exit和abort在c++八股一文有所提及:

构造函数和析构函数是类的成员函数,用于对象的创建和销毁。它们的调用是隐式的,不需要手动调用。

  1. 调用顺序
    • 构造函数和析构函数的调用顺序取决于对象的创建和销毁的作用域。
    • 通常情况下,析构函数的调用顺序与对应构造函数的调用顺序相反。
    • 全局对象和静态对象的析构顺序可能会受到存储类的影响。
  2. 全局作用域对象的构造和析构
    • 在全局作用域中定义的对象,在程序执行开始前的任何其他函数(包括main函数)之前会调用其构造函数。
    • 对应的析构函数在main函数终止时调用,或者在程序调用exit函数时调用。
    • 如果程序调用exit或abort函数终止,局部对象的析构函数不会被调用。
  3. 局部作用域对象的构造和析构
    • 局部作用域对象的构造和析构与执行进入和离开对象作用域的次数相关。
    • 如果程序通过调用exit或abort函数终止,局部对象的析构函数不会被调用。
  4. 静态局部对象的构造和析构
    • 静态局部对象的构造函数只会在程序首次到达对象定义点时调用一次。
    • 对应的析构函数在main函数终止时调用,或者在程序调用exit函数时调用。
    • 静态对象的析构函数在程序调用abort函数时不会被调用。

下面是关于这两个关键词的深入剖析:

exit和abort是C++中用于程序终止的函数,它们的行为有一些差异:

  1. exit函数
    • 作用:exit函数用于正常终止程序,它会执行程序终止前的清理工作,包括调用全局对象和静态局部对象的析构函数。
    • 调用时机:可以在程序的任何地方调用exit函数,以终止程序的执行。一般在满足某些条件时,或者在程序出现严重错误时调用exit函数,以安全地结束程序。
    • 参数:exit函数可以接受一个整数参数作为退出代码,用于向操作系统传递程序的退出状态。默认情况下,退出代码为0表示程序正常终止,非零值表示程序异常终止。
    • 清理操作:exit函数会执行全局对象和静态局部对象的析构函数,释放它们占用的资源,并关闭文件等资源。然后,它会终止程序的执行,返回操作系统。
  2. abort函数
    • 作用:abort函数用于异常终止程序,它会立即终止程序的执行,不执行任何清理工作,包括全局对象和静态局部对象的析构函数。
    • 调用时机:通常在程序遇到严重错误时调用abort函数,以避免继续执行可能导致更严重后果的代码。abort函数会立即停止程序的执行,不执行任何清理操作。
    • 行为:调用abort函数会导致程序立即终止,不会执行后续的任何代码。它不会执行全局对象和静态局部对象的析构函数,因此可能导致资源泄漏或其他问题。
    • 调试:abort函数通常用于调试目的,以快速终止程序并生成调试信息。在某些情况下,程序出现无法恢复的错误时,调用abort函数可以帮助定位问题。

总之,exit函数用于正常终止程序,执行清理操作并返回退出代码,而abort函数用于异常终止程序,立即停止执行并不执行任何清理工作。在选择使用哪个函数时,需要根据程序的要求和设计考虑清理操作和终止方式。

当提及引用和私有数据成员的返回值的问题:

  1. 引用的特性
    • 引用是对象名称的别名,因此可以在赋值语句的左侧使用。引用本身就是一个合法的左值,可以接收一个值。
    • 引用的特性导致了可能存在的问题:公有成员函数可以返回对该类的私有数据成员的引用,这样返回的引用就成为了私有数据成员的别名。
  2. 私有数据成员的引用返回
    • 当一个公有成员函数返回对私有数据成员的引用时,实际上是将该成员函数变成了私有数据成员的别名。这意味着通过调用该成员函数返回的引用,可以直接操作私有数据成员,包括在赋值语句中使用作为左值。
    • 如果函数返回的引用被声明为const,则不能用于修改数据,但仍然可以用作左值。

一些可能的解决方法:

要解决公有成员函数返回私有数据成员的引用的问题,可以采取以下方法:

  1. 不返回引用
  • 最简单的方法是避免在公有成员函数中返回对私有数据成员的引用。这样可以防止直接访问私有数据成员,从而避免破坏类的封装性。
  1. 使用const引用
  • 如果确实需要提供对私有数据成员的只读访问,可以返回一个常量引用。这样,返回的引用不能用于修改数据,但可以用作左值进行读取操作。
  1. 提供访问函数
  • 更好的方法是提供专门的公有成员函数来访问私有数据成员,而不是直接返回引用。这样可以在访问数据时提供更多的控制和保护机制,例如可以添加边界检查或其他逻辑。
  1. 友元函数
  • 如果确实需要在外部访问私有数据成员,可以考虑将其他类或函数声明为友元函数。这样,友元函数可以直接访问类的私有成员,但仅限于特定的情况,并且需要谨慎使用以确保封装性。

关于对象之间的赋值操作和拷贝问题:

  1. 赋值操作符的默认行为
    • 对象之间的赋值操作默认采用成员逐一赋值(也称为复制赋值)。这意味着右侧对象的每个数据成员都会分别赋值给左侧对象对应的数据成员。
  2. 传递和返回对象
    • 对象可以作为函数的参数和返回值。默认情况下,采用按值传递的方式进行传递和返回,即会创建一个新的对象,并使用复制构造函数将原始对象的值复制到新对象中。
  3. 默认复制构造函数
    • 对于每个类,编译器提供了一个默认的复制构造函数,用于将原始对象的每个成员复制到新对象的对应成员中。这种默认的复制构造函数通常适用于简单的数据成员类型,但对于包含指向动态分配内存的指针的类,可能会导致严重的问题。
  4. 指针成员的问题
    • 当一个类的数据成员包含指向动态分配内存的指针时,使用默认的复制构造函数会导致指针的浅拷贝,可能会造成内存泄漏、悬空指针等严重问题。这些问题在待会会进行讨论,并介绍如何通过自定义复制构造函数来解决。

Const

  1. 声明常量对象
    • 使用关键字 const 可以声明一个对象为常量对象,即不可修改的对象。例如,const Time noon(12, 0, 0); 声明了一个名为 noon 的常量对象,表示中午12点。
    • 可以同时实例化 const 和非 const 类型的对象,它们在同一类中并存。
  2. 在常量对象上调用成员函数
    • 在常量对象上调用成员函数时,如果成员函数不修改对象的状态,则需要将成员函数声明为 const。否则,在编译时会发生错误。
    • 即使是不修改对象状态的 get 成员函数,在常量对象上也需要声明为 const。
  3. 成员函数的 const 限定符
    • 将成员函数声明为 const 的方法是在函数的参数列表后面插入关键字 const,在函数定义的左大括号之前加上关键字 const。
    • 所有不修改对象状态的成员函数都应该声明为 const,这是良好的编程实践,能够提高代码的可读性和可维护性。
  4. 构造函数和析构函数的 const 限制
    • 构造函数必须能够修改对象以便正确初始化,因此不能声明为 const。声明构造函数为 const 会导致编译错误。
    • 析构函数需要在对象的内存被系统回收之前执行终止清理工作,因此也不能声明为 const。
  5. 编译错误的情况
    • 如果在常量对象上调用了不带 const 限定符的成员函数,或者尝试声明构造函数或析构函数为 const,都会导致编译错误。

在C++中,类中的组合指的是一个类包含另一个类作为其成员变量。这种关系称为“has-a”关系,表示一个类具有另一个类的对象作为其一部分。组合关系允许在一个类中使用另一个类的功能,并在需要时对其进行实例化和操作。

下面是关于类中组合和数据成员构造顺序的详细解释:

  1. 组合关系
    • 组合是一种对象之间的关系,其中一个对象包含另一个对象。在类的定义中,可以声明另一个类的对象作为成员变量,从而实现组合关系。
    • 组合关系用于描述一个对象包含另一个对象的情况,例如一辆车包含引擎、轮子等部件,或者一个学生包含姓名、年龄等属性。
  2. 类中数据成员的构造顺序
    • 在创建一个包含组合关系的类的对象时,类中的数据成员会按照它们在类定义中声明的顺序进行构造。
    • 首先构造基类的成员(如果有的话),然后按照声明的顺序构造类的其他成员。
    • 如果成员变量是基本类型或者内置类型,则按照它们在类定义中声明的顺序进行构造。
    • 如果成员变量是类类型,则会调用对应类的构造函数来构造这些对象。

总之,类中数据成员的构造顺序是按照它们在类定义中声明的顺序进行的。

浅谈一下设计模式中为什么建议多用组合少用继承:

在设计模式中建议多用组合少用继承的原因有几个:

  1. 松耦合:组合关系比继承关系更加松耦合。使用组合可以使类之间的关系更灵活,降低它们之间的耦合度。当一个类包含另一个类作为成员变量时,它们之间的关系更加灵活,一个类的改变不会影响到另一个类。

  2. 更好的封装性:组合可以提供更好的封装性。通过组合,一个类可以隐藏其内部实现细节,只暴露必要的接口给外部使用。这样可以降低类之间的依赖关系,减少代码的耦合度。

  3. 复用性:组合可以提高代码的复用性。通过将功能划分为独立的组件,并在需要时将它们组合在一起,可以更容易地复用这些组件。相比之下,继承关系通常会导致代码的耦合度增加,降低了代码的可复用性。

  4. 更灵活的设计:使用组合可以实现更灵活的设计。通过组合,可以将一个类的功能拆分成多个独立的组件,然后根据需要组合这些组件来实现不同的功能。这样可以更容易地修改和扩展代码,使系统更具有扩展性和灵活性。

友元函数

注意友元函数不是成员函数,而且尽量不要放在任何access specifier后面。 友元函数(Friend Functions)是指具有访问类的公有和非公有成员的权限的非成员函数。可以将独立的函数、整个类或其他类的成员函数声明为另一个类的友元。

声明友元函数: 要将一个函数声明为类的友元函数,需要在类定义中的函数原型前加上关键字friend。例如,要将ClassTwo类的所有成员函数声明为ClassOne类的友元,可以在ClassOne类的定义中添加如下声明:

1
friend class ClassTwo;

友谊是由类主动授予的,而不是被动获得的。如果要让类B成为类A的友元,类A必须显式声明类B是其友元。友谊不是对称的——如果类A是类B的友元,不能推断出类B是类A的友元。友谊也不是传递的——如果类A是类B的友元,而类B是类C的友元,不能推断出类A是类C的友元。

重载的友元函数: 可以将重载的函数指定为类的友元函数。每个打算成为类的友元的函数都必须在类定义中显式声明为类的友元。

设计注意事项:

  • 友元函数的使用应该谨慎,因为它打破了类的封装性,可能导致代码的可维护性和安全性降低。
  • 如果可能,应该尽量减少友元函数的使用,而优先考虑通过类的成员函数来访问类的私有数据。

this指针

this指针的概念:

  • 每个对象都可以通过一个指针(称为this,是C++的关键字)访问自己的地址。
  • this指针并不是对象本身的一部分,即this指针占用的内存不会反映在对对象进行sizeof操作的结果中。
  • 实际上,编译器会将this指针作为一个隐式参数传递给对象的每个非静态成员函数。

使用this指针避免命名冲突:

  • 成员函数可以隐式(或显式)地使用this指针来引用对象的数据成员和其他成员函数。
  • 显式使用this指针的一个常见情况是避免类的数据成员与成员函数参数(或其他局部变量)之间的命名冲突。

例子: 考虑Time类中的hour数据成员和setHour成员函数。我们可以将setHour定义为:

1
2
3
4
5
6
7
// 设置小时值
void Time::setHour(int hour) {
if (hour >= 0 && hour < 24)
this->hour = hour; // 使用this指针访问数据成员
else
throw invalid_argument("hour must be 0-23");
}

this指针的类型:

  • this指针的类型取决于对象的类型以及使用this的成员函数是否声明为const。
  • 例如,在Employee类的非const成员函数中,this指针的类型为Employee。在const成员函数中,this指针的类型为const Employee

使用this指针实现级联函数调用: this指针的另一个用途是实现级联成员函数调用。这意味着可以在同一条语句中调用多个函数。

级联函数调用的概念:

级联函数调用是指在同一条语句中连续调用多个函数。这种技术使得代码更加简洁、易读,并且提高了代码的可维护性。

内核:

  1. 设置函数返回引用:
    • 首先,确保类的成员函数设置返回类型为类的引用。这样可以实现函数调用的链式操作,因为每个函数都可以返回调用该函数的对象的引用。
    • 例如,在类Time中,可以将set函数的返回类型设置为Time类的引用。
1
2
3
4
5
6
7
class Time {
public:
Time& setTime(int hour, int minute, int second);
Time& setHour(int hour);
Time& setMinute(int minute);
Time& setSecond(int second);
};
  1. **函数体中返回*this:**
    • 在函数体中,将返回语句设置为返回this,这样就可以返回调用该函数的对象的引用。因此,可以通过返回this来实现级联函数调用。
1
2
3
4
5
Time& Time::setTime(int hour, int minute, int second) {
// 设置时间
setHour(hour).setMinute(minute).setSecond(second);
return *this;
}
  1. 连续调用:
    • 在调用函数时,通过.->操作符连接函数调用,以实现级联调用。
1
2
Time t;
t.setTime(10, 20, 30).setHour(11).setMinute(45).setSecond(0);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
using namespace std;

class Time {
private:
int hour;
int minute;
int second;

public:
Time& setTime(int hour, int minute, int second) {
// 设置时间
setHour(hour).setMinute(minute).setSecond(second);
return *this;
}

Time& setHour(int hour) {
if (hour >= 0 && hour < 24)
this->hour = hour;
else
throw invalid_argument("hour must be 0-23");
return *this;
}

Time& setMinute(int minute) {
if (minute >= 0 && minute < 60)
this->minute = minute;
else
throw invalid_argument("minute must be 0-59");
return *this;
}

Time& setSecond(int second) {
if (second >= 0 && second < 60)
this->second = second;
else
throw invalid_argument("second must be 0-59");
return *this;
}

void displayTime() {
cout << "Time: " << hour << ":" << minute << ":" << second << endl;
}
};

int main() {
Time t;
t.setTime(10, 20, 30).setHour(11).setMinute(45).setSecond(0);
t.displayTime();
return 0;
}

在上面的示例中,我们通过调用setTime函数来设置时间,并通过级联调用的方式设置小时、分钟和秒。

在级联函数中,传递引用作为参数:

  1. 避免对象拷贝: 级联函数通常需要在多个函数之间传递相同的对象,如果直接传递对象而不是引用,每个函数都会创建对象的副本,导致不必要的对象拷贝,增加额外的开销和内存消耗。通过传递引用,可以避免对象的拷贝,提高程序的效率和性能。
  1. 保持数据一致性: 通过传递引用,级联函数可以直接修改原始对象,而不是操作对象的副本。这样可以确保在级联函数链中对数据的修改是一致的,避免出现不一致或者意外的行为。

static Class Members

静态数据成员在类中的使用有几个重要方面:

  1. 共享类范围: 静态数据成员具有类范围,意味着它们对于该类的所有对象都是共享的。这意味着只有一个副本存在于内存中,被所有实例共享。这对于表示类范围的信息非常有用,例如计数器、配置参数等。

  2. 初始化: 静态数据成员必须被初始化且只能初始化一次。基本类型的静态数据成员默认初始化为0。在C++11之前,静态const数据成员可以在类定义中初始化,而其他静态数据成员必须在类定义外部的全局命名空间范围内进行初始化。C++11引入了内联初始化器,允许在类定义中初始化静态数据成员。

  3. 访问控制: 类的私有和受保护的静态成员通常通过公有成员函数或友元进行访问。公有静态成员可直接通过类名和作用域解析运算符(::)进行访问。而对于私有或受保护的静态成员,则需要提供公有的静态成员函数来访问,通过类名和作用域解析运算符来调用这些函数。

  4. 类的静态成员存在性: 类的静态成员存在于任何类对象存在与否的情况下。这意味着即使没有类的对象被实例化,静态成员仍然可以被访问和使用。静态成员函数是类的服务,而不是特定对象的服务。

看一个问题罢:加深一下对static的理解 为什么静态成员函数不能是const?

static在c++中的第五种含义:用static修饰不访问非静态数据成员的类成员函数。这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量。

不能用const的原因: 这是C++的规则,const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,函数调用方式为thiscall,而类中的static函数本质上是全局函数,调用规约是__cdecl或__stdcall,不能用const来修饰它。一个静态成员函数访问的值是其参数、静态数据成员和全局变量,而这些数据都不是对象状态的一部分。而对成员函数中使用关键字const是表明:函数不会修改该函数访问的目标对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么就没必要使用const了。 什么时候使用静态数据成员和静态函数呢? 定义数据成员为静态变量,以表明此全局数据逻辑上属于该类。 定义成员函数为静态函数,以表明此全局函数逻辑上属于该类,而且该函数只对静态数据、全局数据或者参数进行操作,而不对非静态数据成员进行操作。

static的第一种含义:修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。 static的第二种含义:修饰局部变量时,表明该变量的值不会因为函数终止而丢失。 static的第三种含义:修饰函数时,表明该函数只在同一文件中调用。 static的第四种含义:修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归 所有对象共有。 static的第五种含义:修饰类成员函数,如上。


Operator Overloading; Class string

关于string的一些重载

理解类 string 的重载相等和关系运算符,以及其他成员函数的工作方式:

  • 类 string 重载了相等运算符(==)、不等运算符(!=)、小于运算符(<)、小于等于运算符(<=)、大于运算符(>)、大于等于运算符(>=),以执行字典顺序的比较。
  • 这些运算符比较两个字符串的字符数值,按照字典顺序进行比较。
  • 类 string 还提供了成员函数 empty(),用于判断字符串是否为空。
  • 使用 += 运算符可以进行字符串连接,例如,str += "append"
  • 成员函数 substr() 可以返回原字符串的子字符串。
    • 可以指定起始位置和长度,也可以只指定起始位置。
  • 重载的 [] 运算符可以访问字符串中的字符,但不执行边界检查。
  • 成员函数 at() 提供了边界检查,如果下标无效,会抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <string>

int main() {
std::string s1 = "Hello, World!";
std::string s2 = "Hello, C++!";

// Equality and relational operators
if (s1 == s2) {
std::cout << "s1 is equal to s2\n";
} else if (s1 < s2) {
std::cout << "s1 is less than s2\n";
} else {
std::cout << "s1 is greater than s2\n";
}

// Empty function
if (s1.empty()) {
std::cout << "s1 is empty\n";
} else {
std::cout << "s1 is not empty\n";
}

// Concatenation using +=
s1 += " Welcome to C++!";
std::cout << "Concatenated string: " << s1 << "\n";

// Substring using substr()
std::string substr1 = s1.substr(0, 5); // Get first 5 characters
std::string substr2 = s1.substr(15); // Get substring starting from index 15
std::cout << "Substring 1: " << substr1 << "\n";
std::cout << "Substring 2: " << substr2 << "\n";

// Accessing characters using []
s1[0] = 'h'; // Replace first character with lowercase 'h'
std::cout << "Modified string: " << s1 << "\n";

// Accessing characters using at() (with bounds checking)
try {
char ch = s1.at(20); // Try to access character at index 20
std::cout << "Character at index 20: " << ch << "\n";
} catch (const std::out_of_range& e) {
std::cerr << "Error: " << e.what() << "\n";
}

return 0;
}

姑且利用string类小小编写一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <iostream>
#include <cstring> // For strcpy, strcat, strlen

class MyString {
private:
char* str;
size_t length;

public:
// Default constructor
MyString() : str(nullptr), length(0) {}

// Constructor from char array
MyString(const char* s) {
length = strlen(s);
str = new char[length + 1];
strcpy(str, s);
}

// Copy constructor
MyString(const MyString& other) {
length = other.length;
str = new char[length + 1];
strcpy(str, other.str);
}

// Destructor
~MyString() {
delete[] str;
}

// Assignment operator
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] str;
length = other.length;
str = new char[length + 1];
strcpy(str, other.str);
}
return *this;
}

// Concatenation operator
MyString operator+(const MyString& other) const {
MyString result;
result.length = length + other.length;
result.str = new char[result.length + 1];
strcpy(result.str, str);
strcat(result.str, other.str);
return result;
}

// Append function
MyString& append(const MyString& other) {
size_t newLength = length + other.length;
char* newStr = new char[newLength + 1];
strcpy(newStr, str);
strcat(newStr, other.str);
delete[] str;
str = newStr;
length = newLength;
return *this;
}

// Accessor for length
size_t size() const {
return length;
}

// Accessor for C-style string
const char* c_str() const {
return str;
}

// Overloaded output operator
friend std::ostream& operator<<(std::ostream& os, const MyString& s) {
os << s.str;
return os;
}

// Comparison operators
bool operator==(const MyString& other) const {
return strcmp(str, other.str) == 0;
}

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

bool operator<(const MyString& other) const {
return strcmp(str, other.str) < 0;
}

bool operator>(const MyString& other) const {
return strcmp(str, other.str) > 0;
}
};

int main() {
MyString s1("Hello");
MyString s2(" World!");
MyString s3 = s1 + s2; // Concatenation
std::cout << "s3: " << s3 << "\n";

// Append
s3.append(" How are you?");
std::cout << "s3 after append: " << s3 << "\n";

// Comparison
if (s1 == s2) {
std::cout << "s1 is equal to s2\n";
} else {
std::cout << "s1 is not equal to s2\n";
}

if (s1 < s2) {
std::cout << "s1 is less than s2\n";
} else {
std::cout << "s1 is not less than s2\n";
}

return 0;
}

重载

  1. 运算符重载简介
    • 运算符提供了一种简洁直观的方式来操作对象。
    • C++ 允许大多数现有的运算符进行重载,意味着它们的行为可以针对用户自定义类型进行定制。
  2. 运算符重载机制
    • 运算符重载并不是自动的。您需要显式定义运算符重载函数来执行所需的操作。
    • 要重载一个运算符,您需要编写一个函数,其名称以关键字 operator 开头,后跟要重载的运算符的符号。例如,operator+ 用于重载加法运算符 +
  3. 运算符重载的考虑事项
    • 当作为成员函数重载运算符时,它们必须是非静态的,通常操作于对象本身。
    • 赋值运算符 (=)、地址运算符 (&) 和逗号运算符 () 具有默认行为,可以通过重载进行覆盖。
    • 运算符的优先级、结合性和参数个数不能通过重载来改变。运算符必须保留其原始行为和属性。
  • 优先级 (precedence)
  • 结合性 (associativity)
  • 参数个数 (number of operands)
    • 重载的一元和二元运算符必须保持其原始参数个数。例如,一元运算符应保持一元,二元运算符应保持二元。
    • 不能通过运算符重载来创建新的运算符,且无法改变运算符在基本类型上的含义。
  1. 限制和约束
    • 某些运算符,如 ()[]-> 和赋值运算符,必须作为成员函数进行重载。
    • 相关的运算符,如 ++=,需要单独进行重载。
    • 运算符在基本类型上的行为无法通过运算符重载来改变。
  2. 运算符重载函数
    • 重载的运算符函数可以是成员函数,也可以是非成员函数,但某些运算符必须作为成员函数进行重载。

在C++中,二元运算符可以通过成员函数或非成员函数进行重载。作为成员函数重载时,只需要一个参数;作为非成员函数重载时,需要两个参数,其中一个必须是类对象或类对象的引用。

输入和输出基本数据类型可以使用流提取运算符 >> 和流插入运算符 <<。C++类库为每种基本类型(包括指针和 char * 字符串)重载了这些二元运算符。你也可以重载这些运算符来实现自定义类型的输入和输出。

下面是一个示例代码,展示了如何重载这些运算符来输入和输出电话号码对象 PhoneNumber,格式为“(000) 000-0000”。该程序假设电话号码已经正确输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <iomanip>
#include <string>

class PhoneNumber {
private:
std::string areaCode;
std::string exchange;
std::string line;

public:
PhoneNumber() : areaCode(""), exchange(""), line("") {}

// Overloaded stream insertion operator for output
friend std::ostream& operator<<(std::ostream& output, const PhoneNumber& number) {
output << "(" << number.areaCode << ") " << number.exchange << "-" << number.line;
return output;
}

// Overloaded stream extraction operator for input
friend std::istream& operator>>(std::istream& input, PhoneNumber& number) {
input.ignore(); // Skip '('
input >> std::setw(3) >> number.areaCode; // Read area code
input.ignore(2); // Skip ') ' characters
input >> std::setw(3) >> number.exchange; // Read exchange
input.ignore(); // Skip '-' character
input >> std::setw(4) >> number.line; // Read line
return input;
}
};

int main() {
PhoneNumber myNumber;
std::cout << "Enter a phone number in the format (123) 456-7890:" << std::endl;
std::cin >> myNumber; // Input a phone number
std::cout << "You entered: " << myNumber << std::endl; // Output the phone number
return 0;
}

作为非成员 friend 函数的重载运算符:

函数 operator>> 和 operator<< 被声明为非成员友元函数。 它们是非成员函数,因为类 PhoneNumber 的对象是运算符的右操作数。 为什么流插入和流提取运算符作为非成员函数进行重载:

重载的流插入运算符(<<)在表达式中的左操作数为 ostream & 类型时使用,例如 cout << classObject。 要在这种情况下使用运算符,右操作数必须是一个用户定义类的对象,因此必须将其重载为非成员函数。 类似地,重载的流提取运算符(>>)在表达式中的左操作数为 istream & 类型时使用,例如 cin >> classObject,并且右操作数是用户定义类的对象,因此也必须是非成员函数。

一元运算符重载

一元运算符可以被重载为类的非静态成员函数或非成员函数。

  • 作为非静态成员函数:无参数。这意味着一元运算符的重载函数作为类的成员函数存在,不需要参数,直接在类内部定义。调用时将针对对象进行操作。
  • 作为非成员函数:一个参数。一元运算符的重载函数作为类的友元函数存在,需要一个参数,这个参数必须是类的对象(或对象的引用)。调用时参数作为函数的参数传递进去。

例如,取反运算符(!)可以被重载为一个参数的非成员函数。这意味着可以将取反操作作为类的友元函数进行定义,参数为类的对象或引用。

例子!

好的,下面分别给出非成员函数和成员函数重载取反运算符的示例:

非成员函数重载取反运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

class MyClass {
private:
bool value;
public:
MyClass(bool val) : value(val) {}

// 友元声明
friend MyClass operator!(const MyClass& obj);

// 打印对象的value
void print() const {
std::cout << "Value: " << value << std::endl;
}
};

// 重载取反运算符为非成员函数
MyClass operator!(const MyClass& obj) {
// 对对象的value进行取反操作
return MyClass(!obj.value);
}

int main() {
MyClass obj1(true);
MyClass obj2(false);

// 使用重载的取反运算符
MyClass result1 = !obj1;
MyClass result2 = !obj2;

// 打印结果
std::cout << "Result 1 after negation: ";
result1.print();

std::cout << "Result 2 after negation: ";
result2.print();

return 0;
}

成员函数重载取反运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

class MyClass {
private:
bool value;
public:
MyClass(bool val) : value(val) {}

// 重载取反运算符为成员函数
MyClass operator!() const {
// 对对象的value进行取反操作
return MyClass(!value);
}

// 打印对象的value
void print() const {
std::cout << "Value: " << value << std::endl;
}
};

int main() {
MyClass obj1(true);
MyClass obj2(false);

// 使用重载的取反运算符
MyClass result1 = !obj1;
MyClass result2 = !obj2;

// 打印结果
std::cout << "Result 1 after negation: ";
result1.print();

std::cout << "Result 2 after negation: ";
result2.print();

return 0;
}

这两个示例中,我们都定义了一个 MyClass 类,该类有一个私有成员变量 value 表示一个布尔值。然后,我们分别重载了取反运算符为非成员函数和成员函数。在 main 函数中,我们创建了两个 MyClass 对象 obj1obj2,然后使用重载的取反运算符对它们进行取反操作,并打印出结果。

重载前缀和后缀的递增和递减运算符

重载前缀和后缀版本的递增和递减运算符时,需要注意它们的不同之处以及如何正确地实现它们。以下是一些关键点和示例代码:

重载前缀递增运算符 (++i):

  • 当重载前缀递增运算符时,通常将其实现为成员函数或非成员函数。
  • 成员函数的原型为 T& operator++();,其中 T 是类的类型。
  • 非成员函数的原型为 T& operator++(T&);,其中 T 是类的类型。
  • 下面是一个重载前缀递增运算符的示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

class Date {
private:
int day;

public:
Date(int d) : day(d) {}

// 重载前缀递增运算符为成员函数
Date& operator++() {
++day;
return *this;
}

// 重载前缀递增运算符为非成员函数
friend Date& operator++(Date& d) {
++d.day;
return d;
}

// 打印日期
void print() const {
std::cout << "Day: " << day << std::endl;
}
};

int main() {
Date d1(5);

// 使用重载的前缀递增运算符
++d1;
d1.print(); // 输出 Day: 6

return 0;
}

重载后缀递增运算符 (i++):

  • 后缀递增运算符的区别在于,它需要一个额外的参数来区分前缀和后缀版本。
  • 当重载后缀递增运算符时,通常将其实现为成员函数或非成员函数。
  • 成员函数的原型为 T operator++(int);,其中 T 是类的类型。
  • 非成员函数的原型为 T operator++(T&, int);,其中 T 是类的类型。
  • 下面是一个重载后缀递增运算符的示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

class Date {
private:
int day;

public:
Date(int d) : day(d) {}

// 重载后缀递增运算符为成员函数
Date operator++(int) {
Date temp(day);
++day;
return temp;
}

// 重载后缀递增运算符为非成员函数
friend Date operator++(Date& d, int) {
Date temp(d.day);
++d.day;
return temp;
}

// 打印日期
void print() const {
std::cout << "Day: " << day << std::endl;
}
};

int main() {
Date d1(5);

// 使用重载的后缀递增运算符
d1++;
d1.print(); // 输出 Day: 6

return 0;
}

返回局部变量的引用是一个常见的编程错误,它会导致未定义的行为(Undefined Behavior)。这个错误通常发生在函数中返回了一个对局部变量的引用,并且在函数返回后尝试使用该引用。

为什么会出错:

  1. 生命周期问题: 局部变量是在函数内部定义的,当函数执行完毕时,这些局部变量的生命周期也会结束,它们的内存空间会被释放。
  2. 返回引用: 如果函数返回了一个对局部变量的引用,那么在函数返回后,引用指向的内存空间已经被释放,这样使用该引用就会访问无效的内存地址。
  3. 未定义的行为: 访问已释放的内存是未定义的行为,这意味着程序可能会出现任何不确定的行为,包括崩溃、输出错误结果等。

示例:

1
2
3
4
5
6
7
8
9
10
int& foo() {
int x = 10;
return x; // 返回对局部变量 x 的引用
}

int main() {
int& ref = foo();
std::cout << ref << std::endl; // 使用返回的引用
return 0;
}

在上面的示例中,函数 foo() 返回了一个对局部变量 x 的引用。当 foo() 函数执行完毕后,x 的生命周期结束,它的内存空间被释放。但是在 main() 函数中,我们仍然尝试使用对 x 的引用 ref,这会导致未定义的行为。

如何避免:

  • 避免返回对局部变量的引用: 在函数中不要返回对局部变量的引用,而是返回一个对象或者在堆上分配内存。
  • 使用静态变量或静态成员变量: 如果需要返回一个固定的变量,可以使用静态变量或静态成员变量,它们的生命周期延长到程序结束。
  • 使用堆内存: 如果需要返回一个动态分配的对象,可以使用 new 关键字在堆上分配内存,并返回指向该对象的指针。

动态内存管理允许程序员在运行时控制对象和任何内置或用户定义类型的数组的分配和释放。这是通过使用 newdelete 操作符实现的。

使用 new 分配动态内存:

  • new 操作符用于在程序运行时动态分配所需大小的内存空间。
  • 分配的对象或数组在自由存储区(也称为堆)中创建,堆是每个程序用于存储动态分配对象的一块内存区域。
  • new 操作符返回指向分配的内存空间的指针,可以通过该指针访问该内存。
1
Time *ptr = new Time; // 分配一个 Time 对象并返回指针

使用 delete 释放动态内存:

  • delete 操作符用于释放通过 new 分配的动态内存空间。
  • 使用 delete 操作符时,首先调用对象的析构函数,然后释放与对象关联的内存,将其返回到自由存储区。
1
delete ptr; // 释放动态分配的对象的内存空间

动态分配数组:

  • 使用 new 操作符可以动态分配数组。
  • 可以指定数组的大小,并使用圆括号初始化数组元素。
1
int *gradesArray = new int[10](); // 分配一个包含10个元素的整型数组并将其初始化为0

使用 delete[] 释放动态分配的数组:

  • 对于动态分配的数组,必须使用 delete[] 操作符释放内存,否则会导致内存泄漏。
  • delete[] 操作符会先调用数组中每个对象的析构函数,然后释放与数组关联的内存。
1
delete[] gradesArray; // 释放动态分配的数组的内存空间

C++11 中的 unique_ptr

  • C++11 引入了 unique_ptr,它是一种智能指针,用于管理动态分配的内存。
  • unique_ptr 超出作用域时,它的析构函数会自动将管理的内存返回到自由存储区。
1
std::unique_ptr<Time> ptr(new Time); // 使用 unique_ptr 管理动态分配的对象

以下是使用 unique_ptr 进行动态内存管理的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <memory>

class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}

~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}

void display() {
std::cout << "Displaying MyClass object" << std::endl;
}
};

int main() {
// 使用 unique_ptr 动态分配一个 MyClass 对象
std::unique_ptr<MyClass> ptr(new MyClass);

// 调用对象的成员函数
ptr->display();

// 不需要显式释放内存,unique_ptr 超出作用域时会自动释放内存
return 0;
}

在这个示例中,我们创建了一个名为 MyClass 的类,然后使用 unique_ptr 动态分配了一个 MyClass 对象。当 unique_ptr 超出作用域时,它会自动释放管理的内存,因此无需显式调用 delete。这种自动内存管理可以避免内存泄漏和悬挂指针问题。

两个问题的解释:

  1. 内存泄漏: 内存泄漏指的是程序在分配了一块内存后,再也没有释放或回收它,导致该内存块无法再被使用,但却一直占据着内存。内存泄漏可能会导致程序运行时内存消耗过多,最终耗尽系统资源,甚至导致程序崩溃。内存泄漏通常发生在程序员忘记释放动态分配的内存、释放内存的顺序错误或者无法访问释放内存的代码路径等情况下。

  2. 悬挂指针: 悬挂指针是指指向已被释放的内存的指针。当程序中的某个指针被释放了但没有被设置为 nullptr 或者被重新分配,而其他部分仍然尝试通过该指针来访问内存时,就会导致悬挂指针问题。悬挂指针可能会导致程序崩溃、数据损坏或者未定义的行为。悬挂指针的产生通常是由于指针被重复释放、指针被释放后未被置空、或者指针超出了其作用域而未被及时释放等情况引起的。

下面是一个示例,展示了内存泄漏和悬挂指针问题的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

void memoryLeak() {
// 内存泄漏:未释放分配的内存
int* ptr = new int(5);
// 没有 delete ptr;
}

void danglingPointer() {
// 悬挂指针:释放后的内存仍被访问
int* ptr = new int(10);
delete ptr;
std::cout << *ptr << std::endl; // 尝试访问已释放的内存
}

int main() {
// 内存泄漏示例
memoryLeak();

// 悬挂指针示例
danglingPointer();

return 0;
}

在这个示例中,memoryLeak 函数展示了内存泄漏问题,因为在动态分配内存后没有释放它。而 danglingPointer 函数展示了悬挂指针问题,因为在释放内存后,仍然尝试通过指针访问已经释放的内存。

实例: 内置数组存在很多问题:

  1. 越界访问
    • 程序可以很容易地访问数组的两端之外的内存,因为C++不会检查下标是否超出了数组的范围。
  2. 固定下标范围
    • 内置数组的大小为n时,其元素必须编号为0到n-1,不允许使用其他下标范围。
  3. 无法整体输入或输出
    • 整个内置数组无法一次性输入或输出。
  4. 无法比较
    • 两个内置数组不能使用相等或关系运算符进行有意义的比较。
  5. 需要传递数组大小
    • 当数组传递给处理任意大小数组的通用函数时,必须将数组的大小作为附加参数传递。
  6. 不能直接赋值
    • 一个内置数组不能用赋值运算符直接赋值给另一个数组。

为了解决这些问题,我们可以通过类和运算符重载来实现更强大的数组功能,例如C++标准库中的arrayvector类模板。在这个部分,我们将开发一个自定义的数组类,这个类相比于内置数组具有以下优点:

  • 范围检查
    • 实现数组范围检查,防止越界访问。
  • 赋值运算符
    • 允许一个数组对象通过赋值运算符赋值给另一个数组对象。
  • 自知大小
    • 数组对象知道自己的大小。
  • 输入输出运算符
    • 可以通过流提取和插入运算符分别输入和输出整个数组。
  • 比较运算符
    • 可以使用相等运算符==和不等运算符!=比较数组。

C++标准库中的vector类模板也提供了许多类似的功能。以下是一个实现自定义数组类的详细笔记和代码示例:

Array类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include <iostream>
#include <stdexcept> // for std::out_of_range
#include <cstring> // for std::memcpy

class Array {
public:
// Constructor
Array(size_t size) : size_(size), data_(new int[size]) {}

// Destructor
~Array() { delete[] data_; }

// Copy constructor
Array(const Array& other) : size_(other.size_), data_(new int[other.size_]) {
std::memcpy(data_, other.data_, size_ * sizeof(int));
}

// Assignment operator
Array& operator=(const Array& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::memcpy(data_, other.data_, size_ * sizeof(int));
}
return *this;
}

// Access operator with range checking
int& operator[](size_t index) {
if (index >= size_) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}

// Const access operator with range checking
const int& operator[](size_t index) const {
if (index >= size_) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}

// Equality operator
bool operator==(const Array& other) const {
if (size_ != other.size_) return false;
for (size_t i = 0; i < size_; ++i) {
if (data_[i] != other.data_[i]) return false;
}
return true;
}

// Inequality operator
bool operator!=(const Array& other) const {
return !(*this == other);
}

// Stream insertion operator
friend std::ostream& operator<<(std::ostream& os, const Array& array) {
for (size_t i = 0; i < array.size_; ++i) {
os << array.data_[i] << ' ';
}
return os;
}

// Stream extraction operator
friend std::istream& operator>>(std::istream& is, Array& array) {
for (size_t i = 0; i < array.size_; ++i) {
is >> array.data_[i];
}
return is;
}

// Get size of the array
size_t size() const { return size_; }

private:
size_t size_; // size of the array
int* data_; // pointer to the array data
};

int main() {
Array arr1(5);

std::cout << "Enter 5 integers for arr1: ";
std::cin >> arr1;

std::cout << "arr1: " << arr1 << std::endl;

Array arr2 = arr1; // Copy constructor
std::cout << "arr2 (copy of arr1): " << arr2 << std::endl;

Array arr3(5);
arr3 = arr1; // Assignment operator
std::cout << "arr3 (assigned from arr1): " << arr3 << std::endl;

std::cout << "arr1 == arr2: " << (arr1 == arr2 ? "true" : "false") << std::endl;
std::cout << "arr1 != arr3: " << (arr1 != arr3 ? "true" : "false") << std::endl;

try {
std::cout << "Accessing arr1[10]: " << arr1[10] << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << e.what() << std::endl;
}

return 0;
}

代码解释

  1. 构造函数和析构函数
    • Array(size_t size):构造函数,初始化数组大小并分配内存。
    • ~Array():析构函数,释放分配的内存。
  2. 复制构造函数
    • Array(const Array& other):复制构造函数,创建一个新数组并将另一个数组的数据复制到新数组中。
  3. 赋值运算符
    • Array& operator=(const Array& other):赋值运算符,释放当前数组的内存,然后复制另一个数组的数据。
  4. 访问运算符
    • int& operator[](size_t index)const int& operator[](size_t index) const:带有范围检查的访问运算符,用于访问数组元素。
  5. 比较运算符
    • bool operator==(const Array& other) constbool operator!=(const Array& other) const:比较运算符,用于比较两个数组是否相等或不等。
  6. 流插入和提取运算符
    • friend std::ostream& operator<<(std::ostream& os, const Array& array):用于输出数组内容的流插入运算符。
    • friend std::istream& operator>>(std::istream& is, Array& array):用于输入数组内容的流提取运算符。
  7. 获取数组大小的成员函数
    • size_t size() const:返回数组的大小。

通过上述代码实现的自定义数组类,我们可以避免使用内置数组时遇到的一些常见问题,并提供更强大、更安全的功能。

重载有一个需要注意的点就是:交换性运算符 为了使运算符交换性(如加法),可以将其实现为非成员函数。非成员函数可以交换其参数并调用成员函数。

类型转换与转换运算符详细笔记

概述

在C++中,类型转换是一项重要功能,允许将一种数据类型转换为另一种数据类型。对于内置类型,编译器可以执行一些自动转换。但是,对于用户定义的类型之间以及用户定义的类型和内置类型之间的转换,必须显式指定如何进行。

转换构造函数

转换构造函数是一种可以通过单个参数调用的构造函数,用于将其他类型(包括内置类型)的对象转换为特定类的对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
// 转换构造函数
MyClass(int x) {
// 将int类型的x转换为MyClass类型的对象
}
};

int main() {
MyClass obj = 10; // 使用转换构造函数将int转换为MyClass对象
}

在上述示例中,构造函数 MyClass(int x) 可以通过一个整数参数调用,因此它是一个转换构造函数。它允许将 int 类型的值自动转换为 MyClass 类型的对象。

转换运算符(Cast Operators)

转换运算符,也称为类型转换运算符或强制转换运算符,是用于将一个类的对象转换为另一种类型的运算符。转换运算符必须是非静态成员函数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
private:
int value;

public:
MyClass(int val) : value(val) {}

// 转换运算符,将MyClass对象转换为int
operator int() const {
return value;
}
};

int main() {
MyClass obj(42);
int val = static_cast<int>(obj); // 使用转换运算符将MyClass对象转换为int
std::cout << val << std::endl; // 输出: 42
}

在上述示例中,operator int() const 是一个转换运算符,用于将 MyClass 类型的对象转换为 int 类型。转换运算符必须返回要转换的目标类型。

转换构造函数与转换运算符的隐式调用

一个很有用的特性是,当需要时,编译器可以隐式地调用转换构造函数和转换运算符来创建临时对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
private:
int value;

public:
MyClass(int val) : value(val) {}

// 转换运算符,将MyClass对象转换为int
operator int() const {
return value;
}
};

void printValue(int x) {
std::cout << "Value: " << x << std::endl;
}

int main() {
MyClass obj(42);
printValue(obj); // 隐式调用转换运算符,将MyClass对象转换为int
}

在上述示例中,函数 printValue 需要一个 int 类型的参数。当我们传递 MyClass 对象 obj 时,编译器隐式地调用 operator int()MyClass 对象转换为 int

小结

  • 转换构造函数 通过单个参数调用,可以将其他类型(包括内置类型)转换为特定类的对象。
  • 转换运算符 是非静态成员函数,用于将一个类的对象转换为另一种类型。
  • 转换运算符的返回类型是隐式的,即转换的目标类型。
  • 编译器可以隐式调用转换构造函数和转换运算符来创建临时对象。

详细示例

转换构造函数和转换运算符结合使用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>

class StringConverter {
private:
std::string str;

public:
// 转换构造函数,从const char*转换为StringConverter
StringConverter(const char* s) : str(s) {}

// 转换运算符,将StringConverter转换为std::string
operator std::string() const {
return str;
}
};

int main() {
// 使用转换构造函数
StringConverter sc = "Hello, World!";

// 使用转换运算符
std::string stdStr = sc;

// 输出结果
std::cout << stdStr << std::endl; // 输出: Hello, World!

return 0;
}

在这个示例中:

  • 转换构造函数 StringConverter(const char* s)const char* 类型转换为 StringConverter 对象。
  • 转换运算符 operator std::string() constStringConverter 对象转换为 std::string

有一个东西总会有问题:

隐式转换可能引发的问题

在C++中,隐式转换可以使代码更简洁,但也可能引发一些意想不到的问题,导致编译错误或运行时逻辑错误。下面详细解释这些问题。

隐式转换导致的编译错误

当编译器在不明确的上下文中进行隐式转换时,可能会产生二义性,导致编译错误。例如,当有多个重载函数或运算符存在时,编译器可能无法确定应该调用哪个版本。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

class MyClass {
public:
MyClass(int x) {}
};

void func(MyClass obj) {
std::cout << "func(MyClass) called" << std::endl;
}

void func(int x) {
std::cout << "func(int) called" << std::endl;
}

int main() {
func(10); // 编译器无法确定调用哪个版本的func
return 0;
}

在这个例子中,调用 func(10) 时,编译器无法确定是应该调用 func(MyClass) 还是 func(int),因为 10 可以隐式转换为 MyClass 对象,导致编译错误。

隐式转换导致的运行时逻辑错误

即使编译成功,隐式转换也可能导致逻辑错误。特别是在没有明确意识到隐式转换发生的情况下,代码的行为可能与预期不符。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

class MyClass {
private:
int value;
public:
MyClass(int x) : value(x) {}

// 转换运算符,将MyClass对象转换为int
operator int() const {
return value;
}
};

void printValue(int x) {
std::cout << "Value: " << x << std::endl;
}

int main() {
MyClass obj(42);
int val = 0;

if (obj) { // obj被隐式转换为int,然后与0比较
val = 100;
}

printValue(val); // 输出的值可能不是预期的
return 0;
}

在这个示例中,if (obj) 会隐式调用 operator int(),将 MyClass 对象转换为 int,然后与 0 进行比较。这可能不是预期的逻辑,导致 val 的值不是预期的 100

避免隐式转换问题的方法

  1. 使用 explicit 关键字:

    使用 explicit 关键字可以防止隐式转换,只有在显式调用时才会进行转换。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class MyClass {
    public:
    explicit MyClass(int x) {}
    };

    void func(MyClass obj) {
    std::cout << "func(MyClass) called" << std::endl;
    }

    void func(int x) {
    std::cout << "func(int) called" << std::endl;
    }

    int main() {
    func(10); // 调用func(int),没有歧义
    func(MyClass(10)); // 必须显式转换
    return 0;
    }
  2. 限制转换运算符的使用:

    避免定义过多的转换运算符,尤其是那些可能导致模糊转换的运算符。

  3. 明确传递参数类型:

    确保在调用函数时传递的参数类型明确,避免不必要的隐式转换。

总结

隐式转换虽然在某些情况下提高了代码的简洁性和可读性,但也带来了潜在的编译错误和运行时逻辑错误风险。通过使用 explicit 关键字和限制转换运算符的使用,可以有效地避免这些问题,确保代码的正确性和可维护性。

重载函数调用运算符()

重载函数调用运算符 () 是 C++ 中一种强大的功能,因为它允许函数接受任意数量的用逗号分隔的参数。在自定义的类中,函数调用运算符可以用来实现各种功能,比如从字符串中选取子字符串,或者提供一个替代的数组下标访问方式。

示例:重载函数调用运算符以选取子字符串

假设我们有一个自定义的 String 类,我们可以重载 operator() 来实现从字符串中选取子字符串的功能。这个操作符可以接收两个整数参数,分别表示子字符串的起始位置和长度。

定义:

我们首先在 String 类中定义 operator(),该函数应该是一个非静态成员函数,并且因为选取子字符串不会修改原始字符串对象,所以它应该是一个 const 成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class String {
public:
// 构造函数和其他成员函数的定义
...

// 重载函数调用运算符
String operator()(size_t index, size_t length) const {
// 检查参数的合法性
if (index >= strLength || length < 0 || index + length > strLength) {
// 处理错误情况,比如抛出异常或返回空字符串
return String("");
}

// 选取子字符串并返回
return String(substr(index, length));
}

private:
char *strData; // 存储字符串数据的指针
size_t strLength; // 字符串长度

// 辅助函数,用于获取子字符串
String substr(size_t index, size_t length) const {
char *subStr = new char[length + 1];
strncpy(subStr, strData + index, length);
subStr[length] = '\0';
return String(subStr);
}
};
使用:

假设 string1 是一个包含字符串 "AEIOU" 的 String 对象,当编译器遇到表达式 string1(2, 3) 时,它会生成成员函数调用 string1.operator()(2, 3),该调用将返回一个包含 "IOU" 的 String 对象。

1
2
String string1("AEIOU");
String subString = string1(2, 3); // 返回 "IOU"

示例:重载函数调用运算符以实现二维数组的下标访问

除了选取子字符串外,函数调用运算符还可以用来实现二维数组的下标访问。这样,我们可以用 chessBoard(row, column) 代替传统的 chessBoard[row][column] 访问方式。

定义:

我们可以在二维数组类中定义 operator() 来实现这种访问方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ChessBoard {
public:
ChessBoard(size_t rows, size_t cols) : rows(rows), cols(cols) {
board = new int*[rows];
for (size_t i = 0; i < rows; ++i) {
board[i] = new int[cols];
}
}

~ChessBoard() {
for (size_t i = 0; i < rows; ++i) {
delete[] board[i];
}
delete[] board;
}

int& operator()(size_t row, size_t col) {
// 检查参数的合法性
if (row >= rows || col >= cols) {
throw std::out_of_range("Index out of range");
}
return board[row][col];
}

private:
size_t rows, cols;
int **board;
};
使用:
1
2
3
ChessBoard chessBoard(8, 8);
chessBoard(0, 0) = 1; // 访问并设置第 0 行第 0 列的元素
int value = chessBoard(0, 0); // 访问第 0 行第 0 列的元素

Object-Oriented Programming: Inheritance

继承与关系

在面向对象编程中,我们通常关注系统中对象之间的共性,而不是特例。

is-a 和 has-a 关系

is-a 关系 表示继承。在 is-a 关系中,派生类的对象也可以被视为其基类的对象。相反,has-a 关系 表示组合。

继承的概念

继承是一种软件重用形式,它使得我们可以创建一个类,它吸收了现有类的数据和行为,并且增加了新的功能。我们可以指定一个新类应该继承现有类的成员。现有的类称为基类,而新的类称为派生类。派生类表示更专业化的对象组。

C++ 中的继承类型

在 C++ 中,我们有三种类型的继承:公有继承、受保护继承和私有继承。使用公有继承时,派生类的每个对象也是该派生类的基类对象。然而,基类对象不是其派生类的对象。

继承的层次结构

继承关系构成了类层次结构。基类存在于与其派生类的层次关系中。一旦类在继承关系中被使用,它们就会与其他类关联起来。一个类可以成为基类,为其他类提供成员,也可以成为派生类,从其他类继承成员,或者两者兼而有之。

继承关系示例

在继承关系中,基类通常更通用,而派生类通常更具体。每个派生类对象都是其基类的对象,一个基类可以有多个派生类,因此由基类表示的对象集合通常比任何一个派生类表示的对象集合都要大。

类图示例

以下是一个简单的类图示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+-------------------+         +------------------+
| 基类 A | | 派生类 B |
+-------------------+ +------------------+
| | | |
| 属性: int x | | 属性: int y |
| 方法: void func()| | 方法: void func()|
+-------------------+ +------------------+
| |
| 继承 |
+---------------------------+
|
|
+------------------+
| 派生类 C |
+------------------+
| |
| 属性: int z |
| 方法: void func()|
+------------------+

在上面的类图中,类 B 和类 C 继承自基类 A,并且类 C 也继承自类 B

示例:员工类的继承关系

我们以一个公司的工资单应用程序中的员工类型为例,来说明基类和派生类之间的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
#include <string>

using namespace std;

// 基类:提成员工
class CommissionEmployee {
protected:
string firstName;
string lastName;
string socialSecurityNumber;
double grossSales;
double commissionRate;

public:
CommissionEmployee(const string& first, const string& last, const string& ssn,
double sales = 0.0, double rate = 0.0)
: firstName(first), lastName(last), socialSecurityNumber(ssn),
grossSales(sales), commissionRate(rate) {}

// 获取提成员工的工资
double earnings() const {
return grossSales * commissionRate;
}

// 打印提成员工的信息
void print() const {
cout << "Commission employee: " << firstName << ' ' << lastName
<< "\nSocial security number: " << socialSecurityNumber
<< "\nGross sales: " << grossSales
<< "\nCommission rate: " << commissionRate << endl;
}
};

// 派生类:有底薪的提成员工
class BasePlusCommissionEmployee : public CommissionEmployee {
private:
double baseSalary;

public:
BasePlusCommissionEmployee(const string& first, const string& last, const string& ssn,
double sales = 0.0, double rate = 0.0, double salary = 0.0)
: CommissionEmployee(first, last, ssn, sales, rate), baseSalary(salary) {}

// 获取有底薪的提成员工的工资
double earnings() const {
return baseSalary + CommissionEmployee::earnings();
}

// 打印有底薪的提成员工的信息
void print() const {
cout << "Base-salaried " << firstName << ' ' << lastName
<< "\nSocial security number: " << socialSecurityNumber
<< "\nGross sales: " << grossSales
<< "\nCommission rate: " << commissionRate
<< "\nBase salary: " << baseSalary << endl;
}
};

int main() {
// 测试提成员工类
CommissionEmployee emp1("John", "Doe", "123-45-6789", 10000, 0.1);
emp1.print();
cout << "Earnings: $" << emp1.earnings() << endl;

// 测试有底薪的提成员工类
BasePlusCommissionEmployee emp2("Jane", "Smith", "987-65-4321", 5000, 0.05, 300);
emp2.print();
cout << "Earnings: $" << emp2.earnings() << endl;

return 0;
}

在上面的示例中,我们定义了基类 CommissionEmployee 和派生类 BasePlusCommissionEmployee,并进行了测试。基类表示提成员工,派生类表示有底薪的提成员工。

使用受保护数据的注意事项:

  1. 性能影响:
    • 继承受保护的数据成员可以提高性能,因为可以直接访问成员而无需调用设置或获取成员函数。
  2. 使用受保护数据可能引发的严重问题:
    • 直接访问基类受保护数据可能导致:
      • 封装违规:派生类可以直接修改基类数据而无需使用成员函数。
      • 基类与派生类之间紧密耦合:派生类可能依赖于基类实现,这可能使代码变得脆弱且难以维护。
  3. 最佳软件工程实践:
    • 私有数据成员: 将数据成员声明为私有以强制封装。
    • 成员函数操作: 使用成员函数操作私有数据成员。
    • 成员初始化器: 在构造函数中使用成员初始化器来设置私有数据成员。
  4. 对类 CommissionEmployee 的更改:
    • 将数据成员(firstNamelastNamesocialSecurityNumbergrossSalescommissionRate)声明为私有。
    • 在构造函数中使用成员初始化器设置私有数据成员。
    • 提供用于访问私有数据成员的设置器和获取器函数。
  5. 对类 BasePlusCommissionEmployee 的更改:
    • 重新定义成员函数以区分其与以前版本的类。
    • 利用基类成员函数来操作私有数据成员。
    • 示例:
      • earnings 函数计算基本工资加销售佣金的员工的收入,并调用 CommissionEmployeeearnings 函数。
      • print 函数输出基本工资加销售佣金的员工的信息。
  6. 良好的软件工程实践:
    • 代码重用性: 调用基类成员函数而不是重复代码逻辑。
    • 继承用法: 有效地利用继承来隐藏数据并确保一致性。

通过遵循这些实践,构建了一个良好的类层次结构,促进了代码的可重用性、可维护性和封装性。

以下是一些示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 类 CommissionEmployee 的示例代码
class CommissionEmployee {
private:
string firstName;
string lastName;
string socialSecurityNumber;
double grossSales;
double commissionRate;
public:
CommissionEmployee(string first, string last, string ssn, double sales, double rate)
: firstName(first), lastName(last), socialSecurityNumber(ssn), grossSales(sales), commissionRate(rate) {}
void setFirstName(string first) { firstName = first; }
string getFirstName() const { return firstName; }
// 同样的方式实现其他设置器和获取器函数
};

// 类 BasePlusCommissionEmployee 的示例代码
class BasePlusCommissionEmployee : public CommissionEmployee {
private:
double baseSalary;
public:
BasePlusCommissionEmployee(string first, string last, string ssn, double sales, double rate, double salary)
: CommissionEmployee(first, last, ssn, sales, rate), baseSalary(salary) {}
double earnings() const {
return baseSalary + (grossSales * commissionRate);
}
void print() const {
cout << "Name: " << getFirstName() << " " << getLastName() << endl;
cout << "Social Security Number: " << getSocialSecurityNumber() << endl;
cout << "Gross Sales: " << grossSales << endl;
cout << "Commission Rate: " << commissionRate << endl;
cout << "Base Salary: " << baseSalary << endl;
}
};

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

  1. 构造过程:
    • 实例化一个派生类对象会启动一个构造函数调用链,在此链中,派生类的构造函数在执行自己的任务之前,显式(通过基类成员初始化器)或隐式(调用基类的默认构造函数)地调用其直接基类的构造函数。
    • 如果基类又派生自另一个类,则基类的构造函数需要调用层次结构中更高一级的类的构造函数,依次类推。
    • 在此链中,最后一个调用的构造函数是层次结构底部类的构造函数,其实际完成执行的顺序最早。
    • 最派生类的构造函数的体最后执行。
  2. 析构过程:
    • 当销毁一个派生类对象时,程序会调用该对象的析构函数。
    • 这会启动一个析构函数调用链(或级联),其中派生类的析构函数以及直接和间接基类的析构函数以及这些类的成员的析构函数按照构造函数执行的顺序的相反顺序执行。
    • 当调用派生类对象的析构函数时,析构函数会执行其任务,然后调用层次结构中更高一级的基类的析构函数。
    • 这个过程重复直到调用了层次结构顶部的最后一个基类的析构函数,然后对象从内存中删除。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <string>

class Base {
private:
int baseValue;
public:
Base(int value) : baseValue(value) {
std::cout << "Base Constructor. BaseValue: " << baseValue << std::endl;
}

~Base() {
std::cout << "Base Destructor. BaseValue: " << baseValue << std::endl;
}
};

class Intermediate : public Base {
private:
std::string intermediateName;
public:
Intermediate(int value, const std::string& name) : Base(value), intermediateName(name) {
std::cout << "Intermediate Constructor. Name: " << intermediateName << std::endl;
}

~Intermediate() {
std::cout << "Intermediate Destructor. Name: " << intermediateName << std::endl;
}
};

class Derived : public Intermediate {
private:
double derivedValue;
public:
Derived(int value, const std::string& name, double dValue) : Intermediate(value, name), derivedValue(dValue) {
std::cout << "Derived Constructor. DerivedValue: " << derivedValue << std::endl;
}

~Derived() {
std::cout << "Derived Destructor. DerivedValue: " << derivedValue << std::endl;
}
};

int main() {
std::cout << "Creating Derived Object..." << std::endl;
Derived derivedObj(10, "DerivedObject", 3.14);
std::cout << "Derived Object Created." << std::endl;

std::cout << "\nDestroying Derived Object..." << std::endl;
// 当程序执行结束时,对象的析构函数被调用。
return 0;
}

解释:

  1. 定义了三个类:BaseIntermediateDerived,它们分别代表层次结构中的基类、中间类和派生类。
  2. 每个类都有一个构造函数和一个析构函数,用于在对象的创建和销毁时执行相应的操作。
  3. main 函数中,创建了一个 Derived 类对象 derivedObj,实例化过程会启动构造函数调用链。首先调用 Base 类构造函数,然后是 Intermediate 类构造函数,最后是 Derived 类构造函数。每个构造函数都会输出相应的信息。
  4. 然后程序运行结束时,会销毁 derivedObj 对象,析构函数调用链会按照相反的顺序执行,首先调用 Derived 类析构函数,然后是 Intermediate 类析构函数,最后是 Base 类析构函数。每个析构函数都会输出相应的信息。

运行结果:

1
2
3
4
5
6
7
8
9
10
Creating Derived Object...
Base Constructor. BaseValue: 10
Intermediate Constructor. Name: DerivedObject
Derived Constructor. DerivedValue: 3.14
Derived Object Created.

Destroying Derived Object...
Derived Destructor. DerivedValue: 3.14
Intermediate Destructor. Name: DerivedObject
Base Destructor. BaseValue: 10
  1. 构造函数、析构函数和重载的赋值运算符不会被继承:
    • 派生类不会继承基类的构造函数、析构函数和重载的赋值运算符。
    • 然而,派生类的构造函数、析构函数和重载的赋值运算符可以调用基类版本。

下面是一个具体的代码示例,说明派生类的构造函数、析构函数和重载的赋值运算符不会被继承,但可以调用基类版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>

class Base {
public:
Base() {
std::cout << "Base Constructor" << std::endl;
}

Base(const Base& other) {
std::cout << "Base Copy Constructor" << std::endl;
}

~Base() {
std::cout << "Base Destructor" << std::endl;
}

Base& operator=(const Base& other) {
std::cout << "Base Assignment Operator" << std::endl;
return *this;
}
};

class Derived : public Base {
public:
// 派生类的构造函数不会继承基类的构造函数
Derived() {
std::cout << "Derived Constructor" << std::endl;
}

// 派生类的析构函数不会继承基类的析构函数
~Derived() {
std::cout << "Derived Destructor" << std::endl;
}

// 派生类可以调用基类的构造函数
Derived(const Base& other) : Base(other) {
std::cout << "Derived Copy Constructor" << std::endl;
}

// 派生类可以调用基类的重载赋值运算符
Derived& operator=(const Base& other) {
Base::operator=(other);
std::cout << "Derived Assignment Operator" << std::endl;
return *this;
}
};

int main() {
std::cout << "Creating Base Object..." << std::endl;
Base baseObj;
std::cout << "\nCreating Derived Object..." << std::endl;
Derived derivedObj;

std::cout << "\nAssigning Base Object to Derived Object..." << std::endl;
derivedObj = baseObj;

std::cout << "\nEnd of Program." << std::endl;
return 0;
}

解释:

  1. Base 类具有默认构造函数、复制构造函数、析构函数和重载的赋值运算符。
  2. Derived 类继承了 Base 类。
  3. main 函数中创建了 Base 类对象 baseObjDerived 类对象 derivedObj
  4. 创建 Base 类对象时,会调用 Base 类的构造函数,输出 "Base Constructor"。
  5. 创建 Derived 类对象时,会先调用 Base 类的构造函数,然后调用 Derived 类的构造函数,输出 "Base Constructor" 和 "Derived Constructor"。
  6. Base 类对象赋值给 Derived 类对象时,会调用 Base 类的赋值运算符,输出 "Base Assignment Operator" 和 "Derived Assignment Operator"。

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
Creating Base Object...
Base Constructor

Creating Derived Object...
Base Constructor
Derived Constructor

Assigning Base Object to Derived Object...
Base Assignment Operator
Derived Assignment Operator

End of Program.

以上代码示例说明了派生类的构造函数、析构函数和重载的赋值运算符不会继承,但可以调用基类版本。

C++11中的基类构造函数继承:

  1. 继承基类构造函数:
    • 有时,派生类的构造函数只是模仿基类的构造函数。
    • C++11中引入的一个经常被请求的便利功能是能够继承基类的构造函数。
    • 现在可以通过在派生类定义中显式包含形如 using BaseClass::BaseClass; 的using声明来实现这一点。
    • 在这个声明中,BaseClass是基类的名称。
  2. 注意事项:
    • 默认情况下,每个继承的构造函数的访问级别(public、protected或private)与其对应的基类构造函数相同。
    • 默认、拷贝和移动构造函数不会被继承。
    • 如果在基类中通过在原型中放置 = delete 来删除一个构造函数,则派生类中对应的构造函数也会被删除。
    • 如果派生类没有显式定义构造函数,编译器会在派生类中生成默认构造函数,即使它从基类继承了其他构造函数。
    • 如果你在派生类中显式定义一个与基类构造函数具有相同参数列表的构造函数,那么基类构造函数不会被继承。
    • 基类构造函数的默认参数不会被继承,而是由编译器在派生类中生成重载的构造函数。

基类成员的可访问性总结:

基类访问说明 公有继承 保护继承 私有继承
公有成员 公有 保护 私有
保护成员 保护 保护 私有
私有成员 不可见 不可见 不可见

使用保护和私有继承的情况较为罕见,通常使用公有继承。

继承的作用:

当我们使用继承从一个现有类创建一个新类时,新类会继承现有类的数据成员和成员函数。我们可以通过重新定义基类成员和添加额外成员来定制新类,而不需要访问基类的源代码。派生类程序员可以在C++中实现这一点,而不必访问基类的源代码(派生类必须能够链接到基类的目标代码)。

具体笔记:

通过继承,新类可以利用现有类的功能,并在此基础上进行扩展和定制。下面是一个示例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

// 基类
class Base {
public:
void baseFunction() {
std::cout << "Base Function" << std::endl;
}
};

// 派生类,继承自基类
class Derived : public Base {
public:
// 新的成员函数
void derivedFunction() {
std::cout << "Derived Function" << std::endl;
}

// 重新定义基类的成员函数
void baseFunction() {
std::cout << "Redefined Base Function" << std::endl;
}
};

int main() {
Derived derivedObj;

// 调用基类的成员函数
derivedObj.baseFunction();

// 调用派生类的成员函数
derivedObj.derivedFunction();

return 0;
}

代码示例解释:

  • 我们定义了一个基类 Base,其中包含一个成员函数 baseFunction
  • 然后,我们创建了一个派生类 Derived,它继承了基类 Base
  • 在派生类中,我们添加了一个新的成员函数 derivedFunction
  • 我们还重新定义了基类的成员函数 baseFunction,以便在派生类中定制它。
  • main 函数中,我们创建了一个 Derived 类对象 derivedObj,并演示了如何调用基类和派生类的成员函数。

运行结果:

1
2
Redefined Base Function
Derived Function

继承与软件开发的相关知识点:

  1. 开发专有类(Proprietary Classes):
    • 软件开发人员可以开发专有类,用于销售或许可。
    • 用户可以快速从这些库类派生新类,而无需访问专有源代码。
    • 软件开发人员需要提供头文件以及目标代码(object code)。
  2. 类库的重要性:
    • 大量且有用的类库的可用性通过继承实现了最大的软件重用优势。
    • 类库中包含了常见的功能和模块,开发人员可以通过继承和定制来快速构建新的应用程序。
  3. 设计阶段的继承使用:
    • 在面向对象系统的设计阶段,设计师通常确定某些类之间存在密切关联。
    • 设计师应该将共同的属性和行为“分离”出来,并将它们放置在一个基类中,然后使用继承形成派生类。
    • 这种设计方式有助于提高代码的可维护性和灵活性,同时也提高了代码的复用性。
  4. 继承的影响:
    • 创建派生类不会影响其基类的源代码。
    • 继承保持了基类的完整性,使得基类的功能和行为得以保留和使用。

Object-Oriented Programming: Polymorphism

多态性使我们能够“以通用方式编程”而不是“以具体方式编程”。 它使我们能够编写处理属于同一类层次结构的类对象的程序,就好像它们都是该层次结构的基类对象一样。 多态性基于基类指针句柄和基类引用句柄工作,但不基于名称句柄。 依靠每个对象知道如何在对相同函数调用的响应中“做正确的事情”是多态性的关键概念。 发送给各种对象的相同消息具有“多种形式”的结果,因此称为多态性。 通过多态性,我们可以设计和实现易于扩展的系统。 只要新类是程序通常处理的继承层次结构的一部分,就可以很少或几乎不修改程序的一般部分来添加新类。 必须更改的程序部分仅是需要直接了解您将添加到层次结构的新类的部分。

优点:

多态性使您能够处理通用性,并让执行时环境处理具体细节。您可以使各种对象按照其自身特定的行为方式行事,而无需了解它们的类型 - 只要这些对象属于相同的继承层次结构,并且通过一个公共基类指针或一个公共基类引用进行访问。多态性促进了以下方面的发展:

可扩展性(Extensibility):编写用于调用多态行为的软件与发送消息的对象的特定类型无关。因此,新类型的对象可以响应现有消息并纳入到系统中,而无需修改基本系统。只需修改实例化新对象的客户端代码即可适应新类型。

这一系列示例演示了如何使用基类和派生类指针来指向基类和派生类对象,并且如何使用这些指针来调用操作这些对象的成员函数。这些示例的关键概念是展示派生类对象可以被视为基类对象。尽管派生类对象是不同类型的,但编译器允许这样做,因为每个派生类对象都是其基类对象。然而,我们不能将基类对象视为其任何派生类的对象。is-a关系仅适用于从派生类到其直接和间接基类的情况。

在示例中,我们创建了类CommissionEmployee和BasePlusCommissionEmployee的最终版本。

  • 首先,我们将一个基类指针指向一个基类对象,并调用基类的功能。
  • 然后,我们将一个派生类指针指向一个派生类对象,并调用派生类的功能。
  • 接着,我们演示了派生类和基类之间的关系(即继承中的is-a关系),将一个基类指针指向一个派生类对象,并展示了在派生类对象中确实可用的基类功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// CommissionEmployee.h
#ifndef COMMISSIONEMPLOYEE_H
#define COMMISSIONEMPLOYEE_H

#include <iostream>
#include <string>

class CommissionEmployee {
public:
CommissionEmployee(const std::string& first, const std::string& last, const std::string& ssn, double sales = 0.0, double rate = 0.0)
: firstName(first), lastName(last), socialSecurityNumber(ssn) {
setGrossSales(sales);
setCommissionRate(rate);
}

void setFirstName(const std::string& first) { firstName = first; }
std::string getFirstName() const { return firstName; }

void setLastName(const std::string& last) { lastName = last; }
std::string getLastName() const { return lastName; }

void setSocialSecurityNumber(const std::string& ssn) { socialSecurityNumber = ssn; }
std::string getSocialSecurityNumber() const { return socialSecurityNumber; }

void setGrossSales(double sales) { grossSales = (sales < 0.0) ? 0.0 : sales; }
double getGrossSales() const { return grossSales; }

void setCommissionRate(double rate) { commissionRate = (rate > 0.0 && rate < 1.0) ? rate : 0.0; }
double getCommissionRate() const { return commissionRate; }

double earnings() const { return commissionRate * grossSales; }

virtual void print() const {
std::cout << "commission employee: " << firstName << ' ' << lastName
<< "\nsocial security number: " << socialSecurityNumber
<< "\ngross sales: " << grossSales
<< "\ncommission rate: " << commissionRate;
}

private:
std::string firstName;
std::string lastName;
std::string socialSecurityNumber;
double grossSales;
double commissionRate;
};

#endif // COMMISSIONEMPLOYEE_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// BasePlusCommissionEmployee.h
#ifndef BASEPLUSCOMMISSIONEMPLOYEE_H
#define BASEPLUSCOMMISSIONEMPLOYEE_H

#include "CommissionEmployee.h"

class BasePlusCommissionEmployee : public CommissionEmployee {
public:
BasePlusCommissionEmployee(const std::string& first, const std::string& last, const std::string& ssn, double sales = 0.0, double rate = 0.0, double salary = 0.0)
: CommissionEmployee(first, last, ssn, sales, rate), baseSalary(salary) {}

void setBaseSalary(double salary) { baseSalary = (salary < 0.0) ? 0.0 : salary; }
double getBaseSalary() const { return baseSalary; }

double earnings() const override { return getBaseSalary() + CommissionEmployee::earnings(); }

void print() const override {
std::cout << "base-salaried " << CommissionEmployee::getFirstName() << ' ' << CommissionEmployee::getLastName()
<< "\nsocial security number: " << CommissionEmployee::getSocialSecurityNumber()
<< "\ngross sales: " << CommissionEmployee::getGrossSales()
<< "\ncommission rate: " << CommissionEmployee::getCommissionRate()
<< "\nbase salary: " << baseSalary;
}

private:
double baseSalary;
};

#endif // BASEPLUSCOMMISSIONEMPLOYEE_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.cpp
#include <iostream>
#include "CommissionEmployee.h"
#include "BasePlusCommissionEmployee.h"

int main() {
CommissionEmployee commissionEmployee("John", "Doe", "123-45-6789", 5000, .05);
BasePlusCommissionEmployee basePlusCommissionEmployee("Jane", "Smith", "987-65-4321", 3000, .04, 300);

CommissionEmployee* commissionEmployeePtr = &commissionEmployee;
BasePlusCommissionEmployee* basePlusCommissionEmployeePtr = &basePlusCommissionEmployee;

// Aiming a Base-Class Pointer at a Base-Class Object
commissionEmployeePtr->print();

// Aiming a Derived-Class Pointer at a Derived-Class Object
basePlusCommissionEmployeePtr->print();

// Aiming a Base-Class Pointer at a Derived-Class Object
commissionEmployeePtr = &basePlusCommissionEmployee;
commissionEmployeePtr->print();

return 0;
}
  • 将基类指针commissionEmployeePtr指向基类对象commissionEmployee,然后调用print成员函数,这会调用基类CommissionEmployee中定义的print版本。
  • 将派生类指针basePlusCommissionEmployeePtr指向派生类对象basePlusCommissionEmployee,然后调用print成员函数,这会调用派生类BasePlusCommissionEmployee中定义的print版本。
  • 将基类指针commissionEmployeePtr指向派生类对象basePlusCommissionEmployee,然后调用print成员函数,这依然会调用基类CommissionEmployee中定义的print版本,而不是派生类BasePlusCommissionEmployee中的版本。

这些成员函数的调用表明,调用的功能取决于用于调用函数的指针(或引用)的类型,而不是调用成员函数的对象的类型。

如果我们尝试将一个派生类指针指向一个基类对象。具体来说,我们将一个指向BasePlusCommissionEmployee类的指针试图指向一个CommissionEmployee类的对象。

在C++中,指针之间的赋值需要满足类型兼容性的要求。基类指针可以指向派生类对象,因为派生类对象包含了基类的所有成员,但是派生类指针却不能指向基类对象。这是因为派生类可能包含了基类没有的成员或者行为,如果允许将派生类指针指向基类对象,那么在访问派生类特有的成员时会出现问题。

因此,尝试将一个指向基类对象的指针赋值给一个指向派生类的指针会导致编译器报错,提示类型不匹配。这是编译器对类型安全性的一种保护机制,确保在程序中正确使用基类和派生类的关系。

在C++中,通过向下转型(downcasting),可以将基类指针或引用转换为指向派生类的指针或引用,从而访问派生类特有的成员函数或数据成员。下面是一个简单的示例:

假设我们有一个基类 Shape 和一个派生类 Circle,并且 Circle 类有一个特有的成员函数 getRadius()。首先,我们创建一个指向基类对象的指针,并将其指向一个派生类对象。然后,我们使用向下转型将基类指针转换为指向派生类的指针,从而调用派生类的特有函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

// 基类 Shape
class Shape {
public:
virtual void print() const {
std::cout << "Shape\n";
}
};

// 派生类 Circle
class Circle : public Shape {
public:
void print() const override {
std::cout << "Circle\n";
}

void getRadius() const {
std::cout << "Radius: 5\n";
}
};

int main() {
// 创建一个指向基类对象的指针,并将其指向一个派生类对象
Shape* shapePtr = new Circle();

// 向下转型
Circle* circlePtr = dynamic_cast<Circle*>(shapePtr);
if (circlePtr) {
// 调用派生类特有的成员函数
circlePtr->getRadius();
} else {
std::cout << "Failed to cast to Circle\n";
}

delete shapePtr;
return 0;
}

在上面的示例中,我们使用 dynamic_castshapePtr 指针转换为 Circle 类型的指针 circlePtr。然后,我们检查是否成功进行了转型。如果成功,我们就可以使用 circlePtr 指针来调用派生类特有的成员函数 getRadius()。需要注意的是,向下转型只有在基类指针指向的确实是派生类对象时才会成功。

在C++中,虚函数(virtual functions)的使用非常有用。假设我们有一个基类 Shape 和一些派生类,如 CircleTriangleRectangleSquare,每个派生类都可能具有绘制自身的能力,但每个形状的绘制方式可能会有所不同。在一个绘制形状的程序中,我们希望能够将所有形状都当作基类 Shape 的对象来处理。为了实现这一点,我们可以声明基类中的绘制函数为虚函数,并在每个派生类中重写该函数,以实现多态行为。

以下是一个简单示例,展示了如何使用虚函数来实现多态行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

// 基类 Shape
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing a shape\n";
}
};

// 派生类 Circle
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle\n";
}
};

// 派生类 Triangle
class Triangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a triangle\n";
}
};

int main() {
Shape* shapes[2]; // 基类指针数组
shapes[0] = new Circle();
shapes[1] = new Triangle();

// 通过基类指针调用虚函数,实现多态行为
for (int i = 0; i < 2; ++i) {
shapes[i]->draw();
}

// 释放内存
for (int i = 0; i < 2; ++i) {
delete shapes[i];
}

return 0;
}

在上面的示例中,我们通过基类指针数组存储了两个派生类的对象,并使用循环来调用基类的虚函数 draw()。尽管我们只有一个指针类型,但在运行时,程序会根据实际对象的类型来动态选择调用相应的派生类函数,从而实现多态行为。

这种动态绑定或者晚绑定(dynamic binding or late binding)是虚函数的重要特性之一。另外,值得注意的是,如果通过对象名和点运算符来调用虚函数,那么函数调用会在编译时(静态绑定)解析,这时调用的是对象所属类的函数,而不是实现了多态行为。 在C++中,一旦一个函数被声明为虚函数,它将一直保持虚函数的属性,直到继承层次结构的最底层。即使在派生类覆盖该函数时没有显式地声明为虚函数,在继承层次结构中的后续类中,该函数仍然会被视为虚函数。

尽管某些函数由于在类层次结构的更高层次上的声明而隐式成为虚函数,但为了提高程序的清晰度,建议在类层次结构的每个级别都显式声明这些函数为虚函数。

当派生类选择不覆盖基类的虚函数时,派生类会简单地继承基类的虚函数实现。

在C++11中,可以使用 override 关键字来标记派生类中重写的每个虚函数。这样做会强制编译器检查基类是否有与之相同名称和参数列表(即相同签名)的成员函数。如果没有,则编译器会生成一个错误。

示例代码,演示了如何使用 override 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

// 基类 Shape
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing a shape\n";
}
};

// 派生类 Circle
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle\n";
}
};

int main() {
Circle circle;
circle.draw(); // 调用派生类的虚函数

Shape& shapeRef = circle; // 基类引用指向派生类对象
shapeRef.draw(); // 通过基类引用调用虚函数

return 0;
}

在上面的示例中,我们显式地使用了 override 关键字来重写基类 Shape 中的 draw() 函数。这样做可以提高代码的清晰度,并让编译器能够在派生类中进行更严格的检查。

使用 switch 语句来检查对象的字段值以确定对象类型是一种常见的方法。这允许我们区分对象类型,然后针对特定对象执行适当的操作。然而,使用 switch 逻辑会暴露程序面临各种潜在问题。例如,可能会忘记在必要时包含类型测试,或者可能会忘记在 switch 语句中测试所有可能的情况。当通过添加新类型来修改基于 switch 的系统时,可能会忘记在所有相关的 switch 语句中插入新的情况。每次添加或删除一个类都需要修改系统中的每个 switch 语句;追踪这些语句可能会耗费大量时间并且容易出错。

多态编程可以消除对 switch 逻辑的需求。通过使用多态机制执行等效逻辑,可以避免与 switch 逻辑通常相关的各种错误。使用多态的一个有趣后果是,程序呈现出更简化的外观。它们包含更少的分支逻辑和更简单的顺序代码。

抽象类是指那些你不打算实例化任何对象的类。这样的类通常被称为抽象基类,因为它们通常用作继承层次结构中的基类。这些类不能用来实例化对象,因为抽象类是不完整的——派生类必须定义“缺失的部分”。抽象类是其他类可以继承的基类。可以用来实例化对象的类称为具体类。这些类定义了它们声明的每个成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

class Shape {
public:
virtual void draw() const = 0; // pure virtual function
};

class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle\n";
}
};

class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle\n";
}
};

int main() {
Shape* shapePtr = new Circle();
shapePtr->draw(); // Output: Drawing a circle

shapePtr = new Rectangle();
shapePtr->draw(); // Output: Drawing a rectangle

delete shapePtr;
return 0;
}

抽象基类通过声明一个或多个虚函数为“纯虚函数”来定义。纯虚函数的声明中会使用“= 0”,例如 virtual void draw() const = 0;。纯虚函数通常不提供实现,虽然它们可以。每个具体的派生类必须重写所有基类的纯虚函数,否则该派生类也会是抽象的。虚函数和纯虚函数的区别在于,虚函数有一个实现,并且让派生类有选择地覆盖该函数。相比之下,纯虚函数没有实现,要求派生类为了变成具体类而必须重写该函数。纯虚函数在基类没有实现函数的情况下使用,但是你希望所有具体的派生类都实现该函数。

尽管我们不能实例化抽象基类的对象,但我们可以使用抽象基类声明指针和引用,这些指针和引用可以引用从抽象类派生出的任何具体类的对象。程序通常使用这些指针和引用以多态方式操作派生类对象。

纯虚函数是一种在C++中使用的特殊类型的虚函数。它的本质是定义了一个接口或协议,但没有提供具体的实现。换句话说,纯虚函数没有函数体,只有函数的声明,且在声明的结尾处使用 "= 0" 表示。这使得纯虚函数成为抽象基类的一部分,因为它要求任何派生类都必须提供对该函数的具体实现,以便被实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>
#include <vector>

class Employee {
public:
// Constructor
Employee(const std::string& firstName, const std::string& lastName, const std::string& socialSecurityNumber)
: firstName(firstName), lastName(lastName), socialSecurityNumber(socialSecurityNumber) {}

// Virtual destructor
virtual ~Employee() {}

// Set functions
void setFirstName(const std::string& firstName) { this->firstName = firstName; }
void setLastName(const std::string& lastName) { this->lastName = lastName; }
void setSocialSecurityNumber(const std::string& socialSecurityNumber) { this->socialSecurityNumber = socialSecurityNumber; }

// Get functions
std::string getFirstName() const { return firstName; }
std::string getLastName() const { return lastName; }
std::string getSocialSecurityNumber() const { return socialSecurityNumber; }

// Pure virtual function for calculating earnings
virtual double earnings() const = 0;

// Virtual function to print employee information
virtual void print() const {
std::cout << "Employee: " << getFirstName() << ' ' << getLastName() << "\n"
<< "Social Security Number: " << getSocialSecurityNumber() << std::endl;
}

private:
std::string firstName;
std::string lastName;
std::string socialSecurityNumber;
};

// Derived classes for specific types of employees will override the earnings function and possibly the print function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "Employee.h" // Include the header file for the Employee class

// Derived class for salaried employees
class SalariedEmployee : public Employee {
public:
// Constructor
SalariedEmployee(const std::string& firstName, const std::string& lastName, const std::string& socialSecurityNumber, double weeklySalary)
: Employee(firstName, lastName, socialSecurityNumber), weeklySalary(weeklySalary) {}

// Override the earnings function
virtual double earnings() const override {
return weeklySalary;
}

// Override the print function
virtual void print() const override {
std::cout << "Salaried Employee: ";
Employee::print();
std::cout << "Weekly Salary: " << weeklySalary << std::endl;
}

private:
double weeklySalary; // Additional data member for salaried employees
};

// Similar derived classes for other types of employees (e.g., CommissionEmployee, BasePlusCommissionEmployee)

这段代码定义了一个员工类(Employee),其中包含了虚拟函数 earningsprintearnings 函数是一个纯虚拟函数,因此 Employee 类是一个抽象类。派生类(例如 SalariedEmployee)将覆盖这些函数以提供适当的实现。

虚函数表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <iostream>
#include <vector>
#include <string>

// Abstract base class Employee
class Employee {
private:
std::string firstName;
std::string lastName;
std::string socialSecurityNumber;
public:
Employee(const std::string& first, const std::string& last, const std::string& ssn)
: firstName(first), lastName(last), socialSecurityNumber(ssn) {}

// Virtual destructor
virtual ~Employee() {}

// Pure virtual function
virtual double earnings() const = 0;

// Virtual function
virtual void print() const {
std::cout << "Employee: " << firstName << ' ' << lastName << "\nSSN: " << socialSecurityNumber << std::endl;
}
};

// Derived class SalariedEmployee
class SalariedEmployee : public Employee {
private:
double weeklySalary;
public:
SalariedEmployee(const std::string& first, const std::string& last, const std::string& ssn, double salary)
: Employee(first, last, ssn), weeklySalary(salary) {}

// Override earnings function
virtual double earnings() const override {
return weeklySalary;
}

// Override print function
virtual void print() const override {
std::cout << "Salaried Employee: " << firstName << ' ' << lastName << "\nSSN: " << socialSecurityNumber
<< "\nWeekly Salary: " << weeklySalary << std::endl;
}
};

// Derived class CommissionEmployee
class CommissionEmployee : public Employee {
private:
double grossSales;
double commissionRate;
public:
CommissionEmployee(const std::string& first, const std::string& last, const std::string& ssn, double sales, double rate)
: Employee(first, last, ssn), grossSales(sales), commissionRate(rate) {}

// Override earnings function
virtual double earnings() const override {
return grossSales * commissionRate;
}

// Override print function
virtual void print() const override {
std::cout << "Commission Employee: " << firstName << ' ' << lastName << "\nSSN: " << socialSecurityNumber
<< "\nGross Sales: " << grossSales << "\nCommission Rate: " << commissionRate << std::endl;
}
};

int main() {
std::vector<Employee*> employees;

// Add instances of derived classes to the vector
employees.push_back(new SalariedEmployee("John", "Doe", "123-45-6789", 1000.0));
employees.push_back(new CommissionEmployee("Jane", "Smith", "987-65-4321", 5000.0, 0.05));

// Call earnings and print functions for each employee polymorphically
for (const auto& emp : employees) {
std::cout << "Earnings: " << emp->earnings() << std::endl;
emp->print();
std::cout << std::endl;
}

// Cleanup
for (auto& emp : employees) {
delete emp;
}

return 0;
}

虚函数表是一种用于实现多态性、虚函数和动态绑定的内部数据结构。它是为拥有一个或多个虚函数的类构建的。

当一个类有虚函数时,C++编译器会为该类构建一个虚函数表。这个虚函数表存储了该类的虚函数的地址,以便在调用虚函数时动态确定要调用的实际函数。

虚函数表的第一列是指向虚函数的指针。例如,假设有一个Employee类,它有一个纯虚函数earnings()和一个虚函数print()。Employee类的虚函数表的第一个指针通常是空指针,因为纯虚函数没有实现,而第二个指针会指向print()函数的地址。

当一个类的对象被实例化时,编译器会将指向该类虚函数表的指针附加到对象上。这个指针通常位于对象的前面,但并不一定要实现成这样。

在多态性的执行过程中,使用三级指针来实现。第一级指针是虚函数表中的函数指针,用于调用实际的函数。第二级指针是附加到对象上的指向虚函数表的指针。第三级指针是指向接收虚函数调用的对象的句柄。这些句柄也可以是引用。

当我们调用一个虚函数时,编译器会首先确定这是通过一个基类指针进行调用的,并查找该函数在虚函数表中的位置。然后,编译器会生成代码,执行一系列操作,包括选择正确的函数指针,并执行相应的虚函数。

这些操作使得程序能够动态地选择要执行的虚函数,从而实现了多态性和动态绑定。

1
2
3
4
5
6
7
----------------------------------------
| 虚函数表 (vtable) |
----------------------------------------
| | | |
| Ptr1 | Ptr2 | Ptr3 |
| | | |
----------------------------------------

在上面的图表中,我们展示了一个简化的虚函数表示例。每个指针(Ptr1、Ptr2、Ptr3)指向一个虚函数的地址。这些指针是在编译时由编译器生成的。

接下来,让我们看看如何将虚函数表与对象相关联:

1
2
3
4
5
6
7
8
9
----------------------------------------
| 虚函数表指针 (vptr) |
----------------------------------------
| | |
| vptr | |
| | |
----------------------------------------
| 数据成员 |
----------------------------------------

在这个图表中,我们展示了一个对象的内部结构。在对象的开头,有一个指向虚函数表的指针(vptr)。这个指针指向该对象所属类的虚函数表。

最后,让我们看看如何使用虚函数表进行动态绑定:

1
2
3
4
5
6
7
8
9
----------------------------------------
| 虚函数表指针 (vptr) |
----------------------------------------
| | |
| vptr | 对象 |
| | |
----------------------------------------
| 数据成员 |
----------------------------------------

当调用一个虚函数时,编译器首先查找对象的虚函数表指针。然后,它根据对象的实际类型在虚函数表中找到正确的函数指针,并调用相应的虚函数。

这就是虚函数表是如何实现多态性和动态绑定的。通过使用虚函数表,程序能够在运行时动态地确定要调用的函数,从而实现了多态性和动态绑定的特性。

运行时类型信息(RTTI,Runtime Type Information)是一种C++特性,允许程序在运行时获取对象的类型信息。RTTI提供了一种在程序执行期间查询对象类型的机制,这对于实现多态行为以及在处理对象时进行类型检查是非常有用的。

在C++中,可以使用 typeid 运算符和 dynamic_cast 运算符来实现RTTI。

使用 typeid 运算符获取对象的类型信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <typeinfo>

class Base {
public:
virtual ~Base() {}
};

class Derived : public Base {};

int main() {
Base* basePtr = new Derived();

// 使用typeid获取对象的类型信息
const std::type_info& typeInfo = typeid(*basePtr);

// 输出类型信息的名称
std::cout << "Object belongs to type: " << typeInfo.name() << std::endl;

delete basePtr;
return 0;
}

在上面的示例中,我们创建了一个基类 Base 和一个派生类 Derived。然后,我们创建了一个指向 Derived 类对象的基类指针 basePtr。接着,我们使用 typeid 运算符获取 basePtr 指向对象的类型信息,并将其存储在 typeInfo 中。最后,我们通过调用 name() 函数来获取类型信息的名称,并将其打印到控制台上。

使用 dynamic_cast 运算符进行动态类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

class Base {
public:
virtual ~Base() {}
};

class Derived : public Base {};

int main() {
Base* basePtr = new Derived();

// 使用dynamic_cast进行动态类型转换
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

if (derivedPtr) {
std::cout << "Object successfully cast to Derived class." << std::endl;
} else {
std::cout << "Object cannot be cast to Derived class." << std::endl;
}

delete basePtr;
return 0;
}

在上面的示例中,我们首先创建了一个基类 Base 和一个派生类 Derived。然后,我们创建了一个指向 Derived 类对象的基类指针 basePtr。接着,我们使用 dynamic_cast 运算符将 basePtr 动态转换为 Derived* 类型的指针 derivedPtr。如果转换成功,derivedPtr 将指向同一个对象,并且我们会在控制台上输出成功的信息;否则,转换失败,我们将输出相应的信息。

好的,让我用例子重新解释动态联编和静态联编。

静态联编(Static Binding)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

class Base {
public:
void display() {
cout << "Base class display()" << endl;
}
};

class Derived : public Base {
public:
void display() {
cout << "Derived class display()" << endl;
}
};

int main() {
Base obj;
Derived derivedObj;

Base *ptr1 = &obj;
Base *ptr2 = &derivedObj;

// 静态联编,根据指针类型确定调用的函数
ptr1->display(); // 调用 Base 类的 display() 函数
ptr2->display(); // 调用 Base 类的 display() 函数

return 0;
}

在上面的示例中,我们有一个基类 Base 和一个派生类 Derived。当我们通过基类指针调用 display() 函数时,无论指针指向哪种类型的对象,都会调用基类中的 display() 函数。这是因为编译器在编译时根据指针的类型确定调用的函数,这就是静态联编。

动态联编(Dynamic Binding)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

class Base {
public:
virtual void display() {
cout << "Base class display()" << endl;
}
};

class Derived : public Base {
public:
void display() {
cout << "Derived class display()" << endl;
}
};

int main() {
Base obj;
Derived derivedObj;

Base *ptr1 = &obj;
Base *ptr2 = &derivedObj;

// 动态联编,根据对象的实际类型确定调用的函数
ptr1->display(); // 调用 Base 类的 display() 函数
ptr2->display(); // 调用 Derived 类的 display() 函数

return 0;
}

在上面的示例中,我们仍然有相同的基类 Base 和派生类 Derived。但现在,当我们通过基类指针调用 display() 函数时,实际调用的函数取决于指针指向的对象的类型。这是因为在动态联编中,函数调用的实现在运行时根据对象的实际类型确定。


Stream Input/Output

C++使用类型安全的输入输出(I/O)。 每个I/O操作都以一种对数据类型敏感的方式执行。 如果一个I/O函数已经被定义来处理特定的数据类型,那么调用该成员函数来处理该数据类型。 如果实际数据的类型与处理该数据类型的函数不匹配,则编译器会生成一个错误。 因此,不适当的数据无法“潜入”系统。 用户可以通过重载流插入运算符(<<)和流提取运算符(>>)来指定如何对用户定义类型的对象执行I/O操作。

C++ 输入/输出(I/O)流:

  • 流是C++中的字节序列。
  • 输入操作:字节从设备(例如键盘、磁盘驱动器)流向主存储器。
  • 输出操作:字节从主存储器流向设备(例如显示屏、打印机)。
  • 应用程序将含义与字节相关联。

系统I/O机制:

  • 确保字节在设备和内存之间的传输一致可靠。
  • 通常涉及机械运动(例如磁盘旋转、键盘输入)。
  • 传输时间通常远大于处理器内部操作时间,需要精心规划以确保最佳性能。

C++ I/O能力:

  • 低级I/O(未格式化I/O):
    • 指定字节在设备和内存之间传输。
    • 个别字节是感兴趣的项。
    • 提供高速、高容量传输,但不太方便。
  • 高级I/O(格式化I/O):
    • 字节分组为有意义的单位:整数、浮点数、字符、字符串和用户定义类型。
    • 大多数I/O首选此方法,除了高容量文件处理。

C++ 中的字符输入/输出(I/O):

  • 传统的 C++ 流库允许对字符进行输入和输出。
  • 由于一个 char 通常占用一个字节,因此它只能表示一组有限的字符(如 ASCII 字符集中的字符)。
  • 然而,许多语言使用的字母表包含比单字节 char 可表示的字符更多的字符。
  • ASCII 字符集并不提供这些字符;Unicode® 字符集提供了这些字符。
  • Unicode 是一个庞大的国际字符集,代表了世界上大多数“商业可行”的语言、数学符号等等。

C++ 标准流库:

  • C++ 包含标准流库,使开发人员能够构建能够处理 Unicode 字符的 I/O 操作系统。
  • 为此,C++ 包含了类型 wchar_t,它可以存储 2 个字节的 Unicode 字符。
  • C++ 标准还重新设计了传统的 C++ 流类,原来只处理 char 的类模板,现在分别为处理 char 和 wchar_t 类型的字符提供了单独的专门化。
  • 我们使用 char 的专门化。
  • 类型 wchar_t 的大小没有被标准指定。C++11 引入了新的 char16_t 和 char32_t 类型,用于表示具有明确定义大小的 Unicode 字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>

int main() {
// 使用 char 类型的专门化来处理字符
std::cout << "Using char specialization:" << std::endl;
char asciiChar = 'A';
std::cout << "ASCII char: " << asciiChar << std::endl;

// 使用 wchar_t 类型的专门化来处理 Unicode 字符
std::cout << "\nUsing wchar_t specialization:" << std::endl;
wchar_t unicodeChar = L'文'; // 使用 L 前缀来表示宽字符
std::wcout << "Unicode char: " << unicodeChar << std::endl;

// 使用 char16_t 和 char32_t 类型来处理具有明确定义大小的 Unicode 字符
std::cout << "\nUsing char16_t and char32_t:" << std::endl;
char16_t utf16Char = u'𐍈'; // 使用 u 前缀来表示UTF-16字符
char32_t utf32Char = U'𐍈'; // 使用 U 前缀来表示UTF-32字符
std::cout << "UTF-16 char: " << utf16Char << std::endl;
std::cout << "UTF-32 char: " << utf32Char << std::endl;

return 0;
}

这个代码示例演示了如何使用C++标准流库以及处理Unicode字符的各种数据类型。

  • char: 是C++中最基本的字符类型,通常用于表示ASCII字符集中的字符。在示例中,我们使用char来存储和输出ASCII字符'A'。
  • wchar_t: 是一种宽字符类型,用于存储Unicode字符,它通常占用2个字节。在示例中,我们使用wchar_t来存储和输出一个Unicode字符'文'。
  • char16_tchar32_t: 是C++11引入的两种新的字符类型,用于明确表示UTF-16和UTF-32编码的Unicode字符。在示例中,我们使用char16_t来存储和输出一个UTF-16编码的Unicode字符,使用char32_t来存储和输出一个UTF-32编码的Unicode字符。

C++ iostream库概述:

  • C++ iostream库提供了许多I/O功能。
  • 多个头文件包含了该库的接口的部分。
  • 大多数C++程序包含 <iostream> 头文件,它声明了所有流I/O操作所需的基本服务。
  • <iostream>头文件定义了 cin、cout、cerr 和 clog 对象,分别对应于标准输入流、标准输出流、无缓冲标准错误流和带缓冲的标准错误流。
  • 提供了格式化和未格式化的I/O服务。

<iomanip> 头文件:

-<iomanip>头文件声明了一些有用的服务,用于使用所谓的带参数的流操作符(manipulators)执行格式化I/O,如 setw 和 setprecision。

<fstream> 头文件:

-<fstream>头文件声明了用于文件处理的服务。

好的,让我们来深入剖析这些知识点,并给出相应的代码示例。


模板类 basic_istream、basic_ostream 和 basic_iostream:

  • basic_istream: 支持流输入操作。
  • basic_ostream: 支持流输出操作。
  • basic_iostream: 同时支持流输入和流输出操作。
  • 每个模板都有一个预定义的模板专门化,使得 char I/O 成为可能。

typedef 声明:

  • istream: 表示一个 basic_istream<char> 实例,支持 char 输入。
  • ostream: 表示一个 basic_ostream<char> 实例,支持 char 输出。
  • iostream: 表示一个 basic_iostream<char> 实例,同时支持 char 输入和输出。

继承关系:

  • basic_istreambasic_ostream 都通过单继承从基本模板 basic_ios 派生。
  • basic_iostream 通过多继承从 basic_istreambasic_ostream 派生。

cin、cout、cerr 和 clog:

  • cin: 是一个 istream 实例,连接到标准输入设备,通常是键盘。
  • cout: 是一个 ostream 实例,连接到标准输出设备,通常是显示屏。
  • cerr: 是一个 ostream 实例,连接到标准错误设备,通常也是显示屏,但输出是无缓冲的,适合用于立即通知用户有关错误的信息。
  • clog: 是一个 ostream 实例,连接到标准错误设备,输出是带缓冲的。

文件处理模板:

  • 文件处理使用 basic_ifstream(文件输入)、basic_ofstream(文件输出)和 basic_fstream(文件输入输出)模板。
  • 类似于标准流,C++为这些类模板提供了 typedefs。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <fstream>

int main() {
// 文件输入流
std::ifstream inputFile("input.txt");
int number;
inputFile >> number;
std::cout << "Read from file: " << number << std::endl;

// 文件输出流
std::ofstream outputFile("output.txt");
outputFile << "Hello, World!";
std::cout << "Data written to file." << std::endl;

// 文件输入输出流
std::fstream ioFile("data.txt", std::ios::in | std::ios::out);
std::string data;
ioFile >> data;
std::cout << "Read from file: " << data << std::endl;
ioFile << "Adding more data.";
std::cout << "Data appended to file." << std::endl;

return 0;
}

C++中的格式化和未格式化输出:

在C++中,ostream类提供了格式化和未格式化输出的能力。

重载<<操作符用于char*输出:

C++中已经重载了<<操作符,可以将char*输出为以空字符结尾的C风格字符串。要输出地址而不是字符串内容,可以将char*转换为void*

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main() {
char str[] = "Hello, World!";

// 作为字符串输出
std::cout << "字符串输出:" << str << std::endl;

// 作为地址输出
std::cout << "地址输出:" << static_cast<void*>(str) << std::endl;

return 0;
}

这个示例演示了以字符串和地址格式打印char*变量。

使用put成员函数:

C++中的put成员函数可以用于输出单个字符。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main() {
// 输出单个字符 'A'
std::cout.put('A');

// 输出 'A' 后接换行字符
std::cout.put('A').put('\n');

// 输出ASCII值为65的字符,表示为 'A'
std::cout.put(65);

return 0;
}

在这个示例中,使用put函数以各种方式输出字符。可以级联调用put,它还可以接受表示ASCII值的数值表达式。

分析:

  • put成员函数提供了一种输出单个字符的方法,相比<<操作符,更灵活。
  • 由于put返回对ostream对象的引用,因此可以级联调用,允许按顺序执行输出操作。
  • 通过重载<<操作符,C++提供了一种方便的方法来输出C风格字符串,同时还允许直接输出其他数据类型和表达式。

C++中的格式化和未格式化输入功能:

在C++中,istream类提供了格式化和未格式化的输入功能。

流提取操作符 (>>):

  • 流提取操作符 (>>) 通常会跳过输入流中的空白字符(如空格、制表符和换行符);稍后我们将了解如何更改此行为。
  • 每次输入后,流提取操作符返回一个对接收到提取消息的流对象的引用(例如,在表达式cin >> grade中是cin)。
  • 如果将该引用用作条件,则流的重载void*转换操作符函数将隐式调用,以根据最后一次输入操作的成功或失败将引用转换为非空指针值或空指针。
  • 非空指针转换为布尔值true表示成功,空指针转换为布尔值false表示失败。
  • 当尝试读取流的结尾时,流的重载void*转换操作符返回空指针以表示文件结尾。

流对象的状态位:

  • 每个流对象包含一组状态位,用于控制流的状态(如格式化、设置错误状态等)。
  • 这些位由流的重载void*转换操作符使用,以确定是否返回非空指针或空指针。
  • 如果输入错误类型的数据,则流提取会导致设置流的failbit,如果操作失败,则会设置badbit

get成员函数:

  • 无参数的get成员函数从指定的流中输入一个字符(包括空白字符和其他非图形字符,如表示文件结尾的键序列),并将其作为函数调用的返回值。
  • 当在流上遇到文件结尾时,此版本的get返回EOF。

使用成员函数eof、get和put:

下面的程序演示了如何在输入流cin上使用成员函数eofget,以及在输出流cout上使用成员函数put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main() {
int character;

std::cout << "Enter text, end with EOF (Ctrl+Z on Windows, Ctrl+D on Linux/Mac):" << std::endl;

while ((character = std::cin.get()) != EOF) {
std::cout.put(character);
}

if (std::cin.eof()) {
std::cout << "\nEnd of file reached." << std::endl;
} else {
std::cout << "\nInput error!" << std::endl;
}

return 0;
}

get成员函数的其他版本:

  • 带字符引用参数的get成员函数从输入流中输入下一个字符(即使是空白字符)并将其存储在字符参数中。
  • 第三种版本的get接收三个参数:一个字符数组、一个大小限制和一个分隔符(默认值为\n)。这种版本从输入流中读取字符,直到读取到指定的最大字符数减一或读取到分隔符为止。在用作缓冲区的字符数组中插入一个空字符以终止输入字符串。分隔符不放入字符数组中,但仍保留在输入流中。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main() {
char c;
char buffer[100];

// 使用带字符引用参数的get函数
std::cin.get(c);
std::cout << "Character read using get: " << c << std::endl;

// 使用带字符数组、大小限制和分隔符的get函数
std::cin.get(buffer, 100, '\n');
std::cout << "String read using get: " << buffer << std::endl;

return 0;
}

getline成员函数:

  • getline成员函数与第三种版本的get成员函数类似,并在字符数组的行后插入一个空字符。
  • getline函数从流中删除分隔符(即,读取字符并将其丢弃),但不将其存储在字符数组中。

示例:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
char buffer[100];

std::cout << "Enter a line of text:" << std::endl;
std::cin.getline(buffer, 100);
std::cout << "Line read using getline: " << buffer << std::endl;

return 0;
}

C++ 中 istream 类的成员函数 ignoreputbackpeek

ignore 成员函数

  • 功能ignore 成员函数读取并丢弃指定数量的字符(默认是一个字符),或在遇到指定的分隔符时终止(默认是 EOF,当从文件读取时,导致 ignore 跳到文件末尾)。
  • 使用场景:当需要跳过输入流中的某些字符时,ignore 非常有用,例如跳过不需要的分隔符或注释。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main() {
std::cout << "Enter a line of text: ";
std::cin.ignore(5, ' '); // 忽略最多5个字符,直到遇到空格为止

char c;
std::cin.get(c); // 读取下一个字符
std::cout << "First character after ignore: " << c << std::endl;

return 0;
}

putback 成员函数

  • 功能putback 成员函数将由 get 从输入流中获取的前一个字符放回到该流中。
  • 使用场景:对于扫描输入流并查找以特定字符开头的字段的应用程序非常有用。当输入该字符时,应用程序将字符返回到流中,以便字符可以包含在输入数据中。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main() {
std::cout << "Enter a character: ";
char c;
std::cin.get(c); // 读取一个字符
std::cout << "Character read: " << c << std::endl;

std::cin.putback(c); // 将字符放回流中
std::cin.get(c); // 再次读取字符
std::cout << "Character read again after putback: " << c << std::endl;

return 0;
}

peek 成员函数

  • 功能peek 成员函数返回输入流中的下一个字符,但不从流中移除该字符。
  • 使用场景:当需要查看下一个字符但不希望影响流状态时,peek 非常有用。例如,在决定如何处理接下来的输入之前查看下一个字符。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main() {
std::cout << "Enter a character: ";
char c;
c = std::cin.peek(); // 查看下一个字符
std::cout << "Next character (using peek): " << c << std::endl;

std::cin.get(c); // 读取并移除该字符
std::cout << "Character read: " << c << std::endl;

return 0;
}

综合示例

结合使用 ignoreputbackpeek 来处理更复杂的输入流场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

int main() {
std::cout << "Enter a line of text ending with a period: ";
char c;

while (true) {
c = std::cin.peek(); // 查看下一个字符
if (c == '.') {
break; // 如果是句号,则退出循环
}

if (c == ' ') {
std::cin.ignore(); // 如果是空格,则忽略
} else {
std::cin.get(c); // 否则,读取并显示字符
std::cout << c;
}
}

std::cout << "\nEnd of input processing." << std::endl;

return 0;
}

这个综合示例展示了如何使用 ignoreputbackpeek 来处理更复杂的输入流,处理字符的同时忽略空格并检测特定终止符。

类型安全I/O概述

C++ 提供了类型安全的输入输出(I/O)机制,通过重载 <<>> 操作符来处理特定类型的数据项。这种机制确保了输入和输出操作的安全性和正确性,避免了类型错误。

重载的 <<>> 操作符

  • 重载操作符:在C++中,<<>> 操作符被重载以接受特定类型的数据。例如,intdoublecharstring等基本类型。
  • 用户自定义类型:如果用户定义了自己的类型(例如类或结构体),则需要显式重载 <<>> 操作符来支持该类型的输入输出操作。如果未重载这些操作符,编译器会在尝试进行I/O操作时报告错误。

I/O错误处理

  • 错误位:C++标准库提供了多种错误位来表示I/O操作的状态。这些错误位包括:
    • eofbit:表示已到达输入流的末尾。
    • failbit:表示I/O操作失败,例如试图读取非数字字符到整数变量中。
    • badbit:表示I/O操作遇到不可恢复的错误。
    • goodbit:表示没有错误发生(这是默认状态)。
  • 测试错误位:用户可以测试这些错误位来确定I/O操作是否成功。例如,使用成员函数 good()fail()bad()eof() 来检查对应的错误状态。

示例代码

以下是一些示例代码,展示如何重载 <<>> 操作符,如何进行类型安全的I/O操作,以及如何检查I/O错误状态。

重载操作符示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>

// 自定义类型
class Person {
public:
std::string name;
int age;

// 重载输入操作符 >>
friend std::istream& operator>>(std::istream& input, Person& p) {
input >> p.name >> p.age;
return input;
}

// 重载输出操作符 <<
friend std::ostream& operator<<(std::ostream& output, const Person& p) {
output << "Name: " << p.name << ", Age: " << p.age;
return output;
}
};

int main() {
Person person;

// 输入自定义类型
std::cout << "Enter name and age: ";
std::cin >> person;

// 检查输入操作是否成功
if (std::cin.fail()) {
std::cerr << "Error reading input." << std::endl;
} else {
// 输出自定义类型
std::cout << person << std::endl;
}

return 0;
}

测试错误位示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

int main() {
int number;

std::cout << "Enter a number: ";
std::cin >> number;

// 检查错误位
if (std::cin.good()) {
std::cout << "You entered: " << number << std::endl;
} else if (std::cin.eof()) {
std::cerr << "End of file reached." << std::endl;
} else if (std::cin.fail()) {
std::cerr << "Input mismatch. Failed to read an integer." << std::endl;
// 清除错误状态并忽略错误输入
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
} else if (std::cin.bad()) {
std::cerr << "Unrecoverable error occurred during input." << std::endl;
}

return 0;
}

扩展内容:I/O状态管理

清除错误状态

当检测到输入错误时,可以使用 clear() 成员函数清除错误状态,以便继续进行后续的I/O操作。

1
std::cin.clear(); // 清除所有错误位

忽略错误输入

使用 ignore() 成员函数忽略错误输入,避免影响后续的I/O操作。

1
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略当前行中的所有字符,直到遇到换行符

复位输入流

在检测到 failbitbadbit 后,可以通过 clear()ignore() 来复位输入流,从而确保后续输入操作正常进行。

1
2
3
4
if (std::cin.fail()) {
std::cin.clear(); // 清除错误状态
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略错误输入
}

C++中的非格式化输入/输出

在C++中,非格式化输入/输出通过istreamostream类的readwrite成员函数进行。这些函数直接操作字节,不进行任何格式化处理,适用于需要处理原始字节数据的情况。


主要成员函数

read 成员函数

  • 功能:从输入流中读取指定数量的字符到内存中的字符数组。
  • 使用场景:适用于从文件或其他输入流读取原始数据,不进行格式化处理。

write 成员函数

  • 功能:将内存中的字符数组中的指定数量的字符写入输出流。
  • 使用场景:适用于将原始数据写入文件或其他输出流。

gcount 成员函数

  • 功能:返回上一次输入操作读取的字符数。
  • 使用场景:在使用read成员函数后,获取实际读取的字符数。

代码示例

以下代码示例演示了istreamreadgcount成员函数以及ostreamwrite成员函数的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <fstream>

int main() {
// 打开一个文件进行读取
std::ifstream inputFile("input.txt", std::ios::binary);
if (!inputFile) {
std::cerr << "Error opening file for reading." << std::endl;
return 1;
}

// 创建一个字符数组用于存储读取的数据
const int bufferSize = 100;
char buffer[bufferSize];

// 从文件中读取指定数量的字符
inputFile.read(buffer, bufferSize);

// 检查是否读取失败
if (inputFile.fail() && !inputFile.eof()) {
std::cerr << "Error reading file." << std::endl;
}

// 获取实际读取的字符数
std::streamsize bytesRead = inputFile.gcount();
std::cout << "Bytes read: " << bytesRead << std::endl;

// 关闭输入文件
inputFile.close();

// 打开一个文件进行写入
std::ofstream outputFile("output.txt", std::ios::binary);
if (!outputFile) {
std::cerr << "Error opening file for writing." << std::endl;
return 1;
}

// 将读取的数据写入输出文件
outputFile.write(buffer, bytesRead);

// 检查是否写入失败
if (!outputFile) {
std::cerr << "Error writing to file." << std::endl;
}

// 关闭输出文件
outputFile.close();

return 0;
}

扩展内容:非格式化I/O的应用场景

1. 处理二进制文件

非格式化I/O非常适合处理二进制文件(如图片、音频、视频等),因为这些文件的数据不能被解释为文本。

2. 提高性能

由于非格式化I/O不涉及数据转换和格式化处理,相对于格式化I/O,性能更高。

3. 存储和传输原始数据

在网络传输和数据存储中,使用非格式化I/O可以确保数据的准确性和一致性。

错误处理和状态管理

在使用readwrite时,需要注意以下几点:

  • 检查流状态:通过检查failbiteofbitbadbit来判断I/O操作是否成功。
  • 处理部分读取:当读取的字符数少于指定数量时,使用gcount获取实际读取的字符数,并采取适当措施。
1
2
3
4
5
6
7
8
// 处理读取错误
if (inputFile.fail() && !inputFile.eof()) {
std::cerr << "Error reading file." << std::endl;
// 清除错误状态
inputFile.clear();
// 忽略剩余输入
inputFile.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

C++ 流操作符及其格式化任务

C++ 提供了各种流操作符用于执行格式化任务。这些流操作符能够设置字段宽度、精度、格式状态、填充字符、刷新流、在输出流中插入换行符或空字符,以及在输入流中跳过空白字符。

1. 数字的基数设置

整数通常被解释为十进制(基数 10)值。可以使用以下流操作符更改流中的整数基数:

  • hex:将基数设置为十六进制(基数 16)。
  • oct:将基数设置为八进制(基数 8)。
  • dec:将基数重置为十进制(基数 10)。
  • setbase:通过参数设置基数,可以接受 10、8 或 16 作为参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <iomanip>

int main() {
int number = 255;

std::cout << "Decimal: " << number << std::endl;
std::cout << "Hexadecimal: " << std::hex << number << std::endl;
std::cout << "Octal: " << std::oct << number << std::endl;
std::cout << "Back to Decimal: " << std::dec << number << std::endl;

std::cout << "Using setbase manipulator:" << std::endl;
std::cout << std::setbase(16) << number << " (Hex)" << std::endl;
std::cout << std::setbase(8) << number << " (Oct)" << std::endl;
std::cout << std::setbase(10) << number << " (Dec)" << std::endl;

return 0;
}

2. 设置浮点数精度

通过 setprecision 流操作符或 ios_base 类的 precision 成员函数可以控制浮点数的精度(即小数点右侧的位数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <iomanip>
#include <cmath>

int main() {
double value = sqrt(2.0);

std::cout << "Default precision: " << value << std::endl;

for (int i = 0; i <= 9; ++i) {
std::cout << "Precision " << i << ": " << std::setprecision(i) << value << std::endl;
}

return 0;
}

3. 设置字段宽度

通过 ios_base 类的 width 成员函数可以设置字段宽度,或者使用 setw 流操作符。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <iomanip>

int main() {
std::cout << std::setw(10) << "Value" << std::setw(10) << "Square" << std::endl;

for (int i = 1; i <= 5; ++i) {
std::cout << std::setw(10) << i << std::setw(10) << i * i << std::endl;
}

return 0;
}

4. 自定义流操作符

可以创建自己的流操作符。例如,创建一个 bell 操作符,当调用时打印一个铃声字符(\a)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

std::ostream& bell(std::ostream& os) {
return os << '\a';
}

std::ostream& carriageReturn(std::ostream& os) {
return os << '\r';
}

std::ostream& tab(std::ostream& os) {
return os << '\t';
}

std::ostream& endLine(std::ostream& os) {
return os << std::endl;
}

int main() {
std::cout << "Hello" << bell << tab << "world" << carriageReturn << endLine;
return 0;
}

扩展内容

1. 设置填充字符

可以通过 fill 成员函数或 setfill 流操作符设置填充字符。

1
2
3
4
5
6
7
#include <iostream>
#include <iomanip>

int main() {
std::cout << std::setfill('*') << std::setw(10) << 123 << std::endl;
return 0;
}

2. 刷新流

可以使用 flush 操作符强制刷新流。

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "This is a test" << std::flush;
return 0;
}

3. 插入换行符和跳过空白字符

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
char ch;
std::cout << "Enter a character: ";
std::cin >> std::noskipws >> ch; // 禁止跳过空白字符
std::cout << "You entered: " << ch << std::endl;
return 0;
}

确实如此,setw 是唯一一个非粘性的流操纵器,它只影响紧随其后的单个输出操作。下面是详细的笔记和代码示例,以更好地理解 C++ 中的流操纵器及其“粘性”特性。

C++ 流操纵器

C++ 提供了一些流操纵器,用于格式化输入和输出。这些操纵器可以设置字段宽度、精度、填充字符等。有些操纵器是粘性的,这意味着一旦设置,它们会影响后续所有的输出操作,直到明确地重新设置它们或重置为默认值。而 setw 是唯一一个非粘性的操纵器,它只对一次输出操作有效。

粘性流操纵器

以下是一些常见的粘性流操纵器:

  • hex:将流设置为十六进制模式。
  • dec:将流设置为十进制模式。
  • oct:将流设置为八进制模式。
  • setfill:设置填充字符。
  • fixed:将浮点数设置为定点表示。
  • scientific:将浮点数设置为科学计数法表示。
  • setprecision:设置浮点数的精度。
  • leftrightinternal:设置对齐方式。

非粘性流操纵器

唯一的非粘性流操纵器是:

  • setw:设置字段宽度,仅对当前操作有效。

代码示例

下面的代码示例展示了粘性和非粘性流操纵器的使用及其效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <iomanip>

int main() {
int number = 255;

// 默认情况下,数字以十进制格式显示
std::cout << "Default (decimal): " << number << std::endl;

// 设置流为十六进制模式
std::cout << std::hex;
std::cout << "Hexadecimal: " << number << std::endl;

// 流仍然处于十六进制模式
std::cout << "Still Hexadecimal: " << 100 << std::endl;

// 重置流为十进制模式
std::cout << std::dec;
std::cout << "Back to Decimal: " << number << std::endl;

// 设置填充字符为 '*'
std::cout << std::setfill('*');
std::cout << std::setw(10) << number << std::endl;

// 填充字符仍然是 '*'
std::cout << std::setw(10) << 100 << std::endl;

// 使用 setw 仅对当前操作有效
std::cout << std::setw(10) << number << std::endl;
std::cout << number << std::endl; // 不受 setw 影响

// 设置精度为 3
std::cout << std::setprecision(3);
std::cout << 3.14159 << std::endl;

return 0;
}

分析

  1. 粘性流操纵器
    • 一旦设置了 hex,输出就会以十六进制格式显示,直到使用 dec 将其重置为十进制。
    • setfill('*') 设置的填充字符会影响后续的所有 setw 操作,直到再次更改填充字符。
    • setprecision(3) 设置的精度会影响后续所有的浮点数输出,直到重新设置。
  2. 非粘性流操纵器
    • setw(10) 仅对紧随其后的单个输出操作有效,后续的输出操作不会受到影响。

C++ 流操纵器类别

这些操纵器都属于 ios_base 类。

常见的流操纵器

  1. showpoint 和 noshowpoint
    • showpoint:强制输出浮点数时显示小数点和尾随零。
    • noshowpoint:重置 showpoint 设置。
  2. left 和 right
    • left:将字段左对齐,右侧用填充字符填充。
    • right:将字段右对齐,左侧用填充字符填充。
  3. internal
    • internal:将符号(或基数,使用 showbase 时)左对齐,数值部分右对齐,中间用填充字符填充。
  4. fill 和 setfill
    • fill:指定用于对齐字段的填充字符,默认填充字符是空格。
    • setfill:参数化流操纵器,用于设置填充字符。
  5. dec、hex 和 oct
    • dec:指定整数以十进制显示。
    • hex:指定整数以十六进制显示。
    • oct:指定整数以八进制显示。
  6. showbase 和 noshowbase
    • showbase:强制输出整数时显示基数前缀(十进制无前缀,八进制前缀为 0,十六进制前缀为 0x 或 0X)。
    • noshowbase:重置 showbase 设置。
  7. setprecision
    • setprecision:设置浮点数的精度。
  8. setw
    • setw:设置字段宽度(非粘性,只对紧随其后的单个输出操作有效)。

代码示例

以下代码示例展示了各种流操纵器的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
double pi = 3.14159;

// showpoint 和 noshowpoint 示例
cout << "Default: " << pi << endl;
cout << showpoint << "Showpoint: " << pi << endl;
cout << noshowpoint << "No Showpoint: " << pi << endl;

// left 和 right 示例
cout << left << setw(10) << 123 << "left" << endl;
cout << right << setw(10) << 123 << "right" << endl;

// internal 示例
cout << internal << setw(10) << -123 << "internal" << endl;

// fill 和 setfill 示例
cout << setfill('*') << setw(10) << 123 << "setfill" << endl;
cout << setfill(' ') << setw(10) << 123 << "default fill" << endl;

// dec、hex 和 oct 示例
int number = 255;
cout << dec << "Decimal: " << number << endl;
cout << hex << "Hexadecimal: " << number << endl;
cout << oct << "Octal: " << number << endl;

// showbase 和 noshowbase 示例
cout << showbase << hex << "Showbase Hex: " << number << endl;
cout << showbase << oct << "Showbase Oct: " << number << endl;
cout << noshowbase << dec << "No Showbase Dec: " << number << endl;

// setprecision 示例
cout << setprecision(2) << "Set precision: " << pi << endl;

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Default: 3.14159
Showpoint: 3.141590
No Showpoint: 3.14159
123 left
123right
- 123internal
*******123setfill
123default fill
Decimal: 255
Hexadecimal: ff
Octal: 377
Showbase Hex: 0xff
Showbase Oct: 0377
No Showbase Dec: 255
Set precision: 3.14

boolalpha的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用流操作符 boolalpha 来控制输出布尔值的格式。
#include <iostream>
using namespace std;

int main() {
bool booleanValue{true};

// 显示默认的 0/1 输出
cout << "booleanValue is " << booleanValue;

// 使用 boolalpha 将布尔值显示为 true 或 false
cout << boolalpha << "\nbooleanValue is " << booleanValue;

// 将输出流设置为默认的 0/1 格式
cout << noboolalpha << "\nbooleanValue is " << booleanValue << endl;
return 0;
} // end main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 使用成员函数 flags 来保存和恢复输出流的格式状态。
#include <iostream>
using namespace std;

int main() {
bool booleanValue{true};

// 保存原始的格式状态
ios_base::fmtflags originalFormat = cout.flags();

// 显示默认的 true/false 输出
cout << "booleanValue is " << booleanValue;

// 使用 boolalpha 将布尔值显示为 true 或 false
cout << boolalpha << "\nbooleanValue is " << booleanValue;

// 将输出流设置为默认的 0/1 格式
cout << noboolalpha << "\nbooleanValue is " << booleanValue;

// 恢复原始的格式状态
cout.flags(originalFormat);
cout << "\n重置格式状态后,booleanValue 为 "
<< booleanValue << endl;
return 0;
} // end main

解释:

在第一个程序(Fig. 13.20)中,我们演示了如何使用 boolalphanoboolalpha 操纵符来控制布尔值的输出格式。初始时,布尔值 booleanValue 使用默认格式进行输出,然后使用 boolalpha 将其显示为 "true" 或 "false",最后使用 noboolalpha 恢复为默认格式,显示为 0 或 1。

在第二个程序(Fig. 13.21)中,我们展示了如何使用 flags() 成员函数来保存和恢复输出流的原始格式状态。在应用任何操纵之前,我们使用 cout.flags() 来保存原始的格式状态,并将其存储在 originalFormat 中。在执行操纵后,我们使用 cout.flags(originalFormat) 来恢复原始的格式状态,将格式状态设置回其初始设置。

附加说明:

  • C++ 中的流操纵符提供了一种方便的方式来控制输入和输出操作的格式。
  • ios_base 类的 flags() 成员函数允许我们访问和修改输出流的格式标志。
  • 通过保存和恢复原始的格式状态,我们确保后续的输出操作保持所需的格式,而不受先前操纵的意外副作用影响。
  • 理解流操纵符和格式标志对于编写健壮且易于维护的 C++ 代码至关重要,特别是在处理输入/输出操作时。

处理流状态

流状态位

  • eofbit 在输入流遇到文件结束时设置。
  • failbit 当流发生格式错误且没有输入字符时设置。
  • badbit 当发生导致数据丢失的错误时设置。
  • goodbit 如果流中没有设置eofbitfailbitbadbit,则设置为好。

用于测试状态的成员函数

  • eof() 在尝试提取流末尾之外的数据后,确定是否遇到了文件末尾。
  • fail() 报告流操作是否由于格式错误而失败。
  • bad() 报告流操作是否由于严重错误导致数据丢失而失败。
  • good() 如果没有错误位(bad()fail()eof())设置,则返回true。
  • rdstate() 返回流的错误状态。
  • clear() 将流的状态恢复为“好”,以便继续I/O操作。

C++代码示例,演示了如何使用流状态位和成员函数来处理流状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

int main() {
// 创建一个输入流
std::istringstream iss("Hello, World!");

// 读取流中的字符直到遇到文件结束
char ch;
while (iss.get(ch)) {
std::cout << ch; // 输出字符
}

// 检查流的状态并输出相应信息
if (iss.eof()) {
std::cout << "\nReached end of file." << std::endl;
} else if (iss.fail()) {
std::cout << "\nFormat error occurred." << std::endl;
} else if (iss.bad()) {
std::cout << "\nCritical error occurred. Data loss possible." << std::endl;
} else if (iss.good()) {
std::cout << "\nStream is in good state." << std::endl;
}

// 使用rdstate()函数获取流的状态并输出
std::cout << "Stream state: " << iss.rdstate() << std::endl;

// 清除流的状态,使其恢复为“好”
iss.clear();

// 再次检查流的状态并输出
if (iss.good()) {
std::cout << "Stream state reset to good." << std::endl;
}

return 0;
}

此代码片段执行以下操作:

  1. 创建一个字符串流iss,其中包含字符串"Hello, World!"。
  2. 使用get()函数逐个字符读取流中的字符,并将它们输出到标准输出。
  3. 使用成员函数eof()fail()bad()good()检查流的状态,并根据状态输出相应的信息。
  4. 使用rdstate()函数获取流的状态并输出。
  5. 使用clear()函数清除流的状态,使其恢复为“好”状态。
  6. 再次检查流的状态并输出。

流状态测试的成员函数

  • operator! 如果设置了badbitfailbit,则返回true。
  • operator void * 如果设置了badbitfailbit,则返回false。

在文件处理中的使用

这些函数在文件处理循环或条件中测试真/假条件时非常有用。


同步输入和输出流

使用cin.tie()

  • 目的: 确保提示在输入操作之前出现。
  • 用法: cin.tie(&cout);
  • 效果:cout(一个ostream)与cin(一个istream)绑定,同步它们的操作。

示例用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main() {
// 将cin和cout绑定在一起,以实现输入/输出同步
std::cin.tie(&std::cout);

// 提示消息
std::cout << "请输入一个数字:";

// 输入操作
int num;
std::cin >> num;

// 输出输入的数字
std::cout << "你输入的数字是:" << num << std::endl;

return 0;
}

缓冲和绑定机制

输出缓冲

输出缓冲是指系统将输出数据暂时存储在内存中,直到满足某些条件后才将其写入到输出设备(如显示器或文件)。这样做的好处是可以提高程序的性能,减少对IO设备的访问次数,从而提高效率。

在 C++ 中,设置缓冲区对于提高程序性能和资源管理具有重要意义。缓冲区是一块用于临时存储数据的内存区域,常用于 I/O 操作中。以下是缓冲区在 C++ 中的主要用途和意义:

1. 提高 I/O 操作效率

I/O 操作(例如文件读写、网络通信)通常比内存操作慢得多。缓冲区通过减少 I/O 操作的频率来提高效率。例如,当从文件中读取数据时,将数据块读入缓冲区,而不是每次读取一个字节。这样,程序可以在内存中处理数据,而不是频繁地与外部设备交互。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <fstream>

int main() {
std::ifstream file("example.txt");

// 设置缓冲区
const size_t bufferSize = 1024;
char buffer[bufferSize];
file.rdbuf()->pubsetbuf(buffer, bufferSize);

std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}

file.close();
return 0;
}

2. 减少系统调用次数

每次 I/O 操作都会涉及系统调用,这通常是昂贵的操作。通过使用缓冲区,可以一次性执行较大块的数据传输,从而减少系统调用的次数,提高性能。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cstdio>

int main() {
FILE* file = fopen("example.txt", "r");

// 设置缓冲区
const size_t bufferSize = 1024;
char buffer[bufferSize];
setvbuf(file, buffer, _IOFBF, bufferSize); // 全缓冲

char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}

fclose(file);
return 0;
}

3. 控制输出行为

使用缓冲区可以控制数据何时实际输出到目标设备。例如,标准输出(stdout)默认是行缓冲模式,即每当遇到换行符时,缓冲区的内容才会被刷新到显示设备。通过手动刷新缓冲区,可以更灵活地控制输出行为。

示例:

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "Hello, world!" << std::flush; // 手动刷新缓冲区
return 0;
}

4. 提高多线程程序的性能

在多线程程序中,缓冲区可以减少线程间的资源争用。例如,每个线程可以有自己的缓冲区,从而避免频繁地锁定共享资源。

5. 提供数据的临时存储

缓冲区可以用于存储临时数据,特别是在数据需要在不同函数或模块之间传递时。例如,在网络编程中,接收到的数据通常先存入缓冲区,然后再进行处理。

总结

设置缓冲区在 C++ 中具有以下主要意义: - 提高 I/O 操作效率。 - 减少系统调用次数。 - 控制输出行为。 - 提高多线程程序的性能。 - 提供数据的临时存储。

通过合理使用缓冲区,可以显著提高程序的性能和响应速度,同时增强对资源的控制和管理。

  • 工作原理: 当使用cout进行输出时,输出的数据首先被存储在内存中的输出缓冲区中,直到缓冲区被填满或者手动刷新(使用flush函数),才会将数据写入到输出设备。

  • 刷新缓冲区: 输出缓冲区在以下情况下会被刷新:

    1. 缓冲区被填满。
    2. 调用endl插入符或者flush函数。
    3. 程序正常结束。

绑定机制

绑定机制用于确保输出操作发生在输入操作之前,尤其是在需要用户输入数据时。这样做的目的是为了在用户看到输出并作出相应响应之前先向其展示提示信息或其他输出。

  • cin.tie()函数: 用于将一个输出流(通常是cout)绑定到一个输入流(通常是cin)上,以确保在输入操作之前先刷新输出流的缓冲区。

  • 作用: 绑定机制确保在程序提示用户输入之前,所有之前的输出都已经被显示,避免了输出被延迟到用户输入后才显示的情况。

代码示例

下面是一个使用绑定机制的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main() {
// 将cout与cin绑定,确保输出在输入之前显示
std::cin.tie(&std::cout);

// 输出提示信息
std::cout << "请输入您的名字:";

// 接收用户输入
std::string name;
std::cin >> name;

// 输出用户输入的名字
std::cout << "你好," << name << "!" << std::endl;

return 0;
}

在这个示例中,使用cin.tie(&cout)函数将输出流cout绑定到输入流cin上,这样在等待用户输入之前,输出提示信息就会被立即显示出来。


C++ How to Program

内存中的数据存储是临时的。 文件用于数据持久性——数据的永久保留。 计算机将文件存储在二级存储设备上,如硬盘、CD、DVD、闪存驱动器和磁带。

文件处理基础概念

  • C++将每个文件视为一系列字节(图14.1)。
  • 每个文件要么以文件结束标记结束,要么在操作系统维护的特定字节号处结束。
  • 打开文件时,会创建一个对象,并将流与该对象关联。
  • 我们看到当包含 <iostream>时,会创建对象 cin、cout、cerr 和 clog。
  • 与这些对象关联的流提供了程序与特定文件或设备之间的通信渠道。

文件处理类模板

  • 要在C++中进行文件处理,必须包含头文件<iostream><fstream>
  • 头文件 <fstream> 包含了流类模板 basic_ifstream(用于文件输入)、basic_ofstream(用于文件输出)和 basic_fstream(用于文件输入和输出)的定义。
  • 每个类模板都有一个预定义的模板特化,使得可以进行 char 类型的 I/O。

<fstream> 库提供了这些模板特化的 typedef 别名。

  • typedef ifstream 表示 basic_ifstream 的特化,使得可以从文件进行 char 类型的输入。
  • typedef ofstream 表示 basic_ofstream 的特化,使得可以向文件进行 char 类型的输出。
  • typedef fstream 表示 basic_fstream 的特化,使得可以进行 char 类型的输入和输出。

这些模板继承自类模板 basic_istream、basic_ostream 和 basic_iostream。

  • 因此,所有属于这些模板的成员函数、操作符和操作符操纵符(我们在第13章中描述过)也可以应用于文件流。
  • 图14.2 总结了我们到目前为止讨论的 I/O 类的继承关系。
  • 文件处理在C++中是非常重要的,它允许程序与外部存储设备(如硬盘)进行交互,从而实现数据的永久存储和读取。
  • 通过文件处理类模板,我们可以轻松地进行文件的输入和输出操作,使得程序可以读取和修改文件中的数据。
  • 在进行文件处理时,务必注意文件的打开和关闭操作,以确保文件资源被正确释放,避免资源泄漏和文件损坏的情况发生。
  • 另外,在处理文件时,还需要考虑文件的格式化和解析问题,以确保数据的正确读取和写入。
  • 文件处理是C++编程中的重要部分,它为程序提供了与外部环境进行数据交换的能力,是编写高效、实用的程序的关键之一。

代码示例

下面是一个简单的C++代码示例,演示了如何使用文件流进行文件的输入和输出操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <fstream>

int main() {
// 打开一个名为 "data.txt" 的文件,用于写入数据
std::ofstream outputFile("data.txt");

// 写入数据到文件
outputFile << "Hello, World!\n";
outputFile << "This is a file.\n";
outputFile << "File processing in C++ is awesome!\n";

// 关闭文件
outputFile.close();

// 打开刚刚创建的文件,用于读取数据
std::ifstream inputFile("data.txt");

// 从文件读取数据,并输出到标准输出
std::string line;
while (std::getline(inputFile, line)) {
std::cout << line << std::endl;
}

// 关闭文件
inputFile.close();

return 0;
}

这个程序的功能是:创建一个名为 "data.txt" 的文件,将数据写入其中,然后再从该文件中读取数据并输出到标准输出。

C++ 中的文件结构:

C++ 并不会对文件施加任何固定的结构。开发者需要根据应用程序的需求来组织文件。

顺序文件创建: 可以创建顺序文件来管理数据,例如在应收账款系统中。文件中的每个记录代表有关客户的信息,通常包括账号、姓名和余额。

记录概念: 在 C++ 中,并没有对文件中的“记录”进行明确的定义。但在实际中,可以将记录视为一组相关的数据项,在文件中通常以特定格式组织。

C++ 中的文件打开: 要向文件中写入数据,在 C++ 中可以创建一个 ofstream 对象。该对象的构造函数接受两个参数:文件名和文件打开模式。文件打开模式可以是 ios::out(默认)用于向文件输出数据,或者 ios::app 用于将数据附加到现有文件中。

文件打开模式: 文件打开模式包括 ios::out 用于输出和 ios::app 用于附加。在 C++11 之前,文件名是指定为基于指针的字符串,但自 C++11 起,也可以指定为字符串对象。

使用 open 成员函数打开文件: 也可以使用 ofstream 对象的 open 成员函数打开文件。这允许将文件附加到现有的 ofstream 对象上。例如:

1
2
ofstream outClientFile;
outClientFile.open("clients.dat", ios::out);

检查文件打开成功: 在尝试打开文件后,有必要验证操作是否成功。可以使用 ios 的重载 ! 运算符来检查 failbitbadbit。如果其中任一位被设置,表示文件打开操作失败。

错误处理和程序终止: 如果文件打开操作失败或出现其他错误,程序可以使用 exit 函数终止。传递 EXIT_FAILURE 表示错误,而传递 EXIT_SUCCESS 表示成功终止。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <fstream>
#include <cstdlib> // 用于退出状态宏

using namespace std;

int main() {
// 打开输出文件
ofstream outFile("clients.dat", ios::out);

// 检查文件是否成功打开
if (!outFile) {
cerr << "打开文件时发生错误。" << endl;
exit(EXIT_FAILURE);
}

// 文件操作...

// 关闭文件
outFile.close();

return 0;
}

重载的 void * 运算符:

  • operator void *ios 类的一个成员函数,用于将流对象转换为指针。
  • 可以将流对象用作条件,C++ 将空指针解释为 false,非空指针解释为 true
  • 如果流的 failbitbadbit 被设置,operator void * 返回 0,否则返回非零值。
  • while 循环等条件语句中,可以隐式调用 operator void *,例如 while (cin)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
int num;

// 输入数据,直到遇到文件结束符
while (std::cin >> num) {
// 处理输入的数据
std::cout << "输入的数字是:" << num << std::endl;
}

// 到达文件结束时的处理
if (std::cin.eof()) {
std::cout << "已达到文件结束。" << std::endl;
} else {
std::cout << "输入错误或发生其他问题。" << std::endl;
}

return 0;
}

解释:

  • while (std::cin >> num) 循环中,std::cin >> num 表达式会调用 std::istream 类的 operator void * 成员函数,将输入流对象 std::cin 转换为指针。
  • 如果输入操作成功,则表达式返回非零值(true),继续循环;如果遇到文件结束符或输入错误,则返回 0false),循环结束。
  • std::cin.eof() 可以进一步检查是否达到了文件结束。

数据处理:

  • 可以使用 operator void * 来测试输入对象是否到达文件结束,也可以调用成员函数 eof()
  • 不同计算机系统有不同的键盘组合来输入文件结束符。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
char ch;

// 输入数据,直到遇到文件结束符
while (std::cin.get(ch)) {
// 处理输入的字符
std::cout << "输入的字符是:" << ch << std::endl;
}

// 到达文件结束时的处理
if (std::cin.eof()) {
std::cout << "已达到文件结束。" << std::endl;
} else {
std::cout << "输入错误或发生其他问题。" << std::endl;
}

return 0;
}

解释:

  • while (std::cin.get(ch)) 循环中,std::cin.get(ch) 表达式会调用 std::istream 类的 operator void * 成员函数,将输入流对象 std::cin 转换为指针。
  • 如果输入操作成功,则表达式返回非零值(true),继续循环;如果遇到文件结束符或输入错误,则返回 0false),循环结束。
  • std::cin.eof() 可以进一步检查是否达到了文件结束。

数据处理:

  • 当遇到文件结束符或错误数据时,operator void * 返回空指针(转换为布尔值 false),导致循环终止。
  • 用户输入文件结束符来通知程序停止处理额外数据。
  • 文件结束指示符会在用户输入文件结束键组合时被设置。

文件写入:

  • 使用流插入运算符 << 可以将数据写入文件。
  • 关联的输出文件流对象(如 outClientFile)可用于向文件中写入数据。

文件查看:

  • 创建的文件是一个文本文件,可以使用任何文本编辑器查看。

关闭文件:

  • 当程序终止时,文件流对象的析构函数会被隐式调用,关闭文件。
  • 也可以显式地关闭文件流对象,使用成员函数 close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <fstream>

int main() {
int data;

// 打开输出文件
std::ofstream outFile("clients.txt");

// 写入数据到文件,直到遇到文件结束符或错误数据
while (std::cin >> data) {
outFile << data << std::endl;
}

// 文件结束时的处理
if (std::cin.eof()) {
std::cout << "文件结束或输入错误数据。" << std::endl;
} else {
std::cout << "未知错误发生。" << std::endl;
}

// 关闭文件
outFile.close();

return 0;
}

解释:

  • 这个程序从标准输入(键盘)读取整数数据,并将其写入名为 clients.txt 的文件中。
  • 循环会在遇到文件结束符或错误数据时终止。
  • 当文件结束时,输出相应的消息。
  • 最后,关闭输出文件流对象,确保数据被正确写入文件并且文件被关闭。

读文件的基本步骤?

好的,以下是带有代码示例的总结:

  1. 文件处理基础

    • 使用ifstream从文件中读取数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <fstream>
    using namespace std;

    int main() {
    ifstream inFile("clients.txt"); // 打开文件以供读取
    // 从文件中读取数据
    // 当`inFile`超出作用域时,文件会自动关闭
    return 0;
    }
  2. 打开文件以供输入

    • 使用文件名和打开模式创建ifstream对象。
    1
    2
    3
    ifstream inFile("clients.txt"); // 打开文件以供读取
    // 从文件中读取数据
    inFile.close(); // 完成后关闭文件
  3. 从文件中读取数据

    • 使用循环读取数据,直到到达文件末尾。
    1
    2
    3
    4
    5
    6
    7
    8
    ifstream inFile("clients.txt");
    int account;
    string name;
    double balance;
    while (inFile >> account >> name >> balance) {
    // 处理数据
    }
    inFile.close();
  4. 文件位置指针

    • 每个istream对象都有一个读取指针,指示下一次输入将发生的文件中的字节号,而每个ostream对象都有一个写入指针,指示下一次输出应该放置的文件中的字节号。
    1
    inClientFile.seekg(0); // 将文件位置指针重新定位到文件的开头(位置0)

    指定的seekg参数

    • seekg函数通常接受一个long整数作为参数。可以指定第二个参数以指示搜索方向。
    1
    2
    3
    4
    fileObject.seekg(n); // 定位到文件对象的第n个字节(假设`ios::beg`)
    fileObject.seekg(n, ios::cur); // 在文件对象中向前移动n个字节
    fileObject.seekg(n, ios::end); // 从文件对象末尾向后移动n个字节
    fileObject.seekg(0, ios::end); // 定位到文件对象的末尾

成员函数tellgtellp

  • 提供了tellgtellp成员函数来分别返回读取和写入指针的当前位置。 tellgtellp成员函数分别用于返回读取和写入指针的当前位置。下面是它们的简单示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <fstream>
using namespace std;

int main() {
ifstream inFile("example.txt");
if (!inFile) {
cerr << "Failed to open the file." << endl;
return 1;
}

// 获取读取指针的当前位置
streampos readPos = inFile.tellg();
cout << "Current read position: " << readPos << endl;

// 假设在文件中读取一些数据后
string data;
inFile >> data;

// 再次获取读取指针的当前位置
readPos = inFile.tellg();
cout << "Updated read position: " << readPos << endl;

inFile.close();

ofstream outFile("output.txt");
if (!outFile) {
cerr << "Failed to create the file." << endl;
return 1;
}

// 获取写入指针的当前位置
streampos writePos = outFile.tellp();
cout << "Current write position: " << writePos << endl;

// 假设写入一些数据到文件后
outFile << "Hello, World!";

// 再次获取写入指针的当前位置
writePos = outFile.tellp();
cout << "Updated write position: " << writePos << endl;

outFile.close();

return 0;
}

不可修改的问题

在序列文件中,通过格式化和写入的数据是不可修改的,否则会破坏文件中的其他数据。比如,如果需要将名字“White”改为“Worthington”,则无法直接覆盖原有数据而不破坏文件。

原始记录为:

1
300 White 0.00

如果直接在文件中相同位置上重写记录,并使用更长的名字,则新记录将会是:

1
300 Worthington 0.00

新记录比原记录多了六个字符。因此,超出“Worthington”中第二个“o”后的字符将覆盖文件中下一个顺序记录的开始部分。

格式化输入/输出模型的问题

使用流插入运算符 << 和流提取运算符 >> 的格式化输入/输出模型中,字段(因此也是记录)的大小是可变的。例如,值为7、14、-117、2074和27383的整数都是 int 类型,内部存储的“原始数据”字节数是相同的(在32位机器上通常为四个字节,在64位机器上为八个字节)。

然而,当这些整数以格式化文本(字符序列)的形式输出时,它们变成了不同大小的字段,取决于它们的实际值。因此,格式化输入/输出模型通常不用于原地更新记录。这样的更新通常会很麻烦。

例如,要进行前述姓名更改,可以将300 White 0.00之前的记录复制到一个新文件中,然后将更新后的记录写入新文件,最后将300 White 0.00之后的记录复制到新文件中。然后删除旧文件并重命名新文件。

这种方法需要处理文件中的每个记录来更新一个记录。但是,如果在一次文件扫描中要更新许多记录,这种技术是可以接受的。

在格式化输入/输出模型中,由于字段大小的可变性,对记录进行原地更新是有挑战性的。通常,更好的方法是通过复制、更新和重写文件来实现更新操作。这虽然可能需要处理文件中的所有记录,但在一次文件扫描中更新多个记录时,这种方法是可行的。

随机访问文件

顺序文件不适合需要立即访问特定记录的应用程序,比如航空订票系统、银行系统、销售点系统、自动取款机以及其他需要快速访问特定数据的交易处理系统。

随机访问文件的单个记录可以直接(并快速地)访问,而无需搜索其他记录。在C++中,文件的结构不是强制的。因此,想要使用随机访问文件的应用程序必须自己创建它们。

创建一个随机访问文件并向其写入记录,可以像打开一个流一样简单。首先,需要指定文件的打开模式为输出模式(ios::out),并使用二进制模式(ios::binary)打开文件,以确保不对记录进行任何格式化。

接下来,可以定义记录的结构,并使用 fstream 类创建一个文件对象。将记录写入文件时,需要使用 write 函数,并将记录的地址强制转换为字符指针,并指定写入的字节数(即记录的大小)。

随机访问文件允许我们从文件中的任何位置读取记录。为了随机读取记录,我们需要使用 seekg 函数来移动文件指针到所需记录的位置。然后使用 read 函数来读取记录的内容。在读取时,需要将记录的地址强制转换为字符指针,并指定要读取的字节数(即记录的大小)。

下面是一个示例代码,演示了如何创建、写入和读取固定长度的记录,并且可以在文件中随机访问记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <fstream>

using namespace std;

// 定义固定长度的记录结构
struct Record {
char name[30];
int age;
double salary;
};

int main() {
// 创建一个随机访问文件并写入记录
fstream file("records.dat", ios::out | ios::binary);
if (!file) {
cerr << "Error creating file!" << endl;
return 1;
}

Record rec1 = {"John Smith", 30, 50000.00};
Record rec2 = {"Jane Doe", 25, 60000.00};

file.write(reinterpret_cast<char*>(&rec1), sizeof(Record));
file.write(reinterpret_cast<char*>(&rec2), sizeof(Record));

file.close();

// 从文件中随机读取记录
file.open("records.dat", ios::in | ios::binary);
if (!file) {
cerr << "Error opening file!" << endl;
return 1;
}

Record readRec;

// 移动指针到第二个记录的位置
file.seekg(sizeof(Record), ios::beg);
file.read(reinterpret_cast<char*>(&readRec), sizeof(Record));

cout << "Name: " << readRec.name << endl;
cout << "Age: " << readRec.age << endl;
cout << "Salary: " << readRec.salary << endl;

file.close();

return 0;
}

在这个示例中,我们首先创建了一个随机访问文件,并向其写入了两个记录。然后,我们随机读取了第二个记录,并打印了其内容。

write 函数的使用:write 函数用于向文件中写入固定数量的字节,从指定的内存位置开始。当文件与流相关联时,write 函数将数据写入文件中指定的位置,位置由文件指针决定。

read 函数的使用:read 函数用于从指定的流中读取固定数量的字节到内存中的指定位置。当文件与流相关联时,read 函数将从文件中指定的位置读取字节,位置也由文件指针决定。

reinterpret_cast 操作符的作用:大多数情况下,我们将不同类型的指针传递给 write 函数作为第一个参数,这些指针不是 const char * 类型的。为了确保编译器能够编译 write 函数的调用,我们需要将这些指针转换为 const char * 类型。这时就需要使用 reinterpret_cast 操作符进行指针类型的转换。该操作符在编译时执行,不会改变其操作数所指向的对象的值。

随机访问文件:在示例中,我们打开了一个二进制文件,并使用 write 函数将数据写入其中。通过移动文件指针,我们可以在文件中定位到指定位置,并在该位置读取或写入数据,这就是随机访问文件的基本原理。

面向对象的输入/输出风格

在面向对象的编程中,输入和输出的风格与传统的基于流的输入/输出不同。对象的成员函数不直接与对象的数据进行输入和输出,而是由类的成员函数的一个共享副本来处理这些操作。

当对象的数据成员被输出到磁盘文件时,我们失去了对象的类型信息。在文件中,我们只存储对象属性的值,而不包含对象的类型信息。如果读取数据的程序知道数据对应的对象类型,那么它可以将数据读取到相应类型的对象中。

对象序列化

在存储不同类型的对象时,我们面临一个问题:如何在读取时区分它们?由于对象通常不包含类型字段,因此我们需要一种方法来保存对象的类型信息。

对象序列化是一种常见的解决方案。序列化对象是对象的字节序列,包括对象的数据以及关于对象类型和数据类型的信息。将序列化对象写入文件后,可以从文件中读取并反序列化,以重新创建对象在内存中的表示。

如何系列化?

对象序列化是将对象转换为字节序列的过程,以便将其保存到文件或通过网络进行传输。在C++中,可以使用一些技术来实现对象的序列化,其中最常见的方法是通过重载流操作符 (<<>>) 或者使用序列化库(如Boost.Serialization)。

下面是一个简单的示例,演示了如何使用流操作符来序列化和反序列化一个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <fstream>
#include <string>

class Person {
private:
std::string name;
int age;

friend std::ostream& operator<<(std::ostream& os, const Person& person);
friend std::istream& operator>>(std::istream& is, Person& person);

public:
Person(const std::string& n, int a) : name(n), age(a) {}

// Getter methods
std::string getName() const { return name; }
int getAge() const { return age; }
};

std::ostream& operator<<(std::ostream& os, const Person& person) {
os << person.name << " " << person.age << " ";
return os;
}

std::istream& operator>>(std::istream& is, Person& person) {
is >> person.name >> person.age;
return is;
}

int main() {
// 创建对象
Person p1("Alice", 30);
Person p2("Bob", 25);

// 序列化对象到文件
std::ofstream outFile("people.txt");
outFile << p1 << p2;
outFile.close();

// 从文件中反序列化对象
std::ifstream inFile("people.txt");
Person p3("", 0), p4("", 0);
inFile >> p3 >> p4;
inFile.close();

// 输出反序列化后的对象
std::cout << "Name: " << p3.getName() << ", Age: " << p3.getAge() << std::endl;
std::cout << "Name: " << p4.getName() << ", Age: " << p4.getAge() << std::endl;

return 0;
}

在这个示例中,我们定义了一个 Person 类来表示人员对象,然后重载了流操作符 <<>> 来实现对象的序列化和反序列化。通过将对象写入文件,然后再从文件中读取并重新创建对象,我们实现了对象的序列化和反序列化过程。


Standard Library Containers and Iterators

容器、迭代器和算法是C++标准库的核心组件。本章介绍了这些组件,并解释了它们在实现常见数据结构和处理算法中的作用。

顺序容器概述(Overview of the Sequential Containers)

类型 特性
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入或删除速度很快
list 双向链表。只支持双向顺序访问。在任何位置插入或删除速度都很快
forward_list 单向链表。只支持单向顺序访问。在任何位置插入或删除速度都很快
array 固定大小数组。支持快速随机访问。不能添加或删除元素
string 类似vector,但用于保存字符。支持快速随机访问。在尾部插入或删除速度很快

forward_listarray是C++11新增类型。与内置数组相比,array更安全易用。forward_list没有size操作。

容器选择原则:

  • 除非有合适的理由选择其他容器,否则应该使用vector
  • 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用listforward_list
  • 如果程序要求随机访问容器元素,则应该使用vectordeque
  • 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用deque
  • 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
    • 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向vector追加数据,再调用标准库的sort函数重排元素,从而避免在中间位置添加元素。
    • 如果必须在中间位置插入元素,可以在输入阶段使用list。输入完成后将list中的内容拷贝到vector中。
  • 不确定应该使用哪种容器时,可以先只使用vectorlist的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vectorlist都很方便。

容器库概览(Container Library Overview)

每个容器都定义在一个头文件中,文件名与类型名相同。容器均为模板类型。

迭代器(Iterators)

forward_list类型不支持递减运算符--

一个迭代器范围(iterator range)由一对迭代器表示。这两个迭代器通常被称为beginend,分别指向同一个容器中的元素或尾后地址。end迭代器不会指向范围中的最后一个元素,而是指向尾元素之后的位置。这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为[begin,end)。迭代器beginend必须指向相同的容器,end可以与begin指向相同的位置,但不能指向begin之前的位置(由程序员确保)。

假定beginend构成一个合法的迭代器范围,则:

  • 如果begin等于end,则范围为空。
  • 如果begin不等于end,则范围内至少包含一个元素,且begin指向该范围内的第一个元素。
  • 可以递增begin若干次,令begin等于end
1
2
3
4
5
while (begin != end)
{
*begin = val; // ok: range isn't empty so begin denotes an element
++begin; // advance the iterator to get the next element
}

容器类型成员(Container Type Members)

通过类型别名,可以在不了解容器元素类型的情况下使用元素。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的引用,可以使用referenceconst_reference

beginend成员(begin and end Members)

beginend操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围。

beginend操作有多个版本:带r的版本返回反向迭代器。以c开头的版本(C++11新增)返回const迭代器。不以c开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对const对象调用时,返回const迭代器。

1
2
3
4
5
list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin(); // list<string>::const_iterator
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator

autobeginend结合使用时,返回的迭代器类型依赖于容器类型。但调用以c开头的版本仍然可以获得const迭代器,与容器是否是常量无关。

当程序不需要写操作时,应该使用cbegincend

容器定义和初始化(Defining and Initializing a Container)

将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。

传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。

1
2
3
4
5
6
7
8
// each container has three elements, initialized from the given initializers
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
list<string> list2(authors); // ok: types match
deque<string> authList(authors); // error: container types don't match
vector<string> words(articles); // error: element types must match
// ok: converts const char* elements to string
forward_list<string> words(articles.begin(), articles.end());

C++11允许对容器进行列表初始化。

1
2
3
// each container has three elements, initialized from the given initializers
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

定义和使用array类型时,需要同时指定元素类型和容器大小。

1
2
3
4
array<int, 42>      // type is: array that holds 42 ints
array<string, 10> // type is: array that holds 10 strings
array<int, 10>::size_type i; // array type includes element type and size
array<int>::size_type j; // error: array<int> is not a type

array进行列表初始化时,初始值的数量不能大于array的大小。如果初始值的数量小于array的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。

可以对array进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。

赋值和swap(Assignment and swap

赋值运算符两侧的运算对象必须类型相同。assign允许用不同但相容的类型赋值,或者用容器的子序列赋值。

1
2
3
4
5
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // error: container types don't match
// ok: can convert from const char*to string
names.assign(oldstyle.cbegin(), oldstyle.cend());

由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器本身。

swap交换两个相同类型容器的内容。除array外,swap不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。

1
2
3
vector<string> svec1(10);   // vector with ten elements
vector<string> svec2(24); // vector with 24 elements
swap(svec1, svec2);

赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作交换容器内容,不会导致迭代器、引用和指针失效(arraystring除外)。

对于arrayswap会真正交换它们的元素。因此在swap操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换。

1
2
3
4
5
6
7
8
9
10
11
12
13
array<int, 3> a = { 1, 2, 3 };
array<int, 3> b = { 4, 5, 6 };
auto p = a.cbegin(), q = a.cend();
a.swap(b);
// 输出交换后的值,即4、5、6
while (p != q)
{
cout << *p << endl;
++p;
}
4
5
6

对于其他容器类型(除string),指针、引用和迭代器在swap操作后仍指向操作前的元素,但这些元素已经属于不同的容器了。

1
2
3
4
5
6
7
8
9
10
vector<int> a = { 1, 2, 3 };
vector<int> b = { 4, 5, 6 };
auto p = a.cbegin(), q = a.cend();
a.swap(b);
// 输出交换前的值,即1、2、3
while (p != q)
{
cout << *p << endl;
++p;
}

在这段代码中,即使在执行完 a.swap(b) 之后,pq 仍然保留了交换前的值,因为它们是在 a 被交换前就已经初始化的,它们所指向的仍然是 a 的原始内存。

因此,输出仍然是原始的 a 的值:

1
2
3
1
2
3

array不支持assign,也不允许用花括号列表进行赋值。

1
2
3
4
array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> a2 = {0}; // elements all have value 0
a1 = a2; // replaces elements in a1
a2 = {0}; // error: cannot assign to an array from a braced list

这段代码涉及了 std::array 的初始化和赋值操作。

  1. array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
    • 这行代码创建了一个名为 a1std::array,它包含了 10 个整数,初始化为 0 到 9。
  2. array<int, 10> a2 = {0};
    • 这行代码创建了另一个名为 a2std::array,它包含了 10 个整数,所有元素的值都被初始化为 0。注意,只提供了一个 0,但是因为 std::array 在初始化时会使用剩余的元素自动填充默认值,所以所有的元素都被初始化为 0。
  3. a1 = a2;
    • 这行代码将 a2 的值赋给了 a1,即用 a2 中的元素替换了 a1 中的元素。因为 a1a2 都是相同类型和大小的 std::array,所以可以直接进行赋值操作。
  4. a2 = {0};
    • 这行代码尝试将一个大括号初始化列表赋给 a2,但是这种赋值方式是不合法的。原因是对于 std::array,不能直接将大括号初始化列表赋值给它,必须通过逐个元素赋值或者通过另一个同类型的 std::array 进行赋值。因此,这行代码会导致编译错误。

容器大小操作(Container Size Operations)

size成员返回容器中元素的数量;emptysize为0时返回true,否则返回falsemax_size返回一个大于或等于该类型容器所能容纳的最大元素数量的值。forward_list支持max_sizeempty,但不支持size

关系运算符(Relational Operators)

每个容器类型都支持相等运算符(==!=)。除无序关联容器外,其他容器都支持关系运算符(>>=<<=)。关系运算符两侧的容器类型和保存元素类型都必须相同。

两个容器的比较实际上是元素的逐对比较,其工作方式与string的关系运算符类似:

  • 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。
  • 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
  • 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。
1
2
3
4
5
6
7
8
vector<int> v1 = { 1, 3, 5, 7, 9, 12 };
vector<int> v2 = { 1, 3, 9 };
vector<int> v3 = { 1, 3, 5, 7 };
vector<int> v4 = { 1, 3, 5, 7, 9, 12 };
v1 < v2 // true; v1 and v2 differ at element [2]: v1[2] is less than v2[2]
v1 < v3 // false; all elements are equal, but v3 has fewer of them;
v1 == v4 // true; each element is equal and v1 and v4 have the same size()
v1 == v2 // false; v2 has fewer elements than v1

容器的相等运算符实际上是使用元素的==运算符实现的,而其他关系运算符则是使用元素的<运算符。如果元素类型不支持所需运算符,则保存该元素的容器就不能使用相应的关系运算。

顺序容器操作(Sequential Container Operations)

向顺序容器添加元素(Adding Elements to a Sequential Container)

array外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。

push_back将一个元素追加到容器尾部,push_front将元素插入容器头部。

1
2
3
4
// read from standard input, putting each word onto the end of container
string word;
while (cin >> word)
container.push_back(word);

insert将元素插入到迭代器指定的位置之前。一些不支持push_front的容器可以使用insert将元素插入开始位置。

1
2
3
4
5
6
7
vector<string> svec;
list<string> slist;
// equivalent to calling slist.push_front("Hello!");
slist.insert(slist.begin(), "Hello!");
// no push_front on vector but we can insert before begin()
// warning: inserting anywhere but at the end of a vector might be slow
svec.insert(svec.begin(), "Hello!");

将元素插入到vectordequestring的任何位置都是合法的,但可能会很耗时。

在新标准库中,接受元素个数或范围的insert版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回void。如果范围为空,不插入任何元素,insert会返回第一个参数。

1
2
3
4
list<string> lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); // same as calling push_front

新标准库增加了三个直接构造而不是拷贝元素的操作:emplace_frontemplace_backemplace,其分别对应push_frontpush_backinsert。当调用pushinsert时,元素对象被拷贝到容器中。而调用emplace时,则是将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素。

1
2
3
4
5
6
7
// construct a Sales_data object at the end of c
// uses the three-argument Sales_data constructor
c.emplace_back("978-0590353403", 25, 15.99);
// error: there is no version of push_back that takes three arguments
c.push_back("978-0590353403", 25, 15.99);
// ok: we create a temporary Sales_data object to pass to push_back
c.push_back(Sales_data("978-0590353403", 25, 15.99));

这段代码涉及了使用 push_back()emplace_back() 方法向容器中添加元素的不同方式。下面对每一部分进行解释:

  1. c.emplace_back("978-0590353403", 25, 15.99);
  • 这行代码使用 emplace_back() 方法向容器 c 的末尾添加一个元素。在这种情况下,使用了 Sales_data 类型的构造函数,该构造函数接受三个参数,分别是 ISBN 编号、售出册数和价格。emplace_back() 方法会在容器中直接构造一个 Sales_data 对象,而不是先创建一个临时对象然后再复制或移动到容器中。因此,它比 push_back() 方法更高效。
  1. c.push_back("978-0590353403", 25, 15.99);
  • 这行代码试图使用 push_back() 方法将一个具有三个参数的 Sales_data 对象添加到容器 c 的末尾。然而,push_back() 方法并不支持接受多个参数的情况,因此会导致编译错误。
  1. c.push_back(Sales_data("978-0590353403", 25, 15.99));
  • 这行代码使用 push_back() 方法向容器 c 的末尾添加一个元素。首先,创建了一个临时的 Sales_data 对象,然后将其作为参数传递给 push_back() 方法。这种方式虽然可以实现向容器中添加元素,但是需要额外的复制或移动操作,可能会影响性能。

传递给emplace的参数必须与元素类型的构造函数相匹配。

forward_list有特殊版本的insertemplace操作,且不支持push_backemplace_backvectorstring不支持push_frontemplace_front

访问元素(Accessing Elements)

每个顺序容器都有一个front成员函数,而除了forward_list之外的顺序容器还有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。

在调用frontback之前,要确保容器非空。

在容器中访问元素的成员函数都返回引用类型。如果容器是const对象,则返回const引用,否则返回普通引用。

可以快速随机访问的容器(stringvectordequearray)都提供下标运算符。保证下标有效是程序员的责任。如果希望确保下标合法,可以使用at成员函数。at类似下标运算,但如果下标越界,at会抛出out_of_range异常。

1
2
3
vector<string> svec;  // empty vector
cout << svec[0]; // run-time error: there are no elements in svec!
cout << svec.at(0); // throws an out_of_range exception

删除元素(Erasing Elements)

删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。删除vectorstring的元素后,指向删除点之后位置的迭代器、引用和指针也都会失效。

删除元素前,程序员必须确保目标元素存在。

pop_frontpop_back函数分别删除首元素和尾元素。vectorstring类型不支持pop_frontforward_list类型不支持pop_back

erase函数删除指定位置的元素。可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除元素(最后一个)之后位置的迭代器。

1
2
3
// delete the range of elements between two iterators
// returns an iterator to the element just after the last removed element
elem1 = slist.erase(elem1, elem2); // after the call elem1 == elem2

clear函数删除容器内的所有元素。

特殊的forward_list操作(Specialized forward_list Operations)

forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。

改变容器大小(Resizing a Container)

resize函数接受一个可选的元素值参数,用来初始化添加到容器中的元素,否则新元素进行值初始化。如果容器保存的是类类型元素,且resize向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。

容器操作可能使迭代器失效(Container Operations May Invalidate Iterators)

向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。

  • 向容器中添加元素后:
    • 如果容器是vectorstring类型,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。
    • 如果容器是deque类型,添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。
    • 如果容器是listforward_list类型,指向容器的迭代器、指针和引用仍然有效。
  • 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效:
    • 如果容器是listforward_list类型,指向容器其他位置的迭代器、指针和引用仍然有效。
    • 如果容器是deque类型,删除除首尾之外的任何元素都会使迭代器、指针和引用失效。如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。如果删除首元素,这些也不会受影响。
    • 如果容器是vectorstring类型,指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。

必须保证在每次改变容器后都正确地重新定位迭代器。

不要保存end函数返回的迭代器。

1
2
3
4
5
6
7
8
// safer: recalculate end on each trip whenever the loop adds/erases elements
while (begin != v.end())
{
// do some processing
++begin; // advance begin because we want to insert after this element
begin = v.insert(begin, 42); // insert the new value
++begin; // advance begin past the element we just added
}

vector对象是如何增长的(How a vector Grows)

vectorstring的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。

capacity函数返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。reserve函数告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小。

只有当需要的内存空间超过当前容量时,reserve才会真正改变容器容量,分配不小于需求大小的内存空间。当需求大小小于当前容量时,reserve并不会退回内存空间。因此在调用reserve之后,capacity会大于或等于传递给reserve的参数。

在C++11中可以使用shrink_to_fit函数来要求dequevectorstring退回不需要的内存空间(并不保证退回)。

额外的string操作(Additional string Operations)

构造string的其他方法(Other Ways to Construct strings)

从另一个string对象拷贝字符构造string时,如果提供的拷贝开始位置(可选)大于给定string的大小,则构造函数会抛出out_of_range异常。

如果传递给substr函数的开始位置超过string的大小,则函数会抛出out_of_range异常

改变string的其他方法(Other Ways to Change a string

append函数是在string末尾进行插入操作的简写形式。

1
2
3
string s("C++ Primer"), s2 = s;     // initialize s and s2 to "C++ Primer"
s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed."
s2.append(" 4th Ed."); // equivalent: appends " 4th Ed." to s2; s == s2

replace函数是调用eraseinsert函数的简写形式。

1
2
3
4
5
// equivalent way to replace "4th" by "5th"
s.erase(11, 3); // s == "C++ Primer Ed."
s.insert(11, "5th"); // s == "C++ Primer 5th Ed."
// starting at position 11, erase three characters and then insert "5th"
s2.replace(11, 3, "5th"); // equivalent: s == s2

string搜索操作(string Search Operations)

string的每个搜索操作都返回一个string::size_type值,表示匹配位置的下标。如果搜索失败,则返回一个名为string::nposstatic成员。标准库将npos定义为const string::size_type类型,并初始化为-1。

不建议用int或其他带符号类型来保存string搜索函数的返回值。

compare函数(The compare Functions)

string类型提供了一组compare函数进行字符串比较操作,类似C标准库的strcmp函数。

数值转换(Numeric Conversions)

C++11增加了string和数值之间的转换函数。

进行数值转换时,string参数的第一个非空白字符必须是符号(+-)或数字。它可以以0x0X开头来表示十六进制数。对于转换目标是浮点值的函数,string参数也可以以小数点开头,并可以包含eE来表示指数部分。

如果给定的string不能转换为一个数值,则转换函数会抛出invalid_argument异常。如果转换得到的数值无法用任何类型表示,则抛出out_of_range异常。

容器适配器(Container Adaptors)

标准库定义了stackqueuepriority_queue三种容器适配器。容器适配器可以改变已有容器的工作机制。

默认情况下,stackqueue是基于deque实现的,priority_queue是基于vector实现的。可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。

1
2
3
4
// empty stack implemented on top of vector
stack<string, vector<string>> str_stk;
// str_stk2 is implemented on top of vector and initially holds a copy of svec
stack<string, vector<string>> str_stk2(svec);

这段代码展示了如何使用 vector 作为底层容器来实现一个栈(stack)。

  1. stack<string, vector<string>> str_stk;
    • 这行代码声明了一个名为 str_stk 的栈对象,它使用 vector<string> 作为底层容器。这意味着 str_stk 将使用 vector 来存储栈中的元素,以实现栈的基本功能。
  2. stack<string, vector<string>> str_stk2(svec);
    • 这行代码声明了另一个名为 str_stk2 的栈对象,并通过复制 svec 来初始化它。在这种情况下,str_stk2 中的元素与 svec 中的元素相同,因为它们是从同一份数据复制而来的。这种初始化方式可以用于创建一个已有容器的副本,作为新的栈的初始状态。

所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在array上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用forward_list构造适配器。

栈适配器stack定义在头文件stack中。队列适配器queuepriority_queue定义在头文件queue中。

queue使用先进先出的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。

关联容器

关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是mapset

  • map中的元素是一些键值对(key-value):关键字起索引作用,值表示与索引相关联的数据。
  • set中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在set中。

标准库提供了8个关联容器,它们之间的不同体现在三个方面:

  • map还是set类型。
  • 是否允许保存重复的关键字。
  • 是否按顺序保存元素。

允许重复保存关键字的容器名字都包含单词multi;无序保存元素的容器名字都以单词unordered开头。

有序容器:

类型 特性
map 保存键值对的关联数组
set 只保存关键字的容器
multimap 关键字可重复出现的map
multiset 关键字可重复出现的set

无序容器:

类型 特性
unordered_map 用哈希函数管理的map
unordered_set 用哈希函数管理的set
unordered_multimap 关键字可重复出现的unordered_map
unordered_multiset 关键字可重复出现的unordered_set

mapmultimap类型定义在头文件map中;setmultiset类型定义在头文件set中;无序容器定义在头文件unordered_mapunordered_set中。

使用关联容器(Using an Associative Container)

map类型通常被称为关联数组(associative array)。

map中提取一个元素时,会得到一个pair类型的对象。pair是一个模板类型,保存两个名为firstsecond的公有数据成员。map所使用的pairfirst成员保存关键字,用second成员保存对应的值。

1
2
3
4
5
6
7
8
9
// count the number of times each word occurs in the input
map<string, size_t> word_count; // empty map from string to size_t
string word;
while (cin >> word)
++word_count[word]; // fetch and increment the counter for word
for (const auto &w : word_count) // for each element in the map
// print the results
cout << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;

set类型的find成员返回一个迭代器。如果给定关键字在set中,则迭代器指向该关键字,否则返回的是尾后迭代器。

关联容器概述(Overview of the Associative Containers)

定义关联容器(Defining an Associative Container)

定义map时,必须指定关键字类型和值类型;定义set时,只需指定关键字类型。

初始化map时,提供的每个键值对用花括号{}包围。

1
2
3
4
5
6
7
8
9
10
map<string, size_t> word_count;   // empty
// list initialization
set<string> exclude = { "the", "but", "and" };
// three elements; authors maps last name to first
map<string, string> authors =
{
{"Joyce", "James"},
{"Austen", "Jane"},
{"Dickens", "Charles"}
};

mapset中的关键字必须唯一,multimapmultiset没有此限制。

对于有序容器——mapmultimapsetmultiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来进行比较操作。

用来组织容器元素的操作的类型也是该容器类型的一部分。如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型。操作类型在尖括号中紧跟着元素类型给出。

1
2
3
4
5
6
7
8
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() < rhs.isbn();
}

// bookstore can have several transactions with the same ISBN
// elements in bookstore will be in ISBN order
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

这段代码定义了一个比较函数 compareIsbn,然后创建了一个 multiset 容器,用于存储 Sales_data 类型的对象。让我逐步解释:

  1. bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs): 这是一个自定义的比较函数,用于比较两个 Sales_data 对象的 ISBN。lhsrhs 是要比较的两个对象的引用。函数返回 true 表示 lhs 的 ISBN 小于 rhs 的 ISBN,否则返回 false

  2. multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);: 这行代码创建了一个 multiset 容器,名为 bookstore,用于存储 Sales_data 类型的对象。multiset 是一个有序容器,它可以包含重复的元素,并且元素会按照指定的比较函数进行排序。

    • <Sales_data, decltype(compareIsbn)*> 指定了容器中存储的元素类型为 Sales_data,并且指定了比较函数的类型为 decltype(compareIsbn)*,即指向 compareIsbn 函数的指针。

    • bookstore(compareIsbn) 创建了 bookstore 容器,并将 compareIsbn 函数作为参数传递给容器的构造函数,用于指定元素的排序规则。由于 multiset 容器要求指定一个比较函数来决定元素的顺序,因此需要将 compareIsbn 函数的地址传递给容器,以便容器内部能够调用该函数来进行元素的比较和排序。

因此,bookstore 是一个按照 compareIsbn 函数指定的排序规则进行排序的 multiset 容器,可以存储多个具有相同 ISBN 的 Sales_data 对象,并且这些对象将按照 ISBN 的顺序进行排列。

pair类型(The pair Type)

pair定义在头文件utility中。一个pair可以保存两个数据成员,分别命名为firstsecond

1
2
3
pair<string, string> anon;        // holds two strings
pair<string, size_t> word_count; // holds a string and an size_t
pair<string, vector<int>> line; // holds string and vector<int>

pair的默认构造函数对数据成员进行值初始化。

在C++11中,如果函数需要返回pair,可以对返回值进行列表初始化。早期C++版本中必须显式构造返回值。

1
2
3
4
5
6
7
8
9
10
pair<string, int> process(vector<string> &v)
{
// process v
if (!v.empty())
// list initialize
return { v.back(), v.back().size() };
else
// explicitly constructed return value
return pair<string, int>();
}

关联容器操作(Operations on Associative Containers)

关联容器定义了类型别名来表示容器关键字和值的类型。

类型 含义
key_type 容器的关键字类型
mapped_type map的值类型
value_type 对于set,与key_type相同 对于map,为pair<const key_type, mapped_type>

对于set类型,key_typevalue_type是一样的。set中保存的值就是关键字。对于map类型,元素是键值对。即每个元素是一个pair对象,包含一个关键字和一个关联的值。由于元素关键字不能改变,因此pair的关键字部分是const的。另外,只有map类型(unordered_mapunordered_multimapmultimapmap)才定义了mapped_type

1
2
3
4
5
set<string>::value_type v1;        // v1 is a string
set<string>::key_type v2; // v2 is a string
map<string, int>::value_type v3; // v3 is a pair<const string, int>
map<string, int>::key_type v4; // v4 is a string
map<string, int>::mapped_type v5; // v5 is an int

关联容器迭代器(Associative Container Iterators)

解引用关联容器迭代器时,会得到一个类型为容器的value_type的引用。对map而言,value_typepair类型,其first成员保存const的关键字,second成员保存值。

1
2
3
4
5
6
7
// get an iterator to an element in word_count
auto map_it = word_count.begin();
// *map_it is a reference to a pair<const string, size_t> object
cout << map_it->first; // prints the key for this element
cout << " " << map_it->second; // prints the value of the element
map_it->first = "new key"; // error: key is const
++map_it->second; // ok: we can change the value through an iterator

虽然set同时定义了iteratorconst_iterator类型,但两种迭代器都只允许只读访问set中的元素。类似mapset中的关键字也是const的。

1
2
3
4
5
6
7
set<int> iset = {0,1,2,3,4,5,6,7,8,9};
set<int>::iterator set_it = iset.begin();
if (set_it != iset.end())
{
*set_it = 42; // error: keys in a set are read-only
cout << *set_it << endl; // ok: can read the key
}

mapset都支持beginend操作。使用迭代器遍历mapmultimapsetmultiset时,迭代器按关键字升序遍历元素。

通常不对关联容器使用泛型算法。

添加元素(Adding Elements)

使用insert成员可以向关联容器中添加元素。向mapset中添加已存在的元素对容器没有影响。

通常情况下,对于想要添加到map中的数据,并没有现成的pair对象。可以直接在insert的参数列表中创建pair

1
2
3
4
5
// four ways to add word to word_count
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));

insertemplace的返回值依赖于容器类型和参数:

  • 对于不包含重复关键字的容器,添加单一元素的insertemplace版本返回一个pair,表示操作是否成功。pairfirst成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值。如果关键字已在容器中,则insert直接返回,bool值为false。如果关键字不存在,元素会被添加至容器中,bool值为true
  • 对于允许包含重复关键字的容器,添加单一元素的insertemplace版本返回指向新元素的迭代器。

删除元素(Erasing Elements)

与顺序容器不同,关联容器提供了一个额外的erase操作。它接受一个key_type参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量。对于不包含重复关键字的容器,erase的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。

map的下标操作(Subscripting a map

操作 含义
c[k] 返回关键字为k的元素;若k不存在,则向c中添加并进行值初始化
c.at(k) 返回关键字为k的元素;若k不存在,则抛出out_of_range异常

map下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。

由于下标运算符可能向容器中添加元素,所以只能对非constmap使用下标操作。

map进行下标操作时,返回的是mapped_type类型的对象;解引用map迭代器时,返回的是value_type类型的对象。

访问元素(Accessing Elements)

操作 含义
c.find(k) 返回指向第一个关键字为k的元素的迭代器或尾后迭代器
c.count(k) 返回关键字为k的元素的数量
c.lower_bound(k) 返回指向第一个关键字不小于k的元素的迭代器
c.upper_bound(k) 返回指向第一个关键字大于k的元素的迭代器
c.equal_range(k) 返回一个迭代器pair,表示关键字为k的元素范围

如果multimapmultiset中有多个元素具有相同关键字,则这些元素在容器中会相邻存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
multimap<string, string> authors;
// adds the first element with the key Barth, John
authors.insert({"Barth, John", "Sot-Weed Factor"});
// ok: adds the second element with the key Barth, John
authors.insert({"Barth, John", "Lost in the Funhouse"});

string search_item("Alain de Botton"); // author we'll look for
auto entries = authors.count(search_item); // number of elements
auto iter = authors.find(search_item); // first entry for this author
// loop through the number of entries there are for this author
while(entries)
{
cout << iter->second << endl; // print each title
++iter; // advance to the next title
--entries; // keep track of how many we've printed
}

这段代码的目的是从 multimap 容器 authors 中查找特定作者的所有作品,并打印它们的标题。让我逐步解释:

  1. 定义了一个 multimap 容器 authors

    1
    multimap<string, string> authors;

    这行代码创建了一个 multimap 容器,用于存储作者名和作品名的键值对。每个键(作者名)可以对应多个值(作品名)。

  2. 向容器中插入元素:

    1
    2
    authors.insert({"Barth, John", "Sot-Weed Factor"});
    authors.insert({"Barth, John", "Lost in the Funhouse"});

    这两行代码分别向 authors 容器中插入了两个具有相同键("Barth, John")的元素。因为 multimap 允许多个元素具有相同的键,所以这两个作品都会被插入。

  3. 定义要搜索的作者:

    1
    string search_item("Alain de Botton");

    这行代码定义了要搜索的作者名称。在这个示例中,搜索的作者并不存在于 authors 容器中。

  4. 查找元素:

    1
    2
    auto entries = authors.count(search_item);  // number of elements
    auto iter = authors.find(search_item); // first entry for this author
    • authors.count(search_item) 返回与搜索项匹配的元素数量。在这里,因为搜索项并不存在于容器中,所以 entries 被初始化为0。
    • authors.find(search_item) 返回与搜索项匹配的第一个元素的迭代器。在这里,由于搜索项并不存在于容器中,iter 被初始化为指向容器末尾的迭代器(即 authors.end())。
  5. 输出作者作品:

    1
    2
    3
    4
    5
    6
    while(entries)
    {
    cout << iter->second << endl; // print each title
    ++iter; // advance to the next title
    --entries; // keep track of how many we've printed
    }

    这是一个循环,它的条件是 entries 不为0。但是,由于搜索项并不存在于容器中,所以循环体不会执行。因此,没有任何输出。

综上所述,这段代码的作用是试图从 multimap 容器 authors 中查找指定作者的作品,并打印它们的标题。但由于搜索项并不存在于容器中,所以不会有任何输出。

lower_boundupper_bound操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,lower_bound返回的迭代器会指向第一个匹配给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配元素之后的位置。如果关键字不在multimap中,则lower_boundupper_bound会返回相等的迭代器,指向一个不影响排序的关键字插入位置。因此用相同的关键字调用lower_boundupper_bound会得到一个迭代器范围,表示所有具有该关键字的元素范围。

1
2
3
4
5
6
// definitions of authors and search_item as above
// beg and end denote the range of elements for this author
for (auto beg = authors.lower_bound(search_item),
end = authors.upper_bound(search_item);
beg != end; ++beg)
cout << beg->second << endl; // print each title

lower_boundupper_bound有可能返回尾后迭代器。如果查找的元素具有容器中最大的关键字,则upper_bound返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound也返回尾后迭代器。

equal_range操作接受一个关键字,返回一个迭代器pair。若关键字存在,则第一个迭代器指向第一个匹配关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置。若关键字不存在,则两个迭代器都指向一个不影响排序的关键字插入位置。

1
2
3
4
5
// definitions of authors and search_item as above
// pos holds iterators that denote the range of elements for this key
for (auto pos = authors.equal_range(search_item);
pos.first != pos.second; ++pos.first)
cout << pos.first->second << endl; // print each title

这段代码使用了 equal_range 函数来查找 multimap 容器中指定键的所有元素,并将它们的位置存储在迭代器对 pos 中。然后,通过循环遍历该范围,并输出每个匹配键的对应值。让我逐步解释:

  1. auto pos = authors.equal_range(search_item);: 这行代码调用了 equal_range 函数,该函数会返回一个表示指定键在容器中的范围的迭代器对。authors 是一个 multimap 容器,search_item 是要查找的键值。pos 是一个自动类型的变量,用于存储返回的迭代器对。

  2. pos.first != pos.second: 这是一个循环条件,它检查迭代器对 pos 所表示的范围是否为空。如果范围不为空,则说明在容器中找到了与 search_item 匹配的元素。

  3. ++pos.first: 在循环的每次迭代中,迭代器 pos.first 会向前移动,以便遍历匹配键的所有元素。

  4. cout << pos.first->second << endl;: 这行代码输出了匹配键的对应值。由于 multimap 容器可以包含多个具有相同键的元素,因此 pos.first 是一个迭代器,它指向范围内的当前元素。pos.first->second 访问了该元素的值部分,即容器中存储的第二个数据成员(假设作者与书名关联),并将其输出到标准输出流中。

通过这种方式,该循环会逐个输出与指定键匹配的所有元素的对应值,直到范围中的所有元素都被遍历完毕。

无序容器(The Unordered Containers)

新标准库定义了4个无序关联容器(unordered associative container),这些容器使用哈希函数(hash function)和关键字类型的==运算符组织元素。

无序容器和对应的有序容器通常可以相互替换。但是由于元素未按顺序存储,使用无序容器的程序输出一般会与有序容器的版本不同。

无序容器在存储上组织为一组桶,每个桶保存零或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。

默认情况下,无序容器使用关键字类型的==运算符比较元素,还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型和一些标准库类型提供了hash模板。因此可以直接定义关键字是这些类型的无序容器,而不能直接定义关键字类型为自定义类类型的无序容器,必须先提供对应的hash模板版本。


Standard Library Algorithms

概述(Overview)

大多数算法都定义在头文件algorithm中,此外标准库还在头文件numeric中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的元素范围进行操作。

find函数将范围中的每个元素与给定值进行比较,返回指向第一个等于给定值的元素的迭代器。如果无匹配元素,则返回其第二个参数来表示搜索失败。

1
2
3
4
5
6
int val = 42;   // value we'll look for
// result will denote the element we want if it's in vec, or vec.cend() if not
auto result = find(vec.cbegin(), vec.cend(), val);
// report the result
cout << "The value " << val
<< (result == vec.cend() ? " is not present" : " is present") << endl;

迭代器参数令算法不依赖于特定容器,但依赖于元素类型操作。

泛型算法本身不会执行容器操作,它们只会运行于迭代器之上,执行迭代器操作。算法可能改变容器中元素的值,或者在容器内移动元素,但不会改变底层容器的大小(当算法操作插入迭代器时,迭代器可以向容器中添加元素,但算法自身不会进行这种操作)。

初识泛型算法(A First Look at the Algorithms)

只读算法(Read-Only Algorithms)

accumulate函数(定义在头文件numeric中)用于计算一个序列的和。它接受三个参数,前两个参数指定需要求和的元素范围,第三个参数是和的初值(决定加法运算类型和返回值类型)。

1
2
3
4
5
// sum the elements in vec starting the summation with the value 0
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
string sum = accumulate(v.cbegin(), v.cend(), string(""));
// error: no + on const char*
string sum = accumulate(v.cbegin(), v.cend(), "");

建议在只读算法中使用cbegincend函数。

equal函数用于确定两个序列是否保存相同的值。它接受三个迭代器参数,前两个参数指定第一个序列范围,第三个参数指定第二个序列的首元素。equal函数假定第二个序列至少与第一个序列一样长。

1
2
// roster2 should have at least as many elements as roster1
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

只接受单一迭代器表示第二个操作序列的算法都假定第二个序列至少与第一个序列一样长。

写容器元素的算法(Algorithms That Write Container Elements)

fill函数接受两个迭代器参数表示序列范围,还接受一个值作为第三个参数,它将给定值赋予范围内的每个元素。

1
2
// reset each element to 0
fill(vec.begin(), vec.end(), 0);

fill_n函数接受单个迭代器参数、一个计数值和一个值,它将给定值赋予迭代器指向位置开始的指定个元素。

1
2
// reset all the elements of vec to 0
fill_n(vec.begin(), vec.size(), 0);

向目的位置迭代器写入数据的算法都假定目的位置足够大,能容纳要写入的元素。

插入迭代器(insert iterator)是一种向容器内添加元素的迭代器。通过插入迭代器赋值时,一个与赋值号右侧值相等的元素会被添加到容器中。

back_inserter函数(定义在头文件iterator中)接受一个指向容器的引用,返回与该容器绑定的插入迭代器。通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。

1
2
3
4
5
vector<int> vec;    // empty vector
auto it = back_inserter(vec); // assigning through it adds elements to vec
*it = 42; // vec now has one element with value 42
// ok: back_inserter creates an insert iterator that adds elements to vec
fill_n(back_inserter(vec), 10, 0); // appends ten elements to vec

copy函数接受三个迭代器参数,前两个参数指定输入序列,第三个参数指定目的序列的起始位置。它将输入序列中的元素拷贝到目的序列中,返回目的位置迭代器(递增后)的值。

1
2
3
4
int a1[] = { 0,1,2,3,4,5,6,7,8,9 };
int a2[sizeof(a1) / sizeof(*a1)]; // a2 has the same size as a1
// ret points just past the last element copied into a2
auto ret = copy(begin(a1), end(a1), a2); // copy a1 into a2

replace函数接受四个参数,前两个迭代器参数指定输入序列,后两个参数指定要搜索的值和替换值。它将序列中所有等于第一个值的元素都替换为第二个值。

1
2
// replace any element with the value 0 with 42
replace(ilst.begin(), ilst.end(), 0, 42);

相对于replacereplace_copy函数可以保留原序列不变。它接受第三个迭代器参数,指定调整后序列的保存位置。

1
2
// use back_inserter to grow destination as needed
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);

很多算法都提供“copy”版本,这些版本不会将新元素放回输入序列,而是创建一个新序列保存结果。

重排容器元素的算法(Algorithms That Reorder Container Elements)

sort函数接受两个迭代器参数,指定排序范围。它利用元素类型的<运算符重新排列元素。

1
2
3
4
5
6
7
8
9
10
void elimDups(vector<string> &words)
{
// sort words alphabetically so we can find the duplicates
sort(words.begin(), words.end());
// unique reorders the input range so that each word appears once in the
// front portion of the range and returns an iterator one past the unique range
auto end_unique = unique(words.begin(), words.end());
// erase uses a vector operation to remove the nonunique elements
words.erase(end_unique, words.end());
}

unique函数重排输入序列,消除相邻的重复项,返回指向不重复值范围末尾的迭代器。

定制操作(Customizing Operations)

默认情况下,很多比较算法使用元素类型的<==运算符完成操作。可以为这些算法提供自定义操作来代替默认运算符。

向算法传递函数(Passing a Function to an Algorithm)

谓词(predicate)是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为一元谓词(unary predicate,接受一个参数)和二元谓词(binary predicate,接受两个参数)。接受谓词参数的算法会对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。

1
2
3
4
5
6
7
8
// comparison function to be used to sort by word length
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}

// sort on word length, shortest to longest
sort(words.begin(), words.end(), isShorter);

lambda表达式(Lambda Expressions)

find_if函数接受两个迭代器参数和一个谓词参数。迭代器参数用于指定序列范围,之后对序列中的每个元素调用给定谓词,并返回第一个使谓词返回非0值的元素。如果不存在,则返回尾迭代器。

对于一个对象或表达式,如果可以对其使用调用运算符(),则称它为可调用对象(callable object)。可以向算法传递任何类别的可调用对象。

一个lambda表达式表示一个可调用的代码单元,类似未命名的内联函数,但可以定义在函数内部。其形式如下:

1
[capture list] (parameter list) -> return type { function body }

其中,capture list(捕获列表)是一个由lambda所在函数定义的局部变量的列表(通常为空)。return typeparameter listfunction body与普通函数一样,分别表示返回类型、参数列表和函数体。但与普通函数不同,lambda必须使用尾置返回类型,且不能有默认实参。

定义lambda时可以省略参数列表和返回类型,但必须包含捕获列表和函数体。省略参数列表等价于指定空参数列表。省略返回类型时,若函数体只是一个return语句,则返回类型由返回表达式的类型推断而来。否则返回类型为void

1
2
auto f = [] { return 42; };
cout << f() << endl; // prints 42

lambda可以使用其所在函数的局部变量,但必须先将其包含在捕获列表中。捕获列表只能用于局部非static变量,lambda可以直接使用局部static变量和其所在函数之外声明的名字。

1
2
3
// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a) { return a.size() >= sz; });

解释一下:

这段代码使用了STL(标准模板库)中的find_if算法来查找满足特定条件的元素。让我们逐步解释每个部分的功能:

  1. auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });:这一行使用了find_if算法,它在指定范围内查找第一个满足特定条件的元素。具体来说:
    • words.begin()words.end()指定了查找范围,即从words向量的起始位置到末尾位置。

    • [sz](const string &a) { return a.size() >= sz; }是一个lambda表达式,它定义了查找条件。lambda表达式的形式是[capture list](parameter list) { function body },在这里:

      • [sz]是捕获列表,它捕获了外部变量sz,使lambda表达式可以访问并使用它。

      • (const string &a)是参数列表,它指定了lambda表达式的参数,这里是一个const引用到字符串。

      • { return a.size() >= sz; }是函数体,它定义了查找条件。这里,lambda表达式返回一个布尔值,即判断字符串a的大小是否大于等于sz。如果是,则返回true,表示找到了满足条件的元素。

  2. auto wc = ...;:使用auto关键字自动推导出迭代器的类型。wc是一个迭代器,它指向第一个满足条件的元素。

综上所述,这段代码的功能是查找在words向量中,第一个满足长度大于等于sz的字符串,并将指向该字符串的迭代器存储在wc中。

for_each函数接受一个输入序列和一个可调用对象,它对输入序列中的每个元素调用此对象。

1
2
3
// print words of the given size or longer, each one followed by a space
for_each(wc, words.end(),
[] (const string &s) { cout << s << " "; });

lambda捕获和返回(Lambda Captures and Returns)

lambda捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。在lambda创建后修改局部变量不会影响lambda内对应的值。

1
2
3
4
5
size_t v1 = 42; // local variable
// copies v1 into the callable object named f
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j is 42; f stored a copy of v1 when we created it

这段代码使用了 C++11 中的 lambda 表达式来创建了一个可调用对象,并且捕获了一个局部变量 v1。让我们一步步解释代码的功能:

  1. size_t v1 = 42;: 这行代码声明并初始化了一个名为 v1 的本地变量,其值为 42

  2. auto f = [v1] { return v1; };: 这行代码定义了一个 lambda 表达式,并将其赋值给变量 f。 lambda 表达式的形式为 [capture-list] (parameters) { body },在这里:

    • [v1] 是捕获列表,它允许 lambda 表达式访问在 lambda 函数体内部使用的外部变量。[v1] 捕获了变量 v1,并在 lambda 函数体内部使用它。

    • { return v1; } 是 lambda 函数体,它包含了 lambda 表达式要执行的操作。在这里,lambda 函数体返回捕获的变量 v1 的值。

  3. v1 = 0;: 这行代码将变量 v1 的值更改为 0

  4. auto j = f();: 这行代码调用了 lambda 表达式,并将其返回值赋值给变量 j。由于 lambda 表达式 [v1] { return v1; } 捕获了变量 v1 的值(在创建时进行了捕获),即使在调用 lambda 表达式时 v1 的值已经被修改为 0,lambda 表达式仍然返回了捕获时的值,也就是 42。因此,变量 j 的值为 42

lambda可以以引用方式捕获变量,但必须保证lambda执行时变量存在。

1
2
3
4
5
size_t v1 = 42; // local variable
// the object f2 contains a reference to v1
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2(); // j is 0; f2 refers to v1; it doesn't store it

可以让编译器根据lambda代码隐式捕获函数变量,方法是在捕获列表中写一个&=符号。&为引用捕获,=为值捕获。

可以混合使用显式捕获和隐式捕获。混合使用时,捕获列表中的第一个元素必须是&=符号,用于指定默认捕获方式。显式捕获的变量必须使用与隐式捕获不同的方式。

1
2
3
4
5
6
// os implicitly captured by reference; c explicitly captured by value
for_each(words.begin(), words.end(),
[&, c] (const string &s) { os << s << c; });
// os explicitly captured by reference; c implicitly captured by value
for_each(words.begin(), words.end(),
[=, &os] (const string &s) { os << s << c; });

这两段代码是使用 lambda 表达式结合 STL 中的 for_each 算法来对 words 容器中的元素进行处理。它们展示了 lambda 表达式中捕获变量的不同方式,通过引用捕获和值捕获的不同组合。

第一段代码:

1
2
3
// os implicitly captured by reference; c explicitly captured by value
for_each(words.begin(), words.end(),
[&, c] (const string &s) { os << s << c; });

这段代码使用了 [&, c] 的捕获列表。其中 & 表示以引用方式捕获外部作用域的变量,而 c 则表示以值的方式捕获。这意味着 os 变量会被隐式以引用方式捕获,而 c 则被显式以值的方式捕获。在 lambda 函数体中,os 是以引用方式捕获的,而 c 则是以值的方式捕获的。

第二段代码:

1
2
3
// os explicitly captured by reference; c implicitly captured by value
for_each(words.begin(), words.end(),
[=, &os] (const string &s) { os << s << c; });

这段代码使用了 [=, &os] 的捕获列表。其中 = 表示以值的方式捕获所有外部作用域的变量,而 &os 则表示以引用的方式捕获 os 变量。这意味着 os 变量会被显式以引用方式捕获,而 c 则会被隐式以值的方式捕获。在 lambda 函数体中,os 是以引用方式捕获的,而 c 则是以值的方式捕获的。

总的来说,两段代码都使用了相同的 for_each 算法来遍历 words 容器中的元素,并对每个元素执行相同的操作,即将每个字符串及变量 c 的值输出到输出流 os 中。唯一的区别在于,它们对于变量 osc 的捕获方式不同。

默认情况下,对于值方式捕获的变量,lambda不能修改其值。如果希望修改,就必须在参数列表后添加关键字mutable

1
2
3
4
5
size_t v1 = 42; // local variable
// f can change the value of the variables it captures
auto f = [v1] () mutable { return ++v1; };
v1 = 0;
auto j = f(); // j is 43

对于引用方式捕获的变量,lambda是否可以修改依赖于此引用指向的是否是const类型。

transform函数接受三个迭代器参数和一个可调用对象。前两个迭代器参数指定输入序列,第三个迭代器参数表示目的位置。它对输入序列中的每个元素调用可调用对象,并将结果写入目的位置。

1
2
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int { if (i < 0) return -i; else return i; });

lambda定义返回类型时,必须使用尾置返回类型。

参数绑定(Binding Arguments)

bind函数定义在头文件functional中,相当于一个函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表。一般形式如下:

1
auto newCallable = bind(callable, arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个以逗号分隔的参数列表,对应给定的callable的参数。之后调用newCallable时,newCallable会再调用callable,并传递给它arg_list中的参数。arg_list中可能包含形如_n的名字,其中n是一个整数。这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置:_1newCallable的第一个参数,_2newCallable的第二个参数,依次类推。这些名字都定义在命名空间placeholders中,它又定义在命名空间std中,因此使用时应该进行双重限定。

1
2
3
4
5
6
7
8
9
using std::placeholders::_1;
using namespace std::placeholders;
bool check_size(const string &s, string::size_type sz);

// check6 is a callable object that takes one argument of type string
// and calls check_size on its given string and the value 6
auto check6 = bind(check_size, _1, 6);
string s = "hello";
bool b1 = check6(s); // check6(s) calls check_size(s, 6)

bind函数可以调整给定可调用对象中的参数顺序。

1
2
3
4
// sort on word length, shortest to longest
sort(words.begin(), words.end(), isShorter);
// sort on word length, longest to shortest
sort(words.begin(), words.end(), bind(isShorter, _2, _1));

这段代码演示了如何使用 C++ 的 sort 算法对一个字符串向量 words 进行排序,以字符串的长度作为排序的标准。然而,这里使用了两种不同的排序方式,并且利用了函数对象和函数适配器。让我们逐步解释:

  1. sort(words.begin(), words.end(), isShorter);: 这行代码使用了 sort 算法来对字符串向量 words 进行排序。words.begin()words.end() 指定了排序的范围,即从 words 的起始位置到末尾位置。排序的方式由第三个参数 isShorter 指定。这里 isShorter 是一个自定义的函数对象,它定义了字符串长度的比较规则,即按照字符串长度从短到长的顺序进行排序。

  2. sort(words.begin(), words.end(), bind(isShorter, _2, _1));: 这行代码也使用了 sort 算法来对字符串向量 words 进行排序。words.begin()words.end() 仍然指定了排序的范围。不同之处在于第三个参数,这里使用了 bind 函数适配器。bind 函数适配器可以用来修改函数的参数顺序。在这里,bind(isShorter, _2, _1)isShorter 函数对象的参数顺序进行了调换。具体来说,它将 isShorter 的第一个参数 _1 绑定到了 isShorter 的第二个参数,将 isShorter 的第二个参数 _2 绑定到了 isShorter 的第一个参数。这意味着排序将按照字符串长度从长到短的顺序进行,因为在比较时,先比较的是第二个字符串的长度(即 _2),再比较第一个字符串的长度(即 _1)。

综上所述,这段代码展示了如何使用不同的比较函数对象来对字符串向量进行排序,并且展示了如何使用 bind 函数适配器来修改函数的参数顺序。

默认情况下,bind函数的非占位符参数被拷贝到bind返回的可调用对象中。但有些类型不支持拷贝操作。

如果希望传递给bind一个对象而又不拷贝它,则必须使用标准库的ref函数。ref函数返回一个对象,包含给定的引用,此对象是可以拷贝的。cref函数生成保存const引用的类。

1
2
ostream &print(ostream &os, const string &s, char c);
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));

这段代码使用了 for_each 算法,结合 bind 函数适配器,来对字符串向量 words 中的每个字符串调用 print 函数进行处理。让我们逐步解释:

  1. ostream &print(ostream &os, const string &s, char c);: 这是一个名为 print 的函数声明,它接受一个输出流 os、一个字符串 s 和一个字符 c 作为参数,并返回一个输出流引用。这个函数的功能可能是将字符串 s 输出到输出流 os 中,并在其末尾添加字符 c

  2. for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));: 这行代码使用了 for_each 算法,它遍历了字符串向量 words 中的每个字符串,并对每个字符串调用 print 函数进行处理。在这里,bind 函数适配器用来绑定 print 函数的参数。具体来说:

    • words.begin()words.end() 指定了需要遍历的范围,即从 words 的起始位置到末尾位置。

    • bind(print, ref(os), _1, ' ')print 函数与参数绑定,创建了一个函数对象。这里:

      • print 是要绑定的函数。
      • ref(os) 用来将输出流 os 以引用的方式传递给 print 函数,因为通常输出流需要以引用方式传递。
      • _1 是一个占位符,表示绑定的函数对象将接受一个参数,该参数将在调用时传递给 print 函数的第二个参数 s_1for_each 算法中遍历的每个字符串 s 相对应。
      • ' ' 是要绑定的函数对象的最后一个参数,表示将空格字符添加到每个字符串的末尾。

    因此,for_each 算法会遍历 words 中的每个字符串,并对每个字符串调用 print 函数,将其输出到输出流 os 中,并在末尾添加一个空格字符。

综上所述,这段代码展示了如何使用 for_each 算法和 bind 函数适配器来对字符串向量中的每个元素调用自定义函数进行处理,并根据需要绑定函数的参数。

再探迭代器(Revisiting Iterators)

除了为每种容器定义的迭代器之外,标准库还在头文件iterator中定义了另外几种迭代器。

  • 插入迭代器(insert iterator):该类型迭代器被绑定到容器对象上,可用来向容器中插入元素。
  • 流迭代器(stream iterator):该类型迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
  • 反向迭代器(reverse iterator):该类型迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。
  • 移动迭代器(move iterator):该类型迭代器用来移动容器元素。

插入迭代器(Insert Iterators)

插入器是一种迭代器适配器,它接受一个容器参数,生成一个插入迭代器。通过插入迭代器赋值时,该迭代器调用容器操作向给定容器的指定位置插入一个元素。

插入器有三种类型,区别在于元素插入的位置:

  • back_inserter:创建一个调用push_back操作的迭代器。
  • front_inserter:创建一个调用push_front操作的迭代器。
  • inserter:创建一个调用insert操作的迭代器。此函数接受第二个参数,该参数必须是一个指向给定容器的迭代器,元素会被插入到该参数指向的元素之前。
1
2
3
4
5
6
list<int> lst = { 1,2,3,4 };
list<int> lst2, lst3; // empty lists
// after copy completes, lst2 contains 4 3 2 1
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
// after copy completes, lst3 contains 1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));

这段代码使用了STL算法 copy 以及插入迭代器 front_inserterinserter 来将一个列表 lst 中的元素复制到另外两个列表 lst2lst3 中。让我们逐步解释这段代码的功能:

  1. list<int> lst = { 1,2,3,4 };: 这行代码创建了一个名为 lst 的列表,并初始化它为包含 1、2、3、4 这四个整数的列表。

  2. list<int> lst2, lst3;: 这行代码创建了两个名为 lst2lst3 的空列表。

  3. copy(lst.cbegin(), lst.cend(), front_inserter(lst2));: 这行代码使用 copy 算法,将列表 lst 中的元素复制到列表 lst2 中。front_inserter(lst2) 是一个插入迭代器,它会将复制的元素插入到目标列表 lst2 的前面。因此,复制完成后,lst2 中的元素顺序是 4、3、2、1。

  4. copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));: 这行代码也使用 copy 算法,将列表 lst 中的元素复制到列表 lst3 中。inserter(lst3, lst3.begin()) 是另一种插入迭代器,它会将复制的元素插入到目标列表 lst3 中的指定位置,即 lst3.begin() 所指示的位置。由于是从列表的开始处插入,因此复制完成后,lst3 中的元素顺序是 1、2、3、4。

综上所述,这段代码展示了如何使用插入迭代器 front_inserterinserter 将列表中的元素复制到另一个列表中,并控制复制后元素在目标列表中的顺序。

iostream迭代器(iostream Iterators)

istream_iterator从输入流读取数据,ostream_iterator向输出流写入数据。这些迭代器将流当作特定类型的元素序列处理。

创建流迭代器时,必须指定迭代器读写的对象类型。istream_iterator使用>>来读取流,因此istream_iterator要读取的类型必须定义了>>运算符。创建istream_iterator时,可以将其绑定到一个流。如果默认初始化,则创建的是尾后迭代器。

iostream迭代器(iostream Iterators)

istream_iterator从输入流读取数据,ostream_iterator向输出流写入数据。这些迭代器将流当作特定类型的元素序列处理。

创建流迭代器时,必须指定迭代器读写的对象类型。istream_iterator使用>>来读取流,因此istream_iterator要读取的类型必须定义了>>运算符。创建istream_iterator时,可以将其绑定到一个流。如果默认初始化,则创建的是尾后迭代器。

这段代码涉及到了 C++ 中的输入流迭代器 istream_iterator 的使用,以及如何从不同的输入源(标准输入流 cin 和文件流 ifstream)中读取数据。让我们一步步来解释:

  1. istream_iterator<int> int_it(cin);: 这行代码创建了一个名为 int_itistream_iterator 对象,它被初始化为从标准输入流 cin 中读取整数。这意味着它将从标准输入中读取用户输入的整数数据,并将其作为迭代器进行处理。

  2. istream_iterator<int> int_eof;: 这行代码创建了一个名为 int_eofistream_iterator 对象,它被初始化为默认的“结束迭代器”值。结束迭代器用于指示输入流的末尾,通常用于在循环中检查迭代器是否已经到达了流的末尾。

  3. ifstream in("afile");: 这行代码创建了一个名为 in 的文件流对象,并打开了名为 "afile" 的文件。ifstream 类是 C++ 中用于从文件中读取数据的输入流类。

  4. istream_iterator<string> str_it(in);: 这行代码创建了一个名为 str_itistream_iterator 对象,它被初始化为从文件流 in 中读取字符串。这意味着它将从文件 "afile" 中读取字符串数据,并将其作为迭代器进行处理。

综上所述,这段代码展示了如何使用 istream_iterator 类从标准输入流和文件流中读取数据,并将其用作迭代器以便于在程序中进行处理。

对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或IO错误,迭代器的值就与尾后迭代器相等。

1
2
3
4
5
6
istream_iterator<int> in_iter(cin);     // read ints from cin
istream_iterator<int> eof; // istream ''end'' iterator
while (in_iter != eof) // while there's valid input to read
// postfix increment reads the stream and returns the old value of the iterator
// we dereference that iterator to get the previous value read from the stream
vec.push_back(*in_iter++);

这段代码使用了输入流迭代器 istream_iterator 从标准输入流 cin 中读取整数,并将其存储到一个向量 vec 中。让我逐步解释:

  1. istream_iterator<int> in_iter(cin);: 这行代码创建了一个名为 in_iteristream_iterator 对象,它被初始化为从标准输入流 cin 中读取整数。这意味着 in_iter 将从标准输入中读取用户输入的整数数据,并将其作为迭代器进行处理。

  2. istream_iterator<int> eof;: 这行代码创建了一个名为 eofistream_iterator 对象。在这种情况下,没有提供流作为参数,因此 eof 被初始化为默认的“结束迭代器”值。结束迭代器用于指示输入流的末尾,通常用于在循环中检查迭代器是否已经到达了流的末尾。

  3. while (in_iter != eof): 这是一个 while 循环,它的条件是迭代器 in_iter 不等于结束迭代器 eof。这个条件保证了只要输入流中还有有效的输入,就会继续执行循环。

  4. vec.push_back(*in_iter++);: 在循环体内部,这行代码执行了以下操作:

    • *in_iter 解引用迭代器 in_iter,获取迭代器当前指向的元素(即从输入流读取的整数)。

    • vec.push_back(...) 将解引用后的值添加到向量 vec 的末尾。

    • in_iter++ 是迭代器的后置递增操作符,它将迭代器向前移动一个位置,指向下一个输入流中的元素。注意,这里是后置递增,因此 *in_iter 返回的是旧的迭代器指向的元素,然后迭代器再自增,指向下一个位置。

综上所述,这段代码会持续从标准输入流中读取整数,直到输入流中没有更多的有效数据为止,然后将读取的整数存储到向量 vec 中。

可以直接使用流迭代器构造容器。

1
2
istream_iterator<int> in_iter(cin), eof;    // read ints from cin
vector<int> vec(in_iter, eof); // construct vec from an iterator range
  1. istream_iterator<int> in_iter(cin), eof;: 这行代码创建了两个 istream_iterator 对象。第一个对象是 in_iter,它被初始化为从标准输入流 cin 中读取整数。第二个对象是 eof,它是一个默认构造的 istream_iterator,没有指定输入流作为参数。因此,eof 被初始化为默认的“结束迭代器”值。结束迭代器用于指示输入流的末尾,通常用于在循环中检查迭代器是否已经到达了流的末尾。

  2. vector<int> vec(in_iter, eof);: 这行代码创建了一个名为 vec 的向量,并通过迭代器范围构造函数来初始化它。这个范围从 in_itereof,即从标准输入流中读取的整数序列的起始迭代器到结束迭代器。向量 vec 会包含从输入流中读取的所有整数。

综上所述,这段代码的作用是从标准输入流 cin 中读取整数,然后将这些整数存储到向量 vec 中。

istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。但可以保证在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。

定义ostream_iterator对象时,必须将其绑定到一个指定的流。不允许定义空的或者表示尾后位置的ostream_iterator

*++运算符实际上不会对ostream_iterator对象做任何操作。但是建议代码写法与其他迭代器保持一致。

1
2
3
4
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
*out_iter++ = e; // the assignment writes this element to cout
cout << endl;

可以为任何定义了<<运算符的类型创建istream_iterator对象,为定义了>>运算符的类型创建ostream_iterator对象。

反向迭代器(Reverse Iterators)

递增反向迭代器会移动到前一个元素,递减会移动到后一个元素。

1
2
3
sort(vec.begin(), vec.end());   // sorts vec in "normal" order
// sorts in reverse: puts the smallest element at the end of vec
sort(vec.rbegin(), vec.rend());

不能从forward_list或流迭代器创建反向迭代器。

调用反向迭代器的base函数可以获得其对应的普通迭代器。

1
2
3
4
5
6
// find the last element in a comma-separated list
auto rcomma = find(line.crbegin(), line.crend(), ',');
// WRONG: will generate the word in reverse order
cout << string(line.crbegin(), rcomma) << endl;
// ok: get a forward iterator and read to the end of line
cout << string(rcomma.base(), line.cend()) << endl;

反向迭代器的目的是表示元素范围,而这些范围是不对称的。用普通迭代器初始化反向迭代器,或者给反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同元素。

这段代码是在处理一个逗号分隔的字符串时,使用了反向迭代器来找到最后一个逗号,并根据其位置来拆分字符串。让我们逐步解释代码的功能:

  1. auto rcomma = find(line.crbegin(), line.crend(), ',');: 这行代码使用了 find 算法,通过反向迭代器在字符串 line 中查找最后一个逗号的位置。line.crbegin() 返回 line 字符串的反向起始迭代器,而 line.crend() 返回 line 字符串的反向结束迭代器。这样,find 将从字符串的末尾开始搜索,直到找到第一个逗号。

  2. cout << string(line.crbegin(), rcomma) << endl;: 这行代码尝试使用从字符串末尾到最后一个逗号之间的字符来构造一个新的字符串。但是,它使用了 string 构造函数,将反向迭代器作为参数传递,这会导致构造的字符串是反向的,即以逆序的方式输出。

  3. cout << string(rcomma.base(), line.cend()) << endl;: 这行代码则采取了不同的方法。它使用了 base() 函数来获取反向迭代器的正向迭代器,然后使用这两个正向迭代器来构造一个新的字符串,从逗号的下一个位置直到字符串的末尾。因此,它能够正确地输出从逗号后面到字符串末尾的内容。

综上所述,第二行代码由于使用了反向迭代器构造字符串,导致了字符串逆序输出的问题,而第三行代码则通过转换为正向迭代器来解决了这个问题,正确地输出了所需的字符串片段。

泛型算法结构(Structure of Generic Algorithms)

五类迭代器(The Five Iterator Categories)

C++标准指定了泛型和数值算法的每个迭代器参数的最小类别。对于迭代器实参来说,其能力必须大于或等于规定的最小类别。向算法传递更低级的迭代器参数会产生错误(大部分编译器不会提示错误)。

迭代器类别:

  • 输入迭代器(input iterator):可以读取序列中的元素,只能用于单遍扫描算法。必须支持以下操作:
    • 用于比较两个迭代器相等性的相等==和不等运算符!=
    • 用于推进迭代器位置的前置和后置递增运算符++
    • 用于读取元素的解引用运算符*;解引用只能出现在赋值运算符右侧。
    • 用于读取元素的箭头运算符->
  • 输出迭代器(output iterator):可以读写序列中的元素,只能用于单遍扫描算法,通常指向目的位置。必须支持以下操作:
    • 用于推进迭代器位置的前置和后置递增运算符++
    • 用于读取元素的解引用运算符*;解引用只能出现在赋值运算符左侧(向已经解引用的输出迭代器赋值,等价于将值写入其指向的元素)。
  • 前向迭代器(forward iterator):可以读写序列中的元素。只能在序列中沿一个方向移动。支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此可以使用前向迭代器对序列进行多遍扫描。
  • 双向迭代器(bidirectional iterator):可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,还支持前置和后置递减运算符--。除forward_list之外的其他标准库容器都提供符合双向迭代器要求的迭代器。
  • 随机访问迭代器(random-access iterator):可以在常量时间内访问序列中的任何元素。除了支持所有双向迭代器的操作之外,还必须支持以下操作:
    • 用于比较两个迭代器相对位置的关系运算符<<=>>=
    • 迭代器和一个整数值的加减法运算++=--=,计算结果是迭代器在序列中前进或后退给定整数个元素后的位置。
    • 用于两个迭代器上的减法运算符-,计算得到两个迭代器的距离。
    • 下标运算符[]

算法形参模式(Algorithm Parameter Patterns)

大多数算法的形参模式是以下四种形式之一:

1
2
3
4
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);

这个描述指的是STL(标准模板库)中的算法通常采用的四种参数模式。让我们逐一解释这些参数模式:

  1. alg(beg, end, other args);: 这种模式是最常见的,其中 alg 是算法的名称,begend 表示一个范围,通常是指定容器的起始迭代器和结束迭代器。算法将在此范围内操作。other args 表示其他可能需要传递给算法的参数。

  2. alg(beg, end, dest, other args);: 在这种情况下,除了传递输入范围的起始和结束迭代器外,还传递了目标容器的起始迭代器 dest。算法将结果存储到目标容器中,而不是修改输入范围。other args 表示其他可能需要传递给算法的参数。

  3. alg(beg, end, beg2, other args);: 在这种情况下,除了传递了输入范围的起始和结束迭代器外,还传递了第二个范围的起始迭代器 beg2。某些算法需要使用两个范围进行操作,例如合并两个有序序列。other args 表示其他可能需要传递给算法的参数。

  4. alg(beg, end, beg2, end2, other args);: 这种模式类似于第三种模式,不同之处在于它还传递了第二个范围的结束迭代器 end2。这种模式通常用于需要操作两个范围的算法,例如交集、并集等。other args 表示其他可能需要传递给算法的参数。

这些参数模式提供了一种通用的方式来在STL中调用算法,并且能够适应不同的情况和需求。

算法命名规范(Algorithm Naming Conventions)

接受谓词参数的算法都有附加的_if后缀。

1
2
find(beg, end, val);       // find the first instance of val in the input range
find_if(beg, end, pred); // find the first instance for which pred is true

将执行结果写入额外目的空间的算法都有_copy后缀。

1
2
reverse(beg, end);              // reverse the elements in the input range
reverse_copy(beg, end, dest); // copy elements in reverse order into dest

一些算法同时提供_copy_if版本。

特定容器算法(Container-Specific Algorithms)

对于listforward_list类型,应该优先使用成员函数版本的算法,而非通用算法。

链表特有版本的算法操作会改变底层容器。


Exception Handling: A Deeper Look

异常是程序执行过程中出现问题的指示。异常处理使您能够创建能够解决(或处理)异常的应用程序。在许多情况下,这使得程序能够继续执行,就好像没有遇到任何问题一样。

异常处理是程序中一种重要的技术,它可以帮助我们在程序运行过程中处理各种错误和异常情况,使程序更加健壮。

  1. 异常类定义

    • 提供的代码定义了一个自定义的异常类,名为 DivideByZeroException,它是从 std::runtime_error 类派生而来的。
    • 这个异常类表示了试图进行除以零操作的情况。
    • 该类通常包含一个构造函数,用于初始化基类 runtime_error 并传递错误消息。
    1
    2
    3
    4
    5
    6
    7
    // DivideByZeroException.h
    #include <stdexcept>

    class DivideByZeroException : public std::runtime_error {
    public:
    DivideByZeroException() : std::runtime_error("试图除以零") {}
    };
  2. 函数定义

    • 定义了一个名为 quotient 的函数,用于执行除法操作,并处理除以零的可能性。
    • 这个函数接受两个整数参数,并返回一个双精度浮点数结果。
    • 在执行除法之前,函数会检查分母是否为零,如果是,则抛出一个 DivideByZeroException 异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // fig17_02.cpp
    #include <iostream>
    #include "DivideByZeroException.h"

    double quotient(int numerator, int denominator) {
    if (denominator == 0) {
    throw DivideByZeroException();
    }
    return static_cast<double>(numerator) / denominator;
    }
  3. 主函数中的异常处理

    • 主函数通过在 try 块中调用 quotient 函数来演示异常处理。
    • 如果发生异常(即除以零),它将被一个专门设计用于处理 DivideByZeroExceptioncatch 块捕获。
    • catch 块打印出错信息并提示用户输入新的值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int main() {
    int num1, num2;
    std::cout << "请输入两个整数:";
    std::cin >> num1 >> num2;

    try {
    double result = quotient(num1, num2);
    std::cout << "商是:" << result << std::endl;
    } catch (const DivideByZeroException& e) {
    std::cerr << "错误:" << e.what() << std::endl;
    std::cout << "请重新输入非零的分母。" << std::endl;
    }

    return 0;
    }

在 C++ 中,e.what() 是异常类的一个成员函数,通常用于返回异常对象的错误消息。在标准异常类 std::exception 中,what() 函数返回一个 C 风格的字符串,该字符串包含了关于异常的描述信息。

在给定的例子中,e.what() 是在 DivideByZeroException 类中调用的,它继承自 std::runtime_error,而后者又继承自 std::exception。因此,e.what() 返回的是 DivideByZeroException 对象中存储的错误消息字符串。

在异常处理中,通常会使用 e.what() 来获取异常的描述信息,并在处理异常的代码块中使用该信息进行适当的处理或输出。

  1. 控制流
    • 如果在 try 块中没有发生异常,那么其中的语句将正常执行。
    • 如果发生异常,则控制转移到相应的 catch 块。
    • 处理完异常后,控制流将恢复到最后一个 catch 块后面的语句。
    • 如果没有找到匹配的 catch 块,程序可能会终止,或者尝试在调用函数中找到包含的 try 块(栈展开)。

1. 资源管理和异常处理

  • 当函数使用资源(如文件)时,可能希望在异常发生时释放资源(如关闭文件)。
  • 异常处理程序在接收到异常时,可以释放资源,然后通过 throw; 语句重新抛出异常,通知调用者发生了异常。
  • 无论处理程序是否能够处理异常,都可以通过重新抛出异常进行进一步处理。
  • 下一个封闭的 try 块会检测重新抛出的异常,然后尝试处理该异常的 catch 处理程序列在该封闭 try 块之后。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <stdexcept>

void processResource() {
// Assume opening a file
// If an exception occurs during file processing, we want to close the file
// before rethrowing the exception
std::cout << "Processing resource..." << std::endl;
throw std::runtime_error("Exception occurred during resource processing");
// Assume closing the file
}

int main() {
try {
processResource();
} catch (...) {
std::cerr << "Caught exception in main" << std::endl;
// Rethrow the exception for further processing
throw; // Rethrow the same exception
}
return 0;
}

在这个示例中,processResource 函数模拟了使用资源(例如文件)的过程。如果在处理资源期间发生异常,我们希望在重新抛出异常之前释放资源。main 函数中的异常处理程序捕获异常,并重新抛出相同的异常以进行进一步处理。

栈展开

  1. 异常抛出和捕获机制
    • 当在程序执行过程中抛出异常时,程序会尝试在当前作用域内的 try...catch 块中捕获该异常。
    • 如果没有合适的 try...catch 块来捕获异常,异常会沿着调用栈向上传播,直到遇到能够处理异常的 try...catch 块或者直到达到程序的入口点。
    • 如果在传播过程中遇到了没有被捕获的异常,程序将终止。
  2. 栈展开
    • 当异常在一个函数中抛出但未在该函数内被捕获时,会触发栈展开过程。
    • 栈展开意味着异常发生的函数会被“展开”,即函数的执行被中断,函数内的局部变量被销毁,控制权返回到调用该函数的语句处。
    • 然后,程序会尝试在调用函数的上一级作用域中捕获异常,即沿着调用栈向上继续查找 try...catch 块。
    • 如果在调用栈的某个级别找到了合适的 try...catch 块,异常将被捕获并相应处理;否则,栈展开会继续,直到达到程序的入口点或者直到异常被捕获。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <stdexcept>

void thirdFunction() {
std::cout << "Inside thirdFunction" << std::endl;
throw std::runtime_error("Exception thrown from thirdFunction");
}

void secondFunction() {
std::cout << "Inside secondFunction" << std::endl;
thirdFunction();
}

void firstFunction() {
std::cout << "Inside firstFunction" << std::endl;
secondFunction();
}

int main() {
try {
std::cout << "Inside main" << std::endl;
firstFunction();
} catch (const std::runtime_error& e) {
std::cerr << "Caught exception in main: " << e.what() << std::endl;
}
return 0;
}

在这个示例中,我们有四个函数:mainfirstFunctionsecondFunctionthirdFunction。每个函数都会打印一条消息,以便我们可以了解程序的执行流程。同时,在 thirdFunction 中会抛出一个 std::runtime_error 异常。

当我们运行这段代码时,会发生以下事情:

  1. main 函数开始执行,打印 "Inside main"。
  2. main 调用 firstFunction
  3. firstFunction 打印 "Inside firstFunction",然后调用 secondFunction
  4. secondFunction 打印 "Inside secondFunction",然后调用 thirdFunction
  5. thirdFunction 打印 "Inside thirdFunction",然后抛出一个异常。
  6. 异常被抛出,当前函数 thirdFunction 结束执行,它的局部变量被销毁,控制权返回到调用它的函数 secondFunction
  7. secondFunction 没有捕获异常,因此终止执行,它的局部变量被销毁,控制权返回到调用它的函数 firstFunction
  8. firstFunction 没有捕获异常,因此终止执行,它的局部变量被销毁,控制权返回到 main 函数。
  9. main 函数的异常处理程序捕获了异常,并打印出错误消息,程序继续执行。

因此,程序的打印输出将会是:

1
2
3
4
5
Inside main
Inside firstFunction
Inside secondFunction
Inside thirdFunction
Caught exception in main: Exception thrown from thirdFunction
  1. 异常处理与软件元素交互
    • 在一个程序中,各种软件元素(如成员函数、构造函数、析构函数和类)相互交互,执行各种操作。
    • 当这些软件元素执行过程中遇到问题时,它们可能会使用异常来通知程序发生了异常情况,这种情况可能需要程序进行处理。
  2. 自定义错误处理
    • 异常处理机制使得程序可以实现自定义的错误处理逻辑。
    • 每个应用程序可能对异常情况的处理方式不同,因此程序员可以根据具体需求编写适合自己应用程序的异常处理代码。
  3. 预定义组件和应用程序特定组件的交互
    • 复杂的应用程序通常由预定义的通用组件和特定于应用程序的组件构成。
    • 当预定义组件遇到问题时,它们需要一种机制来通知应用程序特定组件,以便应用程序可以采取适当的行动。
  4. C++11中的异常规范说明
    • 从C++11开始,如果一个函数不会抛出任何异常,并且不会调用任何会抛出异常的函数,应该明确声明该函数不会抛出异常。
    • 这样做可以向客户端程序员明确表示,他们不需要在调用该函数的地方使用try块来捕获异常。
    • 在函数的参数列表后面加上noexcept关键字,表示该函数不会抛出异常。
    • 如果声明了noexcept的函数调用了另一个会抛出异常或执行throw语句的函数,程序将终止。

Constructors, Destructors and Exception Handling

当在构造函数中检测到错误时,对象的构造函数应该如何响应?因为构造函数无法返回值来指示错误,我们必须选择另一种方式来指示对象未正确构造。

一种方案是返回未正确构造的对象,并希望任何使用它的人会进行适当的测试,以确定它处于不一致状态。另一种方案是在构造函数之外设置某些变量。

首选的替代方案是要求构造函数抛出一个包含错误信息的异常,从而为程序提供处理失败的机会。

在构造函数抛出异常之前,会调用构造已完成的任何成员对象的析构函数作为正在构造的对象的一部分。在try块中,会在捕获异常之前调用每个自动对象的析构函数。确保在异常处理程序开始执行时,堆栈展开已经完成。如果由于堆栈展开而调用的析构函数抛出异常,则程序将终止。这已被关联到各种安全攻击。

如果一个对象有成员对象,并且如果在外部对象完全构造之前抛出异常,则将执行已构造的成员对象的析构函数。

如果在异常发生时部分构造了对象数组,则仅调用数组中已构造对象的析构函数。

初始化局部对象以获取资源

异常可能会阻止通常释放资源(如内存或文件)的代码操作,从而导致资源泄漏,阻止其他程序获取资源。解决这个问题的一种技术是初始化局部对象以获取资源。当异常发生时,该对象的析构函数将被调用,并且可以释放资源。

让我们通过一个代码示例来说明这些概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <stdexcept>

class Resource {
public:
Resource() {
// Simulate resource acquisition
std::cout << "Resource acquired" << std::endl;
}

~Resource() {
// Simulate resource release
std::cout << "Resource released" << std::endl;
}
};

class MyClass {
private:
Resource res;

public:
MyClass() {
// Simulate constructor error
throw std::runtime_error("Constructor error");
}
};

int main() {
try {
MyClass obj; // Constructor of MyClass throws exception
} catch (const std::runtime_error& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
// Resource will be automatically released here due to destructor call
}

return 0;
}

在这个示例中,MyClass 的构造函数抛出了一个 std::runtime_error 异常,模拟了构造函数中的错误情况。当异常抛出时,Resource 类中的析构函数被调用,模拟了资源的释放过程。这样,即使在构造函数中发生了异常,资源也能够得到正确释放,从而避免了资源泄漏。

Exceptions and Inheritance

在C++中,可以从一个共同的基类派生多个异常类,这些异常类可能用于不同的错误情况。通过使用继承,我们可以实现对相关异常的多态处理。让我们深入剖析这个概念,并提供一个代码示例来说明。

考虑以下的异常类继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <exception>

// 基类异常
class BaseException : public std::exception {
public:
virtual const char* what() const noexcept {
return "Base Exception";
}
};

// 派生异常类1
class DerivedException1 : public BaseException {
public:
const char* what() const noexcept override {
return "Derived Exception 1";
}
};

// 派生异常类2
class DerivedException2 : public BaseException {
public:
const char* what() const noexcept override {
return "Derived Exception 2";
}
};

int main() {
try {
// 模拟抛出 DerivedException1 异常
throw DerivedException1();
} catch (const BaseException& e) {
std::cerr << "Caught Base Exception: " << e.what() << std::endl;
}

try {
// 模拟抛出 DerivedException2 异常
throw DerivedException2();
} catch (const BaseException& e) {
std::cerr << "Caught Base Exception: " << e.what() << std::endl;
}

return 0;
}

在这个示例中,我们定义了一个基类异常 BaseException,然后派生了两个异常类 DerivedException1DerivedException2。这两个派生类覆盖了 what() 函数以提供自己的异常信息。

main() 函数中,我们模拟了抛出这两种异常,并使用基类异常的引用来捕获它们。由于派生类是从基类继承的,因此基类异常的引用可以捕获任何派生异常的对象。

这种方式允许我们在处理异常时采用多态的方式,即使我们在 catch 块中使用基类异常的引用,我们仍然可以捕获并处理所有相关的派生异常。

Processing new Failures

两种处理失败的方式:通过抛出 bad_alloc 异常和通过设置 set_new_handler 函数。

  1. new 抛出 bad_alloc 异常
    • 当内存分配失败时,operator new 可能会抛出 bad_alloc 异常,通常发生在内存不足的情况下。
    • 通过 try-catch 机制捕获异常并处理。
    • 下面是一个示例,其中使用 try-catch 来捕获 bad_alloc 异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <new>

int main() {
try {
for (int i = 0; i < 50; ++i) {
double *ptr = new double[50000000];
std::cout << "Iteration " << i + 1 << " successful" << std::endl;
delete[] ptr; // Release memory
}
} catch (const std::bad_alloc& e) {
std::cerr << "Exception occurred: " << e.what() << std::endl;
}

return 0;
}
  1. new 返回 nullptr
    • C++标准允许使用一个老版本的 new,它在内存分配失败时返回 nullptr
    • 可以使用 nothrow 参数来使用此版本的 new,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <new>

int main() {
double *ptr = new (std::nothrow) double[50000000];
if (ptr == nullptr) {
std::cerr << "Memory allocation failed" << std::endl;
} else {
std::cout << "Memory allocation successful" << std::endl;
delete[] ptr; // Release memory
}

return 0;
}
  1. 使用 set_new_handler 函数处理失败
    • set_new_handler 函数用于设置一个新的内存分配失败处理函数。
    • 如果内存分配失败,而已经注册了新的处理函数,则该处理函数会被调用。
    • 下面是一个示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <new>
#include <cstdlib>

void customNewHandler() {
std::cerr << "Memory allocation failed" << std::endl;
std::abort(); // Terminate program
}

int main() {
std::set_new_handler(customNewHandler);

for (int i = 0; i < 50; ++i) {
double *ptr = new double[50000000];
std::cout << "Iteration " << i + 1 << " successful" << std::endl;
delete[] ptr; // Release memory
}

return 0;
}

###unique_ptr C++11 中的 unique_ptr 类模板,它是用于管理动态分配内存的智能指针。

  1. unique_ptr 的基本使用
    • unique_ptr 是一个智能指针,用于管理动态分配的内存。
    • unique_ptr 对象被销毁时(例如,当其超出作用域时),它会自动调用 delete 操作来释放其指向的内存。
    • 可以通过 *-> 运算符来访问 unique_ptr 指向的对象,就像使用原始指针一样。

下面是一个示例,演示了如何使用 unique_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <memory>

class Integer {
public:
Integer(int val) : value(val) {}
void display() {
std::cout << "Value: " << value << std::endl;
}
private:
int value;
};

int main() {
std::unique_ptr<Integer> ptrToInteger(new Integer(42));
ptrToInteger->display(); // Access object via unique_ptr

// No need to manually delete ptrToInteger
// When ptrToInteger goes out of scope, its destructor will automatically release the memory

return 0;
}

在这个示例中,我们使用 unique_ptr 来管理动态分配的 Integer 对象。当 ptrToInteger 超出作用域时,它的析构函数会自动释放内存。

  1. unique_ptr 管理动态数组
    • unique_ptr 还可以用于管理动态分配的数组。
    • unique_ptr 管理一个数组时,它会使用 delete [] 来释放内存,确保每个数组元素都会调用析构函数。
    • unique_ptr 提供了重载的 [] 运算符,以便访问数组的元素。

下面是一个示例,演示了如何使用 unique_ptr 来管理动态数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <memory>
#include <string>

int main() {
std::unique_ptr<std::string[]> ptr(new std::string[10]);

ptr[2] = "hello"; // Assign value to array element
std::cout << ptr[2] << std::endl; // Access array element

// No need to manually delete ptr
// When ptr goes out of scope, its destructor will automatically release the memory

return 0;
}

异常类

C++ 标准库中的异常类,包括 runtime_errorlogic_error,它们各自派生了一系列的异常类,用于指示程序执行过程中可能发生的错误。

  1. logic_error 类及其派生类
    • logic_error 类是一系列表示程序逻辑错误的标准异常类的基类。
    • 例如,invalid_argument 类表示函数接收到了无效的参数,通常是因为调用者传递了不合法的参数。
    • 另外,length_error 类表示对象的长度超出了允许的最大大小,out_of_range 类表示值超出了允许的范围。
    • 下面是一个示例,演示了如何使用 invalid_argument 异常类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdexcept>
#include <iostream>

void processInput(int value) {
if (value < 0) {
throw std::invalid_argument("Value must be non-negative");
}
// Process the input value
}

int main() {
try {
processInput(-5);
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << std::endl;
}

return 0;
}
  1. runtime_error 类及其派生类
    • runtime_error 类是一系列表示程序执行时错误的标准异常类的基类。
    • 例如,overflow_error 类表示算术运算溢出,而 underflow_error 类表示算术运算下溢。
    • 下面是一个示例,演示了如何使用 overflow_error 异常类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdexcept>
#include <iostream>

void performArithmeticOperation() {
// Simulating an arithmetic overflow
throw std::overflow_error("Arithmetic overflow occurred");
}

int main() {
try {
performArithmeticOperation();
} catch (const std::overflow_error& e) {
std::cerr << "Overflow error: " << e.what() << std::endl;
}

return 0;
}

Introduction to Custom Templates

模板的声明

1
2
template <typename T>  int compare (T t1, T t2);
template <typename T> class compare;

定义一个模板函数

1
2
3
4
5
6
7
8
9
10
template <typename T>
int compare(T & t1, T & t2)
{
if(t1 > t2)
return 1;
if(t1 == t2)
return 0;
if(t1 < t2)
return -1;
}

定义一个模板类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
class compare
{
private:
T _val;
public:
explicit compare(T & val) : _val(val) { }
explicit compare(T && val) : _val(val) { }
bool operator==(T & t)
{
return _val == t;
}
};

模板定义并不是真正的定义了一个函数或者类,而是编译器根据程序员缩写的模板和形参来自己写出一个对应版本的定义,这个过程叫做模板实例化。编译器成成的版本通常被称为模板的实例。编译器为程序员生成对应版本的具体过程。类似宏替换。

模板类在没有调用之前是不会生成代码的。

由于编译器并不会直接编译模板本身,所以模板的定义通常放在头文件中。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack {
private:
vector<T> elems; // 元素

public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};

template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}

int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈

// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;

// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}