C++实战笔记(五)

标准容器

C++容器分类:顺序容器,有序容器,无序容器。所有的容器都有一个基本的特性:容器保存元素采用的是值语义,也就是说,里面存储的是元素的副本、复件,而不是引用。

从这个基本的特性可以得出一个推论,容器操作元素的很大一块成本就是值的复制。所以,如果元素比较大, 或者非常多,那么操作时的复制成本就会很高,性能也就不会太好。

一种解决办法就是尽量为元素实现转移构造函数和转移赋值函数,在加入容器的时候使用std::move来转移元素,降低元素复制的成本。例如:

Point p;   //复制成本很高的对象
v.push_back(p);    //存储对象,复制构造,成本很高
v.push_back(std::move(p));  //定义转移构造函数后可对元素转移,降低成本

C++11之后新增加的emplace()函数操作,他可以就地构造元素,免去了构造再复制、转移的成本,不但高效,还方便。

v.emplace_back(..); //直接在容器中构造元素,不需要复制或者移动

顺序容器

顺序容器就是数据结构里的线性表,一共5种:array/vector/deque/list/forward_list

这5种容器又可以按照内部的存储结构细分为两组。

  • 连续存储的数组:array/vector/deque
  • 指针结构的链表:list/forward_list 

array/vector直接对应C的内置数组,内存布局与C完全兼容,所以是成本最低,速度最快的容器。

它们的区别在于容量能否动态增长:array是静态数组,大小在初始化的时候就固定了,不能再容纳更多的元素,而vector是动态数组,虽然初始化的时候设定了大小,但可以在后续运行时按需增长,容纳任意数量的元素。

array<int, 2> arr;  //初始化一个静态、数组,长度是2
assert(arr.size() == 2); //静态数组的长度总是2

vector<int> v(2);   //初始化一个动态数组,长度是2
for(int i = 0; i < 10; i++){
    v.emplace_back(i);   //追加多个元素
}

assert(v.size() == 12); //长度动态增长到12

deque也是一种可以动态增长的数组,它与vector的区别是它可以在两端高效地插入和删除元素,这也是它的名字"double-end queue"的来历,而vector则只能用push_back在末端追加元素。

deque<int> d;  //初始化一个动态数组,长度是0
d.emplace_back(9);  //在后端添加一个元素
d.emplace_front(1);  //在前端添加一个元素
assert(d.size() == 2); //长度动态增长到2

vector/deque里的元素因为是连续的,所以在中间插入和删除效率很低,而list/forward_list因为是链表结构,插入和删除只需要调整指针,所以在任意位置操作都很高效。

链表结构的缺点是查找效率低,只能沿着指针顺序访问,这方面不如vector随机访问的效率高,list是双向链表,forward_list是单向链表。

当vector的容量达到上限的时候,通常会再分配一块两倍大小的新内存,然后把旧元素复制或者移动过去,这个操作成本非常大,所以在使用vector的时候最好能够预估容量,使用reserve()提前分配足够的空间,降低动态扩容的复制成本。deque/list的扩容策略保守,只会按照固定的步长去增加容量,但短时间内插入大量数据的时候,这种做法就会频繁分配内存,效果反而不如vector一次分配。

首选容器array/vector,它们速度最快、成本最低,数组的形式也令它们最容易使用,搭配算法也可以实现快速排序和查找。deque/list/forward_list适合对插入和删除性能比较敏感的场合,如果还很在意空间成本,那就只能选择非链表deque.

有序容器

有序容器,它的元素在插入容器后就被按照某种规则重新调整次序,所以是有序的。

C++的有序容器使用的是树结构,通常是红黑树——有着最好查找性能的二叉树。

标准库一共有4种有序容器:set/multiset和map/multimap,set是集合,map是关联数组,在其他变成语言里也叫字典。有multi前缀的容器表示可以容纳重复的key,而内部结构与无前缀的相同。

在定义容器时候必须要指定key的比较函数,只不过这个函数通常默认为less(),表示小于关系,不用特意写出来。

解决自定义类型作为容器的key,一是重载比较操作符"<",另一个是自定义模板参数。

//重载比较操作符"<"
bool operaotr<(const Point& a, const Point& b)
{
    return a.x < b.x;  //自定义比较运算符
}

