首页 > 编程语言 >C++ STL迭代器、resize和reserve区别、map和unordered_map区别,vector和List区别、map和set区别、push_back和emplace_back区别

C++ STL迭代器、resize和reserve区别、map和unordered_map区别,vector和List区别、map和set区别、push_back和emplace_back区别

时间:2024-10-16 09:18:15浏览次数:8  
标签:std map cout 迭代 区别 元素 back vector

1. STL 迭代器删除元素

迭代器失效是指在对容器进行修改时,原有的迭代器可能变得不可用。正确删除元素的方法是使用 erase,并返回新的有效迭代器。

示例代码
#include <iostream>
#include <vector>

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

    // 输出初始状态
    std::cout << "Initial vector: ";
    for (int n : vec) std::cout << n << " ";
    std::cout << std::endl;

    // 使用迭代器遍历并删除元素
    for (auto it = vec.begin(); it != vec.end(); ) {
        if (*it == 3) {
            it = vec.erase(it); // 删除元素并返回下一个有效迭代器
            std::cout << "Removed 3, new vector: ";
            for (int n : vec) std::cout << n << " "; // 输出当前 vector
            std::cout << std::endl;
        } else {
            ++it; // 继续下一个元素
        }
    }

    // 输出最终结果
    std::cout << "Final vector: ";
    for (int n : vec) std::cout << n << " "; // 输出 1 2 4 5
    std::cout << std::endl;

    return 0;
}
删除值为 3 的元素时,程序通过更新迭代器避免了失效问题。

1. 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个 元素都往前移动一位,erase返回下一个有效的迭代器;

2. 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可;

3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。

2. resizereserve 的区别

  • reserve:预留内存,不改变 size,只影响 capacity
  • resize:改变 size,多余的元素会被移除或填充默认值。
示例代码

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    // 预留空间
    vec.reserve(10);
    std::cout << "After reserve(10): Size = " << vec.size() << ", Capacity = " << vec.capacity() << std::endl;

    // 调用 resize 改变大小
    vec.resize(10);
    std::cout << "After resize(10): Size = " << vec.size() << ", Capacity = " << vec.capacity() << std::endl;

    // 输出元素
    std::cout << "Elements: ";
    for (int n : vec) std::cout << n << " "; // 输出 0 0 0 0 0 0 0 0 0 0
    std::cout << std::endl;

    // 继续添加元素
    for (int i = 0; i < 5; ++i) {
        vec.push_back(i + 1);
    }
    std::cout << "After adding 5 elements: Size = " << vec.size() << ", Capacity = " << vec.capacity() << std::endl;

    // 输出最终元素
    std::cout << "Final Elements: ";
    for (int n : vec) std::cout << n << " "; // 输出 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5
    std::cout << std::endl;

    return 0;
}

这里 reserve 预留了空间而不初始化元素,resize 则初始化了新增加的元素。

1. 首先必须弄清楚两个概念:

(1)capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通 过下标等访问,因为此时容器中还没有创建任何对象。

(2)size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。

2. resize和reserve区别主要有以下几点:

(1)resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对 象,需要通过insert()或push_back()等创建对象。

(2)resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大 小。

(3)两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为 0);reserve只带一个参数,表示容器预留的大小。

问题延伸: resize 和 reserve 既有差别,也有共同点。两个接口的共同点是它们都保证了vector的空间大小 (capacity)最少达到它的参数所指定的大小。

下面就他们的细节进行分析。 为实现resize的语义,resize接口做了两个保证:

(1)保证区间[0, new_size)范围内数据有效,如果下标index在此区间内,vector[indext]是合法的;

(2)保证区间[0, new_size)范围以外数据无效,如果下标index在区间外,vector[indext]是非法的。

reserve只是保证vector的空间大小(capacity)最少达到它的参数所指定的大小n。在区间[0, n)范围内, 如果下标是index,vector[index]这种访问有可能是合法的,也有可能是非法的,视具体情况而定。 以下是两个接口的源代码:

template <class T, class Allocator = std::allocator<T>>
class vector {
public:
    void reserve(size_type new_cap) {
        if (new_cap > capacity()) {
            pointer new_data = alloc.allocate(new_cap); // 分配新内存
            for (size_type i = 0; i < size(); ++i) {
                alloc.construct(new_data + i, std::move_if_noexcept(data[i])); // 移动元素
            }
            for (size_type i = 0; i < size(); ++i) {
                alloc.destroy(data + i); // 销毁旧元素
            }
            alloc.deallocate(data, capacity()); // 释放旧内存
            data = new_data;
            cap = new_cap;
        }
    }
};
 

template <class T, class Allocator = std::allocator<T>>
class vector {
public:
    void resize(size_type count, T value = T()) {
        if (count < size()) {
            for (size_type i = count; i < size(); ++i) {
                alloc.destroy(data + i); // 销毁多余的元素
            }
        } else if (count > size()) {
            reserve(count); // 确保容量足够
            for (size_type i = size(); i < count; ++i) {
                alloc.construct(data + i, value); // 添加新元素
            }
        }
        size_ = count; // 更新大小
    }
};

注意:如果n大于当前的vector的容量(是容量,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。而内存的重新配置会很耗时间。

3. STL 容器动态链接可能产生的问题

动态链接可能引发多种问题,主要包括 ABI 兼容性、符号冲突和性能开销。

  • ABI 兼容性:不同编译器或 STL 实现可能导致二进制不兼容,进而引发运行时错误。
  • 符号冲突:同名符号在不同库中可能导致链接错误。
  • 性能开销:动态链接在每次调用时可能引入查找开销,影响性能。

4. mapunordered_map 的区别

map
  • 实现:红黑树,元素按键值排序。
  • 复杂度:O(log n) 的插入、删除和查找。
unordered_map
  • 实现:哈希表,无序存储。
  • 复杂度:平均 O(1) 的插入、删除和查找,最坏 O(n)。
示例代码

#include <iostream>
#include <map>
#include <unordered_map>

int main() {
    // 使用 map
    std::map<int, std::string> orderedMap;
    orderedMap[1] = "One";
    orderedMap[3] = "Three";
    orderedMap[2] = "Two";

    std::cout << "Ordered map (sorted by keys):" << std::endl;
    for (const auto& pair : orderedMap) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    // 使用 unordered_map
    std::unordered_map<int, std::string> unorderedMap;
    unorderedMap[1] = "One";
    unorderedMap[3] = "Three";
    unorderedMap[2] = "Two";

    std::cout << "Unordered map (unordered keys):" << std::endl;
    for (const auto& pair : unorderedMap) {
        std::cout << pair.first << ": " << pair.second << std::endl; // 输出顺序不确定
    }

    return 0;
}

map和unordered_map的区别在于他们的实现基理不同。

1. map实现机理

map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索 树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表 着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。

2. unordered_map实现机理

unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位 置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素 的排列顺序是无序的。

5. vectorlist 的区别及适用场景

vector
  • 动态数组,支持随机访问。
  • 适合场景:大量读操作、尾部插入。
list
  • 双向链表,每个元素包含前后指针。
  • 适合场景:频繁中间插入和删除。
示例代码

#include <iostream>
#include <vector>
#include <list>

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

    // vector 随机访问
    std::cout << "Vector element at index 2: " << vec[2] << std::endl; // 输出 3

    // list 中间插入
    auto it = lst.begin();
    std::advance(it, 2); // 移动到第三个元素
    lst.insert(it, 10); // 在第三个元素之前插入 10

    std::cout << "List after insertion: ";
    for (int n : lst) std::cout << n << " "; // 输出 1 2 10 3 4 5
    std::cout << std::endl;

    return 0;
}

vector和list区别在于底层实现机理不同,因而特性和适用场景也有所不同。

vector:一维数组

特点:元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。

优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)。

缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度O (n),另外当空间不足时还需要进行扩容。

list:双向链表

特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。

优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)。

缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n),没有提供[]操作符的重载。

