mxxhcm's blog

  • 首页

  • 标签

  • 分类

  • 归档

C++ STL sequential container

发表于 2019-11-10 | 更新于 2020-01-09 | 分类于 C/C++

C++标准库中的顺序容器

vector

  • 内部数据结构为数组,可以自动增长
  • 在后端插入和删除,push_back()和pop_back(),时间复杂度为$O(1)$
  • 在中间和前段插入和删除,insert()和erase(),时间和空间复杂度是$O(n)$
  • 分配连续内存,
  • 支持随机数组存取,查找的时间复杂度$O(1)$
  • 支持[]访问
  • 头文件vector

list

  • 内部数据结构为双向环状链表
  • 任意位置插入和删除的时间复杂度是$O(1)$
  • 链式存储,非连续内存
  • 不支持随机存取,查找的时间复杂度是$O(n)$
  • 不支持[]访问
  • 头文件list

forward_list

deque

  • vector和deque的结合,使用若干个内存片段进行链接。兼有vector和list的好处。
  • 内部数据结构为数组
  • 头文件deque

array

大小固定的容器,还需要指定元素类型。

顺序容器的操作

添加元素(增)

容器添加元素使用的是拷贝一份元素的值到容器中(非引用传参)。

  • c.push_back(t)在容器尾部插入。除了array和forward_list,每个顺序容器和string都支持。
  • c.push_front(t)在容器头部插入。list, forwrad_list和deque支持。
  • c.insert(p, t)在任意位置插入。vector, list, deque, string都支持insert,forward_list有特殊的insert。将元素插入到vector,deque,string的任何位置都是合法的,但是非常耗时。
    insert有多个版本,还可以直接插入一个范围。
    如果通过一个迭代器指定插入位置,插入的元素会放在这个迭代器之前,insert的返回值是第一个新加入元素的迭代器,如果没有插入任何元素,返回第一个参数。
  • c.emplace(p, args)是直接构造而不是拷贝元素。emplace,emplace_front,emplace_back分别对应insert, push_front和push_back。

删除元素(删)

  • c.pop_back(),forward_list不支持。
  • c.pop_front(),vector和string不支持。
  • c.erase(p),删除迭代器p指定的内容。
  • c.erase(b, e),删除迭代器b和e指定的范围。
  • c.clear(),删除容器中的所有元素。

访问元素(改和查)

下面的四个操作返回的都是引用。

  • c.front(),返回begin对应的元素。
  • c.back(),返回end之前的元素,forward_list没有。
  • c[n],如果n>c.size(),无定义,只适用于string, vector, deque, array。
  • c.at(n),如果下标越界,抛出out of range异常,只适用于string, vector, deque, array。

forward_list的操作

forward_list提供了insert_after, emplace_after和erase_after。

改变容器大小

将容器大小调整为n,n小于c.size(),将超过的舍去;n大于c.size(),使用值初始化或者指定一个元素t。

  • c.resize(n),
  • c.resize(n, t)

迭代器失效

  1. 容器添加元素之后
  2. 从一个容器中删除元素之后

vector的增长

不同的实现中,vector的增长速度也不同,有的是2,有的是1.多。可以使用c.capaticy查看vector的容量。capacity和size的区别在于,size指的是它已经保存的元素的数目,而capacity是在不分配新的内存空间的前提下最多可以保存多少元素。

容器适配器

适配器接收一种已有的容器类型,让它的行为看起来像另一种不同的类型。标准库中定义了下面三个适配器:

  • stack
  • queue
  • priority_queue

每个适配器都有两个构造函数:

  • 默认构造函数创建一个空对象
  • 一个构造函数接收容器参数,拷贝该容器初始化适配器。

参考文献

1.《C++ Primer》第五版

C++ STL container

发表于 2019-11-10 | 更新于 2019-12-24 | 分类于 C/C++

什么是容器

一个容器就是一些特定类型对象的集合。C++标准库中提供了两类容器,一类是顺序容器,一类是关联容器。
关联容器和顺序容器有着根本上的不同:
关联容器的元素是按照关键字保存和访问的;而顺序容器是按照他们在容器中的位置顺序保存和访问的,这种顺序不依赖于元素的值,而跟元素加入容器时的位置相对应。关联容器中的许多行为和顺序容器相同,但是他们的不同之处反映了关键字的作用。顺序容器和关联容器共享公共的接口,不同容器可以按照不同的方式对其进行扩展,这个公共接口使得容器学习起来更容器,基于某个容器学习的内容可以扩展到其他容器上。
关联容器支持高效的关键字查找和访问,两个主要的关联容器是map和set,map中的元素是一些关键字-值(key-value)对,关键字索引,而值表示和索引相关的数据。而set中的每个元素只有一个关键字。

顺序容器概述

关于顺序容器更详细的内容可以查看C++ sequential container。

顺序容器种类

所有C++标准库中的顺序容器包括:

  • vector: 可变大小数组,
  • list:双向链表,
  • forward_list:单向链表,
  • deque:双端队列
  • array:固定大小数组
  • string:与vector类似,但是专门用于字符操作,

定义顺序容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <array>
#include <vector>
#include <list>
#include <forward_list>
#include <deque>

using std::array;
using std::vector;
using std::list;
using std::forward_list;
using std::deque

#include <string>
using std::string;

array<string, 1000> c1;
vecotr<string> c2;
list<string> c3;
forward_list<string> c4;
deque<string> c5;

存取时间复杂度和存储策略

