C++ Primer 学习(第三章)

1.命名空间的using声明

使用using namespace::name的形式,就可以直接访问命名空间中的名字,而不需要在加(形如命名空间::)前缀。例如,在程序中声明:

using std::cin;

其中,std即namespace,命名空间;name即cin,成员名。声明了这句话之后,以后不需要再写std::cin,只需要cin即可。

另外,头文件当中不应包含using的声明,因为头文件的的内容会被拷贝到所有引用它的文件中去,如果某个头文件里含有using声明,那么每个使用了该头文件的文件就都有了这个声明。对于某些程序来说,不经意的包含了一些名字,反而可能引起一些始料未及的名字冲突。

2.直接初始化和拷贝初始化

如果使用了等号(=)初始化一个变量,实际上执行的是拷贝初始化。反之,如果不使用等号,则执行的是直接初始化。

例如:

string s1="Hello,World!";//拷贝初始化,因为用了等号(=)
string s2("Hello,World!");//直接初始化,因为没有用等号(=)

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

例如:

#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main()
{
	string s;
	cin >> s;
	cout << s<< endl;
	return 0;
}

如果程序的输入为"   hello world",则输出为"hello",如下图所示。

扫描二维码关注公众号,回复: 3204971 查看本文章

4.使用getline函数读取一整行

由3可知,string的输入在遇到空白符就会停止,而有时候我们希望保留程序中输入的空白符,这时我们可以使用getline函数代替<<运算符。其中,getline函数的参数为一个输入流和一个string对象。例如:

string line;
while (getline(cin, line));

在上述代码中,cin就是一个输入流,line是一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到string对象中去,在本例中即line(注意尽管换行符被读进来了,但是保存的内容中不存在换行符,换行符在返回结果的时候被丢弃了)。另外,getline函数只要一遇到换行符就结束读取操作并返回保存结果,即使一开始输入就是换行符

5.标准类型库string中含有size函数,该函数返回的是一个string::size_type类型的值,而不是int或者unsigned类型。string::size_type是一个无符号整型数,但是它与unsigned是有所不同的,其要求能够足够存放下任何string对象的大小。所以如果一条表达式中已经有了size()函数,就不要再使用int了,这样可以避免有符号数与无符号数同时在一个表达式中带来的问题。另外,允许编译器使用auto或者decltype来推断变量的类型。

6.两个string对象可以直接相加,也允许字面值(字符字面值' '或字符串字面值" ")和string对象相加,字面值与string对象相加字面值会转换成string对象,但是不允许两个字面值直接相加。例如:

string s1 = "hello", s2 = "world";
string s3=s1+s2;//两个string对象直接相加
string s4 = s1 + "," + s2 + '!';//字面值与string对象相加,包括字符字面值、字符串字面值与string对象相加
string s5="hello"+","+s2;//错误,两个字面值不能直接相加

注意,当string对象与字面值相加的时候,必须保证每个加法运算符(+)的两侧至少有一个为string对象。上述代码第3行,s1+','会率先转化成string对象,所以是合法的,而第4行(+)两侧都是字面值,所以是不合法的。

7.因为C++是在C的基础上改进的,所以必须与C兼容。所以C++语言中的字符串字面值并不是标准库类型的string对象。切记,字符串字面值与string是不同的类型。另外,C++也兼容了C语言的标准库,在C语言中头文件的形式为name.h,C++则把这些文件命名为cname,去掉了.h后缀,而在文件名name之前添加字母c,但实际上里面的内容是一样的。例如,C语言中头文件ctype.h,在C++中则为cctype,这两者内容是一样的。但是特别的,在名为cname的头文件中定义的额名字从属于命名空间std,标准库中的额名字总能在命名空间std中找到,而定义在名为.h的头文件则不然。

8.基于范围的for语句,语法形式:

for(declaration:expression)

其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列的基础元素。每次迭代,declaration部分的变量会初始化为expression部分的下一个元素值。

例如:

string str("Some string");
for(auto c:str)
  cout<< c <<endl;

上述代码的作用就是每行输出str中的一个字符。str中的字符分别从S到g逐次的拷贝给c,在这里,c的类型是它所拷贝的字符的类型,所以c的类型为char。

