001 C++语言纲要
1
Gemini(2.0 Flash Thinking) Prompts:
2
3
请生成系统性、结构化的学习笔记,要求涵盖各个知识点。关于:c++语言纲要。
4
**内容要求**如下:
5
1. 先写介绍,再写正文,最后做总结。
6
2. 合理使用emoji;
7
3. 不能遗漏任何知识点;
8
4. 对每个API(函数、变量)给出名称、用途、原型、代码、注释;
9
5. 在专业的角度指明最佳实践、常见错误用法、陷阱等;
10
**格式要求**如下:
11
1. **多级标题**:用 `## 1 一级标题` → `### 1.1 二级标题` → `### 1.1.1 三级标题` 的层级编号;
12
2. **单级条目**:所有 `1.` `2.` `3.` 这样的单级列表,改用带圈数字 `①` `②` `③` 表示;
13
3. **禁止使用**:Markdown 的 `-` `*` 无序列表或 `1.` `2.` 有序列表语法;
14
4. 请自动生成层级编号(如 1.1、1.2),无需我手动输入数字。不要大标题,正文从## 1. 开始;
15
5. 在表格或代码块的前后添加空行;
C++语言纲要
简介 🚀
C++ 是一种功能强大的通用编程语言,它是 C 语言的扩展,增加了面向对象编程(OOP)、泛型编程等特性。C++ 以其高性能、灵活性和广泛的应用领域而闻名,常用于系统软件、游戏开发、嵌入式系统、高性能计算以及客户端和服务器应用程序的开发。本学习笔记旨在系统性地梳理 C++ 的核心概念和语法,帮助学习者建立扎实的 C++ 基础。
1. C++ 基础
1.1 第一个 C++ 程序 🌍
1.1.1 main
函数
① 名称:main
② 用途:程序的入口点,C++ 程序从 main
函数开始执行。
③ 原型:
1
int main() { /* ... */ }
2
// 或者
3
int main(int argc, char *argv[]) { /* ... */ }
④ 代码:
1
#include <iostream> // 包含输入输出流库
2
3
int main() {
4
std::cout << "Hello, C++!" << std::endl; // 输出 "Hello, C++!" 到控制台
5
return 0; // 表示程序执行成功
6
}
⑤ 注释:
→ #include <iostream>
:预处理指令,用于包含 iostream 库,该库提供了输入和输出功能。
→ int main()
:定义主函数,返回一个整型值。
→ std::cout
:标准输出流对象,通常连接到控制台。
→ <<
:插入运算符,用于将数据发送到输出流。
→ "Hello, C++!"
:字符串字面量。
→ std::endl
:换行符,用于在输出后移动到下一行。
→ return 0;
:表示程序正常结束。
最佳实践:始终在 main
函数的末尾返回 0,以表明程序成功执行。
常见错误用法:忘记包含必要的头文件,导致编译器报错。
1.2 基本语法元素 📝
1.2.1 注释
① 单行注释:以 //
开头,直到行尾。
② 多行注释:以 /*
开头,以 */
结尾。
1.2.2 标识符
① 定义:用于命名变量、函数、类等的名称。
② 规则:
→ 由字母、数字和下划线组成。
→ 必须以字母或下划线开头。
→ 区分大小写。
→ 不能是 C++ 的关键字。
最佳实践:选择具有描述性的标识符,提高代码的可读性。
常见错误用法:使用关键字作为标识符,或以数字开头。
1.2.3 关键字
① 定义:C++ 语言预定义的具有特殊含义的单词,不能用作标识符。
② 示例:int
, float
, class
, if
, else
, while
, for
, return
等。
1.2.4 空格和语句分隔符
① 空格:空格、制表符和换行符统称为空白字符,用于提高代码的可读性,编译器通常会忽略多余的空白字符。
② 语句分隔符:分号 ;
用于标记一个语句的结束。
1.3 数据类型 📊
1.3.1 基本数据类型
① 整型:
→ int
:通常占用 4 个字节,用于存储整数。
→ short
:通常占用 2 个字节,用于存储较小的整数。
→ long
:通常占用 4 或 8 个字节,用于存储较大的整数。
→ long long
:通常占用 8 个字节,用于存储非常大的整数。
→ 可以使用 signed
和 unsigned
修饰符来指定有符号或无符号类型。
② 浮点型:
→ float
:通常占用 4 个字节,单精度浮点数。
→ double
:通常占用 8 个字节,双精度浮点数。
→ long double
:通常占用 10 或 16 个字节,扩展精度浮点数。
③ 字符型:
→ char
:通常占用 1 个字节,用于存储单个字符。
④ 布尔型:
→ bool
:占用 1 个字节,只能存储 true
或 false
。
⑤ void
类型:表示没有类型,常用于函数没有返回值或指针指向未知类型的情况。
1.3.2 复合数据类型
① 数组:存储相同类型元素的集合。
→ 声明:type arrayName[arraySize];
→ 示例:
1
int numbers[5]; // 声明一个包含 5 个整数的数组
2
numbers[0] = 10; // 访问数组的第一个元素
→ 最佳实践:注意数组的索引从 0 开始,避免越界访问。
→ 常见错误用法:数组越界访问导致程序崩溃或未定义行为。
② 结构体(struct):将不同类型的变量组合在一起。
→ 声明:
1
struct Person {
2
std::string name;
3
int age;
4
};
→ 使用:
1
Person person1;
2
person1.name = "Alice";
3
person1.age = 30;
③ 联合体(union):与结构体类似,但所有成员共享同一块内存空间。
→ 声明:
1
union Data {
2
int i;
3
float f;
4
};
→ 注意:一次只能存储一个成员的值。
④ 枚举(enum):为一组相关的常量命名。
→ 声明:
1
enum Color { RED, GREEN, BLUE };
→ 使用:
1
Color myColor = GREEN;
1.3.3 用户自定义类型别名
① typedef
:为现有类型创建别名。
→ 语法:typedef existing_type new_type_name;
→ 示例:typedef unsigned int uint;
② using
:C++11 引入的更现代的类型别名方式。
→ 语法:using new_type_name = existing_type;
→ 示例:using uint = unsigned int;
1.4 变量和常量 📦
1.4.1 变量
① 定义:用于存储数据的内存位置,其值可以在程序执行期间改变。
② 声明:type variableName;
③ 初始化:在声明时或之后为变量赋值。
→ 示例:
1
int count; // 声明一个整型变量 count
2
count = 10; // 为 count 赋值 10
3
int age = 25; // 声明并初始化整型变量 age 为 25
1.4.2 常量
① 定义:其值在程序执行期间不能被修改。
② 字面常量:直接出现在代码中的常量值(例如:10
, 3.14
, "hello"
)。
③ 符号常量:用名称表示的常量。
→ const
关键字:用于声明常量变量。
1
const double PI = 3.14159;
→ #define
预处理器指令:用于定义宏常量。
1
#define MAX_SIZE 100
→ 最佳实践:优先使用 const
关键字声明常量,因为它具有类型检查。
→ 常见错误用法:尝试修改 const
变量的值。
1.5 运算符 ⚙️
1.5.1 算术运算符
① +
:加法
② -
:减法
③ *
:乘法
④ /
:除法
⑤ %
:取模(求余数)
⑥ ++
:自增(前缀和后缀)
⑦ --
:自减(前缀和后缀)
1.5.2 关系运算符
① ==
:等于
② !=
:不等于
③ >
:大于
④ <
:小于
⑤ >=
:大于等于
⑥ <=
:小于等于
1.5.3 逻辑运算符
① &&
:逻辑与(AND)
② ||
:逻辑或(OR)
③ !
:逻辑非(NOT)
1.5.4 位运算符
① &
:按位与
② |
:按位或
③ ^
:按位异或
④ ~
:按位取反
⑤ <<
:左移
⑥ >>
:右移
1.5.5 赋值运算符
① =
:简单赋值
② +=
:加法赋值
③ -=
:减法赋值
④ *=
:乘法赋值
⑤ /=
:除法赋值
⑥ %=
:取模赋值
⑦ &=
:按位与赋值
⑧ |=
:按位或赋值
⑨ ^=
:按位异或赋值
⑩ <<=
:左移赋值
⑪ >>=
:右移赋值
1.5.6 条件(三元)运算符
① 语法:condition ? value_if_true : value_if_false
1.5.7 逗号运算符
① 用于分隔多个表达式,结果是最后一个表达式的值。
1.5.8 sizeof
运算符
① 返回变量或数据类型的大小(以字节为单位)。
1.5.9 作用域解析运算符 ::
① 用于访问全局命名空间或类的静态成员。
1.6 控制流 🚦
1.6.1 条件语句
① if
语句:
1
if (condition) {
2
// 如果条件为真,则执行这里的代码
3
}
② if-else
语句:
1
if (condition) {
2
// 如果条件为真,则执行这里的代码
3
} else {
4
// 如果条件为假,则执行这里的代码
5
}
③ if-else if-else
语句:
1
if (condition1) {
2
// 如果条件1为真
3
} else if (condition2) {
4
// 如果条件2为真
5
} else {
6
// 如果以上条件都为假
7
}
④ switch
语句:
1
switch (expression) {
2
case value1:
3
// 如果 expression 的值等于 value1,则执行这里的代码
4
break; // 记得使用 break 退出 switch 语句
5
case value2:
6
// 如果 expression 的值等于 value2,则执行这里的代码
7
break;
8
default:
9
// 如果 expression 的值与任何 case 都不匹配,则执行这里的代码
10
}
→ 最佳实践:在每个 case
语句后使用 break
,除非有意让程序执行到下一个 case
。
→ 常见错误用法:忘记在 case
语句后使用 break
,导致意外的 fall-through 行为。
1.6.2 循环语句
① for
循环:
1
for (initialization; condition; increment) {
2
// 循环体
3
}
② while
循环:
1
while (condition) {
2
// 循环体
3
}
③ do-while
循环:
1
do {
2
// 循环体
3
} while (condition); // 注意分号
→ do-while
循环至少执行一次循环体。
1.6.3 跳转语句
① break
语句:用于立即退出循环或 switch
语句。
② continue
语句:用于跳过当前循环迭代的剩余部分,并开始下一次迭代。
③ goto
语句:用于无条件地跳转到程序中的标记位置(通常不推荐使用,因为它可能导致代码难以理解和维护)。
④ return
语句:用于从函数返回。
1.7 函数 🛠️
1.7.1 函数的定义
① 语法:
1
return_type functionName(parameter_list) {
2
// 函数体
3
return value; // 如果函数有返回值
4
}
② return_type
:函数返回值的类型,如果函数不返回任何值,则为 void
。
③ functionName
:函数的名称。
④ parameter_list
:函数参数的列表,每个参数由类型和名称组成,用逗号分隔。
⑤ return value;
:用于返回一个与 return_type
兼容的值。
1.7.2 函数的声明(原型)
① 在调用函数之前告知编译器函数的存在。
② 语法:return_type functionName(parameter_list);
③ 注意:声明中只需要参数的类型,参数名是可选的。
1.7.3 函数的调用
① 使用函数名后跟一对括号 ()
来调用函数。
② 如果函数有参数,则需要在括号内提供相应的实参。
1.7.4 参数传递
① 值传递:将实参的值复制给形参,函数内部对形参的修改不会影响实参。
② 引用传递:使用引用(&
)作为形参,形参是实参的别名,函数内部对形参的修改会直接影响实参。
→ 原型示例:void myFunction(int& ref)
③ 指针传递:使用指针作为形参,函数内部通过解引用指针来修改实参的值.
→ 原型示例:void myFunction(int* ptr)
1.7.5 默认参数
① 可以在函数声明或定义中为参数指定默认值。
② 调用函数时,如果省略了带有默认值的参数,则使用默认值。
③ 注意:默认参数只能从右向左设置。
1.7.6 函数重载
① 在同一个作用域内,可以定义多个具有相同名称但参数列表不同的函数。
② 编译器根据调用时提供的参数类型和数量来决定调用哪个函数。
1.7.7 内联函数
① 使用 inline
关键字修饰的函数,建议编译器在调用该函数的地方直接展开函数体,以减少函数调用的开销。
② 适用于函数体较小且频繁调用的函数。
1.7.8 递归函数
① 在函数体内调用自身的函数。
② 需要设置合适的终止条件,以防止无限递归。
1.8 指针 📍
1.8.1 指针的概念
① 指针是一个变量,其值为另一个变量的内存地址。
1.8.2 指针的声明
① 语法:type *pointerName;
② 示例:int *ptr;
// 声明一个指向整型变量的指针
1.8.3 取地址运算符 &
① 用于获取变量的内存地址。
② 示例:int x = 10; int *ptr = &x;
// ptr
存储了 x
的地址
1.8.4 解引用运算符 *
① 用于访问指针所指向的内存位置的值。
② 示例:int value = *ptr;
// value
现在等于 x
的值 (10)
1.8.5 空指针 nullptr
(C++11)
① 表示指针不指向任何有效的内存地址。
② 替代了之前的 NULL
和 0
,更安全和类型安全。
1.8.6 指针的算术运算
① 可以对指针进行加法、减法运算,但只能与整数进行。
② 指针的算术运算是基于指针所指向的数据类型的大小的。
1.8.7 指向数组的指针
① 数组名本身就是指向数组第一个元素的常量指针。
② 可以使用指针遍历数组。
1.8.8 指向常量的指针 vs. 常量指针
① 指向常量的指针:指针指向的内存位置的值是常量,不能通过该指针修改值,但指针本身可以指向其他内存位置。
→ 声明:const int *ptr;
② 常量指针:指针本身是常量,一旦初始化后就不能指向其他内存位置,但可以通过该指针修改其指向的非常量内存位置的值。
→ 声明:int *const ptr;
③ 指向常量的常量指针:指针本身和其指向的内存位置的值都是常量,都不能被修改。
→ 声明:const int *const ptr;
1.8.9 函数指针
① 指向函数的指针,存储了函数的内存地址。
② 可以用于回调函数等场景。
→ 声明:return_type (*pointerName)(parameter_list);
→ 示例:
1
int add(int a, int b) { return a + b; }
2
int (*funcPtr)(int, int) = add;
3
int result = funcPtr(3, 5); // 调用 add 函数
最佳实践:在使用指针之前始终进行初始化,避免悬空指针。使用 nullptr
表示空指针。
常见错误用法:空指针解引用、悬空指针、指针算术运算越界。
1.9 引用 🔗
1.9.1 引用的概念
① 引用是已存在变量的别名,它与原始变量共享同一块内存空间。
② 一旦引用被初始化为某个变量,就不能再引用其他变量。
1.9.2 引用的声明
① 语法:type &referenceName = variableName;
② 示例:
1
int x = 10;
2
int &refX = x; // refX 是 x 的引用
1.9.3 引用的特性
① 引用在声明时必须被初始化。
② 引用一旦初始化后,就不能再引用其他变量。
③ 不存在空引用。
④ 引用本质上是通过常量指针实现的。
1.9.4 引用作为函数参数和返回值
① 作为函数参数:允许函数修改实参的值(类似于指针传递,但语法更简洁)。
② 作为函数返回值:可以返回一个变量的引用,使得函数调用可以作为左值。
→ 注意:不要返回局部变量的引用,因为局部变量在函数返回后会被销毁。
最佳实践:使用引用可以使代码更简洁易懂,尤其是在函数参数传递和返回值方面。
常见错误用法:返回局部变量的引用。
1.10 字符串 🔤
1.10.1 C 风格字符串
① 以空字符 \0
结尾的字符数组。
② 需要手动管理内存。
③ 示例:char str[] = "Hello";
1.10.2 std::string
类 (C++ 标准库)
① 提供了更方便和安全的字符串操作。
② 自动管理内存。
③ 需要包含头文件 <string>
.
④ 常用操作: * 创建和初始化:
1
std::string s1 = "Hello";
2
std::string s2("World");
3
std::string s3(s1); // 拷贝构造
4
std::string s4(5, 'a'); // 创建包含 5 个 'a' 的字符串
- 访问字符:使用下标运算符
[]
或.at()
方法。 - 连接字符串:使用
+
运算符或.append()
方法。 - 比较字符串:使用关系运算符 (
==
,!=
,<
,>
,<=
,>=
) 或.compare()
方法。 - 获取长度:使用
.length()
或.size()
方法。 - 查找子串:使用
.find()
方法。 - 替换子串:使用
.replace()
方法。 - 插入子串:使用
.insert()
方法。 - 删除子串:使用
.erase()
方法。 - 提取子串:使用
.substr()
方法。
最佳实践:优先使用 std::string
类进行字符串操作,因为它更安全且易于使用。
常见错误用法:在使用 C 风格字符串时忘记添加空字符,导致缓冲区溢出。
1.11 输入/输出流 ⌨️
1.11.1 标准输入/输出流
① 需要包含头文件 <iostream>
.
② std::cin
:标准输入流对象,通常连接到键盘。
→ 用法:std::cin >> variable;
// 从输入流读取数据到变量
③ std::cout
:标准输出流对象,通常连接到控制台。
→ 用法:std::cout << expression;
// 将表达式的值输出到输出流
④ std::endl
:换行符。
⑤ std::cerr
:标准错误流对象,用于输出错误信息(通常不缓冲)。
⑥ std::clog
:标准日志流对象,用于输出日志信息(通常缓冲)。
1.11.2 格式化输出
① 可以使用操纵符来格式化输出(需要包含头文件 <iomanip>
)。
② 常用操纵符:
→ std::setw(n)
:设置输出字段的宽度为 n
。
→ std::setprecision(n)
:设置浮点数的精度为 n
。
→ std::fixed
:以固定点表示法显示浮点数。
→ std::scientific
:以科学计数法显示浮点数。
→ std::left
:左对齐输出。
→ std::right
:右对齐输出。
最佳实践:合理使用输入输出流进行用户交互和程序调试。
常见错误用法:输入的数据类型与期望的变量类型不匹配。
1.12 命名空间 🏷️
1.12.1 命名空间的概念
① 用于解决不同代码库中命名冲突的问题。
② 将全局作用域划分为更小的、命名的作用域。
1.12.2 命名空间的定义
① 语法:
1
namespace namespaceName {
2
// 声明和定义变量、函数、类等
3
}
1.12.3 使用命名空间
① 作用域解析运算符 ::
:
1
namespaceName::variableName;
2
namespaceName::functionName();
② using
声明:将命名空间中的特定名称引入当前作用域。
1
using namespaceName::variableName;
2
using namespaceName::functionName;
③ using
指令:将整个命名空间引入当前作用域(可能导致命名冲突,谨慎使用)。
1
using namespace namespaceName;
最佳实践:在大型项目中合理使用命名空间,避免命名冲突。优先使用作用域解析运算符或 using
声明来引入所需的名称。
常见错误用法:滥用 using
指令导致命名冲突。
1.13 预处理器 ⚙️
1.13.1 预处理器指令
① 以 #
开头,在编译之前由预处理器处理。
1.13.2 #include
指令
① 用于包含头文件,将头文件的内容插入到当前源文件中。
② #include <filename>
:用于包含标准库头文件。
③ #include "filename"
:用于包含用户自定义头文件。
1.13.3 #define
指令
① 用于定义宏常量或宏函数。
② 定义宏常量:#define identifier value
③ 定义宏函数:#define identifier(parameters) replacement_text
→ 注意:宏函数是简单的文本替换,可能导致类型安全问题和副作用。建议优先使用 const
变量和内联函数。
1.13.4 条件编译指令
① 根据条件决定是否编译某部分代码。
② #ifdef
/ #ifndef
/ #else
/ #endif
:
1
#ifdef DEBUG
2
std::cout << "Debugging information." << std::endl;
3
#endif
③ #if
/ #elif
/ #else
/ #endif
:
1
#if VERSION > 10
2
// ...
3
#elif VERSION == 10
4
// ...
5
#else
6
// ...
7
#endif
1.13.5 #pragma
指令
① 编译器特定的指令,用于提供额外的编译控制。
② 例如:#pragma once
(防止头文件被多次包含)。
2. 面向对象编程 (OOP) 🧩
2.1 类和对象
2.1.1 类的定义
① 定义:类是用户自定义的数据类型,是创建对象的蓝图。
② 语法:
1
class ClassName {
2
public: // 公有成员
3
// 数据成员(属性)
4
// 成员函数(方法)
5
protected: // 受保护成员
6
// ...
7
private: // 私有成员
8
// ...
9
}; // 注意分号
③ 访问修饰符:
→ public
:成员可以在类的内部和外部访问。
→ protected
:成员可以在类的内部以及派生类中访问。
→ private
:成员只能在类的内部访问。
2.1.2 对象的创建
① 声明对象:ClassName objectName;
② 使用指针创建对象:ClassName *objectPtr = new ClassName();
→ 需要使用 delete objectPtr;
释放内存。
2.1.3 成员访问
① 使用点运算符 .
访问对象的公有成员。
② 使用箭头运算符 ->
访问通过指针指向的对象的公有成员。
2.2 构造函数和析构函数 🏗️🗑️
2.2.1 构造函数
① 定义:一种特殊的成员函数,在创建对象时自动调用,用于初始化对象。
② 特点:
→ 与类同名。
→ 没有返回值类型(包括 void
)。
→ 可以重载。
③ 默认构造函数:如果没有定义任何构造函数,编译器会自动生成一个默认构造函数(无参数)。
④ 参数化构造函数:接受参数的构造函数,用于在创建对象时初始化成员变量。
⑤ 拷贝构造函数:用于创建一个新对象作为现有对象的副本。
→ 原型:ClassName(const ClassName& other);
→ 如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数(浅拷贝)。
⑥ 移动构造函数 (C++11):用于将资源(例如动态分配的内存)从一个对象“移动”到另一个对象,而不是进行深拷贝,提高性能。
2.2.2 析构函数
① 定义:一种特殊的成员函数,在对象销毁时自动调用,用于执行清理操作(例如释放动态分配的内存)。
② 特点:
→ 与类同名,并在前面加上波浪线 ~
。
→ 没有参数。
→ 没有返回值类型。
→ 一个类只能有一个析构函数。
最佳实践:在构造函数中初始化所有成员变量。如果类中使用了动态分配的内存,务必在析构函数中释放这些内存,以防止内存泄漏。
常见错误用法:在析构函数中忘记释放动态分配的内存。拷贝构造函数和赋值运算符的实现不当导致浅拷贝问题。
2.3 封装 📦
2.3.1 封装的概念
① 将数据(属性)和操作数据的函数(方法)捆绑在一起,形成一个独立的单元(类)。
② 通过访问修饰符(public
、protected
、private
)控制对类成员的访问权限,隐藏实现细节,提供清晰的接口。
2.3.2 实现封装
① 将数据成员声明为 private
或 protected
。
② 提供 public
的成员函数(getter 和 setter)来访问和修改私有数据成员。
最佳实践:遵循最小特权原则,将不需要在类外部直接访问的成员声明为 private
或 protected
。
2.4 继承 👪
2.4.1 继承的概念
① 允许一个类(派生类或子类)继承另一个类(基类或父类)的属性和方法。
② 提高了代码的重用性和可维护性。
2.4.2 继承的语法
① 语法:
1
class DerivedClass : access_specifier BaseClass {
2
// 派生类的成员
3
};
② 访问说明符:
→ public
:基类的 public
和 protected
成员在派生类中保持原来的访问级别。
→ protected
:基类的 public
和 protected
成员在派生类中变为 protected
。
→ private
:基类的 public
和 protected
成员在派生类中变为 private
,基类的 private
成员在派生类中不可访问。
2.4.3 构造函数和析构函数在继承中的调用顺序
① 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
② 销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
2.4.4 函数重写(Override)
① 派生类可以重新定义从基类继承的虚函数,以实现不同的行为。
② 使用 override
关键字 (C++11) 可以显式地标记一个函数为重写函数,提高代码的可读性和安全性。
2.4.5 final
关键字 (C++11)
① 可以用于修饰类,表示该类不能被继承。
② 可以用于修饰虚函数,表示该虚函数不能在派生类中被重写。
最佳实践:合理使用继承来表示类之间的“is-a”关系。使用虚函数实现多态性。
常见错误用法:忘记在派生类的构造函数中调用基类的构造函数(如果需要参数)。继承层次过深导致代码复杂性增加。
2.5 多态 🎭
2.5.1 多态的概念
① 指不同类的对象对同一个消息做出不同的响应。
② 允许使用指向基类类型的指针或引用来操作派生类对象。
2.5.2 实现多态
① 虚函数(Virtual Functions):在基类中使用 virtual
关键字声明的函数,可以在派生类中被重写。
→ 当通过基类指针或引用调用虚函数时,会根据对象的实际类型(而不是指针或引用的类型)来调用相应的函数版本(动态绑定或运行时多态)。
② 纯虚函数(Pure Virtual Functions):在基类中声明的虚函数,但不提供默认实现,用 = 0
标记。
→ 包含纯虚函数的类是抽象类,不能创建抽象类的对象。
→ 派生类必须提供纯虚函数的实现才能被实例化。
2.5.3 抽象类
① 包含至少一个纯虚函数的类。
② 用于定义接口,强制派生类实现特定的方法。
2.5.4 虚析构函数(Virtual Destructors)
① 如果基类指针指向派生类对象,并且基类析构函数不是虚函数,那么在通过基类指针删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,可能导致内存泄漏。
② 将基类的析构函数声明为 virtual
可以解决这个问题,确保在删除派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
最佳实践:在基类中,如果希望派生类能够重写某个方法,并且需要通过基类指针或引用调用该方法时实现多态行为,则应该将该方法声明为虚函数。如果基类可能作为其他类的基类,并且涉及到动态内存分配,则应该将基类的析构函数声明为虚函数。
常见错误用法:忘记声明虚析构函数导致内存泄漏。混淆静态绑定和动态绑定。
2.6 运算符重载 🔄
2.6.1 运算符重载的概念
① 允许为已有的运算符赋予新的含义,使其能够操作自定义类型的对象。
2.6.2 运算符重载的语法
① 作为成员函数重载:
1
class ClassName {
2
public:
3
ReturnType operator op (ParameterList);
4
};
② 作为友元函数重载:
1
class ClassName {
2
friend ReturnType operator op (ParameterList);
3
};
③ op
是要重载的运算符,例如 +
, -
, *
, ==
, <<
等。
2.6.3 可重载和不可重载的运算符
① 可重载的运算符:大多数运算符都可以重载,例如算术运算符、关系运算符、逻辑运算符、赋值运算符、输入/输出流运算符等。
② 不可重载的运算符:.
(成员访问运算符), ::
(作用域解析运算符), sizeof
, ?:
(三元条件运算符) 等。
2.6.4 重载运算符的注意事项
① 重载运算符应该保持其原有的语义。
② 不要滥用运算符重载,使其含义与预期不符。
③ 某些运算符只能作为成员函数重载(例如 =
, ()
, []
, ->
)。
④ 某些运算符通常作为友元函数重载(例如 <<
, >>
),以便访问类的私有成员。
最佳实践:只重载那些能够使代码更清晰和易于理解的运算符。
常见错误用法:重载运算符改变了其原有的优先级或结合性。
2.7 模板 📄
2.7.1 模板的概念
① 模板是一种泛型编程工具,允许创建可以操作不同数据类型的函数或类,而无需为每种数据类型编写重复的代码。
2.7.2 函数模板
① 定义:
1
template <typename T>
2
ReturnType functionName(T parameter) {
3
// 函数体
4
}
1
或
1
template <class T>
2
ReturnType functionName(T parameter) {
3
// 函数体
4
}
② typename
或 class
关键字用于声明类型参数 T
。
③ 编译器会根据函数调用时提供的实际参数类型来生成相应的函数代码(模板实例化)。
2.7.3 类模板
① 定义:
1
template <typename T>
2
class ClassName {
3
public:
4
T memberVariable;
5
T memberFunction(T parameter);
6
};
② 在使用类模板创建对象时,需要指定实际的类型参数。
→ 示例:ClassName<int> object1;
最佳实践:使用模板可以提高代码的重用性和灵活性。
常见错误用法:模板代码的编译错误通常比较难以理解,需要仔细检查类型参数的使用。
2.8 异常处理 ❗
2.8.1 异常的概念
① 异常是在程序执行期间发生的错误或意外情况。
② C++ 提供了异常处理机制,用于捕获和处理这些异常,以防止程序崩溃。
2.8.2 try
, catch
, throw
关键字
① try
块:包含可能抛出异常的代码。
② catch
块:用于捕获和处理特定类型的异常。可以有多个 catch
块来处理不同类型的异常。
③ throw
表达式:用于抛出一个异常。可以抛出任何类型的对象作为异常。
2.8.3 异常处理的流程
① 如果在 try
块中的代码抛出了一个异常,程序会立即跳转到与该异常类型匹配的第一个 catch
块。
② 如果在当前的 try
块中没有找到匹配的 catch
块,异常会向上层调用栈传递,直到找到匹配的 catch
块或程序终止。
③ 可以使用 catch (...)
捕获所有类型的异常。
2.8.4 标准异常库
① C++ 标准库提供了一组预定义的异常类,位于 <stdexcept>
头文件中。
② 常用的标准异常类包括:std::exception
, std::runtime_error
, std::logic_error
, std::out_of_range
等。
最佳实践:使用异常处理机制来处理程序中的错误情况,提高程序的健壮性。只抛出应该被处理的异常。
常见错误用法:捕获异常后不进行处理或处理不当。忘记在必要的地方使用 try-catch
块。
3. 标准模板库 (STL) 📦
3.1 STL 概述
① STL 是 C++ 标准库的一部分,提供了一组通用的模板类和函数,用于实现常用的数据结构和算法。
② STL 的主要组件包括:
→ 容器(Containers):用于存储数据的对象,例如 vector
, list
, deque
, set
, map
等。
→ 迭代器(Iterators):用于遍历容器中元素的类似指针的对象。
→ 算法(Algorithms):用于操作容器中元素的函数,例如排序、查找、复制等。
→ 函数对象(Function Objects 或 Functors):行为类似于函数的对象,可以作为算法的参数。
→ 适配器(Adapters):用于修改现有组件接口的组件,例如 stack
, queue
, priority_queue
。
→ 分配器(Allocators):用于管理内存分配的对象(通常使用默认分配器)。
3.2 容器
3.2.1 顺序容器
① std::vector
:动态数组,支持快速随机访问,在尾部插入和删除元素效率高。
→ 常用 API:
→ push_back(value)
:在尾部添加元素。
→ pop_back()
:移除尾部元素。
→ size()
:返回容器中元素的个数。
→ capacity()
:返回容器当前分配的存储空间。
→ resize(n)
:改变容器的大小为 n
。
→ operator[]
和 .at()
:访问元素。
→ .begin()
和 .end()
:返回指向第一个元素和最后一个元素之后位置的迭代器。
② std::list
:双向链表,支持在任何位置快速插入和删除元素。
→ 常用 API:
→ push_back(value)
:在尾部添加元素。
→ push_front(value)
:在头部添加元素。
→ pop_back()
:移除尾部元素。
→ pop_front()
:移除头部元素。
→ insert(iterator, value)
:在指定位置插入元素。
→ erase(iterator)
:移除指定位置的元素。
→ .begin()
和 .end()
:返回迭代器。
③ std::deque
:双端队列,支持在头部和尾部快速插入和删除元素,也支持随机访问(但效率不如 vector
)。
→ 常用 API:类似于 vector
和 list
的组合。
3.2.2 关联容器
① std::set
:存储唯一且已排序的元素(通常使用红黑树实现)。
→ 常用 API:
→ insert(value)
:插入元素。
→ erase(value)
或 erase(iterator)
:移除元素。
→ find(value)
:查找元素,返回迭代器。
→ count(value)
:返回元素出现的次数(0 或 1)。
→ .begin()
和 .end()
:返回迭代器。
② std::multiset
:存储已排序的元素,允许重复。
→ 常用 API:类似于 std::set
。
③ std::map
:存储键值对,键是唯一且已排序的(通常使用红黑树实现)。
→ 常用 API:
→ insert({key, value})
或 operator[]
:插入键值对。
→ erase(key)
或 erase(iterator)
:移除键值对。
→ find(key)
:查找键,返回迭代器。
→ .begin()
和 .end()
:返回迭代器。
④ std::multimap
:存储键值对,键已排序,允许重复的键。
→ 常用 API:类似于 std::map
。
3.2.3 无序容器 (C++11)
① 使用哈希表实现,提供平均常数时间复杂度的插入、删除和查找操作。
② std::unordered_set
:存储唯一的无序元素。
③ std::unordered_multiset
:存储无序元素,允许重复。
④ std::unordered_map
:存储无序的键值对,键是唯一的。
⑤ std::unordered_multimap
:存储无序的键值对,允许重复的键。
3.2.4 容器适配器
① std::stack
:后进先出 (LIFO) 的数据结构。
→ 常用 API:push()
, pop()
, top()
, empty()
, size()
。
② std::queue
:先进先出 (FIFO) 的数据结构。
→ 常用 API:push()
, pop()
, front()
, back()
, empty()
, size()
。
③ std::priority_queue
:优先级队列,元素按照一定的优先级排序。
→ 常用 API:push()
, pop()
, top()
, empty()
, size()
。
3.3 迭代器
① 用于遍历容器中元素的类似指针的对象。
② 提供了统一的访问容器元素的方式,使得算法可以独立于特定的容器类型。
③ 迭代器类型:
→ 输入迭代器(Input Iterator):只能单向读取元素。
→ 输出迭代器(Output Iterator):只能单向写入元素。
→ 前向迭代器(Forward Iterator):可以多次读取元素,并支持单向移动。
→ 双向迭代器(Bidirectional Iterator):支持双向移动。
→ 随机访问迭代器(Random Access Iterator):支持随机访问元素(例如通过下标)。
④ 可以使用迭代器访问容器中的元素:
→ *iter
:访问迭代器指向的元素。
→ ++iter
:将迭代器移动到下一个元素。
→ --iter
:将迭代器移动到上一个元素(仅限双向和随机访问迭代器)。
→ iter + n
,iter - n
,iter[n]
(仅限随机访问迭代器)。
3.4 算法
① STL 提供了大量的通用算法,用于操作容器中的元素(需要包含头文件 <algorithm>
)。
② 常用算法:
→ std::sort()
:排序。
→ std::find()
:查找元素。
→ std::binary_search()
:在已排序的序列中进行二分查找。
→ std::copy()
:复制元素。
→ std::transform()
:对元素进行转换。
→ std::remove()
和 std::remove_if()
:移除满足条件的元素(注意:这些算法并不真正删除元素,而是将要保留的元素移动到容器的开头,并返回一个指向新逻辑结尾的迭代器,通常需要与容器的 erase()
方法一起使用)。
→ std::erase(container.begin(), std::remove(container.begin(), container.end(), value), container.end());
→ std::for_each()
:对每个元素执行操作。
→ std::count()
和 std::count_if()
:统计满足条件的元素个数。
→ std::max_element()
和 std::min_element()
:查找最大和最小元素。
3.5 函数对象
① 行为类似于函数的对象,通常是重载了函数调用运算符 operator()
的类的对象。
② 可以作为算法的参数,用于自定义算法的行为。
③ 示例:
1
struct GreaterThanZero {
2
bool operator()(int x) const {
3
return x > 0;
4
}
5
};
6
7
std::vector<int> numbers = {-1, 2, 0, 5, -3};
8
int count = std::count_if(numbers.begin(), numbers.end(), GreaterThanZero());
④ STL 也提供了一些预定义的函数对象,位于 <functional>
头文件中,例如 std::plus
, std::minus
, std::less
, std::greater
等。
最佳实践:充分利用 STL 提供的容器、迭代器和算法,可以大大简化代码的编写,提高开发效率和代码质量。
常见错误用法:迭代器失效问题(例如在遍历容器时插入或删除元素)。使用不匹配的容器和算法。忘记包含必要的头文件。
总结 📝
本学习笔记涵盖了 C++ 语言的基础语法、面向对象编程特性以及标准模板库 (STL) 的核心概念。掌握这些知识点是学习和使用 C++ 进行软件开发的关键。C++ 是一门博大精深的语言,还有许多高级特性和库等待我们去探索和学习。希望这份纲要能够为你提供一个系统性的学习框架,祝你在 C++ 的学习之旅中取得成功!🚀