第3章 字符串、向量和数组
文章目录
更新记录:
(1)2023/2/16
待更新:
(1)[p76]直接初始化和拷贝初始化(细节)
(2)[p113]多维数组的下标引用(细节)
(3)[p114]使用范围for语句处理多维数组(细节)
(4)[p115]指针和多维数组(细节)
(5)[p116]类型别名简化多维数组的指针
3.1 命名空间的using声明[p74]
1、作用域操作符::
:如std::cin
的意思是使用命名空间std中的名字cin。
2、using声明:有了using声明就无需专门的前缀(形如命名空间::)也能使用所需名字了。
using namespace::name; // 格式
using std::cin; // 举例
3、每个用到的名字都需要独立的using声明。每个using引入命名空间中的一个成员。
4、头文件不应包含using声明。因为头文件的内容会拷贝到引用它的文件中去,由于不经意间包含了一些名字可能带来名字冲突。
3.2 标准库类型string[p75]
3.2.1 定义和初始化string对象[p76]
标准库类型string表示可变长的字符序列,使用string类型必须首先包含
<string>
头文件。作为标准库的一部分,string定义在命名空间std中。string对象不含结尾的
'\0'
,字符串字面值含有。
六大初始化方式
string s1; // 默认初始化,s1是空字符串
string s2 = s1;
string s2(s1); // s2是s1的副本
string s3 = "hiya";
string s3("hiya");
string s4(10, 'c'); // 内容是cccccccccc
直接初始化与拷贝初始化
直接初始化与拷贝初始化的区分原则为是否使用"=":
string s5 = "hiya"; // 拷贝初始化
string s6("hiya"); // 直接初始化
3.2.2 string对象上的操作[p77]
读写string对象
1、读取一个string对象:cin >> s
,读取规则如下:
(1)忽略开头空白,从第一个真正字符开始读取,直到遇见下一个空白(空格、制表符和换行符),不储存空白;
(2)cin只能读取一个单词,不读取换行符;
(3)如果一次输入了多个单词,第二个及之后的单词将在输入缓冲区,待下一个输入指令直接使用。
2、读取未知数量的string对象:while (cin >> s) {}
#include <iostream>
#include <string>
using namespace std;
int main()
{
string word;
while (cin >> word) // 反复读取,直到到达文件结尾
cout << word << endl;
return 0;
}
//win10输入Ctrl + z停止
3、使用getline读取一整行:getline(cin, s)
,读取规则如下:
(1)遇到换行符停止读取;
(2)一开始就是换行符,则读取到空string对象;
(3)读取并丢弃换行符,得到不含换行符的string对象。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
while (getline(cin, s))
cout << s << endl;
return 0;
}
//win10输入Ctrl + z停止
注意事项:如果前面使用了cin,后面有getline,则必须在中间加入
cin.get()
防止getline读取到空串。
#include <iostream>
#include <string>
using namespace std;
int main()
{
int classid;
string name;
cout << "请输入你的班级编号:" << endl;
cin >> classid;
cin.get();
//如果没有这行,由于输入班级编号后输入换行符不被cin读取,留在缓冲区被getline()读取并丢弃,则姓名为空。
cout << "请输入你的姓名:";
getline(cin, name);
cout << "班级:" << classid << "\n"
<< "姓名:" << name << endl;
return 0;
}
string的empty和size操作
1、s.empty()
if (!s.empty()) {} // 字符串非空才继续执行大括号中语句
示例:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string line;
while (getline(cin, line)) // 每次读入一整行,遇到空行直接跳过
if (!line.empty())
cout << line << endl;
return 0;
}
2、s.size():返回string对象的长度,指的是字符的个数。
string::size_type类型
string::size_type类型在类string中定义,是一种无符号整型,足够存放下任何string对象,存放string类的size函数返回值类型为string::size_type。
string::size_type的使用方式:
// 以下三个语句中的a都是string::size_type类型
auto a = s.size(); // 法一
string::size_type a; // 法二
decltype(s.size()) a; // 法三
string::size_type为无符号整型数,不能与有符号整型数混用,因为运算时有符号类型会自动转化为无符号类型,可能导致出错。如果一条表达式中有size()就不要使用int,应使用unsigned。
string对象的运算
1、比较string对象
(1)==
、!=
:根据长度和包含的字符是否完全相同判断,对大小写敏感,结果是布尔值。
(2)>
、<
、>=
、<=
:根据字母表判断;根据第一个不相同字符比较的结果判断;结果是布尔值。
string str = "Hello";
string phrase = "Hello World";
string slang = "Hiya";
// 比较结果:str < phrase; slang > str; slang > phrase
2、string对象的赋值
s2 = s1;
3、两个string对象相加
string s1 = "hello";
string s2 = "world";
s3 = s1 + s2;
4、字符串字面值和string对象的相加
规则:"+"两侧至少有一个string对象,注意按整体看。
字符串字面值与string对象相加结果是一个string对象。
string s1 = "hello", s2 = "world";
string s3 = s1 + ","; //正确:string对象可以与一个字符串字面值相加
string s4 = s1 + "," + s2; //正确:每个"+"都有一个运算对象是string对象
string s5 = s1 + "," + "world"; //正确:s1 + ","的结果是string对象,可以与字符串字面值"world”相加
string s6 = "hello" + "," + s2; //错误:"hello" + ","是两个字符串字面值相加,不可行
3.2.3 处理string对象中的字符[p81]
处理单个字符的函数
建议使用C++版本的C标准库头文件,如
<cctype>
而不是<ctype.h>
。形如cname
的头文件第一个c表示从C语言而来。里昂个头文件内容一样,但cname
更符合C++命名规范:在名为cname
的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。
#include <cctype> // 然后可以使用下表的函数
处理每个字符
1、不改变string对象中的字符ch
for (auto ch : str) {} // 对于string对象str中的每个字符ch执行大括号中的操作
使用范围for语句和ispunct函数来统计string对象中标点符号的个数的例子:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s("Hello World!!!");
decltype(s.size()) punct_cnt = 0;
for (auto c : s)
if (ispunct(c))
++punct_cnt;
cout << punct_cnt << " punctuation characters in " << s << endl;
return 0;
}
输出结果为:
3 punctuation characters in Hello World!!!
2、改变string对象中的字符ch
for (auto &ch : str) {} // 如果不用引用,则ch是str中字符的拷贝,不修改“原件”
将字符串中所有小写字母修改为大写字母的例子:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s("Hello World!!!");
for (auto &ch : s)
ch = toupper(ch);
cout << s << endl;
return 0;
}
输出结果为:
HELLO WORLD!!!
处理一部分字符
处理字符串中的一部分字符可以使用下标或迭代器。本节只讨论使用下标,迭代器见3.4节。
注意检查下标的合法性。
1、下标运算符类型最好设置为string::size_type,不然可能触发警告。
2、下标计数从0开始。
3、下标范围:>=0且<s.size()
。下标超出范围可能带来不可预知的错误。
4、如果某个索引是带符号类型的值将自动转换成由string::size_type表达的无符号类型。
5、使用下标处理一部分字符的示例:
(1)使用下标输出字符
// 使用下标运算符输出string对象中的第一个字符
// 必须先考虑第一个字符是否确实存在,否则结果将是未定义的。
if (!s.empty())
cout << s[0] << endl;
(2)使用下标修改string对象中的字符
// 只要字符串不是常量,就能为下标运算符返回的字符赋新值,从而修改string对象
string s("some string");
if (!s.empty())
s[0] = toupper(s[0]);
// 输出为:Some string
// 将s的第一个词改成大写形式
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s("some string");
// 依次处理s中的字符直至处理完全部字符或者遇到一个空白
for (decltype(s.size()) i = 0; i != s.size() && !isspace(s[i]); ++i)
s[i] = toupper(s[i]);
cout << s << endl;
return 0;
}
// 输出结果为:SOME string
(3)使用下标进行随机访问
将0~15之间的十进制数转换成对应的十六进制形式:
#include <iostream>
#include <string>
using namespace std;
int main()
{
const string hexdigits = "0123456789ABCDEF"; // 后面的程序不再打算修改其值
cout << "输入0~15的数字,用空格分开:" << endl;
string result; // 用于保存十六进制的字符串
string::size_type n;
// 用于保存从输入流读取的数,用string::size_type类型可以保证其大于等于0
while (cin >> n)
if (n < hexdigits.size())
result += hexdigits[n];
cout << "十六进制形式为:" << result << endl;
return 0;
}
运行结果:
输入0~15的数字,用空格分开:
12 0 5 15 8 15
^Z
十六进制形式为:C05F8F
3.3 标准库类型vector[p86]
1、vector是类模版而非类型。
2、容器:能容纳绝大多数对象,不能容纳引用。
3、实例化:编译器根据模板创建类或函数的过程。
3.3.1 定义和初始化vector对象[p87]
1、创建一个空vector
vector<int> vInt; // 默认初始化
2、将一个vector对象的元素拷贝给另一个vector对象
// 两种方法:对象类型必须完全相同,如下方要求ivec1、ivec2存储的对象都是int
vector<int> ivec2 = ivec1;
vector<int> ivec2(ivec1);
3、列表初始化vector对象
vector<string> articles = {"a", "an", "the"}; // 必须使用花括号
4、创建指定数量的元素
vector<int> ivec(10, -1); // 包含10个-1
vector<string> svec(10, "hi"); // 包含10个"hi"
5、值初始化
如果vector对象的元素是内置类型,比如int,则元素初始值自动设为0。
vector<int> ivec(10); // 包含10个0
vector<string> svec(10); // 包含10个空串
6、花括号与圆括号的初始化问题
(1)int类型
vector<int> v1(10); // 包含10个0
vector<int> v2{10}; // 包含1个10
vector<int> v3(10, 1); // 包含10个1
vector<int> v4{10, 1}; // 包含10和1两个元素
(2)string类型
vector<string> v5{"hi"}; // 包含1个字符串"hi"
// vector<string> v6("hi"); 错误:不能用字符串字面值构建vector对象
vector<string> v7{10}; // 包含10个空串
vector<string> v8{10, "hi"}; // 包含10个"hi"
3.3.2 向vector对象中添加元素[p90]
1、一般思路:先创建一个空vector,再用for循环添加元素。
2、常用代码
vector<int> v; // 空vector对象
for (int i = 0; i != 100; ++i)
v.push_back(i);
// 循环结束后v由100个元素,值从0~99
string word;
vector<string> v; // 空vector对象
while (cin >> word)
v.push_back(word);
3、注意事项
(1)向vector添加元素不能使用范围for语句。
(2)必须保证所写的循环正确无误,特别是在循环有可能改变vector对象容量的时候。
3.3.3 其他vector操作[p91]
处理vector中的每个对象
1、不改变vector对象中的对象的值:for (auto i : v) {}
2、改变vector对象中的对象的值:for (auto &i : v) {}
3、注意事项:范围for语句体内不应改变其所遍历序列的大小。
// 对于v中的每个元素,求其平方并输出
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
for (auto &i : v)
i *= i;
for (auto i : v)
cout << i << " ";
cout << endl;
return 0;
}
vector的empty和size操作以及关系运算符
1、vector的empty和size操作方式同string。
2、vector的size操作返回值类型为vector<T>::size_type
。
3、vector各个相等性运算符和关系运算符与string相应运算符功能一致。
4、vector关系运算符按照字典顺序进行比较:
(1)vector对象容量不同,但相同位置元素相同,元素较少的vector对象较小。
(2)vector对象元素值有区别,由第一对相异的元素值的大小关系决定vector对象大小。
5、只有当元素的值可以比较时,vector对象才能被比较。
下标用法同string
1、vector下标操作只能用于已存在的元素。
vector<int> ivec; // 空vector对象
cout << ivec[0]; // 错误:ivec不包含任何元素
vector<int> ivec2(10); // 含有10个元素的vector对象
cout << ivec2[10]; // 错误:ivec2元素的合法索引是从0~9
以下标形式访问不存在的元素将引发错误,这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。
2、vector下标不能用于添加元素,要添加可以使用push_back。
3、确保下标合法,尽可能使用范围for语句。
vector对象下标操作程序示例:
/*
* 以10分为一个分数段统计成绩的数量
* 共分为11段:0~9、10~19、...、90~99、100
*/
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// 11个分数段,要11个无符号整数统计每个分数段的个数,每个分数段个数都初始化为0
vector<unsigned> scores(11, 0);
unsigned grade; // 成绩一定非负,用无符号整型
while (cin >> grade)
if (grade <= 100)
++scores[grade / 10];
for (auto n : scores)
cout << n << " ";
cout << endl;
return 0;
}
运行结果为:
42 65 95 100 39 67 95 76 88 76 83 92 76 93
^Z
0 0 0 1 1 0 2 3 2 4 1
3.4 迭代器介绍[p95]
1、所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。
2、string对象不属于容器类型,但string支持迭代器。
3、类似于指针类型,迭代器也提供了对对象的间接访问。
4、迭代器的对象是容器中的元素或者string对象中的字符。
3.4.1 使用迭代器[p95]
begin和end成员
// b表示v的第一个元素,e表示v尾元素的下一位置。
auto b = v.begin(), e = v.end(); // b、e都是迭代器
1、有迭代器的类型拥有返回迭代器的成员,如begin和end成员函数。
(1)begin成员返回指向第一个元素的迭代器;
(2)end成员返回指向尾元素的下一位置(即尾后元素)的迭代器,称为尾后迭代器或尾迭代器。
2、如果容器为空,则begin和end都返回尾后迭代器。
// s为一个string对象,则可以使用以下语句检查s是否非空
if (s.begin() != s.end()) {} // s非空则执行大括号中的语句
3、推荐使用auto获得迭代器类型。
迭代器运算符
程序示例:
1、用迭代器实现将string对象的第一个字母改为大写:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s("some string");
if (s.begin() != s.end()) // 确保s非空
{
auto it = s.begin(); // it表示s的第一个字符
*it = toupper(*it); // 将当前字符改成大写形式
}
cout << s << endl;
return 0;
}
2、利用迭代器及其递增运算符将string对象中第一个单词改写为大写形式:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s("some string");
// 依次处理s的字符直至处理完全部字符或者遇到空白
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it);
cout << s << endl;
return 0;
}
注意事项:
1、执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或尾后迭代器都是未被定义的行为。
2、end成员返回的迭代器(即尾后迭代器)不能执行递增或解引用操作。
3、C++在for循环中使用!=
进行判断而不是<
,是因为所有标准库容器的迭代器都支持==
和!=
运算,但是大多数不支持<
运算符。
4、使用迭代器前需要检查容器非空。例如:
if (v.begin() != v.end()) {}
if (v.cbegin() != v.cend()) {}
迭代器类型
1、迭代器分为iterator
和const_iterator
。
(1)iterator的对象可读可写;const_iterator的对象只读,类似指向常量的指针,能读取但不能修改其所指的元素值。
(2)如果string/vector对象是一个常量,只能使用const_iterator;如果string/vector对象不是常量,那么iterator和const_iterator都适用。
vector<int>::iterator it1; // it1能读写vector<int>元素
string::iterator it2; // it2能读写string对象中的字符
vector<int>::const_iterator it3; // it3只能读元素,不能写元素
string::const_iterator it4; // it4只能读字符,不能写字符
2、迭代器这个名词本身有三种不同含义:
(1)迭代器概念本身;(2)容器定义的迭代器类型;(3)某个迭代器对象。
begin、end与cbegin、cend
1、begin、end返回的具体类型由对象是否是常量决定:
(1)对象是常量,返回const_iterator类型;
(2)对象不是常量,返回iterator类型。
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator
2、cbegin、cend也分别返回指示容器第一个元素或尾后元素的迭代器:
(1)不论对象是否为常量,总是返回const_iterator类型;
(2)如果不需要修改对象的值,建议使用const_iterator(cbegin、cend)。
vector<int> v;
auto it3 = v.cbegin; // it3的类型是vector<int>::const_iterator
结合解引用和成员访问操作
解引用迭代器可以获得迭代器所指的对象,如果该对象是类,就可能需要进一步访问它的成员。
例如对于一个由字符串组成的vector对象来说,假设it是该vector对象的迭代器,则检查it所指字符串是否为空的代码如下:
(*it).empty()或it->empty() // 正确:解引用it,然后调用结果对象的empty成员
*it.empty() // 错误:试图访问it的名为empty的成员,但it是个迭代器,没有empty成员
假设用一个名为text的字符串向量存放元素,要输出其每一行直到遇到空字符串为止,代码如下:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> text;
string s;
while (getline(cin, s))
text.push_back(s);
// 无需修改text中的内容,用const_iterator
for (auto it = text.cbegin(); it != text.cend(); ++it)
cout << *it << endl;
return 0;
}
某些对vector对象的操作会使迭代器失效
1、不能在范围for循环中向vector对象添加元素。
2、任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。
3、但凡是使用了迭代器的循环体,都不要向迭代器所述的容器添加元素。
3.4.2 迭代器运算[p99]
迭代器的算术运算
例如,下述代码得到一个指向某vector对象v中间位置的元素的迭代器:
auto mid = v.begin() + v.size() / 2;
假如v有20个元素,则mid相当于v.begin() + 10
,mid指向v[10]
。
当两个迭代器合法且指向同一个容器的元素(或尾后元素)时,可以用关系运算符(<、<=、>、>=)对其进行比较。
if (it < mid)
// 处理v前半部分元素
当两个迭代器合法且指向同一个容器的元素(或尾后元素)时,可以将其相减,所得结果为两个迭代器的距离。
1、距离指的是右侧的迭代器向前移动多少位置能追上左侧迭代器。
2、距离的类型为difference_type
,距离可正可负,为带符号整型数。
3、string和vector都定义了difference_type
。
使用迭代器运算
二分搜索算法示例:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> ivec = { 1, 4, 6, 13, 57, 76, 88, 102, 145 }; // ivec必须是排好序的
auto beg = ivec.begin(), end = ivec.end(); // beg和end表示搜索的范围
auto mid = ivec.begin() + (end - beg) / 2; // 初始状态下的中间点
int num; // 要查找的数字
cin >> num;
// 当还有元素尚未检查并且还没有找到num时执行循环
while (mid != end && *mid != num)
{
if (num < *mid) // 要找的元素在前半部分吗?
end = mid; // 如果是,调整搜索范围,忽略掉后半部分
else // 要找的元素在后半部分
beg = mid + 1; // 在mid之后寻找
mid = beg + (end - beg) / 2; // 新的中间点
}
// 循环结束时,mid要么等于end要么指向要找的元素。如果mid等于end,说明没有找到。
if (mid != end)
cout << "找到了" << endl;
else
cout << "没找到" << endl;
}
3.5 数组[p101]
数组与vector的比较:
1、数组的大小固定不变,vector可变;
2、数组某些时候性能更好,但损失了一些了灵活性;
3、操作数组的本质是操作指向数组首元素的指针;
4、不清楚元素确切个数时,应使用vector。
3.5.1 定义和初始化内置数组[p101]
数组定义和初始化的注意事项
1、数组a[d],a是数组的名字,d是数组的维度。
2、数组的维度必须是常量表达式,即编译时维度就是已知的。
string strs[get_size()]; // 当get_size是constexpr时正确;否则错误
3、不允许拷贝和赋值
(1)数组的内容不能拷贝给其他数组作为初始值;
(2)不能用数组给其他数组赋值。
int a[] = {0, 1, 2};
int a2[] = a; // 错误
a2 = a; // 错误
4、数组不显式初始化
和内置类型变量一样,如果在函数内部定义了某种内置类型的数组,那么不xian’shi会令数组含有未定义的值。
#include <iostream>
using namespace std;
int main()
{
int a[5];
for (auto n : a)
cout << n << " ";
}
输出结果如下,输出结果因机器与运行环境不同而异:
-858993460 -858993460 -858993460 -858993460 -858993460
5、定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。
6、数组的元素应为对象,不存在引用的数组。
显式初始化数组元素
1、对数组元素进行列表初始化时,允许忽略数组维度,编译器会根据初始值的数量计算其维度。
2、如果指明了维度,那么初始值的总数量不应超过指定的大小。
3、如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素执行值初始化,不再会是未定义状态。
const unsigned sz = 3;
int a1[sz] = {0, 1, 2};
int a2[] = {0, 1, 2};
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}; // 错误:初始值过多
字符数组的特殊性
可以用字符串字面值初始化字符数组,结尾处有一个空字符\0
。
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
char a3[] = "C++"; // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // 错误:没有空间存放空字符,维度应为7
理解复杂数组的声明
理解方法:从变量名开始从内向外,从右向左。
默认情况下,类的修饰符从右向左依次绑定。
int *ptrs[10]; // ptrs是含有10个整型指针的数组
// 从右向左:右侧是数组符号,所以ptrs是个数组,左侧有指针符号和int,所以数组的元素是整型指针
int &refs[10] = /* ? */; // 错误:不存在引用的数组
// 从右向左:同上,数组的元素是引用,错误。
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
// 从内向外:圆括号内*Parray意味着Parray是个指针;
// 再从右向左:右侧是数组符号,Parray指向大小为10的数组;左侧int表示数组的元素是int。
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
// 从内向外:圆括号内&arrRef表示arrRef是个引用;
// 再从右向左:右侧是数组符号,arrRef引用一个大小为10的数组;左侧int表示数组的元素是int。
// 修饰符数量没有特殊限制
int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有10个int型指针
// 从内向外:&arry表示arry是个引用;
// 再从右向左:右侧表示arry引用一个含有10个元素的数组;左侧int *表示数组元素是int型指针。
3.5.2 访问数组元素[p103]
1、数组的下标:size_t类型
size_t类型是无符号整型,它被设计得足够大以便能表示内存中任意对象的大小,定义在cstddef头文件中(在C语言中为stddef.h)。
2、遍历数组:范围for语句
for (auto i : arr) {}
3、缓冲区溢出错误:数组下标越界并试图访问非法内存区域。
3.5.3 指针和数组[p105]
指针与数组的关系
1、对数组的元素使用取地址符能得到指向该元素的指针。
string nums[] = {"one", "two", "three"};
string *p = &nums[0];
2、数组的名字本质是一个指向数组首元素的指针。
string nums[] = {"one", "two", "three"};
string *p2 = nums; // 等价于p2 = &nums[0]
3、auto和decltype与数组:auto推断得到指针而非数组,decltype得到的是数组。
int a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto a2(a); // a2是一个整型指针,指向ia的第一个元素
a2 = 42; // 错误,a2是一个指针,不能用int值给指针赋值
auto a3(&a[0]); // a3的类型是int
int a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
decltype(a) a2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // a2是数组
指针也是迭代器
vector和string的迭代器支持的运算,数组的指针全都支持。见3.4.2、3.4.1迭代器运算符
可以通过指针获取数组尾后元素的地址,但不推荐这种使用方式,推荐使用标准库函数end。
#include <iostream>
using namespace std;
int main()
{
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;
return 0;
}
标准库函数begin和end
标准库函数begin和end定义在
iterator
头文件中,分别返回指向数组首元素的指针和尾后指针。尾后指针不能执行解引用和递增操作。
int arr[] = {1, 2, 3, 4, 5};
int *pbeg = begin(arr); // 指向数组arr首元素的指针
int *pend = end(arr); // 指向数组arr尾元素的下一位置的指针
// 变量命名可以使用beg,但不能使用end,建议使用pbeg和pend。
使用标准库函数begin和end,找到数组arr中的第一个负数并输出:
#include <iostream>
#include <iterator>
using namespace std;
int main()
{
int arr[] = { 1, 2, -3, 4, 5 };
int* pbeg = begin(arr);
int* pend = end(arr);
// 寻找第一个负值元素,pend指向arr尾元素的下一位置
while (pbeg != pend && *pbeg >= 0)
++pbeg;
cout << *pbeg << endl;
return 0;
}
指针运算
1、指针加上整数:结果指向同一数组的元素或尾后元素。
2、两个指针相减:表示距离,指向同一数组的元素,结果为ptrdiff_t带符号类型,定义在cstddef头文件中。
3、使用关系运算符比较(>、<等):指向同一数组的元素或其尾后元素才能比较。
4、特殊情况:
(1)空指针:允许给空指针加上或减去一个值为0的整型常量表达式。两个空指针允许相减,结果为0。
(2)所指对象并非数组的指针:上述运算适用的条件为两个指针必须指向同一个对象或该对象的下一位置。
解引用与指针运算的交互
int arr[] = {0, 2, 4, 6, 8};
int last = *(arr + 4); // 相当于arr[4]
下标和指针
标准库类型的下标必须是无符号类型,而数组的下标可正可负,只要仍然表示同一数组中的元素或尾后元素即可,如下方
p[-2]
仍表示arr中的元素。
int arr[] = {0, 2, 4, 6, 8};
int *p = &arr[2]; // p指向索引为2的元素
int j = p[1]; // p[1]等价于*(p + 1),即arr[3]表示的元素
int k = p[-2]; // p[-2]是arr[0]表示的元素
3.5.4 C风格字符串[p109]
C风格字符串存放在字符数组中且末尾带有’\0’。
比起C风格字符串,标准库string更安全、更高效。
C标准库cstring函数
此类函数必须传入以空字符作为结束的数组:
char arr[] = {'C', '+', '+'}; // 不以空字符结束
cout << strlen(arr) << endl; // 错误:arr没有以空字符结束,产生未定义结果
比较C风格字符串
比较两个C风格字符串应使用
strcmp(p1, p2)
函数。
不能用关系运算符比较C风格字符串,这实际比较的是指针而非字符串的本身,因为C风格字符串本质类型是const char*
。
const char c1[] = "A string example";
const char c2[] = "A different string";
if (ca1 < ca2) // 未定义的:试图比较两个无关地址
// 指针指向的并非同一对象,所以将得到未定义的结果。
连接和拷贝C风格字符串
连接和拷贝C风格字符串应使用
strcat(p1, p2)
、strcpy(p1, p2)
函数。
1、使用“+”不能连接C风格字符串,因为不能将两个指针相加。
2、strcat(p1, p2)
、strcpy(p1, p2)
的潜在问题:需要检查C风格字符串的大小,判断其空间是否足够,有安全隐患。
3.5.5 与旧代码的接口[p111]
旧代码:在标准库出现之前就写成的C++程序,充满数组和C风格字符串。
混用string对象和C风格字符串
1、任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代。
2、如果程序某处需要一个C风格字符串,不能直接用string对象代替它,需要先将string对象转换为C风格字符串。例如:不能用string对象直接初始化指向字符的指针。
// s是一个string对象
char *str = s; // 错误:不能用string对象初始化char*
3、将string对象转换为C风格字符串:
const char *str = s.c_str(); // s是一个string对象
// c_str()返回一个C风格字符串,即一个const char*类型指针,指向一个以空字符结束的字符数组,该字符数组所存的数据与string对象s一致。
使用数组初始化vector对象
1、不允许使用vector对象初始化数组。
2、允许使用数组来初始化vector对象,只需指明要拷贝区域的首元素地址和尾后地址即可(左闭右开区间)。
int arr[] = {0, 1, 2, 3, 4, 5};
vector<int> v1(begin(arr), end(arr)); // v1包含0, 1, 2, 3, 4, 5
vector<int> v2(arr + 1, arr + 4); // v2包含1, 2, 3
3.6 多维数组[p112]
3.6.1 二维数组的初始化[p113]
1、将所有初值写在一个花括号内,按顺序初始化:
int a[3][4]={ 1,2,3,4,5,6,7,8,9,10,11,12 };
2、分行列出二维数组元素的初值:
int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
3、可以只对部分元素初始化:
int a[3][4] = { {1},{0,6},{0,0,11} };
4、列出全部初始值时,第1维下标个数可以省略:
// 两种写法均可
int a[][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
int a[][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
如果只对部分元素初始化,剩下的未显式初始化的元素,将自动被初始化为零。
如果不作任何初始化,定义在函数体内的数组中会存在垃圾数据,函数体外数组中的数据默认初始化为0。
3.6.2 使用范围for语句处理多维数组(C++11)[p114]
将多维数组元素的值设为该元素在整个数组中的序号:
size_t cnt = 0;
for (auto &row : arr)
{
for (auto &col : row)
{
col = cnt;
++cnt;
}
}
要使用范围for语句处理多维数组,哪怕只读不写,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
// 正确示例
for (const auto &row : arr)
for (auto col : row)
cout << col << endl;
// 错误示例
for (auto row : arr)
for (auto col : row)
3.6.3 指针和多维数组[p115]
多维数组是数组的数组,所以多维数组名是指向第一个内层数组的指针。
int arr[3][4]; // 大小为3的数组,每个元素都是含有4个整数的数组
// arr作为多维数组名,指向第一个含有4个整数的数组
int (*p)[4] = arr; // 由内向外,p是指针,指向含有4个整数的数组
p = &arr[2]; // p指向arr的尾元素,即最后一个含有4个整数的数组
注意:
int *p[4]; // p是整型指针的数组
int (*p)[4]; // p是指向含有4个整数的数组的指针