9.如果想要改变string对象中的值,需要将循环变量设置为引用类型。例如:

string s("Hello World!");
for(auto &c:s)
c=toupper(c);
cout<<s<<endl;

上述代码的作用是将字符串改写成大写的形式。只有当使用引用的时候,循环控制变量被依次绑定到了序列的每个元素上。如果把上述代码的第二行改成for(auto c:s),那么改变的是循环变量本身,而不是字符串s,并不能达到改变字符串s的作用。

10.注意下述代码:

const string s="Keep out!";
for ( auto &c:s)
{/*...*/}

上述代码是合法的,但是一旦在花括号中对c进行赋值,程序就不合法了。因为对于类型为auto的引用,其顶层const属性仍会保留,所以c的类型是const char&,因此不能对c进行赋值。

11.vector是一种标准库类型,标准库类型vector表示对象的集合,其中所有对象的类型都必须相同。vector是一种类模板而非类型,对于类模板来说,编译器会根据类模板提供的信息创建具体的类或者函数,模板提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。以vector为例,该类模板提供的信息是vector内所存放对象的类型,例如:

vector<int> ivec;//ivec保存int类型的对象
vector<Sales_item> Sales_vec;//保存Sales_item类型的对象
vector<vector<string>> file;//该向量的元素是vector的对象

vector能容纳大多数类型的对象作为其元素,但是由于引用不是对象,所以不存在包含引用的vector

12.通常情况下,对于vector对象的初始化,可以只提供vector对象容纳的元素数量而略去初始值,此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。但有两种特例:

一、有些类要求必须明确地提供初始值,对于这种类型的对象来说,只提供元素数量而不设定初始值无法完成初始化工作。

二、如果只提供了元素数量而没有设定初始值,只能使用直接初始化,不能使用拷贝初始化。例如:

vector<int> vi=10;//错误:必须使用直接初始化的形式指定向量的大小

上述代码,本意是想创建10个值初始化了的元素的vector对象,而非把10拷贝到vector当中,因此这个地方不能用拷贝初始化,而应该使用直接初始化。

13.在某些情况下,初始化的真实含义表示的是列表初始值还是元素数量依赖于传递初始值时用的是花括号还是圆括号。

如果使用的是圆括号,可以说提供的值是用来构造vector对象的。

如果使用的是花括号,可以表述成我们想列表初始化该vector对象。也就是说,初始化过程中会尽可能地把花括号内的值当成是元素初始值的列表来处理。

例如:

vector<int> v1(10);//v1中有10个元素,每个元素的值都是0
vector<int> v2{10};//v2中有1个元素,该元素的值是10
vector<int> v3(10,1);//v3中有10个元素,每个元素的值都是1
vector<int> v4{10,1};//v4中有2个元素,值分别为10和1

上述代码可以看出,圆括号是尽可能地构造vector对象,所以其第1个参数10都表示构造的对象中包含10个元素。花括号是尽可能地把括号内的值当成是元素的初始值。

另一方面,当使用花括号的形式但是提供的值不能用来列表初始化的时候,就要考虑这样的值是用来构造vector对象了。

例如:

vector<string> v5{10};//v5中有10个默认初始化的元素
vector<string> v6{10,"hi"};//v6中有10个值为"hi"的元素

显然,要想列表初始化vector对象,花括号里的值必须与元素类型相同,而显然不能用int初始化string对象,所以v5、v6提供的值不能作为元素的初始值,编译器会尝试用该值去构造vector对象。

另外,下面的代码也要注意区分:

vector<string> v7{"hi"};//列表初始化,v7只有一个元素
vector<string> v8("hi");//错误,不能使用字符串字面值构建vector对象

上述代码第2行是不合法的,不能使用字符串字面值构建vector对象。

13.C++标准要求vector应该能够在运行时高效快速地添加元素,因此一般来说,定义vector对象的时候无需指定大小,只有一种例外情况就是所有的元素值都相同。而且很多时候我们也不知道对象的元素个数,那么此时如何添加元素呢?C++中vector有成员函数push_back ,该函数负责把一个值当成vector对象的尾元素压到vector对象的尾端。