应用场景

vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。

list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

6. vector 原理

vector底层实现原理为一维数组(元素在空间连续存放)。

1. 插入元素

insert 方法用于在指定位置插入一个或多个元素。它会返回指向新插入元素的迭代器。

示例代码

#include <iostream>
#include <vector>

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

    // 在第二个位置插入元素 3
    auto it = vec.insert(vec.begin() + 2, 3);

    std::cout << "Vector after insert: ";
    for (int n : vec) std::cout << n << " "; // 输出 1 2 3 4 5
    std::cout << "\nInserted element: " << *it << std::endl; // 输出插入的元素

    return 0;
}
 

2. 删除元素

erase 方法用于删除指定位置的元素或删除一个范围内的元素。删除后,迭代器会失效。

示例代码

#include <iostream>
#include <vector>

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

    // 删除第三个元素
    auto it = vec.erase(vec.begin() + 2); // 删除 3

    std::cout << "Vector after erase: ";
    for (int n : vec) std::cout << n << " "; // 输出 1 2 4 5
    std::cout << "\nNext element after erased: " << *it << std::endl; // 输出 4

    return 0;
}
 

3. 迭代器的管理

  • 失效性:当进行插入或删除操作后,原有的迭代器可能会失效。vectoreraseinsert 操作可能导致原有元素的移动,因此必须小心使用。

  • 更新迭代器:在插入和删除后,通常需要重新获取有效的迭代器。如果使用 erase,它会返回指向下一个有效元素的迭代器。

示例代码:插入与删除的迭代器管理

#include <iostream>
#include <vector>

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

    // 使用迭代器遍历
    for (auto it = vec.begin(); it != vec.end(); ) {
        if (*it % 2 == 0) { // 删除偶数
            it = vec.erase(it); // 删除元素并更新迭代器
        } else {
            ++it; // 继续下一个元素
        }
    }

    std::cout << "Vector after removing even numbers: ";
    for (int n : vec) std::cout << n << " "; // 输出 1 3 5
    std::cout << std::endl;

    return 0;
}

4. 总结

  • 插入:使用 insert 方法可以在任意位置插入元素,并返回指向新元素的迭代器。

  • 删除:使用 erase 方法删除指定元素,返回指向下一个元素的迭代器。

  • 迭代器管理:在插入和删除后,需谨慎管理迭代器,避免使用失效的迭代器。

vector 使用动态数组,支持按需扩展。其内部维护一个指向动态分配内存的指针和两个变量表示 sizecapacity

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    for (int i = 0; i < 15; ++i) {
        vec.push_back(i); // 动态扩展
        std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
    }

    return 0;
}
 

7. map 原理

map是关联式容器,它们的底层容器都是红黑树。map的所有元素都是pair,同时拥有实值(value) 和键值(key)。pair的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。

 map的特性如下

(1)map以RBTree作为底层容器;

(2)所有元素都是键+值存在;

(3)不允许键重复;

(4)所有元素是通过键进行自动排序的;

(5)map的键是不能修改的,但是其键对应的值是可以修改的。

红黑树特性
  1. 节点颜色为红或黑。
  2. 根节点为黑色。
  3. 每个叶子节点(空节点)都是黑色。
  4. 红色节点的子节点必须为黑色。
  5. 从根到每个叶子的路径上,黑色节点的数量相同。
示例代码

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap;
    myMap[1] = "One";
    myMap[3] = "Three";
    myMap[2] = "Two";

    std::cout << "Map elements in sorted order:" << std::endl;
    for (const auto& pair : myMap) {
        std::cout << pair.first << ": " << pair.second << std::endl; // 输出按键排序
    }

    return 0;
}
 

8. 删除元素后的指针和迭代器变化

  • 删除末尾元素
    • vectorlist 删除末尾元素时不会影响其他元素的迭代器。
  • 删除中间元素
    • vector 删除中间元素会导致后续元素的迭代器失效。
    • list 删除中间元素不会导致其他元素的迭代器失效。