所有的顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在随机访问或者增删元素上的能力上做了不同的性能折中:

  • string和vector:支持$O(1)$时间的随机访问;尾部增删是$O(1)$,在尾部之外的位置插入或者删除可能很慢$O(n)$。string和vector`都存储在连续的的内存空间中,在中间增删需要移动增删位置之后所有的元素;在任何位置添加一个元素时,如果当前的存储空间不够,还需要分配新的存储空间,需要将所有的元素都移动到新的存储空间去。
  • list和forward_list:list是双向链表,forward_list是单向链表,它们都不支持随机访问,在寻找某一个元素时,只能以$O(n)$的时间复杂度遍历整个链表,list支持双向顺序访问$O(n)$,forward_list只支持单向顺序访问$O(n)$;它们在任何位置插入或者删除的时间复杂度都是$O(1)$。
    和vector,deque,array相比,链表需要存放指针记录前(后)节点的信息。此外forward_list没有size()和back()成员,因为forward_list的设计目标是和手写的单向链表性能相似,size操作会增大计算开销,对于其他容器而言,size是一个$O(1)$的操作。
  • deque:deque支持$O(1)$时间的随机访问;在中间位置增删都是$O(n)$的开销,但是在deque两端增删是$O(1)$的事件开销。
  • array:$O(1)$时间复杂度的随机访问;不支持增删操作。array和内置数组一样,大小固定,不支持增删,但是更安全。

顺序容器的选择

  1. 通常使用vector是最好的选择,除非有更好的理由。
  2. 程序有很多小的元素,而且空间额外开销很重要,不要用list或者forward_list。
  3. 要求支持随机访问元素,使用vector或者deque。
  4. 在容器中间插入或者删除,使用list或者forward_list。
  5. 程序需要在容器头尾增删,而不会在中间增删,使用deque。
  6. 如果即需要随机存取,又需要在容器中间增删,这个时候根据存取和增删的操作数量进行选择,哪种操作占据主导地位,就使用相对应的容器。
  7. 如果不确定到底应该使用vector还是list,那么就只使用它们都支持的操作,不使用下标运算,使用迭代器,避免随机访问。

关联容器概述

关于关联容器更详细的内容可以查看C++ associative container。
标准库共有8个关联容器,他们在三个维度上有差异

  1. set还是map,map存放key-value,set只存放key,或者说key=value。
  2. 关键字是否可以重复,是否容器名字中包含multi
  3. 元素顺序无序还是有序,容器名字是否包含unordered

关联容器种类

具体如下:

  • map,关联数组,保存key-value
  • set,只保存key,或者说key=value
  • unordered_map,无序map,底层用hash实现
  • unordered_set,无序set,底层用hash实现
  • multimap,key可以重复出现的map
  • multiset,key字可以重复出现的set
  • unordered_multimap,key可以重复出现的无序map,底层用hash实现
  • unordered_multiset,key可以重复出现的无序set,底层用hash实现

关联容器不支持顺序容器和位置相关的操作,如push_back,push_front等,因为关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。此外,关联容器也不支持接收一个元素值和一个数量值的插入操作和构造函数。
不过关联容器支持一些顺序容器不支持的操作。关联容器的迭代器都是双向的。

定义关联容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>
#include <string>

using std::set;
using std::map;
using std::multiset;
using std::multimap;

using std::unordered_set;
using std::unordered_map;
using std::unordered_multiset;
using std::unordered_multimap;

set<string> c1;
map<long, string> c2;
multiset<string> c3;
multimap<long, string> c4;

unordered_set<string> c5;
unordered_map<long, string> c6;
unordered_multiset<string> c7;
unordered_multimap<long, string> c8;

顺序容器和关联容器的公共操作

  1. 每一个容器都定义在一个头文件中,文件名和类型相同(除了multiset和multimap以及unordered_multiset, unordered_multimap。

  2. 容器类型成员。每个容器都定义了多个类型:

    • iterator,容器的迭代器类型
    • const_iterator,无法修改元素的迭代器类型
    • reverse_iterator,反向迭代器
    • const_reverse_iterator,不能修改元素的反向迭代器
    • size_type,无符号整数,足够保存容器的最大大小。
    • difference_type,有符号整数,足够保存两个迭代器之间的距离
    • value_type,元素类型
    • reference,元素的左值类型
    • const_reference,元素的const左值类型。
  3. 迭代器和迭代器范围。所有容器都支持迭代器,通过解引用迭代器访问容器中的元素。一个迭代器范围由一对迭代器构成,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置,通常一个被称为begin,一个被称为end。如何获取迭代器:

    • c.begin()和c.end(),返回指向c的首元素和尾后元素的迭代器
    • c.rbegin()和c.rend(),返回指向c的尾元素和首元素之前的反向迭代器
    • c.cbegin()和c.cend(),返回指向c的首元素和尾后元素的const_iterator
    • c.crbegin()和c.crend(),返回指向c的尾元素和首元素之前的const_reverse_iterator

    当auto和begin,end结合使用时,获得的迭代器类型依赖于容器的类型。只有当容器本身是const时,才能够得到const_iterator。
    而auto和cbegin和cend使用时,获得的迭代器类型和容器类型无关,一直都是const_iterator。
    关于迭代器的具体内容可以查看。

  4. 容器定义和初始化。

    • C c;,默认构造
    • C c1(c2);或者C c1=c2;,拷贝构造,直接拷贝容器
    • C c(b, e);,拷贝构造,通过迭代器范围进行拷贝,将迭代器b和e指定范围的元素拷贝到c,不适用于array。这种方式不要求容器类型相同,只要能将要拷贝的对象转化为要初始化的容器的元素类型即可。
    • C c{a, b, c...};或者C c={a, b, c...},列表初始化c,元素类型必须相同,同时显式的指定了容器的大小

    只有顺序容器(除了array)的构造函数才能接收大小参数。

    • C seq(n);,进行值初始化,不适用于string
    • C seq(n, t);,seq是包含n个初始值为t的元素

    总结一下:

    1. 将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型必须相同
    2. 使用迭代器拷贝构造容器时,不需要容器类型和元素类型相同,只需要待拷贝对象能够转换成要初始化的元素对象即可。
    3. 对于顺序容器(除了array)来说,它还有另一个构造函数,它的参数是容器大小和一个可选的元素初始值。如果不提供元素初始值,标准库会创建一个值初始化器,内置类型,如int,取0,string等类类型,由类进行默认初始化。即当如果元素是内置类型,或者具有默认构造函数的类类型,那么可以只提供一个容器大小参数。如果没有默认构造函数,就必须指定显式的元素初始值。
    4. 标准库array具有固定大小,定义array时,除了指定元素类型,还要指定元素个数。一个默认构造的array是非空的,这些元素都被默认初始化。如果进行列表初始化,初始值如果小于array大小,剩余的元素执行值初始化。对于类类型来说,不论是默认初始化还是值初始化,都需要类有一个默认构造函数。
  5. 赋值, assign和swap。

    • c1 = c2,将c1的元素用c2的元素替换,c1和c2类型必须相同
    • c1 = {a, b, c, ...},将c1中的元素替换为列表中元素,不适用于array
    • a.swap(b),交换a和b的元素
    • swap(a,b),和a.swap(b)相同。

    还有不适用于关联容器和array的assign操作,

    • seq.assign(b, e);,将seq中元素替换为迭代器b和e中的元素,迭代器b和e不能指向seq中的元素
    • seq.addign(il);,将seq中的元素替换成初始化列表il中的元素
    • seq.assign(n, t);,将deq中的元素替换成n个值为t的元素
    1. 赋值号左右两边的运算对象必须具有相同的类型,而assign不需要两个容器的类型相同,只需要元素类型相容即可。array允许直接赋值array,但是不支持assign操作,也不允许用花括号包围的值列表进行赋值,因为右面运算对象的大小可能和左面运算对象的大小不同(见C++ primer第五版302页),而array的大小是不可变的。
    2. 赋值操作会让指向左边容器内部的迭代器,引用和指针失效。而swap交换容器内容不会使得指向容器的迭代器,指针和引用失效(容器类型为array和string除外)。
    3. swap交换array时,两个array的大小必须相同,类型相同。swap交换两个array会真正交换两个array的元素。
    4. swap交换除了string之外的容器时,指向容器的迭代器,引用和指针都不会失效,即访问的还是未交换之前的对象,但是这些对象所属的容器变了。
  6. 大小。

    • size(),容器当前容纳的元素个数,不支持forward_list
    • max_size(),容器所能容纳的最大元素个数
    • empty(),容器是否为空
  7. 关系运算符。

    1. ==和!=,所有的容器都支持的运算符。
    2. <=,<,>=, >,关系运算符(无序关联容器不支持),关系运算符两侧的容器类型必须一样,容器类的元素类型也必须一样。
    3. 容器的相等运算实际上是使用的元素的==运算实现的,而容器的关系运算实际上是使用元素的<运算实现的。对于类类型来说,必须对相应的操作符重载,才能进行相应的关系运算,否则就无法进行。
    4. 两个容器比较大小的规则:
      两个容器大小相等,对应元素相等,这两个容器相等。
      两个容器大小不同,但是较小元素中每个元素都等于较大容器中的对应元素,较小容器小于较大容器。
      如果两个容器都不是另一个容器的前缀子序列,则他们的结果取决于第一个不相等的元素的比较结果。
  8. **增删元素(不适用于array)。**注意,在不同的容器中,操作的接口都不同

    • c.insert(args),将args中的元素拷贝进c
    • c.emplace(inits),使用inits构造c中的一个元素
    • c.erase(args),删除args指定的元素
    • c.clear(),删除c中所有元素,返回void
  9. 顺序容器几乎可以保存任意类型的元素,但是某些容器对于元素类型有特殊的要求,我们可以为不支持特定操作的类型定义容器,但是使用只用那些没有特殊要求的容器操作了。
    顺序容器构造函数的一个版本接受容器大小参数,它使用元素类型的默认构造函数,但是有些类没有默认构造函数,这时候我们可以定义这种类型的容器,但是需要传入一个元素的初始化器。例如:

    1
    2
    vector<noDefault> v1(10, init); //正确,
    vector<noDefault> v2(10); //错误,因为没有默认构造函数

参考文献

1.《C++ Primer》第五版

C++ customed type class

发表于 2019-11-10 | 更新于 2020-02-04 | 分类于 C/C++

概念

  1. 数据抽象和封装。类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作,类的实现则包括类的数据成员,负责接口实现的函数以及定义类所需要的各种私有函数。封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。
  2. **成员函数是定义为类的一部分的函数,有时候也被称为方法。使用.运算符后跟要使用的成员函数,同时使用调用运算符()来访问一个函数。**成员函数的声明必须在类的内部,成员函数的定义既可以在类的内部也可以在类的外部。定义在类内部的的函数是隐式的内联函数。而作为接口部分的非成员函数,如add,print,read等都必须在类的外部。
  3. 友元的声明只是指定了访问权限(可以访问类的私有成员,和第四条不冲突),而并非一个普通的函数声明,如果希望类的用户能够调用某个友元函数,必须在友元声明之外再次对函数进行一次声明.
  4. 封装的好处
    • 确保用户代码不会无意间破坏对象的状态,防止因为引入的原因造成数据被破坏,如果有程序缺陷破坏了对象的数据成员的状态,那么只有实现部分的代码可能产生这样的错误.降低了代码维护和错误修正的难度
    • 被封装的类的具体实现细节可以随时改变,无序调整用于级别的代码.类的作者可以比较自由的修改数据.当实现部分改变时,只要类的接口不变,用户代码就不需要改变.如果数据是public的,所有使用了原来数据成员的代码都可能失效,需要先定位并重写这部分代码.注意当类的实现发生改变时无序更改用户代码,但是使用了该类的源文件必须重新编译.
  5. 构造函数。类通过一个或几个特殊的成员函数控制其对象的初始化过程,这些函数叫做构造函数。
  6. 构造函数不能声明为const类型。在创建一个const对象时,直到构造函数完成初始化,对象才算真正取得了const属性。

类

类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作,类的实现则包括类的数据成员,负责接口实现的函数以及定义类所需要的各种私有函数。
封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。

类想要实现数据抽象和封装,需要首先定义一个抽象数据类型,在抽象数据类型中,类的设计者负责考虑类的实现过程;使用该类的程序员只需要抽象的思考类型做了什么,不需要了解细节.

**类是基于对象的,类之间的关系(继承,组合,委托)是面向对象的。**C++ 是由C语言和C 标准库组成。

类的定义

每个类都定义了一个唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型。对一个类来说,它的成员和其他任何类的对象都不是一回事。
仅仅声明类而暂时不定义它,这种声明有时候也叫前向声明。在类声明之后定义之前,它属于不完全类型。我们知道它是一个类,但是不清楚它到底包含哪些类型。

类的组成包括成员函数,就是定义在类内部的函数;数据成员变量,定义在类内的数据变量;类的类型成员,就是typedef重命名的类型;访问控制等。

访问控制和封装

访问说明符

访问说明符用于加强类的封装性,让用户不能直接访问对象的内部。

  • public, 定义在public说明符后的成员在整个程序内可以被访问,public定义类的接口,向类的用户提供访问数据成员的功能。
  • private,定义在private说明符后的成员可以被类的成员函数访问,但是不能被该类的独享访问,它封装了类的实现细节。

C++中的struct和class很像,只不过struct默认访问权限是public,而class默认访问权限是private的。

封装

封装的好处:

  1. 确保用户代码不会无意间破坏对象的状态,防止因为引入的原因造成数据被破坏,如果有程序缺陷破坏了对象的数据成员的状态,那么只有实现部分的代码可能产生这样的错误.降低了代码维护和错误修正的难度
  2. 被封装的类的具体实现细节可以随时改变,无序调整用于级别的代码。类的作者可以比较自由的修改数据。当实现部分改变时,只要类的接口不变,用户代码就不需要改变。如果数据是public的,所有使用了原来数据成员的代码都可能失效,需要先定位并重写这部分代码。注意当类的实现发生改变时无序更改用户代码,但是使用了该类的源文件必须重新编译。

类的作用域

类本身是一个作用域,类的成员函数的定义在类的作用域之内。编译器在处理类的时候,先编译成员的声明,然后编译成员函数体。
在类的外部定义成员函数时,成员函数的定义必须和它的声明匹配。

类的定义

成员函数

成员函数是定义为类的一部分的函数,有时候也被称为方法。使用.运算符后跟要使用的成员函数,同时使用调用运算符()来访问一个函数。
成员函数的声明必须在类的内部,成员函数的定义既可以在类的内部也可以在类的外部,定义在类内部的的函数是隐式的内联函数。而作为接口部分的非成员函数,如add,print,read等都必须在类的外部。

构造函数

构造函数完成类对象的初始化过程。详细介绍可以查看。

类数据成员的初值

使用=或者列表初始化的方式为类的数据成员变量。

this指针

当一个对象调用类的成员函数时,到底发生了什么?比如:

1
2
Sales_data book;
book.isbn();

上面第二行代码其实相当于:

1
Sales_data::isbn(&book);

成员函数通过一个名字为this的额外的隐式参数来访问调用它的那个对象,编译器负责把book的地址传递给isbn的隐式形参this。在函数内部可以直接使用调用该函数的对象的成员,不需要通过成员访问运算符来实现,因为this所指的就是这个对象,***任何对类成员的直接访问都被当成this的隐式使用。**当isbn使用bookNo时,隐式的使用this指向的成员,就像我们写了this->bookNo一样.
虽然this形参是隐式定义的,但是定义任何名字为this的变量或者函数都是非法的.我们可以在成员函数体内部调用this,this的目的是总是指向当前这个对象,所以this是一个常量指针(顶层const),不允许更改它的指向。

const成员函数

默认情况下,this的类型是指向类类型的非常量版本的常量指针(顶层const).比如在Sales_data的成员函数中,this的类型是Sales_data *const,尽管this是隐式的,它仍然需要遵循初始化规则,即不能把它绑定到常量对象上,也就使得常量对象无法调用普通的成员函数(因为不能把常量对象绑定到普通指针上)。
如果isbn是一个普通函数而且this是一个普通的指针参数,应该把this生命成const Sales_data *const。顶层const是它自己带的,底层const是为了能够使得常量对象也能够调用普通的成员函数。但是因为this是隐式参数,C++ 选择将const关键字放在函数的参数列表之后,这个const表示this是一个指向常量的指针,这样的函数称为常量成员函数。
常量对象,常量的引用和指针只能调用常量成员函数.并且只能读取它的对象的数据成员,无法修改

返回this对象

当我们定义的函数类似于某个内置运算符时,应该尽量让函数的行为和内置运算符类似.比如内置运算符把它的左侧运算对象当做左值返回.如果我们在写一个复合赋值运算的时候,就需要返回一个引用类型,具体的返回值应该是this指针指向的整个对象,即*this。如下所示:

1
2
3
4
5
6
7
8
Sales_data &combine(const Sales_data &rhs)
{

units_sold += rhs.units_sold;
revenut += ths.revenue;

return *this;
}

函数返回指向当前类对象的this指针时,返回值是否为引用类型,结果完全不一样。不加引用的话,是this指针的一个副本,所有后续操作都是在这个副本上进行的,而不是在this指针指向的对象上。

从const成员函数返回*this。一个const成员函数如果以引用的形式返回*this,那么它的返回类型是常量引用。

内联成员函数

定义在类内部的函数隐式的被定义为内联的(即使不加inline关键字),定义在类内的构造函数也是内联函数。可以在类内把inline作为声明的一部分显式的声明成员函数,也能在类的外部用inline关键字修饰函数的定义。
内联成员函数最好和相应的类定义在同一个头文件中。
通常将内联函数和constexpr函数定义在头文件中。
内联函数允许多次定义,但是每次的定义必须一致。为什么?什么是内联函数,在每个调用点上将函数内联的展开。如果有两个文件foo.cpp和bar.cpp都需要调用一个相同的内联函数myinline,在编译这两个头文件的时候,需要将函数myinline进行展开,所以它们都需要定义myinline,但是在生成目标文件的时候,因为函数是强类型,就会出错,所以内联函数允许多次一致的定义。而最简单的方法就是将内联函数定义在头文件中,然后使用它的源文件包含它即可。

重载(const)成员函数

成员函数的重载和普通函数的重载很像,只不过成员函数是在类内。
同样的,对于const成员函数的重载,也和const函数的重载很像,底层const可以重载,顶层const不能重载。

可变数据成员

使用mutable关键字声明一个可变数据成员,即使它是常量对象的成员。因此,一个const成员函数(this指针是指向常量的指针)可以改变一个可变成员的值。可以使用mutable声明一个变量做特别的用途,比如统计类的某个(常量)成员函数被调用了多少次。

友元

  1. 友元提供了其他类或者函数(非类的成员函数)访问类的私有对象的功能。
  2. 友元的声明只需要在其他函数或者类前加上friend关键字即可。
    • 类之间的友元。一个类可以把其他类定义成友元,友元类的成员函数可以访问这个类包括非公有成员在内的所有成员。
    • 成员函数作为友元。可以把其他类中的某个函数设置成友元。但是这几个类之间的声明和定义需要满足一定的依赖关系。
    • 函数重载和友元。如果一个类想要一组重载函数声明为它的友元,需要对重载的每一个函数都声明为友元。
  3. 友元声明只能出现在类定义的内部,并且不会受区域访问控制级别(public,private, protected)的约束。
  4. 友元的声明只是指定了访问权限(可以访问类的私有成员,和第三条不冲突),而并非一个普通的函数声明,如果希望类的用户能够调用某个友元函数,必须在友元声明之外再次对函数进行一次声明。
    即使在类的内部定义函数,也必须在类的外部提供相应的声明从而使得函数变得可见。即使我们仅仅使用声明友元类的成员调用该友元函数,它也必须是被声明过的。友元声明的作用仅仅是影响访问权限,而非普通意义的声明。
  5. tips,一般来说,最好在类定义开始或者结束前的位置集中声明友元。
  6. 友元不具有传递性。
  7. 友元声明和作用域。类和非成员函数的声明不是必须在它们的友元声明之前。而友元类的成员函数的声明必须在它们的友元声明之前。

类的作用域

每个类都有自己的作用域,在类的作用域之外,普通的数据和函数成员只能通过对象,引用或者指针使用成员访问运算符进行访问,对于类类型成员则使用作用域运算符访问。
一个类就是一个作用域,在类的外部,成员的名字被隐藏起来了,在类的外部定义成员函数时必须同时提供类名和函数名。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,剩余部分包括参数列表和函数体,接下来我们就可以直接使用类的其他成员而无需再次授权。如果函数的返回类型不是在当前类的作用域内定义的,还需要指定返回类型是哪个类的成员。

名字查找的过程:

  1. 在名字所在的块中寻找其声明语句,只考虑在名字的使用前出现的声明。
  2. 如果没找到,继续查找外层作用域。
  3. 如果没找到匹配的声明,则程序报错。

类的定义

类的定义分为两步处理,编译器处理完类中的全部声明后才会处理成员函数的定义:

  1. 首先,编译成员(包括函数和数据成员)的声明。
  2. 直到类的声明全部完成后才编译函数体。

成员函数声明的名字查找

在处理成员函数的声明时,函数声明中的返回值类型和参数列表中出现的名字,都必须在使用前确保可见。按照名字查找的过程进行查找这些名字。

类型名字

类型名字不能被重新定义。

成员函数定义的名字查找

  1. 首先在成员函数,该名字出现之前,查找该名字的使用。
  2. 如果在成员函数内部没有找到,在类的所有成员内查找。
  3. 如果类内没有找到,在成员函数定义之前的作用域内继续查找。

类的静态成员

声明

  1. 静态成员属于类本身,而不属于某个对象,可以通过在成员的声明前面加上static关键字表示这是一个静态成员。
  2. 类的静态成员存储在任何对象之外,对象中不包含任何与静态数据成员有关的数据。
  3. 类的静态成员函数也不和任意对象绑定在一起,因此它们也不包含this指针。因此,静态成员函数不能声明为const的,也不能在static对象内使用this指针。包含this指针的显示调用和其他非静态成员的调用。

静态成员的定义

静态成员函数既可以定义在类的内部,也可以定义在类的外部。定义在外部的时候,不需要重复static关键字。
而静态数据成员不属于类的任何一个对象,所以静态成员不能在构造函数中初始化。
一般来说,不在类内初始化静态数据成员,而是在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次(最好把静态成员的定义和其他函数的定义放在同一个文件中)。下面会说到不一般的情况。

静态成员的类内初始化

不一般的情况是,可以为静态成员提供const类型的的类内初始值,在这种情况下,要求静态成员必须是字面值常量类型的常量表达式(constexpr)。

使用

  1. 静态成员不属于任何类的对象,但是可以通过类的对象,指针和引用访问静态成员(成员变量和成员函数)。成员函数也可以直接使用静态成员,不需要加上作用域运算符。

其他

头文件一旦改变,相关的源文件必须重新编译获取更新过的声明。
预处理器变量无视C++中关于作用域的规则。加上头文件保护符,防止重复包含。头文件保护符必须唯一。

1
2
3
#ifndef SALES_DATA_H
#define SALES_DATA_H
#endif

参考文献

1.《C++ Primer第五版》

C++ function

发表于 2019-11-10 | 更新于 2019-12-17 | 分类于 C/C++

知识点

  1. 函数的调用主要完成两项工作,一个是用实参初始化函数的形参,一个是将控制权从主调函数(calling function)转交给被调用函数。

什么是函数

函数是一个命名了的代码块,可以通过调用函数执行相应的代码。函数可以有0个或者多个参数,通常会产生一个结果。C++可以重载函数,一个名字可以对应几个不同的函数。

调用运算符

通过调用运算符执行函数,调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针,圆括号内是一个用逗号分隔开来的实参列表,我们用实参初始化函数的形参。调用功能表达式的类型就是函数的返回值类型。

函数调用和return

函数的调用主要完成两项工作,一个是用实参初始化函数的形参,一个是将控制权从主调函数(calling function)转交给被调用函数。执行函数的第一步是隐式的而定义并初始化它的形参。遇到一条return语句时函数结束执行过程。函数调用一样,return语句也完成两项工作,一个是返回return语句中的值,一个是将控制权从被调用函数转交给主调函数。函数的返回值用于初始化调用表达式的结果,完成调用所在表达式的声誉部分。

形参和实参

实参是形参的初始值。实参的类型必须和对应的形参相匹配,或者可以经过隐式的类型转换转换成形参的类型。

函数的形参列表

隐式的定义空形参列表

1
void f1()

显式的定义空形参列表

1
void f2(void)

函数返回值类型

  1. 函数的返回值不能是数组类型或者函数类型,但是可以是指向数组或者函数的指针。
  2. 函数的返回值可以是一种特殊类型void。

局部对象

作用域和生命周期

在C++中,名字有作用域,对象具有生命周期:

  • 名字的作用域是程序文本的一部分。
  • 对象的生命周期是程序执行过程中该对象存在的一段时间。

局部变量

函数体是一个语句块,块构成一个新的作用域,可以在其中定义变量。形参和函数体内的变量统称为局部变量。它们对于函数而言是局部的,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中所有同名的其他声明。
定义在所有函数体之外的对象存在于程序执行的整个过程中。这类对象在程序启动时被创建,到程序结束时被销毁。

自动对象

对于普通局部变量对应的对象来说,函数的控制流经过变量定义语句时创建该对象,到达定义所在的块末尾时销毁它,只存在于块执行期间的对象称为自动变量。当块的执行结束时,块中创建的对象的值也就变成未定义的了。
形参是一种自动对象,函数开始时为形参申请存储空间,一旦函数终止,形参就被销毁。
对于局部变量对应的自动对象来说,如果变量本身有初始值,使用这个初始值进行值初始化;否则,执行默认初始化。这就意味着内置类型的未初始化的局部变量具有未定义的值。

局部静态对象

局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止时被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
如果没有执行局部静态变量的值,执行值初始化,内置类型的静态局部变量被初始化为0。

在头文件中进行函数声明

和变量在头文件中声明,在源文件中定义类似。函数也应该在头文件中声明而在源文件中定义。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

分离式编译

如何编译和链接多个头文件。查看C/C++分离式编译。

参数传递

形参初始化的机理和变量初始化一样。形参的类型决定了形参和实参交互的方式。如形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递,或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。

因为C中没有引用,所以C中传递参数的方式只有值传递,而C++中多引用,不仅有值传递,还有引用传递。
更多关于参数传递的内容可以查看。

return和返回值类型

return语句结束当前正在执行的函数并将控制权返回到函数调用的地方。return语句有两种形式:

1
2
return ;
return expression ;

更多关于函数返回值的内容可以查看。

函数重载

如果同一个作用域内的几个函数名字相同但是形参列表不同,被称为重载函数。main函数不能被重载。
更多关于函数重载的内容可以查看。

函数的特殊语言特性

这一节介绍三种函数相关的语言特性,它们分别是默认实参,内联函数和constexpr函数,以及程序调用过程中的一些常用功能。
默认实参是函数声明时指定形参的值。一般某个形参被赋予了默认值,它后面的所有形参都必须要有默认值。

函数的特殊语言特性的具体内容可以查看。

函数指针

函数指针指向函数,它指向某种特定类型的函数,函数的类型由它的返回值和形参类型决定。声明一个函数指针:

1
2
3
4
5
6
int add(int a, int b)
{
return a+b;
}
int (*pf)(int, int) = add;
// pf是一个函数指针,pf的类型是int(*)(int, int)。

更过关于函数指针的内容可以查看。

参考文献

1.《C++ Primer》第五版

C++ flow of control

发表于 2019-11-10 | 更新于 2019-12-17 | 分类于 C/C++

范围for语句

语法格式:

1
2
for(declaration: expression)
statement

expression表示的必须是一个序列,比如花括号括起来的初始化值列表,数组,或者vector,string等类型的对象,这些类型的共同特点是拥有能返回迭代器的begin和end成员。
declaration定义一个变量,序列中的每个元素都能转换成该变量的类型,最简单的方法就是使用auto语句。
每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行statement。
范围for语句的定义来源于和它等价的传统for语句。在范围for语句中,相当于预存了end()的值,如果改变序列的话,end()的值可能会变得无效了。

try block和异常处理

异常处理机制为程序中异常检测和异常处理这两部分的写作提供支持。在C++中,异常处理包括:

  • throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题。我们说throw引发了异常。
  • try语句块,异常处理部分使用try语句块处理异常。try语句块以关键字try开始,以一个或者多个catch子句结束。try语句块中代码抛出的异常通常会被某个子句处理。因为catch子句处理异常,它们也被称为异常处理代码。
  • 一套异常类,用于在throw和catch`语句之间传递异常的具体信息。

