有了using声明就无须专门的前缀(形如命名空间 :: )也能使用所需的名字了。using声明具有如下的形式:
using namespace::name ;
一旦声明了上述语句,就可以直接访问命名空间中的名字:
#include
//using声明,当我们使用名字cin时,从命名空间std中获取它
using std::cin;
int main (){
int i;
cin >> i; //正确:cin和std: :cin含义相同
cout << i; //错误:没有对应的using声明,必须使用完整的名字
std::cout << i; //正确:显式地从std中使用cout
return 0;
}
注:头文件不应包含using声明
#include
using std::string;
直接初始化和拷贝初始化
string s5 = "hiya" ; //拷贝初始化
string s6 ( "hiya" ) ; //直接初始化
string s7(10, 'c') ; //直接初始化,s7的内容是cccccccccc
读写string对象
int main ()
string s; //空字符串
cin >> s; //将string对象读入s,遇到空白(即空格符、换行符、制表符等)停止
cout << s << endl; //输出s
string line;
//每次读入一整行,直至到达文件末尾
while (getline (cin, line)) //getline函数直到遇到换行符为止
cout << line << endl;
return 0;
string::size_type类型
由于size函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果
string加法:
当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保 每个加法运算符(+) 的两侧的运算对象至少有一个是string:
string s4 = s1 +","; //正确:把一个string对象和一个字面值相加
string s5 = "hello"+ ","; //错误:两个运算对象都不是string
//正确:每个加法运算符都有一个运算对象是string
string s6 = s1 + "," + "wor1d" ; //表达式s1+ ","的结果是一个string对象
string s7 = "hello" + "," + s2; //错误:不能把字面值直接相加
注:因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。
在cctype头文件中定义了一组标准库函数处理这部分工作,表3.3列出了主要的函数名及其含义。
使用C++版本的C标准库头文件
cctype头文件(从C语言继承)和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间 std,而定义在名为.h的头文件中的则不然。
使用范围for语句改变字符串中的字符
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。
string s("Hello world!!!" );
//转换成大写形式。
for (auto &c : s) //对于s中的每个字符(注意:c是引用)
c= toupper(c) ; //c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;
下标运算符
string::size_type num; //用于保存从输入流读取的数
//依次处理s中的字符直至我们处理完全部字符或者遇到一个空白
for (decltype(s.size()) index = 0; index != s.size() && !isspace (s[index]); ++index)
s[index] = toupper(s[index] );//将当前字符改成大写形式
程序的输出结果将是:
SOME string
#include //要想使用vector,必须包含适当的头文件
using std::vector;
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化( instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; //保存 sales_item类型的对象
vector<vector<string>> file; //该向量的元素是vector对象
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector
。
定义vector对象的常用方法:
列表初始化vector对象
vector<string> articles = { "a", "an", "the" }; //拷贝初始化(即使用=时),只能提供一个初始值
如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化
vector<string> v1{ "a", "an", "the" }; //列表初始化
vector<string> v2("a", "an", "the") ; //错误,(提供的初始元素值的列表,只能使用花括号,不能使用圆括号)
还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化vector对象:
vector<int> ivec (10,-1); //10个int类型的元素,每个都被初始化为-1
vector<string> svec (10,"hi!"); // 10个string类型的元素,每个都被初始化为"hi!"
vector<string> v6("hi"); //错误:不能使用字符串字面值构建vector对象
初始化一定数量的相同元素
vector<int> ivec(10) ; //10个元素,每个都初始化为0
vector<string> svec(10); //10个元素,每个都是空string对象
//注意是初始化时用的圆括号还是花括号
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对象了:
vector<string> v5 {"hi"}; //列表初始化:v5有一个元素
vector<string> v5 {"hi"}; //列表初始化:v5有一个元素
vector<string> v6 ("hi"); //错误:不能使用字符串字面值构建vector对象
vector<string> v7 {10}; //v7有10个默认初始化的元素
vector<string> v8 {10,"hi"}; //v8有10个值为"hi"的元素
元素数量无法确定时,利用vector 的成员函数push _back向其中添加元素。push_back负责把一个值当成vector对象的尾元素 “压到 (push)” vector对象的“尾端( back)”。例如:
vector<int> v2; //空 vector对象
for (int i = 0; i != 100; ++i)
v2.push_back(i); //依次把整数值放到v2尾端
//循环结束后v2有100个元素,值从0到99
//从标准输入中读取单词,将其作为vector对象的元素存储string word;
vector<string> text; //空vector对象
while (cin >> word){
text.push_back ( word); //把 word添加到text后面
}
vector使用的蕴含要求:
大多数都和string的相关操作类似
size返回vector对象中元素的个数,返回值的类型是由vector定义的size_type类型。要使用size_type,需首先指定它是由哪种类型定义的、vector对象的类型总是包含着元素的类型:
vector<int>::size type //正确
vector::size type //错误
vector对象(以及 string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
类似于指针类型
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin 和 end 的成员
//b表示v的第一个元素,e表示v尾元素的下一位置
auto b = v.begin(), e = v.end (); //b 和e的类型相同
使用 == 和 != 来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。
//利用下标运算符把string对象的第一个字母改为大写
string s("some string");
if (s.begin () != s.end()) {//确保s非空
auto it = s.begin(); // it表示s的第一个字符
*it = toupper(*it); //将当前字符改成大写形式
}
//依次处理s的字符直至我们处理完全部字符或者遇到空白
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it) ; //将当前字符改成大写形式
泛型编程
原来使用C或 Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有点儿奇怪。C++程序员习惯性地使用 != ,其原因和他们更愿意使用 迭代器 而非 下标 的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。
之前已经说过,只有string和 vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。
迭代器类型
那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型:
vector<int>::iterator it; //it能读写vector的元素
string::iterator it2; // it2能读写string对象中的字符
vector<int>::const_iterator it3; //it3只能读元素,不能写元素
string::const_iterator it4; //it4只能读字符,不能写字符
begin和end返回的具体类型由对象是否是常量决定
vector<int> v;
const vector<int> cv;
auto it1 = v.begin () ; //it1的类型是vector::iterator
auto it2 = cv.begin(); //it2的类型是vector::const_iterator
为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是 cbegin 和 cend:
auto it3 = v.cbegin(); // it3的类型是vector::const_iterator
结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。
//前面的圆括号必不可少
(*it).empty() //解引用it,然后调用结果对象的empty成员
*it.empty() //错误:试图访问it的名为empty的成员,但it是个迭代器
//没有empty成员
为了简化上述表达式,C++语言定义了 箭头运算符(->),箭头运算符把解引用和成员访问两个操作结合在一起,
也就是说 , it->mem和(*it).mem表达的意思相同。
//依次输出text的每一行直至遇到第一个空白行为止
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
cout<<*it<<endl;
谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
//计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size() / 2;
if (it < mid)
//处理vi前半部分的元素
两个指向同一个容器中的元素的人迭代器相减,所得距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type的带符号整型数。
使用迭代器
使用迭代器运算的一个经典算法是二分搜索。
// text必须是有序的
//beg 和 end表示我们搜索的范围
auto beg = text.begin() , end = text.end();
auto mid = text.begin() + (end - beg) / 2; //初始状态下的中间点
//当还有元素尚未检查并且我们还没有找到sought时执行循环
while (mid != end && *mid != sought) {
if (sought< *mid) //我们要找的元素在前半部分吗?
end = mid; //如果是,调整搜索范围使得忽略掉后半部分
else //我们要找的元素在后半部分
beg = mid + l; //在mid之后寻找
mid = beg + (end - beg) / 2 ; //新的中间点
)
数组是一种类似于标准库类型vector 的数据结构, 但是数组的大小确定不变,不能随意向数组中增加元素。因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。
如果不清楚元素的确切个数,请使用vector。
数组的声明形如a[d],数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式:
unsigned cnt = 42; //不是常量表达式
constexpr unsigned sz = 42; //常量表达式,关于constexpr;
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有42个整型指针的数组
string bad[cnt]; //错误: cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确; 否则错误
显式初始化数组元素
const unsigned sz = 3;
int ia1[sz] = {0, 1, 2}; //含有3个元素的数组,元素值分别是0,1,2
int a2[]= {0, 1, 2}; //维度是3的数组
int a3[5]= {0, 1, 2}; //等价于a3 []= {0, 1, 2, 0, 0}
string a4[3]= ("hi", "bye"}; //等价于a4[] = { "hi", "bye", "")
int a5 [2] = {0, 1, 2}; //错误:初始值过多
字符数组的特殊性
char a1 [] = {'c', '+’,'+’}; //列表初始化,没有空字符
char a2[] - {'C', '+', '+' , \0'}; //列表初始化,含有显式的空字符
char a3[] = "C++"; //自动添加表示字符串结束的空字符
const char a4[6]= "Daniel"; //错误:没有空间可存放空字符!
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
int a[] = {0, 1, 2}; //含有3个整数的数组
int a2[]= a; //错误:不允许使用一个数组初始化另一个数组
a2 = a; //错误:不能把一个数组直接赋值给另一个数组
理解复杂的数组声明
int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10]=/* ?*/; //错误:不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针
//以10分为一个分数段统计成绩的数量:0~9,10~19,....90~99,100
unsigned scores [11] = {}; //11个分数段,全部初始化为О
unsigned grade;
while (cin >> grade){
if (grade <= 100)
++scores[grade/10]; //将当前分数段的计数值加1
}
string nums[] = {"one", "two","three"); //数组的元素是string对象
string *p = &nums [0]; //p指向nums的第一个元素
//在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:
string *p2 = nums; //等价于 p2 =&nums[0]
在一些情况下数组的操作实际上是指针的操作
int ia[] = {0,1,2,3,4,5,6,7,8,9} ; // ia是一个含有10个整数的数组
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素, 等价于auto ia2(&ia[0]); 显然ia2的类型是int*
ia2 = 42; //错误:ia2是一个指针,不能用int值给指针赋值
当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是由10个整数构成的数组:
// ia3是一个含有10个整数的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9} ;
ia3 = p; //错误:不能用整型指针给数组赋值
ia3[4] = i; //正确:把i的值赋给ia3的一个元素
指针也是迭代器
指向数组元素的指针拥有更多功能,vector和string 的迭代器支持的运算,数组的指针全都支持。
int arr[]= {0,1,2,3,4,5,6,7,8,9};
int *p = arr; //p指向arr的第一个元素
++p; //p指向arr[1]
int *e = &arr[10]; //指向arr尾元素的下一位置的指针,尾后迭代器,(但这种方法易出错)
for (int *b = arr; b != e; ++b)
cout << *b << endl; //输出arr的元素
标准库函数 begin 和 end
int ia[] = {0,1,2,3,4,5,6,7,8,9}; //ia是一个含有10个整数的数组
int *beg = begin(ia) ; //指向ia首元素的指针
int *last = end(ia); //指向arr尾元素的下一位置的指针
指针运算
constexpr size_t sz = 5;
int arr[sz] = {1,2,3,4,5};
int *ip = arr; //等价于int *ip = &arr[0]
int *ip2 = ip + 4; //ip2指向arr的尾元素arr[4]
//减法
auto n = end(arr) - begin(arr); //n的值是5,也就是arr中元素的数量
两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,和 size_t一样,ptrdiff_t 也是一种定义在 cstddef头文件中的机器相关的类型。因为差值可能为负值,所以ptrdiff_t是一种带符号类型。
解引用和指针运算的交互
指针加上一个整数所得的结果还是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针:
int ia [ ] = {0,2,4,6,8}; //含有5个整数的数组
int last = *(ia + 4); //正确:把last初始化成8,也就是ia[4]的值
last = *ia + 4; //正确:last = 4等价于ia[0] + 4
下标和指针
只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算:
int *p = &ia[2]; //p指向索引为2的元素
int j = p[1]; //p[1]等价于*(p + 1),就是ia[3]表示的那个元素
int k = p [-2]; //p[-2]是ia [ 0]表示的那个元素
尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。操作C风格字符串的函数定义在cstring头文件中。
对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。
现代的C++程序应当尽量使用 vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。
严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用多维数组大有益处。
当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
//大小为10的数组,它的每个元素都是大小为20的数组,
//这些数组的元素是含有30个整数的数组
int arr [10][20][30]= {0}; //将所有元素初始化为О
对于二维数组来说,常把第一个维度称作行,第二个维度称作列。
多维数组的初始化
允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组一样。下面的初始化形式中,多维数组的每一行分别用花括号括了起来;
int ia[3][4] = { //三个元素,每个元素都是大小为4的数组
{0, 1,2, 3}, //第1行的初始值
{4, 5,6,7}, //第2行的初始值
{8,9,10,11} //第3行的初始值
};
//其中内层嵌套着的花括号并非必需的
//没有标识每行的花括号,与之前的初始化语句是等价的
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
//显式地初始化每行的首元素
int ia[3][4] = {{ 0 }, { 4 }, { 8 }};
//显式地初始化第1行,其他元素执行值初始化
int ix[3][4]= {0, 3, 6, 9};
多维数组的下标引用
//用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1] ; //把row绑定到ia的第二个4元素数组上
程序中经常会用到两层嵌套的for循环来处理多维数组的元素, 我们将元素的值设为该元素在整个数组中的序号。
constexpr size_t rowCnt = 3, colCnt = 4 ;
int ia[rowCnt][colcnt]; //12个未初始化的元素
//对于每一行
for (size_t i = 0; i != rowCnt; ++i){
//对于行内的每一列
for (size_t j = 0; j != colCnt; ++j){
//将元素的位置索引作为它的值
ia[i][j] = i * colCnt + j;
}
}
使用范围for语句处理多维数组
由于在C++11新标准中新增了范围for语句,所以前一个程序可以简化为如下形式:
size_t cnt = 0;
for (auto &row : ia) //对于外层数组的每一个元素
for (auto &col : row){ //对于内层数组的每一个元素
col = cnt; //将下一个值赋给该元素
++cnt; //将cnt加1
}
要使用范围 for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
循环中并没有任何写操作,可是我们还是将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转成指针
for (const auto &row : ia) //对于外层数组的每一个元素
for (auto col : row) //对于内层数组的每一个元素
cout << col << endl;
假设不用引用类型,则循环如下述形式:
for (auto row: ia)
for (auto col : row)
程序将无法通过编译。这是因为,像之前一样第一个循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row 的类型就是 int*,显然内层的循环就不合法了,编译器将试图在一个 int* 内遍历,这显然和程序的初衷相去甚远。
指针和多维数组
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia; //p指向含有4个整数的数组
p = &ia[2]; //p指向ia的尾元素
我们首先明确(*p)意味着p是一个指针。接着观察右边发现,指针p所指的是一个维度为4的数组;再观察左边知道,数组中的元素是整数。因此,p就是指向含有4个整数的数组的指针。
随着C++11新标准的提出,通过使用auto或者decltype 就能尽可能地避免在数组前面加上一个指针类型了:
// 输出ia中每个元素的值,每个内层数组各占一行
// p指向含有4个整数的数组
for (auto p = ia; p != ia + 3; ++p){
//q指向4个整数数组的首元素,也就是说,q指向一个整数
for (auto q - *p; q != *p + 4; ++q)
cout << *q << ' ';
cout <<endl;
当然,使用标准库函数begin和 end 也能实现同样的功能,而且看起来更简洁一些:
//p指向ia的第一个数组
for (auto p = begin(ia) ; p!= end(ia) ; ++p){
//q指向内层数组的首元素
for (auto q = begin(*p); q != end (*p); ++q)
cout<< *q << ' '; //输出q所指的整数值
cout << endl;
}
虽然我们也能推断出p 的类型是指向含有4个整数的数组的指针,q的类型是指向整数的指针,但是使用auto关键字我们就不必再烦心这些类型到底是什么了。
类型别名简化多维数组的指针
using int_array = int [4]; //新标准下类型别名的声明
//输出ia中每个元素的值,每个内层数组各占一行
for (int_array *p = ia; p != ia + 3; ++p){
for (int *q = *p; q != *p + 4; ++q)
cout<< *q << ' ';
cout << endl;
}
程序将类型“4个整数组成的数组”命名为int_array,用类型名int_array定义外层循环的控制变量让程序显得简洁明了。
注:仅供学习参考,有什么错误欢迎指正!