《C++Primer》再学笔记

using声明

作用域操作符(::)的含义是:编译器应从操作符左侧的作用域中寻找右侧那个名字。

using声明:

 using namespace::name;

每个using声明引入命名空间的一个成员。
当我们使用名字name时,从命名空间namespace中获取。如:

using std::cin;
cin>>str;

若无此声明,则我们在使用时需显示声明:

std::cin>>str;

什么是命名空间?

在编程语言中,命名空间是一种特殊的作用域,它包含了处于该作用域中的所有标示符,而且其本身也是由标示符表示的。
命名空间的使用目的是为了将逻辑相关的标示符限定在一起,组成相应的命名空间,可使整个系统更加模块化,最重要的是它可以防止命名冲突。就好比在两个函数或类中定义相同名字的对象一样,利用作用域标示符限定该对象是哪个类里定义的。

std的由来:

因为标准库非常的庞大,所程序员在选择的类的名称或函数名时就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。

故而不推荐使用using namespace std;声明。

头文件不应包含using声明:

头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。由此可能会产生名字冲突。

标准库类型string

string表示可变长的字符序列,使用时需包含头文件:

#include<string>
using std::string;

初始化

char array[100];        //字符数组
string s1;             //默认初始化为空串
string s2(array);       //拷贝array中的字符直到遇到空字符'\0',若无且未指定拷贝大小则构造函数行为未定义
string s2(array,n);     //拷贝初始化数组array的前n个字符,此数组至少包含n个字符
string s2(s1);         //直接初始化为s1的副本
string s2(s1,pos);      //拷贝初始化s1从下标pos开始的字符串,若pos>s1.size(),则构造函数行为未定义
string s2(s1,pos,len);  //拷贝初始化s1从下标pos开始的len个字符。不管len的值为多少,至多拷贝s1.size()-pos个字符
string s2=s1;          //拷贝初始化为s1的副本
string s3("value");    //直接初始化为字面值"value"的副本,但不含最后'\0'字符
string s4(n,'a');      //把s4拷贝初始化为由连续n个字符'a'组成的串

常用操作

os<<s                   //将s写到输出流os中,返回os
is>>s                   //将is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is,s)           //从is中读取一行(无论是否有空格)赋给s,返回is
s.empty()               //判断是否为空串
s.size()                //返回字符串长度
s1==s2                  //若s1与s2字符完全相同(区分大小写),则相等
s.substr(pos,n)         //返回s的从pos开始的n个字符的拷贝string对象。pos默认0,n默认s.size()-pos

is>>s中string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处的空白为止。如:

string s;
cin>>s;         //若输入"      Hello World!       "
cout<<s;        //输出的是"Hello"
//读取未知数量的string对象
while(cin>>s) {//对字符串执行某种操作}

上述循环条件负责在读取时检测流的情况,若流有效即未遇到文件结束标记或非法输入,则执行while语句内部的操作。

//利用getline读取未知数量的string对象
while(getline(cin,s)) {//对字符串执行某种操作}

getline()函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入string对象中(但并不包含换行符!)。getline()只要一遇到换行符就结束读取操作,哪怕一开始就是换行符。

注意size()函数返回的是一个string::size_type 类型的值,这种配套类型体现了标准库类型与机器无关的特性,该类型是一个无符号类型的值且能足够存放下任何string对象的大小。所有用于存放string类的size函数返回值的变量,都应该是该类型的。所有string对象的索引若为带符号类型也将自动转换为该类型。故而:

如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题

string对象比较:
1. 若两string对象s1,s2的长度不同即是s1.size()<s2.size(),而且较短string对象s1的每个字符都与较长string对象s2对应位置上的字符相同,则s1<s2
2. 若两string对象在某些对应的位置不一致,则比较结果是第一对相异字符比较的结果

string对象相加:
标准库允许字符字面值和字符串字面值转换成string对象,故而允许字面值和string对象相加,但必须确保每个加法运算符两侧的运算对象至少有一个是string,即

string s1=s+" World!";          //正确
string s2="Hello"+" World!" ;   //错误。两个运算对象都不是string
string s3=s+"Hello"+" World!";  //正确,等价于s3=(s+"Hello")+" World!"
string s4="Hello"+" World!"+s;  //错误,等价于s4=("Hello"+" World!")+s

单个字符操作:

#include<cctype>
ispunct(s[i]);      //当s[i]是标点符号时为真
isspace(s[i]);      //当s[i]是空白时为真,即s[i]是空格、横向制表符、纵向制表符、回车符、换行符、进纸符等
tolower(s[i]);      //若s[i]是大写字母,输出对应小写字母,否则原样输出
toupper(s[i]);      //若s[i]是小写字母,输出对应大写字母,否则原样输出

范围for语句:

for(auto c:str){//...}      //c是str每个元素的拷贝
for(auto &c:str){//...}     //c是str每个元素的引用

插入删除操作:

s.erase(pos,len)                //删除s的pos处开始的len个(默认到结尾)字符
s.insert(pos,num,'c')           //在pos处插入num个字符'c'
s.insert(pos,s1,start,len)      //在pos处插入s1中从start(默认0)开始的len长(默认s1.size())的拷贝,
s.assign(array,num)             //把字符数组array中的前num个(默认'\0'结束)字符赋给s
s.aasign(s1,pos,len)            //把s1从pos处(默认0)开始的len长(默认s1.size())的string对象赋给s
s.assign(num,'c')               //把num个字符'c'赋给s
s.append(s1,pos,len)            //把s1从pos处默认0)开始的len长(默认s1.size())的字符追加到s末尾
s.append(array,num)
s.replace(start,end,s1,pos,len) //用s1pos处默认0)开始的len长(默认s1.size())的字符替换sstartend处的字符
s.replace(start,end,array,num)

搜索操作:

s.find(c,pos)                   //s中从pos处(默认0)开始查找字符c第一次出现的位置
s.find(array,pos)               //s中从pos处(默认0)开始查找字符串array第一次出现的位置
s.rfind(c,pos)                  //s中从pos处(默认0)开始查找字符c最后一次出现的位置
s.rfind(array,pos)              //s中从pos处(默认0)开始查找array最后一次出现的位置
s.find_first_of(array,pos)      //s中从pos处(默认0)开始查找array中任何一个字符第一次出现的位置
s.find_last_of(array,pos)       //s中从pos处(默认0)开始查找array中任何一个字符最后一次出现的位置
s.find_first_not_of(array,pos)  //s中从pos处(默认0)开始查找第一个不在array中的字符的位置
s.find_last_not_of(array,pos)   //s中从pos处(默认0)开始查找最后一个不在array中的字符的位置

数值转换:

to_string(val)      //返回val的string表示,val可以是任何算术类型
stoi(s,pos,base)    //返回s的int数值表示,p为size_t的指针保存s中第一个非数值字符下标,默认0,base表s中数值的基数,默认10
stoul(s)            //返回s的unsigned long数值表示
stod(s,p)           //返回s的double数值表示
stof(s)             //返回s的float数值表示

注:

string str="2018,Now";
string::size_type lost;
int num=stoi(str,&lost);    //得到num=2018,lost=",Now"

标准库类型vector

#include<vector>
using std::vector;

vector表示对象的集合,是模版而非类型,由vector生成的类型必须包含vector中元素的类型。

模版本身不是类或函数,编译器根据模版创建类或函数的过程称为实例化,当使用模版时,需要指出编译器应把类或函数实例化成何种类型。

初始化:

vector<T> v1;           //空vector,默认初始化
vector<T> v2(v1);       //v2是包含v1所有元素的副本,两vector元素类型必须相同
vector<T> v3(n,val);    //v3包含了n个重复元素,每个元素的值为val
vector<T> v4(n);        //v4包含了n个值初始化的对象,注意T必须支持默认初始化
vector<T> v5{a,b,c};    //列表初始化

事实上,vector对象能高效地增长,在定义对象时设定其大小没什么必要

添加元素:

v.push_back(val);   //把值val压入vector的尾端

注意若循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环

常用操作:

v.empty()           //判断是否为空
v.size()            //返回v中元素的个数,返回类型为vector<T>::size_type
v1==v2              //当且仅当v1与v2的元素数量相同且对应位置的元素值都相同
v1<v2               //比较的是vector的元素
v[n]                //下标访问v的第n+1个元素,只可用于访问已存在的元素,不能用于添加元素

对象增长机制(适用vector和string):
vector通常会分配比需求更大的内存空间,系统预留这些空间作为备用用来保存更多的新元素,只有当不得不获取新的内存空间时系统才重新分配更大的内存空间并移动所有元素。

v.capacity()            //内存容量,v现可以保存多少元素,空容器大小为0
v.reserve(n)            //预分配至少能容纳n个元素的内存空间,并不改变容器中元素的数量
v.shrink_to_fit()       //将内存容量减少为size()相同的大小

只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。若需求大小小于或等于当前容量,reserve什么都不做,且容器不会退回内存空间。注意capacity的大小永远大于等于传入reserve的参数n的大小。

reserve永远不会减少容器占用的内存空间。类似的,resize成员函数只改变容器中元素的数目。
capacity表示在不分配新的内存空间的前提下它最多可保存多少元素。size指已经保存的元素数目。

我们可以调用shrink_to_fit()来退回不需要的内存空间,,但这只是一个请求,标准库并不保证退还内存。

迭代器

一般来说,我们不清楚迭代器的具体类型是什么。但迭代器的对象是容器中的元素或string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。

auto b=v.begin();       //返回指向第一个元素的迭代器
auto e=v.end();         //返回指向容器尾元素的下一个位置的迭代器

一般来说拥有迭代器的标准库类型使用iterator和const_iterator:

vector<int>::iterator iter;
vector<int>::const_iterator iter;

begin和end返回的具体类型由对象是否是常量决定,若对象是常量,begin和end返回const_iterator,若对象不是常量,返回iterator

auto cb=v.cbegin();     //返回指向第一个元素的const_iterator
auto ce=v.cend();       //返回指向容器尾元素的下一个位置的const_iterator

常用操作:

*iter               //返回迭代器iter所指元素的引用
iter->mem           //解引用iter(必须合法且确实指向一个元素)并获取该元素的名为mem的成员,等价于(*iter).mem
++iter              //令iter指向容器中的下一个元素
--iter              //令iter指向容器中的上一个元素
iter1==iter2        //若两迭代器指示的是同一个元素则相等

迭代器这个名词有三种不同的含义:迭代器概念本身;容器定义的迭代器类型;某个迭代器对象。

注意:

凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素!否则迭代器会失效
如在for循环中向vector对象添加元素或任何一种可能改变vector对象容量的操作

vector和string迭代器支持的额外操作:

iter+n          //迭代器后的第n个元素
iter-n          //迭代器前的第n个元素
iter+=n         //迭代器向后移动n个元素
iter-=n         //迭代器向前移动n个元素
iter1-iter2     //两迭代器间的距离,返回类型为difference_type的带符号整数类型
iter1<iter2     //迭代器1位于迭代器2之前

额外的四种迭代器:
1.插入迭代器:
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。

it=t;           //在it指定的当前位置插入值t。

假定c是it绑定的容器,依赖插入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t)或c.insert(t,pos)

插入器有三种类型,差异在于元素插入的位置:

  • back_inserter:创建一个使用push_back的迭代器
  • front_inserter:创建一个使用push_front的迭代器
  • inserter:创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前

当调用inserter(c,iter) 时,我们得到一个迭代器,接下来使用它时会将元素插入到iter原来所指向的元素之前的位置,即 *it=val;等价于

it=c.insert(it,val);        //it指向新加入的元素
++it;                       //递增it使它指向原来的元素

但当我们使用front_inserter时,元素总是插入到容器的第一个元素之前,也就是说it并不会递增到原来的位置,而是一直处于第一个元素处。

2.流迭代器
流迭代器不支持递减操作。

  • istream_iterator读取输入流
  • ostream_iterator向一个输出流写数据

这些迭代器将它们对应的流当做一个特定类型的元素序列来处理。

istream_iterator<int> inIter(cin);      //从cin读取int
istream_iterator<int> eof;              //当关联流遇到文件尾或遇到IO错误,迭代器的值就与eof尾后迭代器相等
while(inIter!=eof){
    //后置递增运算读取流,返回迭代器的旧值
    //解引用迭代器,获得从流读取的前一个值
    v.push_back(*inIter++);
}

上述代码可简单直接写成如下形式

istream_iterator<int> inIter(cin),eof;
vector<int> v(inIter,eof);

当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。

对于ostream_iterator,必须绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。

ostream_iterator<T> out(os);    //out将类型为T的值写到输出流os中
ostream_iterator<T> out(os,d);  //写值时每个值后面都输出一个d,d指向一个空字符结尾的字符数组
out=val;                        //用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容
ostream_iterator<int> outIter(cout," ");    //每输出一个值后面就加一个空格
for(auto e:v)
    outIter=e; //赋值语句实际上将元素写到cout

上述代码可简单直接写成如下形式

copy(v.begin(),v.end(),outIter);

3.反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增递减的含义会颠倒过来:

it++;       //移动到前一个元素
it--;       //移动到后一个元素

我们可以通过调用rbegin,rend,crbegin,crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。

4.移动迭代器
用于移动其中元素的迭代器。

lambda表达式

对于一个对象或表达式,若可以对其使用调用运算符,则称它为可调用的。可调用对象包括函数、函数指针、重载了函数调用运算符的类以及lambda表达式

一个lambda表达式表示一个可调用的代码单元。它具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义的函数内部,其具有如下形式:

[capture list](parameter list)->return type{function body}
[捕获列表] (参数列表) -> 返回类型 {函数体}

可以忽略参数列表和返回类型,但必须包括捕获列表和函数体,另外lambda表达式必须使用尾置返回来指定返回类型。

auto f=[]{return 42;};      //定义一个可调用对象f,f不接受参数
cout<<f();                  //输出f()的值

在上述例子中,忽略括号和参数列表等价于指定一个空参数列表。空捕获列表表明此lambda不使用它所在函数中的任何局部变量。若忽略返回类型,lambda根据函数体中的代码推断出返回类型。若lambda的函数体包含任何单一return语句之外的内容且未指定返回类型,则返回void

调用一个lambda时给定的实参被用来初始化lambda的形参,但与普通函数不同,lambda不能有默认实参,因此一个lambda调用的实参数目永远与形参数目相等。

//传入lambda表达式作为compare函数
sort(v.begin(),v.end(),
        [](const int &a,const int &b
            {return a>b;});

一个lambda只能在函数体中使用那些明确指明的局部变量,一个lambda通过将局部变量包含在其捕获列表中来指出将来会使用这些变量,捕获列表指引lambda在其内部包含访问局部变量所需的信息。变量的捕获方式可以是值也可以是引用。

当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的

当定义一个lambda时,编译器生成一个与lambda对应的新的未命名的类类型。当向一个函数传递lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员,在lambda对象创建时被初始化。

除了显示列出捕获列表外,还可以指示编译器推断捕获列表,即在捕获列表中写一个&表引用方式或=表值方式:

int x=2018;                     //函数中一局部变量
auto f1=[x]{reutrn x;}          //值捕获方式,lambda不能改变x的值,除非在参数列表后加上关键字mutable
auto f2=[&x]{return ++x;}       //引用捕获方式,能否修改x的值依赖于x是否为const类型
auto f3=[=]{return x;}          //让编译器以值捕获方式推断捕获列表

int y=123;                      //另一个局部变量
auto f4=[=,&y]{return x+y;}     //混合使用隐式和显示捕获

注意混合使用时捕获列表中的第一个元素必须是&或=,且显示捕获的变量必须使用与隐式捕获不同的方式。

对于一个值被拷贝的变量,lambda不会改变其值,如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。

auto f5=[x]()mutable{return ++x;}   //值捕获,但lambda可以修改局部变量x的值

注意lambda表达式只能包含单一的return语句:

//正确,只有一个return语句
auto f6=[](const int i)
            return i<0?-i:i;});
//错误,有两个return语句不能推断lambda的返回类型
auto f7=[](const int i)             
            {if(i<0) return -i;
             else return i;});
//正确,指定了lambda返回类型
auto f8=[](const int i)->int
            {if(i<0) return -i;
             else return i;});