try语句块和throw表达式

如下代码所示,try语句块中是程序的正常逻辑,和其他任何块一样。try语句块的变量在块外部无法访问,即使实在catch块内。
try语句块对应一个catch子句,这个子句负责处理runtime_error的异常。如果try语句块的代码中跑出了runtime_error的异常,接下来执行catch块中的语句。catch块中的内容进行异常处理,这里是输出了err.what的返回信息。runtime_error是标准库异常类型的一种,所有的标准库异常类都定义了what的成员函数,这些函数不需要参数,返回值是C风格字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while(cin >> item1 >> item2)
{

try{

if(item1.isbn()!= item2.isbn())
throw runtime_error("data must refer to same ISBN.\n");
}
catch (runtime_error err)
{
cout << err.what() < "\n Try again ?" << endl;
char c ;
cin >> c;
if(!cin || c = 'n')
break;

}
}

标准异常

C++标准库定义了一组类,用于报告函数库遇到的问题。这些异常类也可以用在用户编写的程序中,它们分别定义在4个头文件中:

  • exception头文件中定义了最通用的异常类exception,它只报告异常类的发生,不做任何处理。
  • stdexcept头文件定义了集中常用的异常类,如下所示:
    • exception
    • runtime_error
    • range_error
    • overflow_error
    • underflow_error
    • logic_error
    • domain_error
    • invalid_argument
    • length_error
    • out_of_range
      我们只能以默认初始化
  • new头文件定义了bad_alloc异常类型。
  • type_info定义了bad_cast异常类型。

