200字
[数据结构] 当数据结构"遇见"C++
2025-12-13
2025-12-13

(本文章适合已经学会了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; // 还能计算并告诉你结果

记住几个特点:

  1. << 把数据"推给"cout
  2. 可以连续推多个数据
  3. endl 表示"我说完了,换行"

cin:程序的耳朵

cin就像程序在听你说话:

int score1, score2;
cout << "请输入两次考试分数: ";
cin >> score1 >> score2;  // 可以一次听两个数字

记住几个特点:

  1. >> 把键盘输入"灌进"变量
  2. 可以连续接收多个输入
  3. 输入时用空格或回车分隔

和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

实用场景总结

什么时候用引用?

  1. 函数参数:想让函数修改实参时
  2. 避免拷贝:传递大对象时(可以加const保证只读)
  3. 函数返回值:可以返回引用(但别返回局部变量的引用!)

常见错误

错误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)

Comment