set<Point> s;    //现在可以正常放入容器了
s.emplace(7);
s.emplace(3);
//编写专门的函数对象或者lambda表达式,然后在容器的模板参数里将其指定为比较函数

set<int> s = {7, 3, 9};  //定义集合并初始化3个元素
for(auto &x: s){         //循环输出元素
    cout << x << ",";    //从小到大排序3,7,9
}

auto comp = [](auto&& a, auto&& b){ //定义一个lambda表达式,用来比较大小
    return a>b;   //定义大于关系
}

set<int, decltype(comp)> gs(comp); //使用decltype得到lambda表达式的类型
std::copy(begin(s), end(s), inserter(gs, gs.end())); //复制算法,复制到另一个容器(使用迭代器插入)

for(auto& x : gs){
    cout << x << ",";    //从大到小排序9,7,3
}

如果需要实时插入排序,则选择map/set,如果不需要实时插入排序,那么最好还是用vector,全部插完后再一次性排序。

无序容器 

 无序容器有4种:unordered_set/unordered_multiset/unordered_map/unordered_multimap

无序容器的使用和有序容器一样,区别仅限于内部的数据结构:它不是红黑树。而是散列表,也叫哈希表。因为它采用散列表,元素位置取决于计算的散列值,没有规律可言,所以是无序的,我们可以把它理解为乱序容器。

using map_type =                  //类型别名
    unordered_map<int, string>;    //使用无序关联数组
map_type dict;          //定义一个无序关联数组
dict[1] = "one";    //添加3个元素
dict.emplace(2, "two");
dict[10] = "ten";

for(auto &x: dict){         //遍历输出
    cout << x.first << "=>"   //顺序不确定
        << x.second << ",";    //既不是插入顺序,也不是大小顺序
}

选择:只想单纯的set/map。没有排序需求,就应该使用无序容器,没有升序比较的成本,它的速度会非常快。

无序容器要求key具备两个条件,一是可以计算散列表,二是可以执行相等比较操作。第一个是因为散列表的要求,只有计算散列值才能放入散列表;第二个是因为散列值可能会冲突,所以当散列值相同时,就要比较真正的key值。

与有序容器类似,要把自定义类型作为Key放入无序容器就必须要实现以下两个函数。

相等函数,使用重载实现

bool operator == (const Point& a, const Point& b)
{
    return a.x == b.x;
}

散列函数,可以使用函数对象或者lambda表达式实现,在内部最好调用标准的散列函数对象,而不要自己直接计算,否则容易造成散列值冲突。

auto hasher = [](const auto& p){
    return std::hash<int>()(p.x);  //调用标准散列函数对象计算
};

unordered_set<Point, decltype(hasher)> s(10, hasher);
s.emplace(7);
s.emplace(3);

特殊容器

可选值(optional)

C++提供的函数只能返回一个值,这在需要表示无效概念的时候就会比较麻烦,无效值通常是一个特殊0或者-1,例如分配内存返回nullptr,查找字符返回npos.

C++模板类optional,可以近似地看作只能容纳一个元素的特殊容器,通过判断容器是否为空来检查有效或者无效,因此可以调用成员函数has_value(),示例代码:

optional<int> op;  //持有Int的optional对象
assert(!op.has_value());  //默认是无效值

op = 10;   //赋值,持有有效值
if(!op.has_value()){
    cout << op.value() << endl;  //获取值的引用
}

optional<int> op2;  //初始化无效optional
cout << op2.value_or(99) << endl;  //无效,返回给定的替代值

另一方面,optional的行为又很像指针,可以用"*","->"来直接访问内部的值,也能够显式的转换为bool值,或用reset()清空内容,用起来像unique_ptr:

optional<string> op {"zelda"};  //持有string的optional对象
assert(op);  //可以像指针一样用于bool型逻辑判断
assert(!op->empty() && *op == "zelda");  //使用* ->  访问内部值
op.reset();  //清空内部值
assert(!op); //此时是无效值

同样optional也可以用于工厂函数make_optional()来创建,不过与直接构造不同,即使不提供参数,工厂函数必定会创建出一个持有有效值的optional对象,例如:

auto op1 = make_optional<int>();  //使用默认值构造有效值
auto op2 = make_optional<string>(); 

当函数需要返回可能无效的值,只需要把返回值用optional包装就可以。

auto safe_sqrt = [](double x){
    optional<double> v; //默认无效
    
    if(x < 0){
        return v;
    }

    v = ::sqrt(x); //值有效
    return v;
};

assert(!safe_sqrt(-1));  //负数无法求平方根
assert(safe_sqrt(9).value() == 3);  //正数平方根

optional最后注意:当它内部持有bool 类型的optional对象时,由于它本身可以被转换成bool类型,但这个bool类型表示的是optional的有效性,而非内部的bool真假,就必须判断两次

optional<bool> op {false}; //持有bool类型的optional对象

if(op){  //错误用法,实际判断的是有效性
    cout << "misuse" << endl;
}

if(op && op.value()){  //正确,有效后再检查值
    cout << "right" << endl;
}

可变值

C++17引入了一个新模板类。variant,它可以说是一个智能union,可以聚合任意类型,同时用起来又和union几乎一样了。它像只能容纳一个元素的"异质"容器,想知道当前哪种元素可以调用成员函数index():

variant<int, float, double> v; //可以容纳3种不同的整数
v = 42;
assert(v.index() == 0);  //索引是0
v = 3.14f;  //直接赋值float
assert(v.index() == 1);  //索引是1
v = 2.718;
assert(v.index() == 2);  //索引是2

variant不能用成员变量的形式来访问内部的值,必须用外部的模板函数get()来获取值,模板参数可以是类型名或者是索引。

如果用get()访问了不存在的值就会出错,以抛出异常的方式告知用户。

v = 42;
assert(get<0>(v) == 42); //取索引值为0的值,即int

v = 2.718;
auto x = get<double>(v);  //取double得值,即索引为2

get<int>(v);  //当前是double,所以出错,抛出异常

抛出异常不太友好,使用另外一个函数get_if(),它以指针的方式返回variant内部的值,如果内部值不存在,返回空指针。

auto p = get_if<int>(&v);   //取int得值,不存在就返回空指针
assert(p == nullptr);

函数visit()提供了get()/get_if()之外的另一种更灵活的使用方式,不需要考虑类型的索引,而是一个集中业务逻辑的访问器函数来专门处理variant对象。

这个访问器最好是泛型的lambda表达式,写起来更方便;

variant<int, string> v; //可以容纳int和String

auto vistor = [](auto &x){
    x = x + x;
    cout << x << endl;
}

v = 10;
std::visit(vistor, v); //输出20

v = "ok";
std::visit(vistor, v); //输出"okok"

实现访问器函数的时候,它必须能处理variant的任何可能的类型,否则无法编译通过。如果我们将lambda里的赋值语句改写为 "x = x * x",那么它肯定是无法应用于string.

variant异质容器的特性非常有价值,它可以完全在不使用继承、虚函数的情况下实现面向对象里的多态特性,因为没有虚表指针,它的运行效率会更高。

struct Swan final{   //天鹅类,可以飞 。使用struct方便不写public
    void fly(){
        cout << "swan files" << endl;
    }
};

struct Ostrich final{ //鸵鸟类,不可以飞
    void fly(){
        cout << "Ostrich cant fly" << endl;
    }
};

struct Phoenix final{ //凤凰类
    void fly(){
        cout << "Phoenix fly high" << endl;
    }
};

//定义能容纳它们的variant类型

variant<Swan, Ostrich, Phoenix> bird;
auto fly_it = [](auto & x){  //泛型lambda表达式
    x.fly();
}

bird = Swan(); //天鹅对象
std::visit(fly_it, bird);

bird = Ostrich(); //鸵鸟对象
std::visit(fly_it, bird);

任意值

any,能够在运行时任意改变类型,是无界的。

any a;
a = 10;
a = 0.618;
a = "hello any"s;
a = vector<int>(10);

any基本用法与optional有点像,可以用has_value()检查是否有值,可以用reset清空内容,也可以调用工厂函数make_any()存入值。

auto a = make_any<long>(99); //调用工厂函数存入整数
assert(a.has_value()); //检查是否有值