lambda的实现:
当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符,如:

sort(str.begin(),str.end(),
        [](const string &a,const string &b)
            {return a.size()<b.size();});
//其中的lambda类似于下面这个类
class SortForString{
public:
    bool operator()(const string &s1,const string &s2) const 
        {return s1.size()<s2.size();}
};
//使用类替代lambda表达式
sort(str.begin(),str.end(),SortForString());

产生的类只有一个函数调用运算符成员,它负责接受两个string并比较它们的长度,它的形参列表和函数体与lambda表达式完全一样。另外默认情况下lambda不能改变它捕获的变量,因此在默认情况下由lambda产生的类当中的函数调用运算符是一个const成员函数,若lambda被声明成可变的,则调用运算符就不是const的了。

当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引的对象确实存在,因此编译器可以直接使用该引用而无须再lambda产生的类中将其存储为数据成员。相反通过值捕获的变量被拷贝到lambda中,因此这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。

auto f9=[x](const int &y){return x+y;}
//lambda产生的类形如
class GetX{
private:
    //该数据成员对应通过值捕获的变量
    int x;
public:
    //构造函数形参对应捕获的变量
    GetX(int xx):x(xx){}
    //调用运算符的返回类型、形参和函数体都与lambda一致
    int operator()(const int &y) const
        {return x+y;}
};

