北邮崔毅东cpp期末复习文档

一、类和对象

1. 类的成员数据初始化

C++ 提供了多种方式来进行成员数据的初始化:

  1. 构造函数初始化列表:可以在类的构造函数中使用初始化列表,显式地初始化成员数据。这是最常用的方式,可以在对象创建时初始化部分或全部成员数据。
class MyClass {
private:
    int value1;
    double value2;
    std::string value3;

public:
    MyClass(int v1, double v2, std::string v3)
        : value1(v1), value2(v2), value3(v3) {
        // constructor body
    }
};
  1. 默认成员初始化(C++11 及以上版本):可以在类的成员变量声明时,使用等号和默认值进行初始化。这样,如果对象创建时没有提供对应成员数据的初始化值,会自动使用默认值进行初始化。
  2. 成员初始化函数(C++11 及以上版本):可以定义成员初始化函数,在类的构造函数中调用这个函数来初始化成员数据。成员初始化函数可以在构造函数体内部任意位置被调用。

需要注意的是,如果类的成员数据没有在上述方式中进行初始化,它们将以未定义的值进行初始化。这可能导致程序行为的不确定性和错误。因此,为了避免这种情况,建议在类对象创建时对所有成员数据进行初始化

类对象实例化时,先执行就地初始化(默认成员初始化),再执行构造函数初始化列表,最后执行成员初始化函数(如果在构造函数中调用了的话)。

2. 静态成员

类的静态成员是属于整个类而不是类的实例的成员。它们被共享并在类的所有实例之间保持一致。静态成员可以是静态数据成员或静态函数成员。

  1. 静态数据成员:静态数据成员是类的成员变量,它们在类的所有实例之间共享相同的值。静态数据成员在类声明内部进行定义,但不能在类声明内进行初始化。必须在类外部进行初始化,通常在类的实现文件中进行初始化。静态数据成员可以通过类名和作用域解析运算符(::)进行访问。
class MyClass {
  static int count; // 静态数据成员的声明
};

int MyClass::count = 0; // 静态数据成员的定义和初始化
  1. 静态函数成员:静态函数成员是属于类而不是类的实例的成员函数。它们可以在没有类的实例的情况下被调用,只能访问静态数据成员和其他静态函数成员。静态函数成员在声明时使用关键字 static 进行标识。
class MyClass {
  static void printCount(); // 静态函数成员的声明
};

void MyClass::printCount() {
  // 实现静态函数成员的代码
}

静态成员的特点包括:

  • 静态成员在内存中只有一个副本,由类共享。
  • 可以在没有类的实例的情况下访问静态成员。
  • 静态成员可以通过类名和作用域解析运算符进行访问。
  • 静态成员函数只能访问静态成员变量和其他静态成员函数。
  • 静态成员可以通过类名或对象进行访问,但推荐使用类名访问静态成员。

个人见解:

  1. 静态成员变量相当于全局变量,不过在使用时需要通过className::访问。
  2. 静态成员变量不能在类声明内进行初始化,必须在类外部进行初始化,通常在类的实现文件中进行初始化。类外初始化时不再需要声明static

3. 继承

Ⅰ. 构造函数和析构函数的执行顺序:

父类构造->子类构造
子类析构->父类析构

Ⅱ. 继承方式

在C++中,有三种不同的继承方式:公有继承(public inheritance)、私有继承(private inheritance)和保护继承(protected inheritance)。这些继承方式决定了派生类如何继承基类的成员和如何访问它们。

