嵌入式Qt开发C++核心编程知识万字总结

C++核心编程

文章目录

1、程序的内存模型

C++程序的内存模型主要涉及到程序在运行过程中如何管理内存。C++提供了几种不同的内存区域来存储不同类型的变量和对象,这些区域包括栈(stack)、堆(heap)、全局/静态存储区(global/static storage)和常量存储区(constant storage)。

  1. 栈(Stack)

    • 栈内存由编译器自动分配和释放,其操作方式类似于数据结构中的栈。
    • 局部变量(包括非静态局部变量)、函数参数以及函数调用的返回地址等通常存储在栈上。
    • 栈内存分配是快速的,但通常空间有限。
  2. 堆(Heap)

    • 堆内存由程序员通过newdelete操作符(或在C++11及以后版本中使用智能指针)来动态分配和释放。
    • 堆内存主要用于存储那些需要动态分配大小或生命周期需要跨越多个函数调用的对象。
    • 堆内存分配相对较慢,并且需要程序员手动管理内存,否则可能导致内存泄漏或野指针等问题。
  3. 全局/静态存储区(Global/Static Storage)

    • 全局变量和静态变量(包括静态局部变量和静态全局变量)存储在这里。
    • 这些变量的生命周期与整个程序相同,从程序启动到程序结束。
    • 编译器在编译时确定这些变量的内存位置,并在程序启动时初始化它们。
  4. 常量存储区(Constant Storage)

    • 常量(包括字符串常量和其他字面常量)存储在这里。
    • 这些常量在程序生命周期内保持不变。
    • 编译器在编译时分配这些常量的内存位置。

除了上述四种主要的内存区域外,C++还提供了其他类型的内存区域,如线程局部存储(Thread-Local Storage)和动态加载库内存(用于存储动态加载库中的代码和数据)。

此外,C++11及以后版本还引入了右值引用和移动语义等特性,这些特性允许程序员更加高效地管理和使用内存。例如,通过使用移动语义,程序员可以避免不必要的拷贝操作,从而提高程序的性能和效率。

2、函数高级

1.函数的默认参数

在C++中,可以为函数参数提供默认值,这允许在调用函数时省略这些参数。如果调用者没有为具有默认值的参数提供值,那么将使用默认值。

以下是一个简单的示例,演示了如何在C++中为函数参数设置默认值:

cpp 复制代码
#include <iostream>

// 声明一个带有默认参数的函数
void greet(std::string name = "World", int times = 1) {
    for (int i = 0; i < times; ++i) {
        std::cout << "Hello, " << name << "!" << std::endl;
    }
}

int main() {
    // 使用默认参数调用函数
    greet();  // 输出 "Hello, World!" 一次

    // 为第一个参数提供值,但使用第二个参数的默认值
    greet("Alice");  // 输出 "Hello, Alice!" 一次

    // 为两个参数都提供值
    greet("Bob", 3);  // 输出 "Hello, Bob!" 三次

    return 0;
}

在上面的示例中,greet 函数有两个参数:nametimesname 的默认值为 "World",times 的默认值为 1。在 main 函数中,我们展示了如何以各种方式调用 greet 函数,包括使用默认参数、只为一个参数提供值以及为两个参数都提供值。其中如果我们自己传入参数,则使用传入的参数。

需要注意的是,函数的参数列表中有默认值的参数必须出现在参数列表的末尾。同时某个形参有默认值后,其后面的参数必须有默认值。

cpp 复制代码
int fun(int a, int b = 10,int c = 20)
{
    return a;
}

另外函数在声明的时候有了默认值,则函数实现时则不能有默认值。默认值只能出现在声明和实现其中之一

cpp 复制代码
void fun(int a , int b = 10);


void fun(int a , int b =10)
{
    
    return 0;
}

//或

void fun(int a , int b);


void fun(int a , int b)
{
    
    return 0;
}

2.函数的占位参数

函数的形参列表可以有占位参数,但是在使用函数时,必须有实参传入。另外占位参数可以有默认值,当有默认值时,函数调用可以不传入实参。

cpp 复制代码
#include "iostream"
using namespace std;

int sum(int a, int b, int c , int){
    return a + b + c;
}

int sum2(int a, int b, int c , int  = 0){
    return a + b + c;
}

int main(){

    cout << sum(1, 2, 3, 4) << endl;
    cout << sum2(1, 2, 3) << endl;
    return 0;
}

3.函数重载

1.基本语法

在C++中,函数重载(Function Overloading)允许你定义多个具有相同名称但参数列表不同的函数。编译器根据调用时提供的参数类型和数量来确定应该调用哪个函数。这是一种多态性的体现,使得代码更加灵活和易于理解。

函数重载的几个要点:

  1. 函数名称必须相同:重载函数的名称必须完全一样。
  2. 参数列表必须不同:这包括参数的类型、数量或顺序(对于某些类型)。
  3. 返回类型可以不同:但返回类型本身不能作为区分重载函数的依据。
  4. 作用域必须相同:重载函数必须在同一作用域内(如同一个类内部或全局作用域)。

下面是一个简单的函数重载示例:

cpp 复制代码
#include <iostream>
using namespace std;

// 第一个版本:没有参数
void printMessage() {
    cout << "Hello, World!" << endl;
}

// 第二个版本:接受一个字符串参数
void printMessage(const string& message) {
    cout << message << endl;
}

// 第三个版本:接受两个整数参数
void printMessage(int x, int y) {
    cout << "The sum of " << x << " and " << y << " is " << (x + y) << endl;
}

int main() {
    printMessage();                  // 调用第一个版本
    printMessage("Hello, C++!");     // 调用第二个版本
    printMessage(5, 3);              // 调用第三个版本
    return 0;
}

在上面的例子中,printMessage 被重载了三次,每次的参数列表都不同。在main函数中,根据提供的参数,编译器可以确定应该调用哪个版本的printMessage函数。

注意,函数重载是通过参数的静态类型(即声明时的类型)来区分的,而不是通过参数的动态类型(即运行时实际存储的类型)。此外,对于引用和指针参数,重载也是基于引用或指针所指向的对象的静态类型来区分的。

返回类型可以不同:但返回类型本身不能作为区分重载函数的依据。即下面的示例是不被允许的。

cpp 复制代码
// 第二个版本:接受一个字符串参数
void printMessage(int x, int y) {
    cout << "The sum of " << x << " and " << y << " is " << (x + y) << endl;
}

// 第三个版本:接受两个整数参数
int printMessage(int x, int y) {
    cout << "The sum of " << x << " and " << y << " is " << (x + y) << endl;
}
2.注意事项

引用也可以作为函数重载的条件,但在调用时,第一个函数由于传入的是变量所以调用void printMessage(int &a),这是合法的语法,当传入为常量是则调用void printMessage(const int &a)

cpp 复制代码
#include "iostream"
using namespace std;

void printMessage(int &a) {
    cout << "int" << endl;
}

void printMessage(const int &a) {
    cout << "const int" << endl;
}

int main() {
    int a = 1;
    printMessage(a);
    printMessage(1);

    return 0;
}

如果函数重载的函数有默认条件,语法合法,但在调用时,不能让编译器产生歧义。

cpp 复制代码
void func (int a , int b = 10) {
    cout << "int int" << endl;
}
void func (int a) {
    cout << "int const int" << endl;
}

调用函数时,如果传入一个参数会让编译器产生歧义。

cpp 复制代码
func(10);//这种调用是C++不允许的,应避免重载时使用默认参数。
func(10,10);//合法调用,但不建议重载函数使用默认值。

3、类和对象

1.类

1.类的组成

一个类通常由属性(也叫成员属性)和行为(成员函数、成员方法)组成,类有不同的访问权限(公共、保护和私有),假设现在有一个学生类Student:

cpp 复制代码
//
// Created by 86189 on 2024/6/1.
//
#include "iostream"
using namespace std;


class Student{
public:
    string name;
    int grade{};

    void printMessage() const{
        cout<<"姓名:"<<name<<" 年级:"<<grade<<endl;
    }
};


int main(){
    Student stu;
    stu.name="张三";
    stu.grade=21107123;
    stu.printMessage();
    return 0;
}

Student stu称为创建一个对象。

2.类的访问权限

公共权限:即类内可以访问,类外也可访问。

保护权限:即类内可以访问,类外不可访问。

私有权限:即类内可以访问,类外不可访问。

cpp 复制代码
#include "iostream"
using namespace std;

class Student{
public:
    int sex;
protected:
    int age;
private:
    int height;
};


int main(){
    Student s{};
    s.sex = 1;
//    s.age = 18;
//    s.height = 180;
    return 0;
}

类内:class内。

从上边示例可以看到,在类外无法访问保护和私有权限的成员属性。但在类内可以访问:

cpp 复制代码
#include "iostream"
using namespace std;

class Student{
public:
    int sex;
protected:
    int age;
private:
    int height;
    void print(){
        cout << sex << endl;
        cout << age << endl;
    }
};


int main(){
    Student s{};
    s.sex = 1;
//    s.age = 18;
//    s.height = 180;
//    s.print();
    return 0;
}

在实际应用中,通常设置私有的成员属性,再通过共有的成员函数进行操作,实现数据的可读可写,只读等等操作,同时也可以实现数据的效验。

cpp 复制代码
//
// Created by 86189 on 2024/6/2.
//
#include "iostream"
using namespace std;

class Person{
private:
    string name; // 姓名,私有成员,不在类的外部直接访问
    int age = 18; // 年龄,初始化为18
    int weight{}; // 体重,初始化为空值
    int height{}; // 身高,初始化为空值
    int id{}; // ID,初始化为空值

public:
    // 获取ID值
    // 如果ID小于0,则设置为0;如果大于10亿,则设置为10亿
    // 这样做是为了确保ID的合理性和范围限制
    int getId(){
        if(id < 0){
            id = 0;
        }
        if (id > 1000000000)
        {
            id = 1000000000;
        }
        return id;
    }
    
    // 设置ID值
    // 参数p_id: 新的ID值
    void setId(int p_id){
        id = p_id;
    }
    
    // 设置年龄
    // 参数P_age: 新的年龄值
    // 返回值: 设置后的年龄
    // 如果年龄小于0,则设置为0;如果大于150,则设置为150
    // 这样做是为了确保年龄的合理性和范围限制
    int setAge(int P_age){
        if(age < 0){
            age = 0;
        }
        if (age > 150)
        {
            age = 150;
        }
        age = P_age;
        return age;
    }
};

int main(){
    Person p; // 创建Person对象
    p.setId(1); // 设置对象的ID为1
    cout << p.getId() << endl; // 输出ID值
    cout << p.setAge(15) << endl; // 设置年龄为15并输出
    return 0;
}
3.class和struct的区别

在C++中,classstruct都可以用来定义一种新的数据类型,但它们在默认情况下和在某些其他细节上有一些重要的区别。以下是它们之间的主要区别:

  1. 默认访问权限

    • struct中的成员默认是public的,即可以从任何地方访问。
    • class中的成员默认是private的,即只能在类的内部被访问,除非在定义时特别声明为publicprotected
  2. 用途

    • 尽管structclass在C++中可以用于定义几乎相同的数据结构,但传统上,struct更多地被用作数据的集合(即数据结构),而class则用于定义具有某些操作的对象(即封装数据和方法的对象)。
  3. 继承

    • 两者都可以用于实现继承,但由于class的默认访问权限是private,因此当使用class进行继承时,派生类无法直接访问基类的私有成员。而struct由于默认访问权限是public,因此派生类可以直接访问基类的公有成员。
  4. C和C++中的差异

    • 在C语言中,struct是唯一的用户定义数据类型,并且它只能包含数据成员(即变量)。
    • 在C++中,structclass都可以包含数据成员和成员函数(即方法)。但如上所述,它们之间的默认访问权限不同。
  5. 其他C++特性

    • 在C++中,你还可以为structclass添加其他特性,如模板、构造函数、析构函数、操作符重载、继承、友元等。这些特性在structclass之间没有区别。
  6. 语法糖

    • 从语法的角度来看,classstruct是C++的"语法糖"。也就是说,除了默认访问权限不同外,它们在语法上几乎是相同的。
  7. 命名约定

    • 虽然这不是语言规则,但在某些编程环境中,程序员可能会根据命名约定来使用classstruct。例如,你可能会看到struct更多地用于定义简单的数据结构,如点(包含x和y坐标)或矩形(包含宽度和高度),而class则用于定义更复杂的对象,如汽车或人。

classstruct在C++中的主要区别在于它们的默认访问权限和可能的命名约定。但在功能上,它们几乎是一样的。

在C++中,构造函数和析构函数是特殊的成员函数,它们在对象的生命周期中起着关键的作用。

2.构造函数(Constructor)

构造函数用于初始化对象的状态。当创建类的对象时,构造函数会被自动调用。构造函数的名字与类名相同,并且没有返回类型(包括void)。

1.示例
cpp 复制代码
class MyClass {
public:
    int x;

    // 构造函数
    MyClass(int val) : x(val) {} // 初始化列表用于初始化成员变量

    // 其他成员函数...
};

int main() {
    MyClass obj(10); // 调用构造函数,创建MyClass对象,并初始化x为10
    // ...
    return 0;
}
2.特点
  1. 构造函数可以有参数,也可以没有参数(即默认构造函数)。
  2. 构造函数可以被重载,以接受不同类型的参数或不同数量的参数。
  3. 如果不提供构造函数,编译器会生成一个默认构造函数(默认构造函数也可能被其他用户定义的构造函数所抑制)。
  4. 构造函数不能是虚函数,因为它们在运行时绑定到对象的实际类型,而不是指向对象的指针或引用的类型。

3.析构函数(Destructor)

析构函数用于释放对象使用的资源,并在对象生命周期结束时执行清理操作。析构函数的名字是类名前加上波浪号(~),也没有返回类型(包括void)。

1.示例
cpp 复制代码
class MyClass {
public:
    int* ptr;

    MyClass(int val) {
        ptr = new int(val); // 分配内存
    }

    // 析构函数
    ~MyClass() {
        delete ptr; // 释放内存
    }

    // 其他成员函数...
};

int main() {
    MyClass obj(10); // 调用构造函数
    // ...
    // 当obj离开作用域时,析构函数会自动被调用,释放ptr指向的内存
    return 0;
}
2.特点
  1. 每个类只能有一个析构函数。
  2. 析构函数没有参数,也不能被重载。
  3. 析构函数可以是虚函数,这在处理多态和基类指针指向派生类对象时非常有用(以避免对象切片和资源泄露)。
  4. 当对象离开其作用域时(例如,函数结束时),或者对象被删除(对于动态分配的对象)时,析构函数会自动被调用。

构造函数和析构函数是C++面向对象编程中非常重要的概念,它们分别用于初始化对象和清理对象使用的资源。

在C++中,构造函数可以根据其参数和用途进行分类,并且它们的调用时机和方式也有所不同。

4.构造函数的分类

1.默认构造函数
  • 无参数的构造函数。
  • 如果类中未定义任何构造函数,编译器会自动生成一个默认构造函数(但如果有其他用户定义的构造函数,编译器不会自动生成默认构造函数)。
  • 示例:MyClass() {}
2.带参数的构造函数
  • 接受一个或多个参数的构造函数。
  • 可以根据需求定义多个带参数的构造函数,实现构造函数的重载。
  • 示例:MyClass(int val) : x(val) {}
3.拷贝构造函数
  • 用于创建一个与现有对象具有相同值的新对象。
  • 通常接受一个指向同类型对象的常量引用作为参数。
  • 如果类中没有定义复制构造函数,编译器会提供一个默认的拷贝构造函数。
  • 示例:MyClass(const MyClass& other) { /* 复制操作 */ }
4.默认构造函数和带参数的构造函数
cpp 复制代码
#include <iostream>

class MyClass {
public:
    int value;

    // 默认构造函数
    MyClass() : value(0) {
        std::cout << "Default constructor called." << std::endl;
    }

    // 带参数的构造函数
    MyClass(int val) : value(val) {
        std::cout << "Parameterized constructor called with value: " << value << std::endl;
    }
};

int main() {
    MyClass obj1; // 调用默认构造函数
    MyClass obj2(10); // 调用带参数的构造函数

    return 0;
}
5.拷贝构造函数
cpp 复制代码
#include <iostream>

class MyClass {
public:
    int value;

    MyClass(int val) : value(val) {
        std::cout << "Parameterized constructor called." << std::endl;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) : value(other.value) {
        std::cout << "Copy constructor called." << std::endl;
    }
};

int main() {
    MyClass obj1(10); // 调用带参数的构造函数
    MyClass obj2 = obj1; // 调用拷贝构造函数

    // 使用初始化列表也可以调用拷贝构造函数
    MyClass obj3(obj1); // 调用拷贝构造函数

    return 0;
}

5.构造函数的调用

  1. 隐式调用
    • 当创建类的对象时,构造函数会被隐式调用。
    • 示例:MyClass obj; // 调用默认构造函数
    • MyClass obj2(10); // 调用带参数的构造函数
  2. 显式调用
    • 在某些情况下,可以显式调用构造函数来创建临时对象,但这并不常见。
    • 示例:MyClass temp = MyClass(10); // 虽然这里实际上发生了拷贝构造函数或移动构造函数的调用,但MyClass(10)显式调用了带参数的构造函数
  3. 复制初始化与直接初始化
    • 在C++中,可以使用复制初始化或直接初始化来创建对象。
    • 复制初始化使用等号(=),而直接初始化不使用。
    • 示例:MyClass obj1 = 10; // 复制初始化,可能会调用构造函数进行类型转换
    • MyClass obj2(10); // 直接初始化,调用带参数的构造函数

构造函数的调用时机和方式可能受到编译器优化、右值引用和移动语义、C++版本等多种因素的影响。

cpp 复制代码
void test()
{
    //调用无参构造函数
    myClass obj;  // 调用无参构造函数不能加括号
    //调用有参的构造函数
    //括号法
     myClass obj(10);
    //显式调用
    MyClass temp = MyClass(10); //myClass(10)单独调用则为匿名对象,调用后立马析构
    //隐式调用
    MyClass obj1 = 10;  // MyClass obj1 = MyClass(10);
    MyClass obj1 = obj2; //MyClass obj1 = MyClass(obj2);
    
    //不要用拷贝构造函数初始化匿名对象
    MyClass(obj1); //编译器会认为是变量的声明
}
1.构造函数的调用时机
  1. 对象通过值传递:当一个对象被作为参数通过值传递给函数时,会调用拷贝构造函数来创建一个新的对象副本。
cpp 复制代码
class MyClass {
public:
    MyClass(const MyClass& other) {
        // 复制构造函数的实现
    }
};

void foo(MyClass obj) {
    // 在这里,MyClass 的对象是通过值传递的,因此会调用拷贝构造函数
}

MyClass obj;
foo(obj); // 调用拷贝构造函数
  1. 对象从函数返回时:当函数返回一个对象(不是引用或指针)时,会调用拷贝构造函数来创建一个返回值的副本。
cpp 复制代码
MyClass foo() {
    MyClass obj;
    // ...
    return obj; // 调用拷贝构造函数来创建返回值的副本
}

MyClass obj = foo(); // 调用拷贝构造函数

注意:在现代 C++ 中,对于支持移动语义的类型,编译器可能会选择使用移动构造函数(Move Constructor)而不是拷贝构造函数,以提高性能。

  1. 对象被初始化时:当使用已存在的对象来初始化一个新对象时,会调用拷贝构造函数。
cpp 复制代码
MyClass obj1;
MyClass obj2(obj1); // 显式地调用复制构造函数

MyClass obj3 = obj1; // 使用拷贝初始化,也会调用拷贝构造函数
  1. 在构造函数中初始化成员变量:如果类的成员变量是另一个类的对象,并且该成员变量在初始化列表中没有被显式地初始化,那么当创建该类的对象时,会调用成员变量的拷贝构造函数(如果成员变量是通过值传递的)。
cpp 复制代码
class OtherClass {
    // ...
};

class MyClass {
    OtherClass oc;
public:
    MyClass(const OtherClass& oc_val) : oc(oc_val) {
        // 在这里,oc 的初始化会调用 OtherClass 的拷贝构造函数
    }
};

OtherClass oc;
MyClass my_obj(oc); // MyClass 的构造函数会调用 OtherClass 的拷贝构造函数
2.构造函数的调用规则

默认情况下,在创建一个类的时,编译器会给我们添加三个函数:构造、析构、拷贝。

构造函数的调用规则:

如果用户定义有有参构造函数,则编译器不再提供无参构造函数。但会提供拷贝构造函数。

如果用户定义有拷贝构造函数,则编译器不再提供其他普通构造函数(析构函数除外)。

示例:

关于构造函数的调用规则,您提到的部分有些不准确。我将对其进行澄清并提供示例。

首先,如果用户定义了一个或多个有参构造函数,编译器不会自动提供一个无参构造函数(默认构造函数)。但是,如果用户没有定义任何构造函数(包括有参和无参),编译器会提供一个默认的构造函数。

其次,如果用户定义了一个拷贝构造函数,编译器仍然会提供其他必要的特殊成员函数,如默认构造函数(如果用户没有提供)、析构函数和移动构造函数/移动赋值运算符(如果它们没有被显式地禁用或定义)。但是,拷贝构造函数和移动构造函数/移动赋值运算符是用户可以选择定义的,以提供自定义的拷贝和移动语义。

以下是一些示例:

示例 1:只定义有参构造函数

cpp 复制代码
class MyClass {
public:
    MyClass(int value) : m_value(value) {} // 用户定义的有参构造函数
    int m_value;
};

int main() {
    // 下面的代码会编译失败,因为没有默认构造函数
    // MyClass obj; // 错误:没有匹配的构造函数

    // 需要使用有参构造函数
    MyClass obj1(10); // 正确
    MyClass obj2(obj1); // 拷贝构造函数被调用
    return 0;
}

示例 2:定义拷贝构造函数

cpp 复制代码
class MyClass {
public:
    //MyClass(int value) : m_value(value) {} // 用户定义的有参构造函数
    MyClass(const MyClass& other) : m_value(other.m_value) {
        std::cout << "拷贝构造函数被调用" << std::endl;
    } // 用户定义的拷贝构造函数
    int m_value;
};

int main() {
     // 下面的代码会编译失败,因为没有默认和有参构造函数
     // MyClass obj; // 错误:没有匹配的构造函数
   // MyClass obj1(10);错误:没有匹配的构造函数
    MyClass obj2(obj1); // 拷贝构造函数被调用
    return 0;
}

在这个示例中,虽然用户定义了拷贝构造函数,但编译器仍然会提供一个析构函数(如果用户没有定义)。此外,如果用户没有定义移动构造函数或移动赋值运算符,编译器也会提供它们(但在某些情况下,例如当类中有被声明为delete的移动构造函数或移动赋值运算符时,编译器不会提供)。

6.深拷贝和浅拷贝

在C++中,深拷贝(deep copy)和浅拷贝(shallow copy)是对象复制时的两种主要策略。理解这两种拷贝方式的区别对于编写正确的、避免潜在问题的代码非常重要。

1.浅拷贝(Shallow Copy)

浅拷贝只是复制了对象的指针或者值(如果对象是基本数据类型的话),而不去复制对象本身。对于包含动态分配内存(如使用newmalloc分配的内存)或指向其他对象的指针的类来说,浅拷贝可能会引发问题。

例如:

cpp 复制代码
class MyClass {
public:
    int* ptr;
    MyClass(int value) {
        ptr = new int(value);
    }

    // 假设这是浅拷贝的拷贝构造函数
    MyClass(const MyClass& other) {
        ptr = other.ptr;  // 浅拷贝,只是复制了指针
    }

    ~MyClass() {
        delete ptr;
    }
};

在上面的例子中,如果创建了两个MyClass对象并使用浅拷贝的拷贝构造函数,那么两个对象都会指向同一块内存区域。当其中一个对象被销毁时,它会释放内存,而另一个对象则会尝试再次释放同一块内存,导致未定义行为(通常是程序崩溃)。

2.深拷贝(Deep Copy)

深拷贝会创建一个新的对象,并将原始对象的内容复制到新对象中。如果对象包含动态分配的内存或指向其他对象的指针,深拷贝会分配新的内存,并将原始数据复制到新内存中。

例如:

cpp 复制代码
class MyClass {
public:
    int* ptr;
    MyClass(int value) {
        ptr = new int(value);
    }

    // 这是深拷贝的拷贝构造函数
    MyClass(const MyClass& other) {
        ptr = new int(*other.ptr);  // 深拷贝,分配了新的内存并复制了数据
    }

    ~MyClass() {
        delete ptr;
    }
};

在上面的例子中,即使创建了两个MyClass对象并使用深拷贝的拷贝构造函数,每个对象也会有自己的内存区域,并且当对象被销毁时,它们会释放自己的内存,而不会互相干扰。

3.注意事项
  • 在编写包含动态分配内存或指向其他对象的指针的类的拷贝构造函数和赋值运算符时,通常需要使用深拷贝来避免潜在的问题。
  • 默认情况下,C++编译器会提供浅拷贝的拷贝构造函数和赋值运算符。如果类需要深拷贝,你需要显式地定义它们。
  • 另一个常见的策略是使用智能指针(如std::unique_ptrstd::shared_ptr)来管理动态分配的内存。智能指针可以自动处理内存释放,从而避免许多与手动内存管理相关的问题。

7.初始化列表

在C++中,初始化列表(Initializer List)是构造函数的一部分,用于初始化类的成员变量。它允许我们以特定的顺序和方式初始化成员变量,这在某些情况下是必需的,特别是当成员变量是常量、引用或没有默认构造函数的对象时。

初始化列表在构造函数的冒号(:)之后和函数体的大括号({})之前声明。下面是初始化列表的基本用法示例:

cpp 复制代码
class MyClass {
private:
    int x;
    const int y; // 常量成员变量,必须在构造函数的初始化列表中初始化
    std::string str; // 可以使用默认构造函数初始化,但也可以使用初始化列表

public:
    MyClass(int valX, int valY) : x(valX), y(valY) {
        // 初始化列表在这里
        // str 会使用默认构造函数初始化,除非在初始化列表中明确指定
    }

    MyClass(int valX, int valY, const std::string& valStr) : x(valX), y(valY), str(valStr) {
        // 初始化列表明确指定了str的初始化
    }

    // ... 其他成员函数 ...
};

在上面的例子中,MyClass有两个构造函数,它们都使用了初始化列表来初始化成员变量xy。对于str,第一个构造函数没有显式地在初始化列表中初始化它,所以它会使用std::string的默认构造函数进行初始化。第二个构造函数则在初始化列表中明确指定了str的初始化值。

使用初始化列表而不是在构造函数体内赋值有以下几个优点:

  1. 效率:对于某些类型(如基本数据类型和POD类型),初始化列表通常比赋值更快,因为它避免了不必要的拷贝或移动操作。
  2. 必要性:对于常量成员、引用成员、没有默认构造函数的类类型的成员,必须在初始化列表中初始化。
  3. 初始化顺序:成员变量是按照它们在类中声明的顺序进行初始化的,而不是按照它们在初始化列表中出现的顺序。但是,在初始化列表中明确指定初始化顺序可以使代码更清晰。
  4. 常量性:对于常量成员,初始化列表是唯一的初始化方式。

请注意,即使对于可以在构造函数体内通过赋值初始化的成员,使用初始化列表通常也是一个好习惯,因为它可以提高代码的可读性和效率。

在C++中,类的成员(包括对象成员)的构造顺序和析构顺序是确定的,并且与它们在类定义中的声明顺序有关。这确保了依赖于特定初始化顺序的成员能够正确初始化,并在析构时以适当的顺序进行清理。

8.构造顺序和析构顺序

1.构造顺序
  • 构造函数在创建对象时被调用。
  • 对象的构造顺序总是按照它们在类定义中声明的顺序进行。
  • 如果类有基类,那么基类的构造函数会首先被调用,然后是成员对象的构造函数,最后才是类本身的构造函数体(如果有的话)。
  • 如果一个类有多个基类,则基类的构造顺序是由它们在派生类列表中的声明顺序决定的,而不是它们在继承层次结构中的层次顺序。