需要注意的是,由于能够高效便捷地向vector对象中添加元素,很多编程得以简化,然而其也对编写程序提出了更高的而要求。其中一条就是必须确保所写的循环正确无误,特别是在循环有可能改变vector对象容量的时候。如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环,所以范围for语句体内不应改变其所遍历序列的大小。

14.vector也有成员函数size;其返回值的类型是由vector定义的size_type类型。要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型。例如:

vector<int>::size_type    //正确
vector::size_type         //错误

上述第2行代码错误的原因在于它没有指定是由哪种类型定义的。

15.不能使用下标的形式向vector对象添加元素,只能使用下标访问已经存在的元素。如下代码:

vector<int> ivec;
for(decltype(ivec.size()) ix=0;ix!=10;++ix)
ivec[ix]=ix;

 上述代码是错误的,因为ivec是一个空vector,根本不包含任何元素,当然不能使用下标去添加元素。正确的方法是使用push_back函数。

16.迭代器可用于访问容器中的元素或者在元素之间移动。所有标准库容器都可以使用迭代器,但是其中只有少数几种支持下标运算符。迭代器有自己的类型同时也能够返回迭代器的成员。迭代器都拥有名为beginend的成员。其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器,end成员负责返回指向容器(或string对象)尾元素的下一位置的迭代器。标准库类型使用iteratorconst_iterator来表示迭代器的类型。beginend返回的具体类型是由其对象是否是常量决定的,如果对象是常量,beginend返回const_iterator,如果对象不是常量,返回iterator

例如代码:

vector<int> v;
const vector<int> cv;
auto it1=v.begin();//it1的类型是vector<int>::iterator,it1指向v的第一个元素
auto it2=cv.begin();//it2的类型是vector<int>::const_iterator

另外,C++11新标准引入了两个新函数,分别是cbegincend。使用这两个函数的时候,不论vector对象(或string对象)本身是否是常量,返回的类型都是const_iterator。例如:

vector<int> v;
auto it=v.cbegin();//it的类型是vector<int>::const_iterator

上述代码中,尽管v不常量,但是由于cbegin的原因,it的类型是vector<int>::const_iterator。

17.解引用迭代器可获得所指的对象,如果该对象的类型恰好是类,就有可能进一步访问它的成员。令it是vector对象的迭代器,那么如下代码:

(*it).empty();//解引用it,然后调用结果对象的empty成员
*it.empty();//错误:试图访问it的名为empty的成员,但it是一个迭代器,没有empty成员

注意,上述代码中(*it).empty()中的圆括号必不可少,因为解引用运算符的优先级低于点运算符。

C++中定义了箭头运算符(->),箭头运算符把解引用成员访问两个操作结合在一起,即it->mem等价于(*it).mem.

另外,如下代码:

for(auto it=text.begin());it!=text.end()&&!it->empty();++it)
cout<<*it<<endl;

注意上述代码中,begin和end是每个容器类型的成员函数,而如果通过解引用迭代器获得迭代器所指的对象,该对象的类型恰好是类,empty是这个对象的类型中的成员函数,即empty是调用结果对象的成员,注意区别begin函数和empty函数的关系,它们并不是同一个类型的成员函数。

18.关于数组的几点注意事项:

数组的维度必须是一个常量表达式。

数组的元素应为对象,故不存在引用的数组。

定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型

不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值。

19.理解复杂的数组声明:

int *p[10];          //p是含有10个整型指针的数组
int &p1[10]=/*?*/    //错误:不存在引用的数组
int (*p2)[10]=&arr;  //p2指向一个含有10个整数的数组
int (&p3)[10]=arr;   //p3引用一个含有10个整数的数组
int *(&p4)[10]=p;    //p4是数组的引用,该数组含有10个指针

对于数组的声明,由内向外读有助于理解。

第1行:首先我们知道定义的是一个大小为10的数组,它的名字是p,然后知道数组中存放的是指向int的指针。

第3行:首先圆括号括起来的部分,*p2意味着p2是个指针,接下来观察右边,可以知道p2是一个指向大小为10的数组的指针,最后观察左边,它指向一个int数组,数组包含10个元素。