注意:无论何种继承方式,都无法访问父类的私有成员!

  1. 公有继承(public inheritance):

    • 使用关键字 public 来指定公有继承。
    • 公有继承意味着基类的公有成员在派生类中仍然是公有的,保护成员在派生类中变为保护的,私有成员在派生类中不可访问。
    • 公有继承满足"是一个"的关系,派生类对象可以被视为基类对象。
    • 公有继承用于建立派生类与基类之间的"is-a"关系。
  2. 私有继承(private inheritance):

    • 使用关键字 private 来指定私有继承。
    • 私有继承意味着基类的公有和保护成员在派生类中都变为私有的,私有成员在派生类中不可访问。
    • 私有继承用于建立派生类与基类之间的"实现细节"关系,派生类可以使用基类的实现,但不公开继承关系。
  3. 保护继承(protected inheritance):

    • 使用关键字 protected 来指定保护继承。
    • 保护继承意味着基类的公有和保护成员在派生类中都变为保护的,私有成员在派生类中不可访问。
    • 保护继承用于建立派生类与基类之间的"限制访问"关系,派生类可以访问基类的成员,但对外部是受保护的。

Ⅲ. 子类构造函数成员初始化列表

在继承关系中,使用成员初始化列表来初始化继承的父类成员数据时,有以下几个注意事项:

  1. 初始化顺序:成员初始化列表中的初始化顺序应与继承顺序保持一致。即先初始化基类的成员数据,然后再初始化派生类自身的成员数据。

  2. 通过构造函数传参:如果父类的构造函数需要参数,在成员初始化列表中传递参数给父类的构造函数来初始化继承的父类成员数据。

  3. 默认构造函数:如果父类没有默认构造函数,那么在子类的成员初始化列表中必须调用父类带参数的构造函数,并提供合适的参数。

  4. 虚继承:如果使用了虚继承(通过虚基类指定),在成员初始化列表中需要显示调用虚基类的构造函数来初始化虚基类的成员数据。

下面是一个示例代码,演示了继承关系中的成员初始化列表的注意事项:

class Base {
protected:
    int baseData;
public:
    Base(int data) : baseData(data) {
        cout << "Base constructor called" << endl;
    }
};

class Derived : public Base {
private:
    int derivedData;
public:
    Derived(int baseData, int derivedData) : Base(baseData), derivedData(derivedData) {
        cout << "Derived constructor called" << endl;
    }
};

int main() {
    Derived derived(10, 20);
    return 0;
}

在上述代码中,派生类Derived通过成员初始化列表调用基类Base的构造函数,然后再初始化自身的成员数据。注意在成员初始化列表中的调用顺序与继承顺序保持一致。

输出结果:

Base constructor called
Derived constructor called

通过成员初始化列表,首先调用了基类Base的构造函数,然后调用派生类Derived的构造函数。这样可以确保在派生类的构造函数执行之前,基类的成员数据已经正确初始化。

Ⅳ. 虚函数和多态

虚函数关键字:virtual
下面是一个简单的示例代码,展示了虚函数和多态的使用:

#include <iostream>

// 基类 Animal
class Animal {
public:
    virtual void makeSound() {
        std::cout << "Animal makes a sound." << std::endl;
    }
};

// 派生类 Cat
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows." << std::endl;
    }
};

// 派生类 Dog
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks." << std::endl;
    }
};

int main() {
    Animal* animal1 = new Cat();
    Animal* animal2 = new Dog();

    animal1->makeSound();  // 调用派生类 Cat 的实现
    animal2->makeSound();  // 调用派生类 Dog 的实现

    delete animal1;
    delete animal2;

    return 0;
}

在上述代码中,基类 Animal 声明了一个虚函数 makeSound()。派生类 CatDog 分别重写了这个虚函数,并提供了它们自己的实现。在 main() 函数中,我们使用基类指针指向派生类对象,并通过虚函数调用 makeSound(),实现了多态性。根据指针所指向的实际对象类型,会调用相应的派生类实现。

输出结果:

Cat meows.
Dog barks.

可以看到,虽然通过基类指针调用了同一个函数 makeSound(),但实际调用的是派生类的具体实现,实现了多态性的效果。

4.友元函数

友元函数是在C++中用来授权非成员函数访问类的私有成员的机制。通过在类中声明友元函数,可以使得该函数能够访问类的私有成员和保护成员,即使它不是类的成员函数。