2.析构顺序
  • 析构函数在对象生命周期结束时被调用,这通常发生在对象离开其作用域时,或者是被delete运算符显式删除时。
  • 析构顺序与构造顺序相反。首先执行类本身的析构函数体(如果有的话),然后是成员对象的析构函数,最后是基类的析构函数。
  • 如果有多个基类,则基类的析构顺序与它们在派生类列表中的声明顺序相反。
3.示例
cpp 复制代码
#include <iostream>

class Base {
public:
    Base() { std::cout << "Base constructor\n"; }
    ~Base() { std::cout << "Base destructor\n"; }
};

class Member {
public:
    Member() { std::cout << "Member constructor\n"; }
    ~Member() { std::cout << "Member destructor\n"; }
};

class Derived : public Base {
    Member m;
public:
    Derived() { std::cout << "Derived constructor\n"; }
    ~Derived() { std::cout << "Derived destructor\n"; }
};

int main() {
    Derived d;
    return 0;
}

输出将是:

Base constructor
Member constructor
Derived constructor
Derived destructor
Member destructor
Base destructor

在这个例子中,首先调用了基类Base的构造函数,然后调用了成员对象m(类型为Member)的构造函数,最后调用了派生类Derived的构造函数。在main函数结束时,析构顺序相反:首先调用Derived的析构函数,然后调用成员对象m的析构函数,最后调用基类Base的析构函数。

9.静态成员

在C++中,静态成员(包括静态数据成员和静态成员函数)是类的一部分,但与类的任何特定对象实例无关。静态成员在类的所有实例之间共享,并且可以在没有创建类对象的情况下直接通过类名进行访问。

1.静态数据成员

静态数据成员在类的所有对象之间共享一个存储位置。静态数据成员在类定义中声明,但在类定义外部初始化。它们不能在类内部进行初始化(除了用常量表达式初始化静态整型或枚举类型的静态成员)。

2.示例
cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 静态数据成员声明
    static int count;

    MyClass() {
        // 递增静态数据成员以跟踪创建的对象数量
        ++count;
    }

    ~MyClass() {
        // 递减静态数据成员(可选,但通常用于调试或统计)
        --count;
    }

    // 静态成员函数声明
    static void printCount() {
        std::cout << "Number of objects created: " << count << std::endl;
    }
};

// 静态数据成员在类定义外部初始化
int MyClass::count = 0;

int main() {
    MyClass obj1, obj2, obj3;
    MyClass::printCount(); // 输出:Number of objects created: 3

    return 0;
}

在这个例子中,MyClass有一个静态数据成员count,它用于跟踪创建的MyClass对象的数量。静态成员函数printCount用于输出count的值。注意,静态数据成员count在类定义外部初始化,并且只能初始化一次。

3.静态成员函数

静态成员函数不与类的任何对象实例关联,因此它们没有this指针。静态成员函数只能访问静态成员(包括静态数据成员和其他静态成员函数),而不能访问非静态成员。

4.示例
cpp 复制代码
class MyClass {
public:
    static void staticFunc() {
        // 只能访问静态成员
        std::cout << "This is a static function.\n";
    }

    void nonStaticFunc() {
        // 可以访问静态和非静态成员
        std::cout << "This is a non-static function.\n";
        // MyClass::staticFunc(); // 也可以这样调用静态函数
    }
};

int main() {
    MyClass::staticFunc(); // 直接通过类名调用静态函数

    MyClass obj;
    obj.nonStaticFunc(); // 通过对象实例调用非静态函数
    // obj.staticFunc(); // 错误!不能通过对象实例调用静态函数

    return 0;
}

在这个例子中,MyClass有一个静态成员函数staticFunc和一个非静态成员函数nonStaticFunc。静态函数staticFunc只能通过类名来调用,而非静态函数nonStaticFunc可以通过对象实例来调用。尝试通过对象实例来调用静态函数会导致编译错误。

5.补充

静态成员(包括静态数据成员和静态成员函数)可以在类外调用。由于静态成员与类的任何特定实例无关,因此它们可以通过类名直接访问,而无需创建类的对象。

对于静态数据成员,你可以在类外通过类名来访问或修改它。但是,静态数据成员必须在类外进行初始化,通常是在类的定义之后。

对于静态成员函数,你可以通过类名来调用它们,就像调用普通的非成员函数一样。

以下是一个示例,展示了如何在类外调用静态成员:

cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 静态数据成员声明
    static int count;

    // 静态成员函数声明
    static void printCount() {
        std::cout << "Number of objects created: " << count << std::endl;
    }

    // 构造函数,用于更新静态数据成员
    MyClass() {
        ++count;
    }

    // 析构函数,这里不用于修改count,但为了完整性列出
    ~MyClass() {
        // 在实际使用中,析构函数通常不用于修改静态成员
    }
};

// 静态数据成员在类外初始化
int MyClass::count = 0;

int main() {
    // 创建对象
    MyClass obj1;
    MyClass obj2;

    // 在类外通过类名调用静态成员函数
    MyClass::printCount(); // 输出:Number of objects created: 2

    // 在类外直接访问静态数据成员(但通常不建议这样做,除非有明确的理由)
    std::cout << "Direct access to static member: " << MyClass::count << std::endl; // 输出:Direct access to static member: 2

    return 0;
}

在这个示例中,MyClass::printCount()MyClass::count 都是在类外通过类名 MyClass 访问的静态成员。注意,尽管可以直接访问静态数据成员 MyClass::count,但在实践中,通常更推荐通过静态成员函数(如 printCount)来访问和修改它们,因为这可以提高封装性和代码的可读性。

10.成员变量和成员函数分开存储

  1. 成员变量

    • 成员变量是类的数据部分,它们存储了类的实例(对象)的状态信息。
    • 当创建一个类的实例时,会为该实例分配内存来存储其成员变量。这些变量通常存储在堆(对于动态分配的对象)或栈(对于局部或自动分配的对象)上。
    • 成员变量在内存中的存储位置是连续的(对于简单的数据类型),或者是指向实际数据的指针(对于复杂的数据类型,如字符串或动态数组)。
  2. 成员函数

    • 成员函数是类的行为部分,它们定义了对象可以执行的操作。
    • 与成员变量不同,成员函数本身并不存储在类的实例的内存中。相反,成员函数的代码(即函数体)存储在代码段(也称为文本段或文本区)中,这是程序的一部分,其中包含了程序的执行指令。
    • 当成员函数被调用时,它使用特定的机制(如this指针在C++中)来访问和修改对象的状态(即成员变量)。this指针是一个指向调用该成员函数的对象的指针,它允许成员函数知道它正在操作哪个对象。
cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 成员变量
    int myInt;
    double myDouble;

    // 成员函数
    MyClass(int i, double d) : myInt(i), myDouble(d) {} // 构造函数
    void printValues() {
        std::cout << "myInt: " << myInt << ", myDouble: " << myDouble << std::endl;
    }
};

int main() {
    // 创建MyClass的实例(对象)
    MyClass obj(10, 3.14);

    // 调用成员函数
    obj.printValues();

    return 0;
}

在这个示例中:

  • MyClass 是一个类,它有两个成员变量 myIntmyDouble,以及一个成员函数 printValues
  • main 函数中,我们创建了一个 MyClass 的实例 obj,并通过构造函数初始化了它的成员变量。
  • 当我们调用 obj.printValues() 时,成员函数 printValues 被执行。尽管我们调用的是 obj 的方法,但 printValues 函数的代码本身并不存储在 obj 所占用的内存中。相反,它的代码存储在程序的代码段中。

从内存的角度来看:

  • obj 的成员变量 myIntmyDouble 会被分配在栈上(如果 obj 是在 main 函数中局部声明的)或者在堆上(如果 obj 是通过 new 运算符动态分配的)。
  • 成员函数 printValues 的代码则存储在程序的代码段(或文本段)中,这是程序在加载到内存时就已经确定好的。

printValues 被调用时,它会通过 this 指针(尽管在上面的示例中没有显式使用)知道它正在操作哪个对象(即 obj),并可以访问和修改该对象的成员变量。

注意:空对象占用的内存空间大小为:1。非静态成员变量,属于类的对象上。静态成员变量,不属于类的对象上边。

11.this指针

1.概念

在C++中,this指针是一个隐式的指针,它指向调用成员函数的对象本身。当一个成员函数被调用时,编译器会自动将调用该函数的对象的地址赋值给this指针。this指针主要用于以下目的:

  1. 区分成员变量和局部变量 :当成员变量和函数参数或局部变量重名时,this->前缀可以用来明确指出引用的是成员变量。

  2. 返回对象本身的引用 :在成员函数中,this指针可以被返回以获取对调用对象的引用。

  3. 用于链式调用 :一些成员函数返回*this的引用,允许链式调用多个成员函数。

  4. 在构造函数中初始化其他成员 :在构造函数中,可以使用this指针来区分成员变量和构造函数参数。

下面是一个使用this指针的示例:

cpp 复制代码
#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;

public:
    Person(std::string name, int age) : name(name), age(age) {}

    // 使用this指针来区分成员变量和参数
    void setName(std::string name) {
        this->name = name; // 这里的this->name指的是类的成员变量
    }

    void setAge(int age) {
        this->age = age; // 这里的this->age也是类的成员变量
    }

    // 返回一个指向调用对象的引用,用于链式调用
    Person& printAndModifyName(const std::string& newName) {
        std::cout << "Original name: " << this->name << std::endl;
        this->name = newName;
        std::cout << "Modified name: " << this->name << std::endl;
        return *this; // 返回当前对象的引用
    }

    void printInfo() const {
        std::cout << "Name: " << this->name << ", Age: " << this->age << std::endl;
    }
};

int main() {
    Person p("Alice", 30);
    p.printInfo(); // 输出原始信息

    // 链式调用printAndModifyName和printInfo
    p.printAndModifyName("Bob").printInfo(); // 输出修改后的信息

    return 0;
}

在这个例子中,setNamesetAge成员函数使用this指针来区分成员变量和参数。printAndModifyName成员函数返回*this的引用,允许链式调用。printInfo成员函数是一个常量成员函数,它使用this指针(尽管在这个特定的例子中并不需要显式使用this,因为编译器会自动处理)来访问成员变量。

尽管this指针在成员函数中经常被隐式使用,但在某些情况下,如需要显式区分成员变量和参数,或者需要返回对象的引用以进行链式调用时,你可以使用this指针。然而,在大多数情况下,你不需要(也不应该)在代码中显式地写出this指针。

2.链式调用

链式调用(Chaining)是一种在面向对象编程中常用的技术,它允许在单个语句中连续调用同一个对象的多个方法。为了支持链式调用,这些方法通常返回对调用对象的引用(通常是*this)。

cpp 复制代码
#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;

public:
    Person(std::string name, int age) : name(name), age(age) {}

    // 设置名字,并返回对当前对象的引用以支持链式调用
    Person& setName(const std::string& newName) {
        name = newName;
        return *this; // 返回当前对象的引用
    }

    // 设置年龄,并返回对当前对象的引用以支持链式调用
    Person& setAge(int newAge) {
        age = newAge;
        return *this; // 返回当前对象的引用
    }

    // 打印信息
    void printInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Person p("Alice", 30);

    // 链式调用 setName 和 setAge 方法
    p.setName("Bob").setAge(40);

    // 打印信息
    p.printInfo(); // 输出:Name: Bob, Age: 40

    return 0;
}

在这个示例中,setNamesetAge 方法都返回 *this,即当前对象的引用。这样,我们就可以在单个语句中连续调用这两个方法,实现链式调用。最后,我们调用 printInfo 方法来打印修改后的信息。

链式调用可以使代码更加简洁和易读,特别是在需要连续设置多个属性或执行多个操作时。但是,过度使用链式调用也可能导致代码难以理解和维护,因此应该根据具体情况谨慎使用。

12.空指针调用成员函数

在C++中,尝试通过空指针(nullptrNULL)调用成员函数通常是未定义行为(Undefined Behavior, UB),但这是因为对成员函数本身的调用并不直接通过指针解引用。然而,如果成员函数内部试图访问或修改对象的成员变量(即使用this指针),并且该指针是空的,那么这会导致运行时错误或未定义行为。

成员函数并不存储在对象的内存中,而是存储在代码段中。当通过对象指针调用成员函数时,编译器实际上是在函数内部隐式地传递一个指向对象的指针(即this指针)。但是,如果传递的是一个空指针,那么在函数内部使用该指针来访问成员变量就会是危险的。

虽然很多编译器在通过空指针调用成员函数时可能不会立即报错(特别是当函数不实际使用this指针时),但这并不意味着这是安全的或可移植的。未定义行为意味着程序可能崩溃、产生不正确的结果或表现出其他不可预测的行为。

下面是一个示例,展示了通过空指针调用成员函数并尝试访问成员变量时可能发生的问题:

cpp 复制代码
#include <iostream>

class MyClass {
public:
    int value = 42;

    void printValue() {
        std::cout << "Value: " << this->value << std::endl; // 如果this是空指针,这里会崩溃
    }
};

int main() {
    MyClass* ptr = nullptr;
    ptr->printValue(); // 这是未定义行为,可能会导致程序崩溃
    return 0;
}

在这个例子中,printValue成员函数试图通过this指针访问成员变量value。但是,因为ptr是一个空指针,所以this指针也是空的,这会导致尝试访问无效的内存地址,从而可能导致程序崩溃。

为了避免这种情况,你应该始终确保在调用成员函数之前,对象指针不是空的。可以通过检查指针是否为空来避免这种错误:

cpp 复制代码
if (ptr != nullptr) {
    ptr->printValue();
} else {
    std::cout << "Pointer is null!" << std::endl;
}

12.const修饰成员函数

在C++中,使用const修饰成员函数表示该函数不会修改其所属对象的任何非静态成员变量(除非这些成员变量被声明为mutable)。这允许我们在常对象上调用这些函数,因为编译器知道这些函数不会改变对象的状态。

  1. 声明 :在成员函数的声明后添加const关键字,以表示该函数是常量成员函数。

    cpp 复制代码
    class MyClass {
    public:
        int value;
    
        // 这是一个常量成员函数
        int getValue() const { return value; }
    
        // 这是一个非常量成员函数
        void setValue(int v) { value = v; }
    };
  2. 调用:常量成员函数可以在常对象或非常对象上调用,而非常量成员函数只能在非常对象上调用。

    cpp 复制代码
    int main() {
        MyClass obj;
        obj.setValue(10); // 正确:非常对象上调用非常量成员函数
        std::cout << obj.getValue() << std::endl; // 正确:非常对象上调用常量成员函数
    
        const MyClass constObj;
        // constObj.setValue(20); // 错误:不能在常对象上调用非常量成员函数
        std::cout << constObj.getValue() << std::endl; // 正确:常对象上调用常量成员函数
    
        return 0;
    }
  3. 函数体中的限制 :在常量成员函数的函数体中,你不能修改任何非静态成员变量的值(除非它们被声明为mutable)。如果尝试这样做,编译器会报错。

    cpp 复制代码
    class MyClass {
    public:
        int value;
    
        // 错误:尝试在常量成员函数中修改非静态成员变量
        void someFunction() const {
            value = 10; // 错误:不能修改成员变量在常量成员函数中
        }
    };
  4. 重载与const成员函数 :你可以根据成员函数是否带const来重载它。这意味着你可以有一个非const版本的成员函数和一个const版本的成员函数,它们具有相同的名称和参数列表,但一个带const而另一个不带。

    cpp 复制代码
    class MyClass {
    public:
        int value;
    
        int* getValuePtr() { return &value; } // 非常量版本
        const int* getValuePtr() const { return &value; } // 常量版本
    };
  5. this指针的const版本 :在常量成员函数中,this指针的类型是指向常量对象的指针(即const MyClass* const this)。这确保了成员函数不能通过this指针来修改对象的状态。

  6. mutable关键字 :尽管const成员函数通常不会修改其所属对象的状态,但某些情况下可能需要在const成员函数中修改某个成员变量。为此,可以使用mutable关键字来修饰该成员变量,这样即使在const成员函数中也可以修改它。

    cpp 复制代码
    class MyClass {
    public:
        mutable int mutableValue; // 可以在const成员函数中修改
    
        void someConstFunction() const {
            mutableValue = 10; // 正确:可以在const成员函数中修改mutable成员变量
        }
    };

13.常对象

常对象在C++中是一个特殊的对象,它是指用const修饰符声明的对象。常对象具有以下特性和限制:

  1. 定义

    • 常对象在声明时必须进行初始化,因为它不能被修改。
    • 定义格式通常如下:const 类名 对象名; 或者 类名 const 对象名;
  2. 特性

    • 不可修改:常对象的数据成员在对象创建后不能被更新。
    • 函数调用限制 :常对象只能调用类的常成员函数(即成员函数声明中包含const关键字的函数)。这是因为非const成员函数可能会尝试修改对象的状态,而常对象是不允许修改的。
  3. 初始化

    • 常对象必须在定义时进行初始化,否则编译器会报错。
  4. 语法示例

    cpp 复制代码
    class MyClass {
    public:
        int value;
        void setValue(int v) { value = v; } // 非const成员函数
        void printValue() const { std::cout << "Value: " << value << std::endl; } // const成员函数
    };
    
    int main() {
        const MyClass obj1{42}; // 常对象,初始化时设置value为42
        // obj1.setValue(100); // 错误:不能调用非const成员函数来修改常对象
        obj1.printValue(); // 正确:可以调用const成员函数
    
        MyClass obj2{50}; // 非常对象
        obj2.setValue(100); // 正确:可以修改非常对象
        obj2.printValue(); // 正确:可以调用const成员函数
    
        return 0;
    }
  5. 注意事项

    • 当尝试在常对象上调用非const成员函数时,编译器会报错,因为这违反了常对象的不可修改性。
    • 类的常成员函数可以访问常对象和非常对象的成员(包括数据成员和成员函数),但是它们不能修改任何非静态成员变量(除非这些变量被声明为mutable)。
  6. 总结

    • 常对象是一种特殊的对象,它在整个生命周期中都是只读的,不能被修改。
    • 常对象只能调用类的常成员函数,这是为了确保对象的不可修改性。
    • 常对象在定义时必须进行初始化,并且之后不能被重新赋值。

14.友元

1.什么是友元

在C++中,友元(friend)是一种允许一个类或函数访问另一个类的非公有(private 或 protected)成员的机制。这种机制打破了类的封装性,因此在使用时需要谨慎。然而,在某些情况下,友元提供了一种方便的方式来实现特定的功能,如操作符重载、输入输出流操作等。

友元可以是另一个类、类的成员函数、或者全局函数。当一个类或函数被声明为另一个类的友元时,它就可以访问那个类的所有私有和保护成员。

2.全局函数做友元

全局函数(非类成员函数)也可以被声明为类的友元。这样做允许全局函数访问类的私有(private)和保护(protected)成员。

cpp 复制代码
#include <iostream>

class MyClass {
    // 声明全局函数 friendFunc 为友元
    friend void friendFunc(MyClass& obj);

private:
    int secretValue;

public:
    MyClass(int value) : secretValue(value) {}
};

// 全局函数,它可以访问 MyClass 的私有成员
void friendFunc(MyClass& obj) {
    std::cout << "Secret value: " << obj.secretValue << std::endl;
}

int main() {
    MyClass myObj(42);
    friendFunc(myObj);  // 输出: Secret value: 42
    return 0;
}

在这个例子中,friendFunc 是一个全局函数,它被声明为 MyClass 的友元。因此,friendFunc 可以访问 MyClass 的私有成员 secretValue。在 main 函数中,我们创建了一个 MyClass 对象 myObj,并将其传递给 friendFunc,后者成功输出了 secretValue 的值。

2.类做友元

在C++中,一个类也可以被声明为另一个类的友元。当一个类被声明为另一个类的友元时,这个友元类可以访问另一个类的私有(private)和保护(protected)成员。这种机制在某些情况下可能很有用,特别是当两个类需要紧密协作,并且一个类需要直接访问另一个类的内部数据时。

cpp 复制代码
#include <iostream>

class MyClass {
    // 声明另一个类(例如 MyFriendClass)为友元
    friend class MyFriendClass;

private:
    int secretValue;

public:
    MyClass(int value) : secretValue(value) {}

    // 提供一个公共函数以显示secretValue的值(仅用于比较)
    void displaySecretValue() const {
        std::cout << "Secret value: " << secretValue << std::endl;
    }
};

// 友元类 MyFriendClass
class MyFriendClass {
public:
    // MyFriendClass 可以访问 MyClass 的私有成员
    void printSecret(const MyClass& obj) {
        std::cout << "MyFriendClass sees: " << obj.secretValue << std::endl;
    }
};

int main() {
    MyClass myObj(42);
    MyFriendClass friendObj;

    // 通过 MyClass 的公共函数显示 secretValue
    myObj.displaySecretValue(); // 输出: Secret value: 42

    // 通过 MyFriendClass 访问 MyClass 的私有成员
    friendObj.printSecret(myObj); // 输出: MyFriendClass sees: 42

    return 0;
}

在这个例子中,MyFriendClass 被声明为 MyClass 的友元。因此,MyFriendClass 中的成员函数 printSecret 可以访问 MyClass 对象的 secretValue 成员,即使它是私有的。注意,友元关系不是双向的;即 MyClass 不是 MyFriendClass 的友元,除非另外声明。

使用类作为友元时要特别小心,因为它破坏了封装性,并可能导致代码难以维护和理解。在可能的情况下,最好使用公共接口和受保护的成员来实现类之间的协作。然而,在某些情况下,类作为友元可能是实现特定功能所必需的。

3.成员函数做友元

在C++中,成员函数本身并不直接作为另一个类的友元,因为成员函数是类的一部分,它总是能够访问其所在类的所有成员(包括私有和保护成员)。然而,你可以将一个类的成员函数声明为另一个类的友元,但实际上是将这个成员函数所属的整个类声明为友元。

但如果你想让另一个类的成员函数能够访问当前类的私有或保护成员,你应该做的是将该成员函数所在的整个类声明为当前类的友元。

cpp 复制代码
#include <iostream>

class MyClass {
    // 声明另一个类(例如 OtherClass)为友元
    friend class OtherClass;

private:
    int secretValue;

public:
    MyClass(int value) : secretValue(value) {}

    // 提供一个公共函数以显示secretValue的值(仅用于比较)
    void displaySecretValue() const {
        std::cout << "Secret value: " << secretValue << std::endl;
    }
};

class OtherClass {
public:
    // 这个成员函数可以访问 MyClass 的私有成员
    void printSecret(const MyClass& obj) {
        std::cout << "OtherClass sees: " << obj.secretValue << std::endl;
    }
};

int main() {
    MyClass myObj(42);
    OtherClass friendObj;

    // 通过 MyClass 的公共函数显示 secretValue
    myObj.displaySecretValue(); // 输出: Secret value: 42

    // 通过 OtherClass 的成员函数访问 MyClass 的私有成员
    friendObj.printSecret(myObj); // 输出: OtherClass sees: 42

    return 0;
}

在这个例子中,OtherClass 被声明为 MyClass 的友元,这意味着 OtherClass 的所有成员函数(不仅仅是 printSecret)都可以访问 MyClass 的私有成员。然而,在实际编程中,通常最好只将真正需要访问私有成员的成员函数所在的类声明为友元,以保持封装的完整性。

cpp 复制代码
//
// Created by 86189 on 2024/6/6.
//
#include "iostream"
using namespace std;

class myClassFriend;
class myClass{
public:
    myClass();
    void visit();
    myClassFriend *myClassfriend;
};
class myClassFriend{
    friend void  myClass::visit();;
public:
    myClassFriend();
public:
    string name;
private:
    string sex;
};

myClassFriend::myClassFriend() {
    name = "张三";
    sex = "男";
}

myClass::myClass() {
    myClassfriend = new myClassFriend;
}

void myClass::visit() {
    cout << myClassfriend->name << endl;
    cout << myClassfriend->sex << endl;
}

int main() {
    myClass myclass;
    myclass.visit();
    delete myclass.myClassfriend;
    return 0;
}

只通过成员函数访问另一个类的私有成员。

15.运算符的重载

运算符重载在C++等编程语言中是一种重要的特性,它允许程序员为已存在的运算符提供额外的或定制的行为,尤其是针对用户自定义类型(如类和结构体)。以下是几个关键原因,解释了为什么需要运算符重载:

  1. 提升代码的自然度和可读性 :通过运算符重载,可以让自定义类型像内置类型一样使用熟悉的运算符。例如,如果你定义了一个复数类,重载加号运算符(+)可以让复数相加就像普通整数或浮点数相加一样自然,这使得代码更易于阅读和理解。

  2. 增强表达能力:运算符重载能够以紧凑的形式表达复杂的操作,避免了使用长而晦涩的函数名称。这样可以让代码更加简洁,减少编程时的认知负担。

  3. 保持一致性:对于用户自定义类型,如果不能重载运算符,那么在处理这些类型时,可能需要引入全新的方法或函数来完成类似内置类型的运算,这会破坏语言使用的一致性体验。

  4. 支持泛型编程:运算符重载是实现模板和泛型算法的关键,它允许算法以统一的方式处理多种类型,只要这些类型支持相应的运算符。

  5. 提高效率:在某些情况下,通过精心设计的运算符重载,可以避免不必要的对象复制,减少临时对象的创建,从而提升程序的运行效率。

  6. 适应面向对象编程:在面向对象编程中,类和对象经常需要进行比较、组合等操作,运算符重载使得这些操作可以直接利用运算符,而不是通过复杂的函数调用来实现,更加符合面向对象的设计理念。

运算符重载是实现代码高效、清晰、一致的重要机制,特别是在处理自定义类型时,它极大地增强了语言的表达能力和灵活性。

1."+"运算符的重载

在C++中,加号运算符(+)的重载允许你自定义当加号应用于自定义类型时的行为。你可以通过两种方式重载加号运算符:作为成员函数或作为友元函数(全局函数)。下面是两种方式的概览和示例:

1.1 作为成员函数重载

当作为成员函数重载时,加号运算符接受一个参数,这个参数是你要与当前对象相加的对象。返回值通常是该操作的结果,通常是一个新对象或者引用。

cpp 复制代码
class MyClass {
public:
    // 成员函数重载加号运算符
    MyClass operator+(const MyClass& other) {
        MyClass result;
        // 执行相加操作,例如:
        result.value = this->value + other.value;
        return result;
    }

private:
    int value;
};

int main() {
    MyClass obj1(10);
    MyClass obj2(20);
    MyClass sum = obj1 + obj2; // 使用重载的加号运算符
    // ...
}

示例2:

cpp 复制代码
//
// Created by 86189 on 2024/6/10.
//
#include "iostream"
using namespace std;

class  Complex {
public:
    int real;
    int imag;
    Complex operator+(Complex &c2) const{
        Complex c3{};
       c3.real = this->real + c2.real;
       c3.imag = this->imag + c2.imag;
        return c3;
    }
};

int main() {
    Complex c1{}, c2{} , c3{};
    c1.real = 10;
    c1.imag = 20;
    c2.real = 5;
    c2.imag = 10;
    c3 = c1 + c2;
    cout << c3.real << endl;
    cout << c3.imag << endl;
    return 0;
}
1.2 作为全局函数重载

作为全局函数(友元函数)重载时,加号运算符接受两个参数,这两个参数都是要相加的对象。这种方式的好处是可以访问第一个对象的私有或保护成员。

cpp 复制代码
class MyClass {
public:
    int value;

    // 声明为友元函数,可以在类外部定义
    friend MyClass operator+(const MyClass& lhs, const MyClass& rhs);
};

// 全局函数实现加号运算符重载
MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
    MyClass result;
    result.value = lhs.value + rhs.value;
    return result;
}

int main() {
    MyClass obj1{10};
    MyClass obj2{20};
    MyClass sum = obj1 + obj2; // 同样使用重载的加号运算符
    // ...
}

示例2:

cpp 复制代码
//
// Created by 86189 on 2024/6/10.
//
#include <iostream>
using namespace std;

class A {
public:
    int a;
    int b;
};
A operator+(A &a, A &b) {
    A c{};
    c.a = a.a + b.a;
    c.b = a.b + b.b;
    return c;
}

int main() {
    A a1{}, a2{}, a3{};
    a1.a = 1;
    a1.b = 2;
    a2.a = 3;
    a2.b = 4;
    a3 = a1 + a2;
    cout << a3.a << " " << a3.b << endl;
    return 0;
}