a.reset();  //清空存储值
assert(!a.has_value());  //检查是否有值

any的目标不是标记值得有效性,它不提供类似指针的操作,而且接口比variant的还少,没有get()/get_if(),只能用模板函数any_cast()指定类型来进行转换得到内部值,用法上和传统的转型操作符static_cast/dynamic_cast很像。不过在进行类型转换的时候,我们应当尽量用引用或指针的形式,否则它会创建出一个临时对象,带来额外的成本。

a = 100L; //存储整数
assert(any_cast<long>(a) == 100); //转型成长类型,有临时成本

auto &v = any_cast<long&>(a); //转型成引用,推荐使用
v = 200L; //引用可以直接赋值
assert(any_cast<long>(a) == 200);  //转型长整型

a = "any"s; //存入字符串
auto p = any_cast<string>(&a); //转型成指针,推荐使用
assert(p && p->size() == 3);

如果类型不对,转型失败,any_cast()就会抛出异常,如果是指针形式,则会返回空指针,这和variant的get_if()行为有些类似。

any_cast<int>(a); //类型不对,抛出异常
assert(any_cast<int>(&a) == nullptr); //指针转型失败,返回空指针

想要检查any是否持有特定的对象,可以使用成员函数type(),该函数返回std::type_info对象,与其他类型用typeid计算后比较是否相等

a = 10;
assert(a.type() == typeid(int));  //比较整数的typeid

a = "string"s;
assert(a.type() == typeid(string));

//any运行时动态检查

if(a.type() == typeid(long)){
    //any为long
}else if(a.type() == typeid(string)){
    //any为string
}

另外一种方式是使用带初始化的if,调用any_cast()转换出指针变量,通过空指针来判断类型是否正确:

if(auto p = any_cast<long>(&a); p){  //初始化,转换成指针再判断
    cout << *p << endl;  //以指针方式使用any变量
}

any没有variant的编译期检查功能。

多元组

 C++里的pair类型,它可以持有两个不同类型的值,也就是二元组。而新的数据结构tuple是对pair的进一步增强,能够持有任意多个不同类型的值,他可以被称为“多元组”,或者简称元祖。

tuple的声明和pair差不多,都需要在模板参数列表里声明元素类型,或者使用工厂函数make_tuple().

tuple要用和variant类似的get()函数,可以通过索引或者类型来定位。例如:

tuple<int, double, string> t1{0, 1, "x"};  //三元组
assert(get<0>(t1) == 0);  //指定按索引取成员
assert(get<1>(t1) == 1);  //指定按索引取成员
assert(get<string>(t1) == "x");  //指定按类型取成员

auto t2 = make_tuple(1L, "string"s);
assert(get<long>(t2) == 1); //指定类型取成员

get<1>(t2) = "hi";    //get()获取的是引用类型
assert(get<1>(t2) == 1); //指定索引取成员

代替struct:

using Student = tuple<int, string, double>;  //using定义类型别名

struct Student{  //等价的struct定义
    int id;
    string name;
    double score;
}

tuple另一个常见的用法就是作为函数的返回值,让函数轻松的返回任意多个值:

auto f = [](){            //tuple用法的Lambda
    return make_tuple(true, "ok"s);  //使用tuple返回多个值
}

为了获取函数返回的tuple内部成员变量,我们可以使用配套的函数tie,还有C++17引入的结构化绑定。

//C++17
auto [flag, msg] = f();  //结构化绑定自动类型推导
assert(flag && msg == "ok");  //得到tuple内部的变量

//C++11
std::tuple<int,std::string> mytuple (10,"message");
int code;
std::string mean;
std::tie(code, mean) = mytuple;
std::cout << "code is " << code << "; mean is " << mean << std::endl;

总结:

  1. optional主要用来表示值有效或者无效,用法很像智能指针
  2. variant是一种异质容器,可以在运行时改变类型,实现泛型多态
  3. any与variant类似,但可以容纳任意类型,在运行时检查类型会更灵活
  4. tuple可以打包多种不同类型的数据,也可以算是一种异质容器
  5. optional/variant/any在任何时刻只能持有一个元素,而tuple则可以持有多个元素
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值