友元函数的特点:

  1. 友元函数被声明在类的内部,但不是类的成员函数。
  2. 友元函数的声明通常放在类的定义中,在类的内部声明友元函数,可以让友元函数在类的内外都可见。
  3. 友元函数可以访问类的所有成员,包括私有成员和保护成员。
  4. 友元函数的定义在类的外部实现,可以在定义之前或之后声明。

下面是一个简单的示例,展示了如何使用友元函数:

class MyClass {
private:
    int privateData;

public:
    MyClass(int data) : privateData(data) {}

    friend void FriendFunction(const MyClass& obj);
};

void FriendFunction(const MyClass& obj) {
    // 友元函数可以访问类的私有成员
    int data = obj.privateData;
    std::cout << "Friend Function: " << data << std::endl;
}

int main() {
    MyClass obj(42);
    FriendFunction(obj);

    return 0;
}

二、运算符重载

以下是常见的运算符重载函数声明的示例:

  1. 一元运算符:
    • 前置递增运算符 (++): T& operator++()
    • 前置递减运算符 (–): T& operator--()
    • 后置递增运算符 (++, 后置): T operator++(int)
    • 后置递减运算符 (–, 后置): T operator--(int)
    • 取反运算符 (!): bool operator!()
    • 成员访问运算符 (->): T* operator->()
    • 下标运算符 ([]):T& operator[](int index)
  2. 二元运算符:
    • 加法运算符 (+): T operator+(const T& other)
    • 减法运算符 (-): T operator-(const T& other)
    • 乘法运算符 (*): T operator*(const T& other)
    • 除法运算符 (/): T operator/(const T& other)
    • 等于运算符 (==): bool operator==(const T& other)
    • 不等于运算符 (!=): bool operator!=(const T& other)
    • 大于运算符 (>): bool operator>(const T& other)
    • 小于运算符 (<): bool operator<(const T& other)
    • 大于等于运算符 (>=): bool operator>=(const T& other)
    • 小于等于运算符 (<=): bool operator<=(const T& other)
    • 赋值运算符 (=): T& operator=(const T& other)
    • 输入运算符 (>>): friend std::istream& operator>>(std::istream& is, T& obj)
    • 输出运算符 (<<): friend std::ostream& operator<<(std::ostream& os, const T& obj)
  3. 复合赋值运算符:
    • 加法赋值运算符 (+=): T& operator+=(const T& other)
    • 减法赋值运算符 (-=): T& operator-=(const T& other)
    • 乘法赋值运算符 (*=): T& operator*=(const T& other)
    • 除法赋值运算符 (/=): T& operator/=(const T& other)

要点:

  • 一元运算符重载函数没有参数,二元运算符重载函数有一个参数。
  • 关注返回值应当是对象本身还是临时对象,若返回值为对象本身则返回引用
  • 二元运算符如果希望改变次序,则需要将其声明为友元函数,如a>>cout,如果声明为成员数据相当于operator>>(a,cout)。

三、文件流

1. 打开方式

通过构造函数:

#include <fstream>
int main() {
    std::ifstream inputFile("filename.txt");  // 打开输入文件
    std::ofstream outputFile("filename.txt"); // 打开输出文件
    // 使用文件流对象进行读写操作
    inputFile.close();  // 关闭输入文件
    outputFile.close(); // 关闭输出文件
    return 0;
}

通过成员函数open():

#include <fstream>
int main() {
    std::ifstream inputFile;
    inputFile.open("filename.txt"); // 打开输入文件
    std::ofstream outputFile;
    outputFile.open("filename.txt"); // 打开输出文件
    // 使用文件流对象进行读写操作
    inputFile.close();  // 关闭输入文件
    outputFile.close(); // 关闭输出文件
    return 0;
}

2. 随机地址访问文件

随机访问文件是一种文件访问方式,它允许在文件中随机地读取和写入数据,而不仅仅是顺序读取或写入。在C++中,可以使用文件流对象和文件指针来实现随机访问文件的操作。下面是一种常见的方法:

  1. 打开文件并定位到指定位置:首先,使用文件流对象打开文件,并指定打开模式为std::ios::binary,以支持二进制文件的读写。然后,使用seekg()seekp()函数将文件指针定位到指定位置,以进行随机访问。例如:
#include <fstream>
int main() {
    std::fstream file("filename.txt", std::ios::binary | std::ios::in | std::ios::out);
    if (!file) {
        // 处理文件打开失败的情况
        return 1;
    }
    // 将文件指针定位到指定位置
    file.seekg(10); // 将读取位置定位到第10个字节
    file.seekp(20); // 将写入位置定位到第20个字节
    // 进行读取和写入操作
    file.close(); // 关闭文件
    return 0;
}
  1. 读取和写入数据:在定位到指定位置后,可以使用文件流对象的读写操作符<<>>来读取和写入数据。例如:
// 读取数据
int data;
file >> data;
// 写入数据
int newData = 100;
file << newData;
  1. 获取当前文件指针位置:可以使用tellg()tellp()函数获取当前的读取和写入位置。例如:
std::streampos readPos = file.tellg(); // 获取当前读取位置
std::streampos writePos = file.tellp(); // 获取当前写入位置

注意事项:

  • 在使用随机访问文件时,确保打开文件时使用了适当的打开模式(如std::ios::binary)。
  • 在进行定位操作前,需要先判断文件是否成功打开,以避免使用无效的文件流对象。
  • 在进行读取和写入操作前,可以通过检查文件指针的状态来确保操作的有效性。
  • 在文本方式下,使用seekg()seekp()函数进行定位时,参数表示的是字符的偏移量,而不是字节的偏移量。这意味着如果文件中包含多字节字符(如中文字符),那么定位到的位置可能不是准确的字节位置。另外,使用文本方式打开文件时,读取和写入的数据会被进行字符编码的转换。这可能导致在读取或写入时,实际的字节数和字符数不一致。
    综上所述,虽然可以在文本方式下进行定位操作,但由于字符和字节之间的转换以及多字节字符的存在,可能会导致定位的准确性和一致性问题。如果需要进行精确的字节级别的随机访问,建议使用二进制方式打开文件

四、异常处理

当我们需要处理多种类型的异常时,可以使用多个 catch 块来捕获不同类型的异常并采取相应的处理措施。下面是一个示例,展示了多个 catch 块的异常处理:

#include <iostream>
#include <stdexcept>