无论哪种方式,重载的加号运算符都应当遵循运算符的一般语义和预期行为,确保操作的直观性和代码的可读性。同时,需要注意的是,当涉及自增(++)、自减(--)、赋值(=)等运算符时,还需要考虑前置与后置版本以及可能需要的拷贝构造或移动构造等细节。

2."<<"运算符重载

在C++中,重载运算符是一种特殊函数,它用于给已有运算符提供自定义的行为,特别是针对用户自定义类型。左移运算符(<<)通常与输入/输出流(如std::cout)一起使用来进行数据的输出。但当你想要自定义类型也能支持这样的输出操作时,就需要重载这个运算符。

2.1为什么需要重载左移运算符

当你定义了一个新的类或结构体,并希望像基本数据类型那样方便地将其实例打印出来时,就需要重载<<运算符。这在调试、日志记录和用户界面显示等方面非常有用。

2.2如何重载左移运算符

以下是一个简单的例子,展示了如何为一个自定义的类MyClass重载左移运算符:

cpp 复制代码
#include <iostream>

class MyClass {
public:
    int value;

    // 构造函数
    MyClass(int v) : value(v) {}

    // 左移运算符重载函数
    friend std::ostream& operator<<(std::ostream& os, const MyClass& obj);
};

// 实现左移运算符重载函数
std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
    os << "MyClass object with value: " << obj.value;
    return os;
}

int main() {
    MyClass obj(10);
    std::cout << obj << std::endl;  // 调用重载后的<<运算符输出obj的内容
    return 0;
}

在这个例子中,我们定义了一个MyClass,它有一个成员变量value。然后,我们声明了一个友元函数来重载<<运算符,该函数接受一个输出流对象(std::ostream&)和一个MyClass的常引用作为参数。在函数内部,我们将MyClass对象的value成员插入到输出流中。通过返回os,我们可以链式调用这个运算符。

2.3注意事项
  • 友元函数:在这里,我们将运算符重载函数声明为类的友元,以便它可以直接访问类的私有和保护成员。
  • 返回值 :重载函数应该返回输出流对象的引用,这样就可以支持连续输出(例如 std::cout << obj1 << obj2;)。
  • const引用:传递对象作为const引用是为了避免复制,提高效率,并允许对const对象进行操作。

通过这种方式,你可以使自定义类型更自然、更方便地融入C++的标准I/O机制中。

重载的一般方式:

cpp 复制代码
//
// Created by 86189 on 2024/6/11.
//
#include "iostream"
using namespace std;

class MyClass {
public:
    int a;
    int b;
    MyClass(int a, int b) {
        this->a = a;
        this->b = b;
    }
};
ostream &operator<<(ostream &out, MyClass &myClass) {
    out << myClass.a << " " << myClass.b;
    return out;
}

int main() {
    MyClass myClass(1, 2);
    cout << myClass;
    return 0;
}

一般情况下的全局函数重载,但这种方式只能访问公有的成员属性和方法(函数)。

3."++"运算符重载
3.1 前置递增运算符重载

前置递增运算符直接对对象进行修改并返回修改后的对象本身。通常,它的声明和定义如下:

cpp 复制代码
class MyClass {
public:
    // 前置递增运算符重载
    MyClass& operator++() {
        // 在这里实现自增逻辑
        // 例如,如果MyClass代表一个数值,可以简单地增加其值
        ++value; // 假设value是类的一个成员变量
        return *this; // 返回当前对象的引用
    }
private:
    int value;
};
3.2后置递增运算符重载

后置递增运算符需要创建一个临时对象来保存自增前的状态,然后原对象再进行自增操作。C++通过接受一个额外的无关参数(通常是int类型的占位符)来区分前置和后置版本:

cpp 复制代码
class MyClass {
public:
    // 后置递增运算符重载
    MyClass operator++(int) { // int参数是后置递增的标志
        MyClass temp(*this); // 创建临时对象保存当前状态
        ++(*this); // 调用前置递增运算符实现自增
        return temp; // 返回自增前的对象状态
    }
private:
    int value;
};
3.3注意事项
  • 确保递增运算符合乎预期,特别是对于复合数据类型或有特殊逻辑的对象。
  • 由于后置递增需要复制当前对象状态,因此在性能敏感的应用中应谨慎使用。
  • 保持运算符重载的一致性,即前置和后置版本在效果上应等价(除了返回类型和是否立即修改对象外)。

通过上述方式,就可以自定义类对象在使用递增运算符时的行为,使其更符合类的设计意图和使用场景。

递增运算符重载的实际应用:

cpp 复制代码
#include <iostream>
using namespace std;

class Complex {
public:
    int value;
    explicit Complex(int value) {
        this->value = value;
    }
    Complex& operator++() {
        ++this->value;
        return *this;
    }
};

class Complex2 {
public:
    int value;
    explicit Complex2(int value) {
        this->value = value;
    }
    Complex2 operator++(int) {
        Complex2 temp(*this);
        this->value++;
        return temp;
    }
};
ostream &operator<<(ostream &out, const Complex c) {
    out << c.value;
    return out;
}
ostream &operator<<(ostream &out, const Complex2 c) {
    out << c.value;
    return out;
}
int main() {
    Complex c(1);
    cout << ++c << endl;
    cout << c << endl;
    Complex2 c2(1);
    cout << c2++ << endl;
    cout << c2 << endl;
    return 0;
}
4.赋值运算符重载

在C++中,赋值运算符(=, assignment operator)可以被重载,以自定义类对象之间赋值的行为。当你想要控制一个类实例如何将值赋给另一个类实例时,重载赋值运算符变得尤为重要。下面是一个简单的示例,展示了如何重载赋值运算符:

cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 成员变量
    int value;

    // 默认构造函数
    MyClass(int v = 0) : value(v) {}

    // 赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        // 防止自我赋值
        if (this != &other) {
            // 实现赋值逻辑
            value = other.value;
        }
        // 返回当前对象的引用,支持连续赋值 (a = b = c)
        return *this;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2;

    std::cout << "Before assignment: obj1.value = " << obj1.value << ", obj2.value = " << obj2.value << std::endl;

    // 使用重载的赋值运算符
    obj2 = obj1;

    std::cout << "After assignment: obj1.value = " << obj1.value << ", obj2.value = " << obj2.value << std::endl;

    return 0;
}
4.1解释
  • 成员变量MyClass中有一个成员变量value
  • 默认构造函数 :提供了默认构造方式,可以初始化value为0或指定的值。
  • 赋值运算符重载 :定义了operator=函数,它接受一个对同类对象的引用other作为参数。
    • 自我赋值检查 :通过比较this(当前对象地址)和&other(传入对象地址)来判断是否是自我赋值,这是为了避免在自我赋值情况下出现不必要的操作或错误。
    • 赋值逻辑 :如果不是自我赋值,就将other.value的值赋予当前对象的value
    • 返回值 :重载的赋值运算符通常返回一个对当前对象(*this)的引用,这样可以支持连续赋值操作。
4.2注意事项
  • 深拷贝与浅拷贝:当类中有指针或动态分配的资源时,必须小心处理深拷贝(复制资源的内容)与浅拷贝(只复制指针)的问题,以避免悬挂指针或资源泄露。
  • 复制与交换:有时使用"复制并交换"(copy-and-swap)技术来实现赋值运算符,这是一种既安全又高效的方法。
  • 三/五法则:如果重载了赋值运算符,一般也建议重载拷贝构造函数、析构函数、移动构造函数和移动赋值运算符,以保持类的完整性和资源管理的一致性。这被称为C++的"三/五法则"。
5.关系运算符的重载
5.1解释

在C++中,你可以重载关系运算符(如==, !=, <, >, <=, >=)来定义它们在自定义类型上的行为。重载关系运算符可以帮助你更直观地比较自定义类的实例。下面是一个简单的例子,展示了如何为一个简单的Point类重载==<运算符:

cpp 复制代码
#include <iostream>

class Point {
public:
    int x, y;

    // 构造函数
    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // 重载等于运算符
    bool operator==(const Point& other) const {
        return (x == other.x) && (y == other.y);
    }

    // 重载小于运算符
    bool operator<(const Point& other) const {
        if (y == other.y) {
            return x < other.x;
        } else {
            return y < other.y;
        }
    }
};

// 为了支持cout输出,重载<<运算符
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

int main() {
    Point p1(1, 2);
    Point p2(1, 2);
    Point p3(2, 1);

    std::cout << "p1 == p2: " << (p1 == p2) << std::endl; // 应输出1,表示真
    std::cout << "p1 < p3: " << (p1 < p3) << std::endl;   // 应输出0,表示假,因为我们定义了先按y比较,后按x比较

    return 0;
}
5.2关键点
  • 重载函数 :重载的关系运算符通常被声明为类的成员函数(对于非成员函数,需要定义为友元函数以访问私有成员),并且通常被声明为const,因为它们不应该修改对象的状态。
  • 返回类型 :重载的关系运算符应该返回bool类型,表示比较的结果。
  • 逻辑一致性 :当你重载一个关系运算符时,应确保所有相关运算符(如<>)的逻辑一致,以避免违反关系运算符的传递性、对称性等属性。
  • 友元函数 :对于非成员函数形式的关系运算符重载(如==),通常将其声明为类的友元函数,以便直接访问类的私有和保护成员。

重载关系运算符可以让你的自定义类型更加自然地融入C++的标准表达式语法中,提高代码的可读性和易用性。

6.函数调用运算符重载

在C++中,直接重载圆括号()操作符通常是用来实现类的实例作为函数的调用,这种机制常用于仿函数(functor)、智能指针或者任何希望像函数一样被调用的类。重载圆括号操作符是通过在类中定义一个名为operator()的成员函数来实现的。

6.1示例

下面是一个简单的示例,展示了如何定义一个类来重载圆括号操作符,使其实例可以像函数一样被调用:

cpp 复制代码
#include <iostream>

class Functor {
public:
    // 重载圆括号操作符
    int operator()(int x, int y) {
        return x + y; // 这个操作符现在像一个求和函数
    }
};

int main() {
    Functor adder; // 创建类的实例

    // 使用类实例像函数一样调用
    int result = adder(10, 20); 
    std::cout << "Result: " << result << std::endl; // 输出: Result: 30

    return 0;
}

在这个例子中,Functor类通过重载operator(),使得创建的adder对象可以像函数那样被调用,传入两个整数参数,并返回它们的和。

以及使用匿名对象调用:

cpp 复制代码
#include <iostream>
using namespace std;

class myClass{
public:
    myClass(int a, int b) {
        this->a = a;
        this->b = b;
    }

    myClass(int a) {
        this->a = a;
        this->b = 0;
    }

    myClass() {
        this->a = 0;
        this->b = 0;
    }
    void operator()(const string& s)
    {
        cout << s << endl;
    }
    int operator()(int i, int j)
    {
        return i + j;
    }
private:
    int a;
    int b;
   };

int main() {
    myClass myClass1(1, 2);
    myClass1("hello");
    cout << myClass()(1, 2) << endl; //匿名对象调用
    return 0;
}
6.2注意事项
  • 返回类型operator()可以有任意合法的返回类型,根据实际需求定义。
  • 参数列表:圆括号内的参数列表也可以根据需要自由定义,就像普通函数的参数列表一样。
  • 多态性 :重载的operator()可以实现多态行为,使得类的实例能够以统一的方式处理不同类型的输入。

通过重载圆括号操作符,C++提供了强大的灵活性,允许用户定义能够像函数一样被调用的对象,这对于函数对象(functors)、函数适配器、策略模式等设计模式非常有用。

16.继承

C++中的继承是面向对象编程的一个核心特性,它允许我们创建一个类(派生类),该类可以从已有的类(基类)中继承属性和行为。

1. 基本继承语法

在C++中,使用冒号(:)和访问修饰符来表示继承关系。访问修饰符可以是publicprotectedprivate,它们定义了基类成员在派生类中的可访问性。

cpp 复制代码
class 基类名 {
    // 基类的成员
};

class 派生类名 : 访问修饰符 基类名 {
    // 派生类的成员
};

//例如:
//
// Created by 86189 on 2024/6/21.
//
#include <iostream>
#include <utility>

using namespace std;

class Base {
public:
    int age{};
    int sex{};
    string name = " ";
    string phone = " ";
};

class Student : public Base {
public:
    string job;
    Student(string name, int age, int sex, string phone, const string& job) {
        this->name = std::move(name);
        this->age = age;
        this->sex = sex;
        this->phone = std::move(phone);
        this->job = job;
    }
};

ostream &operator<<(ostream &out, Student &s) {
    out << "姓名:" << s.name << " 年龄:" << s.age << " 性别:" << s.sex << " 电话:" << s.phone << " 职业:" << s.job;
    return out;
}

class Teacher : public Base {
public:
    string job;
    Teacher(string name, int age, int sex, string phone, const string& job) {
        this->name = std::move(name);
        this->age = age;
        this->sex = sex;
        this->phone = std::move(phone);
        this->job = job;
    }
};

ostream &operator<<(ostream &out, Teacher &t) {
    out << "姓名:" << t.name << " 年龄:" << t.age << " 性别:" << t.sex << " 电话:" << t.phone << " 职业:" << t.job;
    return out;
}

int main() {
    Student s("张三", 18, 1, "123456789", "学生");
    Teacher t("李四", 18, 1, "123456789", "教师");
    cout << s << endl;
    cout << t << endl;
    return 0;
}
2. 继承方式
  • public继承:基类的公有成员在派生类中仍为公有;保护成员变为派生类的保护成员;私有成员对派生类不可见,但影响派生类的布局。
  • protected继承:基类的公有和保护成员都变为派生类的保护成员;私有成员对派生类不可见。
  • private继承:基类的所有公有和保护成员都变为派生类的私有成员;私有成员对派生类不可见。
cpp 复制代码
//
// Created by 86189 on 2024/6/21.
//
#include <iostream>

using namespace std;

class Base{
public:
    int age;
protected:
    int height;
private:
    int weight;
};

class Son:public Base{
public:
    Son(int a,int b) : Base(){
        age = a;
        height = b;
    }
};
class Son2:private Base{
public:
    Son2(int a,int b) : Base(){
        age = a;
        height = b;
    }
};

class Son3:protected Base{
public:
    Son3(int a) : Base(){
        age = a;
        //weight = b;  无法访问
    }
};

int main(){
    Son s(10,20);
    Son2 s2(10,20);
    Son3 s3(10);
    cout<<s.age<<endl;
//    cout<<s2.age<<endl;  保护成员 无法访问
//    cout<<s3.age<<endl;  私有成员 无法访问

    return 0;
}

注意:从父类继承的属性同样位于子类上。

3. 多重继承

C++还支持一个派生类从多个基类继承,这称为多重继承。在多重继承的情况下,访问修饰符需要分别指定。

cpp 复制代码
class 派生类名 : 访问修饰符 基类名1, 访问修饰符 基类名2, ... {
    // 派生类的成员
};

//
// Created by 86189 on 2024/6/21.
//

#include <iostream>

using namespace std;

class Base1 {
public:
    int a;
    static void func1() {
        cout << "Base1::func1" << endl;
    }
};

class Base2 {
public:
    int a;
    static void func2() {
        cout << "Base2::func2" << endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    int b;
    Derived(int a, int b,int c) : Base1(), Base2() {
        Base1::a = a;
        this->b = b;
        Base2::a = c;
    }
    static void func3() {
        cout << "Derived::func3" << endl;
    }
};


int main() {
    Derived d(1, 2,3);
    cout << d.Base1::a << endl;
    cout << d.Base2::a << endl;
    Derived::func1();
    Derived::func2();
    Derived::func3();
    cout << d.b << endl;
    return 0;
}
4. 构造和析构顺序

在C++中,继承涉及的构造函数和析构函数调用顺序遵循以下规则:

4.1 构造函数的调用顺序:
  1. 基类构造函数:当创建派生类的对象时,首先会调用基类的构造函数。如果有多个基类(多继承情况),则按照派生类定义时基类出现的顺序调用。每个基类构造函数按其自身的参数列表进行初始化。

  2. 成员对象构造函数:接下来,按照它们在派生类中声明的顺序,调用派生类中任何非静态成员对象的构造函数。

  3. 派生类构造函数:最后,调用派生类自身的构造函数体。这里可以进一步初始化派生类新增的成员变量。

4.2 析构函数的调用顺序:

析构函数的调用顺序与构造函数的调用顺序相反:

  1. 派生类析构函数:当派生类对象生命周期结束时,首先执行派生类的析构函数体,释放派生类特有的资源。

  2. 成员对象析构函数:接着,按照它们在派生类中声明的逆序,调用派生类中非静态成员对象的析构函数。

  3. 基类析构函数:最后,按照派生类继承列表中基类出现的逆序调用基类的析构函数,释放基类的资源。

总结来说,构造函数是从基类到派生类、从父到子的方向进行初始化,而析构函数则是从派生类到基类、从子到父的方向进行资源的清理。这样的设计保证了资源的正确分配和释放。

5. 虚继承

虚继承是用来解决多重继承中可能出现的二义性和数据冗余问题的。当一个类作为多个派生类的基类时,如果每个派生类都包含基类的一个完整副本,这将导致存储空间的浪费。通过在派生类声明时使用virtual关键字,可以实现虚继承。

cpp 复制代码
class 基类名 {
    // ...
};

class 派生类1 : virtual public 基类名 {
    // ...
};

class 派生类2 : virtual public 基类名 {
    // ...
};

//
// Created by 86189 on 2024/6/21.
//
#include <iostream>

using namespace std;

class Base{
public:
    int age;
};

class Son1 : virtual public Base{
public:
    int sex{};
};

class Son2 : virtual public Base{
public:
    int high{};
};

class GrandSon : public Son1 , public Son2{
public:
    int weight{};
};

int main(){
    Son1 s1;
    Son2 s2;
    s1.age = 18;
    s2.age = 20;
    cout << s1.age << endl;
    cout << s2.age << endl;
    GrandSon g;
    g.age = 20;
    cout << g.age << endl;
    return 0;
}
6. 访问基类成员

在派生类中,可以直接访问基类的公有和受保护成员(根据继承的访问权限)。如果需要明确地指明成员来自哪个基类,可以使用基类名::操作符。

cpp 复制代码
class Base {
public:
    void baseFunction() { /*...*/ }
};

class Derived : public Base {
public:
    void derivedFunction() {
        baseFunction(); // 直接调用
        Base::baseFunction(); // 明确指定基类
    }
};
7. 同名成员和同名静态成员的处理

在C++继承中,如果派生类(子类)和基类(父类)拥有同名的成员(包括数据成员和成员函数),可以通过以下几种方式来解决同名成员的访问问题:

1. 成员变量的处理
  • 优先级:如果派生类中有一个与基类同名的数据成员,那么通过派生类对象访问该同名成员时,将优先访问派生类的成员变量。
  • 访问基类成员 :如果需要访问被派生类同名成员隐藏的基类成员,可以使用作用域解析运算符 :: 明确指定基类的作用域。例如,baseClass::memberName
2. 成员函数的处理
  • 覆盖(Overriding)与隐藏:如果派生类定义了一个与基类同名的成员函数,这可能构成重写(如果是虚函数)或隐藏(如果是非虚函数或参数列表不同)。对于虚函数,派生类会覆盖基类的同名函数,而对于非虚函数或签名不同的函数,则是隐藏基类的同名函数。
  • 访问基类函数 :如果要访问被派生类同名函数隐藏的基类函数,同样需要使用作用域解析运算符,如 baseClass::functionName()
  • 注意重写的规则:重写要求函数签名(除了返回类型外的参数列表)必须与基类中的虚函数完全匹配,并且访问权限不能比基类中的更严格。
3. 静态成员的处理
  • 静态成员的处理方式与非静态成员类似,即派生类可以重新定义同名的静态成员,但访问时需明确指定是基类还是派生类的静态成员。
4. 示例代码
cpp 复制代码
class Base {
public:
    int var = 10;
    void display() { cout << "Base's var: " << var << endl; }
};

class Derived : public Base {
public:
    int var = 20;
    void display() { cout << "Derived's var: " << var << endl; }

    void accessBaseVar() {
        cout << "Accessing Base's var: " << Base::var << endl; // 明确访问基类的var
    }

    void callBaseDisplay() {
        Base::display(); // 明确调用基类的display函数
    }
};

int main() {
    Derived d;
    d.display(); // 输出 Derived's var: 20
    d.accessBaseVar(); // 输出 Accessing Base's var: 10
    d.callBaseDisplay(); // 输出 Base's var: 10
    return 0;
}

这段代码展示了如何在派生类中处理同名的成员变量和成员函数,以及如何通过作用域解析运算符访问基类的同名成员。

同时:需要注意的是,当基类的成员函数有重载时,需要指定基类的作用域才可以访问到发生了重载的函数。

17. 多态

C++中的多态(Polymorphism)是一种面向对象编程的重要特性,它允许你使用一个接口来表示不同的类型。C++中的多态主要通过虚函数(Virtual Functions)来实现,包括静态多态(编译时多态,主要是函数重载和模板)和动态多态(运行时多态)。下面将介绍C++中动态多态的基础语法:

1.虚函数(Virtual Function)

虚函数是实现动态多态的关键。在基类中声明函数为虚函数,使得派生类可以重写该函数。虚函数的声明需要在基类的函数声明前加上virtual关键字。

cpp 复制代码
class Base {
public:
    virtual void display() { /* 默认实现 */ }
    // 注意:析构函数建议声明为虚函数,以确保通过基类指针删除对象时能正确调用派生类的析构函数
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void display() override { /* 重写实现 */ }
};

//
// Created by 86189 on 2024/6/22.
//
#include <iostream>

using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "Animal speak" << endl;
    }
};

class Dog : public Animal {
    void speak() override {
        cout << "Dog speak" << endl;
    }
};

class Cat : public Animal {
    void speak() override {
        cout << "Cat speak" << endl;
    }
};

void doWork(Animal &p) {
    p.speak();
}

int main() {
    Animal *p = new Dog(); // 动态绑定 父类的指针指向子类对象
    p->speak();
    Animal *p2 = new Cat();
    p2->speak();

    Cat c;
    Dog d;
    doWork(c);   //父类的引用指向子类对象
    doWork(d);

    delete p;
    delete p2;
    return 0;
}
2.纯虚函数(Pure Virtual Function)和抽象类(Abstract Class)

纯虚函数是在基类中没有具体实现的虚函数,要求派生类必须提供实现。含有纯虚函数的类称为抽象类,不能实例化。

cpp 复制代码
class Base {
public:
    virtual void func() = 0; // 纯虚函数
};

// Base类现在是一个抽象类,不能直接创建对象
// Base b; // 错误

class Derived : public Base {
public:
    void func() override { /* 实现 */ }
};

//
// Created by 86189 on 2024/6/22.
//
#include <iostream>
using namespace std;

class Base {
public:
    virtual void func() = 0;  //纯虚函数
};


//派生类必须重写纯虚函数  否则无法实例化对象
class Derived : public Base {
public:
    void func() override {
        cout << "Derived::func()" << endl;
    }
};

int main() {
    Base *p = new Derived();
    p->func();
    return 0;
}
3.重写(Override)

在派生类中重新定义基类的虚函数称为重写。使用override关键字可以明确指出某个函数是重写的意图,同时编译器会进行检查,确保该函数确实是在重写基类的一个虚函数。

cpp 复制代码
class Derived : public Base {
public:
    void display() override { /* 重写实现 */ }
};
4.动态绑定(Dynamic Binding)或迟绑定(Late Binding)

动态绑定是运行时根据对象的实际类型来决定调用哪个函数的机制。这要求通过基类指针或引用来调用虚函数。如果直接通过对象名调用,则仍然是静态绑定。

cpp 复制代码
Base* basePtr = new Derived();
basePtr->display(); // 运行时会调用Derived类的display()
delete basePtr;
5.虚析构函数和纯虚析构
5.1虚析构函数

基类的析构函数应该声明为虚函数,以确保当通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数,避免内存泄漏。

在多态场景中,如果基类的析构函数不是虚函数,当通过基类指针删除派生类对象时,只会调用基类的析构函数,而派生类特有的资源可能不会被正确释放,从而导致资源泄漏或更严重的问题。

解决这个问题的方法很简单,就是确保所有包含纯虚函数的抽象基类,其析构函数也应该是虚函数。这样,无论通过基类指针指向的是基类对象还是派生类对象,都能够保证调用到正确的析构函数链,从派生类开始,逐级向上调用到基类的析构函数,彻底释放资源。

cpp 复制代码
//
// Created by 86189 on 2024/6/23.
//
#include <iostream>
using namespace std;

class Base {
public:
    virtual void func () = 0;
    virtual ~Base() {
        cout << "Base::~Base()" << endl;
    }
};

class Derived : public Base {
public:
    void func() override {
        cout << "Derived::func()" << endl;
        p = new int(10);
    }
    ~Derived() override {
        cout << "Derived::~Derived()" << endl;
        delete p;
    }
    int *p{};
};

int main() {
    Derived d;
    Base *p = new Derived();
    p->func();
    delete p;
    return 0;
}
  • 定义 :当在基类中将析构函数声明为虚函数时,它就成为了虚析构函数。声明时不需要赋值为0,格式为 virtual ~ClassName() {}
  • 用途:确保当通过基类指针或引用删除派生类对象时,能够调用到派生类的析构函数,从而正确释放派生类特有的资源。如果不声明为虚析构,那么仅基类的析构函数会被调用,可能导致派生类的资源泄露。
  • 实例化:声明了虚析构函数的类仍然可以实例化对象,不是抽象类。
5.2纯虚析构函数
  • 定义 :在基类中将析构函数声明为纯虚函数,即在声明后加上= 0,格式为 virtual ~ClassName() = 0;。需要注意的是,纯虚函数需要在类外提供定义。
  • 用途:与虚析构函数类似,用于确保多态销毁时调用正确的析构函数。但除此之外,声明了纯虚析构函数的类自动成为抽象类。
  • 实例化:拥有纯虚析构函数的类是抽象类,这意味着不能直接实例化此类的对象。抽象类的主要目的是作为其他类的基类,强制要求派生类实现某些纯虚函数(不仅仅是析构函数)。
cpp 复制代码
//
// Created by 86189 on 2024/6/23.
//
#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() = 0;  // 纯虚析构函数 确保了派生类中内存可以被释放
};
class Derived : public Base {
public:
    void func() {
        cout << "func()" << endl;
        p = new int(10);
    }
    ~Derived() override {
        cout << "~Derived()" << endl;  //纯虚析构保证了释放派生类中申请的内存
        delete p;
        p = nullptr;
    }
    int *p{};
};

Base::~Base() {
    cout << "Base()" << endl;
}

int main() {
    Derived d;
    d.func();
    Base *p = new Derived();
    delete p;
    return 0;
}
5.3总结

两者都用于支持多态性下的正确资源管理,但纯虚析构函数额外具有将类定义为抽象类的特性。选择使用哪一种取决于你的设计需求:如果你希望基类可以实例化,并且需要多态性的析构,使用虚析构函数;如果你的基类根本就不应该直接实例化,而总是作为派生类的基类,并且同样需要多态性的析构,那么纯虚析构函数更为合适。

4、文件操作

1.读写TXT文件

在C++中,读取和写入文件通常涉及到使用<fstream>库中的类,主要是ifstream(用于读取)、ofstream(用于写入)和fstream(既能读又能写)。下面我将分别说明如何进行文件的读取和写入操作。

1.写文件

要向文件写入内容,你需要创建一个ofstream对象,并打开一个文件。如果文件不存在,它会被创建;如果存在,则会被覆盖。

cpp 复制代码
#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::string filename = "example.txt";
    std::ofstream outFile(filename);

    if (!outFile) { // 检查是否成功打开文件
        std::cerr << "无法打开文件 " << filename << std::endl;
        return 1;
    }

    outFile << "Hello, World!" << std::endl; // 写入文本
    outFile << "这是另一行内容。" << std::endl;

    outFile.close(); // 关闭文件
    std::cout << "内容已成功写入文件 " << filename << std::endl;
    return 0;
}

