C++核心编程

以下是c++核心编程部分

本页的pdf版点击下载:https://kblog.ink?download=204&tmstv=1764307479

C++ 内存分区模型(Memory Partition Model)

C++ 程序在运行时,内存会被系统划分成若干区域,不同区域负责存放不同的数据。

掌握内存分区对理解:

  • 变量的生命周期
  • 指针
  • new / delete
  • 全局变量与局部变量
  • 类对象的行为
  • 堆区泄漏

等内容非常关键。


C++ 程序内存的四大区域

根据 PDF 内容,C++ 程序运行时,主要的内存区域包括:

  1. 代码区(Code Segment)
  2. 全局区 / 静态区(Global / Static Segment)
  3. 栈区(Stack)
  4. 堆区(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++ 中,堆区是程序员手动管理的区域,通过 newdelete 对内存进行控制。


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 的区别

对比点newmalloc
语言C++C
类型检查有类型无类型(void*)
构造函数会调用不会调用
内存释放deletefree
是否可重载可重载不可重载
初始化可初始化不自动初始化

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继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
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>
返回值类型 函数名(参数列表)
{
    函数体
}

关键字可以使用typenameclass

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> 提供。
算法只依赖迭代器,不依赖具体容器,因此可以作用于任意支持相应迭代器的容器(vectordequelistsetmap 等)。

常见算法类型包括:

  • 遍历和访问类算法
  • 查找和计数类算法
  • 排序和对比类算法
  • 拷贝与替换类算法
  • 删除类算法
  • 数值类算法
  • 集合类算法
  • 其他辅助算法

在使用算法时,经常会配合“函数对象(仿函数)”与“谓词”。


遍历与访问算法

常见的遍历算法包括 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());
}

查找与计数算法

常见查找与计数函数有:

find
find_if
count
count_if
binary_search

find 用于查找等于某个值的元素。
find_if 用于使用谓词查找满足条件的元素。
countcount_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);

排序与对比算法

常用排序相关算法有:

sort
stable_sort
partial_sort
nth_element
merge
reverse

最常用的是 sort,默认从小到大排序,需要随机访问迭代器(例如 vectordeque)。

示例: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());

拷贝与替换算法

常用的拷贝与替换算法包括:

copy
replace
replace_if
swap
swap_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);

删除类算法

常用删除相关算法有:

remove
remove_if
unique

需要强调的是:
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> 头文件中。
常见的有:

accumulate
inner_product
adjacent_difference
partial_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_union
set_intersection
set_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_ifcount_ifreplace_ifremove_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 需要随机访问迭代器,不能用于 list
  • list 有自己成员函数 list::sort
  • remove 算法不会改变容器大小,要配合 erase 使用
  • 算法不会改变容器的容量,只改变元素内容和顺序
  • 算法主要在 <algorithm><numeric> 中定义

在设计程序时,推荐统一使用 STL 算法而不是手写循环,这可以让代码更简洁、更易读。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