标准库只定义了几种运算,包括创建或者拷贝异常类型的对象,以及为异常类型的对象赋值。
我们只能以默认初始化的方式初始化exception, bad_alloc和bad_cast对象,不允许为这些对象提供初始值。
其他异常类型的行为恰好相反,应该使用string对象或者C风格字符串进行初始化,但是不允许使用默认初始化的方式。创建此类对象时,必须提供初始值。

参考文献

1.《C++ Primer》第五版

C++ expression

发表于 2019-11-10 | 更新于 2019-12-17 | 分类于 C/C++

表达式(expression)

什么是表达式

一个表达式是一个或者多个操作符和操作符的操作数组成的序列。对表达式求值将得到一个结果,字面值和变量是最简单的表达式,其结果就是字面值和变量的值。
每一个C++表达式,都具有两个独立的属性:类型和值的类别。每个表达式都有一个非引用的类型,每一个表达式也正好属于三个主要的值类别之一:prvalue,xvalue和lvalue。

运算类型转换

简单来说,类型转换的规则大多复合情理。比如整形转换成浮点型,浮点型转换成整形。但是指针不能转换成浮点型。小整数(如bool, char, short)通常会被提升成较大的整数类型,通常是int。
在后面的小节会详细总结一下规则。

运算符重载

C++定义了运算符作用于内置对象和复合类型的运算对象时所执行的操作。当运算符作用于类类型的时候,用户可以自行定义它的含义。这种自定义的过程事实上是为了已存在的运算符附上了另外一层含义,所以称为运算符重载。