//
// Created by 86189 on 2024/6/19.
//
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ofstream outFile;
    outFile.open("test.txt", ios::out);
    if (!outFile) {
        cout << "文件打开失败" << endl;
        return 0;
    }
    outFile << "Hello World!" << endl;
    outFile.close();
    return 0;
}
2.读文件

读取文件时,你可以使用ifstream对象。下面的示例展示了如何逐行读取文件内容。

cpp 复制代码
#include <fstream>
#include <iostream>
#include <string>

int main() {
    std::string filename = "example.txt";
    std::ifstream inFile(filename);

    if (!inFile) { // 检查是否成功打开文件
        std::cerr << "无法打开文件 " << filename << std::endl;
        return 1;
    }

    std::string line;
    while (std::getline(inFile, line)) { // 逐行读取
        std::cout << line << std::endl; // 输出读取到的内容
    }

    inFile.close(); // 关闭文件
    return 0;
}
3.读文件的方式

在C++中,读取文件的主要方式是使用<fstream>库中的ifstream类(输入文件流)。

  • 字符读取 :使用get()函数逐个读取字符。

    cpp 复制代码
    char ch;
    while (inFile.get(ch)) {
        std::cout << ch;
    }
  • 行读取 :使用getline()函数读取整行文本。

    cpp 复制代码
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }
  • 直接读入变量 :使用提取运算符>>读取数据直到遇到分隔符(通常是空格或换行符)。

    cpp 复制代码
    int number;
    while (inFile >> number) {
        std::cout << number << std::endl;
    }
4.注意事项
  • 在处理文件操作时,始终检查文件是否成功打开,以避免未预期的行为。
  • 使用close()函数关闭文件是一个好习惯,尽管在对象销毁时(例如函数结束时)也会自动关闭,但显式关闭可以立即释放资源。
  • 对于大文件或性能敏感的应用,考虑使用缓冲区读写以提高效率。
  • 如果需要同时读写同一个文件,可以使用fstream类。

在C++中,当创建一个文件流对象(如ifstreamofstreamfstream)并直接在构造函数中指定文件名时,实际上已经隐式地调用了open函数来打开文件。这种方式的好处在于代码更加简洁易读,例如:

cpp 复制代码
std::ofstream outFile("example.txt");

这条语句就完成了创建一个ofstream对象并打开名为example.txt的文件用于写入的操作。这种方式使得初始化与文件打开动作合为一体,减少了代码量,提高了代码的直观性。

然而,并不是所有情况下都会省略显式的.open调用。在以下几种场景中,你可能会选择直接使用.open方法:

  1. 动态文件名 :如果文件名是在运行时确定的,你可能需要先创建流对象,然后在适当的时候调用.open

  2. 多次打开不同文件 :如果你的程序需要在运行过程中打开多个不同的文件,可以复用同一个流对象,通过调用.close后再次使用.open来切换到另一个文件。

  3. 更精细的控制.open函数允许你更精确地控制文件的打开模式(如追加模式ios::app、二进制模式ios::binary等),这在构造函数中可能不易表达或需要修改时更为灵活。

  4. 错误处理 :显式调用.open可以让你更直接地处理文件打开失败的情况,因为你可以立即检查.open的返回值或使用流对象的布尔转换特性来判断操作是否成功。

2.读写二进制文件

在C++中读写二进制文件,主要使用<fstream>库中的ifstream(输入流,用于读取)和ofstream(输出流,用于写入)类,并设置ios::binary模式来确保以二进制形式处理文件。下面分别介绍读取和写入二进制文件的方法。

1.写入二进制文件
cpp 复制代码
#include <fstream>
#include <iostream>

int main() {
    const char data[] = "Hello, Binary World!";
    std::ofstream outFile("example.bin", std::ios::binary);
    
    if (!outFile) {
        std::cerr << "无法打开文件进行写入!" << std::endl;
        return 1;
    }
    
    // 写入数据
    outFile.write(reinterpret_cast<const char*>(&data), sizeof(data));
    // 或者针对特定类型的数据,例如写入一个整数
    // int num = 12345;
    // outFile.write(reinterpret_cast<char*>(&num), sizeof(num));
    
    outFile.close();
    std::cout << "二进制数据已成功写入文件。" << std::endl;
    return 0;
}
2.读取二进制文件
cpp 复制代码
#include <fstream>
#include <iostream>

int main() {
    std::ifstream inFile("example.bin", std::ios::binary);
    
    if (!inFile) {
        std::cerr << "无法打开文件进行读取!" << std::endl;
        return 1;
    }
    
    // 确定文件大小
    inFile.seekg(0, inFile.end);
    std::streampos fileSize = inFile.tellg();
    inFile.seekg(0, inFile.beg);
    
    // 分配足够的内存来存储文件内容
    char* buffer = new char[fileSize];
    
    // 读取文件内容
    inFile.read(buffer, fileSize);
    
    // 使用或处理读取到的二进制数据
    std::cout.write(buffer, fileSize);
    
    delete[] buffer; // 清理分配的内存
    inFile.close();
    std::cout << "二进制数据已成功读取。" << std::endl;
    return 0;
}
3.注意事项
  • 使用std::ios::binary标志是为了确保文件以二进制形式读写,避免因文本模式下的行结束符转换等问题。
  • 在读取二进制数据时,需要预先知道文件的大小或数据结构,以便正确分配内存和解析数据。
  • 使用完分配的内存记得释放,避免内存泄漏。
  • 当处理特定类型的数据(如整数、浮点数等)时,直接读写其内存表示,需确保数据对齐和类型匹配。

5、模板

1.模板的概念

C++中的模板是一种实现泛型编程的机制,它允许程序员编写与类型无关的代码,从而提高了代码的重用性、灵活性和抽象级别。模板可以用来创建泛型类(类模板)和泛型函数(函数模板),使得这些类和函数能够应用于多种数据类型。

1.函数模板

函数模板是创建通用函数的方式,它允许函数操作不同类型的参数,而无需为每种类型重复编写相同的函数代码。函数模板通过类型参数(通常用typenameclass关键字声明)来指定函数可以接受的任何数据类型。编译器会在实际调用函数时根据传入的参数类型自动实例化具体的函数版本。

示例:

cpp 复制代码
template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

在这个例子中,T是一个类型参数,编译器会根据实际调用时的参数类型(如intdouble等)生成相应类型的max函数。

2.类模板

类模板与函数模板类似,但它是应用于类的,允许类的定义参数化。类模板允许创建可以处理各种数据类型的类,从而增加了类的通用性和适应性。

示例:

cpp 复制代码
template <typename T>
class Vector {
private:
    T* elements;
    int size;
public:
    Vector(int s);
    ~Vector();
    void push_back(T element);
    // ... 其他成员函数
};

这里,Vector类是一个模板类,它可以用任何数据类型T实例化,创建特定类型的向量,如Vector<int>Vector<double>

3.模板特化

模板特化允许为模板提供特定类型的特殊实现。这可以是全特化(为所有类型参数提供具体类型)或偏特化(只为部分类型参数提供具体类型)。

4.模板元编程

模板元编程是利用模板在编译时期进行计算的技术,它可以生成高效的静态(编译时计算)代码。这是通过模板实例化和递归模板实例化实现的,常用于计算编译时常量表达式、类型检查等。

总的来说,C++模板机制极大地增强了语言的能力,使得编写高效、灵活且可重用的代码成为可能。

2.模板的使用

C++模板的使用广泛涵盖了函数模板和类模板两大类别,下面分别简要介绍它们的使用方法:

1.函数模板的使用

函数模板允许你编写一个通用的函数,它能接受多种数据类型的参数。使用函数模板的基本步骤包括定义和调用:

定义:

cpp 复制代码
template <typename T>
T add(T a, T b) {
    return a + b;
}

在这个例子中,T是一个类型参数,表示函数参数和返回类型可以是任何类型。编译器会根据实际调用时提供的参数类型自动实例化相应的函数版本。

调用:

cpp 复制代码
int resultInt = add<int>(5, 3);    // 显式指定类型
double resultDouble = add(2.5, 3.7); // 编译器自动推导类型

//
// Created by 86189 on 2024/6/24.
//
#include <iostream>
using namespace std;

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int a = 10, b = 20;
    cout << add<int>(a, b) << endl;  //显式指定类型
    cout << add(a, b) << endl; //模板推导类型

    char c = 'a';
     //cout << add(a, c) << endl;  模板函数无法自动强制转换类型
     cout << add<int>(a, c) << endl;  //显示指定类型
    return 0;
}
2.类模板的使用

类模板允许你创建可以处理任意数据类型的类。定义和使用类模板的步骤如下:

定义:

cpp 复制代码
template <typename T>
class Box {
private:
    T value;
public:
    Box(T val) : value(val) {}
    T getValue() const { return value; }
    void setValue(T val) { value = val; }
};

在这个例子中,Box类是一个模板类,它有一个私有成员value,其类型由类型参数T决定。

实例化与使用:

cpp 复制代码
Box<int> intBox(10);    // 创建一个Box实例,T被实例化为int
std::cout << intBox.getValue() << std::endl;

Box<std::string> stringBox("Hello"); // T被实例化为std::string
std::cout << stringBox.getValue() << std::endl;
3.模板特化

有时你可能需要为特定类型提供一个不同的实现,这就是模板特化。例如,为整数类型优化add函数:

cpp 复制代码
template <>
int add<int>(int a, int b) {
    return a + b; // 假设这里有一个特别的优化实现
}
4.模板参数推导

在大多数情况下,编译器能够自动推导出模板参数的类型,因此你不需要显式指定类型。但在某些复杂情况或需要明确指定类型以避免歧义时,可以使用尖括号语法来显式指定模板参数。

5.普通函数和函数模板的调用规则

在C++中,普通函数和函数模板的调用遵循一定的规则,这些规则决定了编译器如何解析和选择合适的函数版本来执行。以下是几个关键点:

1. 非模板函数优先

当一个普通函数和一个函数模板都可以作为候选函数时,非模板函数具有更高的优先级。如果一个非模板函数和一个函数模板都能匹配给定的调用,编译器会优先选择非模板函数,这一规则称为"非模板函数优先"。

cpp 复制代码
//
// Created by 86189 on 2024/6/24.
//
#include <iostream>

using namespace std;

void printSay(){
    cout << "普通函数调用" << endl;
}

template<typename T>
void printSay(){
    cout << "函数模板调用" << endl;
}

int main()
{
    printSay();  //模板函数优先
    
    printSay<int>();  //通过空参函数列表调用模板函数
    
    return 0;
}
2. 最佳匹配原则

对于函数模板,编译器会尝试根据传入的实际参数类型来实例化模板,寻找最佳匹配。这意味着编译器会选择那个使得转换成本最小,最特化的模板实例。如果存在多个同样特化的模板实例,且转换成本相同,则会导致编译错误(重载模糊不清)。

cpp 复制代码
#include <iostream>

// 通用模板函数
template <typename T>
void display(T value) {
    std::cout << "Generic template with type: " << typeid(T).name() << ", value: " << value << std::endl;
}

// 对int类型的特化模板
template <>
void display<int>(int value) {
    std::cout << "Specialized template for int, value: " << value << std::endl;
}

// 非模板函数,接受double类型,但限制为非负数,以与int特化有所区别
void display(double value) {
    if (value >= 0) {
        std::cout << "Non-template function for non-negative double, value: " << value << std::endl;
    } else {
        std::cout << "Invalid call for negative double value" << std::endl;
    }
}

int main() {
    display(5);      // 调用特化模板 display<int>
    display(3.14);   // 调用非模板函数 display(double),因为传入的是正浮点数
    display(-3.14);  // 也会调用非模板函数 display(double),但输出提示无效调用
    display('A');    // 调用通用模板 display<char>
    
    // 显示指定模板参数来调用通用模板,尽管对于整数这不是必需的,但展示了显式指定的方法
    display<int>(3);  // 明确指定为int类型,调用特化模板 display<int>
    
    return 0;
}
3. 显式模板参数指定

用户可以显式地指定模板参数,强制编译器使用特定的类型实例化模板函数。这种方式可以解决重载模糊的问题,或是在自动类型推导不足以表达意图时使用。

cpp 复制代码
template <typename T>
void foo(T x);

void foo(int x);

foo(5);    // 调用非模板函数 foo(int)
foo<>(5.0); // 显式指定模板参数,调用模板函数 foo<double>
4. 函数模板特化

如果存在针对特定类型的模板特化,当该特化版本和通用模板都匹配时,编译器会优先选择特化版本。

5. 重载决议

在涉及函数模板和普通函数的重载时,编译器将根据重载决议规则(包括匹配度、转换序列的长度和类型转换的类型等)来决定调用哪个函数。如果模板实例化后的函数和非模板函数都适合作为候选者,非模板函数优先;如果都是模板函数,选择最特化或转换成本最低的那个。

示例

考虑以下代码:

cpp 复制代码
void print(int x) { std::cout << "int: " << x << std::endl; }

template <typename T>
void print(T x) { std::cout << "template: " << x << std::endl; }

int main() {
    print(5);    // 调用非模板函数 print(int)
    print(3.14); // 调用模板函数 print<double>
}

在这个例子中,当调用print(5)时,非模板函数print(int)由于优先级更高而被调用;而print(3.14)则匹配到模板函数,实例化为print<double>

7.注意事项
  • 类型兼容性:模板函数或类中的操作需要确保对所有可能的类型参数都是合法的。
  • 模板实例化:模板只有在被使用时才会被实例化,这意味着如果模板中有错误,可能只在实例化时被发现。
  • 编译时间增加:大量或复杂的模板使用可能会增加编译时间。

模板是C++的强大特性,正确使用它们可以显著提升代码的效率和可维护性。

8.类模板成员函数类外实现

在C++中,类模板的成员函数可以在类定义内声明,也可以在类外实现。如果选择在类外实现成员函数,需要注意保持模板参数列表的一致性。

1.类模板声明及成员函数声明

首先,定义一个类模板及其成员函数的声明:

cpp 复制代码
template<typename T>
class MyClass {
public:
    MyClass(T initVal);
    void display();
private:
    T value;
};
2.类模板成员函数的类外实现

接下来,在类定义之外实现这些成员函数。注意,在实现成员函数时,需要包含完整的模板参数列表:

cpp 复制代码
// 构造函数的实现
template<typename T>
MyClass<T>::MyClass(T initVal) : value(initVal) {
    // 初始化逻辑
}

// 成员函数display的实现
template<typename T>
void MyClass<T>::display() {
    cout << "Value: " << value << endl;
}
3.使用类模板

最后,可以在main函数或其他地方实例化并使用这个模板类:

cpp 复制代码
int main() {
    MyClass<int> objInt(10);
    objInt.display(); // 输出 Value: 10

    MyClass<double> objDouble(3.14);
    objDouble.display(); // 输出 Value: 3.14

    return 0;
}
4.注意事项
  • 模板参数一致性:在类外实现模板成员函数时,确保函数前的模板参数列表与类模板的参数列表完全一致。
  • 实现文件分离 :虽然上述示例将模板的声明和实现放在了同一文件中,但实践中,为了组织清晰,有时会将声明放在头文件(.h.hpp),而将实现放在源文件(.cpp)。不过,由于模板的特例化是在使用时完成的,编译器需要看到模板的完整定义才能生成特定类型的代码,因此通常推荐将模板的声明和实现都放在同一个头文件中。
  • 避免链接错误:遵循上述做法可以防止因模板定义不完整导致的链接错误。
9.类模板的分文件编写

将模板的声明和实现都放在同一个头文件中,命名后缀为.hpp

10.模板案例

实现数组排序:

cpp 复制代码
//
// Created by 86189 on 2024/6/24.
//
#include <iostream>

using namespace std;