示例代码
#include <iostream>
#include <vector>
#include <list>

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

    // 删除末尾元素
    vec.pop_back();
    lst.pop_back();

    std::cout << "Vector after pop_back: ";
    for (int n : vec) std::cout << n << " "; // 输出 1 2 3 4
    std::cout << std::endl;

    std::cout << "List after pop_back: ";
    for (int n : lst) std::cout << n << " "; // 输出 1 2 3 4
    std::cout << std::endl;

    // 删除中间元素
    vec.erase(vec.begin() + 2); // 删除 3
    auto it = lst.begin();
    std::advance(it, 2); // 移动到第三个元素
    lst.erase(it); // 删除 3

    std::cout << "Vector after erase(2): ";
    for (int n : vec) std::cout << n << " "; // 输出 1 2 4
    std::cout << std::endl;

    std::cout << "List after erase(2): ";
    for (int n : lst) std::cout << n << " "; // 输出 1 2 4
    std::cout << std::endl;

    return 0;
}

在这个示例中,vectorlist 删除元素后显示了各自的状态。

1. 迭代器和指针之间的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操 作符,-->、++、--等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容 器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更 高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。 迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输 出其自身。

2. vector和list特性

vector特性:动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删 元素具有较大的性能(大部分情况下是常数时间)。

list特性:双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。

3. vector增删元素

对于vector而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都 往前移动一位,erase返回下一个有效的迭代器。

4. list增删元素

对于list而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影 响

9. mapset 的区别及实现

  • map:键值对,按键排序,使用红黑树实现。
  • set:只存储键,按键排序,使用红黑树实现。

1. set是一种关联式容器,其特性如下:

(1)set以RBTree作为底层容器

(2)所得元素的只有key没有value,value就是key

(3)不允许出现键值重复

(4)所有的元素都会被自动排序

