Appearance
《C++语言实战快速入门》课程讲义
1. C++语言概述与环境搭建
1.1 为什么要学C++?
C++的来历
1979年,贝尔实验室的比雅尼在做项目时发现,C语言写大项目时代码很难维护。想象一下,几百个函数散落在各个文件中,数据和处理数据的函数分离,改个功能得在一堆文件中来回跳转,真的很头疼。
于是他想:"能不能把相关的数据和函数打包在一起?"就这样,C++诞生了。这个"++"来自C语言的自增操作符,意思就是"C语言的升级版"。
C++的优势
C++最大的特点就是"既要又要"——既保持C语言的高效,又提供更好的代码组织方式。
比如做一个学生管理系统:
- C语言:定义结构体存学生信息,再写一堆函数处理,但函数和数据是分离的
- C++:创建一个Student类,把学生信息和操作都封装在一起,结构更清晰
而且C++的标准库很丰富,比如动态数组,C语言可能要写几十行代码管理内存,C++直接用vector就搞定了。
C++的应用场景?
其实C++在我们生活中无处不在:
- 游戏:《王者荣耀》、《原神》的核心引擎
- 浏览器:Chrome的核心部分
- 播放器:音乐、视频播放器的底层
- 金融:高频交易系统
- AI:TensorFlow、PyTorch的底层计算库
1.2 C++和C语言的区别
关系很简单
C++几乎包含了C语言的全部功能,再加上自己的新特性。就像智能手机包含了传统手机的功能,还增加了新功能。这意味着你之前学的C语言知识不会白费。
最直观的区别:输入输出
C语言版本:
c
#include <stdio.h>
int main() {
int age;
printf("请输入年龄:");
scanf("%d", &age);
printf("你的年龄是:%d\n", age);
return 0;
}C++版本:
cpp
#include <iostream>
using namespace std;
int main() {
int age;
cout << "请输入年龄:";
cin >> age;
cout << "你的年龄是:" << age << endl;
return 0;
}C++的输入输出更直观,不需要记住复杂的格式控制符(%d、%s等)。
面向对象:最大的区别
C语言是面向过程的,像做菜一样一步步来。C++增加了面向对象思想,像开餐厅一样,每个人专职做自己的事。
计算圆的面积:
C语言:
c
float calculate_area(float radius) {
return 3.14159 * radius * radius;
}C++:
cpp
class Circle {
private:
float radius;
public:
Circle(float r) : radius(r) {}
float getArea() {
return 3.14159 * radius * radius;
}
};C++把圆的属性(半径)和行为(计算面积)封装在一起,更符合思维习惯。
1.3 开发环境选择
初学者推荐方案
Dev-C++(强烈推荐新手)
- 优点:安装简单,界面清爽,不会被复杂功能干扰
- 缺点:功能相对简单
- 适合:刚开始学习,写小程序练手
Visual Studio(进阶推荐)
- 优点:功能强大,调试方便,智能提示好用
- 缺点:体积大,初学者可能觉得复杂
- 适合:有一定基础后使用
安装Dev-C++
- 搜索"Dev-C++ 官网"下载最新版本
- 双击安装,选择中文界面
- 保持默认设置,一路"下一步"即可
- 安装完成后,设置编码为UTF-8(避免中文乱码)
1.4 第一个C++程序
Hello World程序
cpp
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl;
return 0;
}逐行解析
#include <iostream>:包含输入输出功能,就像准备工具using namespace std;:使用标准库,简化代码书写int main() {:程序入口,从这里开始执行cout << "Hello, World!" << endl;:输出语句,endl表示换行return 0;:告诉系统程序正常结束
试试看
输出你的名字:
cpp
#include <iostream>
using namespace std;
int main() {
cout << "大家好,我是张三!" << endl;
return 0;
}多行输出:
cpp
#include <iostream>
using namespace std;
int main() {
cout << "第一行内容" << endl;
cout << "第二行内容" << endl;
cout << "第三行内容" << endl;
return 0;
}常见错误
忘记分号
cppcout << "Hello, World!" << endl // 缺少分号,编译报错括号不匹配
cppint main() { cout << "Hello, World!" << endl; // 缺少右括号拼写错误
cppcout << "Hello, World!" << endll; // endl写错了
编译运行
在Dev-C++中:
- 写完代码后按F11
- 或者点击"执行"→"编译运行"
- 如果没错误,会弹出黑色窗口显示结果
小结
这个简单的程序包含了C++的基本要素。别着急,慢慢消化这些概念。每个程序员都是从Hello World开始的,多练几遍就熟悉了。
记住关键点:
- C++是C语言的升级版
- 输入输出用cin和cout
- 每个语句要加分号
- 括号要配对
- main函数是程序入口
2. C++对C语言的基础扩展
2.1 C++的输入输出流:告别printf和scanf的时代
什么是"流"?一个生动的比喻
很多同学第一次听到"流"这个概念时都会觉得抽象,其实它很好理解。想象一下你在家里用水管浇花:水从水龙头流出,经过水管,最后流到花盆里。这个过程中,水管就是"流",水是"数据",水龙头是"数据源",花盆是"目的地"。
在C++中,输入输出就是这样的过程:
- 键盘输入:数据从键盘(水龙头)→ 通过cin(水管)→ 流入你的程序变量(花盆)
- 屏幕输出:数据从程序变量(水龙头)→ 通过cout(水管)→ 流向屏幕(花盆)
为什么要用"流"这种方式?因为它简单统一!不管是键盘、文件、网络,都用同样的方式处理。就像不管你要给花浇水、给池塘加水,都是用水管,方法是一样的。
C++输入输出比C语言强在哪里?
还记得C语言的printf和scanf吗?那些%d、%f、%s的格式控制符是不是经常搞混?
c
// C语言的方式 - 容易出错
int age = 20;
float height = 175.5;
printf("年龄:%d,身高:%f\n", age, height); // 搞错%d和%f就出问题了C++的方式简单多了:
cpp
// C++的方式 - 不用记那些格式符
int age = 20;
float height = 175.5;
cout << "年龄:" << age << ",身高:" << height << endl; // 自动识别类型看到区别了吗?C++会自动识别变量类型,你不需要记住那一堆格式控制符!
2.1.1 iostream库:C++输入输出的工具箱
包含头文件:准备工具
使用C++的输入输出功能,首先要包含iostream头文件:
cpp
#include <iostream>注意:C++的头文件名没有.h后缀,这是和C语言的区别。
iostream库里都有什么?
iostream库就像一个工具箱,里面有几个重要的工具:
cout:输出工具,用来向屏幕输出信息cin:输入工具,用来从键盘读取信息cerr:错误输出工具,用来输出错误信息clog:日志输出工具,用来输出日志信息
对于初学者来说,最常用的就是cout和cin。
命名空间:为什么要写using namespace std?
很多同学不理解为什么要写这行代码:
cpp
using namespace std;这其实很好理解。想象一下,你在一个大公司上班,公司有很多部门:技术部、财务部、人事部。如果你想找技术部的张三,你得说"技术部的张三",而不能只说"张三",因为财务部可能也有个张三。
在C++中,cout、cin这些工具都在std这个"部门"里。如果不写using namespace std,你就得这样写:
cpp
std::cout << "Hello World!" << std::endl;但是写了using namespace std之后,就可以直接写:
cpp
cout << "Hello World!" << endl;就像在技术部内部,大家都知道你说的张三就是本部门的张三,不用每次都说"技术部的张三"。
2.1.2 cin和cout:输入输出的基本操作
cout:向屏幕输出信息
cout的使用非常简单,就是用<<操作符把要输出的内容"推送"到屏幕上:
cpp
cout << "欢迎来到C++世界!";连续输出多个内容:
cpp
string name = "小明";
int age = 18;
cout << "我叫" << name << ",今年" << age << "岁";这就像搭积木一样,一块一块地拼接起来。
cin:从键盘读取信息
cin的使用也很直观,用>>操作符把键盘输入的内容"拉取"到变量中:
cpp
int age;
cout << "请输入你的年龄:";
cin >> age;
cout << "你的年龄是:" << age << endl;一次读取多个值:
cpp
string name;
int age;
cout << "请输入姓名和年龄:";
cin >> name >> age; // 用户可以输入:小明 18
cout << "你好," << name << ",你今年" << age << "岁" << endl;初学者常见问题解答
1. 为什么cin读取字符串遇到空格就停了?
这是很多同学都会遇到的问题:
cpp
string fullName;
cout << "请输入你的全名:";
cin >> fullName; // 如果输入"张 三",只能读取到"张"这是因为cin把空格当作分隔符。如果要读取带空格的字符串,要用getline:
cpp
string fullName;
cout << "请输入你的全名:";
getline(cin, fullName); // 这样就能读取"张 三"了2. 为什么输入字母程序就出错了?
如果你定义了一个整数变量,但输入了字母,程序会出现异常:
cpp
int number;
cout << "请输入一个数字:";
cin >> number; // 如果输入了"abc",程序就不正常了这是因为cin期望读取一个整数,但收到了字母。解决方法是在输入前给用户明确的提示,或者添加错误检查(这个我们以后再讲)。
3. 为什么cout输出后没有换行?
cout默认不会换行,如果你想换行,需要加上endl:
cpp
cout << "第一行" << endl;
cout << "第二行" << endl;或者使用\n:
cpp
cout << "第一行\n";
cout << "第二行\n";2.1.3 让输出更美观:格式控制
endl vs \n:换行的两种方式
endl:换行并刷新缓冲区(立即显示)\n:只换行,不刷新缓冲区
对于初学者来说,两者区别不大,但endl更安全一些。
让数字输出更漂亮
很多时候,我们希望数字的输出格式更整齐。这时候需要包含iomanip头文件:
cpp
#include <iostream>
#include <iomanip>
using namespace std;控制小数点后的位数:
cpp
double pi = 3.14159265359;
cout << "默认输出:" << pi << endl;
cout << "保留2位小数:" << fixed << setprecision(2) << pi << endl;
cout << "保留4位小数:" << fixed << setprecision(4) << pi << endl;输出结果:
默认输出:3.14159
保留2位小数:3.14
保留4位小数:3.1416让表格输出更整齐:
cpp
cout << setw(10) << "姓名" << setw(8) << "年龄" << setw(8) << "成绩" << endl;
cout << setw(10) << "张三" << setw(8) << 18 << setw(8) << 85.5 << endl;
cout << setw(10) << "李四" << setw(8) << 19 << setw(8) << 92.0 << endl;输出结果:
姓名 年龄 成绩
张三 18 85.5
李四 19 92改变数字的进制显示:
cpp
int number = 255;
cout << "十进制:" << dec << number << endl;
cout << "十六进制:" << hex << number << endl;
cout << "八进制:" << oct << number << endl;输出结果:
十进制:255
十六进制:ff
八进制:377填充字符让输出更醒目:
cpp
cout << setfill('*') << setw(20) << "重要通知" << endl;
cout << setfill('0') << setw(8) << 123 << endl;输出结果:
***********重要通知
00000123实用技巧:制作简单的表格
cpp
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
cout << "学生成绩表" << endl;
cout << setfill('-') << setw(40) << "" << endl;
cout << setfill(' ');
cout << left; // 左对齐
cout << setw(10) << "姓名" << setw(8) << "语文" << setw(8) << "数学" << setw(8) << "英语" << endl;
cout << setfill('-') << setw(40) << "" << endl;
cout << setfill(' ');
cout << setw(10) << "张三" << setw(8) << 85 << setw(8) << 92 << setw(8) << 78 << endl;
cout << setw(10) << "李四" << setw(8) << 90 << setw(8) << 88 << setw(8) << 85 << endl;
cout << setw(10) << "王五" << setw(8) << 78 << setw(8) << 95 << setw(8) << 90 << endl;
return 0;
}格式控制的注意事项
setw只对下一个输出项有效:
cppcout << setw(10) << "Hello" << "World" << endl; // 只有Hello被设置宽度其他格式设置会一直有效:
cppcout << hex; // 之后的整数都会以十六进制输出 cout << 100 << endl; // 输出64 cout << 200 << endl; // 输出c8
3. **恢复默认设置:**
```cpp
cout << dec; // 恢复十进制
cout << setfill(' '); // 恢复空格填充小结:从混乱到整洁的输出
掌握了这些格式控制技巧,你就能让程序的输出从这样:
张三18185.5
李四1992.0变成这样:
姓名 年龄 成绩
张三 18 85.5
李四 19 92.0这就是专业程序和学生作业的区别!用户看到整洁的输出,会觉得你的程序很专业。
练习建议
- 写一个程序,让用户输入姓名、年龄、身高,然后格式化输出
- 制作一个简单的计算器,让结果显示得更美观
- 尝试制作一个学生成绩单,包含多个学生的多科成绩
记住:好的程序不仅要功能正确,还要界面美观。这些格式控制技巧会让你的程序看起来更专业!
2.2 bool数据类型:程序世界的"是"与"否"
2.2.1 为什么需要bool类型?
C语言时代的尴尬
还记得你学C语言时是怎么表示"真"和"假"的吗?用整数!1表示真,0表示假。这就像用数字1和0来回答"你吃饭了吗?"这种问题,总感觉怪怪的。
c
// C语言的做法 - 用整数表示真假
int isHungry = 1; // 1表示饿了
int isFinished = 0; // 0表示没完成这种做法有什么问题?
- 不直观:看到
int isHungry = 1;,你得想一下"1是什么意思?" - 容易出错:如果不小心写成
int isHungry = 2;,程序还是认为这是真的 - 代码不清晰:新手看代码时,很难理解这些数字的含义
C++的解决方案:bool类型
C++说:"既然生活中的很多问题都是是/否问题,那我们就专门创造一个数据类型来表示这种情况!"
cpp
// C++的做法 - 专门的bool类型
bool isHungry = true; // 直接说"真的饿了"
bool isFinished = false; // 直接说"假的,没完成"看到区别了吗?现在一眼就能看出变量的含义!
2.2.2 bool类型的基本使用
只有两个值:true和false
bool类型就像一个开关,只有两种状态:
cpp
#include <iostream>
using namespace std;
int main() {
bool lightOn = true; // 灯开着
bool doorClosed = false; // 门关着
cout << "灯开着吗?" << lightOn << endl; // 输出:1
cout << "门关着吗?" << doorClosed << endl; // 输出:0
// 如果想显示true/false字符串
cout << boolalpha;
cout << "灯开着吗?" << lightOn << endl; // 输出:true
cout << "门关着吗?" << doorClosed << endl; // 输出:false
return 0;
}注意事项:
true和false都是小写的- 它们是C++的关键字,不能用作变量名
- 建议声明bool变量时就初始化它
2.2.3 bool类型的运算:逻辑的艺术
逻辑运算符:&&、||、!
bool类型有三个专门的运算符,就像日常生活中的逻辑推理:
1. 逻辑与(&&)- "并且"
cpp
bool hasID = true;
bool hasTicket = true;
bool canEnter = hasID && hasTicket; // 既要有身份证,又要有票才能进入生活例子:进入考场需要身份证AND准考证,缺一不可。
2. 逻辑或(||)- "或者"
cpp
bool isWeekend = true;
bool isHoliday = false;
bool canSleep = isWeekend || isHoliday; // 周末或假日都可以睡懒觉生活例子:感冒了OR发烧了,都应该请假休息。
3. 逻辑非(!)- "不是"
cpp
bool isRaining = false;
bool canGoOut = !isRaining; // 不下雨就可以出门生活例子:不是工作日,就可以不用早起。
运算符的优先级
记住:! > && > ||
cpp
bool result = !a && b || c; // 等价于 ((!a) && b) || c2.2.4 bool类型与其他类型的转换
其他类型转bool:什么是"真"?
C++的转换规则很简单:
- 0就是false(无论是整数0还是浮点数0.0)
- 非0就是true
cpp
bool b1 = 0; // false
bool b2 = 1; // true
bool b3 = -5; // true(非零就是true)
bool b4 = 0.0; // false
bool b5 = 0.1; // true实际应用场景:
cpp
int score;
cout << "请输入分数:";
cin >> score;
if (score) { // 如果分数不是0,就是true
cout << "至少拿到了分数" << endl;
}bool转其他类型:
cpp
int i1 = false; // 0
int i2 = true; // 1
double d1 = true; // 1.02.2.5 bool类型的实际应用
1. 状态标志
cpp
bool isLoggedIn = false;
bool isDataLoaded = false;
bool hasError = false;
// 用户登录后
isLoggedIn = true;2. 条件判断
cpp
int age = 20;
bool isAdult = (age >= 18);
bool isVIP = (memberLevel >= 5);
bool canEnterVIPRoom = isAdult && isVIP;3. 函数返回值
cpp
bool isValidEmail(string email) {
return email.find('@') != string::npos;
}
bool isPasswordStrong(string password) {
return password.length() >= 8;
}2.2.6 bool类型的命名技巧
好的命名让代码自解释
cpp
// 好的命名
bool isReady = false;
bool hasPermission = true;
bool canWrite = false;
bool shouldUpdate = true;
bool isEmpty = false;
// 不好的命名
bool flag = true; // 什么标志?
bool status = false; // 什么状态?
bool check = true; // 检查什么?常用的命名模式:
is+ 形容词:isReady, isEmpty, isValidhas+ 名词:hasError, hasData, hasPermissioncan+ 动词:canRead, canWrite, canExecuteshould+ 动词:shouldSave, shouldUpdate, shouldExit
2.2.7 常见错误和注意事项
错误1:忘记初始化
cpp
bool isReady; // 值是随机的!
if (isReady) {
// 可能会执行,也可能不会
}
// 正确的做法
bool isReady = false;错误2:与数字混淆
cpp
bool flag = 5; // 虽然合法,但不清晰
// 建议
bool flag = (count > 0); // 清晰地表达逻辑关系错误3:复杂的逻辑表达式
cpp
// 难以理解的写法
bool result = !(!a || b) && (c || !d);
// 清晰的写法
bool condition1 = a && !b;
bool condition2 = c || !d;
bool result = condition1 && condition2;2.2.8 实战练习
简单的判断程序
cpp
#include <iostream>
using namespace std;
int main() {
int age;
cout << "请输入你的年龄:";
cin >> age;
bool isAdult = (age >= 18);
bool isChild = (age < 12);
bool isTeenager = (age >= 12 && age < 18);
cout << boolalpha;
cout << "是成年人:" << isAdult << endl;
cout << "是儿童:" << isChild << endl;
cout << "是青少年:" << isTeenager << endl;
return 0;
}用户权限检查
cpp
#include <iostream>
using namespace std;
int main() {
string username;
string password;
cout << "用户名:";
cin >> username;
cout << "密码:";
cin >> password;
bool isValidUser = (username == "admin");
bool isValidPassword = (password == "123456");
bool canLogin = isValidUser && isValidPassword;
if (canLogin) {
cout << "登录成功!" << endl;
} else {
cout << "用户名或密码错误!" << endl;
}
return 0;
}小结:bool类型的价值
bool类型虽然简单,但它让我们的程序更加清晰和易懂。记住:
- 语义明确:用bool表示真假,比用整数更清晰
- 命名要好:用is、has、can等前缀让代码自解释
- 逻辑清晰:善用&&、||、!来表达复杂逻辑
- 避免混淆:不要把bool当数字用
2.3 引用类型:给变量取个小名
2.3.1 引用的基本概念
引用就是给已存在变量起的一个别名。通过引用,我们可以用不同名字访问同一个内存位置。
这就像给朋友起外号一样。比如我同学叫"张三丰",但大家都叫他"老张"。无论是叫他"张三丰"还是"老张",指的都是同一个人。如果我给"老张"一本书,实际上就是给了"张三丰"一本书。
在C++中是这样的:
cpp
int a = 100; // 创建原始变量"张三丰"
int& b = a; // 创建别名"老张"此时,a 和 b 是同一块内存空间的两个名字,修改任一个都会影响另一个。
2.3.2 引用与赋值的区别
很多同学刚开始学C++时会混淆引用和简单赋值:
cpp
int a = 10; // 创建变量a
int b = a; // 创建变量b,并把a的值复制给b
int& ref = a; // 创建引用ref,它是a的别名
b = 20; // 修改b不影响a,此时a仍为10
ref = 30; // 修改ref就是修改a,此时a变为30赋值是复制一份数据,而引用只是原变量的另一个名字。赋值就像拍照,得到的是照片(副本);引用就像起外号,还是同一个人。
2.3.3 引用的语法与使用
引用的声明很简单,在类型后面加"&"符号:
cpp
数据类型& 引用名 = 原变量名;一个简单的例子:
cpp
#include <iostream>
using namespace std;
int main() {
// 创建原始变量
int score = 85;
string name = "小明";
// 创建引用
int& grade = score;
string& student = name;
// 通过原名和引用名访问
cout << "成绩:" << score << "分,姓名:" << name << endl;
cout << "等级:" << grade << "分,学生:" << student << endl;
// 通过引用修改
grade += 5; // 加分啦!
student = "小李"; // 换人啦!
// 看看原始变量的变化
cout << "修改后成绩:" << score << "分,姓名:" << name << endl;
return 0;
}2.3.4 引用的重要规则
在考试和作业中,你需要记住这几条重要规则:
- 必须初始化
引用必须在声明的同时初始化,不能先声明后赋值。
cpp
int value = 42;
int& ref = value; // 正确:声明时初始化
// 错误示例:
// int& badRef; // 编译错误:引用必须初始化- 不能更改绑定关系
一旦引用被初始化后,它就永远绑定到那个变量,不能再指向其他变量。
cpp
int first = 10;
int second = 20;
int& ref = first; // ref引用了first
ref = second; // 这不是让ref引用second!而是把second的值赋给first
// 此时first的值变成了20,ref仍然引用first- 不存在空引用
引用必须引用一个有效的对象,不能为空。
cpp
// int& nullRef = nullptr; // 错误:引用不能为空2.3.5 引用与指针的对比
引用和指针是两个容易混淆的概念,但它们有明显区别:
cpp
#include <iostream>
using namespace std;
int main() {
int number = 100;
// 使用引用
int& numRef = number;
// 使用指针
int* numPtr = &number;
// 通过引用修改
numRef = 200;
cout << "通过引用修改后:" << number << endl; // 输出200
// 通过指针修改
*numPtr = 300;
cout << "通过指针修改后:" << number << endl; // 输出300
return 0;
}它们的区别简单总结:
| 特点 | 引用 | 指针 |
|---|---|---|
| 本质 | 变量别名 | 存储地址的变量 |
| 初始化 | 必须初始化 | 可以不初始化 |
| 重定向 | 不能改变引用对象 | 可以指向不同对象 |
| 空值 | 不能为空 | 可以为nullptr |
| 使用语法 | 直接使用 | 需要解引用* |
| 安全性 | 更安全 | 需要小心使用 |
小结
引用是C++中最常用的特性之一,记住这几点:
引用是别名,不是新对象
必须初始化,不能更改绑定对象
不能为空,比指针更安全
2.4 函数重载:一个名字,多种技能
2.4.1 什么是函数重载?
就像多才多艺的同学
想象一下你们班有个同学叫"小明",他有很多技能:
- 小明会唱歌
- 小明会画画
- 小明会弹钢琴
当你说"小明,表演一下"时,你需要告诉他表演什么,比如"小明,唱首歌"或者"小明,弹个曲子"。根据你的要求不同,小明会表演不同的节目。
函数重载就是这个道理:同一个函数名,根据你给的"参数"不同,执行不同的操作。计算机会根据你传递的参数自动选择应该执行哪个版本的函数,非常智能方便。
在没有函数重载的语言中,如果要实现同样的功能,我们可能需要这样写:
cpp
void printInt(int number);
void printDouble(double number);
void printString(string text);而有了函数重载,我们就可以全部使用同一个函数名:
cpp
void print(int number);
void print(double number);
void print(string text);这使得我们的代码更加直观、易读,也更符合我们的思维方式。
2.4.2 函数重载的基本用法
让我们通过一个简单的示例来看看函数重载的基本用法:
cpp
#include <iostream>
#include <string>
using namespace std;
// 同一个函数名"print",但参数不同
void print(int number) {
cout << "打印整数:" << number << endl;
}
void print(double number) {
cout << "打印小数:" << number << "(精确到小数点后2位:" << fixed << setprecision(2) << number << ")" << endl;
}
void print(string text) {
cout << "打印文字:" << text << endl;
}
int main() {
print(42); // 自动调用int版本
print(3.14159); // 自动调用double版本
print("Hello"); // 自动调用string版本
return 0;
}运行这个程序,你会看到:
打印整数:42
打印小数:3.14159(精确到小数点后2位:3.14)
打印文字:Hello注意我们在每个函数内部的处理方式略有不同:整数直接输出,小数额外显示了保留两位小数的版本,字符串直接输出。这正是函数重载的优势——可以根据不同的参数类型提供最合适的处理逻辑。
2.4.3 函数重载的规则
函数重载必须遵循一定的规则。只有当函数的参数列表不同时,才能构成重载。参数列表不同可以体现在两个方面:参数个数不同或参数类型不同。
规则1:参数个数不同
下面是一个参数个数不同的函数重载示例:
cpp
void show() {
cout << "什么都不显示" << endl;
}
void show(int a) {
cout << "显示一个数:" << a << endl;
}
void show(int a, int b) {
cout << "显示两个数:" << a << ", " << b << endl;
}
void show(int a, int b, int c) {
cout << "显示三个数:" << a << ", " << b << ", " << c << endl;
}使用方法:
cpp
show(); // 调用无参版本
show(10); // 调用一参版本
show(10, 20); // 调用两参版本
show(10, 20, 30); // 调用三参版本规则2:参数类型不同
即使参数个数相同,只要参数类型不同,也可以构成重载:
cpp
void process(int x) {
cout << "处理整数:" << x << ",平方值为:" << x * x << endl;
}
void process(double x) {
cout << "处理小数:" << x << ",平方根约为:" << sqrt(x) << endl;
}
void process(string x) {
cout << "处理字符串:" << x << ",长度为:" << x.length() << endl;
}使用方法:
cpp
process(42); // 调用int版本
process(3.14); // 调用double版本
process("hello"); // 调用string版本规则3:参数顺序不同
参数类型的顺序不同也可以构成重载:
cpp
void setup(int id, string name) {
cout << "设置ID和名称:" << id << ", " << name << endl;
}
void setup(string name, int id) {
cout << "设置名称和ID:" << name << ", " << id << endl;
}使用方法:
cpp
setup(101, "打印机"); // 调用第一个版本
setup("打印机", 101); // 调用第二个版本不过要注意,参数顺序不同的重载容易导致调用混淆,所以实际开发中应该尽量避免这种方式,除非确实有很好的理由这样做。
2.4.4 什么情况下不能重载?
了解函数重载的限制也很重要。以下情况下不能构成函数重载:
错误1:只有返回值不同
cpp
// 错误的写法!编译器会报错
/*
int getValue() {
return 42;
}
double getValue() { // 错误:仅返回值不同
return 3.14;
}
*/为什么不行?因为编译器在调用时不知道你想要哪个版本:
cpp
// 编译器不知道你想要int还是double
auto result = getValue(); // 调用哪个版本?如果我们确实需要返回不同类型的值,应该使用不同的函数名或者添加一些区分参数:
cpp
int getIntValue() {
return 42;
}
double getDoubleValue() {
return 3.14;
}
// 或者添加一个无用但能区分的参数
int getValue(int dummy = 0) {
return 42;
}
double getValue(double dummy = 0.0) {
return 3.14;
}错误2:只有参数名不同
cpp
// 错误的写法!
/*
void process(int x) {
cout << x << endl;
}
void process(int y) { // 错误:只是参数名不同
cout << y << endl;
}
*/参数名在函数重载中不起作用,编译器只关心参数的类型和顺序。
错误3:只有const限定符不同(非成员函数)
对于普通函数,参数只有const限定符不同不能构成重载:
cpp
// 错误的写法
/*
void display(int x) { }
void display(const int x) { } // 错误:const int和int被视为相同
*/不过,如果是引用或指针参数,const限定符可以构成重载:
cpp
// 正确:引用参数const不同可以重载
void show(int& x) {
cout << "非常量引用版本" << endl;
}
void show(const int& x) {
cout << "常量引用版本" << endl;
}
// 正确:指针参数const不同可以重载
void process(int* ptr) {
cout << "非常量指针版本" << endl;
}
void process(const int* ptr) {
cout << "常量指针版本" << endl;
}2.4.5 函数重载实际应用
函数重载在实际编程中有许多应用场景。以下是一些典型例子:
1. 不同类型的打印/显示函数
cpp
void display(int value);
void display(double value);
void display(string value);
void display(vector<int> values);2. 不同参数数量的初始化函数
cpp
Student createStudent(); // 创建默认学生
Student createStudent(string name); // 指定姓名
Student createStudent(string name, int age); // 指定姓名和年龄
Student createStudent(string name, int age, string major); // 更多信息3. 支持多种查找方式的搜索函数
cpp
int findStudent(int id); // 按学号查找
int findStudent(string name); // 按姓名查找4. 数学计算函数
cpp
int max(int a, int b);
double max(double a, double b);
string max(string a, string b); // 按字典序比较5. 构造函数重载
在面向对象编程中,构造函数重载非常常见:
cpp
class Rectangle {
public:
Rectangle(); // 默认构造函数
Rectangle(int width, int height); // 指定宽高的构造函数
Rectangle(const Rectangle& other); // 复制构造函数
};这些例子展示了函数重载在实际编程中的灵活应用。通过使用函数重载,我们可以让代码更加直观、更符合人类思维方式,也更容易维护。
小结:一个名字,多种技能
函数重载是C++中一个强大而实用的特性,它让我们能够:
用相同的名字表达相同的概念:不管处理的是int、double还是string,"打印"就是"打印","查找"就是"查找",让代码更符合人类的思维方式。
让代码更直观:不用记住printInt、printDouble等不同名字,减少记忆负担。
提高代码的可读性和可维护性:看到函数名就知道它的作用,不必关心具体处理的数据类型。
体现面向对象的"多态"思想:同一操作应用于不同类型的对象,产生适当的行为。
2.5 默认参数:函数的"可选配置"
2.5.1 什么是默认参数?
就像点外卖一样
- 想象你在用外卖APP点餐:
选择菜品:宫保鸡丁 辣度:中辣 (默认) 餐具:需要 (默认) 备注:无 (默认) - 你可以只选择菜品,其他选项都用默认值
- 也可以修改某些选项
- C++的默认参数就是这个道理!
- 想象你在用外卖APP点餐:
2.5.2 默认参数的基本语法
函数声明
cpp#include <iostream> #include <string> using namespace std; // 问候函数,可以指定问候语和次数 void greet(string name, string greeting = "你好", int times = 1) { for (int i = 0; i < times; i++) { cout << greeting << ", " << name << "!" << endl; } } int main() { greet("小明"); // 使用默认问候语和次数 greet("小红", "早上好"); // 修改问候语,次数用默认值 greet("小华", "晚上好", 3); // 修改问候语和次数 return 0; }
2.5.3 默认参数的重要规则
规则1:默认参数必须从右往左连续
cpp// 正确的写法 void goodFunction(int a, int b = 10, int c = 20) { cout << a << ", " << b << ", " << c << endl; } // 错误的写法(编译不通过) /* void badFunction(int a, int b = 10, int c) { // 错误!c没有默认值 cout << a << ", " << b << ", " << c << endl; } */- 为什么要这样?因为C++调用函数时参数是从左到右传递的
如果中间有个参数没有默认值,编译器就不知道你跳过了哪个参数
规则2:默认参数只能在函数声明中指定一次
cpp// 正确的写法 void calculate(int x, int y = 10); // 在声明中指定默认值 void calculate(int x, int y) { // 实现中不要再指定默认值 cout << x + y << endl; }
2.5.4 默认参数的实际应用
游戏角色创建函数
cpp// 创建角色函数,大部分参数都有默认值 GameCharacter createCharacter(string name, int level = 1, // 默认1级 int health = 100, // 默认100血 string weapon = "木剑") { // 默认木剑 return GameCharacter(name, level, health, weapon); } // 不同的创建方式 GameCharacter hero1 = createCharacter("新手村的勇士"); GameCharacter hero2 = createCharacter("冒险家", 5); GameCharacter hero3 = createCharacter("战士", 10, 200, "铁剑");
### 2.5.5 常见错误和注意事项
1. **错误1:在实现中重复指定默认值**
```cpp
// 错误的写法
// 头文件中
void myFunction(int x, int y = 10);
// 源文件中
void myFunction(int x, int y = 10) { // 错误!不要重复指定
cout << x + y << endl;
}
// 正确的写法
void myFunction(int x, int y) { // 实现中不指定默认值
cout << x + y << endl;
}错误2:默认参数的顺序问题
cpp// 错误的写法 /* void wrongFunction(int a = 1, int b, int c = 3) { // 错误!b没有默认值 cout << a << b << c << endl; } */ // 正确的写法 void rightFunction(int a, int b = 2, int c = 3) { cout << a << b << c << endl; }
3. **错误3:默认参数与函数重载的冲突**
```cpp
// 可能引起歧义的重载
void func(int a) {
cout << "func(int): " << a << endl;
}
void func(int a, int b = 10) {
cout << "func(int, int): " << a << ", " << b << endl;
}
// 调用时的问题
func(5, 6); // 明确调用第二个版本
// func(5); // 错误!编译器不知道调用哪个版本2.5.6 小结:默认参数的价值
默认参数让我们能够:
- 简化函数调用:常用的参数不用每次都写
- 提高代码灵活性:可以根据需要指定部分参数
- 减少函数重载:一个函数可以处理多种调用方式
记住默认参数的要点:
- 默认参数必须从右往左连续
- 只能在函数声明中指定一次
- 避免与函数重载产生歧义
- 让代码更简洁和易用
2.6 内联函数:把函数"复制粘贴"到调用处
2.6.1 什么是内联函数?
- 就像复制粘贴一样
- 想象你在写作文,需要多次用到"根据相关规定"这个短语,你有两种选择:
- 方式1:每次都写全 - 直接但重复
- 方式2:建立一个简写 - 简洁但需要"查表"
- 内联函数选择第一种方式:编译器会把函数内容直接"复制粘贴"到调用的地方,避免了函数调用的开销
- 想象你在写作文,需要多次用到"根据相关规定"这个短语,你有两种选择:
2.6.2 为什么需要内联函数?
函数调用的"隐藏成本"
- 每次调用函数,计算机都要:
- 保存当前状态
- 跳转到函数位置
- 执行函数代码
- 返回结果
- 恢复之前的状态
- 对于简单的函数,这些"管理费用"可能比函数本身的工作还多!cpp
// 这个函数只做一个简单的计算 int square(int x) { return x * x; } int main() { int result = square(5); // 调用函数的成本可能比计算x*x还大 return 0; }
- 每次调用函数,计算机都要:
2.6.3 内联函数的基本语法
用内联函数很简单,主要就是一个关键字:inline。
比如定义一个计算两数之和的内联函数,只需要在函数前面加上inline:
cpp
#include <iostream>
using namespace std;
// 内联函数的声明
inline int add(int a, int b) {
return a + b;
}
inline double getPI() {
return 3.14159;
}
// 类内定义的函数自动是内联的
class Circle {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 这些函数自动是内联的(不需要写inline)
double getRadius() {
return radius;
}
double getArea() {
return 3.14159 * radius * radius;
}
};2.6.4 什么时候适合用内联函数?
适合内联的场景:短小精悍的函数
cpp// 简单的计算函数 inline int square(int x) { return x * x; } // 简单的getter/setter函数 class Student { private: string name; int age; public: inline string getName() const { return name; } inline int getAge() const { return age; } }; // 简单的判断函数 inline bool isEven(int n) { return n % 2 == 0; }
2. **不适合内联的场景:复杂的函数**
```cpp
// 包含循环的函数
void printNumbers(int count) { // 不要inline
for (int i = 0; i < count; i++) {
cout << i << " ";
}
cout << endl;
}
// 包含复杂逻辑的函数
string getGrade(int score) { // 不要inline
if (score >= 90) return "A";
else if (score >= 80) return "B";
else if (score >= 70) return "C";
else if (score >= 60) return "D";
else return "F";
}
// 递归函数
int factorial(int n) { // 不要inline
if (n <= 1) return 1;
return n * factorial(n - 1);
}小结
内联函数的优势:
- 提高性能:避免函数调用开销
- 保持代码整洁:不用为了性能放弃函数封装
- 让编译器做更好的优化
记住内联函数的要点:
- 只对简单、频繁调用的函数使用内联
- inline只是建议,编译器会做最终决定
- 在头文件中定义内联函数
- 不要滥用
适合与不适合的情况:
- 适合使用:
- 简单的getter/setter函数
- 短小的数学计算函数
- 简单的逻辑判断函数
- 不适合使用:
- 包含循环的函数
- 包含复杂控制结构的函数
- 递归函数
- 很长的函数
- 适合使用:
2.7 命名空间:给代码"分门别类"
2.7.1 什么是命名空间?
在C++程序开发中,随着项目规模增大,代码量增加,我们会遇到一个很常见的问题:不同部分的代码可能会使用相同的变量名、函数名或类名,从而引起命名冲突。为了解决这个问题,C++引入了命名空间(namespace)这个概念,它就像是给不同的代码划分"地盘",让相同的名字可以在不同的地盘中和平共存。
就像学校里的班级制度
想象一下你的学校有很多班级,每个班级都有自己的"张三"、"李四"。如果你想找人,你不能只说"找张三",而要说"找一班的张三"或"找二班的张三"。命名空间就是这样工作的——它给变量、函数和类提供了一个"姓氏"或者说"家族名称",让编译器能够区分同名但不同家族的成员。
cpp
namespace Class1 {
int zhangsan_score = 95;
int lisi_score = 88;
}
namespace Class2 {
int zhangsan_score = 76; // 不会和Class1的冲突
int lisi_score = 92;
}为什么需要命名空间?
假设你正在开发一个大型项目,团队中有多个程序员一起工作。你定义了一个名为count的变量来记录某些操作的次数,而你的同事也可能定义了一个同名的变量用于其他目的。如果没有命名空间,当这些代码合并时就会发生冲突。
cpp
// 没有命名空间时可能遇到的问题
int count = 10; // 你定义的变量
// ... 很多代码 ...
int count = 20; // 别人定义的变量,冲突了!
// 有了命名空间就不会冲突
namespace MyCode {
int count = 10; // 你的变量
}
namespace FriendCode {
int count = 20; // 朋友的变量,不冲突
}命名空间不仅仅是为了避免变量名冲突,它还有助于组织代码结构,将相关的函数、类和变量组织在一起,提高代码的可读性和可维护性。例如,标准库中的所有组件都放在std命名空间中,这样就不会与你自己定义的名字冲突。
2.7.2 命名空间的基本语法
命名空间的语法非常直观。你只需要使用namespace关键字,后面跟着命名空间的名称,然后在花括号中定义该命名空间的内容。一个命名空间可以包含变量、函数、类以及其他命名空间。
定义和使用命名空间
下面是一个更完整的例子,展示了如何定义和使用命名空间:
cpp
#include <iostream>
using namespace std;
// 定义一个游戏相关的命名空间
namespace Game {
int score = 0;
void start() {
cout << "游戏开始!" << endl;
score = 0;
}
void addScore(int points) {
score += points;
cout << "得分:" << score << endl;
}
}
// 定义一个学习相关的命名空间
namespace Study {
int score = 0; // 不会和Game::score冲突
void startExam() {
cout << "开始考试!" << endl;
}
}
int main() {
// 使用Game命名空间
Game::start();
Game::addScore(100);
// 使用Study命名空间
Study::startExam();
cout << "游戏得分:" << Game::score << endl;
return 0;
}2.7.3 using声明和using指令
每次都使用范围解析运算符::来访问命名空间中的元素可能会显得比较繁琐,尤其是当你需要频繁使用某个命名空间中的多个元素时。为了简化代码,C++提供了using声明和using指令。
using声明:只请一个朋友来
using声明允许你在当前作用域中直接使用命名空间中的特定元素,而无需每次都加上命名空间前缀。这就像是你只邀请班级中的某一个同学来你家,而不是整个班级都来。
cpp
namespace Kitchen {
void cook() { cout << "正在做饭..." << endl; }
void wash() { cout << "正在洗碗..." << endl; }
}
void usingDeclarationDemo() {
// 只引入cook函数
using Kitchen::cook;
cook(); // 可以直接使用
Kitchen::wash(); // 其他函数仍需要前缀
}using指令:请整个宿舍的人来
using指令则是将整个命名空间中的所有名称都引入当前作用域,使你可以直接使用该命名空间中的所有元素,而无需添加命名空间前缀。这就像是邀请整个班级的同学都来你家做客。
cpp
namespace Tools {
void hammer() { cout << "使用锤子" << endl; }
void screwdriver() { cout << "使用螺丝刀" << endl; }
}
void usingDirectiveDemo() {
// 引入整个Tools命名空间
using namespace Tools;
hammer(); // 可以直接使用
screwdriver(); // 可以直接使用
}2.7.4 标准命名空间std
C++标准库中的所有组件(如cout、cin、string、vector等)都定义在std命名空间中。这样设计的目的是避免标准库中的名称与你自己定义的名称冲突。
std就像学校的"标准设施"
就像学校的标准设施(图书馆、食堂、体育馆等)对所有学生开放一样,std命名空间中的组件也是为所有C++程序员提供的标准工具。每个人都可以使用,但为了避免混淆,需要明确指出你是在使用"学校的设施"而不是自己家的。
cpp
#include <iostream>
#include <string>
#include <vector>
void stdUsageDemo() {
// 方式1:完全限定名(最安全)
std::cout << "方式1:完全限定名" << std::endl;
std::string name1 = "张三";
// 方式2:选择性using声明(推荐)
using std::cout;
using std::endl;
cout << "方式2:选择性using声明" << endl;
// 方式3:using指令(小项目可以用)
using namespace std;
cout << "方式3:using指令" << endl;
}小结:命名空间的要点
命名空间让我们能够:
- 避免命名冲突:不同模块可以使用相同的名称
- 组织代码结构:相关代码放在一起,逻辑更清晰
记住命名空间的要点:
- 使用
::访问命名空间成员 using声明引入特定成员,using指令引入整个命名空间- 小项目可以使用
using namespace std - 大项目推荐使用完全限定名或选择性using声明
- 头文件中应避免使用
using namespace指令 - std是标准库的命名空间,包含cout、cin、string等
掌握了命名空间,你就能写出更加规范和专业的C++代码!
3. 动态内存管理:程序的"内存大管家"
3.1 new和delete操作符:租房和退房的故事
3.1.1 什么是动态内存管理?
就像租房子一样
想象一下你在大学里需要租房子:
- 静态内存:像学校宿舍,位置固定,大小固定,学校统一分配和回收
- 动态内存:像在外面租房,你可以根据需要选择大小,什么时候租、什么时候退房都由你决定
cpp
// 静态内存 - 就像住宿舍
int dormRoom[4]; // 固定4个床位,编译时就确定了
// 动态内存 - 就像租房
int* apartment = new int[roommates]; // 根据室友数量决定房间大小3.1.2 new操作符:向系统"租内存"
基本类型的new使用
cpp
#include <iostream>
using namespace std;
int main() {
// 就像租不同类型的房间
int* singleRoom = new int; // 租了个单间,但没装修(值不确定)
int* decoratedRoom = new int(42); // 租了个装修好的单间(初始值42)
cout << "装修好的单间: " << *decoratedRoom << endl;
// 使用这些"房间"
*singleRoom = 100;
cout << "装修后的单间: " << *singleRoom << endl;
// 用完了要"退房"
delete singleRoom;
delete decoratedRoom;
return 0;
}对象的动态分配
cpp
class Student {
public:
string name;
int age;
Student() : name("新同学"), age(18) {
cout << "新学生入学!" << endl;
}
Student(string n, int a) : name(n), age(a) {
cout << name << " 同学入学了!" << endl;
}
~Student() {
cout << name << " 同学毕业了!" << endl;
}
void showInfo() {
cout << "学生:" << name << ",年龄:" << age << endl;
}
};
void studentDemo() {
Student* student1 = new Student(); // 默认构造
Student* student2 = new Student("张三", 20); // 带参数构造
student1->showInfo();
student2->showInfo();
// 学生毕业(删除对象)
delete student1;
delete student2;
}3.2 new[]和delete[]:租一整栋楼
3.2.1 数组的动态分配
cpp
void arrayDemo() {
int roomCount;
cout << "你要租几个房间?";
cin >> roomCount;
// 租一排房间
int* rooms = new int[roomCount]; // 空房间(值随机)
// 布置房间
for (int i = 0; i < roomCount; i++) {
rooms[i] = i + 1; // 给每个房间编号
}
cout << "布置好的房间:";
for (int i = 0; i < roomCount; i++) {
cout << rooms[i] << " ";
}
cout << endl;
// 退租所有房间
delete[] rooms;
}对象数组
cpp
class Dormitory {
public:
string roomNumber;
int capacity;
Dormitory() : roomNumber("空房间"), capacity(4) {
cout << "新建了一个 " << roomNumber << endl;
}
~Dormitory() {
cout << roomNumber << " 被拆除了" << endl;
}
void showInfo() {
cout << roomNumber << "(容量:" << capacity << ")" << endl;
}
};
void dormitoryDemo() {
// 建一栋宿舍楼
int floors = 3;
Dormitory* building = new Dormitory[floors];
// 给每层宿舍编号
for (int i = 0; i < floors; i++) {
building[i].roomNumber = "第" + to_string(i + 1) + "层";
}
// 查看宿舍楼
cout << "宿舍楼建成:" << endl;
for (int i = 0; i < floors; i++) {
building[i].showInfo();
}
// 拆除宿舍楼
delete[] building;
}3.3 内存泄漏的预防:不要做"房屋钉子户"
3.3.1 什么是内存泄漏?
内存泄漏的常见情况
cpp
// 情况1:忘记释放内存
void forgetfulRenter() {
int* room = new int(42);
cout << "租了房间,房间号:" << *room << endl;
// 忘记退房
// delete room; // 这行被忘记了!
cout << "搬走了,但忘记退房(内存泄漏)" << endl;
}
// 情况2:指针被覆盖
void lostKeys() {
int* room1 = new int(100);
cout << "租了第一个房间,房间号:" << *room1 << endl;
room1 = new int(200); // 把第一个房间的钥匙弄丢了!
cout << "钥匙弄混了,第一个房间进不去了(内存泄漏)" << endl;
delete room1; // 只能退第二个房间
}
// 情况3:new[]和delete的错误搭配
void wrongKey() {
int* apartments = new int[10];
cout << "租了一整栋公寓(10个房间)" << endl;
// 错误:用单个房间的钥匙退整栋公寓
delete apartments; // 应该用 delete[]
cout << "用错钥匙了,可能会出问题" << endl;
// 正确的做法应该是:
// delete[] apartments;
}3.3.2 预防内存泄漏的方法
记住这些要点:
- 每个
new都要有对应的delete - 每个
new[]都要有对应的delete[] - 不要弄丢指针(房间钥匙)
- 可以把指针初始化为nullptr,用完后也置为nullptr
cpp
int* room = nullptr; // 先不租房
// 需要时再租
room = new int(50);
// 用完立即退租
delete room;
room = nullptr; // 清空钥匙小结:动态内存管理的要点
动态内存管理就像租房子一样:
- new = 租房:根据需要分配内存
- delete = 退房:用完及时释放内存
- new[] = 租一栋楼:分配数组
- delete[] = 退整栋楼:释放数组
- 内存泄漏 = 不退房:占着房子不用,浪费资源
只要记住:租了房子一定要还,租了整栋楼要用对钥匙退。
4. 类与对象基础
4.1 面向对象编程概述
学编程为什么要学面向对象?
想象一下,你要管理一个班级的学生信息。如果用C语言,你可能会这样写:
c
// C语言的做法
char student_names[50][20]; // 50个学生的姓名
int student_ages[50]; // 50个学生的年龄
double student_scores[50]; // 50个学生的成绩
// 添加学生信息
void add_student(char names[][20], int ages[], double scores[],
int index, char* name, int age, double score) {
strcpy(names[index], name);
ages[index] = age;
scores[index] = score;
}这样写有什么问题?
- 数据散乱:学生的姓名、年龄、成绩分别存在不同的数组里,很容易搞混
- 不安全:如果你不小心修改了某个数组,可能会破坏数据
- 难维护:要增加一个新属性(比如电话号码),需要修改很多地方
面向对象编程就是为了解决这些问题。它的核心思想是:把相关的数据和操作放在一起,形成一个整体。
4.1.1 什么是面向对象编程?
简单来说,面向对象就是把现实世界的事物抽象成程序中的"对象"。
cpp
// 面向对象的做法
class Student {
private:
string name; // 学生的姓名
int age; // 学生的年龄
double score; // 学生的成绩
public:
// 设置学生信息
void setInfo(string n, int a, double s) {
name = n;
age = a;
score = s;
}
// 显示学生信息
void showInfo() {
cout << "姓名:" << name << ",年龄:" << age << ",成绩:" << score << endl;
}
// 学生学习(成绩提高)
void study() {
score += 5; // 学习后成绩提高5分
cout << name << "努力学习,成绩提高到" << score << "分!" << endl;
}
};
int main() {
Student zhangsan; // 创建一个学生对象
zhangsan.setInfo("张三", 20, 85.0);
zhangsan.showInfo();
zhangsan.study();
return 0;
}4.1.2 类与对象的关系
类就是图纸,对象就是产品
这个比喻最容易理解:
- 类(Class):就像建筑图纸,定义了房子应该有几个房间、多高、什么结构
- 对象(Object):就像根据图纸建造的实际房子,每栋房子都有自己的门牌号、住户、装修风格
cpp
class House { // 这是图纸
private:
string address;
int rooms;
double area;
public:
House(string addr, int r, double a) : address(addr), rooms(r), area(a) {}
void showInfo() {
cout << "地址:" << address << ",房间数:" << rooms << ",面积:" << area << "平米" << endl;
}
};
int main() {
// 用同一个图纸(类)建造不同的房子(对象)
House house1("北京路1号", 3, 120.5);
House house2("上海路2号", 2, 89.3);
house1.showInfo();
house2.showInfo();
return 0;
}重要的概念:
- 一个类可以创建多个对象
- 每个对象都是独立的
4.1.3 封装:把东西包起来
封装就像给你的私人物品加个锁。你不希望别人随便翻你的书包,同样,对象的内部数据也不应该被随便修改。
cpp
class BankAccount {
private:
double balance; // 余额,外部不能直接访问
public:
BankAccount(double initial_balance = 0) : balance(initial_balance) {}
// 存钱
void deposit(double amount) {
if (amount > 0) {
balance += amount;
cout << "存款成功!当前余额:" << balance << endl;
} else {
cout << "存款金额必须大于0" << endl;
}
}
// 取钱
void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
cout << "取款成功!当前余额:" << balance << endl;
} else {
cout << "余额不足或金额无效" << endl;
}
}
// 查询余额
double getBalance() {
return balance;
}
};
int main() {
BankAccount account(1000); // 初始余额1000
account.deposit(500); // 存500
account.withdraw(200); // 取200
cout << "当前余额:" << account.getBalance() << endl;
return 0;
}为什么要这样做?
想象一下,如果银行账户的余额可以随便修改:
cpp
// 如果balance是public的,就可能出现这种情况
BankAccount account;
account.balance = 1000000; // 直接修改余额为100万?这太危险了!通过封装,我们确保了:
- 存款金额必须大于0
- 取款金额不能超过余额
- 余额只能通过正当途径修改
小结:面向对象编程的核心思想
- 把相关的数据和操作放在一起,形成一个类
- 类是模板,对象是实例
- 封装保护数据安全,只能通过指定的方法访问
- 代码结构更清晰,更容易维护和扩展
4.2 类的定义与声明
4.2.1 类的基本语法
类就像一个设计图,定义了对象应该有什么特征和能做什么事情。
一个类的基本结构如下:
cpp
class 类名 {
private:
// 私有成员变量
public:
// 公有成员函数
}; // 记得加分号!创建一个学生类:
cpp
class Student {
private:
string name;
int age;
float score;
public:
Student(string n, int a, float s) {
name = n;
age = a;
score = s;
}
void showInfo() {
cout << "姓名:" << name << endl;
cout << "年龄:" << age << endl;
cout << "成绩:" << score << endl;
}
};使用类:
cpp
Student zhangsan("张三", 18, 85.5);
zhangsan.showInfo();4.2.2 头文件和源文件分离
大型项目中,通常把类的声明和实现分开:
Student.h (头文件)
cpp
#ifndef STUDENT_H
#define STUDENT_H
class Student {
private:
string name;
int age;
float score;
public:
Student(string n, int a, float s); // 只声明
void showInfo(); // 只声明
};
#endifStudent.cpp (源文件)
cpp
#include "Student.h"
#include <iostream>
// 构造函数实现
Student::Student(string n, int a, float s) {
name = n;
age = a;
score = s;
}
// 成员函数实现
void Student::showInfo() {
cout << "姓名:" << name << endl;
cout << "年龄:" << age << endl;
cout << "成绩:" << score << endl;
}4.2.3 成员变量和成员函数
- 成员变量:描述对象的特征(属性)
- 成员函数:描述对象能做什么(行为)
成员变量就像人的身高、体重、年龄,成员函数就像人能跑步、说话、思考。
例如:
cpp
class Dog {
private:
string name; // 成员变量
int age; // 成员变量
double weight; // 成员变量
public:
void bark() { // 成员函数
cout << name << ":汪汪汪!" << endl;
}
void eat() { // 成员函数
weight += 0.1;
cout << name << "吃饱了,现在" << weight << "公斤" << endl;
}
};4.2.4 访问控制符
访问控制符限定类成员的访问权限:
private (私有): 只有类内部可以访问
- 一般用于成员变量,防止外部直接修改
- 实现封装,保护数据安全
public (公有): 任何地方都能访问
- 一般用于接口函数,供外部调用
- 比如构造函数、设置/获取数据的函数
protected (保护): 类内部和子类可以访问
- 在继承时使用,让子类可以访问父类的某些成员
cpp
class Phone {
private:
int battery; // 电量,不能随便改
public:
void charge() { // 充电方法,任何人都能用
battery += 10;
if(battery > 100) battery = 100;
}
int getBattery() { // 获取电量,任何人都能查看
return battery;
}
};小结
类的定义:使用
class关键字,后面加上类名,然后在花括号内定义成员变量和成员函数,最后记得加分号。头文件和源文件分离:
- 头文件(.h)放类的声明
- 源文件(.cpp)放类的实现
- 分离可以提高编译效率,方便团队协作
成员变量和成员函数:
- 成员变量保存对象的状态和特征
- 成员函数实现对象的行为和功能
- 两者共同构成了一个完整的类
访问控制符:
- private:保护内部数据
- public:提供外部接口
- protected:在继承中使用
记住:合理设计类是面向对象编程的关键,好的类设计应该让使用者只关注"做什么",而不必关心"怎么做"。
4.3 对象的创建与使用
4.3.1 对象的创建方式
类就像图纸,对象就是根据图纸制造出来的实物。掌握创建对象的方式,是使用面向对象编程的第一步。
栈上创建对象(最常用的方式):
cpp
// 方式1:默认构造函数
Student student1;
// 方式2:带参数的构造函数
Student student2("张三", 18, 90.5);
// 方式3:拷贝构造函数
Student student3 = student2; // 或者 Student student3(student2);堆上创建对象(动态创建):
cpp
// 动态创建单个对象
Student* pStudent = new Student("李四", 19, 85.0);
// 使用完后必须释放内存
delete pStudent;
// 动态创建对象数组
Student* students = new Student[3];
// 使用完后释放
delete[] students;记住的要点:
- 栈对象离开作用域会自动销毁
- 堆对象必须手动用delete释放,否则会内存泄漏
Student s = Student("张三", 18);这种写法看起来是赋值,实际是调用构造函数
4.3.2 对象成员的访问
访问对象的成员有两种方式:用点(.)和箭头(->)
点操作符(对象直接访问):
cpp
Student zhang("张三", 18, 90.5);
zhang.showInfo(); // 显示信息
cout << zhang.getName(); // 获取姓名箭头操作符(通过指针访问):
cpp
Student* pLi = new Student("李四", 19, 85.0);
pLi->showInfo(); // 显示信息
cout << pLi->getName(); // 获取姓名
delete pLi;记忆技巧:
- 普通对象用点(.)
- 指针对象用箭头(->)
- 如果你看到变量名前面有星号(*)声明或者用了new,那就用箭头
指针使用注意事项:
- 使用指针前先检查是否为空
cpp
if (pStudent != nullptr) {
pStudent->study();
}- 用完指针后记得删除并置空
cpp
delete pStudent;
pStudent = nullptr; // 防止成为野指针小结
对象创建:
- 栈上创建:
Student s1;、Student s2("张三", 18); - 堆上创建:
Student* ps = new Student(); - 记得堆对象用完后要
delete
- 栈上创建:
对象访问:
- 普通对象用点:
student.getName() - 指针对象用箭头:
pStudent->getName() - 访问前检查指针是否为空
- 普通对象用点:
常见错误:
- 忘记释放堆对象造成内存泄漏
- 混用点和箭头操作符
- 使用已删除的指针
- 忘记检查指针是否为空
学会创建和使用对象是面向对象编程的基础,熟练掌握这些知识点后,你就可以开始设计和实现更复杂的类了。
4.4 构造函数
构造函数是什么?
构造函数就是对象创建时自动调用的特殊函数,主要用来初始化对象的成员变量。它就像工厂的装配线,负责给"产品"安装各种零件。
构造函数有这些特点:
- 函数名与类名完全相同
- 没有返回值类型(连void都不写)
- 创建对象时自动调用,不能手动调用
- 可以有多个版本(重载)
4.4.1 默认构造函数
默认构造函数就是不带参数的构造函数,它给对象的成员变量设置默认值。
cpp
class Student {
private:
string name;
int age;
float score;
public:
// 这就是默认构造函数
Student() {
name = "未命名";
age = 0;
score = 0.0;
cout << "创建了一个默认学生" << endl;
}
void showInfo() {
cout << "姓名:" << name << ", 年龄:" << age << ", 成绩:" << score << endl;
}
};
// 使用默认构造函数
Student s1; // 自动调用默认构造函数特别注意:
- 如果你不写任何构造函数,编译器会自动生成一个默认构造函数
- 但这个自动生成的构造函数不会初始化基本类型成员(如int、float等)
- 一旦你定义了其他构造函数,编译器就不会自动生成默认构造函数了
4.4.2 带参数的构造函数
带参数的构造函数允许你在创建对象时直接设置成员变量的值。
cpp
class Student {
private:
string name;
int age;
float score;
public:
// 带参数的构造函数
Student(string n, int a, float s) {
name = n;
age = a;
score = s;
}
void showInfo() {
cout << "姓名:" << name << ", 年龄:" << age << ", 成绩:" << score << endl;
}
};
// 使用带参数的构造函数
Student zhangsan("张三", 18, 85.5);构造函数中可以进行参数验证,确保对象的初始状态是有效的:
cpp
Student(string n, int a, float s) {
name = n;
// 验证年龄
if (a < 0 || a > 100) {
cout << "年龄无效,设为18" << endl;
age = 18;
} else {
age = a;
}
// 验证成绩
if (s < 0 || s > 100) {
cout << "成绩无效,设为0" << endl;
score = 0;
} else {
score = s;
}
}4.4.3 拷贝构造函数
拷贝构造函数用于根据一个已存在的对象复制出一个新对象。它接收同类型的对象引用作为参数。
cpp
class Student {
private:
string name;
int age;
float score;
public:
// 普通构造函数
Student(string n, int a, float s) {
name = n;
age = a;
score = s;
}
// 拷贝构造函数
Student(const Student& other) {
name = other.name;
age = other.age;
score = other.score;
cout << "复制了学生:" << name << endl;
}
void showInfo() {
cout << "姓名:" << name << ", 年龄:" << age << ", 成绩:" << score << endl;
}
};
// 使用拷贝构造函数
Student zhangsan("张三", 18, 85.5);
Student zhangsan2 = zhangsan; // 调用拷贝构造函数
Student zhangsan3(zhangsan); // 也是调用拷贝构造函数什么时候会用到拷贝构造函数?
- 用一个对象初始化另一个对象时
- 函数参数按值传递对象时
- 函数返回对象时
如果你不自己写,编译器会自动生成一个简单的拷贝构造函数,但如果类中有指针成员,就需要自己写一个深拷贝的拷贝构造函数。
4.4.4 构造函数重载
一个类可以有多个构造函数,只要它们的参数列表不同。这称为构造函数重载。
cpp
class Student {
private:
string name;
int age;
float score;
public:
// 构造函数1:默认构造
Student() {
name = "未命名";
age = 18;
score = 0;
}
// 构造函数2:只设置姓名
Student(string n) {
name = n;
age = 18;
score = 0;
}
// 构造函数3:设置姓名和年龄
Student(string n, int a) {
name = n;
age = a;
score = 0;
}
// 构造函数4:设置全部信息
Student(string n, int a, float s) {
name = n;
age = a;
score = s;
}
};
// 使用不同的构造函数
Student s1; // 调用构造函数1
Student s2("李四"); // 调用构造函数2
Student s3("王五", 20); // 调用构造函数3
Student s4("赵六", 19, 92.5); // 调用构造函数4构造函数重载让我们可以用不同的方式创建对象,非常灵活。
小结
构造函数是对象创建时自动调用的特殊函数,用来初始化对象
默认构造函数没有参数,给成员变量设置默认值
带参数的构造函数允许创建对象时直接设置成员变量值
拷贝构造函数用来根据已有对象复制出新对象
构造函数重载让我们可以用不同方式创建对象
记住:好的构造函数应该确保对象一创建就处于有效状态,就像新产品出厂前要经过全面检查一样。
4.5 析构函数
什么是析构函数?
析构函数是对象被销毁时自动调用的特殊函数。如果说构造函数负责"创建"对象,那么析构函数就负责"清理"对象。
析构函数主要负责:
- 释放动态分配的内存
- 关闭打开的文件
- 断开网络连接
- 执行其他必要的清理工作
4.5.1 析构函数的基本特点
析构函数有以下特点:
- 名字是波浪线(~)加类名,如
~Student() - 没有返回值类型(包括void)
- 不能有参数
- 一个类只能有一个析构函数
- 如果你不写,编译器会自动生成一个简单的析构函数
cpp
class Resource {
private:
int* data;
string name;
public:
// 构造函数
Resource(string n) {
name = n;
data = new int[100]; // 分配内存
cout << "资源" << name << "被创建了" << endl;
}
// 析构函数
~Resource() {
delete[] data; // 释放内存
cout << "资源" << name << "被销毁了" << endl;
}
};4.5.2 析构函数的调用时机
析构函数会在以下几种情况自动调用:
1. 局部对象离开作用域时
cpp
void testLocalObject() {
Resource r1("局部资源"); // 构造函数被调用
cout << "函数即将结束" << endl;
} // r1离开作用域,析构函数被调用2. 使用delete删除堆对象时
cpp
void testHeapObject() {
Resource* r2 = new Resource("堆资源"); // 构造函数被调用
// 使用资源...
delete r2; // 析构函数被调用
}3. 程序结束时,全局对象会被销毁
cpp
Resource globalRes("全局资源"); // 程序开始时构造
int main() {
cout << "主函数开始" << endl;
// ...
return 0;
} // 程序结束时,globalRes的析构函数被调用4.5.3 析构顺序是什么
对象的析构顺序与构造顺序相反:
- 后创建的对象先销毁
- 对象成员的析构顺序与它们在类中声明的顺序相反
cpp
class Example {
private:
Resource r1{"成员1"};
Resource r2{"成员2"};
public:
Example() {
cout << "Example对象创建" << endl;
}
~Example() {
cout << "Example对象销毁" << endl;
}
};
int main() {
Example ex;
return 0;
}
// 输出顺序:
// 资源成员1被创建了
// 资源成员2被创建了
// Example对象创建
// Example对象销毁
// 资源成员2被销毁了
// 资源成员1被销毁了小结
析构函数的作用:
- 释放动态分配的内存
- 关闭文件、网络连接等资源
- 执行其他必要的清理工作
析构函数的特点:
- 名称为~类名
- 没有返回值
- 不能有参数
- 一个类只有一个析构函数
- 自动调用,不需要手动调用
调用时机:
- 局部对象离开作用域时
- 堆对象被delete时
- 程序结束时(全局对象)
必须写析构函数的情况:
- 类中有动态分配的内存(使用了new)
- 类中有需要关闭的资源(文件、网络等)
掌握析构函数是编写可靠程序的关键,它确保资源被正确释放,避免内存泄漏和其他资源浪费问题。
4.6 this指针
什么是this指针?
this指针是C++中的一个特殊指针,它指向当前对象。每个成员函数都隐含一个this指针,让函数知道是哪个对象在调用它。
4.6.1 this指针的基本用法
this指针主要用在两种情况:
1. 区分成员变量和参数
cpp
class Student {
private:
string name;
public:
// 参数名与成员变量名相同时
void setName(string name) {
this->name = name; // this->name是成员变量,name是参数
}
};2. 返回对象自身
cpp
class Student {
private:
string name;
int score;
public:
// 返回自身引用
Student& addScore(int points) {
score += points;
return *this; // 返回当前对象
}
};
// 使用方法
Student s;
s.addScore(10); // 普通调用4.6.2 链式调用
通过返回对象自身,可以实现连续调用多个方法:
cpp
class Counter {
private:
int value;
public:
Counter() : value(0) {}
Counter& add(int n) {
value += n;
return *this;
}
Counter& subtract(int n) {
value -= n;
return *this;
}
int getValue() {
return value;
}
};
int main() {
Counter c;
// 链式调用
int result = c.add(5).subtract(2).add(10).getValue();
cout << "结果: " << result << endl; // 输出: 结果: 13
return 0;
}小结
this指针是什么:
- 指向当前对象的指针
- 每个非静态成员函数都有
主要用途:
- 区分成员变量和同名参数
- 返回对象自身实现链式调用
特点:
- 自动存在,不需要定义
- 只能在非静态成员函数中使用
this指针是面向对象编程中的重要概念,掌握它可以写出更简洁、更优雅的代码。
4.7 对象数组和对象指针
4.7.1 对象数组
对象数组就是存放多个相同类型对象的数组。就像一个班级里有多个学生一样,每个学生都是一个独立的对象。
创建对象数组的两种方式:
cpp
// 方式1:创建默认对象数组,再逐个赋值
Student students[3]; // 创建3个默认学生对象
students[0] = Student("张三", 18); // 重新赋值
students[1] = Student("李四", 19);
students[2] = Student("王五", 20);
// 方式2:创建时直接初始化
Student students[] = {
Student("张三", 18),
Student("李四", 19),
Student("王五", 20)
};动态创建对象数组:
cpp
// 在运行时确定数组大小
int count;
cout << "请输入学生人数:";
cin >> count;
// 动态创建学生数组
Student* students = new Student[count];
// 设置学生信息
for (int i = 0; i < count; i++) {
string name;
int age;
cout << "输入第" << (i+1) << "个学生信息:";
cin >> name >> age;
students[i].setInfo(name, age);
}
// 使用完后释放内存
delete[] students; // 注意要使用delete[]4.7.2 对象指针
对象指针是指向对象的指针,可以通过指针来访问和操作对象。
cpp
class Dog {
public:
void bark() {
cout << "汪汪汪!" << endl;
}
};
int main() {
// 创建对象和指向它的指针
Dog myDog;
Dog* pDog = &myDog; // 指针指向myDog对象
// 通过指针调用方法
pDog->bark(); // 使用->操作符
return 0;
}动态创建单个对象:
cpp
// 动态创建对象
Dog* pDog = new Dog();
// 使用对象
pDog->bark();
// 用完后释放内存
delete pDog; // 注意用delete释放对象指针数组:
cpp
// 创建对象指针数组
Dog* dogs[3];
// 给数组元素分配对象
for (int i = 0; i < 3; i++) {
dogs[i] = new Dog();
}
// 使用对象
for (int i = 0; i < 3; i++) {
dogs[i]->bark();
}
// 释放内存
for (int i = 0; i < 3; i++) {
delete dogs[i];
}4.7.3 内存管理注意事项
使用动态创建的对象和数组时,要注意正确释放内存:
对象数组用delete[]:
cppStudent* students = new Student[10]; // ... delete[] students; // 正确
2. **单个对象用delete**:
```cpp
Student* s = new Student();
// ...
delete s; // 正确小结
对象数组:
- 存放多个相同类型的对象
- 可以静态创建或动态创建
- 动态创建需要用
delete[]释放
对象指针:
- 指向对象的指针
- 通过
->访问对象成员 - 动态创建对象需要用
delete释放
内存管理:
- 数组:
new T[]→delete[] - 对象:
new T→delete - 忘记释放会导致内存泄漏
- 数组:
理解对象数组和对象指针,对于管理多个对象和灵活使用对象非常重要,也是C++面向对象编程的基础知识。
5.类的特殊成员与机制
5.1 静态成员
什么是静态成员?
静态成员是属于整个类而不是单个对象的成员。可以把它们想象成班级里的公共财产,比如:班级里的黑板,不管是哪个学生都用同一个黑板。
静态成员有两个特点:
- 所有对象共享:每个对象看到的静态成员都是同一个
- 可以通过类名访问:不需要创建对象就能使用
5.1.1 静态成员变量
静态成员变量用于存储整个类共享的数据。比如统计创建了多少个对象、保存所有对象共用的设置等。
cpp
class Counter {
private:
static int count; // 声明静态成员变量
public:
Counter() {
count++; // 每创建一个对象,计数器加1
}
static int getCount() {
return count;
}
};
// 必须在类外初始化静态成员变量
int Counter::count = 0;
// 使用
int main() {
Counter c1, c2, c3;
cout << "对象数量:" << Counter::getCount() << endl; // 输出:对象数量:3
return 0;
}重要规则:
- 静态成员变量必须在类外定义和初始化
- 所有对象共享同一份静态成员变量
- 可以通过类名直接访问
5.1.2 静态成员函数
静态成员函数是不依赖于任何对象的函数,它不能访问非静态成员变量。
cpp
class MathTools {
public:
static double PI;
static double circleArea(double r) {
return PI * r * r;
}
static int max(int a, int b) {
return (a > b) ? a : b;
}
};
double MathTools::PI = 3.1416;
// 使用
int main() {
// 不需要创建对象
cout << "PI: " << MathTools::PI << endl;
cout << "圆面积: " << MathTools::circleArea(5) << endl;
cout << "最大值: " << MathTools::max(10, 20) << endl;
return 0;
}静态成员函数的特点:
- 通过类名直接调用:
MathTools::circleArea(5) - 不能访问非静态成员
- 没有this指针
5.1.3 静态成员的应用场景
1. 对象计数:记录创建了多少个对象
2. 共享配置信息:存储全局设置
3. 工具函数:提供与特定对象无关的功能
cpp
// 简单应用示例
class Config {
private:
static string serverIP;
static int port;
public:
static void setServer(string ip, int p) {
serverIP = ip;
port = p;
}
static void showInfo() {
cout << "服务器:" << serverIP << ":" << port << endl;
}
};
string Config::serverIP = "127.0.0.1";
int Config::port = 8080;小结
静态成员变量:
- 被所有对象共享
- 必须在类外定义和初始化
- 通常用于计数器、共享设置等
静态成员函数:
- 不需要对象就能调用
- 不能访问非静态成员
- 通常用于工具函数、获取共享信息等
使用时机:
- 需要所有对象共享数据时
- 不需要对象实例也能工作的函数
- 统计或管理类的所有实例
静态成员是面向对象编程中非常实用的工具,它们让我们能够在类的层面上处理数据和功能,而不仅仅局限于单个对象。
5.2 友元
什么是友元?
友元是C++中的一种机制,允许特定的函数或类访问另一个类的私有成员。它就像给特定朋友一把你房间的钥匙。
5.2.1 友元函数
友元函数可以访问类的私有成员。
cpp
class Box {
private:
double width;
double height;
public:
Box(double w, double h) : width(w), height(h) {}
// 声明友元函数
friend double getArea(const Box& box);
};
// 友元函数实现
double getArea(const Box& box) {
// 可以直接访问私有成员
return box.width * box.height;
}
int main() {
Box myBox(5, 3);
cout << "面积: " << getArea(myBox) << endl; // 输出: 15
return 0;
}5.2.2 友元类
友元类的所有成员函数都可以访问另一个类的私有成员。
cpp
class Student {
private:
string name;
int score;
public:
Student(string n, int s) : name(n), score(s) {}
// 声明Teacher为友元类
friend class Teacher;
};
class Teacher {
public:
void showScore(const Student& s) {
// 可以访问Student的私有成员
cout << s.name << "的分数: " << s.score << endl;
}
};
int main() {
Student s("张三", 85);
Teacher t;
t.showScore(s); // 输出: 张三的分数: 85
return 0;
}5.2.3 友元的特点
单向关系:A是B的友元,B不自动成为A的友元
不可传递:B是A的友元,C是B的友元,C不是A的友元
破坏封装:友元应谨慎使用,因为它降低了类的封装性
小结
友元函数:
- 普通函数可以访问类的私有成员
- 用于需要访问内部数据的操作
- 常用于运算符重载
友元类:
- 一个类可以访问另一个类的私有成员
- 用于紧密合作的类关系
使用原则:
- 尽量减少友元的使用
- 只在真正需要时才声明友元
友元提供了一种有限打破封装的方式,在特定情况下很有用,但要避免过度使用。
5.3 常成员函数和常对象
什么是常成员函数和常对象?
- 常成员函数:以
const关键字修饰的成员函数,承诺不会修改对象的数据 - 常对象:以
const关键字声明的对象,其成员变量不能被修改
5.3.1 常成员函数
cpp
class Student {
private:
string name;
int score;
public:
Student(string n, int s) : name(n), score(s) {}
// 常成员函数 - 不修改对象状态
string getName() const { return name; }
int getScore() const { return score; }
// 非常成员函数 - 可修改对象状态
void setScore(int s) { score = s; }
};特点:
- 函数声明后面加
const关键字 - 不能修改任何成员变量
- 不能调用非常成员函数
5.3.2 常对象
cpp
// 创建常对象
const Student zhang("张三", 85);
// 只能调用常成员函数
cout << zhang.getName() << endl; // 正确
// zhang.setScore(90); // 错误!常对象不能调用非常成员函数特点:
- 声明对象时前面加
const - 成员变量不能修改
- 只能调用常成员函数
5.3.3 mutable关键字
cpp
class Counter {
private:
int value;
mutable int accessCount; // 可在常函数中修改
public:
Counter() : value(0), accessCount(0) {}
int getValue() const {
accessCount++; // 可以修改mutable成员
return value;
}
};mutable成员:
- 即使在常成员函数中也可以修改
- 常对象的mutable成员也可以修改
- 通常用于统计、缓存等不影响对象逻辑状态的情况
5.3.5 应用场景
常成员函数主要用于:
- 获取器函数
cpp
int getAge() const { return age; }- 计算与查询
cpp
double getArea() const { return width * height; }
bool isEmpty() const { return size == 0; }- 显示与打印
cpp
void display() const { cout << "数据: " << data << endl; }小结
常成员函数:
- 不修改对象状态
- 常对象和非常对象都可调用
常对象:
- 成员不可修改
- 只能调用常成员函数
mutable:
- 常成员函数中可修改的特殊成员
最佳实践:
- 不修改对象状态的函数应声明为常成员函数
- getter函数应该是常成员函数
- 使用mutable要谨慎
常成员函数和常对象是提高代码安全性和表达意图的重要机制。
5.4 运算符重载
什么是运算符重载?
运算符重载让我们可以为自定义类定义运算符(如+、-、==等)的行为,使代码更加直观。
比如我们定义了复数类,通过重载可以直接写:
cpp
Complex c3 = c1 + c2; // 比 c1.add(c2) 更自然5.4.1 基本语法
cpp
// 成员函数形式
返回类型 operator运算符(参数列表);
// 友元函数形式
friend 返回类型 operator运算符(参数列表);5.4.2 成员函数重载
cpp
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载加法运算符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 重载减法运算符
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}
// 重载等于比较运算符
bool operator==(const Complex& other) const {
return (real == other.real) && (imag == other.imag);
}
void display() const {
cout << real;
if (imag >= 0) cout << "+" << imag << "i";
else cout << imag << "i";
}
};
// 使用示例
int main() {
Complex c1(3, 4);
Complex c2(1, 2);
Complex c3 = c1 + c2; // 使用重载的+运算符
c3.display(); // 输出 4+6i
if (c1 == c2) // 使用重载的==运算符
cout << "相等" << endl;
else
cout << "不相等" << endl;
return 0;
}5.4.3 友元函数重载
友元函数形式适用于:
- 需要非成员对象作为左操作数
- 输入输出运算符重载
cpp
class Vector {
private:
int x, y;
public:
Vector(int a = 0, int b = 0) : x(a), y(b) {}
// 友元函数声明
friend Vector operator*(int num, const Vector& v);
friend ostream& operator<<(ostream& os, const Vector& v);
};
// 友元函数实现 - 数字乘向量
Vector operator*(int num, const Vector& v) {
return Vector(num * v.x, num * v.y);
}
// 友元函数实现 - 输出流
ostream& operator<<(ostream& os, const Vector& v) {
os << "(" << v.x << "," << v.y << ")";
return os;
}
// 使用示例
int main() {
Vector v1(3, 4);
Vector v2 = 2 * v1; // 使用重载的*运算符
cout << v2 << endl; // 使用重载的<<运算符,输出(6,8)
return 0;
}小结
选择使用方式:
- 成员函数:单目运算符、赋值类运算符(=, +=等)、下标[]、调用()
- 友元函数:双目运算符需要对称性、输入输出(<<, >>)
常用重载运算符:
- 算术运算符:+, -, *, /
- 比较运算符:==, !=, <, >
- 输入输出:<<, >>
- 赋值:=
注意事项:
- 保持运算符原有语义
- 相关运算符应保持一致(如重载+也应重载+=)
运算符重载是C++面向对象编程的强大特性,合理使用可以让代码更加直观自然。
6. 继承
6.1 继承的基本概念
继承是面向对象编程中的重要概念,它允许我们基于已有的类创建新类。通过继承,新类可以获得已有类的所有成员,并可以添加自己的特性。
想象你在设计一个图形程序:所有图形都有位置、颜色等属性,但圆形有半径,矩形有长宽。通过继承,可以先定义一个基本图形类,再让圆形和矩形继承这个类,这样就不用在每个形状中重复编写相同的代码。
6.1.1 继承的语法
cpp
class 派生类名 : 继承方式 基类名 {
// 派生类的成员
};其中继承方式有三种:public、protected和private,最常用的是public。
例如:
cpp
class Shape {
protected:
int x, y;
public:
Shape(int x0, int y0) : x(x0), y(y0) {}
void move(int dx, int dy) {
x += dx;
y += dy;
}
void showPosition() {
cout << "位置: (" << x << "," << y << ")" << endl;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(int x0, int y0, double r) : Shape(x0, y0), radius(r) {}
double getArea() {
return 3.14 * radius * radius;
}
void showInfo() {
showPosition(); // 调用基类方法
cout << "半径: " << radius << endl;
cout << "面积: " << getArea() << endl;
}
};使用示例:
cpp
int main() {
Circle c(10, 20, 5);
c.move(5, 10); // 调用继承的方法
c.showPosition(); // 调用继承的方法
c.showInfo(); // 调用自己的方法
return 0;
}6.1.2 继承的特点
代码重用
- 派生类自动获得基类的所有成员
- 避免重复编写相同的代码
"是一个"关系
- 每个圆是一个形状
- 每个派生类对象也是一个基类对象
扩展和修改
- 派生类可以添加新的成员
- 派生类可以重写(覆盖)基类的方法
访问权限
public:所有人都能访问protected:只有自己和子类能访问private:只有自己能访问
构造和析构顺序
- 构造:先基类,后派生类
- 析构:先派生类,后基类
小结
继承的作用:
- 代码重用,避免重复编写相似代码
- 建立类之间的层次关系
继承的语法:
class 派生类 : public 基类- 派生类构造函数需要调用基类构造函数
注意事项:
- 继承表示"是一个"的关系
- 派生类可以访问基类的public和protected成员
- 构造时先基类后派生类,析构时反过来
继承是面向对象编程的三大特性之一,合理使用继承可以让代码更加模块化、易维护。
6.2 继承方式
C++提供三种继承方式:public、protected和private,它们决定了派生类如何继承基类成员的访问权限。
6.2.1 公有继承(public)
公有继承是最常用的继承方式,表示"是一个"的关系。
规则:
- 基类的public成员 → 派生类的public成员
- 基类的protected成员 → 派生类的protected成员
- 基类的private成员 → 派生类无法访问
cpp
class Person {
private:
string idNumber;
protected:
string name;
public:
int age;
void speak() {
cout << name << "在说话" << endl;
}
};
class Student : public Person {
public:
void study() {
cout << name << "在学习"; // 可访问基类protected成员
// idNumber = "123"; // 错误!不能访问基类private成员
}
};
int main() {
Student s;
s.age = 20; // 可访问继承的public成员
s.speak(); // 可访问继承的public方法
}6.2.2 保护继承(protected)
保护继承将基类的public成员变成派生类的protected成员。
cpp
class Base {
public:
void func() { cout << "Base::func()" << endl; }
};
class Derived : protected Base {
public:
void test() {
func(); // 可访问,但继承为protected
}
};
int main() {
Derived d;
// d.func(); // 错误!protected继承使func变为protected
}6.2.3 私有继承(private)
私有继承将基类的所有可访问成员变成派生类的私有成员。
cpp
class Base {
public:
void func() { cout << "Base::func()" << endl; }
};
class Derived : private Base {
public:
void test() {
func(); // 可访问,继承为private
}
};
class Further : public Derived {
void test2() {
// func(); // 错误!Derived私有继承了Base,这些成员对Further不可见
}
};小结
| 继承方式 | 基类public | 基类protected | 基类private | 支持多态 | 使用频率 |
|---|---|---|---|---|---|
| public | public | protected | 不可访问 | 支持 | 最常用 |
| protected | protected | protected | 不可访问 | 不支持 | 很少用 |
| private | private | private | 不可访问 | 不支持 | 极少用 |
实际开发中:
- 大多数情况下使用public继承
- 如果不确定用哪种继承方式,就用public继承
- 如果要实现"包含"关系而非"是一个"关系,考虑使用组合而不是继承
6.3 派生类的构造与析构
6.3.1 构造函数的调用顺序
在继承关系中,对象的构造是从基类到派生类的,就像盖房子一样,先打地基,再建墙壁。
构造顺序:基类→派生类
cpp
class Base {
public:
Base() {
cout << "基类构造函数" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "派生类构造函数" << endl;
}
};
int main() {
Derived d; // 输出:基类构造函数
// 派生类构造函数
return 0;
}多级继承时,构造顺序是从最基础的类开始,逐级向下:
cpp
class A { public: A() { cout << "A构造" << endl; } };
class B : public A { public: B() { cout << "B构造" << endl; } };
class C : public B { public: C() { cout << "C构造" << endl; } };
// 创建C类对象时的构造顺序:A → B → C6.3.2 初始化列表中调用基类构造函数
重要规则:派生类必须在初始化列表中调用基类构造函数,特别是当基类没有默认构造函数时。
cpp
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n), age(a) {
cout << "创建Person:" << name << endl;
}
};
class Student : public Person {
private:
int score;
public:
// 正确:在初始化列表中调用基类构造函数
Student(string n, int a, int s) : Person(n, a), score(s) {
cout << "创建Student,成绩:" << score << endl;
}
// 错误:如果不在初始化列表中调用基类构造函数
// Student(string n, int a, int s) { // 编译错误!
// score = s;
// }
};6.3.3 析构函数的调用顺序
析构函数的调用顺序与构造函数相反:从派生类到基类。就像拆房子,先拆屋顶,再拆墙壁,最后拆地基。
析构顺序:派生类→基类
cpp
class Base {
public:
~Base() {
cout << "基类析构函数" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "派生类析构函数" << endl;
}
};
int main() {
Derived d; // 创建对象
// 程序结束,d销毁时输出:
// 派生类析构函数
// 基类析构函数
return 0;
}多级继承的析构顺序:
cpp
class A { public: ~A() { cout << "A析构" << endl; } };
class B : public A { public: ~B() { cout << "B析构" << endl; } };
class C : public B { public: ~C() { cout << "C析构" << endl; } };
// 销毁C类对象时的析构顺序:C → B → A小结
- 构造顺序:基类 → 派生类(从最基础的类开始)
- 析构顺序:派生类 → 基类(与构造顺序相反)
- 初始化列表:派生类构造函数必须在初始化列表中调用基类构造函数
- 默认调用:如果基类有默认构造函数,且派生类不显式调用其他构造函数,编译器会自动调用基类的默认构造函数
理解构造和析构的顺序对于正确管理资源和避免内存泄漏非常重要,特别是在复杂的继承关系中。
6.4 成员访问控制
6.4.1 三种访问级别
C++中有三种成员访问级别,它们控制谁可以访问类的成员:
- private:只有类内部能访问(最私密)
- protected:类内部和派生类能访问(家族内部)
- public:任何地方都能访问(公开信息)
cpp
class Student {
private:
string studentId; // 只有Student类内部能访问
protected:
string name; // Student类和派生类能访问
int age;
public:
string major; // 任何地方都能访问
Student(string id, string n, int a, string m)
: studentId(id), name(n), age(a), major(m) { }
void study() { // 公有方法:外部可调用
cout << name << " 正在学习" << endl;
}
// 获取私有成员的公有方法
string getId() { return studentId; }
};6.4.2 继承中的访问控制规则
在派生类内部,能访问基类的成员取决于这些成员的访问级别:
| 基类成员 | 在派生类内部 | 在外部 |
|---|---|---|
| private | 不能访问 | 不能访问 |
| protected | 能访问 | 不能访问 |
| public | 能访问 | 能访问 |
cpp
class GraduateStudent : public Student {
private:
string advisor;
public:
GraduateStudent(string id, string n, int a, string m, string adv)
: Student(id, n, a, m), advisor(adv) { }
void doResearch() {
// 可以访问基类的protected成员
cout << name << "正在研究" << endl;
cout << "年龄:" << age << endl;
// 可以访问基类的public成员
cout << "专业:" << major << endl;
// 不能访问基类的private成员
// cout << studentId; // 错误!
// 但可以通过基类的public方法访问
cout << "学号:" << getId() << endl;
}
};在外部使用:
cpp
int main() {
GraduateStudent gs("G001", "张三", 24, "计算机", "王教授");
// 外部只能访问public成员
cout << gs.major << endl; // 正确
gs.study(); // 正确
// 外部不能访问protected和private成员
// cout << gs.name; // 错误!
// cout << gs.advisor; // 错误!
}小结
访问级别的作用:
- private:数据隐藏,保护类内部实现
- protected:允许派生类访问,支持继承
- public:对外提供接口
访问控制的原则:
- 数据成员通常设为private
- 派生类需要的成员设为protected
- 对外接口设为public
合理使用访问控制可以提高代码的安全性、可维护性,并支持良好的继承设计。
7. 多态性
7.1 多态的概念和类型
7.1.1 什么是多态性
多态是面向对象编程的核心特性之一,指的是"同一个操作作用于不同的对象,可以有不同的解释和执行方式"。
简单来说,多态就是"一个接口,多种实现"。
现实生活中的例子:
- "吃饭"这个动作,不同动物有不同方式(狗用嘴直接吃,猴子用手拿着吃)
- "移动"这个动作,鸟会飞,鱼会游,人会走
7.1.2 多态的作用
多态有以下几个重要好处:
- 简化代码:同一个函数可以处理不同类型的对象
- 提高扩展性:添加新类型时不需要修改现有代码
- 增强灵活性:程序运行时能动态决定调用哪个版本的函数
7.1.3 多态的两种类型
1. 编译时多态(静态多态)
在编译阶段就能确定调用哪个函数,主要通过:
- 函数重载
- 运算符重载
函数重载示例:
cpp
class Calculator {
public:
// 整数加法
int add(int a, int b) {
return a + b;
}
// 小数加法
double add(double a, double b) {
return a + b;
}
};
int main() {
Calculator calc;
cout << calc.add(5, 3) << endl; // 调用int版本
cout << calc.add(2.5, 3.5) << endl; // 调用double版本
return 0;
}2. 运行时多态(动态多态)
在程序运行时才能确定调用哪个函数,通过虚函数实现:
cpp
// 基类
class Animal {
public:
virtual void speak() {
cout << "动物发出声音" << endl;
}
};
// 派生类
class Dog : public Animal {
public:
void speak() override {
cout << "汪汪汪" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "喵喵喵" << endl;
}
};
// 多态函数
void makeSound(Animal* animal) {
animal->speak(); // 调用哪个版本取决于animal指向的实际对象类型
}
int main() {
Dog dog;
Cat cat;
makeSound(&dog); // 输出:汪汪汪
makeSound(&cat); // 输出:喵喵喵
return 0;
}7.1.4 多态的核心要素
实现运行时多态需要三个条件:
- 继承关系:派生类继承自基类
- 虚函数:基类中声明虚函数,派生类重写该函数
- 基类指针或引用:通过基类指针或引用调用虚函数
cpp
// 基类指针示例
Animal* ptr1 = new Dog();
Animal* ptr2 = new Cat();
ptr1->speak(); // 输出:汪汪汪
ptr2->speak(); // 输出:喵喵喵
// 基类引用示例
Dog dog;
Cat cat;
Animal& ref1 = dog;
Animal& ref2 = cat;
ref1.speak(); // 输出:汪汪汪
ref2.speak(); // 输出:喵喵喵小结
- 多态是面向对象编程的核心特性,实现"一个接口,多种实现"
- 编译时多态通过函数重载和运算符重载实现,在编译阶段确定
- 运行时多态通过虚函数实现,在程序运行时动态确定
- 实现运行时多态需要:继承关系、虚函数、基类指针或引用
- 多态能简化代码、提高扩展性、增强程序灵活性
掌握多态是理解和应用面向对象编程的关键一步,它能帮助我们设计出更加灵活、可扩展的程序。
7.2 虚函数
虚函数就是在基类中用virtual关键字声明的函数,它允许派生类“重写”(覆盖)这个函数,并且通过基类指针或引用调用时,会自动执行派生类的版本
7.2.1 虚函数的基本概念
首先,创建一个简单的形状类及其派生类:
cpp
#include <iostream>
using namespace std;
// 基类:形状
class Shape {
public:
// 虚函数
virtual void draw() {
cout << "绘制一个形状" << endl;
}
};
// 派生类:圆形
class Circle : public Shape {
public:
// 重写基类的虚函数
void draw() {
cout << "绘制一个圆形" << endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() {
cout << "绘制一个矩形" << endl;
}
};
// 测试函数
void drawShape(Shape* shape) {
shape->draw(); // 调用虚函数
}
int main() {
Circle circle;
Rectangle rect;
drawShape(&circle); // 输出:绘制一个圆形
drawShape(&rect); // 输出:绘制一个矩形
return 0;
}这个例子展示了虚函数的核心特性:通过基类指针调用时,执行的是实际对象类型的函数版本。
7.2.2 添加更多虚函数
扩展形状类,添加更多功能:
cpp
class Shape {
public:
virtual void draw() {
cout << "绘制一个形状" << endl;
}
virtual double area() {
cout << "计算形状面积" << endl;
return 0.0;
}
virtual void move(int x, int y) {
cout << "将形状移动到(" << x << "," << y << ")" << endl;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override {
cout << "绘制一个半径为" << radius << "的圆形" << endl;
}
double area() override {
return 3.14 * radius * radius;
}
};用统一的接口处理不同的形状:
cpp
int main() {
Circle circle(5);
Rectangle rect(4, 6);
Shape* shapes[] = {&circle, &rect};
for (int i = 0; i < 2; i++) {
shapes[i]->draw();
shapes[i]->area();
}
return 0;
}7.2.3 纯虚函数和抽象类
将Shape类改为抽象类,只定义接口,不提供实现:
cpp
// 抽象基类
class Shape {
public:
// 纯虚函数
virtual void draw() = 0;
virtual double area() = 0;
virtual void move(int x, int y) = 0;
};
// 现在Circle和Rectangle必须实现所有纯虚函数这样Shape类成为一个接口,定义了所有形状必须实现的功能,但自身不能被实例化。
7.2.4 虚析构函数
为形状类添加虚析构函数以正确释放资源:
cpp
class Shape {
protected:
char* name; // 动态分配的资源
public:
Shape(const char* n) {
name = new char[strlen(n) + 1];
strcpy(name, n);
}
// 虚析构函数
virtual ~Shape() {
delete[] name;
}
virtual void draw() = 0;
virtual double area() = 0;
};
class Circle : public Shape {
private:
double radius;
double* data; // 另一个需要管理的资源
public:
Circle(const char* n, double r) : Shape(n), radius(r) {
data = new double[10];
}
~Circle() {
delete[] data;
}
// 其他方法实现...
};测试资源释放:
cpp
int main() {
Shape* shape = new Circle("圆形", 10);
shape->draw();
delete shape; // 正确调用Circle的析构函数,然后是Shape的析构函数
return 0;
}小结
虚函数的几个重要概念:
- 基本虚函数:允许派生类提供自己的实现
- 纯虚函数:定义接口,强制派生类实现
- 抽象类:包含纯虚函数,不能实例化
- 虚析构函数:确保正确释放资源
- 多态使用:通过基类指针调用派生类的函数
虚函数是C++实现多态的关键机制,是面向对象编程中非常重要的概念。
8. 模板
什么是模板?
模板就像一个"万能模具",你只需要制作一次,就能生产出不同材料的产品。
想象一下做月饼:同一个模具,可以做豆沙馅的、蛋黄馅的、五仁馅的。模板也是这样,写一份代码,就能处理int、double、string等各种不同类型的数据。
8.1 函数模板的基本用法
cpp
#include <iostream>
using namespace std;
// 传统方法:为每种类型写一个函数
int maxInt(int a, int b) {
return a > b ? a : b;
}
double maxDouble(double a, double b) {
return a > b ? a : b;
}
// 模板方法:写一次,处理所有类型
template<typename T>
T maxTemplate(T a, T b) {
return a > b ? a : b;
}
int main() {
// 传统方法
cout << "最大整数:" << maxInt(10, 20) << endl;
cout << "最大浮点数:" << maxDouble(3.14, 2.71) << endl;
// 模板方法
cout << "模板整数:" << maxTemplate(10, 20) << endl; // 自动推导为int
cout << "模板浮点数:" << maxTemplate(3.14, 2.71) << endl; // 自动推导为double
cout << "模板字符串:" << maxTemplate<string>("hello", "world") << endl; // 手动指定类型
return 0;
}模板的优点:
- 一份代码,多种类型:不用为每种类型重复写代码
- 自动类型推导:编译器会自动判断你用的是什么类型
- 类型安全:编译时就能发现类型错误
8.2 类模板:让整个类都通用
cpp
template<typename T>
class Stack {
private:
T* data; // 存储数据的数组
int capacity; // 容量
int top; // 栈顶位置
public:
Stack(int size = 10) {
capacity = size;
data = new T[capacity];
top = -1;
}
~Stack() {
delete[] data;
}
void push(T value) {
if (top >= capacity - 1) {
cout << "栈满了!" << endl;
return;
}
data[++top] = value;
}
T pop() {
if (top < 0) {
cout << "栈空了!" << endl;
return T(); // 返回默认值
}
return data[top--];
}
void showAll() {
cout << "栈内容:";
for (int i = 0; i <= top; i++) {
cout << data[i] << " ";
}
cout << endl;
}
};
int main() {
// 整数栈
Stack<int> intStack(5);
intStack.push(10);
intStack.push(20);
intStack.push(30);
intStack.showAll(); // 输出:栈内容:10 20 30
// 字符串栈
Stack<string> stringStack(3);
stringStack.push("苹果");
stringStack.push("香蕉");
stringStack.showAll(); // 输出:栈内容:苹果 香蕉
return 0;
}8.3 常见问题
Q: 模板代码写在哪里? A: 通常写在头文件里,因为编译器需要看到完整的模板定义。
Q: 模板会让程序变大吗? A: 会的。每种类型都会生成一份代码,但现代编译器会优化重复部分。
Q: 什么时候用模板? A: 当你需要对不同类型执行相同的操作时,例如数据结构、排序算法等。
Q: 模板的优缺点? A:
- 优点:代码复用、类型安全
- 缺点:编译时间长、错误信息复杂、可能增加代码体积
小结
- 函数模板:让函数处理多种类型
- 类模板:让整个类支持多种类型
- 自动推导:编译器自动判断类型
- 一次编写,到处使用:减少代码重复
- 编译时处理:类型安全,性能好
模板是C++的强大特性,能让你写出通用且高效的代码。掌握模板,就像拥有了一把万能钥匙,能够解锁各种类型的数据处理问题。
9. 标准模板库(STL)基础
9.1STL概述
9.1.1什么是STL
标准模板库(Standard Template Library,简称STL)是C++标准库的重要组成部分,它提供了许多现成的数据结构和算法。
想象一下STL就像一个工具箱,里面有各种各样的工具,我们不需要自己制造工具,直接拿来用就可以了。比如需要一个可以自动扩容的数组,直接用vector就行了;需要快速查找,用map;需要排序,调用sort函数。
9.1.2 STL的重要性
- 节省时间:不用重复造轮子
- 保证质量:经过严格测试和优化
- 提高可读性:大家都熟悉的标准组件
- 跨平台兼容:不同编译器下表现一致
9.1.3 STL的主要组件
STL主要由以下几部分组成:
容器(Containers)
用来存放数据的"盒子":
序列式容器:
vector:动态数组list:双向链表deque:双端队列
关联式容器:
set:不重复元素集合map:键值对映射
cpp
vector<int> nums = {1, 2, 3};
map<string, int> scores = {{"张三", 90}, {"李四", 85}};迭代器(Iterators)
用来访问和遍历容器中的元素:
cpp
vector<int> vec = {10, 20, 30};
for(auto it = vec.begin(); it != vec.end(); ++it) {
cout << *it << " "; // 输出:10 20 30
}算法(Algorithms)
处理数据的函数:
cpp
// 排序
vector<int> v = {5, 2, 8, 1};
sort(v.begin(), v.end()); // 结果:{1, 2, 5, 8}
// 查找
auto it = find(v.begin(), v.end(), 5);
if(it != v.end()) cout << "找到了:" << *it;9.1.4 STL的设计理念
泛型编程
用模板实现"一次编写,多种类型":
cpp
// 同一个sort函数可以排序不同类型
vector<int> nums = {3, 1, 4};
vector<string> names = {"张三", "李四", "王五"};
sort(nums.begin(), nums.end());
sort(names.begin(), names.end());组件化设计
各组件各司其职,可以自由组合:
- 容器负责存储
- 迭代器负责访问
- 算法负责处理
统一接口
所有容器都提供类似的接口:
begin():返回指向第一个元素的迭代器end():返回指向末尾的迭代器size():返回元素个数empty():检查是否为空
小结
STL是C++中非常实用的工具库,它提供了丰富的容器和算法,让我们能够写出更简洁、高效的代码。作为C++程序员,熟练掌握STL是必不可少的基本功。学会使用STL,就相当于拥有了一个强大的编程工具箱,可以解决大部分常见的编程问题。
9.2. 容器
9.2.1 容器基本概念
容器就是用来装数据的"盒子",STL提供了很多种不同的容器,每种都有自己的特点。就像我们生活中用不同的容器装不同的东西一样——用书包装书,用水杯装水,用抽屉装文具。
STL容器主要分为三类:
- 序列容器:按顺序存放数据,像排队一样
- 关联容器:按某种规律存放数据,能快速查找
- 容器适配器:对其他容器进行包装,提供特殊功能
9.2.2 序列容器
vector - 动态数组
vector就是一个可以自动扩容的数组,用起来特别方便。
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> scores;
// 添加元素
scores.push_back(85);
scores.push_back(92);
scores.push_back(78);
// 访问元素
cout << "第一个成绩:" << scores[0] << endl;
cout << "最后一个成绩:" << scores.back() << endl;
// 遍历所有元素
cout << "所有成绩:";
for(int score : scores) {
cout << score << " ";
}
cout << endl;
// 插入元素
scores.insert(scores.begin() + 1, 90); // 在第2个位置插入90
// 删除元素
scores.pop_back(); // 删除最后一个元素
cout << "容器大小:" << scores.size() << endl;
return 0;
}vector的特点:
- 支持随机访问,可以用[]直接访问任意位置
- 在末尾添加删除元素很快
- 在中间插入删除元素比较慢
- 内存连续,访问速度快
适用场景:数组替代品,存储需要随机访问的数据
list - 双向链表
list就像一串珠子,每个珠子都知道前面和后面的珠子在哪里。
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<string> todoList;
// 添加任务
todoList.push_back("写作业");
todoList.push_back("复习");
todoList.push_front("吃饭"); // 在开头添加
// 遍历任务列表
cout << "待办事项:" << endl;
for(const string& task : todoList) {
cout << "- " << task << endl;
}
// 在中间插入
auto it = todoList.begin();
++it; // 移动到第二个位置
todoList.insert(it, "洗澡");
// 删除第一个任务
todoList.pop_front();
return 0;
}list的特点:
- 在任意位置插入删除都很快
- 不支持随机访问,不能用[]
- 只能通过迭代器逐个访问元素
适用场景:需要频繁插入删除的场合
9.2.3 关联容器
set - 集合
set就像一个自动排序且不允许重复的集合。
cpp
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> uniqueNumbers;
// 插入元素(自动排序,自动去重)
uniqueNumbers.insert(5);
uniqueNumbers.insert(2);
uniqueNumbers.insert(8);
uniqueNumbers.insert(2); // 重复元素,不会插入
// 输出:2 5 8(自动排序)
cout << "集合中的元素:";
for(int num : uniqueNumbers) {
cout << num << " ";
}
cout << endl;
// 查找元素
if(uniqueNumbers.find(5) != uniqueNumbers.end()) {
cout << "找到了5" << endl;
}
// 删除元素
uniqueNumbers.erase(2);
return 0;
}set的特点:
- 自动排序
- 不允许重复元素
- 查找很快(O(log n))
适用场景:需要去重和排序的数据
map - 映射(字典)
map就像一个字典,可以通过"键"快速找到对应的"值"。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> studentScores;
// 添加键值对
studentScores["张三"] = 85;
studentScores["李四"] = 92;
studentScores["王五"] = 78;
// 访问元素
cout << "张三的成绩:" << studentScores["张三"] << endl;
// 遍历所有元素
cout << "所有学生成绩:" << endl;
for(const auto& pair : studentScores) {
cout << pair.first << ": " << pair.second << endl;
}
// 查找元素
if(studentScores.find("李四") != studentScores.end()) {
cout << "找到了李四的成绩" << endl;
}
// 修改值
studentScores["张三"] = 90;
return 0;
}map的特点:
- 键值对存储
- 按键自动排序
- 通过键快速查找值
- 键不能重复
适用场景:需要建立映射关系的数据,如学号对成绩、单词对翻译等
9.2.4 容器适配器
stack - 栈
栈是"后进先出"的容器,就像一摞盘子,只能从顶部取放。
cpp
#include <iostream>
#include <stack>
using namespace std;
int main() {
stack<int> plateStack;
// 入栈
plateStack.push(1);
plateStack.push(2);
plateStack.push(3);
// 出栈(后进先出)
while(!plateStack.empty()) {
cout << plateStack.top() << " "; // 输出:3 2 1
plateStack.pop();
}
cout << endl;
return 0;
}适用场景:函数调用、括号匹配、表达式计算等
queue - 队列
队列是"先进先出"的容器,就像排队买票,先来的先服务。
cpp
#include <iostream>
#include <queue>
using namespace std;
int main() {
queue<string> customerQueue;
// 入队
customerQueue.push("张三");
customerQueue.push("李四");
customerQueue.push("王五");
// 出队(先进先出)
while(!customerQueue.empty()) {
cout << customerQueue.front() << " "; // 输出:张三 李四 王五
customerQueue.pop();
}
cout << endl;
return 0;
}适用场景:任务队列、广度优先搜索等
9.2.5 容器选择指南
根据不同需求选择合适的容器:
| 需求 | 推荐容器 | 原因 |
|---|---|---|
| 当数组用 | vector | 支持随机访问,效率高 |
| 频繁插入删除 | list | 插入删除快 |
| 去重排序 | set | 自动去重和排序 |
| 键值对查找 | map | 快速按键查找 |
| 后进先出 | stack | 栈操作 |
| 先进先出 | queue | 队列操作 |
小结
STL容器就像一套工具箱,不同的工具有不同的用途:
- vector:最常用的容器,可以当做动态数组使用
- list:适合频繁插入删除的场景
- set:需要元素唯一且有序时使用
- map:需要键值对映射时使用
- stack和queue:特殊的数据结构,分别实现后进先出和先进先出的访问方式
选择容器的原则是"合适的工具做合适的事"。根据你的具体需求来选择最合适的容器,这样程序才能既简洁又高效。掌握这些常用容器,能够解决大部分编程问题。
9.3. 迭代器
9.3.1 迭代器的概念和类型
什么是迭代器
迭代器就像是一个"聪明的指针",它能帮我们统一地访问不同容器中的元素。想象一下,不管是vector、list还是map,我们都可以用类似的方式来遍历它们,这就是迭代器的作用。
cpp
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
list<int> lst = {1, 2, 3, 4, 5};
// 用同样的方式遍历vector和list
cout << "vector: ";
for(auto it = vec.begin(); it != vec.end(); ++it) {
cout << *it << " ";
}
cout << endl;
cout << "list: ";
for(auto it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " ";
}
cout << endl;
return 0;
}迭代器的类型
STL根据功能把迭代器分为几种类型,从简单到复杂:
- 前向迭代器:只能向前移动,可以读写
- 双向迭代器:可以前后移动,例如list的迭代器
- 随机访问迭代器:可以跳跃访问,例如vector的迭代器
cpp
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
list<int> lst = {1, 2, 3, 4, 5};
// 随机访问迭代器可以跳跃
auto vec_it = vec.begin();
vec_it += 3; // 直接跳到第4个元素
cout << "vector第4个元素: " << *vec_it << endl;
// 双向迭代器只能一步步移动
auto lst_it = lst.begin();
++lst_it; ++lst_it; ++lst_it; // 一步步移动到第4个元素
cout << "list第4个元素: " << *lst_it << endl;
return 0;
}9.3.2 迭代器的基本使用
获取迭代器
每个容器都有begin()和end()方法:
begin():指向第一个元素end():指向最后一个元素的下一个位置(超尾位置)
cpp
vector<int> numbers = {10, 20, 30, 40, 50};
// 获取迭代器
auto begin_it = numbers.begin(); // 指向第一个元素
auto end_it = numbers.end(); // 指向最后一个元素之后
cout << "第一个元素: " << *begin_it << endl;
// 注意:不能解引用end()迭代器
// cout << *end_it; // 错误!迭代器的基本操作
cpp
vector<string> words = {"hello", "world", "STL"};
auto it = words.begin();
// 解引用:获取元素值
cout << "当前元素: " << *it << endl; // hello
// 移动迭代器
++it; // 移动到下一个元素
cout << "下一个元素: " << *it << endl; // world
// 访问成员(如果元素是对象)
cout << "字符串长度: " << it->length() << endl; // 5遍历容器的几种方式
传统for循环
cpp
vector<int> numbers = {1, 2, 3, 4, 5};
for(auto it = numbers.begin(); it != numbers.end(); ++it) {
cout << *it << " "; // 1 2 3 4 5
}范围for循环(推荐)
cpp
vector<int> numbers = {1, 2, 3, 4, 5};
for(const auto& num : numbers) {
cout << num << " "; // 1 2 3 4 5
}反向遍历
cpp
vector<int> numbers = {1, 2, 3, 4, 5};
// 从后往前遍历
for(auto it = numbers.rbegin(); it != numbers.rend(); ++it) {
cout << *it << " "; // 5 4 3 2 1
}9.3.3 迭代器与算法结合
迭代器的真正威力在于与STL算法的结合使用:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> numbers = {5, 2, 8, 1, 9};
// 排序
sort(numbers.begin(), numbers.end());
cout << "排序后: ";
for(int num : numbers) {
cout << num << " "; // 1 2 5 8 9
}
cout << endl;
// 查找元素
auto it = find(numbers.begin(), numbers.end(), 5);
if(it != numbers.end()) {
cout << "找到了5" << endl;
}
// 查找条件
auto it2 = find_if(numbers.begin(), numbers.end(),
[](int x) { return x > 6; });
if(it2 != numbers.end()) {
cout << "第一个大于6的数: " << *it2 << endl; // 8
}
return 0;
}9.3.4 迭代器失效问题
当容器结构发生变化时,迭代器可能失效。这是使用STL时需要特别注意的问题:
cpp
vector<int> numbers = {1, 2, 3, 4, 5};
// 错误的做法:在循环中删除元素
/*
for(auto it = numbers.begin(); it != numbers.end(); ++it) {
if(*it % 2 == 0) {
numbers.erase(it); // 错误!迭代器失效
}
}
*/
// 正确的做法:使用erase的返回值
for(auto it = numbers.begin(); it != numbers.end(); ) {
if(*it % 2 == 0) {
it = numbers.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
// 结果:{1, 3, 5}9.3.5 常用的迭代器适配器
插入迭代器:可以在容器中插入元素
cpp
vector<int> source = {1, 2, 3};
vector<int> dest;
// 使用back_inserter在尾部插入
copy(source.begin(), source.end(), back_inserter(dest));
// dest现在是{1, 2, 3}小结
迭代器是连接容器和算法的桥梁,是STL的核心概念之一:
- 统一接口:不管什么容器,都可以用类似的方式访问元素
- 基本操作:
begin()、end()、*it(解引用)、++it(前移) - 遍历方式:传统for循环、范围for循环、反向遍历
- 与算法配合:STL算法都是通过迭代器来访问容器元素
- 注意事项:修改容器时要小心迭代器失效
掌握迭代器的使用是学好STL的关键。通过迭代器,我们能够以统一的方式操作各种容器,大大提高编程效率和代码可读性。
9.4 函数对象
9.4.1 函数对象是什么
函数对象(Function Object)就是一个能够像函数一样被调用的对象。实现方法是在类中重载括号运算符operator()。
cpp
#include <iostream>
using namespace std;
// 函数对象类
class Multiply {
public:
int operator()(int a, int b) {
return a * b;
}
};
int main() {
Multiply mult; // 创建函数对象
// 像函数一样使用
int result = mult(3, 4);
cout << "3 * 4 = " << result << endl; // 输出:12
return 0;
}9.4.2 函数对象的优势
与普通函数相比,函数对象有两个主要优势:
1. 可以保存状态
cpp
// 计数器函数对象
class Counter {
private:
int count;
public:
Counter() : count(0) {}
int operator()() {
return ++count; // 记录调用次数
}
};
int main() {
Counter counter;
cout << counter() << endl; // 输出: 1
cout << counter() << endl; // 输出: 2
cout << counter() << endl; // 输出: 3
return 0;
}2. 可以自定义参数
cpp
// 判断数字是否在某个范围内
class InRange {
private:
int min, max;
public:
InRange(int minVal, int maxVal) : min(minVal), max(maxVal) {}
bool operator()(int num) {
return num >= min && num <= max;
}
};
// 使用
vector<int> scores = {45, 67, 78, 89, 92};
InRange passRange(60, 100); // 及格分数范围:60-100
int passCount = count_if(scores.begin(), scores.end(), passRange);9.4.3 常见应用
自定义排序规则
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 按绝对值大小排序
class CompareByAbs {
public:
bool operator()(int a, int b) {
return abs(a) < abs(b);
}
};
int main() {
vector<int> numbers = {-5, 2, -8, 1, 9};
sort(numbers.begin(), numbers.end(), CompareByAbs());
cout << "按绝对值排序: ";
for(int n : numbers)
cout << n << " "; // 输出: 1 2 -5 9 -8
cout << endl;
return 0;
}9.4.4 STL标准函数对象
STL提供了一些常用的函数对象,在<functional>头文件中:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
int main() {
vector<int> numbers = {5, 2, 8, 1, 9};
// 使用greater进行降序排序
sort(numbers.begin(), numbers.end(), greater<int>());
cout << "降序排序: ";
for(int n : numbers)
cout << n << " "; // 输出: 9 8 5 2 1
cout << endl;
return 0;
}常用标准函数对象:
greater<T>: 大于less<T>: 小于(STL默认使用)plus<T>: 加法minus<T>: 减法
9.4.5 函数对象与Lambda表达式
在现代C++中,Lambda表达式往往比函数对象更简洁:
cpp
vector<int> numbers = {1, 2, 3, 4, 5};
// 使用函数对象
class IsEven {
public:
bool operator()(int x) {
return x % 2 == 0;
}
};
int count1 = count_if(numbers.begin(), numbers.end(), IsEven());
// 使用Lambda表达式(更简洁)
int count2 = count_if(numbers.begin(), numbers.end(),
[](int x) { return x % 2 == 0; });9.4.6 实际应用:学生成绩排序
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
// 学生结构体
struct Student {
string name;
int score;
};
// 按成绩排序
class SortByScore {
public:
bool operator()(const Student& a, const Student& b) {
return a.score > b.score; // 从高到低排序
}
};
int main() {
vector<Student> students = {
{"张三", 85}, {"李四", 75}, {"王五", 92}
};
sort(students.begin(), students.end(), SortByScore());
cout << "成绩排名:" << endl;
for(const auto& s : students) {
cout << s.name << ": " << s.score << endl;
}
return 0;
}小结
函数对象是STL中重要的概念,主要特点有:
- 实现方式:重载类的
operator()运算符 - 主要优势:可以保存状态,可以携带额外参数
- 常见用途:与算法配合使用,如自定义排序规则
- 标准库:STL提供了常用的函数对象如
greater、less等 - 现代用法:现在常用Lambda表达式替代函数对象
掌握函数对象的使用,可以让我们更灵活地使用STL算法,写出更通用、更简洁的代码。
9.5 string类
9.5.1 string类基本使用
string类是C++中处理字符串的标准工具,比C语言的字符数组好用太多了!它自动管理内存,提供了丰富的操作函数。
基本构造方式
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
// 各种创建字符串的方法
string s1; // 空字符串
string s2("Hello World"); // 从字符串字面量创建
string s3 = "C++ Programming"; // 赋值方式创建
string s4(s2); // 拷贝构造
string s5(10, 'A'); // 10个'A'
cout << "s1: '" << s1 << "'" << endl;
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
cout << "s5: " << s5 << endl;
return 0;
}基本属性
cpp
string name = "张三";
cout << "字符串: " << name << endl;
cout << "长度: " << name.length() << endl; // 或者用name.size()
cout << "是否为空: " << name.empty() << endl;
// 清空字符串
name.clear();
cout << "清空后是否为空: " << name.empty() << endl;9.5.2 字符串访问和遍历
cpp
string course = "C++程序设计";
// 通过下标访问
cout << "第一个字符: " << course[0] << endl; // C
// 通过at访问(会检查边界)
cout << "第二个字符: " << course.at(1) << endl; // +
// 获取第一个和最后一个字符
cout << "第一个字符: " << course.front() << endl; // C
cout << "最后一个字符: " << course.back() << endl; // 计
// 遍历字符串
for(char c : course) {
cout << c << " ";
}9.5.3 字符串拼接和修改
cpp
// 字符串拼接
string firstName = "张";
string lastName = "三";
string fullName = firstName + lastName; // 张三
// 使用+=操作符
string greeting = "你好, ";
greeting += fullName; // 你好, 张三
// 插入字符串
string text = "我学习编程";
text.insert(2, "喜欢"); // 我喜欢学习编程
// 删除部分字符
string longText = "今天天气真的很好";
longText.erase(2, 2); // 今天真的很好 (删除了"天气")9.5.4 字符串查找
cpp
string email = "student@university.edu.cn";
// 查找子字符串
size_t atPos = email.find("@");
if(atPos != string::npos) { // 找到了@
string username = email.substr(0, atPos); // student
string domain = email.substr(atPos + 1); // university.edu.cn
}
// 从后往前查找
string filename = "homework.cpp";
size_t dotPos = filename.rfind("."); // 查找最后一个点
if(dotPos != string::npos) {
string extension = filename.substr(dotPos + 1); // cpp
}
// 检查是否包含某个子串
string sentence = "我正在学习C++编程";
if(sentence.find("C++") != string::npos) {
cout << "这句话提到了C++" << endl;
}9.5.5 字符串替换
cpp
string text = "Java是一门很好的编程语言";
// 替换子字符串
size_t pos = text.find("Java");
if(pos != string::npos) {
text.replace(pos, 4, "C++"); // C++是一门很好的编程语言
}
// 替换所有出现的子串
string code = "int a = 0; int b = 1;";
string searchStr = "int";
string replaceStr = "double";
pos = 0;
while((pos = code.find(searchStr, pos)) != string::npos) {
code.replace(pos, searchStr.length(), replaceStr);
pos += replaceStr.length();
}
// 结果: double a = 0; double b = 1;9.5.6 字符串与数字转换
cpp
// 数字转字符串
int score = 95;
string scoreStr = to_string(score); // "95"
// 字符串转数字
string ageStr = "20";
string heightStr = "175.5";
int age = stoi(ageStr); // string to int: 20
float height = stof(heightStr); // string to float: 175.5
// 处理转换错误
string invalidStr = "abc";
try {
int num = stoi(invalidStr);
} catch(const exception& e) {
cout << "转换失败!" << endl;
}9.5.7 字符串比较
cpp
string str1 = "apple";
string str2 = "banana";
// 直接比较
if(str1 == str2) {
cout << "两个字符串相等" << endl;
}
// 使用compare方法
int result = str1.compare(str2);
if(result < 0) {
cout << str1 << " 在字典序中小于 " << str2 << endl;
} else if(result > 0) {
cout << str1 << " 在字典序中大于 " << str2 << endl;
} else {
cout << str1 << " 等于 " << str2 << endl;
}9.5.8 实用字符串函数
字符串分割
cpp
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
using namespace std;
// 简单的字符串分割函数
vector<string> split(const string& str, char delimiter) {
vector<string> result;
stringstream ss(str);
string item;
while(getline(ss, item, delimiter)) {
result.push_back(item);
}
return result;
}
int main() {
string data = "张三,20,计算机科学";
vector<string> parts = split(data, ',');
// parts[0]="张三", parts[1]="20", parts[2]="计算机科学"
return 0;
}去除空格
cpp
// 去除字符串前后空格
string trim(const string& str) {
size_t start = str.find_first_not_of(" \t");
if(start == string::npos) return "";
size_t end = str.find_last_not_of(" \t");
return str.substr(start, end - start + 1);
}
string messy = " hello world ";
string clean = trim(messy); // "hello world"小结
string类的常用功能:
- 基本操作:创建、获取长度、判空
- 访问字符:[]、at()、front()、back()
- 修改内容:+、+=、insert()、erase()、replace()
- 查找子串:find()、rfind()、substr()
- 数字转换:to_string()、stoi()、stof()等
- 字符串比较:==、compare()
string类大大简化了字符串处理工作,是C++编程中最常用的工具之一。
9.6 文件操作进阶
9.6.1 文件流类概述
什么是文件流类
文件流类是C++标准库提供的文件操作工具,它把文件操作包装成了"流"的概念。就像水流一样,数据可以从程序"流"到文件,或者从文件"流"到程序。
相比C语言的文件操作,C++的文件流类有几个优势:
- 自动管理资源:文件会自动打开和关闭,不用担心忘记关闭文件
- 类型安全:编译时就能检查类型错误
- 使用简单:用熟悉的>>和<<操作符就能读写文件
三种主要的文件流类:
ifstream:input file stream,专门用来读文件ofstream:output file stream,专门用来写文件fstream:file stream,既能读又能写
cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
// ifstream - 读文件
ifstream readFile("data.txt");
if (readFile.is_open()) {
string content;
while (getline(readFile, content)) {
cout << content << endl;
}
readFile.close(); // 可以不写,析构时会自动关闭
}
// ofstream - 写文件
ofstream writeFile("output.txt");
if (writeFile.is_open()) {
writeFile << "Hello World!" << endl;
writeFile << "学生姓名: 张三" << endl;
writeFile << "成绩: 95" << endl;
}
return 0;
}9.6.2 文件打开模式
为什么需要打开模式
打开模式就是告诉程序"我要怎么使用这个文件"。不同的使用场景需要不同的模式:
- 读取配置文件时,我们只需要读,不能修改
- 写日志时,我们希望新内容添加到文件末尾,而不是覆盖原来的内容
- 处理图片文件时,我们需要按二进制方式处理,不能当作文本
常用的打开模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
ios::in | 只读模式 | 读取配置文件、数据文件 |
ios::out | 只写模式(覆盖) | 生成报告、导出数据 |
ios::app | 追加模式 | 写日志、记录历史 |
ios::binary | 二进制模式 | 处理图片、音频等 |
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main() {
// 只读模式 - 安全地读取文件
ifstream configFile("config.txt", ios::in);
// 覆盖模式 - 创建新文件或清空现有文件
ofstream reportFile("report.txt", ios::out);
reportFile << "今日报告" << endl;
// 追加模式 - 在文件末尾添加内容
ofstream logFile("app.log", ios::app);
logFile << "用户登录: 2024-01-01 10:00" << endl;
// 组合模式 - 可以同时指定多个模式
fstream dataFile("data.txt", ios::in | ios::out | ios::app);
return 0;
}实际应用示例
cpp
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
// 简单的日志记录器
class Logger {
private:
string filename;
public:
Logger(const string& file) : filename(file) {}
void writeLog(const string& message) {
ofstream logFile(filename, ios::app); // 追加模式
if (logFile.is_open()) {
logFile << "[LOG] " << message << endl;
}
}
};
// 配置文件读取器
class ConfigReader {
public:
vector<string> readConfig(const string& filename) {
vector<string> settings;
ifstream file(filename, ios::in); // 只读模式
if (file.is_open()) {
string line;
while (getline(file, line)) {
if (!line.empty() && line[0] != '#') { // 跳过空行和注释
settings.push_back(line);
}
}
}
return settings;
}
};
int main() {
// 使用日志记录器
Logger logger("app.log");
logger.writeLog("程序启动");
logger.writeLog("用户登录成功");
// 读取配置文件
ConfigReader reader;
vector<string> config = reader.readConfig("settings.txt");
cout << "配置项:" << endl;
for (const auto& setting : config) {
cout << setting << endl;
}
return 0;
}9.6.3 文件指针操作
什么是文件指针
文件指针就像一个"书签",标记我们当前在文件的哪个位置。通过移动文件指针,我们可以:
- 跳到文件开头重新读取
- 跳到文件末尾添加内容
- 跳到文件中间修改特定内容
- 获取文件大小
主要的指针操作函数
tellg()/tellp():获取当前位置seekg()/seekp():移动到指定位置
cpp
#include <iostream>
#include <fstream>
using namespace std;
int main() {
fstream file("test.txt", ios::in | ios::out);
if (file.is_open()) {
// 写入一些测试数据
file << "第一行内容" << endl;
file << "第二行内容" << endl;
file << "第三行内容" << endl;
// 获取当前位置
streampos pos = file.tellp();
cout << "当前位置: " << pos << endl;
// 移动到文件开头
file.seekg(0, ios::beg);
// 重新读取内容
string line;
cout << "文件内容:" << endl;
while (getline(file, line)) {
cout << line << endl;
}
// 移动到文件末尾并添加内容
file.clear(); // 清除EOF标志
file.seekp(0, ios::end);
file << "新添加的内容" << endl;
}
return 0;
}实用的文件操作函数
cpp
#include <iostream>
#include <fstream>
using namespace std;
// 获取文件大小
long getFileSize(const string& filename) {
ifstream file(filename, ios::ate); // ate模式直接定位到末尾
if (file.is_open()) {
return file.tellg();
}
return -1;
}
// 读取文件的最后一行
string readLastLine(const string& filename) {
ifstream file(filename);
string line, lastLine;
while (getline(file, line)) {
lastLine = line;
}
return lastLine;
}
int main() {
cout << "文件大小: " << getFileSize("test.txt") << " 字节" << endl;
cout << "最后一行: " << readLastLine("test.txt") << endl;
return 0;
}9.6.4 二进制文件操作
什么是二进制文件
二进制文件直接存储数据的原始字节,不进行文本转换。它的特点是:
- 存储效率高:一个整数只占4个字节,而文本形式可能需要更多
- 读写速度快:不需要文本转换
- 数据精确:浮点数不会有精度损失
- 不可直接阅读:需要专门的程序来解析
适用场景
- 存储大量数值数据(传感器数据、图像像素等)
- 保存程序的内部数据结构
- 处理图片、音频、视频文件
二进制文件读写示例
cpp
#include <iostream>
#include <fstream>
using namespace std;
struct Student {
int id;
char name[20];
double score;
};
int main() {
// 写入二进制数据
ofstream outFile("students.dat", ios::binary);
if (outFile.is_open()) {
Student students[] = {
{1001, "张三", 85.5},
{1002, "李四", 92.0},
{1003, "王五", 78.5}
};
int count = 3;
// 先写入学生数量
outFile.write(reinterpret_cast<char*>(&count), sizeof(count));
// 再写入学生数据
for (int i = 0; i < count; i++) {
outFile.write(reinterpret_cast<char*>(&students[i]), sizeof(Student));
}
}
// 读取二进制数据
ifstream inFile("students.dat", ios::binary);
if (inFile.is_open()) {
int count;
// 先读取学生数量
inFile.read(reinterpret_cast<char*>(&count), sizeof(count));
cout << "共有 " << count << " 个学生:" << endl;
// 再读取学生数据
for (int i = 0; i < count; i++) {
Student student;
inFile.read(reinterpret_cast<char*>(&student), sizeof(Student));
cout << "学号: " << student.id
<< ", 姓名: " << student.name
<< ", 成绩: " << student.score << endl;
}
}
return 0;
}小结
文件操作的核心概念
文件流类型选择:
只读文件用
ifstream只写文件用
ofstream需要读写用
fstream打开模式选择:
普通文本文件用默认模式
日志文件用
ios::app追加模式图片音频用
ios::binary二进制模式文件指针的作用:
控制读写位置
实现随机访问
获取文件信息
二进制vs文本:
文本文件:易读但占空间大
二进制文件:高效但需要专门处理
- 选择建议
- 配置文件、日志 → 文本格式
- 大量数值数据 → 二进制格式
- 需要人工查看 → 文本格式
- 性能要求高 → 二进制格式
掌握这些文件操作技术,就能处理程序中的数据持久化需求,让数据能够长期保存和使用。
学生成绩管理系统 - 项目需求书
1. 项目概述
1.1 项目名称
简易学生成绩管理系统
1.2 项目背景
在教学管理过程中,需要对学生的基本信息和课程成绩进行有效管理。目前许多小型教学场景仍采用手工记录或简单表格的方式,存在查询不便、统计困难、数据易丢失等问题。本项目旨在开发一款适合学生或教师使用的轻量级成绩管理工具,实现学生信息和成绩的数字化管理。
1.3 项目目标
开发一个控制台版学生成绩管理系统,支持学生信息的录入、查询、修改、删除等基本操作,实现成绩的自动计算与排序,并提供数据持久化存储功能,满足小型教学场景的成绩管理需求。
1.4 预期用户
- 教师:用于管理学生成绩
- 学生:用于查询个人成绩
- 教学管理人员:用于统计分析成绩数据
2. 功能需求
2.1 登录模块实现
- 添加了
User类存储用户信息(用户名、加密密码、是否管理员) UserManager类处理登录、注册和权限管理- 实现简单的密码加密功能(异或加密)
- 区分管理员和普通用户权限,部分操作(如删除学生、用户管理)仅管理员可执行
- 系统启动时必须先登录,提供 3 次尝试机会
2.2 多文件拆分
- 按类拆分,每个类有独立的头文件 (.h) 和实现文件 (.cpp)
- 公共常量和函数放在
common.h和common.cpp中 - 主程序逻辑在
main.cpp中,负责菜单交互
2.3 权限控制
- 管理员可以进行所有操作,包括用户管理和删除学生
- 普通用户有查询、添加和修改学生信息的权限,但不能删除
- 默认管理员账户:用户名
admin,密码admin(首次登录后建议修改)
3. 系统文件结构设计
plaintext
学生成绩管理系统/
├── main.cpp // 程序入口点,设置控制台标题并启动应用
├── app.h // 应用控制类声明,负责整体流程管理
├── app.cpp // 应用控制类实现,包含登录、菜单处理等核心流程
├── student.h // 学生数据模型类声明(含Person基类、Course结构体)
├── student.cpp // 学生类相关实现(含文件输入输出运算符重载)
├── student_manager.h // 学生管理类声明,封装学生数据的增删改查操作
├── student_manager.cpp // 学生管理类实现,处理具体的数据管理逻辑
├── console_helpers.h // 控制台辅助函数声明(清屏、光标定位、边框绘制等)
└── console_helpers.cpp // 控制台辅助函数实现,提供界面绘制和交互支持文件结构说明
- 入口模块
main.cpp:程序启动点,仅负责初始化环境和启动应用,不包含业务逻辑,符合 "单一职责原则"
- 应用控制模块
app.h/app.cpp:封装整个应用的流程控制,包括登录验证、主菜单调度、各功能界面切换等,是系统的 "中枢神经"
- 数据模型模块
student.h/student.cpp:定义核心数据结构(Person基类、Student派生类、Course结构体),封装学生和课程的数据及相关操作
- 数据管理模块
student_manager.h/student_manager.cpp:负责学生数据的管理,包括添加、查询、修改、删除、排序、文件读写等,隔离数据操作与界面逻辑
- 界面辅助模块
console_helpers.h/console_helpers.cpp:提供控制台交互所需的辅助功能,如gotoxy光标定位、清屏、边框绘制、密码输入等,统一管理界面绘制逻辑
4. 技术要求
4.1 开发环境
- 开发工具:Dev C++
- 编程语言:C++
- 编译标准:C++11 及以上
4.2 技术要点
- 采用面向对象设计思想
- 使用结构体 / 类封装数据
- 应用 STL 容器(vector 等)存储数据
- 实现文件 IO 操作进行数据持久化
- 运用继承、多态等面向对象特性
- 采用模块化设计,功能分离
4.3 交付物
- 可执行程序(.exe 文件)
- 完整源代码(多个文件组织)
- 程序使用说明
- 测试报告
5. 系统架构设计
5.1 系统架构图
系统采用分层架构设计,各层职责清晰:

- 表现层:负责用户交互和信息展示
- 业务逻辑层:实现核心业务功能,包括认证授权、学生管理等
- 数据访问层:处理数据的读写和解析
- 数据存储:以文件形式持久化存储系统数据
5.2 系统时序图(登录与添加学生流程)
时序图展示了用户登录系统并添加学生的完整流程:

- 系统启动后首先进行身份验证
- 登录模块负责验证用户凭据并返回权限信息
- 验证成功后,用户可使用系统功能
- 添加学生时,系统会验证数据有效性并保存到文件
- 所有操作结果都会反馈给用户
餐馆点菜系统 - 项目需求书
1. 项目概述
1.1 项目名称
简易餐馆点菜系统
1.2 项目背景
传统手写点菜方式存在效率低、易出错、统计困难等问题,尤其在高峰期易出现漏单、错单,影响服务质量。本项目旨在开发轻量级数字化系统,实现餐桌管理、菜品管理、订单处理等功能,提升餐馆运营效率。
1.3 项目目标
开发控制台版点菜系统,支持菜品展示、餐桌状态管理、订单创建与结算等核心功能,通过角色权限控制,满足中小型餐馆日常管理需求。
1.4 预期用户
- 管理员:负责菜品管理、用户管理
- 服务员:负责餐桌查看、订单处理、结算等
2. 功能需求
2.1 登录模块
- 设计
User基类及Admin、Waiter派生类,存储用户名、加密密码、角色及姓名 SystemManager处理登录验证与权限控制,采用异或加密算法保护密码- 系统启动需登录,提供 3 次尝试机会,超次则退出;区分管理员与服务员权限
2.2 菜品管理模块
- 设计
Dish基类及HotDish、ColdDish、Soup、Drink派生类,封装 ID、名称、价格、类型及在售状态 - 支持菜品列表展示(按类型分类);管理员可添加示例菜品,管理菜品在售 / 停售状态
2.3 餐桌管理模块
Table类管理餐桌 ID 及状态(空闲 / 占用)- 支持餐桌状态展示及空闲餐桌查询;订单创建时自动将餐桌改为 “占用”,结算后恢复 “空闲”
2.4 订单管理模块
Order类及OrderItem结构体管理订单信息(ID、餐桌 ID、创建时间、状态、订单项列表)- 支持创建订单、添加菜品(含数量)、展示详情(含小计与总计)、结算功能(更新订单及餐桌状态);支持所有订单列表展示
2.5 界面交互模块
- 控制台菜单导航,实现光标定位、边框绘制等美化功能
- 提供带提示的输入(支持密码隐藏),操作结果通过消息反馈
3. 系统文件结构设计
plaintext
餐馆点菜系统/
├── main.cpp // 程序入口,负责登录与主菜单调度
├── system.h/.cpp // 系统管理类,处理核心业务逻辑
├── order.h/.cpp // 订单类及订单项,实现订单计算与展示
├── dish.h/.cpp // 菜品类(基类及派生类),实现菜品展示
├── table.h/.cpp // 餐桌类,管理餐桌状态
├── user.h/.cpp // 用户类(基类及派生类),实现用户信息管理
├── common.h/.cpp // 公共枚举(角色、状态等)及工具函数(加密)
└── ui.h/.cpp // 界面辅助函数(光标定位、颜色设置等)文件结构说明
- 入口模块:
main.cpp初始化系统,处理登录与菜单交互 - 系统管理模块:
system.h/.cpp协调各模块,处理核心业务 - 数据模型模块:
order、dish、table、user相关类封装数据及基础操作 - 公共模块:
common.h/.cpp提供共用枚举与工具;ui.h/.cpp统一管理界面交互
4. 技术要求
4.1 开发环境
- 开发工具:Dev C++
- 编程语言:C++
- 编译标准:C++11 及以上
4.2 技术要点
- 面向对象设计,通过类封装数据与操作
- 运用继承、多态实现菜品与用户的多类型管理
- 采用 STL 容器(vector)存储数据;分离界面与业务逻辑
- 实现控制台美化及简单密码加密
4.3 交付物
- 可执行程序(.exe)
- 完整源代码(按模块组织)
- 使用说明文档及测试报告
5. 系统架构设计
5.1 系统架构图
采用分层架构:

5.2 核心流程(点餐与结算)