template<typename T>
void mySort(T arr[], int len) {
    for (int i = 0; i < len - 1; ++i) {
        for (int j = 0; j < len - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) {
                T temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {1,2,3,4,6,5};
    mySort<int>(arr,sizeof(arr)/sizeof(arr[0]));
    for (int i : arr) {
        cout << i << " ";
    }
    cout << endl;
    char str[] = "andbhsc"; // 确保数组只包含你想要排序的内容
    int len = sizeof(str)/sizeof(char) - 1; // 减1是为了排除末尾的空字符'\0'
    mySort<char>(str, len);
    for(char j : str){
        if(j != '\0') { // 排序后的数组可能把'\0'排到了前面,这里过滤掉
            cout << j << " ";
        }
    }
    cout << endl;
    return 0;
}

3.类模板与继承和友元

1.类模板与继承

当类模板与继承结合使用时,你可以创建一个泛型的基类,然后从这个基类派生出具体的类。这意味着派生类也可以是泛型的,同时继承了基类的泛型特性。这在实现一些设计模式(如策略模式、工厂模式等)或者需要处理多种数据类型的类层次结构时非常有用。

例如:

cpp 复制代码
template<typename T>
class Base {
public:
    T value;
    void setValue(T val) { value = val; }
};

// 从Base<T>派生出Derived,继承其泛型特性
template<typename T>
class Derived : public Base<T> {
public:
    void printValue() { cout << "Value: " << this->value << endl; }
};

在这个例子中,Derived类继承了Base类,并且保留了其泛型性。这意味着你可以为Derived指定具体类型,比如Derived<int>Derived<string>,同时利用和扩展基类的泛型功能。

2.类模板与友元

类模板与友元功能结合使用时,可以让一个函数或另一个类成为模板类的友元,从而能够访问该模板类的私有或保护成员。这对于需要外部函数或类来协助模板类完成某些操作,而又不希望公开所有内部细节的情况非常有用。下面是一个简单的示例来说明这一点:

1.类模板与非模板友元函数
cpp 复制代码
template<typename T>
class MyClass {
private:
    T data;

public:
    MyClass(T d) : data(d) {}

    // 声明一个非模板的友元函数
    friend void displayMyClass(const MyClass<T>& obj);
};

// 定义友元函数
template<typename T>
void displayMyClass(const MyClass<T>& obj) {
    cout << "Data: " << obj.data << endl;
}

int main() {
    MyClass<int> obj(10);
    displayMyClass(obj); // 输出 Data: 10
    return 0;
}

在这个例子中,displayMyClass是一个非模板的友元函数,但它能够访问模板类MyClass<T>的私有成员data

2.类模板与模板友元类

如果需要一个模板类成为另一个模板类的友元,那么友元声明也需要模板化:

cpp 复制代码
template<typename T>
class MyClass {
private:
    T data;

public:
    MyClass(T d) : data(d) {}

    // 声明一个模板友元类
    template<typename U>
    friend class FriendClass;
};

// 友元类定义
template<typename U>
class FriendClass {
public:
    void displayData(const MyClass<U>& obj) {
        cout << "FriendClass accessing Data: " << obj.data << endl;
    }
};

int main() {
    MyClass<int> obj(20);
    FriendClass<int> friendObj;
    friendObj.displayData(obj); // 输出 FriendClass accessing Data: 20
    return 0;
}

这里,FriendClass是一个模板类,并且被声明为MyClass<T>的模板友元。因此,它可以访问所有MyClass<T>实例的私有成员。

总结来说,通过将函数或类声明为模板类的友元,可以在保持封装性的同时,允许外部代码以一种受控的方式访问模板类的内部数据。这种机制在设计复杂系统时特别有用,特别是当模板类需要与其他组件紧密协作时。

6、容器

1.容器、迭代器、算法

C++中的STL(Standard Template Library)由三大部分组成:容器(Containers)、迭代器(Iterators)和算法(Algorithms),这三者紧密协作,构成了一套强大的数据处理工具。

1.容器(Containers)

容器是用来存储数据的工具,它们是模板类,可以容纳不同类型的对象,如基本类型(int, double等)或用户自定义类型。C++ STL提供的容器类型多样,能够满足不同场景下的数据存储需求。主要分为两大类:

  • 顺序容器:元素在内存中连续存储,如vector、list、deque、array,适合需要快速随机访问的场景。
  • 关联容器:元素根据键(key)排序存储,如map、set、multimap、multiset,适合需要高效查找或保持唯一性的场景。
  • 无序关联容器:类似关联容器,但使用哈希表实现,如unordered_map、unordered_set等,适用于快速查找且不关心元素顺序的情况。
  • 容器适配器:基于其他容器实现特定接口,如stack、queue、priority_queue。
2.迭代器(Iterators)

迭代器是STL中用于遍历容器内元素的机制,它提供了一种统一的访问容器内数据的方式。迭代器就像是指针的概念扩展,可以认为是对不同容器内部元素的通用访问接口。迭代器有五种基本类别,分别对应不同的操作能力:

  • 输入迭代器(Input Iterators)
  • 输出迭代器(Output Iterators)
  • 前向迭代器(Forward Iterators)
  • 双向迭代器(Bidirectional Iterators)
  • 随机访问迭代器(Random Access Iterators)

迭代器使得STL算法能够不关心具体容器的内部实现,就能对容器中的数据进行操作。

3.算法(Algorithms)

STL算法提供了一系列标准的、高度优化的函数,用于执行常见的数据处理任务,如查找、排序、复制、累积等。这些算法通过迭代器来间接作用于容器中的数据,因此它们是泛型的,几乎可以应用于任何支持适当迭代器类型的容器上。这大大提高了代码的可复用性和灵活性。

算法与容器之间通过迭代器解耦,意味着算法不知道也不关心它们正在操作的数据存储在何种容器中,只需知道如何通过迭代器访问这些数据即可。这样设计既简化了算法的编写,也使得算法可以轻松应用于多种不同的容器类型。

综上所述,C++ STL中的容器、迭代器和算法形成了一个强大的生态系统,允许开发者高效地管理和操作数据,同时保持了代码的简洁性和高性能。

2.string容器

1.string容器构造函数
cpp 复制代码
//
// Created by 86189 on 2024/6/25.
//
#include <iostream>
using namespace std;

int main(){
    string str;  // 创建一个字符串对象
    const char *s = "hello world!";
    str = string(s);  //string(const char *s)将S初始化为string字符串
    string string1(str);  //拷贝构造函数,将str拷贝给string1
    string str2 = string(10,'q'); // 生成10个q的字符串
    cout << str << endl;
    cout << string1 << endl;
    cout << str2 << endl;
    return 0;
}

在C++中,std::string虽然严格意义上不属于STL容器(因为它不直接继承自容器要求的基类模板),但它是一个非常重要的类,用于处理和存储字符串数据,功能上与容器相似,提供了很多容器类的特性。std::string是C++标准库的一部分,设计用来处理变长的字符序列,支持像容器一样的操作,如添加、删除、访问字符等。

std::string的主要特点包括:

  1. 动态大小:字符串的长度可以在运行时改变。
  2. 随机访问:可以通过索引(下标)访问单个字符,就像访问数组一样。
  3. 丰富的成员函数 :提供了诸如size(), length()获取长度,append()追加字符串,erase()删除指定范围的字符,find()查找子串,以及substr()提取子串等众多实用功能。
  4. 迭代器支持std::string提供了迭代器,允许使用STL算法对其内容进行操作,如遍历、查找等。
  5. 构造和赋值 :可以轻松地从C风格的字符串(char*)或其他std::string对象构造或赋值。
  6. 内存管理:自动管理内存,避免了手动处理字符数组时可能出现的内存泄漏问题。

由于其便捷性和灵活性,std::string在大多数涉及文本处理的C++程序中被广泛使用,尽管它在STL容器的正式分类之外,但从使用方式和功能上看,它与STL容器的精神是一致的。

3.string赋值操作

赋值操作函数原型:

cpp 复制代码
string& operator=(const char*s);        //char*类型字符串 赋值给当前的字符串
string& operator=(const string &s);     //把字符串s赋给当前的字符串
string& operator=(char c);              //字符赋值给当前的字符串
string& assign(const char *s);      //把字符串s赋给当前的字符串
string& assign(const char *s,int n);//把字符串s的前n个字符赋给当前的字符串
string& assign(const string &s);//把字符串s赋给当前字符串
string& assign(int n,char c);//用n个字符c赋给当前字符串

示例:

cpp 复制代码
#include <iostream>
using namespace std;

void test(const string& str){
    cout << str << endl;
}

int main(){
    const char *c = "hello!";
    string str1 = c;
    test(str1);
    string str2 = "world";
    test(str2);
    string str3;
    str3 = 'a';
    test(str3);
    string str4;
    str4.assign(c);
    test(str4);
    string str5;
    str5.assign(c,1);
    string str6;
    str6.assign(c);
    test(str6);
    string str7;
    str7.assign(10,'s');
    test(str7);
    return 0;
}
4.string字符串拼接
cpp 复制代码
string& operator+=(const char* str);//重载+=操作符
string& operator+=(const char c);//重载+=操作符
string& operator+=(const string& str);//重载+=操作符
string& append(const char *s);//把字符串s连接到当前字符串结尾
string& append(const char *s,int n);//把字符串s的前n个字符连接到当前字符串结尾
string& append(const string &s);//同operator+=(const string& str)
string& append(const string &s,int pos,int n);//字符串s中从pos开始的n个字符连接到字符串结尾
cpp 复制代码
#include <iostream>

using namespace std;

int main(){
    string string1;
    const char *s = "hello";
    string1 += s;
    cout << string1 << endl;
    const char c = 'a';
    string1 += c;
    cout << string1 << endl;
    string str = "world";
    string1 += str;
    cout << string1 << endl;
    string1.append(s);
    cout << string1 << endl;
    string1.append(s,1);
    cout << string1 << endl;
    string1.append(str);
    cout << string1 << endl;
    string1.append(str,2,1);
    cout << string1 << endl;


    return 0;
}

/*
 *运行结果
hello
helloa
helloaworld
helloaworldhello
helloaworldhelloh
helloaworldhellohworld
helloaworldhellohworldr

 */
5.string字符串查找和替换
cpp 复制代码
int find(const string& str,int pos =0)const;//查找str第一次出现位置,从pos开始查找
int find(const char*s,int pos =0)const;//查找s第一次出现位置,从pos开始查找
int find(const char* s,int pos,int n)const;//从pos位置查找s的前n个字符第一次位置
int find(const char c,int pos =0)const;//查找字符c第一次出现位置
int rfind(const string& str,int pos = npos)const;//查找str最后一次位置,从pos开始查找
int rfind(const char* s,int pos = npos)const;//查找s最后一次出现位置,从pos开始查找
int rfind(const char* s,int pos,int n)const;//从pos查找s的前n个字符最后一次位置
int rfind(const char c,int pos =0)const;//查找字符c最后一次出现位置
string& replace(int pos,int n, const string& str);//替换从pos开始n个字符为字符串str
string& replace(int pos,int n,const char* s);//替换从pos开始的n个字符为字符串s
cpp 复制代码
int main(){
    string string1;
    string1 = "hello world!";
    const char *s = "he";
    cout <<  string1.find("ll",0) << endl;
    cout << string1.find(s,0) << endl;
    cout << string1.find(s,0,1) << endl;
    cout << string1.find('o') << endl;
    cout << string1.rfind("ll") << endl;
    cout << string1.rfind(s) << endl;
    cout << string1.rfind(s,0,1) << endl;
    cout << string1.rfind('o') << endl;
    string str = "china";
    string1.replace(0,5,str);
    cout << string1 << endl;
    string1.replace(0,2,s);
    cout << string1 << endl;

    return 0;
}
6.string字符串比较
cpp 复制代码
int compare(const string &s) const;//与字符串s进行比较  返回几个字符不相等
int compare(const char *s) const //与字符串s进行比较 返回不相等的数量
cpp 复制代码
#include <iostream>
using namespace std;

int main(){
    string str = "hello w";
    const char *s = "hello";
    string str1 = "hello";
    cout << -str1.compare(str) << endl;
    cout << str1.compare(s) << endl;
    return 0;
}
7.string字符存取
cpp 复制代码
char& operator[](int n) 通过[]方式获取字符
char& at(int n); 通过at方法获取字符
cpp 复制代码
#include <iostream>

using namespace std;

int main(){
    string str = "hello!";
    for(char i : str){
        cout << i << " ";
    }
    for (int i = 0; i < str.length(); ++i) {
        cout << str.at(i) << " ";
    }

    return 0;
}
8.string字符串插入和删除
cpp 复制代码
* string& insert(int pos, const char* s); 插入字符串
* string& insert(int pos,const string& str);同上
* string& insert(int pos,int n, char c); 在pos位置插入n个字符c
* string& erase(int pos,int n= npos); 删除从pos开始的n个字符
cpp 复制代码
#include <iostream>
using namespace std;

int main(){
    string str;
    const char *s = "hello";
    cout << str.insert(0,s) << endl;
    cout << str.insert(5," world") << endl;
    cout << str.insert(11,1,'a') << endl;
    cout << str.erase(0,2);
    return 0;
}
9.string字串获取
cpp 复制代码
* string substr(int pos=0,int n=npos)const; //返回由pos开始的n个字符组成的字符串
cpp 复制代码
#include <iostream>
using namespace std;

int main(){

    string str = "hello!";
    cout << str.substr(4,1) << endl;
    str = "893754922@qq.com";
    cout << str.substr(0,str.find('@')) << endl;

    return 0;
}

3.vector容器

std::vector是C++标准模板库(STL)中的一个动态数组容器,其内部实现基于动态内存分配,能够自动调整大小。

  1. 动态大小 : vector容器能够在运行时动态地增加或减少其容量以适应元素的数量变化,无需程序员显式地管理内存。

  2. 连续内存 : 虽然可以动态调整大小,但vector中的元素在内存中是连续存储的,这使得它支持快速随机访问,类似于传统的数组。

  3. 容量与尺寸:

    • 容量(capacity) : 容量是指vector当前所分配的内存空间能容纳的元素最大数量,即使这些位置可能尚未被使用。
    • 大小(size) : 大小是指vector当前实际存储的元素数量。
  4. 自动扩展 : 当向vector添加元素导致当前容量不足以容纳新元素时,vector会自动重新分配内存,通常是原容量的一定倍数(例如,常见实现会加倍现有容量),以减少频繁的内存重分配。

  5. 高效操作:

    • 尾部插入和删除元素的操作非常高效。
    • 使用索引或at()方法可以快速访问元素,时间复杂度为O(1)。
    • 插入或删除非尾部元素可能需要移动后续元素,因此操作成本较高。
  6. 迭代器 : vector提供了随机访问迭代器,允许类似指针的操作,可以进行高效的遍历和元素访问。

  7. 构造与初始化 : vector可以通过默认构造函数、指定大小的构造函数、区间构造函数或使用初始化列表等方式进行构造和初始化。

  8. 内存管理 : vector负责其内部元素的内存管理,包括分配和释放,减轻了程序员的负担,但这也意味着在插入和删除操作中可能会有性能开销,尤其是在需要重新分配内存时。

vector结合了数组的高效访问特性和动态数组的灵活性,是处理可变数量序列数据时的常用选择。

1.vector容器构造函数
cpp 复制代码
/*
 * vector<T> v;//采用模板实现类实现,默认构造函数
 * vector(v.begin(),v.end());//将v[begin(),end()]区间的元素拷贝给本身
 * vector(n,elem); //构造函数将n个elem拷贝给本身
 * vector(const vector &vec); //拷贝构造函数
 */

示例:

cpp 复制代码
#include <iostream>
#include "vector"
using namespace std;


int main(){
    vector<int> v;
    v.reserve(10);
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }
    for (int & it : v){
        cout << it << " ";
    }
    cout << endl;
    vector<int>v1(v.begin(),v.end());
    for (int &it : v1){
        cout << it << " ";
    }
    cout << endl;
    vector<int>v3(10,100);
    for (int &it : v3){
        cout << it << " ";
    }
    cout << endl;
    vector<int>v4(v3);
    for (int &it : v4) {
        cout << it << " ";
    }
    return 0;
}
2.vector容器的赋值操作
cpp 复制代码
/*
 * vector& operator=(const vector &vec);//重载等号操作符
 * assign(beg,end);//将[beg,end)区间中的数据拷贝赋值给本身,
 * assign(n, elem);//将n个elem拷贝赋值给本身。
 */

示例:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main(){
    vector<int>v;
    v.push_back(10);
    v.push_back(20);
    vector<int>v1;
    v1 = v;
    for (int &it : v1){
        cout << it << " ";
    }
    cout << endl;
    vector<int>v2;
    v2.assign(v.begin(), v.end());
    for(int &it : v2){
        cout << it << " ";
    }
    cout << endl;
    v2.assign(10,100);
    for(int &it : v2){
        cout << it << " ";
    }
    return 0;
}
3.vector容器的容量和大小
cpp 复制代码
empty();//判断容器是否为空
capacity();//容器的容量
size();//返回容器中元素的个数
resize(int num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
                          //如果容器变短,则末尾超出容器长度的元素被删除。

resize(int num, elem);//重新指定容器的长度为num,若容器变长,则以elem值填充新位置
                                      //如果容器变短,则未尾超出容器长度的元素被删除

示例:

cpp 复制代码
#include <iostream>
#include "vector"
using namespace std;


int main(){
    vector<int>v;
    if (v.empty()){
        cout << "Nothing" << endl;
    }
    v.push_back(10);
    cout << v.size() << endl;
    v.reserve(5);  // 只扩充容量 不填充值  预先为vec分配至少能容纳5个元素的内存空间
    v.resize(5);
    for(int &it : v){
        cout << it << endl;
    }
    cout << v.size() << endl;
    cout << v.capacity() << endl;
    v.reserve(10);
    v.resize(10,10); //扩充部分填充10
    cout << v.size() << endl;
    cout << v.capacity() << endl;
    return 0;
}
4.vector容器的插入和删除
cpp 复制代码
push back(ele);//尾部插入元素ele
pop_back();//删除最后一个元素
insert(const iterator pos,ele);//迭代器指向位置pos插入元素ele
erase(const iterator pos);//删除迭代器指向的元素
insert(const_iterator pos,int count,ele);//迭代器指向位置pos插入count个元素ele
erase(const iterator start,const iterator end);//删除迭代器从start到end之间的元素
clear();//删除容器中所有元素

示例:

cpp 复制代码
#include <iostream>
using namespace std;
#include "vector"
int main(){
    vector<int>v;
    v.push_back(10);
    v.push_back(20);
    for(int &it : v){
        cout << it << " ";
    }
    cout << endl;
    v.pop_back();
    v.insert(v.begin(),5);
    for(int &it : v){
        cout << it << " ";
    }
    cout << endl;
    v.insert(v.begin(),101);
    for(int &it : v){
        cout << it << " ";
    }
    cout << endl;
    v.erase(v.begin());
    for(int &it : v){
        cout << it << " ";
    }
    cout << endl;
    v.erase(v.begin(), v.end());
    for(int &it : v){
        cout << it << " ";
    }
    cout << endl;
    v.clear();
    for(int &it : v){
        cout << it << " ";
    }
    return 0;
}
5.vector容器的数据存取
cpp 复制代码
at(int idx);//返回索引idx所指的数据
operator[];//返回索引idx所指的数据
front();//返回容器中第一个数据元素
back();//返回容器中最后一个数据元素

示例:

cpp 复制代码
#include <iostream>
#include "vector"
using namespace std;

int main(){
    vector<int>v;
    v.reserve(10);
    for(int i = 0;i < 10;++i){
        v.push_back(i);
    }
    cout << v.at(0) << endl;
    cout << v[1] << endl;
    cout << v.front() << endl;
    cout << v.back() << endl;
    return 0;
}
6.vector容器互换容器
cpp 复制代码
swap(vec);//将容器vec与自身互换

示例:

cpp 复制代码
#include <iostream>
#include "vector"
using namespace std;

int main() {
    vector<int> v;
    v.reserve(10);
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }
    vector<int>v1;
    v1.swap(v);
    for(int &it : v1){
        cout << it << " ";
    }
	//使用swap收缩内存
    //vector<int>(v).swap(v); //创建匿名对象以v的大小初始化,互换两个容器,匿名对象在当前行执行完毕后,内存被回收
    return 0;
}
7.vector容器预留空间

在C++的STL中,std::vector容器的reserve方法用于预先为容器分配内存空间,但不初始化元素,也不改变容器中元素的数量。这个方法的主要目的是为了优化性能,特别是当你已经知道或者可以预测到容器未来需要容纳多少元素时,通过提前分配足够的内存,可以减少在插入元素过程中因自动扩容而引起的内存重新分配和数据迁移次数。

  • 功能reserve函数允许你设定容器的容量(capacity)。也就是说,它保证了在不做进一步内存重新分配的情况下,容器至少可以容纳指定数量的元素。

  • 参数:它接受一个参数,即你希望预留的未初始化元素的个数。这个参数应该是非负的,如果小于当前容量,则调用不会有任何效果。

  • 不影响元素 :调用reserve不会改变容器中已有元素的数量,也不会影响已有元素的值。

  • 性能优势 :通过预先分配内存,可以避免在多次插入操作中频繁地重新分配和复制数据,这对于性能敏感的应用特别重要。特别是在循环中添加大量元素时,先调用一次reserve可以显著提升效率。

  • 不初始化元素 :与resize不同,reserve不会修改容器的实际大小(即元素数量),也不会对新分配的内存区域中的元素进行初始化。

  • 示例用法:

    cpp 复制代码
    std::vector<int> vec;
    vec.reserve(100); // 预先为vec分配至少能容纳100个元素的内存空间

reserve是一种优化手段,帮助程序减少运行时的内存管理和分配开销,特别是在明确知道所需内存大小的情况下。

4.deque容器

在C++中,std::deque(发音为 "deck")是双端队列(double-ended queue)的缩写,是STL中的一个容器。它允许在两端高效地进行插入和删除操作,因此得名双端队列。下面是关于std::deque的一些基本概念和特性:

特点:

  1. 动态数组deque内部通常实现为一个分段的动态数组,这意味着它在内存中不是连续的一块,而是由多个连续的区块组成,这使得在两端插入和删除元素都非常高效。

  2. 随机访问 :尽管在内存中不是连续一块,deque仍然支持随机访问,即通过索引直接访问元素,时间复杂度为O(1)。

  3. 双向开口 :可以在deque的前端(front)和后端(back)进行插入和删除操作,时间复杂度通常为O(1)。

  4. 内存分配 :当deque需要更多空间时,它会在两端分别分配新的区块,而不是移动所有元素来腾出空间或扩大现有的连续内存块,这使得在两端的插入操作相对高效。

  5. 容量与尺寸 :同vector一样,deque也有容量(capacity)和大小(size)的概念。容量表示当前分配的内存可以存放多少元素,而大小则是当前实际存放的元素数量。

  6. 迭代器deque提供了随机访问迭代器,可以从前向后或从后向前遍历元素。

常用操作:

  • push_back()pop_back():在deque的后端添加或移除元素。
  • push_front()pop_front():在deque的前端添加或移除元素。
  • insert()erase():在deque的任意位置插入或删除元素。
  • size()capacity():获取deque中的元素数量和当前容量。
  • 使用下标或迭代器访问元素。

应用场景:

deque适合于那些需要在序列的开始和结束位置高效插入和删除元素的场景,比如实现一个循环缓冲区、模拟队列(尤其是需要两端操作的队列)等。

总之,std::deque是一个灵活且高效的容器,特别适合于需要在序列两端进行频繁插入和删除操作的场景。

1.deque容器构造函数
cpp 复制代码
deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身
deque(n, elem);//构造函数将n个elem拷贝给本身
deque(const deque &deq);//拷贝构造函数

示例:

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

int main(){
    deque<int>deque1;
    for(int i = 0;i < 10;++i){
        deque1.push_back(i);
    }
    for(int &it : deque1){
        cout << it << " ";
    }
    cout << endl;
    deque<int>deque2(deque1.begin(), deque1.end());
    for(int &it : deque2){
        cout << it << " ";
    }
    cout << endl;
    deque<int>deque3(10,100);
    for(int &it : deque3){
        cout << it << " ";
    }
    cout << endl;
    deque<int>deque4(deque3);
    for(int &it : deque4){
        cout << it << " ";
    }
    cout << endl;
    return 0;
}
2.deque容器赋值操作
cpp 复制代码
deque& operator=(const deque &deg);//重载等号操作符
assign(beg, end);//将[beg,end)区间中的数据拷贝赋值给本身
assign(n, elem);//将n个elem拷贝赋值给本身。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/6/27.
//
#include <iostream>
#include <deque>
using namespace std;

int main(){
    deque<int>deque1;
    for(int i = 0;i < 10;++i){
        deque1.push_back(i);
    }
    for(int &it : deque1){
        cout << it << " ";
    }
    cout << endl;
    deque<int>deque2;
    deque2 = deque1;
    for(int &it : deque2){
        cout << it << " ";
    }
    cout << endl;
    deque<int>deque3;
    deque3.assign(deque2.begin(), deque2.end());
    for(int &it : deque3){
        cout << it << " ";
    }
    cout << endl;
    deque3.assign(10,520);
    for(int &it : deque3){
        cout << it << " ";
    }
    cout << endl;
    return 0;
}
3.deque容器大小操作
cpp 复制代码
deque.empty();//判断容器是否为空
deque.size();//返回容器中元素的个数
deque.resize(num);
//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem);
//重新指定容器的长度为num,若容器变长,则以elem值填充新位置,//如果容器变短,则末尾超出容器长度的元素被删除。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/6/27.
//
#include <iostream>
#include <deque>
using namespace std;

int main(){
    // 初始化一个双端队列
    deque<int> deque1;
    
    // 向队列中添加10个整数
    for(int i = 0;i < 10;++i){
        deque1.push_back(i);
    }
    
    // 遍历并打印队列中的所有元素
    for(int &it : deque1){
        cout << it << " ";
    }
    cout << endl;
    
    // 检查队列是否为空,并打印相应信息
    if (deque1.empty()){
        cout << "Nothing" << endl;
    }
    
    // 打印队列的大小
    cout << deque1.size() << endl;
    
    // 调整队列大小为15,可能造成元素的添加或删除
    deque1.resize(15);
    
    // 遍历并打印调整后的队列元素
    for(int &it : deque1){
        cout << it << " ";
    }
    cout << endl;
    
    // 将队列大小调整为20,并为新增元素指定默认值1
    deque1.resize(20,1);
    
    // 再次遍历并打印调整后的队列元素
    for(int &it : deque1){
        cout << it << " ";
    }
    cout << endl;
    
    return 0;
}
4.deque容器插入和删除
cpp 复制代码
两端插入操作:
push back(elem);//在容器尾部添加一个数据
push front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据
指定位置操作:
insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
clear();//清空容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置
erase(pos);//删除pos位置的数据,返回下一个数据的位置。

示例:

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

/* 定义一个打印类,用于输出deque的内容 */
class print{
public:
    /* 使用静态成员函数printFunc,接收一个deque<int>类型的引用参数 */
    /* 参数deque1: 要打印的deque<int>对象 */
    /* 该函数不返回任何值,它通过cout输出deque<int>的内容 */
    static void printFunc(const deque<int>& deque1){
        /* 遍历deque1中的每个元素,并打印 */
        for(int it : deque1){
            cout << it << " ";
        }
        /* 行末换行 */
        cout << endl;
    }
};

int main(){
    /* 创建一个deque<int>对象deque1 */
    deque<int> deque1;
    /* 在deque1的尾部添加元素10 */
    deque1.push_back(10);
    /* 在deque1的头部添加元素20 */
    deque1.push_front(20);
    /* 调用printFunc打印当前deque1的内容 */
    print::printFunc(deque1);
    /* 移除deque1的尾部元素 */
    deque1.pop_back();
    /* 移除deque1的头部元素 */
    deque1.pop_front();
    /* 调用printFunc打印当前deque1的内容 */
    print::printFunc(deque1);
    /* 在deque1的开头插入元素2 */
    deque1.insert(deque1.begin(),2);
    /* 调用printFunc打印当前deque1的内容 */
    print::printFunc(deque1);
    /* 在deque1的开头插入两个值为1的元素 */
    deque1.insert(deque1.begin(),2,1);
    /* 调用printFunc打印当前deque1的内容 */
    print::printFunc(deque1);
    /* 将deque1的自身内容复制并插入到deque1的末尾 */
    deque1.insert(deque1.end(),deque1.begin(), deque1.end());
    /* 调用printFunc打印当前deque1的内容 */
    print::printFunc(deque1);
    /* 清空deque1的所有元素 */
    deque1.clear();
    /* 调用printFunc打印当前deque1的内容(此时为空) */
    print::printFunc(deque1);
    /* 从deque1的开头到结尾删除所有元素(因已清空,此操作无实际效果) */
    deque1.erase(deque1.begin(), deque1.end());
    /* 调用printFunc打印当前deque1的内容(此时为空) */
    print::printFunc(deque1);
    /* 在deque1的头部插入元素1 */
    deque1.push_front(1);
    /* 删除deque1的开头元素 */
    deque1.erase(deque1.begin());
    /* 调用printFunc打印当前deque1的内容 */
    print::printFunc(deque1);
    return 0;
}

另外:传入的迭代器posbegin等支持数据偏移。

5.deque容器数据存取

at(int idx);//返回索引idx所指的数据

operator[];//同上

front();//返回容器中的第一个数据元素

back();//返回容器中的最后一个元素

示例:

cpp 复制代码
//
// Created by 86189 on 2024/6/28.
//
#include <iostream>
#include <deque>
using namespace std;

int main(){
    // 创建一个整数类型的双端队列
    deque<int> deque1;
    
    // 向队列中添加10个整数,从0到9
    for(int i = 0;i < 10;++i){
        deque1.push_back(i);
    }
    
    // 输出队列中索引为1的元素
    // 使用.at()方法访问元素时,会进行边界检查,确保索引在有效范围内
    cout << deque1.at(1) << endl;
    
    // 输出队列中索引为2的元素
    // 使用[]操作符访问元素时,不会进行边界检查
    cout << deque1[2] << endl;
    
    // 输出队列的最后一个元素
    // back()方法用于获取队列的最后一个元素
    cout << deque1.back() << endl;
    
    // 输出队列的第一个元素
    // front()方法用于获取队列的第一个元素
    cout << deque1.front() << endl;
    
    return 0;
}
6.deque容器构排序操作

当使用标准算法时,需要包含算法的头文件#include <algorithm>

示例:

cpp 复制代码
//
// Created by 86189 on 2024/6/28.
//
#include <iostream>
#include <deque>
#include <algorithm>

using namespace std;

int main(){
    // 初始化一个双端队列deque1
    deque<int> deque1;
    
    // 向队列中添加五个整数元素
    deque1.push_back(100);
    deque1.push_back(45);
    deque1.push_back(55);
    deque1.push_back(199);
    deque1.push_back(163);
    
    // 对队列中的元素进行排序
    // 为了展示deque可以支持标准库中的排序操作
    sort(deque1.begin(), deque1.end());
    
    // 遍历并输出排序后的队列元素
    // 体现deque支持迭代器遍历的特点
    for(int &it : deque1){
        cout << it << " ";
    }
    
    return 0;
}

5.stack容器

在C++中,std::stack是一个容器适配器,它提供了后进先出(LIFO, Last-In-First-Out)的数据结构特性。stack并不是一个独立的容器,而是基于现有的标准容器(如std::vectorstd::dequestd::list)实现的。默认情况下,如果未指定基础容器类型,std::deque会被用作默认的底层容器。

std::stack主要接口包括:

  • 构造函数

    • stack(); 默认构造函数,使用默认的容器(通常是deque)来创建栈。
    • explicit stack(const container& c); 使用给定的容器c来初始化栈。
  • 成员函数

    • bool empty() const; 检查栈是否为空。
    • size_type size() const; 返回栈中元素的数量。
    • reference top(); 返回栈顶元素的引用,但不移除它。
    • void push(const value_type& x); 在栈顶添加一个元素。
    • void pop(); 移除栈顶元素。

注意,stack不直接提供访问除了栈顶之外的任何元素的方法,这是由其后进先出的特性决定的。

下面是一个简单的使用示例:

cpp 复制代码
//
// Created by 86189 on 2024/6/28.
//
#include <iostream>
#include <stack>
using namespace std;

int main(){
    stack<int>stk;
    stack<int>stk2(stk);
    stk.push(10);
    stk.push(20);
    stk.push(30);
    stk.push(10);
    stk2 = stk;
    cout << stk.size() << endl;
    while (!stk.empty()){
      cout << stk.top() << " ";
      stk.pop();
    }
    cout << endl;
    cout << stk.size() << endl;
    cout << stk2.size() << endl;
    return 0;
}

在这个例子中,我们创建了一个整数栈,向其中压入了几个元素,然后查看和操作栈顶元素,展示了基本的栈操作。

6.queue容器

在C++中,std::queue是一个容器适配器,它提供了先进先出(FIFO, First-In-First-Out)的数据结构特性。与std::stack类似,queue也不是一个独立的容器,而是基于现有的标准容器实现的,最常用的底层容器是std::deque,但也可以是其他容器如std::list。默认情况下,如果没有特别指定,deque就是默认的底层容器。

std::queue的主要接口包括:

  • 构造函数

    • queue(); 默认构造函数,使用默认的容器(通常是deque)来创建队列。
    • explicit queue(const container& c); 使用给定的容器c来初始化队列。
  • 成员函数

    • bool empty() const; 检查队列是否为空。
    • size_type size() const; 返回队列中元素的数量。
    • reference front(); 返回队列头部元素的引用,但不移除它。
    • reference back(); 返回队列尾部元素的引用,但不移除它。
    • void push(const value_type& x); 在队列尾部添加一个元素。
    • void pop(); 移除队列头部元素。

下面是一个简单的使用示例:

cpp 复制代码
//
// Created by 86189 on 2024/6/28.
//
#include <iostream>
#include <queue>
using namespace std;


int main(){
    // 创建一个整数类型的队列
    queue<int>q;
    
    // 向队列中添加三个元素
    q.push(10);
    q.push(50);
    q.push(20);
    
    // 输出队列末尾的元素
    cout << q.back() << endl;
    
    // 输出队列头部的元素
    cout << q.front() << endl;
    
    // 当队列不为空时,循环输出队列的头部元素并将其移除
    while (!q.empty()){
        cout << q.front() << endl;
        q.pop();
    }
    
    // 输出队列当前的大小
    cout << q.size() << endl;
    
    return 0;
}

在这个示例中,我们创建了一个整数队列,向其中添加了几个元素,然后查看和操作队列的头部和尾部元素,展示了基本的队列操作。队列保证了最先添加的元素将会最先被移除,体现了先进先出的原则。

7.list容器

1.基本概念

C++的list容器是标准模板库(STL)中的一种顺序容器,它基于双向链表实现。

  1. 双向链表结构:每个元素(节点)都包含数据部分和两个指针,分别指向前一个元素和后一个元素,这使得在链表的任何位置进行插入和删除操作都非常高效,时间复杂度为O(1)。

  2. 动态大小list容器的大小可以在运行时动态改变,即可以在程序运行过程中添加或移除元素。

  3. 随机访问 :不同于vectorarray等连续内存的容器,list不支持随机访问迭代器,不能直接通过索引获取元素,而需要通过迭代器遍历。

  4. 迭代器稳定性 :在list中插入或删除元素不会导致其他迭代器失效,除了指向被删除元素的迭代器。这是因为它通过调整相邻节点的指针来维护链表结构,而不需要移动元素或重新分配内存。

  5. 空间开销 :每个元素都有额外的存储开销用于存储指针(前驱和后继节点的地址),因此相比于数组或vectorlist在存储同样数量元素时通常占用更多的内存。

  6. 常用操作list容器提供了丰富的成员函数,如push_back()push_front()在链表尾部或头部添加元素,pop_back()pop_front()从链表尾部或头部移除元素,以及在指定位置插入或删除元素的函数等。

  7. 适用场景list特别适合需要频繁进行插入和删除操作,而对随机访问需求不高的场景。例如,实现队列、堆栈、最近最少使用(LRU)缓存等数据结构时,list是一个很好的选择。

2.基本操作
1. 构造与初始化
  • 默认构造list<T> lst; 创建一个空的list,其中T是存储元素的数据类型。
  • 拷贝构造list<T> lst2(lst1); 从另一个list (lst1)拷贝构造一个新的list (lst2)。
  • 区间构造list<T> lst(start, end); 从迭代器startend范围内的元素构造list
  • 初始化列表构造 :C++11起,可以直接用初始化列表创建list,如list<int> lst = {1, 2, 3};
2. 迭代器
  • 正向迭代器反向迭代器list提供iteratorconst_iterator用于正向遍历,以及reverse_iteratorconst_reverse_iterator用于反向遍历。由于链表的特性,这些迭代器都是双向迭代器,但不是随机访问迭代器。
3. 元素访问
  • front()back():分别返回第一个元素和最后一个元素的引用。
  • 访问特定元素 :由于缺乏随机访问迭代器,访问list中的特定元素需要从头或尾开始迭代,或者维护额外的迭代器/指针。
4. 修改操作
  • 插入操作

    • push_back(val):在链表末尾添加元素。
    • push_front(val):在链表开头添加元素。
    • insert(pos, val)insert(pos, n, val)insert(pos, iter, iterEnd):在迭代器pos之前插入单个元素、n个相同元素或[iter, iterEnd)区间的所有元素。
  • 删除操作

    • pop_back():删除链表最后一个元素。
    • pop_front():删除链表第一个元素。
    • erase(pos)erase(start, end):删除单个元素或区间内的所有元素。
5. 大小管理
  • size():返回链表中元素的数量。
  • empty():检查链表是否为空。
6. 合并与分割
  • merge(list& other) :将另一个无序的list合并到当前list中,要求当前list已排序。
  • splice(pos, list& other [, iter]) :将另一个list的所有元素移到当前list的指定位置,可选地也可以只移动一个特定元素。
  • remove(val) :删除所有值为val的元素。
  • remove_if(pred) :删除满足谓词函数pred的元素。
7. 排序与查找

虽然链表不适合于高效的随机访问,但list依然支持一些基本的排序和查找操作:

  • sort():对链表进行原地排序,要求元素类型支持比较操作。
  • unique():删除相邻的重复元素。
  • find(val):使用迭代器遍历查找指定值的元素。
8. 异常安全

list的操作通常提供较强的异常安全性,意味着即使在操作过程中发生异常,容器的状态也是良好的,不会泄露资源或破坏数据结构的完整性。

综上所述,list容器因其高效的插入和删除性能,在需要频繁变动的序列操作中非常有用,尽管牺牲了随机访问的效率。理解和掌握这些特性和操作,能帮助开发者更有效地利用list解决特定问题。

3.list的构造函数

C++ list 容器提供了多种构造函数来初始化容器。

1. 默认构造函数
cpp 复制代码
list<T> lst;

这个构造函数创建一个空的 list,其中 T 是容器内元素的数据类型。

2. 拷贝构造函数
cpp 复制代码
list<T> lst1(anotherList);

这个构造函数用于拷贝一个已存在的 list (anotherList),创建一个内容完全相同的新的 list (lst1)。

3. 列表初始化构造函数

C++11 起,可以使用初始化列表来直接初始化 list

cpp 复制代码
list<int> lst = {1, 2, 3, 4, 5};

这个例子创建了一个包含整数 1 到 5 的 list

4. 区间构造函数
cpp 复制代码
list<T> lst(startIterator, endIterator);

这个构造函数根据两个迭代器所指定的区间 [startIterator, endIterator) 来创建 list,复制该区间内所有元素到新创建的 list 中。这里的迭代器可以来自任何能够提供适当类型的兼容序列,比如另一个 listvector 等。

5. 使用 allocator 构造

虽然较少直接使用,list 还可以通过指定自定义的分配器(allocator)来构造:

cpp 复制代码
list<T, Allocator> lst(customAllocator);

这里的 Allocator 是一个符合 C++ 标准分配器要求的类,用于管理元素的内存分配和释放。如果不指定,默认使用 std::allocator<T>

示例
cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印出来。
 */
void printList(const list<int>&L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L,并添加一些元素
    list<int>L;
    L.push_back(30);
    L.push_back(40);
    L.push_back(50);
    L.push_back(10);
    L.push_back(20);

    // 打印列表L
    printList(L);

    // 通过迭代器初始化一个新的列表L1,包含与L相同的元素
    list<int>L1(L.begin(), L.end());
    printList(L1);

    // 初始化一个包含10个值为100的元素的列表L2
    list<int>L2(10,100);
    printList(L2);

    // 初始化一个常量引用L3,指向列表L2
    const list<int>&L3(L2);
    printList(L3);

    return 0;
}
4.list赋值和交换

在C++中,list容器提供了赋值操作符和交换函数来改变容器的内容。以下是关于list容器赋值和交换的详细说明:

1.赋值操作符
复制赋值
cpp 复制代码
list<T>& operator=(const list<T>& rhs);

这个操作符将rhs(右侧操作数,另一个list对象)的内容复制给左侧的list对象。执行此操作后,左侧的list将拥有与rhs相同的内容,而原先的内容会被覆盖。此操作返回对左侧list的引用,以便可以链接其他操作。

移动赋值
cpp 复制代码
list<T>& operator=(list<T>&& rhs) noexcept(see below);

自C++11起,引入了移动赋值,它将rhs的资源"移动"(而不是复制)给左侧的list对象,如果rhs的资源不再需要,则可能避免了复制的成本。这对于临时对象或即将销毁的对象尤其有用,可以提高效率并减少资源消耗。此操作也是返回左侧list的引用。

2.交换操作
cpp 复制代码
void swap(list<T>& other) noexcept(see below);

swap函数允许交换两个list容器的内容,而无需创建任何副本。它是一种高效的操作,特别是在不需要保留原容器内容的情况下。这个操作是noexcept的,意味着它承诺不抛出异常(除非容器的元素类型的操作可能抛出异常)。

3.示例
cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印。
 */
void printList(const list<int>&L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L,并打印其内容
    list<int>L = {1,3,2,4,6};
    printList(L);
    
    // 创建一个空列表L1,并将其内容设置为列表L的副本,然后打印L1的内容
    list<int>L1;
    L1 = L;
    printList(L1);
    
    // 将列表L的内容移动到列表L1中,并打印L1的新内容
    L1 = std::move(L);
    printList(L1);
    
    // 为列表L2分配L1的内容,并打印L2的内容
    list<int>L2;
    L2.assign(L1.begin(), L1.end());
    printList(L2);
    
    // 为列表L2分配10个值为100的元素,并打印L2的内容
    L2.assign(10,100);
    printList(L2);
    
    // 交换列表L2和L1的内容,并分别打印它们的内容
    L2.swap(L1);
    printList(L2);
    printList(L1);

    return 0;
}

注意:在上述示例中,对于list容器,移动赋值和复制赋值的实际效果是相同的,因为list中的元素是以拷贝的方式存储的,不涉及内部指针的转移。但对于具有动态分配的大型或复杂对象的容器,移动赋值可以显著提升性能。

5.list的大小操作

C++ list 容器提供了几种与大小操作相关的成员函数,用于查询和修改容器的大小。以下是主要的大小操作函数:

1.查询大小
  1. size()

    • 函数原型:size_type size() const;

    • 功能:返回容器中元素的数量。

    • 用法示例:

      cpp 复制代码
      std::list<int> myList = {1, 2, 3, 4, 5};
      std::cout << "List size: " << myList.size() << std::endl;
  2. empty()

    • 函数原型:bool empty() const;

    • 功能:检查容器是否为空。如果容器没有元素,返回true;否则,返回false

    • 用法示例:

      cpp 复制代码
      if (myList.empty()) {
          std::cout << "List is empty." << std::endl;
      } else {
          std::cout << "List is not empty." << std::endl;
      }
2.修改大小
  1. resize(num)

    • 函数原型:void resize(size_type count);

    • 功能:重新指定容器的长度为count。如果容器需要变长,会以默认构造的元素值填充新增的位置;如果容器需要变短,则会移除多出的元素。

    • 用法示例:

      cpp 复制代码
      myList.resize(3); // 如果myList原来有5个元素,现在将只剩下前3个
  2. resize(num, elem)

    • 函数原型:void resize(size_type count, const T& value);

    • 功能:与上面类似,但当容器需要增长时,会用value来初始化新增的元素。

    • 用法示例:

      cpp 复制代码
      myList.resize(7, 0); // 将myList扩展到7个元素,新增的位置用0填充

这些操作允许动态地调整list容器的大小,无论是查询当前状态还是根据需要调整元素数量,都非常直接且方便。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印出来。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表
    list<int> L = {1,2,5,6,4};
    // 打印列表的所有元素
    printList(L);
    // 输出列表的大小
    cout << L.size() << endl;
    // 如果列表不为空,则再次打印列表的所有元素
    if(!L.empty()){
        printList(L);
    }
    // 将列表的大小调整为10,可能删除或添加元素
    L.resize(10);
    // 打印调整大小后的列表的所有元素
    printList(L);
    // 将列表的大小调整为15,如果需要,用3填充新增的部分
    L.resize(15,3);
    // 打印再次调整大小后的列表的所有元素
    printList(L);

    return 0;
}
6.list的插入和删除