bind函数

bind可看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表

#include<functional>
using std::bind;
//一般形式
auto newCallable=bind(callable,argList);

其中newCallable本身是可调用对象,argList是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable并传递给它argList中的参数。

argList的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“站位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1表示newCallable的第一个参数,_2为第二个参数,以此类推。

#include<functional>
using std::bind;
//注意名字_n都定义在std下的placeholders的命名空间中
using namespace std::placeholders;
//原可调用对象,需要两个参数
bool compare(const string &str,const unsigned int len){
    return str.size()>len;
}
//利用bind函数
auto newCompare=bind(compare,_1,6);
//此bind调用只有一个占位符表示newCompare只接受单一参数
//占位符出现在argList的第一个位置表示接受的参数对应compare的第一个参数
//argList第二个参数6表示传给compare的第二个参数永远是6

string str="Hello";
bool b=newCompare(str);     //newCompare会调用compare(str,6)
//利用bind替代lambda
sort(str.begin(),str.end(),bind(compare,_1,strLen));

注意传递给newCallable的参数按位置绑定到占位符上:

//g有两个参数
auto g=bind(func,a,b,_2,c,_1);
//g将它自己的参数作为第三个和第五个参数传递给func
//func的第1、2、4个参数分别被绑定到给定的值a、b、c上
//注意g的第一个参数绑定到_1,第二个参数绑定到_2,即
g(_1,_2);
//实际映射为
func(a,b,_2,c,_1);

