(本文章适合已经学会了C语言的同学食用)
(感谢DeepSeek对本文章的大力赞助)
引入——老朋友的新面孔
C++源自C语言,因此也与C语言有许多共同之处。
- 语法兼容:C++几乎完全兼容C语法
- 基本数据类型:int, char, float, double等保持一致
- 控制结构:if, for, while, switch等完全相同
- 指针机制:指针操作基本一致
- ……
当然,C++和C语言有个最大的不同——
- C语言:面向过程的编程语言
- C++:支持面向对象、泛型编程的多范式语言
具体怎么说呢?我们接下来举个例子:
C语言的视角:数据与操作分离
typedef struct {
int* data;
int size;
int capacity;
} Vector;
void init(Vector* vec, int cap);
void push_back(Vector* vec, int value);
int at(Vector* vec, int index);
void destroy(Vector* vec);
int main() {
Vector vec; // 数据
init(&vec, 10); // 操作1:初始化
push_back(&vec, 5); // 操作2:添加
int x = at(&vec, 0); // 操作3:访问
destroy(&vec); // 操作4:清理
// 数据和操作是分开的!
}
//此处省略各个函数原型的具体实现……
可见,在C语言中,数据和操作是分开的,因此也会造成不少麻烦事。
C++的视角:数据与操作绑定
class Vector {
private:
int* data;
int size;
int capacity;
public:
//接下来省略各个函数的具体实现
Vector(int cap = 10); // 构造函数:创建时自动初始化
void push_back(int value); // 成员函数:操作自己的数据
int at(int index); // 成员函数:访问自己的数据
~Vector(); // 析构函数:自动清理
};
// 使用
int main() {
Vector vec(10); // 创建对象,自动初始化
vec.push_back(5); // 对象操作自己的数据
int x = vec.at(0);// 对象访问自己的数据
// vec销毁时,自动调用析构函数清理!
// 数据和对数据的操作是一个整体!
}
可见,在C++中,数据和操作是一体的,这样我们就不用考虑数据和操作之间的关联的问题,我们有了数据,就能立刻知道有哪些操作。
第一部分——C++中的输入输出
cin 和 cout:数据的搬运工
形象比喻
想象一下数据就像水一样:
- cout:把数据从你的程序"流"到屏幕上
- cin:把数据从键盘"流"到你的程序中
#include <iostream>
using namespace std;
int main() {
int age;
// cout:把数据"流"到屏幕
cout << "请输入年龄: ";
// cin:把键盘输入"流"到变量里
cin >> age;
// 再用cout输出结果
cout << "你的年龄是: " << age << endl;
return 0;
}
cout:你的程序发言人
cout就像你的程序在跟你说话:
cout << "你好,我是程序!" << endl; // 说一句话
cout << "2 + 3 = " << 2 + 3 << endl; // 还能计算并告诉你结果
记住几个特点:
- 用
<<把数据"推给"cout - 可以连续推多个数据
endl表示"我说完了,换行"
cin:程序的耳朵
cin就像程序在听你说话:
int score1, score2;
cout << "请输入两次考试分数: ";
cin >> score1 >> score2; // 可以一次听两个数字
记住几个特点:
- 用
>>把键盘输入"灌进"变量 - 可以连续接收多个输入
- 输入时用空格或回车分隔
和C语言的区别?
| C语言 | C++ | 区别 |
|---|---|---|
printf("%d", age); | cout << age; | 不用记%d、%f |
scanf("%d", &age); | cin >> age; | 不用写&,cin自动处理 |
printf("成绩: %.2f", score); | cout << fixed << setprecision(2) << score; | 格式控制更直观 |
第二部分——C++中的左值引用
引用:给变量起"外号"
基本概念
引用就是给一个已经存在的变量起个别名。它们共享同一块内存地址。
#include <iostream>
using namespace std;
int main() {
int a = 10;
// 给a起个外号叫ref
int &ref = a; // ref是a的引用
cout << "a = " << a << endl; // 输出: 10
cout << "ref = " << ref << endl; // 输出: 10
ref = 20; // 通过"外号"修改值
cout << "a = " << a << endl; // 输出: 20(原变量也变了!)
cout << "ref = " << ref << endl; // 输出: 20
return 0;
}
引用的特点
1. 必须初始化(必须有个本名)
int &ref; // 错误!引用必须在创建时指定它引用谁
int &ref = a; // 正确:一出生就知道是谁的外号
2. 不能重新绑定(一生只有一个本名)
int a = 10, b = 20;
int &ref = a; // ref是a的外号
ref = b; // 这是把b的值赋给a,不是让ref变成b的外号!
cout << "a = " << a << endl; // 输出: 20
cout << "b = " << b << endl; // 输出: 20
cout << "ref = " << ref << endl; // 输出: 20(ref还是a的外号)
3. 不占用额外空间(外号不占地方)
引用只是别名,不是新的变量,不分配内存。
引用在函数中的应用
情景1:交换两个数(经典的例子)
C语言方式(用指针):
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 使用时
int x = 5, y = 10;
swap(&x, &y); // 要传地址
C++方式(用引用):
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
// 使用时
int x = 5, y = 10;
swap(x, y); // 直接传变量,看起来像值传递,其实是引用传递
情景2:避免大对象拷贝
#include <iostream>
#include <vector>
using namespace std;
// 值传递:会拷贝整个vector(如果很大,很耗时)
void printVector(vector<int> vec) {
for (int num : vec) {
cout << num << " ";
}
cout << endl;
}
// 引用传递:不拷贝,直接操作原vector
void printVectorRef(const vector<int> &vec) {
for (int num : vec) {
cout << num << " ";
}
cout << endl;
}
int main() {
vector<int> bigVector(1000000, 1); // 100万个1
printVector(bigVector); // 慢!要拷贝100万个元素
printVectorRef(bigVector); // 快!直接操作原数据
return 0;
}
常量引用:只读的外号
int a = 10;
const int &ref = a; // 常量引用,只能读不能写
cout << ref << endl; // 可以读
// ref = 20; // 错误!不能通过ref修改a的值
a = 20; // 但是可以直接修改a
cout << ref << endl; // 输出: 20(外号看到的变化)
引用 vs 指针
| 特性 | 引用 | 指针 |
|---|---|---|
| 初始化 | 必须初始化 | 可以不初始化 |
| 空值 | 不能为空 | 可以为空(NULL/nullptr) |
| 重绑定 | 不能改变绑定 | 可以指向不同变量 |
| 语法 | 更简洁(不用*和&) | 需要解引用(*) |
| 安全性 | 更安全(不能为空) | 可能为空指针 |
形象理解:
- 指针:快递员(知道你的地址,可以送东西到你家)
- 引用:你家的门牌号(就是你家本身)
// 快递员(指针)可以换地址
int a = 10, b = 20;
int *p = &a; // 快递员知道a的地址
p = &b; // 快递员换了,现在知道b的地址
// 门牌号(引用)不能换
int &ref = a; // 这是a家的门牌号
// ref = b; // 错误!这不是换门牌号,而是把b的值给a
实用场景总结
什么时候用引用?
- 函数参数:想让函数修改实参时
- 避免拷贝:传递大对象时(可以加const保证只读)
- 函数返回值:可以返回引用(但别返回局部变量的引用!)
常见错误
错误1:返回局部变量的引用
int& badFunction() {
int x = 10; // 局部变量
return x; // 错误!x在函数结束后会被销毁
} // 返回的引用指向了无效内存
// 正确做法:返回静态变量或全局变量的引用
int& goodFunction() {
static int x = 10; // 静态变量,生命周期长
return x; // 安全
}
错误2:引用未初始化的变量
int *p;
int &ref = *p; // 危险!p可能指向无效内存
第三部分——C++ STL(标准模板库)
一、字符串类 - string
头文件
#include <string>
主要特点
string str = "Hello";
str = str + " World!"; // 可以直接拼接字符串
cout << str.length(); // 获取长度
cout << str[0]; // 像数组一样访问
常用操作
string s1 = "Hello";
string s2 = "World";
// 比较
if (s1 == s2) { /* ... */ }
// 查找
size_t pos = s1.find("ll"); // 返回位置或string::npos
// 子串
string sub = s1.substr(1, 3); // 从位置1开始,取3个字符
// 替换
s1.replace(1, 3, "aaa"); // 从位置1开始替换3个字符
// 插入
s1.insert(2, "xxx");
二、容器类
1. vector(动态数组)【最常用】
#include <vector>
vector<int> v; // 创建空vector
v.push_back(1); // 添加元素
v.pop_back(); // 删除末尾元素
v.size(); // 获取大小
v[0]; // 访问元素(不检查边界)
v.at(0); // 访问元素(检查边界)
v.resize(10); // 调整大小
2. deque(双端队列)【常用】
#include <deque>
deque<int> dq;
dq.push_back(1); // 尾部添加
dq.push_front(2); // 头部添加
dq.pop_back(); // 尾部删除
dq.pop_front(); // 头部删除
3. list(双向链表)【常用】
#include <list>
list<int> lst;
lst.push_back(1);
lst.push_front(2);
lst.sort(); // 排序
lst.unique(); // 去重
lst.remove(3); // 删除所有值为3的元素
lst.reverse(); // 反转
4. forward_list(单向链表)
#include <forward_list>
forward_list<int> fl;
fl.push_front(1); // 只能从头部添加
fl.insert_after(pos, 2); // 在指定位置后插入
5. set(有序集合,元素唯一)
#include <set>
set<int> s;
s.insert(3);
s.insert(1);
s.insert(2);
// 自动排序:1, 2, 3
s.find(2); // 查找元素,找不到返回s.end()
s.erase(2); // 删除元素
补充:s.find的用法:
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s = {10, 20, 30, 40, 50};
// 查找元素20
auto it = s.find(20);
if (it != s.end()) {
cout << "找到元素: " << *it << endl;
} else {
cout << "未找到元素" << endl;
}
// 查找不存在的元素
it = s.find(99);
if (it == s.end()) {
cout << "元素99不存在于集合中" << endl;
}
return 0;
}
6. multiset(有序集合,元素可重复)
#include <set>
multiset<int> ms;
ms.insert(1);
ms.insert(1); // 可以有多个1
补充:
#include <iostream>
#include <set>
#include <vector>
using namespace std;
int main() {
multiset<int> ms;
// 插入单个元素
ms.insert(10);
ms.insert(20);
ms.insert(10); // 可以重复插入
ms.insert(30);
ms.insert(20);
ms.insert(10); // 再次插入10
cout << "插入后multiset内容: ";
for (int num : ms) {
cout << num << " ";
}
cout << endl; // 输出: 10 10 10 20 20 30
// 插入返回值
auto ret = ms.insert(15);
cout << "插入15的位置: " << *ret << endl;
// 插入重复元素时的返回值
ret = ms.insert(10); // 插入已存在的元素
cout << "再次插入10的位置: " << *ret << endl;
// 批量插入
vector<int> vec = {25, 25, 30, 35};
ms.insert(vec.begin(), vec.end());
cout << "批量插入后: ";
for (int num : ms) {
cout << num << " ";
}
cout << endl;
// 使用emplace(C++11)
ms.emplace(18);
ms.emplace(18); // 插入两个18
cout << "使用emplace后: ";
for (int num : ms) {
cout << num << " ";
}
cout << endl;
return 0;
}
7. map(有序键值对,键唯一)
#include <map>
map<string, int> m;
m["apple"] = 5; // 插入或修改
m.insert({"banana", 3});
m.find("apple"); // 查找
m.erase("apple"); // 删除
for (auto &[key, value] : m) { // C++17结构化绑定
cout << key << ": " << value;
}
8. multimap(有序键值对,键可重复)
#include <map>
multimap<string, int> mm;
mm.insert({"apple", 5});
mm.insert({"apple", 3}); // 可以有多个"apple"
9. unordered系列【常用】
主要包含unordered_set、unordered_multiset、unordered_map、unordered_multimap等,都很常用,用法和上面对应的相同,但是不保证有序,在此不过多赘述。
10. stack(栈)【常用】
#include <stack>
stack<int> stk;
stk.push(1); // 入栈
stk.top(); // 查看栈顶
stk.pop(); // 出栈(不返回栈顶元素)
stk.empty(); // 是否为空
stk.size(); // 大小
11. queue(队列)【常用】
#include <queue>
queue<int> q;
q.push(1); // 入队
q.front(); // 查看队首
q.back(); // 查看队尾
q.pop(); // 出队(不返回元素)
q.empty(); // 是否为空
12. priority_queue(优先队列,默认大顶堆)【常用】
一般我们常用优先队列建堆。
#include <queue>
priority_queue<int> pq; // 最大堆
pq.push(3);
pq.push(1);
pq.push(2);
pq.top(); // 总是返回最大值(3)
pq.pop(); // 删除最大值
// 最小堆
priority_queue<int, vector<int>, greater<int>> min_pq;
三、算法类
头文件
#include <algorithm>
1. sort(排序)
vector<int> v = {3, 1, 4, 1, 5};
// 默认升序
sort(v.begin(), v.end());
// 降序
sort(v.begin(), v.end(), greater<int>());
// 自定义结构体比较函数
struct Student {
string name;
int score;
};
// 单独定义一个比较函数
// 按分数升序排列
bool compareByScoreAsc(const Student &a, const Student &b) {
return a.score < b.score;
}
//调用
vector<Student> students;
sort(students.begin(), students.end(), compareByScore);
2. find(查找)
vector<int> v = {1, 2, 3, 4, 5};
// 查找值为3的元素
auto it = find(v.begin(), v.end(), 3);
if (it != v.end()) {
cout << "找到了,位置:" << it - v.begin();
}
3. binary_search(二分查找)
vector<int> v = {1, 2, 3, 4, 5};
// 必须先排序!
sort(v.begin(), v.end());
// 查找值为3的元素
bool found = binary_search(v.begin(), v.end(), 3);
其他常用算法
// 反转
reverse(v.begin(), v.end());
// 计数
int count = count_if(v.begin(), v.end(),
[](int x) { return x > 3; });
// 填充
fill(v.begin(), v.end(), 0);
// 复制
copy(v1.begin(), v1.end(), v2.begin());
// 最大值/最小值
auto max_it = max_element(v.begin(), v.end());
auto min_it = min_element(v.begin(), v.end());
四、补充
主要介绍一下pair和tuple,用了这两个就不需要再单独建一个结构体去存储了。
1. pair(对组)
#include <utility> // 或 <pair>
pair<string, int> p1("apple", 5);
pair<string, int> p2 = make_pair("banana", 3);
// 访问
cout << p1.first; // apple
cout << p1.second; // 5
// 比较(先比较first,再比较second)
if (p1 < p2) { /* ... */ }
2. tuple(元组,多个值的集合)
#include <tuple>
tuple<string, int, double> t("张三", 20, 85.5);
// 访问(C++11)
cout << get<0>(t); // 张三
cout << get<1>(t); // 20
cout << get<2>(t); // 85.5
// 创建(C++17)
auto t2 = make_tuple("李四", 21, 90.0);
// 结构化绑定(C++17)
auto [name, age, score] = t2;
五、STL使用要点总结
1. 迭代器(容器的指针)
vector<int> v = {1, 2, 3};
// 遍历方法1:下标
for (int i = 0; i < v.size(); i++) {
cout << v[i];
}
// 遍历方法2:迭代器
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it;
}
// 遍历方法3:范围for循环(C++11)(推荐尝试,真的很香)
for (int num : v) {
cout << num;
}
2. 容器的选择原则
- 需要随机访问:vector、deque、array
- 需要频繁插入删除:list、forward_list
- 需要自动排序:set、map
- 需要快速查找:unordered_set、unordered_map
- 先进先出:queue
- 后进先出:stack
- 优先级处理:priority_queue
3. 性能注意事项
- vector在尾部插入删除快,中间慢
- list在任何位置插入删除都快,但访问慢
- map/set查找为O(log n),unordered_map为O(1)
- 算法操作通常使用迭代器范围
[begin, end)