C++ list 容器提供了丰富的成员函数来进行元素的插入和删除操作,这些操作基于双向链表的特性,能够高效地在容器的任何位置插入或删除元素,时间复杂度通常为 O(1)。

1.插入操作
  1. push_back(value)

    • 在链表的尾部插入一个元素。

    • 示例:

      cpp 复制代码
      lst.push_back(42);
  2. push_front(value)

    • 在链表的头部插入一个元素。

    • 示例:

      cpp 复制代码
      lst.push_front(13);
  3. insert(iterator pos, value)

    • 在迭代器 pos 指定的位置之前插入一个元素。

    • 示例:

      cpp 复制代码
      auto it = lst.begin();
      ++it; // 移动到第二个元素之前
      lst.insert(it, 99);
  4. insert(iterator pos, size_t count, value)

    • 在迭代器 pos 指定的位置之前插入 countvalue 复制。

    • 示例:

      cpp 复制代码
      lst.insert(lst.begin(), 3, 7);
  5. insert(iterator pos, InputIt first, InputIt last)

    • 在迭代器 pos 指定的位置之前插入由 [first, last) 迭代器指定的范围内所有元素的副本。

    • 示例:

      cpp 复制代码
      std::list<int> anotherList = {1, 2};
      lst.insert(lst.end(), anotherList.begin(), anotherList.end());
2.删除操作
  1. pop_back()

    • 删除链表的最后一个元素。

    • 示例:

      cpp 复制代码
      lst.pop_back();
  2. pop_front()

    • 删除链表的第一个元素。

    • 示例:

      cpp 复制代码
      lst.pop_front();
  3. erase(iterator pos)

    • 删除迭代器 pos 指向的元素。

    • 示例:

      cpp 复制代码
      auto it = lst.begin();
      ++it; // 删除第二个元素
      lst.erase(it);
  4. erase(iterator first, iterator last)

    • 删除由 [first, last) 迭代器指定范围内的所有元素。

    • 示例:

      cpp 复制代码
      lst.erase(lst.begin(), lst.begin()++); // 删除前两个元素  不支持lst.begin()+2的操作 
3.注意事项
  • 插入和删除操作不会导致其他未被直接删除的迭代器失效。
  • 在进行插入或删除操作后,先前获取的迭代器和引用可能会变得无效,特别是当它们指向被删除或插入位置附近的元素时。
  • list容器的插入和删除操作相比vector等连续存储容器具有更高的效率,特别是在中间位置进行操作时,因为它们不需要移动后续的元素。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印整数列表的所有元素。
 * @param L 一个整数列表,其元素将被打印。
 */
/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,将被打印出来。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表
    // 初始化一个整数列表
    list<int> L = {1,2,5,6,4};
    
    // 打印列表的所有元素
    // 打印列表的所有元素
    printList(L);
    
    // 向列表末尾添加一个元素
    // 输出列表的大小
    L.push_back(10);
    // 打印修改后的列表
    printList(L);
    
    // 向列表前端添加一个元素
    L.push_front(20);
    // 打印修改后的列表
    printList(L);
    
    // 移除列表前端的元素
    L.pop_front();
    // 打印修改后的列表
    printList(L);
    
    // 移除列表末尾的元素
    L.pop_back();
    // 打印修改后的列表
    printList(L);
    
    // 在列表中插入一个元素
    auto it = L.begin();
    ++it;
    L.insert(it,99);
    // 打印修改后的列表
    printList(L);
    
    // 在列表中插入多个相同元素
    L.insert(it , 10,100);
    // 打印修改后的列表
    printList(L);
    
    // 从另一个列表复制元素到当前列表
    list<int>L1;
    L1.insert(L1.begin(),L.begin(), L.end());
    // 打印修改后的列表
    printList(L1);
    
    // 移除列表的第一个元素
    L1.erase(L1.begin());
    // 打印修改后的列表
    printList(L1);
    
    // 移除列表的第一个元素到第二个元素之间的所有元素
    L1.erase(L1.begin(),++L1.begin());
    // 打印修改后的列表
    printList(L1);
    
    // 移除列表中所有值为100的元素
    L1.remove(100);
    // 打印修改后的列表
    printList(L1);
    
    // 清空列表
    L1.clear();
    // 打印修改后的列表
    printList(L1);
    
    return 0;
}
7.list的数据存取

在C++的list容器中,由于它基于双向链表实现,不支持随机访问迭代器,因此数据存取主要通过迭代器进行顺序访问。以下是list容器进行数据存取的主要方法:

1.访问元素
  1. front() 和 back()
    • T& front();
    • const T& front() const;
    • T& back();
    • const T& back() const;
    • 这些函数分别返回对链表首元素和尾元素的引用。如果链表为空,调用这些函数会未定义行为。
2.遍历元素

由于list不支持随机访问迭代器,遍历元素通常需要使用正向迭代器或反向迭代器:

cpp 复制代码
std::list<int> myList = {1, 2, 3, 4, 5};

// 正向遍历
for(std::list<int>::iterator it = myList.begin(); it != myList.end(); ++it) {
    std::cout << *it << " ";
}

// 或者使用C++11范围for循环
for(int val : myList) {
    std::cout << val << " ";
}

// 反向遍历
for(std::list<int>::reverse_iterator rit = myList.rbegin(); rit != myList.rend(); ++rit) {
    std::cout << *rit << " ";
}

list容器的数据存取主要依赖于迭代器的顺序遍历,适合于需要高效插入和删除但不频繁随机访问的场景。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,用于打印其所有元素。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L,包含一组数字
    list<int>L = {1,3,4,3,2,5};
    
    // 调用函数打印列表的所有元素
    printList(L);
    
    // 输出列表的第一个元素
    cout << L.front() << endl;
    
    // 输出列表的最后一个元素
    cout << L.back() << endl;
    
    // 获取列表的开始迭代器,并移动到第二个元素
    auto it = L.begin();
    cout << *(++it) << endl;
    
    return 0;
}
8.list的反转和排序

C++ list 容器提供了内置的方法来实现反转和排序操作。以下是这两个操作的详细介绍:

1.反转

list容器提供了成员函数reverse()来反转容器中元素的顺序。调用此函数后,容器中的元素会按照相反的顺序排列。

cpp 复制代码
std::list<int> myList = {1, 2, 3, 4, 5};
myList.reverse();

在这段代码中,myList的内容将会变为5, 4, 3, 2, 1

2.排序

排序操作稍微复杂一点,因为STL的list容器没有直接提供像vector那样的sort()成员函数。不过,你可以使用标准算法std::sort(),但需要注意的是,std::sort()要求容器提供随机访问迭代器,而list只提供双向迭代器,因此不能直接使用std::sort()

对于list,应该使用std::list特有的成员函数sort(),它需要元素之间定义了比较操作。如果元素类型自身定义了<运算符(即实现了std::less比较),那么可以直接调用:

cpp 复制代码
std::list<int> myList = {5, 3, 1, 4, 2};
myList.sort(); // 对于int类型,默认升序排序

如果需要自定义排序规则,可以传递一个比较函数对象:

cpp 复制代码
struct DescendingComparator {
    bool operator()(const int& a, const int& b) const {
        return a > b; // 降序
    }
};

std::list<int> myList = {1, 3, 5, 2, 4};
myList.sort(DescendingComparator()); // 降序排序

或者使用C++11以后的lambda表达式简化代码:

cpp 复制代码
std::list<int> myList = {1, 3, 5, 2, 4};
myList.sort([](const int& a, const int& b) { return a > b; }); // 降序排序

请注意,list::sort()函数是稳定的排序,意味着相等的元素的相对顺序不会改变,并且这个操作在平均情况下具有O(n log n)的时间复杂度。

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/1.
//
#include <iostream>
#include <list>
using namespace std;

/**
 * 打印列表中的所有元素。
 * @param L 一个整数列表,用于打印其所有元素。
 */
void printList(const list<int>& L){
    for(int it : L){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个整数列表L
    list<int>L = {1,3,2,5,6};
    // 打印列表L的原始顺序
    printList(L);
    // 反转列表L中的元素顺序
    L.reverse();
    // 打印反转后的列表L
    printList(L);
    // 对列表L进行排序(升序)
    L.sort();
    // 打印排序后的列表L
    printList(L);
    // 对列表L进行排序(降序)
    L.sort([](const int &a,const int &b){return a > b;});
    // 打印降序排序后的列表L
    printList(L);
    return 0;
}
9.list排序案例

将学生类按年龄降序排列,年龄相同则按身高排列。

cpp 复制代码
#include <iostream>
#include <list>
#include <utility>
using namespace std;

/**
 * 学生类,用于存储学生的信息。
 * @param name 学生的姓名。
 * @param age 学生的年龄。
 * @param len 学生的身高。
 */
class Student{
public:
    string name;
    int len;
    int age;
    /**
     * 构造函数,初始化学生对象。
     * @param name 学生的姓名。
     * @param age 学生的年龄。
     * @param len 学生的身高。
     */
    Student(string name,int age,int len) : name(std::move(name)),age(age),len(len){

    }
};

/**
 * 打印学生列表。
 * @param L 学生列表。
 */
void printList(const list<Student>&L){
    for(const Student& it : L){
        cout << it.name << " " << it.age << " " << it.len << endl;
    }
}

int main(){
    // 创建并添加学生对象到列表
    Student stu("张三",18,158);
    list<Student>L;
    L.push_back(stu);
    Student stu2("李四",16,155);
    L.push_back(stu2);
    Student stu3("王五",17,166);
    L.push_back(stu3);
    Student stu4("赵六",19,165);
    L.push_back(stu4);
    Student stu5("孙七",16,185);
    L.push_back(stu5);
    // 打印学生列表
    printList(L);
    cout << "--------------------" << endl;
    // 对学生列表按年龄和身高进行排序
    L.sort([](const Student &stu,const Student &stu1){
        if(stu.age != stu1.age) return stu.age > stu1.age;
        else return stu.len > stu1.len;
    });
    // 打印排序后的学生列表
    printList(L);
    return 0;
}

Lambda表达式是C++11引入的一种匿名函数对象,它允许快速定义并创建小型的、一次性的功能函数。Lambda表达式提供了一种简洁、灵活的方式来编写内联代码块,特别适用于传递给算法或作为回调函数。Lambda的基本语法结构如下:

cpp 复制代码
[capture-list] (parameters) -> return-type { function-body }

这里每个部分的含义如下:

  • capture-list (捕获列表): 定义了lambda函数体内部可以访问的外部变量。可以捕获外部变量的值(值捕获)或引用(引用捕获),也可以默认捕获整个作用域。例如,[x, &y]表示捕获x的值和y的引用。

  • parameters(参数列表): 类似于常规函数的参数列表,定义了lambda接受的参数。

  • -> return-type(返回类型声明): 可选部分,显式指定lambda表达式的返回类型。如果不写,默认根据函数体进行推导。

  • function-body(函数体): 包含了实际执行的代码。

Lambda表达式提高了代码的灵活性和可读性,使得可以在不定义独立函数的情况下直接在代码中创建小型函数。

8.set和multiset容器

C++的std::set容器是STL(标准模板库)中的一个关联容器,它用于存储唯一对象的集合。每个元素的位置都是由它的值决定的,这意味着std::set中的元素会自动排序。std::set底层通常通过红黑树实现,提供了对数时间复杂度的插入、删除和查找操作。

1.基本特性
  1. 有序性std::set中的元素默认按照升序排序。也可以通过传入自定义的比较函数来改变排序方式。
  2. 唯一性:不允许有重复的元素,根据元素的排序准则,如果有重复的元素尝试插入,它们会被自动忽略。
  3. 动态大小:容器的大小会根据需要自动调整。
  4. 迭代器:提供了向前和向后遍历的迭代器,但不支持随机访问迭代器,因为元素在内存中不是连续存储的。

常用操作

  • 构造

    cpp 复制代码
    std::set<int> mySet; // 创建一个空的int型set
    std::set<std::string, std::greater<std::string>> mySetStr; // 使用自定义排序(降序)
  • 插入

    cpp 复制代码
    mySet.insert(42); // 插入一个元素
    mySet.insert({1, 2, 3}); // 插入多个元素
  • 查找

    cpp 复制代码
    if (mySet.find(42) != mySet.end()) {
        std::cout << "Found 42 in set.\n";
    }
  • 删除

    cpp 复制代码
    mySet.erase(42); // 删除一个特定值的所有实例
    mySet.erase(it); // 删除迭代器it指向的元素
    mySet.clear(); // 清空整个集合
  • 检查存在性

    cpp 复制代码
    if (mySet.count(42)) {
        std::cout << "42 is in the set.\n";
    }
  • 大小与空判断

    cpp 复制代码
    std::cout << "Size: " << mySet.size() << "\n";
    if (mySet.empty()) {
        std::cout << "Set is empty.\n";
    }
  • 遍历

    cpp 复制代码
    for (const auto& elem : mySet) {
        std::cout << elem << " ";
    }

自定义比较函数

可以通过传递比较函数对象作为第二个模板参数来自定义元素的排序规则。例如,如果要创建一个按字符串长度排序的set,可以这样做:

cpp 复制代码
struct CompareByLength {
    bool operator()(const std::string& s1, const std::string& s2) const {
        return s1.size() < s2.size();
    }
};

std::set<std::string, CompareByLength> mySetByLength;

总结

std::set容器非常适合需要维护唯一且有序数据集合的场景。它的主要优点在于自动排序和保证元素唯一性,但这也意味着插入和删除操作相比无序容器(如std::unordered_set)可能更耗时。选择使用std::set还是其他容器应基于具体的应用需求。

2.multiset容器

在C++的STL(标准模板库)中,std::setstd::multiset都是关联容器,主要用于存储元素的集合,它们的主要区别在于对元素唯一性的处理上:

std::set

  • 唯一性std::set不允许容器中有重复的元素。这意味着如果你尝试插入一个已经存在于set中的元素,插入操作将被忽略,容器不会发生任何变化。
  • 排序 :所有元素都会在插入时自动被排序。排序是基于元素的比较操作符(默认为 < 运算符),或者你可以在创建set时提供一个自定义的比较函数。
  • 用途:适用于需要存储唯一且有序数据的场景,如索引、去重等。

std::multiset

  • 重复性 :与std::set不同,std::multiset允许容器中有重复的元素。它可以存储多个具有相同值的元素,并且这些相同值的元素在multiset中的顺序取决于它们的插入顺序。
  • 排序:虽然允许重复,但元素仍然是排序的,同样基于比较操作符或自定义比较函数。
  • 用途:适合那些需要记录元素出现次数或者保持元素插入顺序的同时还需要维持元素排序的场景,如统计词频、事件日志等。

共同点

  • 底层结构:两者通常都采用红黑树实现,因此提供了对数时间复杂度的高效插入、删除和查找操作。
  • 迭代器:都提供了迭代器支持,可以用来遍历容器中的元素,但不支持随机访问迭代器,因为它们是基于节点的链式存储结构。
  • 动态大小:容器大小会根据元素的插入和删除动态调整。

应用决策

选择使用std::set还是std::multiset取决于你的具体需求:

  • 如果你的应用逻辑要求集合中的元素必须唯一,那么你应该使用std::set
  • 如果你需要保留重复元素并能对它们进行排序和计数,则std::multiset是更好的选择。
3.set的构造和赋值

std::set容器提供了多种构造方法和赋值操作。

1.构造函数
  1. 默认构造函数

    cpp 复制代码
    std::set<T> mySet; // T 是元素类型,例如 int, std::string 等

    创建一个空的set,元素类型为T,按照T的默认排序方式进行排序。

  2. 拷贝构造函数

    cpp 复制代码
    std::set<T> mySetCopy(mySet);

    创建一个新的set,它是已有setmySet)的一个拷贝。

  3. 带有比较函数的构造函数

    cpp 复制代码
    struct MyComparator {
        bool operator()(const T& lhs, const T& rhs) const { /* 自定义比较逻辑 */ }
    };
    std::set<T, MyComparator> mySetWithComparator;

    创建一个空的set,使用自定义的比较函数MyComparator

  4. 区间构造函数

    cpp 复制代码
    std::set<T> mySet(begin(iterable), end(iterable));

    从一个迭代器范围(如另一个容器或数组)构造set,只包含范围内唯一且排序后的元素。

  5. 初始化列表构造(C++11起):

    cpp 复制代码
    std::set<int> mySet = {1, 3, 2}; // 注意自动排序为{1, 2, 3}
2.赋值操作
  1. 拷贝赋值

    cpp 复制代码
    std::set<T> mySet;
    std::set<T> anotherSet;
    // ...填充anotherSet
    mySet = anotherSet; // mySet 现在是 anotherSet 的拷贝
  2. 区间赋值

    cpp 复制代码
    std::set<T> mySet;
    // ...填充mySet
    std::set<T> anotherSet;
    anotherSet.assign(begin(iterable), end(iterable)); // 用 iterable 中的元素替换 anotherSet 的内容
  3. 初始化列表赋值(C++11起):

    cpp 复制代码
    std::set<int> mySet = {4, 2, 5, 1}; // 初始化后自动排序为{1, 2, 4, 5}
3.注意事项
  • 在赋值和构造时,如果源集合中有重复元素(对于std::set而言,这是不可能的情况,因为std::set不允许重复),目标集合将只保留唯一的元素。
  • 所有这些操作都不会影响原集合的迭代器、引用和指针的有效性,除非进行的是自赋值操作(即将一个容器赋值给自己),此时容器保持不变。
  • 当涉及到大型集合时,上述操作的性能需考虑,尤其是拷贝构造和赋值可能会涉及大量元素的复制。C++11引入了右值引用和移动语义,可以更高效地处理这类情况。
cpp 复制代码
// 包含标准输入输出库和集合库
#include <iostream>
#include <set>
using namespace std;

/**
 * 打印集合中的所有元素
 * @param S 需要打印的集合
 */
void printSet(const set<int>& S){
    // 遍历集合中的每个元素并打印
    for(auto &it : S){
        cout << it << " ";
    }
    cout << endl;
}

int main(){
    // 初始化一个集合S并填充元素
    set<int> S = {1,2,3,4};
    // 调用函数打印集合S
    printSet(S);
    
    // 引用集合S,创建常量引用S1
    const set<int>& S1(S);
    // 调用函数打印引用S1
    printSet(S1);
    
    // 初始化一个空集合S2
    set<int> S2;
    // 将集合S的值赋给集合S2
    S2 = S;
    // 调用函数打印集合S2
    printSet(S2);
    
    return 0;
}
4.set的大小和交换

在C++的std::set容器中,获取容器的大小以及交换两个容器的内容是常见的操作。

1.获取大小

使用size()成员函数可以获取std::set容器中当前元素的数量。

cpp 复制代码
std::set<int> mySet = {1, 2, 3, 4, 5};
std::cout << "Size of mySet: " << mySet.size() << std::endl;
2.检查是否为空

使用empty()成员函数可以检查std::set容器是否为空。

cpp 复制代码
if (mySet.empty()) {
    std::cout << "mySet is empty." << std::endl;
} else {
    std::cout << "mySet is not empty." << std::endl;
}
3.交换两个set

std::set提供了swap()成员函数,可以高效地交换两个集合的所有元素,不会抛出异常。

cpp 复制代码
std::set<int> anotherSet = {6, 7, 8, 9, 10};
mySet.swap(anotherSet); // 交换mySet和anotherSet的内容
4.示例综合
cpp 复制代码
// 包含标准输入输出库和集合库
#include <iostream>
#include <set>
// 使用标准命名空间,简化代码中标准库函数的调用
using namespace std;

/**
 * 打印集合中的所有元素
 * @param S 需要打印的集合,以引用传递,避免复制集合导致的性能损失
 */
void printSet(const set<int>& S){
    // 遍历集合中的每个元素,并打印它们
    for(auto &it : S){
        cout << it << " ";
    }
    // 换行,以便后续输出开始于新行
    cout << endl;
}

int main(){
    // 初始化一个集合S,包含整数5, 4, 6, 8, 9
    set<int>S = {5,4,6,8,9};
    // 调用函数打印集合S的内容
    printSet(S);
    // 输出集合S的元素数量
    cout << S.size() << endl;
    // 检查集合S是否为空,如果不为空,则再次打印集合S的内容
    if(!S.empty()){
        printSet(S);
    }
    // 初始化另一个空集合S1
    set<int>S1;
    // 交换集合S和S1的内容,将S的内容转移到S1中,S变为空集合
    S.swap(S1);
    // 分别打印交换后的集合S和S1的内容
    printSet(S);
    printSet(S1);
    return 0;
}

这段代码首先展示了如何获取两个集合的大小以及检查它们是否为空,然后使用swap()函数交换这两个集合的内容,并再次打印交换后的大小,展示了容器大小的查询和元素交换的基本操作。

5.set容器插入和删除

在C++的std::set容器中,插入和删除元素是非常基本且常用的操作。

1.插入元素
  1. 单个元素插入 :使用insert()函数可以向集合中插入一个元素。如果元素已经存在(根据排序准则判断),则不会插入重复项。

    cpp 复制代码
    std::set<int> mySet;
    mySet.insert(42); // 尝试插入数字42
  2. 批量插入:可以通过迭代器范围或初始化列表一次性插入多个元素。重复元素会被自动忽略。

    cpp 复制代码
    mySet.insert({1, 2, 3, 4}); // 使用初始化列表批量插入
    std::vector<int> vec = {5, 6, 7};
    mySet.insert(vec.begin(), vec.end()); // 使用迭代器范围插入
2.删除元素
  1. 删除指定值 :使用erase()函数并提供待删除元素的值。如果该值在集合中存在,则会删除所有匹配的元素。

    cpp 复制代码
    mySet.erase(42); // 删除值为42的所有元素(如果有)
  2. 通过迭代器删除:可以删除特定迭代器指向的元素。

    cpp 复制代码
    auto it = mySet.find(42); // 查找值为42的元素
    if (it != mySet.end()) {
        mySet.erase(it); // 删除找到的元素
    }
  3. 清空集合 :使用clear()函数可以删除集合中的所有元素。

    cpp 复制代码
    mySet.clear(); // 清空整个集合
3.示例

下面是一个综合示例,展示了如何在std::set中插入和删除元素:

cpp 复制代码
// 包含标准输入输出库和集合库
#include <iostream>
#include <set>
using namespace std;

/**
 * 打印集合中的元素
 * @param S 集合,其元素将被打印
 */
void printSet(const set<int>& S){
    // 遍历集合并打印每个元素
    for(auto &it : S){
        cout << it << " ";
    }
    // 换行以分隔不同集合的打印输出
    cout << endl;
}

int main(){
    // 初始化一个整数集合S,包含一组预定义的元素
    set<int>S = {1,8,9,6,3,4};
    // 打印集合S的初始状态
    printSet(S);
    // 向集合S中插入新元素11
    S.insert(11);
    // 打印插入11后的集合S
    printSet(S);
    // 从集合S中删除元素11
    S.erase(11);
    // 打印删除11后的集合S
    printSet(S);
    // 从集合S中删除第一个元素(即1)
    S.erase(S.begin());
    // 打印删除第一个元素后的集合S
    printSet(S);
    // 从集合S中删除第一个元素到第二个元素之间的所有元素(即删除1和3)
    S.erase(S.begin(),++S.begin());
    // 打印删除部分元素后的集合S
    printSet(S);
    // 清空集合S的所有元素
    S.clear();
    // 打印清空后的集合S
    printSet(S);
    return 0;
}

这段代码演示了如何插入单个和多个元素、尝试插入已存在的元素、删除特定值以及清空整个集合。

6.set容器的查找和统计