(5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的

2. map和set一样是关联式容器,其特性如下:

(1)map以RBTree作为底层容器

(2)所有元素都是键+值存在

(3)不允许键重复

(4)所有元素是通过键进行自动排序的

(5)map的键是不能修改的,但是其键对应的值是可以修改的

综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分 开的

示例代码
#include <iostream>
#include <map>
#include <set>

int main() {
    std::map<int, std::string> myMap;
    myMap[1] = "One";
    myMap[2] = "Two";

    std::set<int> mySet = {1, 2, 3};

    std::cout << "Map elements:" << std::endl;
    for (const auto& pair : myMap) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    std::cout << "Set elements:" << std::endl;
    for (int n : mySet) {
        std::cout << n << " "; // 输出 1 2 3
    }
    std::cout << std::endl;

    return 0;
}

10. push_backemplace_back 的区别

  • push_back:需要先构造对象,然后拷贝到容器。
  • emplace_back:直接在容器中构造对象,避免拷贝,提高性能。
示例代码
#include <iostream>
#include <vector>
#include <string>

class MyClass {
public:
    MyClass(int a, const std::string& str) : a(a), str(str) {
        std::cout << "Constructing MyClass with a=" << a << ", str=" << str << "\n";
    }
private:
    int a;
    std::string str;
};

int main() {
    std::vector<MyClass> vec;

    // 使用 push_back
    MyClass obj1(1, "Hello");
    vec.push_back(obj1); // 需要拷贝

    // 使用 emplace_back
    vec.emplace_back(2, "World"); // 直接构造

    return 0;
}

最后,推荐一个学习网站https://github.com/0voice

标签:std,map,cout,迭代,区别,元素,back,vector
From: https://blog.csdn.net/qq_50373827/article/details/142969320

相关文章

  • YOLOv11性能评估指标 AP、mAP、Precision、Recall、FPS、IoU、混淆矩阵、F1等YOLO相关
    开始讲解之前推荐一下我的专栏,本专栏的内容支持(分类、检测、分割、追踪、关键点检测),专栏目前为限时折扣,欢迎大家订阅本专栏,本专栏每周更新3-5篇最新机制,更有包含我所有改进的文件和交流群提供给大家。 专栏回顾:YOLOv11改进系列专栏——本专栏持续复习各种顶会内容——科......
  • 基于SpringBoot + mybatis + logback + shiro的仓库管理系统(完美运行、数据库源代码、
    文章目录前言一、系统功能模块二、开发环境三、部分功能模块展示3.1登录模块3.2后台首页3.3客户管理3.4供应商管理3.5商品管理3.6商品进货3.7商品退货查询3.8商品销售3.9销售退货查询3.10部门管理3.11菜单管理3.12权限管理3.13角色管理3.14用户管理3.15图......
  • 详细解释mAP@10,mAR@10,Prec@10
    系列文章目录文章目录系列文章目录1.精确度(Precision,Prec@N)2.平均精确度(MeanAveragePrecision,mAP@N)3.平均召回率(MeanAverageRecall,mAR@N)总结在信息检索和推荐系统中,mAP@10、mAR@10和Prec@10是常用的评价指标,它们用于衡量检索结果的质量。以下是对这......
  • gcc g++ 的区别
    GNUgccg++的区别GNU(GNUCompilercollection)是编译工具集,而g++(GNUc++compiler),gcc(GNUccompiler)从属于GNU;g++,gcc不是真的编译c/c++程序,而是调用GNU中的编译器;GNU包括:编译器,链接器,组装器等;gcc,g++最好只用来分别编译c/c++,别混用;gcc>g++原因在于gcc可......
  • 【JavaWeb】一文读懂Cookie、Session&Token 的区别和联系
    大佬精心打造:JavaWeb进阶学习资料》》点击免费获取【javaWeb】Cookie&Session&SpringSession原理分析简介Cookie、Session、Token这三者是不同发展阶段的产物,都是为了解决无状态的HTTP协议,提升网站的交互体验。但是他们各有优缺点,三者也没有明显的对立关系,反而常常......
  • APP广告变现,直客广告、广告联盟的区别
    APP广告变现的渠道有以下几种:大网盟/联盟指以穿山甲、优量汇、百度、快手为代表的联盟平台,是程序化广告行业的主要预算来源。头部&腰部DSP/SSP/ADX第三方广告平台拥有专属的电商、品牌、游戏,或RTA等预算资源,虽然预算份额和填充不能与联盟渠道相提并论,但可以对大联盟......
  • Dirmap:一款高级Web目录文件扫描工具
    需求分析何为一个优秀的web目录扫描工具?经过大量调研,总结一个优秀的web目录扫描工具至少具备以下功能:并发引擎能使用字典能纯爆破能爬取页面动态生成字典能fuzz扫描自定义请求自定义响应结果处理...功能特点你爱的样子,我都有,小鸽鸽了解下我吧:支持n个target*n个p......
  • ‌SPI NOR Flash和‌SPI NAND Flash的区别
    ‌SPINORFlash和‌SPINANDFlash的定义和基本特性‌SPINORFlash是一种非易失性存储器,通过串行接口进行数据传输,具有读写速度快、可靠性高、体积小等优点。它采用类似‌SRAM的存储方式,每个存储单元存储一位数据(0或1),可以直接寻址,寻址速度非常快。SPINORFlash支持全双工......
  • 科研绘图系列:R语言象限热图(quadrant heatmap)
    文章目录介绍加载R包数据韦恩图vennRHHO2对象其他系统信息介绍在高通量数据分析中,比较两种实验条件下的差异表达(DE)模式是常见的。传统上,研究人员会设定一个截止值(例如p值=0.05或FDR5%)。然而,采用这些特定的截止值似乎是武断的。例如,可能在这些特定截止......
  • jar包、war包、tar包、tar.gz包有什么区别
    原文链接:jar包、war包、tar包、tar.gz包有什么区别–每天进步一点点(longkui.site)0.前言java项目经常打包,有的打出来的是war包,有的打出来的是jar包,到底有什么区别?1.jar包jar包,JavaArchive,翻译过来就是java档案。是Java编译好之后生成class文件,但是如果直接发布这些class......