注意对于bind中需要以引用方式传递的参数,需要使用标准库中的ref函数:

auto g=bind(func,ref(a),b,_2,c,ref(_1));        //这里参数a和第一个参数要求以引用方式传递

函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。

struct absInt{
    //函数对象类中通常包含其它成员
    int num;
    //重载函数调用运算符,该运算符接受一个int并返回其绝对值
    int operator()(int val) const{
        return val<0?-val:val;
    }
};
//使用对象
int i=-10;
absInt obj;             //定义含有函数调用运算符的对象
cout<<obj(i);           //将i传递给obj.operator()

函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别

如果类定义了调用运算符,则该类的对象称作函数对象。

function类型:

#includ<functional>
function<T> f;          //f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同
function<T> f(nullptr); //显示地构造一个空function
function<T> f(obj);     //在f中存储可调用对象obj的副本
f(args);                //调用f中的对象,参数是args

声明一个function类型,接受两个int并返回一个int的可调用对象

function<int(int,int)> f1=add;              //函数指针
function<int(int,int)> f2=divide();         //函数对象类的对象
function<int(int,int)> f3=[](int i,int j)   //lambda表达式
                            {return i*j;};
cout<<f1(4,2);      //使用可调用对象,参数为两int,返回值为一int

把调用形式相同的可调用对象存入map中,注意虽然调用形式都为int(int,int),但三个可调用对象的类型是不同的

//把可调用对象的类型各不相同的变量存储在同一个function<int(int,int)>中
map<string,function<int(int,int)>> binops={
    {"+",add),
    {"/",divide()},
    {"*",[](int i,int j){return i*j;}}
    {"%",mod}};
//使用
binops["+"](10,5);

但注意不能直接将重载函数的名字存入function类型的对象中,解决二义性问题时应存储函数指针或lambda表达式。

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/80158454