在C++的std::set容器中,查找和统计元素是通过几个核心成员函数完成的,这些操作利用了std::set的有序特性,提供了高效的实现。

1.查找元素
  • find() : 使用此函数可以查找集合中是否存在某个特定值的元素。如果找到,它会返回一个指向该元素的迭代器;如果没有找到,则返回end()迭代器。

    cpp 复制代码
    std::set<int>::iterator it = mySet.find(30);
    if (it != mySet.end()) {
        std::cout << "Found element: " << *it << std::endl;
    } else {
        std::cout << "Element not found." << std::endl;
    }
2.统计元素个数
  • count() : 对于std::set容器,由于它不存储重复元素,因此count()函数的结果只有两种可能:如果元素存在,返回1;如果不存在,返回0。这与std::multiset不同,后者可能返回大于1的计数。

    cpp 复制代码
    int numElements = mySet.count(30);
    std::cout << "Number of occurrences of 30: " << numElements << std::endl;
3.注意事项
  • 查找效率 :由于std::set底层实现通常为红黑树,查找操作的平均时间复杂度为O(log n),其中n是容器中元素的数量,因此非常高效。
  • 统计意义 :在std::set中,由于每个元素都是唯一的,count()函数主要用于逻辑上的确认某个值是否存在,而不用于计算重复次数,因为不会有重复。

结合查找和统计,可以有效地管理并检索std::set中的元素,进行条件检查、元素确认等操作。

cpp 复制代码
// 包含输入输出流和集合的头文件
#include <iostream>
#include <set>
// 使用标准命名空间,简化代码中标准库函数的调用
using namespace std;

/**
 * 打印集合中的所有元素
 * @param S 一个整数集合,作为函数的输入
 */
void printSet(const set<int>& S){
    // 遍历集合中的每个元素,并打印它们
    for(auto &it : S){
        cout << it << " ";
    }
    // 换行,以便后续输出开始于新行
    cout << endl;
}

int main(){
    // 初始化一个整数集合S,包含一组有序的不重复整数
    set<int>S = {1,2,3,6,5,4,8,9};
    // 调用函数打印集合S的内容
    printSet(S);
    // 寻找集合中值为11的元素的位置
    auto pos = S.find(11);
    // 根据查找结果判断是否找到值为11的元素,并输出相应信息
    cout << (pos != S.end() ? "找到该数据" : "未找到") << endl;
    // 输出集合中值为1的元素的数量
    cout << S.count(1) << endl;
    // 程序正常结束
    return 0;
}
9.set和multiset的区别
cpp 复制代码
// 包含标准输入输出库和集合库
#include <iostream>
#include <set>
using namespace std;

// 主函数,程序的入口点
int main(){
    // 创建一个集合对象S,集合内的元素保证唯一
    set<int>S;
    // 创建一个pair对象ret,用于存储插入操作的结果:迭代器和插入标志
    pair<set<int>::iterator ,bool>ret;

    // 尝试向集合S中插入元素10
    ret = S.insert(10);
    // 检查插入操作是否成功,如果成功则输出"success"
    if (ret.second){
        cout << "success" << endl;
    } else{
        cout << "fail" << endl;
    }

    // 再次尝试向集合S中插入元素10,由于集合内已存在,此次插入应失败
    ret = S.insert(10);
    // 检查插入操作是否成功,输出相应的信息
    if (ret.second){
        cout << "success" << endl;
    } else{
        cout << "fail" << endl;
    }

    // 创建一个多重集合对象MS,允许包含重复元素
    multiset<int>MS;
    // 向多重集合MS中插入元素100
    MS.insert(100);
    // 再次插入元素100,多重集合允许重复元素
    MS.insert(100);

    // 遍历多重集合MS,输出其中的所有元素
    for(auto &it : MS){
        cout << it << endl;
    }

    // 程序正常结束
    return 0;
}
9.pair对组的创建

在C++中,std::pair是一种方便的工具,用于将两个相关联的数据作为一个单元存储。这对处理映射关系、返回多值函数结果等场景非常有用。创建std::pair的方式有几种,以下是几种常见的方法:

1. 直接构造

可以直接通过构造函数创建std::pair对象,指定两个元素的类型和值。

cpp 复制代码
std::pair<int, std::string> myPair(42, "Hello");
2. 使用make_pair函数

std::make_pair是一个辅助函数,可以根据提供的两个参数自动推导类型并创建std::pair对象。

cpp 复制代码
auto anotherPair = std::make_pair(3.14, "Pi");
3. 初始化列表

C++11起,可以使用初始化列表语法创建std::pair,这在类型已知的情况下特别简洁。

cpp 复制代码
std::pair<char, double> pairWithInitList{'A', 3.14159};
4. 结构化绑定(C++17起)

在C++17中引入了结构化绑定,可以直接解包std::pair(或tuple等)到单独的变量中,但这不是创建pair的方式,而是使用它的一种方式。

cpp 复制代码
auto [key, value] = std::make_pair('Z', 26.0);
// key 现在是 'Z',value 是 26.0
示例

下面是一个综合示例,展示了不同方式创建std::pair并对它们进行操作:

cpp 复制代码
// 包含标准输入输出库
#include <iostream>

// 使用标准命名空间,简化IO操作
using namespace std;

// 程序入口函数
int main(){
    // 创建一个pair对象,用于表示一个人的名字和年龄
    // string类型的first成员存储名字,int类型的second成员存储年龄
    pair<string,int>p("Jack",17);
    
    // 使用make_pair函数创建另一个pair对象,同样表示一个人的名字和年龄
    pair<string ,int>p1 = make_pair("Li",19);
    
    // 输出第一个pair对象的名字和年龄
    cout << p.first << " " << p.second << endl;
    
    // 输出第二个pair对象的名字和年龄
    cout << p1.first << " " << p1.second << endl;
    
    // 程序正常结束
    return 0;
}

这段代码演示了如何使用不同方法创建std::pair实例,并打印它们的值。注意,根据上下文和偏好,可以选择最适合的创建方式。

10.set内置数据类型排序
cpp 复制代码
/**
 * @file main.cpp
 * @brief 此程序演示了如何使用自定义比较函数对象在set中存储整数并按降序遍历。
 *
 * Created by 86189 on 2024/7/2.
 */

#include <iostream>
#include <set>
using namespace std;

/**
 * @class Compare
 * @brief 自定义比较函数对象,用于比较两个整数,使得set按降序排列。
 */
class Compare {
public:
    /**
     * @brief 比较运算符重载,返回true当且仅当第一个整数大于第二个整数。
     *
     * @param a 第一个整数引用。
     * @param b 第二个整数引用。
     * @return 返回布尔值,表示a是否大于b。
     */
    bool operator()(const int &a, const int &b) const {
        return a > b;
    }
};

int main() {
    /**
     * @var S
     * @brief 使用自定义比较函数对象Compare的set容器,存储降序排列的整数。
     */
    set<int, Compare> S;

    // 向set中插入整数
    S.insert(10);
    S.insert(20);
    S.insert(15);

    /**
     * @brief 遍历set并打印其内容,展示降序排列的效果。
     */
    for (auto &it : S) {
        std::cout << it << std::endl;  // 输出降序排列的整数
    }

    return 0;  // 程序正常结束
}
11.set对自定义数据类型排序
cpp 复制代码
// 包含必要的头文件
#include <iostream>
#include <set>
#include <utility>

// 使用标准命名空间,简化代码中标准库的调用
using namespace std;

/**
 * @brief 学生类定义
 * 
 * 该类用于表示学生的信息,包括姓名、性别和年龄。
 */
class Student{
public:
    // 使用移动语义优化构造函数参数传递,提高效率
    string name;
    string sex;
    int age{};
    /**
     * @brief 构造函数
     * 
     * @param name 学生的姓名
     * @param sex 学生的性别
     * @param age 学生的年龄
     */
    Student(string name,string sex,int age): name(std::move(name)),sex(std::move(sex)),age(age){

    }
};

/**
 * @brief 自定义比较函数类
 * 
 * 该类用于定义对学生对象的比较方式,这里比较的是学生的年龄。
 */
class Compare{
public:
    /**
     * @brief 比较两个学生对象的年龄
     * 
     * @param s1 第一个学生对象
     * @param s2 第二个学生对象
     * @return bool 如果s1的年龄大于s2的年龄返回true,否则返回false
     */
    bool operator()(const Student& s1,const Student& s2) const{
        return s1.age > s2.age;
    }
};

int main(){
    // 使用自定义比较函数类实例化set容器,用于存储学生对象
    set<Student,Compare>S;
    // 创建并插入学生对象到set中
    Student stu1("Jack","男",16);
    S.insert(stu1);
    Student stu2("Mala","男",18);
    S.insert(stu2);
    Student stu3("Joko","女",19);
    S.insert(stu3);
    Student stu4("Mali","男",17);
    S.insert(stu4);
    Student stu5("Doro","女",15);
    S.insert(stu5);
    // 遍历set容器,输出每个学生的信息
    for(auto &it : S){
        cout << it.name << " " << it.sex << " " << it.age << endl;
    }

    return 0;
}

9.map和multimap容器

在C++ STL(标准模板库)中,mapmultimap是两种关联容器,它们用于存储键值对,其中每个键都是唯一的,并且与一个特定的值相关联。然而,它们之间存在一些关键区别:

map

  1. 唯一性map中的键是唯一的,也就是说,一个键只能对应一个值。如果你尝试插入一个键值对,而该键已经存在于map中,那么原有的键对应的值将被新值替换。

  2. 排序map中的元素按照键的升序(默认情况下)进行排序。这是通过比较键实现的,可以自定义比较函数来改变排序方式。

  3. 基本操作 :主要操作包括插入(insert)、删除(erase)、查找(find)和访问(直接通过键访问,如map[key])元素。

multimap

  1. 非唯一性 :与map的主要区别在于,multimap允许有多个相同的键,每个键可以对应多个不同的值。这意味着当你插入一个键已经存在的键值对时,新值会被添加到该键已有的值集合中,而不是替换原有值。

  2. 排序 :和map一样,multimap中的元素也是根据键进行排序的,可以是升序或根据自定义的比较函数排序。

  3. 基本操作 :除了与map共享的基本操作外,由于可能有多个相同键的存在,multimap的查找操作(如find)可能会返回一个迭代器范围,表示所有匹配键的值集合。

使用场景

  • map适用于当你需要确保每个键只对应一个值,且经常需要根据键快速查找或更新其关联值的场景。

  • multimap 适合那些一个键可能对应多个值,且在查询时你可能关心所有与某一键相关联的值的情况,比如存储学生的分数时,如果有多个学生获得相同的分数,multimap能很好地处理这种情况。

1.map容器的构造和赋值

在C++中,std::map容器提供了多种构造函数和赋值操作,以方便初始化和管理键值对数据。

1.构造函数
  1. 默认构造函数

    cpp 复制代码
    std::map<Key, T> m; // 创建一个空的map,默认按键升序排序。
  2. 拷贝构造函数

    cpp 复制代码
    std::map<Key, T> m1;
    // 假设m1已经被初始化
    std::map<Key, T> m2(m1); // m2是m1的一个拷贝
  3. 使用比较函数构造

    cpp 复制代码
    struct MyCompare {
        bool operator()(const Key& lhs, const Key& rhs) const {
            // 自定义比较逻辑
            return lhs < rhs; // 示例:使用默认比较
        }
    };
    std::map<Key, T, MyCompare> m(MyCompare()); // 使用自定义比较函数
  4. 区间构造

    cpp 复制代码
    std::map<Key, T> m.begin(), m.end());
    // 或者使用其他容器/迭代器范围
    std::vector<std::pair<Key, T>> vec;
    // 假设vec已被填充
    std::map<Key, T> m(vec.begin(), vec.end());
2.赋值操作
  1. 拷贝赋值运算符

    cpp 复制代码
    std::map<Key, T> m1, m2;
    // 假设m1和m2已经被初始化
    m1 = m2; // 将m2的内容赋值给m1
  2. 初始化列表赋值(C++11及以上)

    cpp 复制代码
    std::map<Key, T> m = { {key1, value1}, {key2, value2}, ... };
  3. 区间赋值

    cpp 复制代码
    std::map<Key, T> m1, m2;
    // 假设m2已被填充
    m1.assign(m2.begin(), m2.end());
  4. 插入迭代器对

    虽然这不是直接的赋值操作,但可以通过迭代器对插入元素来"赋值"或填充map。

    cpp 复制代码
    std::map<Key, T> m;
    std::vector<std::pair<Key, T>> vec;
    // 假设vec已被填充
    m.insert(vec.begin(), vec.end());

这些构造和赋值方法提供了灵活的方式来创建和管理std::map容器,满足不同场景下的需求。

cpp 复制代码
 #include <iostream>
 #include <map>
 
 // 使用标准命名空间,简化后续代码中的类型名称
 using namespace std;
 
 /**
  * 打印map的内容
  * @param m 一个整数到整数的映射,其内容将被打印
  */
 void printMap(const map<int,int>& m){
     // 遍历映射中的所有元素,并打印每个元素的键和值
     for(auto it : m){
         cout << it.first << " " << it.second << endl;
     }
 }
 
 int main(){
     // 初始化一个map,包含两个元素
     map<int,int> m = {{1,2},{2,4}};
     // 创建一个对m的常量引用,以避免复制map的内容
     const map<int,int>& m1(m);
     // 调用函数打印map的内容
     printMap(m);
     printMap(m1);
     // 初始化一个空的map,并将其设置为与m相同的内容
     map<int,int> m2;
     m2 = m;
     printMap(m2);
 
     return 0;
 }
2.map容器的大小和交换

在C++的STL中,std::map容器提供了以下与大小和交换相关的成员函数:

1.获取大小
  • size() : 此函数返回map中元素的数量,即键值对的总数。

    cpp 复制代码
    std::map<Key, T> myMap;
    // ... 添加元素到myMap
    std::cout << "Size of the map: " << myMap.size() << std::endl;
  • empty() : 判断map是否为空,如果容器中没有元素,则返回true,否则返回false

    cpp 复制代码
    if (myMap.empty()) {
        std::cout << "The map is empty." << std::endl;
    } else {
        std::cout << "The map is not empty." << std::endl;
    }
2.交换内容
  • swap(std::map<Key, T>& other) : 此函数交换两个map容器的内容,交换操作高效执行,不会涉及元素的复制或移动。

    cpp 复制代码
    std::map<Key, T> myMap1, myMap2;
    // ... 分别填充myMap1和myMap2
    myMap1.swap(myMap2);
    // 现在myMap1包含myMap2原来的内容,反之亦然

这些函数允许检查容器的当前状态(是否为空,包含多少元素)以及重新分配两个容器的内容,而无需创建新的容器副本。这对于优化内存使用和性能特别有用。

cpp 复制代码
//
// Created by 86189 on 2024/7/3.
//
#include <iostream>
#include <map>

// 使用标准命名空间,简化后续代码中标准库类型的引用
using namespace std;

/**
 * 打印map的内容
 * @param m 一个整数到整数的映射,其内容将被打印
 */
void printMap(const map<int,int>& m){
    // 遍历map,打印每个键值对
    for(auto it : m){
        cout << it.first << " " << it.second << endl;
    }
}

int main(){
    // 初始化一个map,包含两个元素
    map<int,int> m{{1,2},{2,4}};
    // 调用函数打印map内容
    printMap(m);
    // 打印map的大小
    cout << m.size() << endl;
    // 检查map是否为空,如果不为空,则再次打印其内容
    if(!m.empty()){
        printMap(m);
    }
    // 初始化另一个空的map
    map<int,int> m1;
    // 交换两个map的内容
    m.swap(m1);
    // 分别打印交换后的两个map的内容
    printMap(m);
    printMap(m1);
    return 0;
}
3.map容器的插入和删除

在C++的STL中,std::map容器提供了丰富的成员函数来进行元素的插入和删除操作。以下是几种常见的插入和删除方法:

1.插入元素
  1. insert(const value_type& val): 插入一个键值对。如果键已存在,则不会插入重复的键。

    cpp 复制代码
    myMap.insert(std::make_pair(key, value));
    // 或者使用直接初始化语法(C++11起)
    myMap.insert({key, value});
  2. insert(iterator hint, const value_type& val) : 尝试在迭代器hint指向的位置附近插入元素,以提高效率。如果hint恰好指向要插入位置的下一个位置,则插入操作可能更快。

    cpp 复制代码
    auto it = myMap.lower_bound(key);
    myMap.insert(it, std::make_pair(key, value));
  3. insert(initializer_list<value_type> ilist): 从初始化列表插入多个键值对(C++11起)。

    cpp 复制代码
    myMap.insert({ {key1, value1}, {key2, value2}, ... });
2.删除元素
  1. erase(iterator position) : 删除迭代器position指向的元素。

    cpp 复制代码
    auto it = myMap.find(key);
    if (it != myMap.end()) {
        myMap.erase(it);
    }
  2. erase(const key_type& key): 删除具有指定键的元素。

    cpp 复制代码
    myMap.erase(key);
  3. erase(iterator first, iterator last) : 删除迭代器范围[first, last)内的所有元素。

    cpp 复制代码
    auto startIt = myMap.lower_bound(startKey);
    auto endIt = myMap.upper_bound(endKey);
    myMap.erase(startIt, endIt);
3.注意事项
  • 在删除元素后,之前指向被删除元素的迭代器、指针和引用都会变得无效。
  • erase返回的是下一个元素的迭代器,这在连续删除元素时很有用。
  • 对于insert操作,如果试图插入的键已经存在,该操作不会改变容器且不会抛出异常(除非因内存分配失败)。

通过这些方法,你可以有效地管理std::map容器中的数据,无论是添加新元素还是移除不需要的条目。

cpp 复制代码
//
// Created by 86189 on 2024/7/3.
//
#include <iostream>
#include <map>

// 使用标准命名空间,简化后续代码中标准库类型的引用
using namespace std;

/**
 * 打印map的内容
 * @param m 一个整数到整数的映射,其内容将被打印
 */
void printMap(const map<int,int>& m){
    // 遍历map,打印每个键值对
    for(auto it : m){
        cout << it.first << " " << it.second << endl;
    }
}

int main(){
    // 初始化一个map,包含两个元素
    map<int,int> m{{1,2},{2,4}};
    // 调用函数打印map内容
    printMap(m);
    // 打印map的大小
    cout << m.size() << endl;
    // 检查map是否为空,如果不为空,则再次打印其内容
    if(!m.empty()){
        printMap(m);
    }
    // 初始化另一个空的map
    map<int,int> m1;
    // 交换两个map的内容
    m.swap(m1);
    // 分别打印交换后的两个map的内容
    printMap(m);
    printMap(m1);
    return 0;
}
4.map容器的查找和统计

在C++的STL中,std::map容器提供了几种查找和统计元素的方法:

1.查找元素
  1. find(const key_type& k) : 返回一个迭代器,指向键为k的第一个元素。如果找不到这样的元素,则返回end()

    cpp 复制代码
    auto it = myMap.find(key);
    if (it != myMap.end()) {
        std::cout << "Found element with key: " << it->first << ", value: " << it->second << std::endl;
    } else {
        std::cout << "Element not found." << std::endl;
    }
  2. count(const key_type& k) : 返回键为k的元素数量。对于map,由于键唯一,此函数要么返回0(表示不存在),要么返回1(表示存在)。

    cpp 复制代码
    size_t count = myMap.count(key);
    if (count > 0) {
        std::cout << "Key exists in the map." << std::endl;
    } else {
        std::cout << "Key does not exist in the map." << std::endl;
    }
2.统计元素

map容器中,由于键的唯一性,统计通常较为直接,主要依赖于count函数来确定某个键是否存在。对于键值对的计数,通常意义不大,因为每个键最多只能对应一个值。但在多值映射multimap中,count可以有效统计相同键值出现的次数。

3.示例

假设有一个std::map<int, std::string>类型的myMap,以下是如何使用查找和统计方法的简单示例:

cpp 复制代码
#include <iostream>
#include <map>

// 使用标准命名空间,简化后续代码中标准库的调用
using namespace std;

/**
 * 打印map中的所有元素
 * @param m 一个整数到整数的映射,其元素将被打印
 */
void printMap(const map<int,int>& m){
    // 遍历map中的每个元素,并打印其键值对
    for(auto it : m){
        cout << it.first << " " << it.second << endl;
    }
}

int main(){
    // 初始化一个map,包含两个元素:{1, 2} 和 {2, 4}
    map<int,int> m{{1,2},{2,4}};
    
    // 寻找键值为1的元素
    auto it = m.find(1);

    cout << it->first << endl;
    // 检查是否找到键值为1的元素
    if(it != m.end()){
        cout << "in" << endl;
    } else{
        cout << "no" << endl;
    }
    
    // 调用函数打印map的所有元素
    printMap(m);
    
    // 打印键值为1的元素在map中出现的次数
    cout << m.count(1) << endl;
    
    return 0;
}
5.map容器的排序

std::map容器在C++ STL中自动保持其元素有序。默认情况下,元素是按照键(key)的升序进行排序的。这种排序是由比较函数决定的,std::map默认使用std::less<Key>作为比较器,它提供了一个严格弱序的比较,适用于大多数基本数据类型。

1.自定义排序规则

尽管std::map默认按key排序,但如果你想改变排序逻辑,可以在创建map时指定一个自定义的比较函数对象。例如,如果你想让map按照key降序排列,或者基于键的某些复杂属性排序,你可以这样做:

cpp 复制代码
struct CustomCompare {
    bool operator()(const Key& lhs, const Key& rhs) const {
        // 自定义比较逻辑,例如降序排序:
        return rhs < lhs; // 改变比较逻辑以实现降序
    }
};

std::map<Key, Value, CustomCompare> myMap;
2.注意点
  • 不可更改排序 :一旦map实例化,其排序规则就不能再改变。如果需要改变排序,你需要创建一个新的map实例并使用不同的比较函数。
  • 基于value排序 :如果需要按照value排序,std::map本身不直接支持。一个常见做法是将map的元素复制到一个std::vector中,每个元素是一个键值对(例如,使用std::pair<Key, Value>),然后对这个vector进行排序。可以使用std::sort函数配合自定义的比较器来达到目的。
3.示例:基于value排序
cpp 复制代码
std::map<Key, Value> originalMap;
// ... 填充originalMap

std::vector<std::pair<Key, Value>> items(originalMap.begin(), originalMap.end());
std::sort(items.begin(), items.end(), [](const std::pair<Key, Value>& a, const std::pair<Key, Value>& b) {
    return a.second > b.second; // 降序排序基于value
});

// 现在items是按value排序的键值对集合

综上所述,虽然std::map自动维持其内部元素的排序(基于键),但通过自定义比较函数,可以控制排序逻辑。而对于基于value的排序,则需借助额外的数据结构和排序算法。

cpp 复制代码
//
// Created by 86189 on 2024/7/3.
//
#include <iostream>
#include <map>
using namespace std;

// 自定义比较函数对象,用于map中键值的降序排序
class Compare{
public:
    // 比较两个整数,返回true如果第一个大于第二个
    bool operator()(const int &a,const int &b) const{
        return a > b;
    }
};

// 打印map中的所有元素
// 参数m: 使用自定义比较函数对象Compare的map
void printMap(const map<int, int, Compare>& m){
    // 遍历map中的每个元素并打印
    for(auto it : m){
        cout << it.first << " " << it.second <<endl;
    }
}

int main(){
    // 初始化一个使用自定义比较函数对象的map
    map<int,int,Compare>m{{1,2},{2,4},{3,5}};
    // 调用函数打印map内容
    printMap(m);
    return 0;
}

10.STL函数对象

C++中的函数对象(Function Object),也常被称为仿函数(Functor),是一种模拟函数行为的类对象。它们允许用户自定义操作,并能像普通函数那样被调用,同时还能携带状态,这是普通函数所不具备的能力。函数对象是实现泛型编程、算法定制等高级技术的关键组件之一。下面概述了函数对象的基本概念和使用方法:

1.基本概念
  1. 类定义 :函数对象通常是一个重载了()操作符的类。这个操作符使得该类的对象可以像函数一样被调用。
  2. 状态携带:与普通函数不同,函数对象可以拥有成员变量,从而在不同的调用之间保持状态。
  3. 类型要求 :为了能够用于标准库算法中,函数对象需要满足可调用对象的要求,即至少要有一个operator()成员函数。
2.使用方法
1. 简单函数对象示例
cpp 复制代码
#include <iostream>

// 定义一个简单的函数对象类,重载了()操作符
class Adder {
public:
    // 构造函数可以初始化内部状态
    Adder(int add_value) : value(add_value) {}

    // 重载()操作符,使得对象可以像函数一样被调用
    int operator()(int x) {
        return x + value;
    }

private:
    int value; // 成员变量,保存加到输入值上的额外值
};

int main() {
    Adder addFive(5); // 创建一个Adder对象,初始化加值为5
    std::cout << addFive(10) << std::endl; // 调用仿函数,输出15
    return 0;
}
2. 函数对象作为算法参数

C++标准库中的许多算法都接受函数对象作为参数,以定制其行为。例如,std::sort可以通过传递自定义比较函数来改变排序规则。

cpp 复制代码
#include <algorithm>
#include <vector>
#include <iostream>

// 一个用于降序比较的仿函数
struct DescComp {
    bool operator()(int a, int b) {
        return a > b;
    }
};

int main() {
    std::vector<int> vec = {1, 3, 5, 2, 4};
    
    // 使用自定义的降序比较仿函数进行排序
    std::sort(vec.begin(), vec.end(), DescComp());
    
    for(int i : vec) {
        std::cout << i << " ";
    }
    // 输出:5 4 3 2 1
    return 0;
}
3. Lambda表达式作为函数对象

从C++11开始,Lambda表达式提供了一种更简洁的方式创建匿名函数对象,它可以直接在代码中定义并使用。

cpp 复制代码
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用Lambda表达式作为自定义排序规则
    std::sort(vec.begin(), vec.end(), [](int a, int b) {
        return a > b; // 降序排列
    });
    
    for(int i : vec) {
        std::cout << i << " ";
    }
    // 输出:5 4 3 2 1
    return 0;
}

通过上述示例,可以看到函数对象在C++中提供了强大的灵活性和定制能力,是泛型编程和算法设计中的重要工具。

2.一元谓词和二元谓词

C++中的一元谓词和二元谓词是两种特定类型的函数对象,它们在标准模板库(STL)的算法中扮演着重要的角色,用于定制算法的行为。以下是它们的基本概念和使用方法:

1.一元谓词

基本概念:

  • 一元谓词是只接受一个参数的函数对象,并返回一个布尔值(bool)。
  • 这个函数对象通常用来测试或判断传入的参数是否满足某个条件。
  • 在STL算法中,一元谓词常用于从容器中选择满足特定条件的元素,例如std::find_if

使用示例:

cpp 复制代码
#include <algorithm>
#include <vector>
#include <iostream>

// 一元谓词函数对象,检查一个数是否为偶数
struct IsEven {
    bool operator()(int x) {
        return x % 2 == 0;
    }
};

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    auto it = std::find_if(numbers.begin(), numbers.end(), IsEven());
    
    if(it != numbers.end())
        std::cout << "The first even number is: " << *it << std::endl;
    else
        std::cout << "No even number found." << std::endl;
    
    return 0;
}
2.二元谓词

基本概念:

  • 二元谓词接受两个参数,并返回一个布尔值。
  • 它们通常用于比较这两个参数是否满足某种关系,如小于、大于、等于等。
  • 在STL中,二元谓词广泛应用于排序算法(如std::sort)、查找算法(如std::binary_search)等,用以定义元素间的比较规则。

使用示例:

cpp 复制代码
#include <algorithm>
#include <vector>
#include <iostream>

// 二元谓词函数对象,检查第一个数是否大于第二个数
struct GreaterThan {
    bool operator()(int a, int b) {
        return a > b;
    }
};

int main() {
    std::vector<int> values = {5, 2, 9, 1, 5, 6};
    
    // 使用二元谓词进行降序排序
    std::sort(values.begin(), values.end(), GreaterThan());
    
    for(int val : values) {
        std::cout << val << " ";
    }
    // 输出:9 6 5 5 2 1
    
    return 0;
}
3.总结