IO库中的>>和<<运算符以及string对象,vector对象和迭代器使用的运算符都是重载的运算符。

左值和右值

当对象被用作右值的时候,用的是对象的值。当对象被用作左值的时候,使用的是对象的身份(在内存中的位置)。

主表达式

字面值常量

Unevaluated expressions

typeid,sizeof,decltype运算符的操作数是不被evaluated expressions,因为这些操作符只使用到了它们操作数的编译时属性。

Discarded-value expressions

普通运算符

算术运算符

更多介绍可以查看参考文献2。

  1. 算术类型的运算对象和求值结果都是右值。
  2. 算术运算符包含加,减,乘,除,取余和正负号。
  3. 算术运算符能够作用于任何算术类型以及任何能转换成算术类型的类型。
  4. 一元正号运算符,加法运算符和减法运算符都能作用于指针。当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个提升后的副本。
  5. 在表达式求值前,小整数的类型会被提升成较大的整数类型,所有运算对象最后会转化成同一种类型。
  6. 如果把一个整数赋值给bool对象,只有当这个数是0是才是false,否则都是真,比如-1给一个bool对象也是真。所以布尔对象不应该参与运算。
  7. 整数相除还是整数。如果两个运算对象符号相同,则商为正,否则为负。
  8. 取余运算的运算对象都必须是整数。取余结果的符号和被除数相同。

逻辑运算符和关系运算符

逻辑运算符:与,或,非
关系运算符:等于,不等于,大于,小于,大于等于,小于等于
逻辑运算符和关系运算符的求值结果都是布尔值。

赋值运算符

