类型推断

主要包括auto, decltype, 模板类型推断几种

auto 关键字

auto是C++11引入的关键字,允许编译器根据右侧的表达式自动推断变量的类型。通过使用auto,我们可以避免显式地写出复杂的类型定义,编译器会根据表达式的类型来推断。

特点:

  • auto只能用于声明和定义,不能用作函数的参数类型。
  • 编译器根据初始化的表达式推断类型。

auto 推导规则的特殊情况

  • 指针和引用:如果初始值是指针或引用,auto会保留它们的类型。
  • 数组与函数指针:auto不会直接推断为数组类型或函数类型,而是推断为指针。
#include <iostream>

int main() {
    int x = 42;
    int& ref = x;
    auto a = ref;  // a是int类型,而不是int&类型
    a = 100;
    
    std::cout << "x: " << x << ", a: " << a << std::endl;  // 输出 x: 42, a: 100

    return 0;
}

推断返回类型

#include <iostream>
#include <type_traits>

template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
    return a + b;
}

int main() {
    int x = 5;
    double y = 10.5;

    // 返回类型被推断为 decltype(x + y),即 double
    auto result = add(x, y);

    std::cout << "Result: " << result << std::endl;  // 输出 15.5
}

C++14引入了一种新特性,即允许在函数返回值处使用auto,编译器会根据函数体中的返回语句自动推断返回类型。这对模板函数尤其有用,可以避免显式地指定返回类型。

#include <iostream>

auto add(int x, int y) {
    return x + y;  // 编译器推断返回类型为int
}

int main() {
    auto result = add(5, 3);
    std::cout << "Result: " << result << std::endl;  // 输出Result: 8
    return 0;
}

C++17引入了结构化绑定,使得可以将多个值的返回结果通过auto绑定到多个变量中。这对于返回std::pair或std::tuple的函数特别有用。

#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> getValues() {
    return {42, 3.14, "Hello"};
}

int main() {
    auto [x, y, z] = getValues();  // 结构化绑定
    std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;  // 输出 x: 42, y: 3.14, z: Hello
    return 0;
}

decltype 关键字

decltype也是C++11引入的,用于推断表达式的类型,但与auto不同,decltype不会对表达式进行求值,它只会根据表达式的类型来推断变量类型。

特点:

  • decltype可以用于任何表达式,推断该表达式的类型。
  • 在复杂场景下可以用来获取某个变量或表达式的精确类型。
#include <iostream>

int main() {
    int a = 5;
    decltype(a) b = a * 2;  // 推断b的类型为int
    decltype((a)) c = a;    // 推断为int&(左值引用)

    std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;

    int x = 42;
    double y = 3.14;
    // 使用decltype推断表达式的类型,auto用于变量声明
    decltype(x + y) result = x + y;  // 推断为double

    return 0;
}

模板类型推断

模板类型推断是C++的核心特性之一,允许编译器自动根据传入的模板参数推断类型。

#include <iostream>

template<typename T>
void printType(T x) {
    std::cout << "Type: " << typeid(x).name() << ", Value: " << x << std::endl;
}

int main() {
    printType(42);           // 推断T为int
    printType(3.14);         // 推断T为double
    printType("Hello");      // 推断T为const char*
    return 0;
}

在C++11及以后,auto和decltype与模板结合使用,进一步简化了模板编程。


initializer_list

std::initializer_list 允许通过 {} 语法进行列表初始化。它常用于构造函数、函数参数,尤其适合简洁地传递一组值。常见的 C++ 标准容器和自定义类(构造函数接受 std::initializer_list 作为参数的自定义类。)都可以通过 std::initializer_list 支持列表初始化。

std::initializer_list 的限制:

  • 只读性:std::initializer_list 内的元素是常量,不能被修改。如果需要可修改的列表,可以考虑使用其他容器(如 std::vector)。
  • 不支持动态大小:std::initializer_list 是不可变的,一旦创建,大小固定。

std::initializer_list 是一个轻量级的类模板,通常由两个指针组成:一个指向元素数组的指针,元素数量的计数。这使得它非常高效,因为它只是持有指向数据的指针,并没有进行额外的拷贝或内存管理。元素可以通过迭代器访问。在编译时能够推导出类型。

template<class T>
class initializer_list {
public:
    // 返回指向数组的指针
    const T* begin() const noexcept { return _array; }

    // 返回数组末尾
    const T* end() const noexcept { return _array + _size; }

    // 返回元素个数
    size_t size() const noexcept { return _size; }

private:
    const T* _array;
    size_t _size;
};

自定义类使用 std::initializer_list 构造函数

#include <iostream>
#include <initializer_list>

class MyClass {
public:
    std::initializer_list<int> values;

    MyClass(std::initializer_list<int> list): values(list) {
        for (int value : list) {
            std::cout << "Initializing with value: " << value << std::endl;
        }
    }

    void printValues() const {
        for (int value : values) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MyClass obj = {5, 10, 15, 20};  // 使用std::initializer_list初始化
    obj.printValues();               // 输出 5 10 15 20
    return 0;
}

STL

C++中的STL(标准模板库)是编程中非常重要的一个部分,STL不仅提供了丰富的数据结构和算法,同时也是泛型编程的重要应用。

选择合适的STL容器通常取决于你对数据结构的需求,如访问速度、插入/删除操作的频率、内存使用等。以下是一些常见的选择标准:

  • std::vector:当你需要频繁随机访问元素,且只在末尾插入/删除时。
    • push_back(const T& value):在末尾添加元素。
    • pop_back():移除末尾元素。
    • size():返回当前元素个数。
    • capacity():返回当前分配的内存大小(可以容纳多少元素)。
    • reserve(size_t n):预留至少 n 个元素的存储空间。
    • resize(size_t n):改变容器大小,增加的部分会默认初始化。
    • at(size_t index):访问指定索引处的元素,并进行范围检查。
    • operator[]:通过索引访问元素,不进行范围检查。
    • front():返回第一个元素。
    • back():返回最后一个元素。
    • clear():清空所有元素。
    • insert(iterator pos, const T& value):在指定位置插入元素。
    • erase(iterator pos):移除指定位置的元素。
    • emplace_back(args...):原地构造元素并添加到末尾。
  • std::deque:一个双端队列,支持在两端进行快速插入和删除。
    • push_back(const T& value):在末尾添加元素。
    • push_front(const T& value):在开头添加元素。
    • pop_back():移除末尾元素。
    • pop_front():移除开头元素。
    • size():返回当前元素个数。
    • at(size_t index):访问指定索引处的元素,并进行范围检查。
    • operator[]:通过索引访问元素,不进行范围检查。
    • front():返回第一个元素。
    • back():返回最后一个元素。
    • clear():清空所有元素。
    • insert(iterator pos, const T& value):在指定位置插入元素。
    • erase(iterator pos):移除指定位置的元素。
  • std::array:固定大小数组
  • std::forward_list:单向链表
  • std::list:双向链表,支持高效的插入和删除操作,尤其是在中间或两端。
    • push_back(const T& value):在末尾添加元素。
    • push_front(const T& value):在开头添加元素。
    • pop_back():移除末尾元素。
    • pop_front():移除开头元素。
    • size():返回当前元素个数。
    • empty():检查容器是否为空。
    • front():返回第一个元素。
    • back():返回最后一个元素。
    • insert(iterator pos, const T& value):在指定位置插入元素。
    • erase(iterator pos):移除指定位置的元素。
    • remove(const T& value):移除所有等于 value 的元素。
    • sort():对列表进行排序。
    • reverse():将列表元素顺序反转。
  • std::set:是一个不允许重复元素的有序集合,基于红黑树实现。
    • insert(const T& value):插入元素,元素会自动排序。
    • erase(const T& value):移除指定元素。
    • size():返回当前元素个数。
    • empty():检查容器是否为空。
    • find(const T& value):查找元素,返回指向该元素的迭代器。
    • count(const T& value):检查是否包含某个元素(返回 1 或 0)。
    • clear():清空所有元素。
    • begin():返回指向第一个元素的迭代器。
    • end():返回指向最后一个元素之后的迭代器。
  • std::unordered_set: 一种基于哈希表实现的容器,用于存储唯一的无序元素。通过哈希表实现常数时间复杂度的插入、删除和查找操作。
    • insert(const T& value):向集合中插入元素。如果元素已存在,则不会插入新的值。
    • erase(const T& value):从集合中移除等于 value 的元素,若不存在该元素则不做任何操作。
    • clear():清空集合中的所有元素。
    • find(const T& value):查找元素,返回指向该元素的迭代器,如果未找到则返回 end() 迭代器。
    • count(const T& value):检查集合中是否包含元素 value,如果包含则返回 1,否则返回 0。
    • size():返回集合中的元素个数。
    • empty():检查集合是否为空。
    • begin():返回指向第一个元素的迭代器。
    • end():返回指向容器末尾的迭代器(最后一个元素的下一个位置)。
    • cbegin() 和 cend():返回常量迭代器,保证迭代时集合内容不可修改。
    • bucket_count():返回当前哈希表中桶(bucket)的数量。
    • max_bucket_count():返回容器可能包含的最大桶数。
    • load_factor():返回当前哈希表的装载因子,即元素数量与桶数量的比值。
    • rehash(size_t n):重组容器,使其至少有 n 个桶。
    • reserve(size_t n):预留足够空间以容纳至少 n 个元素,并减少重哈希操作的次数。
  • std::multiset
  • std::unordered_multiset
  • std::multimap:允许存在重复的键,即相同的键可以关联多个值,一对多的关系。使用 find() 方法查找某个键时,只能返回第一个找到的键值对。如果需要找到该键对应的所有值,可以使用 equal_range() 或者 lower_bound() 和 upper_bound() 组合使用来获取一个范围。
  • std::unordered_multimap
  • std::map: 是一个基于键值对的有序关联容器,键是唯一的,一对一的关系。
    • insert(const std::pair<K, V>& value):插入键值对。
    • erase(const K& key):通过键删除元素。
    • size():返回当前元素个数。
    • empty():检查容器是否为空。
    • find(const K& key):查找键值对,返回指向该元素的迭代器。
    • count(const K& key):检查是否包含某个键。
    • clear():清空所有元素。
    • operator[]:通过键访问或插入元素。
  • std::unordered_map: 是一个基于哈希表实现的映射,键是唯一的,且不保证顺序。
    • insert(const std::pair<K, V>& value):插入键值对。
    • erase(const K& key):通过键删除元素。`
    • size():返回当前元素个数。
    • empty():检查容器是否为空。
    • find(const K& key):查找键值对,返回指向该元素的迭代器。
    • count(const K& key):检查是否包含某个键。
    • clear():清空所有元素。
    • operator[]:通过键访问或插入元素。
  • std::stack: 适合后进先出 (LIFO) 的数据结构。
    • push(const T& value):将元素压入栈顶。
    • pop():移除栈顶元素。
    • top():返回栈顶元素。
    • size():返回当前元素个数。
    • empty():检查容器是否为空。
  • std::queue: 一种适合先进先出 (FIFO) 的数据结构
    • push(const T& value):将元素添加到队尾。
    • pop():移除队首元素。
    • front():返回队首元素。
    • back():返回队尾元素。
    • size():返回当前元素个数。
    • empty():检查容器是否为空。
  • std::priority_queue, 是一种元素具有优先级的队列,默认情况下,最大元素会在队首。
    • push(const T& value):将元素插入队列。
    • pop():移除优先级最高的元素。
    • top():返回优先级最高的元素。
    • size():返回当前元素个数。
    • empty():检查容器是否为空。

STL容器本身不是线程安全的。多个线程同时读写同一个容器,或者同时修改容器状态(比如插入或删除元素)时,可能会导致数据竞争。因此,需要通过锁(如std::mutex)来确保线程安全。

STL广泛采用了泛型编程,所有STL容器和算法都使用模板,使它们能够处理不同的数据类型。

#include <iostream>
#include <vector>

template <typename T>
void print_vector(const std::vector<T>& vec) {
    for (const auto& item : vec) {
        std::cout << item << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> int_vec = {1, 2, 3};
    std::vector<double> double_vec = {1.1, 2.2, 3.3};

    print_vector(int_vec);
    print_vector(double_vec);

    return 0;
}

RAII

STL中,像std::vector、std::unique_ptr等容器和智能指针都遵循RAII原则。RAII(Resource Acquisition Is Initialization)是一种资源管理的原则。在C++中,RAII意味着对象的生命周期与资源的管理直接相关。当对象被创建时,资源被分配;当对象被销毁时,资源自动释放。

#include <iostream>
#include <vector>

int main() {
    {
        std::vector<int> vec = {1, 2, 3};
        // vec创建时分配内存,RAII管理其生命周期
    }
    // 离开作用域时,vec自动销毁,内存被释放

    return 0;
}

allocator

Allocator是STL中用于管理内存分配的机制。STL中的所有容器都使用allocator来控制如何分配、释放和管理内存。这使得STL能够更灵活地适应不同的内存管理需求。

默认的allocator是std::allocator,但用户可以自定义allocator来优化内存分配策略(比如用于嵌入式系统中的内存池)。

#include <iostream>
#include <vector>
#include <memory>

int main() {
    std::vector<int, std::allocator<int>> vec = {1, 2, 3};

    return 0;
}

STL中的优先级队列是如何实现的?

STL中的priority_queue是基于堆(heap)实现的。优先级队列默认是一个最大堆,即每次取出的元素都是当前队列中的最大值(或最小值,取决于定义)。堆的性质确保插入和删除的时间复杂度为O(log n)。

代码示例:

#include <iostream>
#include <queue>
#include <vector>

int main() {
    std::priority_queue<int> pq;

    pq.push(10);
    pq.push(5);
    pq.push(20);

    while (!pq.empty()) {
        std::cout << pq.top() << " ";  // 每次输出最大值
        pq.pop();
    }

    return 0;
}

这里,std::priority_queue是通过std::vector存储数据,并使用堆来维持顺序。内部使用std::make_heap()、std::push_heap()和std::pop_heap()等算法。

STL中常见的算法库

STL中的算法库是其核心部分之一,包含了很多常见的算法,比如排序、查找、遍历等。以下是一些常见的算法:

  • std::sort:快速排序,时间复杂度为O(n log n)。
  • std::find:在范围内查找某个元素,线性时间复杂度。
  • std::binary_search:二分查找,时间复杂度为O(log n),需要排序数组。
  • std::accumulate:用于计算区间内所有元素的累加值。

代码示例:

#include <iostream>
#include <vector>
#include <algorithm>

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

    // 排序
    std::sort(vec.begin(), vec.end());

    // 查找
    if (std::binary_search(vec.begin(), vec.end(), 3)) {
        std::cout << "找到元素3" << std::endl;
    }

    return 0;
}

map的底层实现

std::map的底层实现是红黑树,这是一种自平衡二叉搜索树。红黑树保证插入、删除、查找的时间复杂度为O(log n)。红黑树通过颜色标记和旋转操作保持平衡。

set的底层实现

std::set的底层实现与std::map类似,也是基于红黑树。区别在于std::set只存储键,而不存储值。