int main() {
    try {
        // 代码块中可能会抛出异常的操作
        int a = 10;
        int b = 0;

        if (b == 0) {
            throw std::runtime_error("Divide by zero error");  // 抛出异常
        }

        int result = a / b;
        std::cout << "Result: " << result << std::endl;
    } catch (const std::runtime_error& e) {
        // 捕获 std::runtime_error 异常
        std::cout << "Runtime error caught: " << e.what() << std::endl;
    } catch (const std::logic_error& e) {
        // 捕获 std::logic_error 异常
        std::cout << "Logic error caught: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // 捕获其他类型的异常
        std::cout << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

在上述代码中,我们使用了三个 catch 块来捕获不同类型的异常。首先,我们捕获了 std::runtime_error 类型的异常,然后是 std::logic_error 类型的异常,最后是其他类型的异常,这里使用了 std::exception 类来捕获。

当抛出异常时,首先会匹配第一个能够捕获到的 catch 块,如果匹配不到则继续匹配下一个 catch 块。如果所有的 catch 块都没有匹配到相应的异常类型,异常将会继续向上层传递,直到找到合适的异常处理位置或导致程序终止。

在上述示例中,如果除数为0,会抛出一个 std::runtime_error 异常,然后被第一个 catch 块捕获,输出相应的错误信息。

输出结果:

Runtime error caught: Divide by zero error

通过使用多个 catch 块,我们可以对不同类型的异常进行精确的处理,以便针对不同的异常情况采取适当的处理逻辑。

  • throw可以添加在程序任何位置,但trycatch需要配套使用。
  • 一旦遇到throw,该语句块的后续语句将会被跳过。
  • 当一个异常被抛出时,它会开始在调用栈中向上寻找能够处理该异常的 catch 块。调用栈中的每个函数都会被依次查看,以确定是否存在与抛出的异常类型匹配的 catch 块。如果当前函数没有合适的 catch 块来处理异常,该函数就会终止,并将异常传递给调用它的函数。这个过程会一直继续,直到找到能够处理异常的 catch 块或者到达程序的入口点。如果没有找到合适的处理位置,程序将会终止并显示未捕获异常的错误消息。

五、模板

1. 函数模板

#include <iostream>

template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    int maxInt = maximum(5, 10);
    double maxDouble = maximum(3.14, 2.718);
    char maxChar = maximum('a', 'z');

    std::cout << "Max Integer: " << maxInt << std::endl;
    std::cout << "Max Double: " << maxDouble << std::endl;
    std::cout << "Max Char: " << maxChar << std::endl;

    return 0;
}

在上述示例中,maximum 函数是一个模板函数,它接受两个相同类型的参数,并返回其中较大的值。在 main 函数中,我们分别使用了整数、浮点数和字符来调用 maximum 函数,并打印出结果。

输出结果:

Max Integer: 10
Max Double: 3.14
Max Char: z

另外,我们还可以使用类模板来定义通用的类。类模板可以使用一个或多个类型参数,并在类的成员函数和成员变量中使用这些参数。

2.类模板

要使用类模板,首先需要实例化它,将具体的类型作为模板参数传递给模板类。然后可以像使用普通类一样使用模板类的对象和成员函数。

下面是使用类模板的示例:

#include <iostream>

template <typename T>
class MyTemplate {
private:
    T data;
public:
    MyTemplate(T value) : data(value) {}
    void printData() {
        std::cout << "Data: " << data << std::endl;
    }
};

int main() {
    // 实例化 MyTemplate 类模板为 MyTemplate<int>
    MyTemplate<int> obj1(10);
    obj1.printData();  // 输出: Data: 10
    // 实例化 MyTemplate 类模板为 MyTemplate<double>
    MyTemplate<double> obj2(3.14);
    obj2.printData();  // 输出: Data: 3.14
    return 0;
}

在上述示例中,我们定义了一个类模板 MyTemplate,它有一个模板参数 T。模板类中有一个私有数据成员 data 和一个公有成员函数 printData,用于打印数据。

main 函数中,我们实例化了两个对象 obj1obj2,分别为 MyTemplate<int>MyTemplate<double>。在实例化时,我们通过构造函数将具体的值传递给了模板类的数据成员。然后,我们可以通过这些对象调用 printData 函数,输出存储的数据。

编译和运行上述代码,将得到以下输出:

Data: 10
Data: 3.14

通过类模板,我们可以在编写一次代码的基础上,根据不同的类型实例化出多个具体的类,以适应不同类型的数据操作需求。

六、STL标准模板库

STL主要包含以下三个组件:

  • 容器(Containers):提供了各种数据结构,如向量(vector)、链表(list)、集合(set)、映射(map)等。容器用于存储和管理数据,并提供了不同的访问和操作方式。
  • 算法(Algorithms):包含了大量的通用算法,如排序、搜索、遍历等。这些算法可以直接应用于容器,提供了对容器中元素的操作和处理。
  • 迭代器(Iterators):用于遍历和访问容器中的元素。迭代器提供了一种统一的访问接口,使得算法可以独立于容器进行操作,增加了代码的灵活性和可重用性。

容器类型:

  1. 顺序容器(Sequence Containers):

    • std::vector:动态数组,可以快速随机访问元素。
    • std::list:双向链表,支持高效插入和删除操作。
    • std::deque:双端队列,支持在两端进行快速插入和删除操作。
    • std::array:固定大小的数组,大小在编译时确定。
  2. 关联容器(Associative Containers):

    • std::set:有序集合,不允许重复元素。
    • std::multiset:有序集合,允许重复元素。
    • std::map:有序键值对容器,基于键进行排序。
    • std::multimap:有序键值对容器,允许重复的键。
  3. 容器适配器(Container Adapters):

    • std::stack:栈,基于 LIFO(后进先出)原则。
    • std::queue:队列,基于 FIFO(先进先出)原则。

随机访问
在 C++ 标准库中,以下容器支持随机访问:

  1. std::vector:动态数组,支持快速随机访问和修改元素。
  2. std::array:固定大小的数组,支持快速随机访问和修改元素。
  3. std::deque:双端队列,支持快速随机访问和修改元素,也支持在两端进行插入和删除操作。
  4. std::string:字符串类,底层实现为字符数组,支持随机访问和修改字符。
  5. std::mapstd::multimap:关联容器,底层实现为红黑树,支持按键进行随机访问和修改。
  6. std::setstd::multiset:关联容器,底层实现为红黑树,支持按序访问和修改元素。

1. std::array容器

std::array 是 C++ 标准库提供的容器之一,用于存储固定大小的连续元素序列。与 C 风格的数组相比,std::array 提供了更多的功能和安全性,并且可以使用标准库的算法和函数进行操作。

使用 std::array 需要包含 <array> 头文件,并使用 std::array 模板类来定义数组对象。以下是 std::array 的基本用法示例:

#include <iostream>
#include <array>

int main() {
    std::array<int, 5> numbers = {1, 2, 3, 4, 5};

    // 访问数组元素
    std::cout << "First element: " << numbers[0] << std::endl;
    std::cout << "Size: " << numbers.size() << std::endl;

    // 遍历数组
    for (const auto& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 修改数组元素
    numbers[2] = 10;

    // 使用迭代器遍历数组
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上述示例中,我们创建了一个 std::array 对象 numbers,它包含了 5 个整数元素。我们可以使用索引运算符 [] 访问数组中的元素,使用 size() 函数获取数组的大小。使用范围-based for 循环可以方便地遍历数组中的元素。我们还可以通过迭代器来遍历数组,并使用 begin()end() 函数获取迭代器的起始和结束位置。

除了上述示例中的功能,std::array 还提供了一些其他的成员函数和操作符,如 at()front()back()fill()empty()swap() 等,可以根据需要进行使用。

需要注意的是,std::array 的大小是固定的,并且在编译时确定,不支持动态调整大小。如果需要动态大小的容器,可以使用 std::vector 或其他容器类型。

编译和运行上述代码,将得到以下输出:

First element: 1
Size: 5
1 2 3 4 5 
1 2 10 4 5 

2. std::vector容器

std::vector 是 C++ 标准库中的一个动态数组容器,提供了动态调整大小、快速访问元素、插入和删除元素等功能。使用 std::vector 可以方便地管理和操作动态大小的数组。

使用 std::vector 需要包含 <vector> 头文件,并使用 std::vector 模板类来定义向量对象。以下是 std::vector 的基本用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers; // 创建一个空的向量

    // 添加元素
    numbers.push_back(1);
    numbers.push_back(2);
    numbers.push_back(3);

    // 获取向量的大小
    std::cout << "Size: " << numbers.size() << std::endl;

    // 访问元素
    std::cout << "First element: " << numbers[0] << std::endl;

    // 遍历向量
    for (const auto& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 修改元素
    numbers[1] = 5;

    // 使用迭代器遍历向量
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上述示例中,我们创建了一个空的 std::vector 对象 numbers,并使用 push_back() 函数向向量中添加元素。使用 size() 函数可以获取向量的大小,使用索引运算符 [] 可以访问向量中的元素。使用范围-based for 循环可以方便地遍历向量中的元素。我们还可以通过迭代器来遍历向量,并使用 begin()end() 函数获取迭代器的起始和结束位置。

除了上述示例中的功能,std::vector 还提供了一些其他的成员函数和操作符,如 at()front()back()insert()erase()empty()swap() 等,可以根据需要进行使用。

编译和运行上述代码,将得到以下输出:

Size: 3
First element: 1
1 2 3 
1 5 3 

需要注意的是,std::vector 是一个动态数组,可以根据需要动态调整大小,因此适合在需要动态增减元素的情况下使用。

3. std::list容器

std::list 是 C++ 标准库中的一个双向链表容器,提供了高效的插入和删除操作,并支持在任意位置进行快速的元素插入和删除。使用 std::list 可以方便地管理和操作链表结构。

使用 std::list 需要包含 <list> 头文件,并使用 std::list 模板类来定义链表对象。以下是 std::list 的基本用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> numbers; // 创建一个空的链表

    // 添加元素
    numbers.push_back(1);
    numbers.push_back(2);
    numbers.push_back(3);

    // 获取链表的大小
    std::cout << "Size: " << numbers.size() << std::endl;

    // 访问元素
    std::cout << "First element: " << numbers.front() << std::endl;

    // 遍历链表
    for (const auto& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 在指定位置插入元素
    auto it = std::next(numbers.begin()); // 获取第二个元素的迭代器
    numbers.insert(it, 4);

    // 删除元素
    numbers.pop_back();

    // 使用迭代器遍历链表
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上述示例中,我们创建了一个空的 std::list 对象 numbers,并使用 push_back() 函数向链表中添加元素。使用 size() 函数可以获取链表的大小,使用 front() 函数可以获取链表的第一个元素。使用范围-based for 循环可以方便地遍历链表中的元素。我们还可以使用 insert() 函数在指定位置插入元素,使用 pop_back() 函数删除链表的最后一个元素。使用迭代器可以遍历链表中的元素,并使用 begin()end() 函数获取迭代器的起始和结束位置。

除了上述示例中的功能,std::list 还提供了一些其他的成员函数和操作符,如 push_front()pop_front()insert()erase()empty()reverse()sort() 等,可以根据需要进行使用。

编译和运行上述代码,将得到以下输出:

Size: 3
First element: 1
1 2 3
1 4 2

需要注意的是,由于 std::list 是一个双向链表,因此在访问和遍历元素时,可以从前向后或从后向前遍历。同时,由于链表的特性,插入和删除元素的操作是高效的,适合在需要频繁进行插入和删除操作的场景中使用。

4. 迭代器

在 C++ 标准库中,不同的容器提供了不同类型的迭代器,可以通过成员函数 begin() 和 end() 来获取容器的起始和结束迭代器。例如,对于 std::vector 容器,可以使用 std::vector::iterator 类型的迭代器进行遍历;对于 std::list 容器,可以使用 std::list::iterator 类型的迭代器进行遍历。

Ⅰ. std::array迭代器:

std::array 是一个固定大小的数组容器,它的迭代器是普通指针类型。可以使用 begin() 函数获取指向第一个元素的迭代器,使用 end() 函数获取指向最后一个元素之后位置的迭代器。可以使用迭代器进行遍历,也可以使用迭代器进行元素的访问和修改。

示例:

  std::array<int, 5> arr = {1, 2, 3, 4, 5};
  auto it = arr.begin();
  while (it != arr.end()) {
      std::cout << *it << " ";
      ++it;
  }

Ⅱ. std::vector迭代器:

std::vector 是一个动态数组容器,它的迭代器也是普通指针类型,与 std::array 类似。可以使用 begin() 函数获取指向第一个元素的迭代器,使用 end() 函数获取指向最后一个元素之后位置的迭代器。可以使用迭代器进行遍历,也可以使用迭代器进行元素的访问和修改。

示例:

std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
while (it != vec.end()) {
    std::cout << *it << " ";
    ++it;
}

Ⅲ. std::list迭代器:

std::list 是一个双向链表容器,它的迭代器是双向迭代器,可以双向遍历链表的元素。可以使用 begin() 函数获取指向第一个元素的迭代器,使用 end() 函数获取指向最后一个元素之后位置的迭代器。除了正向遍历外,还可以使用 rbegin() 函数获取指向最后一个元素的逆向迭代器,使用 rend() 函数获取指向第一个元素之前位置的逆向迭代器。

示例:

std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
while (it != lst.end()) {
    std::cout << *it << " ";
    ++it;
}

auto rit = lst.rbegin();
while (rit != lst.rend()) {
    std::cout << *rit << " ";
    ++rit;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值