赋值运算的结果是它的左侧运算对象,是一个左值。
赋值可以使用初始化列表。
对于内置类型来说,初始值列表只能包含一个值,而且该值即使转换的话其所占空间也不应该大于目标类型的空间。
对于类类型来说,赋值运算的细节由类本身决定。
对于vector来说,vector模板重载了赋值运算符,并且可以接收初始值列表,当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。
无论左侧运算对象的类型是什么,初始化列表都可以为空。这个时候使用值初始化进行赋值。

递增和递减运算符

这两种运算符的运算对象必须是左值运算对象,前置版本将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回。

成员访问运算符

箭头运算符作用于一个指针类型的对象时,结果是一个左值。

条件运算符

当条件运算符的两个表达式都是左值的时候或者能转换成同一种左值类型时,运算的结果是左值,否则运算的结果是右值。

位运算符

逗号运算符

类型转换

在C++类型中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以使用另一种类型关联的对象或者值来替代。或者说,如果两种类型可以相互转换,那么他们就是关联的。

隐式类型转换,一些类型转换是自动执行的,无需程序员的介入,有时甚至不需要程序员了解,所以叫做隐式类型转换。在发生以下情况时,编译器会自动的转换运算对象的类型:

  • 比int小的整型值首先提升为较大的整形值值。
  • 在条件中,非bool类型转为成bool类型。
  • 初始化过程中,初始化值转换成变量的类型。在赋值语句中,右侧对象转换成左侧运算对象的类型。
  • 如果算术运算或者逻辑运算的对象有多种类型,需要转换成同一种类型。
  • 函数调用时有时候也会发生类型转换。

不同的类型之间是有可能进行相互转换的,并且有不同的规则进行类型转换,主要有以下几种规则:

  1. 算术转换,针对于内置类型
  2. 其他隐式类型转换
  3. 显式类型转换。

算术转换,针对于内置类型

算术转换的含义是把一种算术类型转换成另外一种算术类型。

  1. 整形提升。把整数类型转换成大整数类型。对于bool, char, unisigned char, short和unsigned char等类型来说,只要它们所有可能的值都能存在int里,就把它们提升成int类型,否则,提升成unsigned int类型。
  2. 无符号类型的运算对象。如果一个运算对象是无符号类型,另一个运算对象是有符号类型。
    当无符号类型大于带符号类型,那么带符号类型转换成无符号。比如,int和unsigned int,将int转换成unsigned int类型,如果int是负值,将它转换成正值。相当于int对unsigned int所能所示的最大值取余。
    当无符号类型小于带符号类型时,如果无符号类型的值能存放在有符号类型中,将无符号类型转换成带符号类型。否则将带符号类型转换成无符号类型。

其他隐式类型转换

  1. 数组转换成指针。将数组转换成指向数组首元素的指针。注意,数组名表示的指针是不可修改的。当数组作为decltyp参数,或者作为取值地址符,sizeof或者typeid的运算对象时,上述转换不会发生。同样,使用一个应用来初始化数组,上述转换也不会发生。
  2. 指针的转换。常量0或者nullpt能转换成任意类型的指针。指向非常量的指针能转换成void *类型。指向任意对象的指针能转换成const void *。
  3. 转换成bool类型。存在一种从算术类型或者指针类型到布尔类型自动转换的机制。如果指针或者算术类型的值是0,那么转换结果是false,否则是true。
  4. 转换成常量。允许将指向非常量的指针转换成相应类型的指针常量的类型,对应引用也是如此。
  5. 类类型定义的转换

显式类型转换。

  1. 命名的强制类型转换。它的形式如下所示:
    1
    cast-name<type>(expression)

其中cast-name是static_cast,dynamic_cast, const_cast和reinterpret_cast的一种,type是转换的目标类型,而expression是要转换的值。

  1. static_cast,只要不包含底层const,任何具有明确定义的类型转换,都可以使用static_cast。比如将一个运算对象强制转换成double类型执行浮点数出发。
    1
    double slope = static_cast<double>(j)/i;

可以把较大的算术类型转换成较小的算术类型。这个时候,强制类型转换告诉读者和编译器,我不在乎可能的精度损失。
它对于编译器无法自动执行的类型转换也非常有用。

  1. const_cast,它只能改变底层const,将常量对象转换成非常量对象,这种性质叫做去掉const性质。如果对象本身不是一个常量,使用强制类型转换获得写权限是一个合法的行为,如果对象是一个常量,使用const_cast执行写操作就会产生未定义的后果。
    const_cast只能改变表达式的常量值,使用其他类型的命名强制类型转换改变表达式的常量属性都会引发编译器的错误。同样的,也不能使用const_cast改变表达式的类型。
    通常用于有函数重载的上下文。
  2. reinterpret_cast。尽量不使用强制类型转换,它干扰了正常的类型检查。在有重载的上下文中使用const cast无可厚非。但是在其他情况下使用const_cast也就意味着程序存在某种缺陷。其他的强制类型转换也不应该频繁使用。
  3. 旧式的类型转换。旧式的类型转换分别具有和上述三种强制类型转换相同的结果。如果换成const_cast和static_cast也合法,其行为和对应的命名转换一样。如果替换后不合法,则旧式的强制类型转换执行和reinterpret_cast类似的功能。

总的来说,static_cast可以进行不包含底层const的具有明确定义的类型转换。
cosnt_cast可以去掉底层const的const,但是不能改变表达式类型,也不能对去掉const的常量表达式执行写操作。

内存分配运算符

new表达式

delete表达式

其他

常量表达式

sizeof运算符

sizoef运算符返回一条表达式或者一个类型名字所占的字节数。sizeof运算符满足右结合律,所得的值是一个size_t类型的常量表达式。

sizeof运算的结果部分的依赖于它作用的类型。

  • 对char或者类型为char的表达式执行sizeof运算符,得到1
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小
  • 对指针执行sizeof运算得到指针本身所占空间的大小。
  • 对解引用指针执行sizeof运算得到指针指向对象所占空间的大小,指针不需要有效。
  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一个sizeof运算所得结果之和。sizeof并不会把数组转换为指针进行运算。
  • 对string或者vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

alignof

typeid

throw-expression

运算符的优先级表

表达式的运算顺序,可以参考[5]。

参考文献

1.《C++ Primer》第五版
2.https://en.cppreference.com/w/cpp/language/expressions
3.https://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions
4.https://en.cppreference.com/w/cpp/language/value_category
5.https://en.cppreference.com/w/cpp/language/eval_order

C++ type conversion

发表于 2019-11-09 | 更新于 2020-01-26 | 分类于 C/C++

类型转换

在C类型中,某些类型之间有关联。如果说两种类型有关联,当程序需要其中一种类型的运算对象时,可以使用另一种类型关联的对象或者值来替代。或者说如果两种类型可以相互转换,那么他们就是关联的。
C
提供了两种类型相互转换的规则:

  1. 隐式类型转换,隐式类型转换指的是编译器自动执行的,无需程序员的介入的转换。隐式类型转换包含上下文转换,值转换,算术提升,算术转换等。
  2. 显式类型转换。

隐式类型转换

[13.3.3.1 Implicit conversion sequences]
什么时候会发生隐式类型转换?当一个类型为T1的表达式用在需要类型为T2的上下文时,会发生类型转换。比如:

  • 函数声明中参数是T2类型的,传入参数是T1类型的;
  • 将T1类型的表达式用作需要T2类型操作数的操作符。
  • 用T1类型的表达式初始化一个T2类型的新对象,包含函数的返回语句。
  • 当表达式被用在一个switch语句的条件中时(T2是整型)
  • 表达式用于if的条件或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时或者循环语句的条件时(T2是bool类型)

只有存在从T1到T2的没有歧义的隐式类型转换时,这个程序才能编译成功。

转换的顺序

隐式类型转换序列如下所示:

  1. 0或1个stand convesion sequence;
  2. 0或1个用户自定义的转换;
  3. 0或1个stand convesion sequence;