无论是哪种谓词,它们的核心作用都是提供一个逻辑判断,使得STL算法能够基于这些逻辑判断来处理数据。一元谓词适用于单个元素的条件筛选,而二元谓词则用于定义元素间的关系,比如排序或查找时的比较逻辑。随着C++11及之后版本对Lambda表达式的支持,直接在算法中内联定义谓词变得更加方便快捷。

3.算术仿函数

C++ STL(标准模板库)提供了一系列算术仿函数(Arithmetic Functors),它们是对基本数学运算的封装,使得这些运算可以像函数对象一样被使用,尤其是在算法中进行定制操作时非常有用。以下是一些常用的算术仿函数及其简单说明:

  1. plus :定义了加法操作,重载了operator()以执行加法。
  2. minus:定义了减法操作。
  3. multiplies:实现了乘法操作。
  4. divides:提供了除法操作。
  5. modulus:用于取模运算。
  6. negate:执行取负操作。
1.使用示例

这些仿函数位于<functional>头文件。

cpp 复制代码
#include <iostream>
#include <functional>
#include <algorithm> // 用于std::transform

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> results;

    // 使用plus仿函数将每个元素自身相加(即每个元素乘以2)
    results.resize(numbers.size());
    std::transform(numbers.begin(), numbers.end(), results.begin(), std::plus<int>());

    // 输出结果
    for(int res : results) {
        std::cout << res << " ";
    }
    // 输出:2 4 6 8 10

    return 0;
}

在这个例子中,std::transform算法结合std::plus<int>()仿函数,将numbers容器中的每个元素与其自身相加,结果存储在results容器中。

2.Lambda表达式的替代

虽然算术仿函数非常有用,但在C++11及以后的版本中,Lambda表达式提供了更加灵活和直观的方式来定义这样的操作,特别是在复杂逻辑或需要访问外部变量时。然而,对于简单的数学运算,直接使用STL提供的算术仿函数可以简化代码,提高可读性。

4.关系仿函数

关系仿函数封装了基本的比较运算符,位于<functional>头文件中。

  1. equal_to:测试两个参数是否相等。
  2. not_equal_to:测试两个参数是否不相等。
  3. greater:测试第一个参数是否大于第二个参数。
  4. less:测试第一个参数是否小于第二个参数。
  5. greater_equal:测试第一个参数是否大于等于第二个参数。
  6. less_equal:测试第一个参数是否小于等于第二个参数。
cpp 复制代码
#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    int target = 3;
    

    // 使用equal_to查找目标值
    if(std::find_if(vec.begin(), vec.end(), std::bind2nd(std::equal_to<int>(), target)) != vec.end()) {
        std::cout << target << " is found in the vector." << std::endl;
    } else {
        std::cout << target << " is not found in the vector." << std::endl;
    }
    return 0;

}
5.逻辑仿函数

逻辑仿函数用于组合或反转布尔值,它们也是在<functional>中定义的,包括:

  1. logical_and:对两个布尔值执行逻辑与操作。
  2. logical_or:对两个布尔值执行逻辑或操作。
  3. logical_not:对一个布尔值执行逻辑非操作。

逻辑仿函数示例

cpp 复制代码
#include <iostream>
#include <functional>

int main() {
    bool flag1 = true, flag2 = false;
    
    // 使用逻辑与
    if(std::logical_and<bool>()(flag1, flag2)) {
        std::cout << "Both flags are true." << std::endl;
    } else {
        std::cout << "Not both flags are true." << std::endl;
    }
    
    // 使用逻辑或
    if(std::logical_or<bool>()(flag1, flag2)) {
        std::cout << "At least one flag is true." << std::endl;
    }
    
    // 使用逻辑非
    std::cout << "Negation of flag2: " << std::logical_not<bool>()(flag2) << std::endl;
    
    return 0;
}

7.常用算法

1.常用遍历算法

1.for_each

C++的std::for_each算法是标准库中的一个迭代器算法,它对容器或范围内的每个元素执行指定的操作。这个算法特别适用于你想要对容器内的每个元素应用同一个操作,但操作本身不需要积累结果(例如累加或查找最大值)的情形。std::for_each返回作用于每个元素的函数对象的副本。

基本语法如下:

cpp 复制代码
template< class InputIt, class UnaryFunction>
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );
  • InputIt first, InputIt last: 这是一个半开区间 [first, last),定义了要遍历的元素范围。
  • UnaryFunction f: 一个可调用的对象(如函数、lambda表达式或者函数对象),它接受一个参数(即容器中的元素类型)并对其进行操作。该对象会在区间内的每个元素上被调用。

示例:

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>

void print(int i) {
    std::cout << i << ' ';
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用std::for_each打印vector中的每个元素
    std::for_each(numbers.begin(), numbers.end(), print);

    return 0;
}

在这个例子中,print函数被传递给std::for_each,它会对numbers向量中的每个元素调用print函数,从而打印出向量中的所有数字。

自C++11起,还可以直接使用lambda表达式作为第三个参数,使得操作更加灵活:

cpp 复制代码
std::for_each(numbers.begin(), numbers.end(), [](int i){ std::cout << i * 2 << ' '; });

这段代码会打印出向量中每个数字的两倍。

2.transform

C++的std::transform算法是标准库中的一个迭代器算法,用于将一个范围内或两个范围内的元素通过某个操作转换后存放到另一个范围。这个算法常用于对容器内容进行批量修改或计算转换后的结果。与std::for_each不同的是,std::transform通常涉及输入范围和输出范围,并且转换操作可能产生新的值。

基本语法如下:

cpp 复制代码
// 单输入范围到单输出范围
template< class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op);

// 双输入范围到单输出范围
template< class InputIt1, class InputIt2, class OutputIt, class BinaryOperation>
OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOperation binary_op);
  • 对于单输入范围版本:

    • InputIt first1, InputIt last1: 定义了输入范围。
    • OutputIt d_first: 指向输出范围的开始位置。
    • UnaryOperation unary_op: 一个可调用对象,接受一个输入范围的元素作为参数,并返回转换后的结果。
  • 对于双输入范围版本:

    • InputIt1 first1, InputIt1 last1, InputIt2 first2: 分别定义了两个输入范围。
    • 其余参数同上,但BinaryOperation binary_op接受两个输入范围对应位置的元素作为参数,并返回一个转换后的结果。

示例:

单输入范围示例,将一个vector中的每个元素平方后存入另一个vector:

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>

int square(int x) {
    return x * x;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> squared;

    // 保留足够空间以避免插入时重新分配
    squared.resize(numbers.size());

    // 使用std::transform进行转换
    std::transform(numbers.begin(), numbers.end(), squared.begin(), square);

    for(int num : squared) {
        std::cout << num << ' ';
    }

    return 0;
}

双输入范围示例,将两个vector对应元素相加后存入第三个vector:

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {4, 5, 6};
    std::vector<int> result(vec1.size());

    // 使用std::transform进行转换,这里使用lambda表达式代替函数对象
    std::transform(vec1.begin(), vec1.end(), vec2.begin(), result.begin(), [](int a, int b){ return a + b; });

    for(int res : result) {
        std::cout << res << ' ';
    }

    return 0;
}

这些示例展示了如何使用std::transform进行简单的数值转换和组合操作。

2.常用查找算法

在C++标准库中,提供了多种查找算法来帮助处理容器或范围内的数据。以下是您提到的一些常用查找算法的简要说明:

1. find

std::find用于在一个范围内查找指定的值。如果找到该值,则返回指向该值的第一个匹配项的迭代器;如果没有找到,则返回表示范围末尾的迭代器。

基本用法:

cpp 复制代码
template< class InputIt, class T>
InputIt find(InputIt first, InputIt last, const T& value);

示例:

cpp 复制代码
// 包含标准输入输出库、算法库和向量库
#include <iostream>
#include <algorithm>
#include <vector>

// 使用标准命名空间,简化代码中标准库函数的调用
using namespace std;

// 程序的主入口函数
int main(){
    // 初始化一个字符串变量name
    string name = "Felipe";

    // 查找字符串name中字符'F'的位置
    auto in = find(name.begin(), name.end(),'F');
    // 输出找到的字符
    cout << *in << endl;

    // 初始化一个整数向量V,并赋初值
    vector<int>V = {1,2,3,5,6,7};

    // 在向量V中查找数值1的位置
    auto it = find(V.begin(), V.end(),1);

    // 判断是否找到数值1,如果找到则输出相应信息
    if(it != V.end()){
        cout << "find 1" << endl;
    }

    // 程序正常结束
    return 0;
}
2. find_if

std::find_if类似于find,但它不是直接查找特定值,而是使用提供的谓词(如函数、lambda表达式)来测试每个元素,直到找到第一个使谓词返回true的元素。如果找到这样的元素,则返回指向它的迭代器;否则,返回last

基本用法:

cpp 复制代码
template< class InputIt, class UnaryPredicate>
InputIt find_if(InputIt first, InputIt last, UnaryPredicate pred);

示例:

cpp 复制代码
// Filename: find_example.cpp
// Created by 86189 on 2024/7/7.
//
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 主函数,演示如何使用标准库函数find_if查找向量中第一个大于5的元素
int main() {
    // 初始化一个整数向量,包含1到6的几个数字,其中有重复
    vector<int> V = {1, 2, 3, 3, 4, 5, 6};

    // 使用find_if算法查找向量中第一个大于5的元素
    // find_if函数接受一个范围(向量的开始和结束迭代器)和一个谓词(用于判断条件的lambda表达式)
    // 这里lambda表达式返回值为true时意味着找到了满足条件的元素
    auto it = find_if(V.begin(), V.end(), [](const int &val) { return val > 5; });

    // 检查是否找到了满足条件的元素
    if (it != V.end()) {
        // 如果找到了,输出该元素的值
        cout << *it << endl;
    } else {
        // 如果没有找到,输出提示信息
        cout << "No element greater than 5 found." << endl;
    }

    return 0;
}
3. adjacent_find

std::adjacent_find用于查找序列中相邻重复的元素对。它返回一个指向序列中第一个重复元素的迭代器,如果序列中没有相邻的重复元素,则返回最后一个元素的下一个位置。

基本用法:

cpp 复制代码
template< class InputIt >
InputIt adjacent_find( InputIt first, InputIt last );

template< class InputIt, class BinaryPredicate >
InputIt adjacent_find( InputIt first, InputIt last, BinaryPredicate pred );

第二个版本接受一个比较谓词。

示例:

cpp 复制代码
// 包含标准输入输出库和向量库
#include <iostream>
#include <vector>
#include <algorithm>
// 使用标准命名空间,简化代码中标准库函数的调用
using namespace std;

// 程序入口函数
int main(){
    // 初始化一个整数向量v,包含一组数字
    vector<int>v = {0,1,3,4,5,5,7,7};

    // 使用adjacent_find函数查找向量v中相邻重复的元素
    // adjacent_find返回的是一个迭代器,指向第一个相邻重复元素的位置
    auto it = adjacent_find(v.begin(), v.end());
    // 如果找到相邻重复元素,输出该元素的值
    cout << *it << endl;

    // 使用lambda表达式作为谓词,查找向量v中相邻元素差值绝对值大于1的元素
    // 这里的lambda表达式接收两个int类型的引用,返回一个bool值
    // 它比较两个引用所指的元素的差值的绝对值是否大于1
    auto in = adjacent_find(v.begin(), v.end(),[](const int &val,const int &b){return (abs(b-val)>1);});
    // 如果找到满足条件的元素,输出该元素的值
    cout << *in << endl;

    // 程序正常结束
    return 0;
}

std::binary_search在一个已排序的范围内查找特定值。如果找到了该值,则返回true;否则,返回false。这个算法假设范围是按升序排序的。

基本用法:

cpp 复制代码
template< class ForwardIt, class T>
bool binary_search(ForwardIt first, ForwardIt last, const T& value);

template< class ForwardIt, class T, class Compare>
bool binary_search(ForwardIt first, ForwardIt last, const T& value, Compare comp);

第二个版本接受一个比较函数。

示例:

cpp 复制代码
#include <iostream>
#include <set>
#include <algorithm>
#include <vector>
using namespace std;

int main() {
    // 使用set的查找,无需自定义比较函数,因为set本身就是有序的且默认升序
    set<int> s = {1, 2, 3, 4, 5, 6};
    if (binary_search(s.begin(), s.end(), 5)) {
        cout << "Found: " << 5 << endl; // 更正输出格式
    }

    // 对于vector,如果要使用非默认比较(例如降序),需要正确传递比较函数
    // 但是,注意binary_search要求容器是已排序的。先对vector排序
    vector<int> v = {1, 3, 6, 5, 8, 4, 6};
    sort(v.begin(), v.end(), [](const int &a, const int &b) { return a > b; }); // 先排序

    // 确保v是按所需顺序排序后,再使用binary_search
    if (binary_search(v.begin(), v.end(), 5, [](const int &a, const int &b) { return a > b; })) {
        cout << "Found: " << 5 << endl; // 更正输出格式
    } else {
        cout << "Not found: " << 5 << endl; // 添加未找到时的输出
    }

    return 0;
}

注意:

binary_search算法要求其操作的范围必须是已排序的,不论是否提供了第四个比较函数参数。这是因为binary_search的工作原理基于二分查找算法,该算法的前提是输入数据是有序的。提供自定义的比较函数(即第四个参数)是为了适应不同的排序逻辑(如降序、自定义排序依据等),而不是为了处理无序数据。

如果输入范围是无序的,即使你提供了比较函数,binary_search的结果也是未定义的,可能会导致错误的查找结果。因此,在调用binary_search之前,你应该确保范围内的元素是按照你所指定的比较规则排序的。在上面的例子中,对vector<int> v应用了sort函数配合同样的比较函数进行了排序,之后才进行binary_search,这是正确的做法。

5. count

std::count计算范围内与给定值匹配的元素数量。

基本用法:

cpp 复制代码
template< class InputIt, class T>
typename std::iterator_traits<InputIt>::difference_type count(InputIt first, InputIt last, const T& value);

示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/7.
//
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

// 主函数,程序的入口点
int main(){
    // 初始化一个整数向量v,包含一组数字,用于后续的计数操作
    vector<int>v = {1,2,2,2,2,2,5,6,9,8};

    // 使用标准库函数count计算向量v中值为2的元素的数量,并输出结果
    cout << count(v.begin(), v.end(),2) << endl;
    
    return 0;
}
6. count_if

std::count_if类似于count,但它不是直接计数特定值,而是使用谓词来决定哪些元素应该被计数。

基本用法:

cpp 复制代码
template< class InputIt, class UnaryPredicate>
typename std::iterator_traits<InputIt>::difference_type count_if(InputIt first, InputIt last, UnaryPredicate pred);

示例:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

// 主函数,程序的入口点
int main(){
    // 初始化一个整数向量,包含一组数字用于后续操作
    vector<int>v = {1,2,2,3,2,2,5,6,9,8};

    // 使用标准库函数count计算向量v中值为2的元素个数
    // 并将结果输出到标准输出流
    cout << count(v.begin(), v.end(),2) << endl;

    // 使用标准库函数count_if计算向量v中大于2的元素个数
    // 使用lambda表达式作为谓词,对每个元素进行判断
    cout << count_if(v.begin(), v.end(),[](const int &a){return a>2;});

    return 0;
}

这些算法是C++标准库 <algorithm> 头文件的一部分,为处理容器和数组提供了强大的工具。

3.常用排序算法

在C++中,排序和相关操作是非常实用的功能。

1. sort

sort函数是C++标准库提供的一个强大的排序算法,它能够对容器(如std::vectorstd::liststd::deque等)或数组中的元素进行排序。默认情况下,它执行升序排序,但也可以通过提供自定义比较函数来进行降序或其他定制排序。sort位于<algorithm>头文件中。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

// 主函数,程序的入口点
int main(){
    // 初始化一个整数向量,包含一组待排序的数字
    vector<int>v = {1,3,2,6,5,7,4};

    // 对向量进行升序排序
    sort(v.begin(), v.end());

    // 遍历并打印排序后的向量
    for(int n : v){
        cout << n << " ";
    }
    cout << endl;

    // 对向量进行降序排序
    sort(v.begin(), v.end(),[](int &a,int &b){return a > b;});

    // 再次遍历并打印排序后的向量
    for(int n : v){
        cout << n << " ";
    }

    return 0;
}
2. random_shuffle

random_shuffle函数用于将容器中的元素随机重新排列。不过需要注意的是,在C++14之后,random_shuffle已经被标记为已弃用,推荐使用shuffle函数,它提供了更好的随机性控制。random_shuffle同样位于<algorithm>头文件中。

基本用法示例 :

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <iostream>
#include <algorithm>
#include <vector>
#include <random>
using namespace std;

/* 主函数 */
int main(){
    /* 初始化一个整数向量,包含一组数字 */
    vector<int>v = {1,9,6,4,5,3,5};

    /* 初始化一个随机设备,用于生成随机数种子 */
    random_device rd;

    /* 使用Mersenne Twister算法初始化随机数生成器 */
    mt19937 g(rd());

    /* 使用随机数生成器对向量进行洗牌操作,以打乱其元素顺序 */
    shuffle(v.begin(), v.end(), g);

    /* 遍历并输出洗牌后的向量元素 */
    for(int n : v) cout << n << " ";

    return 0;
}
3. merge

merge函数用于合并两个已排序的序列,将它们合并成一个新的有序序列。这通常与sort配合使用于更复杂的排序算法中,如归并排序。它同样位于<algorithm>头文件中。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

// 主函数,演示如何使用标准库函数merge合并两个有序向量
int main(){
    // 初始化两个有序向量v1和v2
    vector<int>v1 = {1,3,5};
    vector<int>v2 = {2,4,6};

    // 初始化一个足够大的向量v3用于存放合并后的结果
    // 其大小为v1和v2的大小之和
    vector<int>v3(v1.size()+v2.size());

    // 调用标准库函数merge将v1和v2合并到v3中
    // 这里的merge函数假设输入的两个向量v1和v2是有序的
    // 合并后的向量v3仍然保持有序
    merge(v1.begin(), v1.end(),v2.begin(), v2.end(),v3.begin());

    // 遍历并打印合并后的向量v3
    for(int n : v3) cout << n << " ";

    return 0;
}
4. reverse

reverse函数用于反转容器或数组中元素的顺序。它同样简单易用,只需提供起始和结束迭代器即可。此函数也在<algorithm>头文件中定义。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

// 主函数,程序的入口点
int main() {
    // 初始化一个整数向量v1,包含元素1, 3, 5
    vector<int> v1 = {1, 3, 5};

    // 翻转向量v1中的元素顺序
    reverse(v1.begin(), v1.end());

    // 遍历并打印翻转后的向量v1中的每个元素
    for(int n : v1) cout << n << " ";

    return 0;
}

4.常用拷贝和替换算法

在C++中,常用的拷贝和替换算法是处理容器或数组元素时非常基础且重要的操作。

1. copy

copy函数用于将一个范围内的元素复制到另一个范围内。这个函数位于<algorithm>头文件中。

基本用法示例:

cpp 复制代码
// 包含标准库的头文件,用于输入输出和算法操作
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

// 程序的入口点
int main() {
    // 初始化源向量,包含一组整数
    vector<int> src = {1, 2, 3, 4};

    // 初始化目标向量,大小与源向量相同
    vector<int> dest(4);

    // 将源向量的内容复制到目标向量
    // 使用标准库算法copy进行复制操作
    copy(src.begin(), src.end(), dest.begin());

    // 遍历目标向量并打印每个元素
    // 用于展示复制操作的结果
    for(int n : dest) cout << n << " ";

    // 程序正常结束
    return 0;
}
2. replace

replace函数用于在给定范围内查找指定的旧值,并将其替换为新值。它也位于<algorithm>头文件中。

基本用法示例:

cpp 复制代码
#include <algorithm> // 包含算法库,用于使用replace函数
#include <iostream> // 包含输入输出流,用于标准输出
#include <vector> // 包含向量容器,用于存储和操作整数序列
using namespace std;

int main() {
    vector<int> src = {1, 2, 3, 4}; // 初始化一个整数向量,包含1, 2, 3, 4

    // 使用replace函数将向量src中所有的2替换为99
    // replace函数的作用是:在给定的范围内,找到所有与目标值相同的元素,并将其替换为新值
    replace(src.begin(), src.end(), 2, 99);

    // 遍历向量src,并打印每个元素
    // 这里使用范围for循环来遍历向量,展示replace操作后的结果
    for(int n : src) cout << n << " ";

    return 0; // 程序正常退出
}
3. replace_if

replace_if函数类似于replace,但它不是直接匹配值,而是基于谓词(一个返回bool值的函数或函数对象)来决定是否进行替换。当谓词对元素返回true时,该元素会被替换。

基本用法示例:

cpp 复制代码
#include <algorithm> // 使用标准库的算法
#include <iostream> // 使用标准库的输入输出流
#include <vector> // 使用标准库的向量容器
using namespace std;

// 主函数,程序的入口点
int main() {
    // 初始化一个整数向量,包含1到4
    vector<int> src = {1, 2, 3, 4};

    // 使用replace_if算法替换向量中所有满足条件的元素
    // 条件是元素值为偶数,将其替换为99
    replace_if(src.begin(), src.end(), [](int n){return n%2==0;}, 99);

    // 遍历向量并打印每个元素
    for(int n : src) cout << n << " ";

    return 0; // 程序正常结束
}
4. swap

swap函数用于交换两个对象的值。它可以用于内置类型,也可以用于自定义类型(只要这些类型重载了swap函数或遵循移动语义)。对于标准库容器,可以直接调用它们的swap成员函数,或者使用std::swap函数,它位于<utility>头文件中。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

// 主函数,程序的入口点
int main() {
    // 初始化源向量
    vector<int> src = {1, 2, 3, 4};

    // 初始化目标向量
    vector<int> dest = {7,6,8,5,3,4};

    // 交换源向量和目标向量的内容
    // 说明:这里使用swap函数来交换两个向量的内容,目的是为了展示向量内容的交换过程
    swap(src, dest);

    // 遍历并打印源向量的内容
    for (int n : src) cout << n << " ";
    cout << endl;

    // 遍历并打印目标向量的内容
    for (int n : dest) cout << n << " ";

    return 0;
}

这些算法极大地简化了在C++中处理数据集合的任务,提高了代码的可读性和效率。

5.常用算术生成算法

在C++中,除了拷贝和替换算法外,算术生成算法也是处理数据集合时经常使用的工具。

1. accumulate

accumulate函数用于计算一系列数值的总和(累积和)。此函数位于<numeric>头文件中,可以接受一个初始值作为累积和的起始点。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

// 主函数,程序的入口点
int main(){
    // 初始化一个整数向量v,包含一组预设的整数
    vector<int>v = {1,10,2,3,5};

    // 使用accumulate函数对向量v中的所有元素进行累加,初始值为0
    // accumulate函数是标准库中用于累加的算法,这里展示了其在向量操作中的应用
    int sum = accumulate(v.begin(), v.end(),0);

    // 输出累加结果
    cout << sum << endl;

    // 程序正常结束
    return 0;
}
2. fill

fill算法用于将一个范围内所有元素赋值为某个特定的值。它同样位于<algorithm>头文件中。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

// 主函数,程序的入口点
int main() {
    // 初始化一个长度为10的整数向量,所有元素初始值为0
    vector<int> vec(10, 0);

    // 使用标准库函数fill将向量的所有元素赋值为7
    fill(vec.begin(), vec.end(), 7);

    // 遍历向量并打印每个元素
    for(int num : vec)
        cout << num << " ";
    return 0;
}

这两个算法在处理数值集合时非常有用,accumulate用于求和运算,而fill则用于快速设置一系列值,比如初始化数组或向量等。

6.常用集合算法

集合算法在C++标准库中的<algorithm>头文件里定义,它们主要用于处理集合(如数组、向量等容器)之间的数学集合操作。

1. set_intersection

set_intersection算法用于找出两个已排序的集合的交集,并将结果存放到另一个集合中。集合需要是有序的,且不能有重复元素。

基本用法示例:

cpp 复制代码
// 包含必要的头文件
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
using namespace std;

// 主函数
int main(){
    // 初始化两个集合,分别包含一组整数
    set<int>s = {1,2,3,4,5,6};
    set<int>s1 = {2,4,6,8,10};

    // 初始化一个向量,用于存储两个集合的交集元素
    // 其大小预先估计为两个集合元素总数之和
    vector<int>v(s.size()+s1.size());

    // 使用set_intersection算法计算两个集合的交集
    // 并将结果存储在向量v中
    auto it = set_intersection(s.begin(), s.end()
            ,s1.begin(), s1.end(),
                        v.begin());

    // 调整向量的大小,以去除多余的未使用的空间
    // 它的大小现在等于交集中实际元素的数量
    v.resize(it - v.begin());

    // 遍历并打印向量中的元素,即两个集合的交集
    for(int n : v) cout << n << " ";

    // 程序正常退出
    return 0;
}
2. set_union

set_union算法用于合并两个已排序的集合,去除重复元素,得到并集,并将结果存放到另一个集合中。

基本用法示例:

cpp 复制代码
// 包含必要的头文件
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
using namespace std;

// 主函数
int main(){
    // 初始化两个集合,分别包含一组不重复的整数
    set<int>s = {1,2,3,4,5,6};
    set<int>s1 = {2,4,6,8,10};
    
    // 初始化一个向量,大小为两个集合元素总数,用于存储合并后的结果
    vector<int>v(s.size()+s1.size());
    
    // 使用set_union算法将两个集合的元素合并到向量v中,重复的元素只出现一次
    // set_union算法返回一个迭代器,指向向量v中第一个未被写入的元素
    auto it = set_union(s.begin(), s.end()
    ,s1.begin(), s1.end(),
    v.begin());
    
    // 根据返回的迭代器调整向量v的大小,使其正好包含所有合并后的元素
    v.resize(it - v.begin());

    // 遍历向量v,输出合并后的所有元素
    for(int n : v) cout << n << " ";

    return 0;
}
3. set_difference

set_difference算法用于找出两个已排序的集合的差集,即第一个集合中存在但第二个集合中不存在的元素,并将结果存放到另一个集合中。

基本用法示例:

cpp 复制代码
//
// Created by 86189 on 2024/7/10.
//
//

#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
using namespace std;

// 主函数
int main(){
    // 初始化两个集合,分别包含一组整数
    set<int>s = {1,2,3,4,5,6};
    set<int>s1 = {2,4,6,8,10};

    // 初始化一个向量,用于存储两个集合的差集结果
    vector<int>v(s.size()+s1.size());

    // 使用std::set_difference算法计算两个集合的差集
    // 并将结果写入向量v中
    auto it = set_difference(s.begin(), s.end()
            ,s1.begin(), s1.end(),
                               v.begin());

    // 调整向量的大小,使其正好包含所有差集元素
    v.resize(it - v.begin());

    // 遍历并输出向量中的差集元素
    for(int n : v) cout << n << " ";

    return 0;
}

这些算法都要求输入序列是预先排序的,并且对于set_intersectionset_difference来说,如果输入序列中有重复元素,结果只包含每个不同元素的一个实例。

相关推荐
Noric!30 分钟前
108 将有序数组转换为二叉搜索树
c++·leetcode·
霍霍哈嗨1 小时前
【C++11】:lambda表达式&function包装器
开发语言·c++
Fern_v1 小时前
代码随想录算法训练营 | 回溯算法part03
c++·算法·leetcode
Fern_v1 小时前
代码随想录算法训练营 | 回溯算法part02
c++·leetcode
quaer1 小时前
数字阵列雷达:发射数字波束形成
linux·开发语言·数据结构·c++·算法
GOATLong2 小时前
list的简单模拟实现
c++·microsoft·list·c
0xCC说逆向2 小时前
Windows图形界面(GUI)-MFC-C/C++ - 列表框(ListBox) - CListBox
c语言·开发语言·c++·windows·mfc
二进制的声音2 小时前
【矩阵对角线求和】求一个3*3矩阵对角线元素之和
c语言·开发语言·c++·矩阵
李龙蛋Seabiscuit3804 小时前
C++ | (一)C++入门基础
开发语言·c++
EterNity_TiMe_4 小时前
【C++高阶】深入理解C++异常处理机制:从try到catch的全面解析
开发语言·c++·学习·性能优化·学习方法