以下是c++核心编程部分
本页的pdf版点击下载:https://kblog.ink?download=204&tmstv=1764307479
C++ 内存分区模型(Memory Partition Model)
C++ 程序在运行时,内存会被系统划分成若干区域,不同区域负责存放不同的数据。
掌握内存分区对理解:
- 变量的生命周期
- 指针
- new / delete
- 全局变量与局部变量
- 类对象的行为
- 堆区泄漏
等内容非常关键。
C++ 程序内存的四大区域
根据 PDF 内容,C++ 程序运行时,主要的内存区域包括:
- 代码区(Code Segment)
- 全局区 / 静态区(Global / Static Segment)
- 栈区(Stack)
- 堆区(Heap)
下面逐一展开。
代码区(存放程序机器指令)
特点:
- 存放二进制代码(CPU 执行的机器指令)
- 共享:多个程序可访问同一份代码(提升效率)
- 只读:防止程序意外修改指令
- 程序运行前由操作系统加载入内存
全局区 / 静态区
包括两部分:
- 全局变量
- 静态变量(static)
- 字符串常量
- 常量区中的常量(编译器管理)
生命周期:
程序启动 → 内存分配
程序结束 → 自动释放
示例:
#include <iostream>
using namespace std;
int g_a = 10; // 全局变量
static int s_a = 20; // 静态变量
int main()
{
cout << g_a << endl;
cout << s_a << endl;
return 0;
}
栈区
由编译器自动管理,包含:
- 函数的局部变量
- 函数形参
- 返回地址
- 临时变量等
特点:
- 存储速度快(从高地址向低地址增长)
- 由编译器自动分配与释放
- 不需要手动 free
- 超出作用域自动销毁
- 堆栈变量不能返回地址(内存会被释放)
示例(⚠ 错误代码,用于教学):
int* func()
{
int a = 10; // 栈区
return &a; // ❌ 错误:返回局部变量地址
}
堆区
由程序员手动控制:
- 使用 new 开辟
- 使用 delete 手动释放
- 若不释放会导致 内存泄漏(memory leak)
示例:
int* p = new int(10);
cout << *p << endl;
delete p;
堆区和栈区的对比
| 对比 | 栈区 | 堆区 |
|---|---|---|
| 管理者 | 编译器 | 程序员 |
| 生命周期 | 作用域结束自动销毁 | new 分配、delete 销毁 |
| 速度 | 快 | 相对慢 |
| 地址方向 | 向下增长 | 向上增长 |
| 典型问题 | 返回局部变量地址 | 内存泄漏 |
综合示例:观察不同区域的地址
#include<iostream>
using namespace std;
int g_a = 10;
static int s_a = 20;
int main() {
int a = 10;
int b = 20;
static int c = 30;
cout << "局部变量a地址:" << &a << endl;
cout << "局部变量b地址:" << &b << endl;
cout << "静态变量c地址:" << &c << endl;
cout << "全局变量g_a地址:" << &g_a << endl;
cout << "静态变量s_a地址:" << &s_a << endl;
char str[] = "hello world";
cout << "字符串常量地址:" << (void*)"hello world" << endl;
int* p = new int(10);
cout << "堆区开辟地址:" << p << endl;
delete p;
system("pause");
return 0;
}
通过打印地址,可以直观对比不同变量存放的内存区域。
注意事项
- 不要返回局部变量地址(栈区会被销毁)
- 堆区需要 程序员手动释放
- 字符串字面量(如 “hello”)放在全局/常量区
- 栈区变量生命周期由 作用域 决定
- 堆区生命周期由 new / delete 决定
- 全局区变量生命周期是整个程序运行周期
- 尽量避免过多使用堆区对象,否则会造成管理复杂和泄漏风险
new 与 delete(动态开辟和释放内存)
在 C++ 中,堆区是程序员手动管理的区域,通过 new 和 delete 对内存进行控制。
new 的基本用法
new 用于在堆区开辟内存。
语法:
数据类型 * 指针变量 = new 数据类型(初始值);
示例:
int *p = new int(10);
cout << "堆区中的数据为:" << *p << endl;
delete p;
delete 的基本用法
delete 用于释放 new 分配的堆区内存。
语法:
delete 指针变量;
注意:
- 只能释放由
new分配的内存 - 释放后指针变成“野指针”,但不影响语法(⚠ 危险)
- 建议手动置空:
delete p;
p = NULL;
new 开辟数组
语法:
数据类型 * 指针变量 = new 数据类型[数组大小];
示例:
int *arr = new int[10];
for (int i = 0; i < 10; i++)
{
arr[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
delete[] arr;
delete 数组要使用方括号:
delete[] arr;
否则只释放首元素占用空间。
new 和 delete 的注意事项
⚠ 注意事项总结:
- new 开辟的内存必须用 delete 释放
- new[] 开辟数组必须用 delete[] 释放
- delete 后指针不应继续使用
- delete 不会把指针自动置 NULL
- 同一块内存不能 delete 两次
- new 返回的是变量“内存地址”,类型一致
综合示例:使用 new 返回堆区地址
实现一个函数返回堆区数据:
int* func()
{
int *p = new int(10);
return p;
}
int main()
{
int *p = func();
cout << *p << endl;
delete p;
}
注意:
- 此写法 正确,因为返回的是堆区地址
- 与 “返回局部变量地址(栈区)” 完全不同
使用 new 分配结构体
示例:
struct person {
string name;
int age;
};
person *p = new person{"张三", 20};
cout << p->name << endl;
cout << p->age << endl;
delete p;
使用 new 分配结构体数组
示例:
person *pArray = new person[3]
{
{"张三", 18},
{"李四", 20},
{"王五", 22}
};
for (int i = 0; i < 3; i++)
{
cout << pArray[i].name << " " << pArray[i].age << endl;
}
delete[] pArray;
new 和 malloc 的区别
| 对比点 | new | malloc |
|---|---|---|
| 语言 | C++ | C |
| 类型检查 | 有类型 | 无类型(void*) |
| 构造函数 | 会调用 | 不会调用 |
| 内存释放 | delete | free |
| 是否可重载 | 可重载 | 不可重载 |
| 初始化 | 可初始化 | 不自动初始化 |
delete 的常见错误
错误 1:delete 局部变量
int a = 10;
delete &a; // ❌ 错误,局部变量不在堆区
错误 2:数组用 delete
int *arr = new int[10];
delete arr; // ❌ 错误
必须:
delete[] arr;
错误 3:delete 空指针(安全但无意义)
int *p = NULL;
delete p; // ✔ 不会出错,但没有意义
错误 4:内存泄漏
int *p = new int(10);
// 忘记 delete p
函数提高(Function Advanced Topics)
函数提高部分主要包含以下内容:
- 内联函数(inline)
- 默认参数
- 函数重载
- 占位参数
- const 与重载的关系
- 引用与重载的关系
内联函数(inline)
内联函数用于解决频繁调用函数带来的性能损耗问题。
普通函数调用过程包含:
- 压栈(保存现场)
- 跳转
- 执行函数体
- 返回调用点
- 恢复现场
这些步骤会增加开销,而内联函数通过“将代码直接插入调用点”进行优化。
语法:
inline 返回值类型 函数名(参数列表)
{
函数体
}
示例:
inline int add(int a, int b)
{
return a + b;
}
int main()
{
cout << add(10, 20) << endl;
return 0;
}
注意事项:
- 内联函数建议用于函数体代码简单、调用频繁的函数
- 不是所有 inline 都会被编译器内联,编译器有最终决定权
- 函数体过大或包含循环、递归时,一般不会被内联
默认参数(Default Arguments)
C++ 允许在函数形参列表中为参数提供默认值。
语法:
返回值类型 函数名(参数 = 默认值);
示例:
int func(int a, int b = 20, int c = 30)
{
return a + b + c;
}
int main()
{
cout << func(10) << endl; // 10 + 20 + 30
cout << func(10, 100) << endl; // 10 + 100 + 30
return 0;
}
注意事项:
- 如果某个位置开始使用默认参数,则之后的所有参数都必须是默认参数
- 默认参数不能在同时出现声明和定义中重复写入
错误示例(重复默认值):
int func(int a = 10); // 声明
int func(int a = 10) { } // 定义(重复) ❌
正确方式:
int func(int a = 10); // 声明默认
int func(int a) { } // 定义不写默认值
函数重载(Function Overloading)
函数重载指的是:在同一作用域中,函数名相同,但参数列表不同。
重载的要求:
- 参数类型不同
- 或参数个数不同
- 或参数顺序不同
返回值类型不同不能构成重载。
示例:
void func();
void func(int a);
void func(double a);
void func(int a, double b);
void func(double a, int b);
重载与引用的关系
当参数为引用时,重载规则依然适用。
示例:
void func(int &a);
void func(const int &a);
这些是可区分的,因为 const 与非 const 引用会优先匹配。
示例调用:
int a = 10;
func(a); // 调用非 const
func(10); // 调用 const(临时值)
重载与默认参数的冲突
函数重载与默认参数有时会造成二义性。
示例(错误):
void func(int a);
void func(int a, int b = 10);
func(10); // ❌ 二义性:两个都能匹配
为了避免冲突,重载函数中应尽量避免此类模糊写法。
占位参数(Placeholder Parameter)
占位参数格式如下:
返回值类型 函数名(类型)
示例:
void func(int)
{
cout << "hello" << endl;
}
int main()
{
func(10);
return 0;
}
占位参数的意义:
- 使函数签名结构完整
- 有时用于保留接口位置
- 调用时必须传递一个参数,但函数不会使用它
占位参数可以有默认值:
void func(int = 10)
函数提升内容总结
- inline 适合小函数
- 默认参数只能在一个地方声明
- 返回值不同不能构成重载
- 重载时参考参数类型、个数、顺序
- 重载与默认参数可能产生二义性
- 引用与 const 可以构成重载
- 占位参数可用于接口兼容但一般不使用
类与对象(Class and Object)
C++ 面向对象三大特性为:封装、继承、多态。
类与对象是 C++ 最核心的语法基础。
类的基本定义
类的语法结构如下:
class 类名
{
public:
成员列表;
};
示例:
class Person
{
public:
string name;
int age;
void showPerson()
{
cout << "name = " << name << endl;
cout << "age = " << age << endl;
}
};
类是一个“数据类型”,对象是该类型的“变量”。
对象创建方式:
Person p;
p.name = "张三";
p.age = 20;
p.showPerson();
封装(Encapsulation)
封装的核心思想是:
将属性(成员变量)和行为(成员函数)包装到一起,并控制访问权限。
访问权限有三种:public、protected、private。
public:可以在类内、类外访问
protected:可以在类内访问,子类可以访问,类外不能访问
private:只能在类内访问,类外和子类都无法访问
示例:
class Person
{
public:
string name;
protected:
int age;
private:
string secret;
};
注意:
- public 一般用于对外提供接口
- private 一般用于隐藏数据
- protected 一般用于继承体系中保护成员
对象的内存模型
在创建对象时,只有“非静态成员变量”占用对象内存。
成员函数不占对象空间,因为所有对象共用同一份函数代码。
示例:
class Person
{
public:
int age;
void func() {}
};
int main()
{
Person p;
cout << sizeof(p) << endl; // 4,只有 age 占空间
}
注意:
- 空类对象也会占用 1 字节(用于区分不同对象)
- 静态成员不占对象空间(属于类)
构造函数(Constructor)
构造函数用于创建对象时初始化对象。
特点:
- 名字与类名相同
- 没有返回值
- 自动调用
- 可以有重载
语法:
class Person
{
public:
Person() {
cout << "无参构造" << endl;
}
Person(int a) {
age = a;
cout << "有参构造" << endl;
}
int age;
};
创建对象时自动调用:
Person p1; // 调用无参构造
Person p2(10); // 调用有参构造
析构函数(Destructor)
析构函数用于对象销毁时执行清理操作。
特点:
- 名字为
~类名 - 无参数
- 无返回值
- 自动调用
- 类似构造函数,也只有一个
示例:
class Person
{
public:
~Person() {
cout << "析构函数调用" << endl;
}
};
对象作用域结束后自动调用析构。
构造函数的三种调用形式
括号法:
Person p1(10);
显示法:
Person p2 = Person(10);
隐式转换法:
Person p3 = 10; // 相当于 Person p3 = Person(10);
注意:
隐式法只能匹配单参数构造。
拷贝构造函数(Copy Constructor)
拷贝构造用于:
- 用一个对象初始化另一个对象
- 值传递对象时
- 返回局部对象时
语法:
Person(const Person &p)
{
age = p.age;
}
示例:
Person p1(10);
Person p2(p1); // 调用拷贝构造
拷贝构造调用三种情况
用一个对象初始化另一个对象:
Person p2(p1);
函数的值传递:
void func(Person p) { }
func(p1);
函数返回对象:
Person func()
{
Person p;
return p;
}
以上三种情况都会触发拷贝构造。
深拷贝与浅拷贝
浅拷贝:简单的成员逐位拷贝(默认行为)
深拷贝:对堆区对象做独立 copy,避免指针重复释放问题
浅拷贝示例(危险):
class Person
{
public:
Person() {
height = new int(180);
}
~Person() {
delete height;
}
int *height;
};
Person p1;
Person p2 = p1; // 浅拷贝,两个对象共享同一块堆区
p1 与 p2 析构时都 delete height,会发生重复释放错误。
深拷贝应这样写:
Person(const Person &p)
{
height = new int(*p.height);
}
初始化列表(Initialization List)
C++ 提供初始化列表用于初始化成员变量。
语法:
类名(): 属性1(值1), 属性2(值2), ...
{
}
示例:
class Person
{
public:
int a;
int b;
Person(): a(10), b(20)
{
}
};
作用:
- 初始化 const 成员
- 初始化引用成员
- 初始化对象成员
- 更高效
this 指针
this 指针指向当前对象,可以用于解决命名冲突。
示例:
class Person
{
public:
int age;
Person(int age)
{
this->age = age;
}
};
this 指针的特点:
- 指向当前对象
- 用于成员变量与参数同名的情况
- 在成员函数中隐式存在
- 可用于返回对象本身:return *this
const 成员函数
当成员函数后面加 const,表示函数内部不能修改成员变量。
语法:
void show() const;
示例:
class Person
{
public:
int age;
void show() const
{
// age = 20; 错误,不能修改
cout << age << endl;
}
};
const 对象只能调用 const 成员函数。
静态成员
静态成员属于类,而不是对象。
包括静态成员变量和静态成员函数。
静态成员变量:
- 所有对象共享
- 必须类外初始化
示例:
class Person
{
public:
static int count;
};
int Person::count = 0;
静态成员函数:
- 不含 this 指针
- 只能访问静态成员变量
static void func()
{
count++;
}
成员对象(类中包含类)
示例:
class Address
{
public:
string city;
};
class Person
{
public:
string name;
Address addr;
};
注意:
成员对象的构造顺序是:
先构造成员对象,再构造当前对象本体。
析构顺序相反。
对象的创建与销毁顺序
创建对象时:
- 基类构造
- 成员对象构造
- 派生类构造
销毁时顺序相反。
小节总结
- 类是自定义数据类型
- 封装通过访问权限实现数据保护
- 构造函数与析构函数用于初始化和清理对象
- 拷贝构造在对象复制时触发
- 必须处理深浅拷贝带来的堆区问题
- 静态成员属于类共享
- 初始化列表是推荐的初始化方式
- this 指针指向当前对象
- const 成员函数保护对象不被修改
继承(Inheritance)
继承是面向对象三大特性之一,通过继承可以提高代码复用性,让多个类之间建立逻辑关系。
继承的基本思想是:
从已有类创建新类,新类继承旧有类的属性和行为。
旧有类称为父类或基类
新类称为子类或派生类
继承的基本语法
语法格式:
class 子类 : 继承方式 父类
{
};
示例:
class Person
{
public:
string name;
int age;
};
class Student : public Person
{
public:
int score;
};
对象使用:
Student stu;
stu.name = "张三";
stu.age = 18;
stu.score = 100;
三种继承方式
public 公有继承
protected 保护继承
private 私有继承
继承方式控制父类成员在子类中的访问权限。
父类成员访问权限在不同继承方式下的变化规则:
| 父类成员权限 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
无论什么继承方式,父类的 private 成员永远不能被子类访问。
继承中构造与析构顺序
创建子类对象时执行顺序:
父类构造
子类构造
销毁对象时顺序相反:
子类析构
父类析构
示例:
class Base
{
public:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base
{
public:
Derived() { cout << "Derived构造" << endl; }
~Derived() { cout << "Derived析构" << endl; }
};
int main()
{
Derived d;
}
输出顺序:
Base构造
Derived构造
Derived析构
Base析构
同名成员处理
当子类与父类存在同名成员时,会发生隐藏。
示例:
class Base
{
public:
int a = 100;
};
class Derived : public Base
{
public:
int a = 200;
};
访问方式:
Derived d;
cout << d.a << endl; // 子类的 a
cout << d.Base::a << endl; // 父类的 a
注意:
子类对象默认访问子类的同名成员
若要访问父类,需使用作用域限定符
同名成员函数
如果父子类有同名成员函数,子类会隐藏父类同名函数。
示例:
class Base
{
public:
void func() { cout << "Base::func" << endl; }
};
class Derived : public Base
{
public:
void func() { cout << "Derived::func" << endl; }
};
调用方式:
Derived d;
d.func(); // 子类版本
d.Base::func(); // 父类版本
子类重载父类同名函数
如果子类的同名函数与父类参数不同,这属于函数重载,而非覆盖。
示例:
class Base
{
public:
void func() { cout << "Base func" << endl; }
};
class Derived : public Base
{
public:
void func(int a) { cout << "Derived func" << endl; }
};
调用方式:
Derived d;
d.func(); // 使用 d.Base::func() 必须用作用域访问
d.func(10);
注意:
子类如果定义了新的 func(int),不会影响父类的 func()
继承中的静态成员
父类的静态成员在子类中依然存在,并且遵循同名隐藏原则。
示例:
class Base
{
public:
static int a;
};
int Base::a = 10;
class Derived : public Base
{
public:
static int a;
};
int Derived::a = 20;
访问方式:
cout << Derived::a << endl; // 子类静态变量
cout << Derived::Base::a << endl; // 父类静态变量
继承的权限控制总结
父类 private 成员永远不能被子类访问
public、protected 会根据继承方式发生权限转换
无论什么继承方式,父类中的 private 成员对外都不可见
子类可以使用作用域访问父类的同名成员
构造顺序:先父类后子类
析构顺序:先子类后父类
成员同名的注意事项
当父类和子类成员名称相同:
访问子类成员:直接访问
访问父类成员:使用 Base:: 成员名
同名函数会被隐藏
通过作用域可以访问父类版本
小结
继承用于构建类之间的层次结构
提高代码复用性
通过继承方式控制父类成员在子类中的访问权限
同名成员会发生隐藏,可使用作用域解决
构造与析构顺序遵循先父后子、先子后父的原则
多态(Polymorphism)
多态是面向对象三大特性之一,指同一操作在不同对象上表现出不同的行为。
在 C++ 中,多态主要分为两类:
静态多态(编译时多态)
动态多态(运行时多态)
静态多态通过函数重载和模板实现
动态多态通过继承 + 虚函数实现
C++ 中的多态通常指动态多态。
动态多态的条件
要实现动态多态,需要满足两个条件:
父类中有虚函数
子类对虚函数进行重写
示例基础类结构:
class Base
{
public:
virtual void func()
{
cout << "Base func" << endl;
}
};
class Derived : public Base
{
public:
void func()
{
cout << "Derived func" << endl;
}
};
通过父类指针或引用调用虚函数:
Base *p = new Derived;
p->func(); // 输出 Derived func
在运行过程中,根据对象实际类型调用对应的函数,这就是动态多态。
虚函数(virtual)
虚函数由关键字 virtual 声明,表示希望通过指针或引用实现动态绑定。
语法:
virtual 返回值类型 函数名(参数列表);
虚函数特性:
函数调用在运行时根据对象真实类型决定
虚函数必须通过指针或引用调用才有多态效果
构造函数不能是虚函数
析构函数可以是虚函数(常见且必要)
函数重写(override)
子类对父类虚函数进行重新定义。
要求:
函数名相同
参数列表相同
返回值类型相同
访问权限不受限制
示例:
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "猫在说话" << endl;
}
};
调用:
Animal *p = new Cat;
p->speak(); // 调用子类版本
动态多态的作用
通过父类指针或引用操作子类对象,可以让程序更加灵活。
例如:
void doSpeak(Animal &a)
{
a.speak();
}
Cat cat;
doSpeak(cat);
可以将不同子类对象传入同一函数,表现不同行为。
多态与虚函数表机制(vtable)
当一个类中含有虚函数时:
编译器会为该类生成一个虚函数表(vtable)
每个对象中隐含一个指针(vptr)指向虚函数表
虚函数表中记录着当前类所有虚函数的地址。
当父类指针指向子类对象时,vptr 会指向子类的虚函数表,从而调用子类重写版本。
虚函数表的结构属于运行时机制细节,实现动态绑定。
析构函数的多态问题
如果父类指针指向子类对象,必须将父类析构设置为虚析构,否则会导致资源无法释放。
示例:
class Base
{
public:
virtual ~Base()
{
cout << "Base析构" << endl;
}
};
class Derived : public Base
{
public:
~Derived()
{
cout << "Derived析构" << endl;
}
};
调用:
Base *p = new Derived;
delete p;
正确输出:
Derived析构
Base析构
如果父类析构不是虚函数,则会只析构父类,会导致子类资源泄漏。
纯虚函数(Pure Virtual Function)
纯虚函数用于定义抽象行为,必须在子类中实现。
语法:
virtual 返回值类型 函数名(参数) = 0;
示例:
class Animal
{
public:
virtual void speak() = 0;
};
含有纯虚函数的类称为抽象类,不能实例化。
抽象类(Abstract Class)
抽象类特性:
至少有一个纯虚函数
不能实例化对象
可以有成员变量
可以有构造和析构
只能被继承,用于提供公共接口
示例:
class Animal
{
public:
virtual void speak() = 0;
};
class Dog : public Animal
{
public:
void speak()
{
cout << "狗叫" << endl;
}
};
使用:
Animal *p = new Dog;
p->speak();
接口类(Interface)
如果一个类所有成员函数都是纯虚函数,则这个类可被当成接口使用。
用于统一行为的抽象规范。
多态中的重载、重写、隐藏的区别
重载(同名函数,参数不同)
编译时决定
与继承无关
重写(虚函数)
必须发生在继承关系中
运行时决定
参数和返回值必须相同
隐藏(子类函数和父类同名但参数不同)
父类函数被遮蔽
需要使用父类作用域访问
多态的意义
允许从父类出发,针对不同子类对象执行不同操作
可以让代码扩展性更强
通过接口和继承体系构建可复用架构
是设计模式的基础机制之一
多态部分总结
多态依赖虚函数机制实现
父类指针或引用指向子类对象时发生动态绑定
必须使用虚析构来确保资源安全释放
纯虚函数用于定义接口
抽象类不能实例化
虚函数表和虚指针是运行时多态的底层基础
文件操作(File I/O)
C++ 提供三种文件流对象,用于文件操作:
ofstream:写文件ifstream:读文件fstream:读写文件均可
文件操作的两种模式:
文本文件
二进制文件
文本文件以普通字符方式读写
二进制文件以字节为单位读写,适合结构体等复杂数据写入
使用文件操作需要包含:
#include <fstream>
文本文件
文本文件通常通过 ASCII 字符保存内容,适合普通可读文本的读写。
下面分为写文件与读文件两个部分介绍。
写文件(ofstream)
步骤:
创建 ofstream 对象
打开文件
写入数据
关闭文件
示例:
#include <fstream>
using namespace std;
int main()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:20" << endl;
ofs.close();
return 0;
}
常用文件打开模式:
ios::out 创建并写入文件(覆盖)ios::app 追加写入ios::trunc 清空然后写入(默认)
注意:
如果文件不存在,会自动创建
如果文件存在,默认会清空后写入,除非使用追加模式
读文件(ifstream)
步骤:
创建 ifstream 对象
打开文件
读取数据
关闭文件
示例:
#include <fstream>
using namespace std;
int main()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return 0;
}
string buf;
while (getline(ifs, buf))
{
cout << buf << endl;
}
ifs.close();
return 0;
}
文本文件常见读取方式:
方式一:operator>> 直接读单词
方式二:getline() 读一行
方式三:ifs.get() 按字符读取
示例(四种方法汇总示例):
string buf;
// 方式一:operator>>
while (ifs >> buf)
{
cout << buf << endl;
}
// 方式二:getline
while (getline(ifs, buf))
{
cout << buf << endl;
}
// 方式三:ifs.get()
char c;
while ((c = ifs.get()) != EOF)
{
cout << c;
}
二进制文件
二进制文件用于以二进制格式存储数据,适合结构体、对象等读写。
打开方式:
写:ios::out | ios::binary
读:ios::in | ios::binary
二进制写文件
示例:写入结构体
#include <fstream>
using namespace std;
struct Person
{
char name[64];
int age;
};
int main()
{
ofstream ofs("person.dat", ios::out | ios::binary);
Person p = {"张三", 20};
ofs.write((const char *)&p, sizeof(Person));
ofs.close();
return 0;
}
注意:
必须使用 write() 函数,不是 operator<<
必须将结构体地址强制转换为 char*
必须指定写入大小(sizeof(Person))
二进制读文件
示例:
#include <fstream>
using namespace std;
struct Person
{
char name[64];
int age;
};
int main()
{
ifstream ifs("person.dat", ios::in | ios::binary);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return 0;
}
Person p;
ifs.read((char *)&p, sizeof(Person));
cout << p.name << " " << p.age << endl;
ifs.close();
return 0;
}
注意:
同样必须使用 read()
必须强制转换为 char*
必须指定读取大小
文件打开模式补充说明
ios::in:读ios::out:写ios::app:追加写入ios::binary:二进制方式ios::trunc:清空文件ios::ate:定位到文件末尾(读写位置不变)
组合方式示例:
ios::out | ios::binary
ios::in | ios::app
文件操作的注意事项
文件读写结束必须关闭,否则可能造成数据不完整
二进制读写结构体时必须保证结构体内存布局稳定
结构体成员最好不要包含动态内存指针,否则读写将失效
ifstream 的 is_open() 用于判断读取是否成功
读文件循环中要注意 EOF 条件
文件操作小节
文本文件适用于可视化内容保存
二进制文件适用于对象、结构体等的高效保存
ifstream 用于读文件
ofstream 用于写文件
fstream 用于读写文件
文本文件可以逐行、逐字符或按单词读取
二进制读写必须用 read 和 write
文件模式可组合使用
模板(Templates)
模板是 C++ 中的泛型机制,通过模板可以让代码适用于不同类型,提高代码复用性。
模板分为两大类:
函数模板
类模板
模板的核心思想是:在编译阶段根据实际参数类型生成对应的函数或类,从而实现代码复用。
函数模板(Function Template)
函数模板的语法:
template<typename T>
返回值类型 函数名(参数列表)
{
函数体
}
关键字可以使用typename或class:
template<class T>
两者没有区别。
示例:
template<typename T>
void mySwap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
int main()
{
int a = 10;
int b = 20;
mySwap(a, b);
double c = 1.1;
double d = 2.2;
mySwap(c, d);
return 0;
}
模板根据不同类型自动生成不同版本的函数。
函数模板的两种调用方式
自动类型推导:
mySwap(a, b);
显示指定类型:
mySwap<int>(a, b);
注意:
自动推导必须推导出一致的 T 类型,否则编译失败
显示指定类型不需要推导
模板的类型推导规则
类型必须完全匹配:
template<typename T>
void func(T a, T b) {}
func(10, 10.5); // 错误,T 无法同时为 int 和 double
使用显示指定类型可以绕过推导失败:
func<double>(10, 10.5); // 正确
数组传参时,T 推导为数组的指针类型。
普通函数与模板函数的调用优先级
当函数模板与普通函数都能匹配时,普通函数优先。
示例:
void func(int a)
{
cout << "普通函数" << endl;
}
template<typename T>
void func(T a)
{
cout << "模板函数" << endl;
}
int main()
{
func(10); // 调用普通函数
func<>(10); // 强制调用模板
}
使用 func<>(10) 可以强制使用模板版本。
模板的重载
模板可以重载:
void func() {}
template<typename T>
void func(T a) {}
template<typename T>
void func(T a, T b) {}
模板重载遵循与普通函数相同的重载规则。
模板与可变参数模板(略提到)
模板也可以是可变参数模板:
template<typename... Args>
void func(Args... args) {}
主要用于复杂泛型封装,但常规入门不要求掌握。
模板的局限性
类型推导失败时无法调用
模板代码必须在头文件中实现(分文件编写有限制)
编译时生成多份代码,可能导致二进制体积增大
错误信息复杂难懂
类模板(Class Template)
类模板用于创建泛型类。
语法:
template<typename T>
class Person
{
public:
T m_Name;
T m_Age;
Person(T name, T age)
{
m_Name = name;
m_Age = age;
}
};
对象创建方式:
Person<string> p("张三", "20");
注意:
类模板在实例化时必须指定类型
除非有默认模板参数
不能自动推导(不同于函数模板)
类模板的成员函数实现分离(分文件编写)
类模板的成员函数,如果放在 .cpp 里,会出现链接错误,因为模板需要在编译阶段生成代码,而编译器在 .cpp 中看不到模板参数信息。
解决方法有三种:
方式一:所有实现写在 .h 文件中
方式二:在 .cpp 文件底部 include .hpp(常见写法)
方式三:使用模板显式实例化
一般采用方式一,即模板的声明与定义都写在头文件中:
template<typename T>
class Person
{
public:
T name;
T age;
void show();
};
template<typename T>
void Person<T>::show()
{
cout << name << " " << age << endl;
}
类模板与继承
如果子类继承类模板,需要指定父类的类型:
template<typename T>
class Base
{
public:
T t;
};
class Child : public Base<int>
{
};
如果想让子类也成为模板类:
template<typename T1, typename T2>
class Child : public Base<T2>
{
};
类模板作为函数参数
类模板对象作为函数参数有三种方式:
直接使用具体类型:
void func1(Person<string> &p)
{
}
通过类模板本身做参数:
template<typename T>
void func2(Person<T> &p)
{
}
把整个类模板作为模板参数:
template<typename T>
void func3(T &p)
{
}
三种方式都可行,使用场景不同。
模板默认参数
模板可以提供默认类型:
template<typename T = int>
class Array
{
public:
T data;
};
使用:
Array<> a; // T 为 int
Array<double> b;
模板的小结
模板用于泛型编程
函数模板可自动推导类型
类模板必须显示指定类型(除非有默认模板参数)
普通函数优先于模板函数
模板需要写在头文件中实现
类模板对象作为参数有三种写法
继承类模板时必须明确类型或继续做模板
模板重载与普通函数重载规则一致
STL 概述(Standard Template Library)
STL 是 C++ 标准库的重要组成部分,它将数据结构与算法进行了泛型封装,使得程序员可以直接使用高度成熟的容器与算法,而不用频繁造轮子。
STL 由三大部分组成:
- 容器(Container)
- 算法(Algorithm)
- 迭代器(Iterator)
容器负责存储数据
算法负责处理数据
迭代器负责连接容器与算法
这三部分构成了 STL 的核心框架。
容器(Container)
容器是用于存储数据的类模板。
STL 提供多种不同结构、不同用途的容器。
常见容器类型包括:
- vector
- deque
- list
- set / multiset
- map / multimap
- stack
- queue
- priority_queue(优先级队列)
- array(若编译器支持 C++11)
- string(严格来说也属于 STL 容器体系)
容器按内存结构可分为:
- 序列式容器(Sequence containers)
- 关联式容器(Associative containers)
序列式容器强调元素顺序
关联式容器提供树结构、键值对结构等
算法(Algorithm)
STL 提供大量算法用于处理容器中的数据。
算法大体分类为:
- 查找算法
- 排序算法
- 遍历算法
- 拷贝替换算法
- 删除算法
- 数值算法
这些算法均由函数模板实现,保证了强大的通用性。
STL 算法的设计思想是“操作独立于容器”,使得不同容器可以复用同一套算法。
迭代器(Iterator)
迭代器是容器与算法的桥梁,用来访问容器中的元素。
迭代器类似于一个“指针”,但每种容器都有自己专属的迭代器类型。
例如:
vector<int>::iterator it;
迭代器分类:
- 输入迭代器
- 输出迭代器
- 前向迭代器
- 双向迭代器
- 随机访问迭代器
不同容器能提供的迭代器能力不同,例如:
vector 提供随机访问迭代器
list 仅提供双向迭代器
因此部分算法只能应用于支持强大迭代器的容器上。
STL 六大组件概念
STL 被设计为六大基础组件:
容器(Containers)
算法(Algorithms)
迭代器(Iterators)
适配器(Adapters)
分配器(Allocators)
函数对象(Functors)
以下对每个组件进行说明。
容器(Containers)
用于存储数据,均为类模板。
在 STL 中提供不同类型的容器满足不同需求。
算法(Algorithms)
提供对容器中数据进行操作的模板函数,如排序、查找、替换等。
算法具有独立性,可以用于任意符合要求的容器。
迭代器(Iterators)
用于访问容器元素的对象,行为类似指针。
容器通过迭代器暴露内部结构,算法通过迭代器对容器进行操作。
适配器(Adapters)
适配器是一种设计模式,用于改变已有类或函数对象的接口形式。
容器适配器包括:
stack
queue
priority_queue
函数适配器包括:
bind
not1 / not2
ptr_fun(旧标准)
分配器(Allocators)
STL 的内存分配器,用于负责内存申请与释放。
默认情况下使用标准分配器 allocator。
函数对象(Functor)
重载 operator() 的类对象,表现为可调用对象。
示例:
struct MyFunc
{
void operator()(int val)
{
cout << val << endl;
}
};
函数对象可用于算法参数,提高扩展性。
STL 的设计理念
泛型编程(Generic Programming)
算法与容器分离
接口一致、行为统一
自由组合容器与算法
高度复用性
通过模板,将类型与操作分离,实现类型独立的算法。
程序员不再关心类型细节,而关注算法逻辑和容器选择。
STL 使用示例(最基础示例)
最常见的示例:使用 vector 容器并使用遍历算法 for_each
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
void print(int val)
{
cout << val << endl;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for_each(v.begin(), v.end(), print);
return 0;
}
以上示例展示了:
vector 存储
迭代器 begin() / end()
算法 for_each
函数对象(函数指针也可以视为简化形式)
这是最典型的 STL 用法结构。
小结
STL 是 C++ 标准库的核心组成部分
STL 三大模块:容器、算法、迭代器
STL 六大组件:容器、算法、迭代器、适配器、分配器、函数对象
算法与容器解耦
迭代器作为中介连接算法与容器
通过模板实现高度通用和可复用的代码
序列式容器(Sequence Containers)
序列式容器强调元素的顺序,容器中的元素按照特定的顺序依次排列。
常见的序列式容器包括:
vector
deque
list
string
stack(适配器)
queue(适配器)
priority_queue(适配器)
下面开始逐个容器展开。
vector 容器
vector 是 STL 中最常用的顺序容器,相当于动态数组。
具备随机访问能力,支持快速地在尾部插入和删除。
使用 vector 需要包含头文件:
#include <vector>
vector 的构造方式
vector 的常用构造函数:
默认构造
vector<int> v;
指定大小构造
vector<int> v(10);
指定大小和初始值
vector<int> v(10, 100);
区间构造(支持任意迭代器区间)
vector<int> v2(v.begin(), v.end());
拷贝构造
vector<int> v2(v1);
vector 的赋值操作
直接赋值
vector<int> v2 = v1;
assign 区间赋值
v2.assign(v1.begin(), v1.end());
assign 重复赋值
v2.assign(10, 100);
swap 与另一容器交换
v.swap(v2);
vector 的容量与大小
常用成员函数:
size() 获取元素个数
capacity() 获取容量
empty() 判断是否为空
resize(n) 重新设置大小,多余部分删除,不足部分以默认值填充
resize(n, value) 设置大小且指定填充值
reserve(n) 预留容量,避免多次扩容
示例:
vector<int> v;
for(int i = 0; i < 10; i++)
{
v.push_back(i);
}
cout << v.size() << endl;
cout << v.capacity() << endl;
v.reserve(1000);
reserve 在大量插入前使用,可显著减少扩容次数。
vector 插入和删除
尾部插入
v.push_back(100);
尾部删除
v.pop_back();
指定位置插入
v.insert(v.begin(), 100);
删除指定位置
v.erase(v.begin());
清空全部元素
v.clear();
注意:
clear 只清空元素,不释放容量
要释放容量可使用 swap 技巧:
vector<int>(v).swap(v);
vector 数据存取
使用下标访问
v[0];
使用 at 访问(安全)
v.at(0);
访问第一个元素
v.front();
访问最后一个元素
v.back();
vector 的迭代器遍历
迭代器遍历:
for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
{
cout << *it << endl;
}
范围 for 循环(C++11)
for (int x : v)
{
cout << x << endl;
}
vector 的注意事项
vector 连续存储,类似数组
随机访问速度快
在中间位置插入和删除速度慢(需要移动大量元素)
扩容时会重新分配内存并复制旧数据
迭代器在扩容后会失效
reserve 可以减少扩容次数
deque 容器
deque 是双端数组,可以在头部和尾部快速插入删除。
包含头文件:
#include <deque>
相比 vector:
支持头部插入删除,效率高
不保证所有元素连续存储(不是严格的数组结构)
随机访问速度虽然不如 vector,但也接近 O(1)
适合两端操作频繁的场景
deque 构造方式
默认构造
deque<int> d;
指定大小
deque<int> d(10);
指定大小和初值
deque<int> d(10, 100);
区间构造
deque<int> d2(d.begin(), d.end());
拷贝构造
deque<int> d2(d1);
deque 插入和删除
头部插入
d.push_front(100);
尾部插入
d.push_back(200);
头部删除
d.pop_front();
尾部删除
d.pop_back();
任意位置插入
d.insert(d.begin(), 50);
任意位置删除
d.erase(d.begin());
清空
d.clear();
注意:
deque 不像 vector 那样提供 capacity(),因为内部结构更复杂。
deque 数据存取
下标访问
d[0];
at 安全访问
d.at(0);
前后元素访问
d.front();
d.back();
deque 遍历
迭代器遍历:
for (deque<int>::iterator it = d.begin(); it != d.end(); it++)
{
cout << *it << endl;
}
范围 for:
for (int x : d)
{
cout << x << endl;
}
deque 的注意事项
可以头尾高效插入删除
不支持 reserve
随机访问能力比 list 强,比 vector 稍弱
适合频繁在两端操作的需求
内部结构是“分段连续”
list 容器(双向链表)
list 是 STL 的双向链表。
头文件:
#include <list>
链表特性:
不支持随机访问
任何位置插入删除都接近 O(1)
适合频繁插入删除的场景
每个节点存储数据 + 前后指针
占用内存比数组多
list 构造方式
默认构造
list<int> l;
指定大小
list<int> l(10);
指定大小和初值
list<int> l(10, 100);
区间构造
list<int> l2(l.begin(), l.end());
拷贝构造
list<int> l2(l1);
list 插入和删除
头尾插入删除:
l.push_back();
l.push_front();
l.pop_back();
l.pop_front();
指定位置插入:
l.insert(it, value);
删除指定位置:
l.erase(it);
删除某个数值:
l.remove(value);
清空:
l.clear();
list 遍历
迭代器遍历:
for (list<int>::iterator it = l.begin(); it != l.end(); it++)
{
cout << *it << endl;
}
注意:
list 不支持 it + 1
只能“逐节点”移动
list 的排序和反转
list 提供专属 sort 和 reverse 函数:
l.sort();
l.reverse();
list 不支持 STL 的 sort 算法,因为 sort 需要随机访问迭代器,而 list 是双向迭代器。
list 注意事项
不支持随机访问
任何位置插入删除效率高
占用内存较多
自带 sort 和 reverse
使用 remove 可以直接删除匹配值
适用于频繁插入删除的场景
string 容器
string 是 STL 的字符串处理工具,比 C 字符数组功能更丰富。
包含头文件:
#include <string>
支持动态伸缩
支持多种操作,如拼接、查找、替换、插入、删除等
下面列出常用操作。
string 构造方式
string s1; // 空字符串
string s2("hello"); // 直接初始化
string s3(s2); // 拷贝构造
string s4(10, 'a'); // 10 个 'a'
string 拼接
s += "world";
s.append("...");
string 查找
s.find("abc");
s.rfind("abc");
找不到返回 string::npos。
string 替换
s.replace(pos, len, "new content");
string 插入和删除
s.insert(pos, "xxx");
s.erase(pos, len);
string 子串
string sub = s.substr(pos, len);
string 注意事项
与 vector 类似但专门用于字符处理
支持动态增长
可以直接使用下标访问
提供功能丰富的字符串操作接口
适配器容器(stack / queue / priority_queue)
适配器通过隐藏底层容器接口,让容器表现出“特定结构”。
stack(栈)
先进后出结构。
默认底层为 deque。
包含:
#include <stack>
基本操作:
s.push();
s.top();
s.pop();
s.empty();
s.size();
queue(队列)
先进先出结构,底层也为 deque。
包含:
#include <queue>
操作:
q.push();
q.front();
q.back();
q.pop();
q.empty();
q.size();
priority_queue(优先级队列)
默认大顶堆:
priority_queue<int> pq;
小顶堆写法:
priority_queue<int, vector<int>, greater<int>> pq;
操作:
pq.push();
pq.top();
pq.pop();
关联式容器(Associative Containers)
关联式容器的特点是:元素在插入时自动排序,不再像序列式容器那样维护物理顺序。
核心机制:
内部采用平衡二叉树(红黑树)实现
查找、插入、删除都是对数时间复杂度
自动排序且不允许用户指定排序方式(默认按键值升序)
典型关联式容器:
set
multiset
map
multimap
这些容器都依赖键值(key)进行排序与索引操作。
set 容器
set 中的元素具有以下特点:
所有元素自动排序
元素不允许重复
底层是红黑树
头文件:
#include <set>
set 构造与赋值
默认构造
set<int> s;
区间构造
set<int> s2(s.begin(), s.end());
拷贝构造
set<int> s3(s2);
赋值
set<int> s4;
s4 = s3;
set 插入与删除
插入元素
s.insert(10);
s.insert(20);
删除元素
s.erase(20);
按迭代器删除
s.erase(s.begin());
清空
s.clear();
set 的 insert 返回值:
pair<iterator,bool> ret = s.insert(10);
if(ret.second)
{
cout << "插入成功";
}
bool 表示是否插入成功(重复元素会插入失败)。
set 遍历
迭代器访问:
for (set<int>::iterator it = s.begin(); it != s.end(); it++)
{
cout << *it << endl;
}
set 查找与统计
查找:
set<int>::iterator it = s.find(10);
统计数量(set 中只有 0 或 1):
s.count(10); // 返回 0 或 1
set 与排序规则(仿函数)
默认按 < 升序
若需要自定义规则,可使用仿函数(函数对象)
自定义降序示例:
struct MyCompare
{
bool operator()(const int& a, const int& b) const
{
return a > b;
}
};
set<int, MyCompare> s;
此 set 将按照降序排序。
multiset 容器
multiset 与 set 的区别在于:
允许键值重复
插入重复元素不会失败
其他接口完全一致
示例:
multiset<int> ms;
ms.insert(10);
ms.insert(10);
count 返回重复数量:
ms.count(10); // 如 2
注意:
multiset 的 insert 永远不会失败,不需要 pair 判断。
map 容器
map 是键值对容器,每个元素是一个 pair:
pair<key, value>
特点:
键(key)唯一
按 key 自动排序
可通过 key 快速访问 value
底层为红黑树
头文件:
#include <map>
map 构造与赋值
默认构造
map<int, string> m;
插入方式:
方式一:
m.insert(pair<int,string>(1, "one"));
方式二:
m.insert(make_pair(2, "two"));
方式三(更简洁):
m[3] = "three";
注意:
使用 m[key] 访问,如果 key 不存在,会自动创建该键并赋默认值。
例如:
m[100]; // 若不存在,会创建,并填入 ""
不是所有容器都支持中括号访问,map 支持。
map 查找与统计
find 查找键:
map<int,string>::iterator it = m.find(1);
count 获取键数量(map 中只能是 0 或 1):
m.count(1);
map 删除
删除指定键:
m.erase(1);
删除指定位置:
m.erase(m.begin());
清空:
m.clear();
map 遍历
通过迭代器访问 pair:
for (map<int,string>::iterator it = m.begin(); it != m.end(); it++)
{
cout << it->first << " " << it->second << endl;
}
first 是 key,second 是 value。
map 自定义排序(仿函数)
map 支持自定义排序,原则与 set 一致。
自定义降序 map:
struct MyCompare
{
bool operator()(const int& a, const int& b) const
{
return a > b;
}
};
map<int, string, MyCompare> m;
此时 map 按 key 降序排列。
multimap 容器
multimap 支持相同的 key 出现多次。
主要特点:
允许重复键
适用于一对多的场景(例如部门:员工)
插入方式与 map 相同:
multimap<int, string> mm;
mm.insert(make_pair(1, "Tom"));
mm.insert(make_pair(1, "Jack"));
遍历所有 value:
pair<multimap<int,string>::iterator, multimap<int,string>::iterator> range =
mm.equal_range(1);
for (multimap<int,string>::iterator it = range.first; it != range.second; it++)
{
cout << it->first << " " << it->second << endl;
}
equal_range 能获取某个 key 的全部对应元素区间。
关联式容器总结
主要特性:
自动按 Key 排序
底层全部是红黑树
查找、插入、删除时间复杂度为对数级
set 和 map 不允许重复
multiset 和 multimap 允许重复
可自定义排序(使用仿函数)
适用场景:
set: 需要自动排序且唯一元素
multiset: 可重复元素自动排序
map: 需要 key-value 映射
multimap: 一对多映射场景
STL 算法总览
STL 算法是基于模板的泛型算法库,主要通过头文件 <algorithm> 与 <numeric> 提供。
算法只依赖迭代器,不依赖具体容器,因此可以作用于任意支持相应迭代器的容器(vector、deque、list、set、map 等)。
常见算法类型包括:
- 遍历和访问类算法
- 查找和计数类算法
- 排序和对比类算法
- 拷贝与替换类算法
- 删除类算法
- 数值类算法
- 集合类算法
- 其他辅助算法
在使用算法时,经常会配合“函数对象(仿函数)”与“谓词”。
遍历与访问算法
常见的遍历算法包括 for_each,它是 STL 最基础的访问算法。
需要包含的头文件:
#include <algorithm>
调用形式通常为:
for_each(起始迭代器, 结束迭代器, 操作函数或函数对象);
示例:
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
void printInt(int val)
{
cout << val << endl;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for_each(v.begin(), v.end(), printInt);
return 0;
}
第三个参数既可以是普通函数,也可以是自定义的函数对象。
示例(函数对象版):
struct Print
{
void operator()(int val) const
{
cout << val << endl;
}
};
int main()
{
vector<int> v{1,2,3};
for_each(v.begin(), v.end(), Print());
}
查找与计数算法
常见查找与计数函数有:
findfind_ifcountcount_ifbinary_search
find 用于查找等于某个值的元素。find_if 用于使用谓词查找满足条件的元素。count 与 count_if 分别对应统计值和统计满足某个条件的个数。
示例:find
vector<int> v{1,2,3,4,5};
vector<int>::iterator it = find(v.begin(), v.end(), 3);
if (it != v.end())
{
cout << "找到元素:" << *it << endl;
}
else
{
cout << "未找到" << endl;
}
示例:find_if
struct GreaterThanThree
{
bool operator()(int val) const
{
return val > 3;
}
};
int main()
{
vector<int> v{1,2,3,4,5};
vector<int>::iterator it = find_if(v.begin(), v.end(), GreaterThanThree());
}
示例:count 和 count_if
int main()
{
vector<int> v{1,2,3,3,3,4};
int num = count(v.begin(), v.end(), 3); // 等于 3 的个数
struct GreaterThree
{
bool operator()(int val) const { return val > 3; }
};
int num2 = count_if(v.begin(), v.end(), GreaterThree());
}
binary_search 用于二分查找,需要序列已按升序排列:
sort(v.begin(), v.end());
bool found = binary_search(v.begin(), v.end(), 5);
排序与对比算法
常用排序相关算法有:
sortstable_sortpartial_sortnth_elementmergereverse
最常用的是 sort,默认从小到大排序,需要随机访问迭代器(例如 vector、deque)。
示例:sort 升序排序
vector<int> v{3,1,4,2,5};
sort(v.begin(), v.end());
示例:sort 自定义比较(降序)
struct MyCompare
{
bool operator()(int a, int b) const
{
return a > b;
}
};
sort(v.begin(), v.end(), MyCompare());
reverse 用于反转区间内元素:
reverse(v.begin(), v.end());
拷贝与替换算法
常用的拷贝与替换算法包括:
copyreplacereplace_ifswapswap_ranges
copy 将一个容器内元素拷贝到另一容器中:
vector<int> v{1,2,3,4,5};
vector<int> v2(5);
copy(v.begin(), v.end(), v2.begin());
replace 将区间内等于某值的元素替换为新值:
replace(v.begin(), v.end(), 3, 30); // 把所有 3 替换为 30
replace_if 使用谓词替换满足条件的值:
struct LessThree
{
bool operator()(int val) const
{
return val < 3;
}
};
replace_if(v.begin(), v.end(), LessThree(), 0); // 小于 3 的全部改为 0
swap 用于两个容器整体交换,其时间复杂度一般是常数级:
swap(v, v2);
删除类算法
常用删除相关算法有:
removeremove_ifunique
需要强调的是:
STL 算法层面的 remove 系列,并不会真正删除容器元素,只是将“未被删除的元素”移动到前段,并返回新的逻辑末尾迭代器。
容器本身大小并未改变,真正删除要配合容器的 erase 使用,这被称为“erase-remove 惯用法”。
示例:remove + erase
vector<int> v{1,2,3,3,4,5};
vector<int>::iterator newEnd = remove(v.begin(), v.end(), 3);
// v 中的前部已经被调整为不包含 3,后部为未定义区间
v.erase(newEnd, v.end()); // 真正删除多余元素
remove_if 与谓词配合使用,删除满足条件的元素:
struct GreaterThree
{
bool operator()(int val) const
{
return val > 3;
}
};
auto newEnd = remove_if(v.begin(), v.end(), GreaterThree());
v.erase(newEnd, v.end());
unique 可以移除连续重复元素,只保留一个,通常在使用前要先 sort:
sort(v.begin(), v.end());
auto newEnd = unique(v.begin(), v.end());
v.erase(newEnd, v.end());
数值算法
数值相关算法主要集中在 <numeric> 头文件中。
常见的有:
accumulateinner_productadjacent_differencepartial_sum
accumulate 用于对区间做累加:
#include <numeric>
vector<int> v{1,2,3,4,5};
int sum = accumulate(v.begin(), v.end(), 0); // 0 是初始值
inner_product 用于两个区间的内积:
int result = inner_product(v1.begin(), v1.end(), v2.begin(), 0);
partial_sum 计算前缀和:
vector<int> v{1,2,3,4};
vector<int> out(4);
partial_sum(v.begin(), v.end(), out.begin());
// out 得到 {1,3,6,10}
集合算法
针对有序区间的集合运算算法包括:
set_unionset_intersectionset_difference
注意,这里的 set 是“集合意义”,不要求必须是 set 容器,但要求迭代区间已按升序排列。
示例:
vector<int> v1{1,2,3};
vector<int> v2{2,3,4};
vector<int> vOut;
vOut.resize(v1.size() + v2.size());
auto itEnd = set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), vOut.begin());
vOut.erase(itEnd, vOut.end());
set_intersection 计算交集,set_difference 计算差集,使用方式类似。
函数对象与谓词
函数对象(仿函数)是重载 operator() 的类对象。
谓词就是返回 bool 的函数对象,可分为一元谓词与二元谓词。
示例:一元谓词
struct IsGreaterThanThree
{
bool operator()(int val) const
{
return val > 3;
}
};
可以用于 find_if、count_if、replace_if、remove_if 等。
示例:二元谓词(用于排序)
struct MyCompare
{
bool operator()(int a, int b) const
{
return a > b;
}
};
sort(v.begin(), v.end(), MyCompare());
函数对象可以具有状态:
struct Sum
{
int total;
Sum() : total(0) {}
void operator()(int val)
{
total += val;
}
};
int main()
{
vector<int> v{1,2,3,4};
Sum s = for_each(v.begin(), v.end(), Sum());
cout << s.total << endl; // 得到累加结果
}
这是典型的“状态函数对象”使用方式。
算法与容器的配合使用注意事项
使用算法前要清楚容器迭代器类型:
sort需要随机访问迭代器,不能用于listlist有自己成员函数list::sortremove算法不会改变容器大小,要配合erase使用- 算法不会改变容器的容量,只改变元素内容和顺序
- 算法主要在
<algorithm>和<numeric>中定义
在设计程序时,推荐统一使用 STL 算法而不是手写循环,这可以让代码更简洁、更易读。