一个stand conversion sequence由以下顺序组成:

  1. 0或1个左值转换
  2. 0或1个数值提升或者数值转换
  3. 0或1个函数指针转换(从C++17开始)
  4. 0或1个qualification adjustment。

一个用户定义的转换由0个或者一个非显式的单个参数构造函数或者非显式的转换函数调用。

An expression e is said to be implicitly convertible to T2 if and only if T2 can be copy-initialized from e, that is the declaration T2 t = e; is well-formed (can be compiled), for some invented temporary t. Note that this is different from direct initialization (T2 t(e)), where explicit constructors and conversion functions would additionally be considered.

上下文转换

在下面列出来的上下文中,期望获得一个bool类型的变量,如果声明语句bool t(e)能够编译成功,那么就进行隐式转换。这些表达式e被称为上下文转换成bool。

  • 控制流if, while和for;
  • 逻辑操作符!, &&, ||的操作数;
  • 条件操作符?:;的第一个操作数;

在下列上下文中,需要一个和上下文相关的类型T。
…

值转换

每一个表达式都有一个非引用类型和值类别,值转换发生在表达式的值类别上。无论何时当表达式的值类别时和操作符需要的操作数值类别不同时,发生这类转换。

左值到右值转换

任何非函数,非数组类型T的glvalue可以隐式的转换成相同类型的prvalue。如果T是非类类型的话,这个转换还会去掉cv修饰。
在以下情况中,对象表示的glvalue是不可访问的:

  • 转化发生在无法评估的上下文,比如sizoef, decltype或者静态形式的的typid。

数组到指针的转换

函数到指针

算术提升

整形提升

小整形(如char)的prvalue可以转换为大整形(如int)的prvalues。特别的,算术操作符不接受比int还小的类型作为参数,在使用了lvalue到rvalue的转换之后,自动的进行整形提升。这种转换不会丢失精度。
整形提升可以分为以下几类:

  • signed char或者signed short可以被转换成int;
  • unsigned char或者unsigned short被转化成int,如果超过int的表示范围,转换成unsigned int;
  • char被转换成int还是unsigned int取决于它是signed char还是unsigned char;
  • w_char_t, char16_t, char32_t可以被转换成下列第一个可以容纳它们整个取值范围的类型:int, unsigned int, long, unsigned long, long long, unsigned long long;(有个疑问,wchar_t等是有符号的还是无符号的??)

总的来说,就是对于bool, char, unisigned char, short和unsigned char等类型来说,只要它们所有可能的值都能存在int里,就把它们提升成int类型,否则,提升成unsigned int类型。
并不是所有的转换都是提升,当char到short就是一个转换。

浮点数提升

一个float类型的prvalue可以转换成double的prvalue。值不会改变,精度不会丢失。

算术转换

算术转换的含义是把一种算术类型转换成另外一种算术类型。算术转换可能会改变对象的值,丢失一定精度。

整形转换

  • 如果目标类型是无符号的,转换结果是source value对$2^n$取余得到的最小无符号整数,n是目标类型的位数。
  • 如果目标类型是有符号的,如果source整数能够保存在目标类型中,对象的值不变。否则转换的结果是由实现定义的(注意和有符号整形算术溢出的区别,溢出是未定义行为)。
  • 如果source type是bool,值false被转换为0,值true被转换成destination类型的值1(注意,如果destination类型是int的话,就变成了整形提升)。
  • 如果目标类型是bool,这就是一个boolean转换

整形转换的实现,可以查看。基本上所有的C/C++ 实现就是不改变二进制编码,分别使用无符号编码和补码形式进行解释这个二进制编码。
相同长度的无符号数和有符号数,在运算中有有符号数的话,就会把有符号数转换成无符号数。比如for循环中,for(unsigned i = 10; i >= 0; --i),永远不会跳出for循环,因为当i=0时,--i就相当于得到了有符号数-1,而-1的补码形式和无符号数的$2^w -1 $的编码是一样的,而这里就是得到了$2^w -1$。

浮点数转换

一个浮点数类型的prvalue可以被被转换成任何其他浮点数类型的prvalue。如果这个转换是浮点数提升,那么这就是一个浮点数提升而不是一个浮点数转换。

  • 如果source value可以精确的表示在目标类型中,这个值不变。
  • 如果source value在目标类型的两个可表示值之间(就是目标类型的精度比较低),则结果是其中的一个(由实现定义)。
  • 否则是未定义行为。

浮点数和整形之间的转换

  • 一个浮点数类型的prvalue可以转换成一个整形的prvalue,小数部分被截断。如果这个值不适合目标类型,是未定义行为。
  • 一个整形的prvalue或者没有范围的枚举类型可以被转换成浮点数类型的prvalue。如果这个值不能被正确表示,由实现定义选择。如果这个值不适合目标类型,是未定义行为。如果source type是bool,值false被转换成0,值true被转换成1。

指针转换

  1. 指针的转换。常量0或者nullpt能转换成任意类型的指针。指向非常量的指针能转换成void *类型。指向任意对象的指针能转换成const void *。
  2. 数组转换成指针。将数组转换成指向数组首元素的指针。注意,数组名表示的指针是不可修改的。当数组作为decltyp参数,或者作为取值地址符,sizeof或者typeid的运算对象时,上述转换不会发生。同样,使用一个应用来初始化数组,上述转换也不会发生。
  3. 转换成常量。允许将指向非常量的指针转换成相应类型的指针常量的类型,对应引用也是如此。

指针到成员转换

布尔转换

  1. 转换成bool类型。存在一种从算术类型或者指针类型到布尔类型自动转换的机制。如果指针或者算术类型的值是0,那么转换结果是false,否则是true。
  2. 无符号类型的运算对象。如果一个运算对象是无符号类型,另一个运算对象是有符号类型。
    当无符号类型大于带符号类型,那么带符号类型转换成无符号。比如,int和unsigned int,将int转换成unsigned int类型,如果int是负值,将它转换成正值。相当于int对unsigned int所能所示的最大值取余。
    当无符号类型小于带符号类型时,如果无符号类型的值能存放在有符号类型中,将无符号类型转换成带符号类型。否则将带符号类型转换成无符号类型。

其他隐式类型转换

类类型定义的转换

Qualification转换

函数指针转换

bool变量的安全性问题

显式类型转换。

命名的强制类型转换。它的形式如下所示:

1
cast-name<type>(expression)

其中cast-name是static_cast,dynamic_cast, const_cast和reinterpret_cast的一种,type是转换的目标类型,而expression是要转换的值。

static_cast

static_cast,只要不包含底层const,任何具有明确定义的类型转换,都可以使用static_cast。比如将一个运算对象强制转换成double类型执行浮点数出发。

1
double slope = static_cast<double>(j)/i;

可以把较大的算术类型转换成较小的算术类型。这个时候,强制类型转换告诉读者和编译器,我不在乎可能的精度损失。
它对于编译器无法自动执行的类型转换也非常有用。

const_cast

  1. const_cast只能改变运算对象的底层const,将常量对象转换成非常量对象,这种性质叫做去掉const性质。如果对象本身不是一个常量,使用强制类型转换获得写权限是一个合法的行为,如果对象是一个常量,使用const_cast执行写操作就会产生未定义的后果。const_cast还可以将一个非const对象变成const对象。

    1
    2
    3
    4
    5
    6
    7
    8
    string s1("hello"); 
    const string s2("world");

    string *p1 = &s1;
    const string *p2 = &s2;

    string *p3 = const_cast<string *>(p2); //去掉底层const,但是通过p3写它指向的东西是未定义行为。
    const string *p4 = const_cast<const string*>(p1); //将非底层const转换成底层const。

  2. 只有const_cast能改变表达式的常量属性,使用其他类型的命名强制类型转换改变表达式的常量属性都会引发编译器的错误,注意不能使用const_cast改变表达式的类型。

  3. 通常用于有函数重载的上下文。

reinterpret_cast