第5行:首先,p4是一个引用,观察右边知道,p4引用的对象是大小为10的数组,最后观察左边知道,数组的元素类型是指向int的指针。所以,p4是一个含有10个int型指针的数组的引用。

判断技巧:首先利用是否有括号来确定主语,如第3行(*p2),可知其主语是指针,以后的东西都是修饰词,后面的10说明这是一个指向含有10个整数的数组的指针。同样的第4行(&p3),可知其实一个引用,以后的东西都是修饰该引用的。再入第一行,其没有括号,说明其就是一个数组,至于前面的*,只不过是修饰该数组的,p是一个含有10个整型指针的数组。理解了以上的话,第5行就很好理解了,首先括号内是一个引用,说明主语是引用,其他的都是修饰,即p4是一个对含有10个整型指针数组的引用。

20.数组有一个重要的特性:在很多用到数组名字的地方,编译器都会自动地将其替换为指向数组首元素的地址

该语句的一层意思就是当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组

不过需要注意的是,当使用decltype的关键字时,上述转换不会完成

如下代码:

int ia[]={0,1,2,3,4,5,6,7,8,9};
auto ia2(ia);  //ia2是一个整型指针,指向ia的第一个元素
decltype(ia) ia3={0,1,2,3,4,5,6,7,8,9};

第2行:尽管ia是由10个整数构成的数组,但当使用ia作为初始值时,编译器会自动地将数组名字ia替换为指向数组首元素的地址,编译器执行的初始化形式类似于下面的代码:

auto ia2(&ia[0]);

 第3行,由于decltype关键字的原因,编译器并不会自动的将数组名字替换为指向数组首元素的地址。本例中,decltype(ia)返回的类型是一个有10个整数构成的数组。

另外由于数组的这个重要特性,注意看下述代码:

const char ca1[]="a cat";
const char ca2[]="a dog";
if(ca1<ca2)//未定义的,试图比较两个无关地址

因为当使用数组的时候其实真正用的是指向数组首元素的指针,因为上面的if条件实际上比较的是两个const char*的值,实际比较的是指针而非字符串本身。 

但是如果调用strcmp函数,此时比较的不再是指针

if(strcmp(ca1,ca2)<0);

 上述代码确实是比较两个字符串的大小。

21.C++11也为数组引入了begin和end函数,不过由于数组不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数,例如:

int ia[]={0,1,2};
int *beg=begin(ia);//指向ia首元素的指针
int *last=end(ia);//指向ia尾元素的下一个位置的指针

22.注意解引用与指针运算的交互,理解如下代码与注释:

int ia[]={0,2,4,6,8};
int last=*(ia+4);//把last初始化为8,即ia[4]的值
int last1=*ia+4;//last为4,等价于ia[0]+4

23.标准库类型如果涉及到使用下标,则下标必须是无符号类型,而内置的下标运算符所用的索引值不是无符号类型,这一点与vector、string等标准库类型有所不同。例如;

int ia[]={0,2,4,6,8};
int *p=&ia[2];//p指向索引为2的元素
int k=p[-2];//p[-2]是ia[2-2],即ia[0]表示的元素

24.由于历史原因,C标准库中有一些C风格的关于字符串的函数,例如strlen、strcmp、strcat、strcpy等,需要注意的是,传入此类函数的指针必须指向以空字符串作为结束的数组。注意下述代码中的错误:

char ca[]={"c","+","+"};
cout<<strlen(ca)<<endl;//错误:因为ca没有以空字符结束

25允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。但是.如果程序的某处需要C风格字符串,无法直接使用string对象去代替它。例如不能用 string对象直接初始化指向字符的指针,为了完善该功能,string专门提供了一个名为的成员函数,如下述代码,c_str函数返回的是一个C风格字符串。

string s("Hello World");
char *str=s;//错误:不能用string对象初始化char*
const char *str=s.c_str();//正确

26.C++中不允许使用一个数组为另一个内置类型的数组赋值,也不允许使用vector对象初始化数组。但是运行使用数组去初始化vector对象,并且只需指明拷贝区域的首元素地址和尾后地址即可。例如:

int a[]={0,1,2,3,4,5};
vector<int> ivec(begin(a),end(a));//ivec有6个元素,是a中对应元素的副本
vector<int> ivec1(a+1,a+4);//拷贝三个元素:a[1]、a[2]、a[3]

27.要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型

int ia[3][4];
size_t cnt=0;
for(auto &row:ia)
for(auto col:row)
{
col=cnt:
++cnt;
cout<<col<<endl;
 }

解释:第一个for循环遍历ia的所有元素,这些元素是大小为4的数组,因此row的类型就应该是含有4个整数的数组的引用。第二个for循环遍历那些4元素数组中的每一个整数。

如果第二个for循环改成下面的代码也是可以的:

for(auto &col:row)

因为改成这样col的类型是整数的引用。

但是如果两个for循环改成下面这样是不可以的:

for(auto row:ia)
for(auto col:row)

因为row不是引用类型,所以编译器初始化row时会自动将这些元素形式的元素(和其他类型的数组一样)转换成指向该数组首元素的指针,这样row的类型为int*,显然这样内层的循环就不合法了。

28.因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针。

所以注意理解以下代码:

int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
int (*p)[4]=ia;//等价于int (*p)[4]=&ia[0]  p是一个指针,指向含有4个整数的数组
p=&ia[2];      //p指向ia的尾元素,也就是第3个数组

所谓由多维数组名转换得来的指针实际上是指向第一个内层数组的指针,在上面的代码中可以看到,以二维数组ia[3][4]为例,就是指ia是一个指针,因为ia是一个指针,(*p)[4]也是一个指针,所以两者之间是可以直接传递的,不需要像平时初始化指针时再添加取址符。该指针根据实际需要,会指向数组名为ia[0]、ia[1]、ia[2]的含有4个整数的数组。需要注意的是,不能写成int *p=ia,即不能漏写[4],因为指针指向的是含有4个整数的数组,必须要保证指向对象的类型一致。

请看下面两段代码,其是等价的:

int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
for (auto p=ia; p!=ia+3; ++p){
for (auto q=*p; q!=*p+4; ++q)
    cout<<*q<<" ";
cout<<endl;
}
int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
for (int (*p)[4]=&ia[0]; p!=ia+3; ++p){
for (int *q=*p; q!=*p+4; ++q)
    cout<<*q<<" ";
cout<<endl;
}

解释:首先声明一个指针p指向ia的第一个内层数组,然后依次迭代直到ia的全部3行都处理完为止。++p负责将指针p移动到ia的下一行。内层的for循环:因为定义的时候int *p[4]=ia,即 p是一个指针,指向含有4个整数的数组,所以以后用到*p,相当于解引用,因此*p就是一个含有4个整数的数组,分别为ia[0][4]、ia[1][4]、ia[2][4],在auto之后,因为*p本身相当于一个数组,所以根据数组的特性,其会自动转化为指向该元素首地址的指针,这样就可以处理完当前内层数组的所有元素。如果上述解释还是不能理解,参见下述一维数组的代码:

int a[3]={1,2,3};
for (auto q=a;q!=a+3;++q)

此代码中的a相当于上面代码中的*p,由于a本身就是一个数组,可见*p本身也是一个数组;auto之后,数组名被自动转化为指向该数组首元素的指针,所以a代表了一个指针,故而*p也是一个指针,因此其可以作为初始值传递给另一个指针*q,不需要取址符。

29.类型别名简化多为数组指针:

例如:使用下面代码中的任意一种:

using int_array =int[4];
typedef int int_array[4];

上面两种声明等价,上述声明将类型"4个整数组成的数组"命名为int_array注意,[4]不属于名字,而表示一种已有的数据类型

using int_array =int[4];
int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
for (int_array *p=ia; p!=ia+3; ++p){
for (int *q=*p; q!=*p+4; ++q)
    cout<<*q<<" ";
cout<<endl;
}

上述代码与28中的代码等价,这样在定义指针*p的时候,指出指针p指向的是一个4个整数组成的数组。

猜你喜欢

转载自blog.csdn.net/lovebasamessi/article/details/82585888