文章目录
- 1 引题
- 1.1 产生原因(P1)Why
- 1.2 定义(P2)What
- 1.3 具体化
- 2 迭代器种类(P3-P22) What
- 2.1 平凡迭代器(Trivial Iterator)(P3)
- 2.1 输入迭代器(Input Iterator)【顺序只读访问】(P4-P6)
- 2.2 输出迭代器(Output Iterator)【顺序只写访问】(P7-P9)
- 2.3 正向迭代器(Forward Iterator)【顺序读-写访问】(P10-P12)
- 2.3.1 Const Iterator 和Mutable Iterator
- 2.4 双向迭代器(Bidirectional Iterator)【可逆访问】(P13-P15)
- 2.5 随机访问迭代器(Random Access Iterators)【随机访问】(P16-P20)
- 2.6 类别总结 (P22)
- 2.6 完整示例程序
- 3 迭代器区间和越界值(P23-P24)
- 3.1 顺序访问
- 3.2 逆序访问
- 3.3 本质
- 4 指针
- 4.1 概念(P25)
- 4.2 基本用法 P26
- 4.2.1 指向变量的指针
- 4.2.2 指向函数的指针
- 4.2.3 指向函数指针的原理
- 4.3 作为库算法的指针 P27
- 4.4 空指针 P28
- 4.5 指针算法
- 5 作业 P29
- 6 讲解的PPT
这片文章源自于自己的一次授课经验。下面是是授课的内容,末尾附带讲解的PPT。
1 引题
基础小知识:C 丹尼斯·里奇(美国)1970年发明C。比亚尼·斯特劳斯特鲁普(丹麦) 1979研发C++,1983年命名为C++ 1998年加入STL。
【C++中的应用】C++的核心在于类。而C++的类不同于C的结构,是因为C++的类中有继承和多态。而多态这个词源于希腊,意为具有多种形态。多态在C++中的含义是通过一个类型来调用许多函数中的一个(一种类型有表示几种类型的能力)。C++实现多态的方法是通过指针或引用为参数调用虚拟函数,虽然指针(或引用)参数的类型是固定的,但是参数所指(或引用)对象的类型却可以是所指(或所引)对象的类型,或者是有该类派生出来的任何一个子类。
以指针(或引用)为参数调用虚拟函数时,指针指向(或引用)的对象的实际类型在运行时,是可以变化的。(动态绑定)
另外一个就是句柄类(代理类),一个负责管理所指对象的内存分配与释放等的指针类,可以帮助用户隐藏很多使用指针的隐患。(软件工程中的一个理念:任何一个问题都可以通过引入一个间接层来简化)
使用指针的隐患:
- 1 复制一个指针不会导致对指针所指对象都进行复制,当无意使用两个指针指向同一个对象时,常常会出现令人疑惑的结果;
- 2 删除一个指针不会释放指针所指对象所占用的内存,这常常导致内存泄露;
- 为数组释放内存,如果指针delete p会导致只释放一个元素的内存,要使用delete[],带上中括号,告诉系统释放整个数组占用的内存。
- 如果只是为一个元素分配内存,然后用指针p指向它,那么delete p只会删除p指向的对象,但是指针p并没有被删除,此时需要让p = 0;
- 默认的析构函数只会删除一个指针变量,不会释放该指针指向的对象占用的内存。
- new T[n]不仅分配内存空间,还会运行T的构造函数来为每个元素进行默认初始化
- 1 一次是new自己进行的,使用T:T()构造函数为一个类型为T的数组中的每个元素进行初始化;
- 2 另一次是将用户提供的数值赋给Vec类型对象的元素时。
- 使用预分配的方法,<memory>头文件中,提供了一个名为allocator<T>的类,它可以分配一块预备用于存储T类型对象但是尚未被初始化的内存快,并返回一个指向这块内存块中的头元素的指针。
- 1 为用户提供尽可能大的弹性的初衷;
- 2 当使用预分配内存,没必要对多分配空间里的元素进行初始化。
- 3 删除一个对象但没有删除指向该对象的指针会产生一个空悬指针(dangling pointer)。在程序中使用会造成会定义操作;
- 4 如果定义一个指针但不对它进行初始化,会使该指针不指向他任何地方,这是如果程序用到该指针,则会导致未定义操作。
【C中的应用】数组大家都应该使用过吧!这是一个静态分配内存的容器(不像动态分配内存的向量)。在C/C++中,如果只使用数组而不是用指针将不能解决任何有意义的问题。
提到指针的第一印象往往都是“*”,而”*”往往是指针的代言词,它表示间接引用运算符,如果p本身就是对象的指针(即地址),那么*p则表示引用解析p,获取指针地址或引用地址上的值。而获得地址的方法是对一个对象使用&,它表示求址运算符,使用后获得该对象的地址。
这些都是常识,,平时大家见的都是指向变量的指针,有没有见过指向函数的指针,亦或者是返回类型为指向函数的指针。指针算法大家有没有了解过?即指针可以做减法等运算。
追根溯源,大家在使用指针的时候,有没有想过指针的本质是什么?(Why?What?How?)
下面我来为大家揭开这层面纱。
1.1 产生原因(P1)Why
首先指针是一个随机访问迭代器(Random Access Iterators)。所以想了解指针是什么,就要先了解迭代器(iterator)是什么?
说起迭代器就不得不提到C++标准库STL(Standard Template Library)【HP STL】【在惠普实验室开发出来的】。
迭代器并不是C++语言特有的一部分,但它是标准库组织的一个基本的组成部分。
C++标准库STL(Standard Template Library)一个主要贡献是,它确立了一种算法设计思想,将数据容器(containers)和算法(Algorithms)分开,彼此独立的设计,最后再以一贴粘合剂将它们撮合在一起。。这也是标准库的中心思想。
这个粘合剂就是迭代器。迭代器就是算法和数据结构之间的接口,即算法的实参,以此来访问数据结构中的值。
库算法所用到的迭代器都有求某些操作,我们能以这些操作为基础而分解算法,这就意味着可以将一个容器和能够使用这个容器的算法匹配起来。
例如<algorithm>中的sort()函数只能对向量或字符串使用(随机访问迭代器),而对list(双向链表)要使用排序,需要使用其成员函数list.sort()。(双向迭代器)
1.2 定义(P2)What
迭代器:(不同的定义)
- 1 一种函数,会产生有着与其参数相关的属性的迭代器以做他用。————《Accelerate C++》
- 2 任何特定的迭代器都是某个类型的对象。——《The C++ programming Language》(C++发明者)
- 3,用来指向其他对象的一种对象。————《Generic Programming and ths STL》(与STL作者共事的同事)
- 4,提供一种方法,使之能够依序巡访问某个聚合物(容器)所含的各个元素,而又无需暴露该聚合物的内部表述方式。————《Design Patterns》(四位国际公认的面向对象软件领域的专家编写)
迭代器适配器:产生迭代器的函数。
1.3 具体化
- 一个迭代器为一个值,
- 它能够识别容器以及容器中的一个元素;
- 允许我们检查存储在这个元素中的值;
- 提供操作以移动在容器中的元素;
- 采用对应容器所能够有效处理的方式对可用的操作进行约束。
2 迭代器种类(P3-P22) What
由于迭代器是作为算法的参数,不同种类的迭代器提供了不同种类的操作。因此重点是理解不同算法对其使用的迭代器的具体要求以及不同种类的迭代器所分别支持的操作。
库定义了5种迭代器(iterator categories),其中每一种都对应一个特定的迭代器操作集合。这些迭代器种类划分每一个库容器提供的迭代器的类别。每种库算法都有关于迭代器参数期望的种类。因此,迭代器种类可以提供一种方法去理解那些容器可以使用那些算法。
虽然定义了5种,但是我们实际讲到的是6种。
五种类型的迭代器总结:
迭代器类型 | 操作 |
输入迭代器 | 按一个方向顺序访问,只能输入 |
输出迭代器 | 按一个方向顺序访问,只能输出 |
正向迭代器 | 按一个方向顺序访问,既能输入也能输出 |
双向迭代器 | 按两个方向顺序访问,既能输入也能输出 |
随机访问迭代器 | 按两个方向随机访问,既能输入也能输出 |
2.1 平凡迭代器(Trivial Iterator)(P3)
作用:用于理清其他iterator的定义。
条件:
- 1,可以拥有可提领(dereferenceable)值;
- 提领:获得指针或引用地址上的值。(引用解析)
- 2,可以令某个可提领的值无效;
- 例如,p是一个指针,delete p。
- 3,该类别的对象所指的值是可以被更改(mutable)或不能被更改(constant)(不同迭代器不同);
- 例如:
int*
是一个mutable iterator,而从const int*
是一个const iterator。 - 如果某个iterator是mutable,则其value type(迭代器所指对象的类型)必然是Assignable的一个类(【复制某个类型的对象,并将数值赋予该类型的变量身上】具有赋值运算符函数必须要修改对象【计算机中一段具有类型的内存空间】的值),反之不成立(是const,则不能改变其值)。
- 4,可能拥有一个singular值;(奇异值)【联想到奇异矩阵:矩阵的行列式为零】
- singular值:未分配的迭代器(尚未初始化指向任何地方);
- 唯一操作:把一个nonsingular iterator赋值给singular iterator;
- 可提领的iterator都是nonsingular,反之则不成立。
- 例如past-the-end指针,紧邻数组的尾端后一位,但是不可提领,确实nonsingular iterator(指向了分配内存,但是未初始化的空间)。
- 5,复杂度:恒定分摊时间(amoritozed constant time)。
- 分摊时间:执行很多次操作,平均每次分摊下来的时间;
- 分摊时间没必要固定,可以是线性、对数或其他时间;
- 例如往向量中添加元素,当向量未满时,操作费事O(1),当向量满时(假设分配和释放花费常量时间),那么将花费O(n)(复制原始容器中的值)。这意味着,随着时间推移,在最坏的情况下将默认为O(1),或固定的时间。
- 而固定分摊时间是每次操作花费的时间都是一样的。
有效的表达式:
- 1 *x
- 可提领
- 2 *x = t
- 可提领并且可被赋值(mutable iterator type)
- 3 x->m 等价于 (*x).m
- 成员访问,HP STL并没有定义iterators定义operator->。【取成员运算符】
*it
间接引用迭代器it以获得存储在容器中位于it所指示的位置的值。
这个操作经常于“.”结合起来获取类对象的一个成员。
例如(*it).x产生由迭代器it所指示的对象的一个成员。*的优先级比.低,而跟++和–相同。
it->x与(*it).x等价,它返回间接调用迭代器it而获得的对象所指示的成员。
不管什么时候,如果两个迭代器支持同样的操作,它们就会给这个操作同样的名称。例如,所有迭代器都使用++使一个迭代器指向它的容器的下一个元素。
2.1 输入迭代器(Input Iterator)【顺序只读访问】(P4-P6)
作用:只能被用于读一个序列的元素。
条件:
- 1, 支持累加动作;
- 2, 可以比较两个对象的想等性;
- 3,仅支持单回(single pass)算法。(运用Input Iteratorde算法所受的限制,而不是塑造了Input Iterator概念的类型所需接受的限制)
- 只遍历range[first, end)一次,并对range中的值最多读取一次;
- 每个range一次只能够支持一个有效的iterator。
- 例如不能写成p1 = p2++;,然后继续使用p1,p2;
- 如果p==q成立,不能认定++p == ++q也成立。
有效表达式:
- 应该支持++(前缀和后缀)
- ==
- !=
- 一元*运算符
- it->member和(*it).member之间的运算符是等价的
示例标准库函数find():
template<class In, class X>
In find(In begin, In end, const X& x){
while(begin != end && *begin != x){
++begin;
}
return begin;
}
实现方式二:
template<class In, class X>
In find(In begin, In end, const X& x){//递归设计风格
if(begin == end || *begin == x){//常见于List或ML语言
return begin;
}
begin++;
return find(begin, end, x);
}
流迭代器(定义在<iterator>)中用于istream的迭代器就是输入迭代器。输入流迭代器一种名为istream_iterator的输入迭代器类型。
输入迭代器的例子:
vector<int> v;
//从标准输入中读整数值并将它们添加到v中
copy(istream_iterator<int>(cin), istream_iterator<int>(), back_inserter(v));
流迭代器是模版,定义一个流迭代器时,必须设法告诉它应该从流读入什么类型的数据或者应该写什么类型的数据到流。
copy的第一个参数构造了一个istream_iterator类型的迭代器,这个迭代器被链接到cin中,要求读入int类型的值。
第二个参数创建了一个默认(为空)istream_iterator<int>类型的迭代器,这个迭代器不会与任何文件连接在一起。istream_iterator有一个默认值,而这个默认值有一个性质,就是istream_iterator类型的迭代器一旦到达了文件末尾或者是错误状态中,那么它就会和这个默认值相等。
因此可以用这个默认值来为copy表示“超过末尾元素一个单位位置”的协定。
2.2 输出迭代器(Output Iterator)【顺序只写访问】(P7-P9)
作用:只用于被写一个序列的元素。
条件(it:迭代器):
- 1,支持累加动作;
- 2 ,支持复制和赋值操作;
- 没有定义有关的value type(迭代器所指对象的类型)。因为提领动作并不会返回有用的值。
- 因此提领操作符只能被用于如*x = t的赋值动作上。
- 3 ,仅支持单回(single pass)算法(写)。
- 不能在没有对it进行递增的情况下,对it进行多次赋值;
- 不能在对*it的两个赋值运算符之间执行超过一次的++it操作。
有效表达式:
- 1, ++(前缀、后缀);
- 2, X y(x); *y = t;
- 3, 一元*运算符;
- 4, it->member等价于(*it).member。
示例标准库函数copy():
template<class In, class Out>
Out copy(In begin, In end, Out dest){
while(begin != end)
*dest++ = *begin++;
return dest;
}
back_inserter ( c )就是一个输出迭代器,其中c是支持push_back的容器。
流迭代器(定义在<iterator>)中用于osream的迭代器就是输出迭代器。输出流迭代器一种名为ostream_iterator的输入迭代器类型。
输出迭代器的例子:
vector<int> v;
//输出v的元素,元素之间用一个空格隔开
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
将整个向量复制到标准输出。第三个参数构造了一个迭代器,此迭代器被连接到cout,它要求写int类型的值。
用于构造ostream_iterator<int>类型对象的第二个参数指定了一个值,这个值会被写到每个元素之后。通常这个值是一个字符串文字。如果我们不提供一个这样的值,那么istream_iterator类型的迭代器在写数值时就不会带任何的分隔。
如:
//两个元素之间没有分隔
copy(v.begin(), v.end(), ostream_iterator<int>(cout));
2.3 正向迭代器(Forward Iterator)【顺序读-写访问】(P10-P12)
作用:顺序读或者写(一旦处理了一个元素就绝不会在访问它)(所有标准容器都支持正向迭代器)。
条件:
- 1 支持多回(multipass pass)算法(读和写)。
- 由于运用Input Iterator 和Output Iterator的算法,只能是single pass(单回)算法,这是的算法的复杂度最多只能与区间内的元素呈线性关系。但是并非所有算法都是线性的。例如许多有用的排序算法,复杂度为
或
。
- 任何区间内只能有一个有效的(作用中的)Input Iterator 或Output Iterator。
- 这使得算法在同一区间内只能对单一元素做动作。对于那种(行为取决于两个或多个不同元素之间的关系)的算法不能为力。
- 但是Forward Iterator没有这样的限制。
有效表达式:
- 1, ++(前缀、后缀);
- 2,== !=
- 3,X y(x); *y = t;
- 4, 一元*运算符;
- 5, it->member等价于(*it).member。
示例标准库函数repalce():
【同一区间内读取和更新】
template<class For, class X>
void replace(For beg, For end, const X& x, const X& y){
while(beg != end){
if(*beg == x){
*beg = y;
}
++beg;
}
}
线性查找变形:
查找连续两元素具有相同的值【处理一个区间多个元素关系】
template<class For>
For adjacent_find(For beg, For end){
if(beg == end)
return end;
For next = beg;
while(++next != beg){
if(*beg == *next)
return beg;
beg == next;
}
return end;
}
正向迭代器的例子:
slist<int>::iterator;
slist<int>::const_iterator;
char*;//也是随机访问迭代器
2.3.1 Const Iterator 和Mutable Iterator
Forward Iterator可以是常量的(constant),也可使可变的(mutable)。
可变(mutable)迭代器: int*;(也是输入或输出迭代器)
常量(const)迭代器:const int*
注意的是constant Iterator不一定都是常量对象(const object)。
- 它是一种无法用来更改其所指值的iterator,但是iterator自身可能是const,也可能不是。
- 例如const int* 就是一个const iterator,因为它不能用来更改其所指值。
- int* const虽然是常量对象(const object),但是可以更改其所指值。
衍生出
每个标准容器都定义了两种相关的迭代器类型:
container-type::const_iterator
container-type::iterator
其中container-type为容器类型,如vector<string>,它包括容器元素的类型。
可以将iterator类型转为const_iterator,但却不能将 const_iterator转换为iterator。
const_iterator一般用于仅需读取的操作;
iterator类型用于想使用迭代器修改存储在容器中的值。
2.4 双向迭代器(Bidirectional Iterator)【可逆访问】(P13-P15)
作用:逆向顺序访问一个容器的元素(所有标准库容器都支持双向迭代器)。
条件:
- 1, 支持累加和累减动作;
- 2, 可以比较两个对象的想等性;
- 3, 支持复制和赋值操作;
- 4, 仅支持多回(multipass pass)算法(读或写)。
有效表达式:
- 1, ++(前缀、后缀),–(前缀、后缀);
- 2,== !=
- 3,X y(x); *y = t;
- 4, 一元*运算符;
- 5, it->member等价于(*it).member。
算法:能够遍历双端链表。
示例标准库函数reverse():
template<class Bi>
void reverse(Bi begin, Bi end){
while(begin != end){
--end;
if(begin != end){
swap(*begin++, *end);
}
}
}
为什么不直接交换*begn和*end的值,而是只用swap函数?
直接交换的话,需要知道变量的类型。如果建立一个临时的Bit型变量temp,如果建立时没有初始化,而在后面给它赋值
Bi temp;
*temp = *begin;//会导致给空地址赋值,会导致错误
如果建立时并初始化,后面的begin值改变时,temp存的begin值也会相对应的改变(由于存的是迭代器【地址】),会导致没有达到效果(无法反转容器内元素)
Bi temp = begin;//定义并初始化
*temp = *begin;
*begin = *end;
*end = *temp;
实际举例:
list<int>::iterator
set<string>::iterator
char*(同时也是一个随机访问迭代器)
而swap函数(自写):(与上面的区别在于传的是值)
template<class T>
void swap2(T &l, T &r){
T t = l;
l = r;
r = t;
}
2.5 随机访问迭代器(Random Access Iterators)【随机访问】(P16-P20)
作用:在容器中各种跳转的读或写。
是否有必要定义随机访问迭代器?
因为大部分操作都可有operator++和operator–完成。
比如定义一个advance函数,让advance(p,n)将双向迭代器累p累加必要的次数。
原因是算法复杂度(complexity)。advance(p,500)将会是advance(p,100)的五倍,也就是advance的复杂度是。
但是,如果p是指针的话,p+1000000并不会比p+1慢,也就是时间复杂度是O(1)。访问数组中的任何一个元素,都像访问其他元素一样快。
以快排(Quicksort)为例子,它是作用于数组上的排序算法,其快速的原因是可以在固定的时间内内访问任何一个数组。但是,如果作用在链表(linked list)上就不合理。
因此Random Access Iterator的真正独特之处在于它能够以固定的时间(const time)随机访问任何元素
有效表达式::
- 1, ++(前缀、后缀),–(前缀、后缀);
- 2,== !=
- 3,X y(x); *y = t;
- 4, 一元*运算符;
- 5, it->member等价于(*it).member。(继承自双向迭代器)
- 除了双向迭代器支持的(p和q是迭代器,n是一个整数):
- 1 p += n, p -= n, p + n, p - n ,n + p,
- 2 p - q
- 3 p[n](与*(p+q)等价)
- 4 p < q( p > q, p <= q, p >= q)根据第一个由Strict Weakly Comparable条件可以得到括号里的所有。
- Strict Weakly Comparable是在operator<的基础上,满足:如果两个元素具有任何一个都不小于另一个的性质,那么它们视为具备了某种程度的等价关系是合理的。
示例标准库函数binary_search()【简化版:要求有随机访问迭代器】:
template<class Ran, class X>
bool binary_search(Ran begin, Ran end, const X& x){
while(begin < end){
//查找区间的中点
Ran mid = begin + (end - begin)/2;
//查看区间哪一部分含有x,只往下查找这一部分
if(x < *mid) end = mid;
else if(*mid < x) begin = mid + 1;
//得到了待查找的值,即*mid == x,完成查找
else return true;
}
return false;
}
为什么不编写(begin + end)/2以代替那个更为复杂的begin+(end- begin)/2?
原因:
- 因为(begin+end)/2中,begin和end都是迭代器,标准库中随机访问迭代器没有定义两个迭代器的加法运算,因此会导致错误。
- 而begin + (end - begin)/2中,end - begin会先计算出一个无符号整型值,然后除以二,再用无符号整型值加迭代器begin得到最终结果。
常见的还有sort函数。向量和字符串迭代器都是随机访问迭代器,而链表跌打器则不是这样的迭代器,它仅支持双向迭代器。
两个迭代器(p - q)相减会产生一个整数,表示p 和q指向的元素在容器中的间距。由于p-q可能是负值,因此它是一个带符号的整数类型。该类型到底是整型(int)还是长整型(long)取决于系统环境。标准库中在<cstddef>中提供了ptrdiff_t来表示这样的类型。
没有定义n - p的原因是这样的运算没有意义,在一个整数的基础上减去一个迭代器值(可以理解为位置),得到的是什么?这没有定义。
没有 p + q 的原因是两个迭代器值(位置)相加得到的的值也不具有实际的意义。
简写:
string::iterator iter;
iter beg;
//beg[-1]等价于*(beg-1)
iter i;
string sep;
//i[sep.size()] 等价于 *(i + seq.size())
这种写法仅支持随机访问迭代器(可以使用索引的,如向量或字符串),类似于数组的指针,p指向一个数组中第m个元素,那么p[m]就代表数组中第m+n个元素本身。
如果p是一个随机访问迭代器而n是一个整数,按么p[n]将于*(p+n)等价。
因为链表是为了快速插入和删除而被优化,无法快速定位到表中的任意元素,定位的唯一方法是按顺序查看每一个元素。不过list有自己的sort成员函数来进行排序,即list.sort()。
2.6 类别总结 (P22)
五种类型的迭代器总结:
迭代器类型 | 操作 |
输入迭代器 | 按一个方向顺序访问,只能输入 |
输出迭代器 | 按一个方向顺序访问,只能输出 |
正向迭代器 | 按一个方向顺序访问,既能输入也能输出 |
双向迭代器 | 按两个方向顺序访问,既能输入也能输出 |
随机访问迭代器 | 按两个方向随机访问,既能输入也能输出 |
2.6 完整示例程序
//迭代器种类探索
#include
#include
#include
using std::cin; using std::cout;
using std::endl;
using std::vector;
//顺序只读访问
template<class In, class X>
In find2(In begin, In end, const X& x){
while(begin != end && *begin != x){
++begin;
}
return begin;
}
//顺序只写
template<class In, class Out>
Out copy2(In begin, In end, Out dest){
while (begin != end)
*dest++ = *begin++;
return dest;
}
//顺序读写访问
template<class For, class X>
void repalce(For beg, For end, const X& x, const X& y){
while(beg != end){
if(*beg == x){
*beg = y;
}
++beg;
}
}
//可逆访问
template<class Bi>
void reverse2(Bi begin, Bi end){
while(begin != end){
--end;
if(begin != end){
std::swap(*begin++, *end);
}
}
}
//随机访问
template<class Ran, class X>
bool binary_search2(Ran begin, Ran end, const X& x){
while(begin < end){
Ran mid = begin + (end - begin) /2;
if(x < *mid){
end = mid;
}else if(*mid < x){
begin = mid + 1;
}else{
return true;
}
}
return false;
}
int main(int argc, char const *argv[])
{
// vectorv;
// for (int i = 0; i < 10; ++i) {
// v.push_back(i);
// }
std::string v = "abcdefg";
//测试 随机访问
if(binary_search2(v.begin(), v.end(), 'h')){
cout <<"Yes";
}else{
cout <<"No";
}
// //测试逆序访问
// std::string v = "abcdefd";
// reverse2(v.begin(), v.end());
// for (vector::size_type j = 0; j < v.size(); ++j) {
// cout << v[j] <<" ";
// }
// //测试 顺序读写
// repalce(v.begin(), v.end(), 2, 22);
// for (vector::size_type j = 0; j < v.size(); ++j) {
// cout << v[j] <<" ";
// }
//测试 顺序只写
// vectorv2;
// copy2(v.begin(), v.end(), std::back_inserter(v2));
// // std::back_inserter输出迭代器
// cout << v2.size();
//测试 顺序只读访问
// vectorv2;
// for (int j = 0.0; j < 10; j+= 1.0) {
// v2.push_back(j);
// }
//
//
// vector::const_iterator iter;
// iter = find2(v.begin(), v.end(), 8.0);
//
// vector::const_iterator iter2;
// iter2 = find2(v2.begin(), v2.end(), 8.0);
//
// if(iter2 != v2.end()){
// cout <<"Yes";
// }else{
// cout <<"No";
// }
return 0;
}
3 迭代器区间和越界值(P23-P24)
3.1 顺序访问
begin()、end()常与container<T>::iterator(或container<T>:::const_iterator)一起使用,来顺序访问容器中的元素。
例如:
vector<int> r(10, 100);
vector<int> v;
for (vector<int>::const_iterator i = r.begin(); i != r.end(); ++i) {
v.push_back(*i);
}
3.2 逆序访问
rbegin()、rend()常与container<T>::reverse_iterator(或container<T>:::const_reverse_iterator)一起使用,来沿相反的顺序访问容器中的元素。
从最后一个元素到第一个元素遍历容器。反向迭代器将自增(和自减)的含义反过来了
例如:
vector<int> r(10, 100);
vector<int> v;
for (vector<int>::const_reverse_iterator i = r.rbegin(); i != r.rend(); ++i) {
v.push_back(*i);
}
c.rbegin():表示指向容器中最后一个元素的迭代器;
c.rend():表示指向容器中第一个元素之前的那个位置的迭代器。
3.3 本质
在编写程序时,经常会看到使用end()来指向区间最后的元素的后面那个位置(即上界)。
我们要用指示了紧位于区间最后一个元素后面的那个位置的迭代器,而不是用一个直接指向最后的元素的迭代器标记区间终点?
原因:
- 1 简化程序设计。
- 如果区间根本没有元素,我们将无法找到一个最后的元素以标记终点。
- 这样的话,我们将不得不用一个迭代器来指明一个空区间,而这个迭代器将会紧位于区间开头之前的那个位置。
- 如果采取这样的策略,就必须将空区间和其他所有的区间都不同的特例来处理(类似与链表是否加头指针),这样会降低程序的可靠性以及使得难以理解。
- 2 可以比较迭代器来判断容器是否有元素。
- 如果我们用一个迭代器(紧位于区间最后一个元素后面的那个位置) 标记区间终点,那就可以用相等或不相等去比较迭代器,从而判断区间是否为空。
- 只有在两个迭代器(begin(),end())相等的情况下,区间才会为空。如果不相等,开始迭代器(begin())指向了一个元素。
- 例如;常用的判断
while(begin != end){//begin和end都是迭代器,区间[begin,end)
++begin;
}
- 3 能够以一种自然的方式来表示“区间之外”。
- 许多库算法都利用了“”区间之外“的值。它们返回区间的第二个迭代器来指示失败,如前面的库函数find()函数。
- 如果没有这个值,我们就必须创造一个(如string::npos,一个无符号整型,其值为-1或无符号整型的最大值),这样又增加算法以及使用算法的复杂度。
4 指针
4.1 概念(P25)
指针存放的是对象地址的值。
假设x是一个对象,那么&x就是该对象的地址;
如果p是一个对象的地址,那么*p就是该对象本身。
其中&是求地址运算符,*p中*是间接引用运算符。
可以将一个对象理解为一个只包含该对象一个元素的“容器”,而将指向该对象的指针理解成一个指向一个“容器”中唯一的元素的迭代器。
4.2 基本用法 P26
4.2.1 指向变量的指针
定义一个指针变量
//定义一个int型指针变量
int *p;
int* p;//C++程序员习惯用法
容易出错点:
下面写法中,实际上定义了一个int*类型的指针p,以及定义一个int类型的q变量。
int* p, q;
示例程序:
#include
using std::cout;
using std::cin;
using std::endl;
int main(int argc, char** argv){
int x = 5;
int* p = &x;//指向x地址的指针
cout << "x = " << x << endl;
*p = 6;
cout << "x = " << x <<endl;
return 0;
}
上面的程序中,一旦p存储了x的地址,*p和x将是指向同一对象的两种完全等效的方法。
4.2.2 指向函数的指针
下面的代码中,*p具有int类型,p是一个指针。
int *p;
下面的代码中,我们间接引用了fp,调用它是以一个int型变量作为参数,返回结果也具有int型。也就是说,fp是一个指向具有一个int类型参数并返回int类型结果的函数的指针。
int (*fp)(int);
4.2.3 指向函数指针的原理
函数不是对象,无法对其进行复制或者赋值,也无法将它们直接作为参数。特别是在程序中无法创建或者修改函数(只有编译器可以)。
一个程序对函数进行的全部操作只有调用该函数或者得到它的地址。
如果在任何地方出现一个函数名而不是调用该函数时,即使没有显示使用&声明,编译器都会将它解释为该函数的地址。
如我们有一个与fp函数类型匹配的函数
int next(int n){
return n + 1;
}
那么下面任何一种写法都是等价的
int (*fp)(int);
fp = &next;//与下面等价
fp = next;
如果有一个int型变量i,可以通过fp函数调用next来让i加1,下面两种实现方法等价:
int i;
i = (*fp)(i);//与下面等价
i = fp(i);
完整示例程序:
#include
using std::cout;
using std::cin;
using std::endl;
int next(int n){
return n + 1;
}
int main(int argc, char** argv){
int (*fp)(int);
fp = &next;//将fp指向next函数
//fp = next;
int i = (*fp)(6);
// int i = fp(6);
cout << i <<endl;
return 0;
}
如果编写一函数,表面上这个函数以另一函数为参数,编译器会在背后悄悄将该参数转换为一个指向函数的指针。
例如:
下面两种写法是等价的:
void write(ostream& , int test(int ), int )
void write2(ostream& , int (*test)(int ), int )
完整示例程序:
//返回指向函数的指针
#include
#include
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
int test1(int a){
return a + 1;
}
int test2(int b){
return b + 3;
}
void write(ostream& out, int test(int ), int a){
out << test(a) << endl;
}
void write2(ostream& out, int (*test)(int ), int a){
out << test(a) << endl;
}
int main(int argc, char** argv){
int x = 10;
write(cout, test1, x);
write(cout, test2, x);
write2(cout, test1, x);
write2(cout, test2, x);
return 0;
}
但是这一转变对于函数的返回值却不会自动执行。如果我们想写一个返回类型为指向函数的指针的函数,其返回类型要求和与write2函数的类型相同,就必须显示声明返回一个指针。
下面两种显示方法相同:
typedef int (*test_fp)(int );
fp test();
等价于
int (*test())(int );
作为结果调用test()函数并且间接引用结果,那么将会得到一个返沪类型为int,以int类型作为参数的函数
两种声明方式,返回一个指向带有int引用型变量参数的函数的指针:
//声明
typedef int (*test_fp)(int& );
test_fp test3(int&);
等价于下面的
//声明
int (*test4(int& ))(int& );
完整代码:
//返回指向函数的指针
#include
#include
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
int test1(int& a){
a++;
return a;
}
//声明
typedef int (*test_fp)(int& );
test_fp test3(int&);
//定义
test_fp test3(int& x){
int (*fp)(int& ) = &test1;
(*fp)(x);//x的值变为11
return fp;
}
//声明
int (*test4(int& ))(int& );
//定义
int (*test4(int& x))(int& ){
int (*fp)(int& ) = &test1;
(*fp)(x);//x的值变为11
return fp;
}
void write2(ostream& out, int test(int& ), int a){
out << test(a) << endl;
}
int main(int argc, char** argv){
int x = 10 ;
//test传递的时引用会导致x变化,write传递的没哟引用,计算出值后会被丢弃
//在10的基础上加2
//第一调用test时加1,由于是引用,x会实际变为11,第二次调用write时加1,虽然结果是12,但是x值还是11
write2(cout, test3(x), x);
write2(cout, test4(x), x);//在11的基础上加2
return 0;
}
4.3 作为库算法的指针 P27
库算法中指向函数的指针常被用于另一个另一函数的参数
template<class In, class Pred>
In find_if(In begin, In end, Pred f){
while(begin ! = end && !f(*begin)){
++begin;
}
return begin;
}
当f(**begin)具有一个意义的值时,Pred可以时任何类型。先假设一个判断函数,定义如下:
bool is_negative(int n){
return n < 0;
}
我们用find_if找向量v中第一个负值的元素
vector<int>::iterator i = find_if(v.begin(), v.end(), is_negative);
我们可以将&is_negative写成is_negative,编译器会自动将函数名转化成指向函数的指针。
在find_if函数的实现代码中也可以将(*f)(*begin)写成f(begin),编译器会对函数指针的调用自动解释为调用该指针指向的函数。
4.4 空指针 P28
指针类型的局部变量在被赋值之前没有任何有意义的值。
通常用0来初始化指针变量,这是由于将0转换成指针值可以确保产生一个与指向具体对象的指针不同的值。
常量0也是唯一可以用于转换成指针类型的整型值。将0转换成的指针类型值为空指针。
4.5 指针算法
指针是一个随机存储的迭代器,因此有
随机存储的迭代器的必要条件(p和q是迭代器,n是一个整数):
- p + n, p - n , n + p
- p - q
两个迭代器(p - q)相减会产生一个整数,表示p 和q指向的元素在容器中的间距。由于p-q可能是负值,因此它是一个带符号的整数类型。该类型到底是整型(int)还是长整型(long)取决于系统环境。标准库中在<cstddef>中提供了ptrdiff_t来表示这样的类型。
除了指针的加法外,指针可以做减法,指针可以和整数做加、减法。
对于数组
double a[3];
a + 3是一个有效指针,尽管它不指向数组a中的任何一个元素。类似于string和vector这两个容器,对于一个只有n个元素的数组的首地址加n得到一个新地址,该地址不指向数组中的任何对象,但它是有效的。
对于与指针p,整数n相关的p,p+n,p-n,即使它们有些可能会超过数组的地址范围,都是有效的,只是不可预测而已。
对指向一个容器前面的地址的迭代器进行计算是不允许的。类似的对于数组前面的地址进行计算被视为是非法的。
5 作业 P29
输入迭代器:
equal(b,e,b2)
三个参数都是输入迭代器
accumulate(b,e,t)
头在#include <numeric>
库算法连接一个vector对象中的全部元素
vector<string> v = {"i", "love", "you"};
string str;
str = accumulate(v.begin(), v.end(), str);
前两个参数为输入迭代器,最后一个为模版T类型
输出迭代器:
remove_copy(b,e,d,p)
将[b,e)区间内的序列复制全部不等于p的元素到由d指示的目的地中。
前两个为输入的单曲,第三个为输出迭代器,最后一个为值
正向迭代器:
search(b,e,b2,e2)
判断模式串字符串pattern是否是文本字符串text的子串.前两个区间是文本串,后两个是模式串。(提示使用模式匹配算法【朴素算法,不需要用带nextval的KMP算法】)
四个参数都是正向迭代器
双向迭代器:
partition(b,2,p)
以谓词p为基础划分在区域[b,e}中的元素,使那些使谓词为真的元素拆容器的开头。(使用swap)
前两个参数为正向迭代器,最后一个为谓词。
谓词(将调用的每个参数返回bool值)
随机访问迭代器:
Nth_element(b,n,e)
重新排序,保证在[n,e)内元素没有任何一个元素小于[b,n)区间内的元素。
三个参数都是随机访问迭代器