通常用来进行指针类型的转换。
reinterpret_cast。尽量不使用强制类型转换,它干扰了正常的类型检查。在有重载的上下文中使用const cast无可厚非。但是在其他情况下使用const_cast也就意味着程序存在某种缺陷。其他的强制类型转换也不应该频繁使用。

旧式的类型转换。

旧式的类型转换分别具有和上述三种强制类型转换相同的结果。如果换成const_cast和static_cast也合法,其行为和对应的命名转换一样。如果替换后不合法,则旧式的强制类型转换执行和reinterpret_cast类似的功能。

总结

总的来说,static_cast可以进行不包含底层const的具有明确定义的类型转换。
cosnt_cast可以去掉底层const的const,但是不能改变表达式类型,也不能对去掉const的常量表达式执行写操作。

参考文献

1.《C++ Primer》第五版
2.https://en.cppreference.com/w/cpp/language/implicit_conversion
3.https://en.cppreference.com/w/cpp/language/explicit_cast
4.https://en.cppreference.com/w/cpp/language/cast_operator
5.https://en.cppreference.com/w/cpp/language/const_cast
6.http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
7.https://en.cppreference.com/w/cpp/language/value_category

linux man

发表于 2019-11-07 | 更新于 2019-11-11 | 分类于 linux

man的简介

man是linux下的一个文档查看命令,它把所有的命令分为9个部分。

  1. user command
  2. system calls,系统调用,内核提供的函数
  3. library functions,库函数
  4. specifal files,像/dev/文件夹下的
  5. file formats,查看相应文件的文件格式
  6. games,
  7. conventions
  8. administration and privileged commands,系统管理员命令
  9. math library functions,数学函数库
  10. tcl functions

一个命令可能会在多个类别中出现,在查找相应的命令的时候,可使用man加上具体的数字,就可以得到对应类中的手册。
具体示例
查看passwd和group的文件格式:

1
2
man 5 passwd
man 5 group

man命令的介绍

  • 名字(NAME),命令简介
  • 格式(SYSOPSIS),命令格式
  • 描述(DESCRIPTION),介绍命令的参数options
  • 作者(AUTHOR),命令的作者
  • 提交BUG(REPORTINT BUGS),提交一些bug的地址
  • 版权(COPYRIGHT)
  • 更多参考(SEE ALSO)

比如man ls的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NAME
ls - list directory contents

SYNOPSIS
ls [OPTION]... [FILE]...

DESCRIPTION
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is speci‐
fied.

Mandatory arguments to long options are mandatory for short options
too.

-a, --all
do not ignore entries starting with .

-A, --almost-all
do not list implied . and ..

--author
...

命令格式

拿ls举个例子,man ls的SYNOPSIS如下:

1
ls [OPTION]... [FILE]...

其中[]表示可选,...表示有多个。[OPTION]...表示有多个可选参数,[FILE]...表示有多个可选文件。

命令参数

DESCRIPTION部分会给定更加详细一些的介绍,以及给出具体参数的含义。

1
2
3
4
-a, --all
do not ignore entries string with .
--author
with -l, print the author of each file

其中-是缩写,--是全称,他们的作用其实是一样的。

参考文献

1.https://linux.die.net/man/
2.https://www.cnblogs.com/shanyu20/p/10943393.html

C++ namespace

发表于 2019-11-06 | 分类于 C/C++

命名空间(namespace)

  1. 命名空间可以解决名自定义冲突问题。比如有两个不同的库中实现了一个同名的函数,可以通过加上命名空间进行区分。

  2. C++标准库定义的名字在都在std 命名空间,如cin,cout和endl等都在std命名空间中。在访问时需要使用以下方式:std::cin,std::cout,std::endl。
    可以使用using声明来简化使用过程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<iostream>
    using std::endl;
    using std::cin;
    int main()
    {
    int val = 0;
    cin >> val;
    std::cout << val << std::endl;
    return 0;
    }

  3. 使用了using声明之后,在代码中就无须指出namespace了。

  4. 每个名字都需要独立的using声明。

  5. 头文件中不应该包含using声明,因为头文件的内容会拷贝到所有引用它的文件中,如果头文件里有某个using声明,那么每个使用了该头文件的文件都会有这个声明。可能会有冲突。

参考文献

1.《C++ Primer第五版》

C++ string

发表于 2019-11-06 | 更新于 2020-01-08 | 分类于 C/C++

string标准库

string是标准库的一部分,但是它不是一个容器库,不过string和容器一样支持很多操作,这里为了统一,就把它也放在STL这里部分了。
string是可变长的字符串,使用时需要包含<string>头文件。

1
2
#include <string>
using std::string;

string对象的常用操作

  1. 读写

    1
    2
    3
    4
    string s;
    cin >> s; //从遇到的第一个非空白符开始读入,遇到空白符就停止,且不会读入这个空白符
    cout << s;
    getline(cin, s); //遇到换行符停止,读入换行符,但是不会将换行符写入s,并且遇到的第一个换行符就算。

  2. string的大小
    empty()返回string是否为空,
    size()返回string的长度。

  3. string::size_type
    size返回一个无符号类型string::size_type的值,能够容纳放下任何string对象的大小。

  4. 比较string对象。如果两个不同长度的string,如果短的string和长的string的共同部分完全一样,短的小。
    如果两个string对象在某些位置上的字符不一样,按照字典序排大小,字典序在前的小。

  5. string赋值。可以给string直接赋值字符串常量,也可以给string赋值string对象

  6. string相加。字面值和string对象相加,加号两边至少有一个对象是string对象

  7. substr返回一个子字符串。

  8. 标准库cctype提供了操作string中字符的函数。

  9. s.data()返回字符串内存空间的地址,可以看源码。

构造string

  1. 默认初始化,就是简单的声明:

    1
    string s1;

  2. 直接初始化,使用等号就是拷贝初始化:

    1
    2
    3
    string s2(s1);
    string s3("value");
    string s4(10, 'c');

  3. 拷贝初始化,不使用等号的初始化:

    1
    2
    string s2 = s1;
    string s3 = "value";

当初始值只有一个的时候,使用直接初始化或者拷贝初始化都行,但是当初始化用到的值有多个时,只能使用直接初始化的方式。

  1. 和其他顺序容器相同的构造函数。
  2. 其他构造函数
    1
    2
    3
    string s(cp, n);
    string s(s2, pos2);
    string s(s2, pos2, len2);

string的增删改

  1. string支持顺序容器的赋值运算符,assgin,insert和erase。
  2. 除了接收迭代器的insert, erase之之外,string还有接收下标的版本。
  3. string定义了append和replace函数。replace相当于调用erase和insert。

string的搜索

每个string都可以调用以下搜索函数:

  • s.find(args)。在s中查找args第一次出现的位置。
  • s.rfind(args)。在s中查找args最后一次出现的位置。
  • s.find_first_of(args)。在s中查找args中任何一个字符第一次出现的位置。
  • s.find_last_of(args)。在s中查找args中任何一个字符最后一次出现的位置。
  • s.find_first_not_of(args)。在s中查找第一个不在args中的字符。
  • s.find_last_not_of(args)。在s中查找最后一个不在args中的字符。

compare函数

每个string都可以调用compare函数和另一个string或者字符数组比较大小,根据s和参数指定的字符串的大小关系,返回相应的值。有相应的重载类型可以比较部分字符串。

string和其他类型的转换

string和C风格字符串的转换

数值转换

  • to_string(val)
  • stoi(s, p, b)
  • stol(s, p, b)
  • stoul(s, p, b)
  • stoll(s, p, b)
  • stoull(s, p, b)
  • stof(s, p)
  • stod(s, p)
  • stold(s, p)

参考文献

1.《C++ Primer第五版》

1…101112…34
马晓鑫爱马荟荟

马晓鑫爱马荟荟

记录硕士三年自己的积累

337 日志
26 分类
77 标签
RSS
GitHub E-Mail
© 2022 马晓鑫爱马荟荟
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.6.0