前言:主要是自己学习过程的积累笔记,所以跳跃性比较强,建议先自学后拿来作为复习用。
定义一个 int 型的变量 a 并初始化为0:
int a = 0;
int a(0);
int a = {0};
int a{0};
后两条语句即被称为“列表初始化”。其有一个注意点:对于内置类型的变量,使用列表初始化且初始值存在丢失信息的风险时,编译器可能会报错(部分编译器只会警告,初始化依然执行):
long double value = 3.1415926536;
int a(value), b = value; // 正确:虽然丢失了部分值,普通初始化仍然执行
int c{value}, d = {value}; // 错误:存在丢失信息的风险,列表初始化不执行
使用赋值号(=)初始化一个变量即为拷贝初始化,不用则为直接初始化:
string a = "hi"; // 拷贝初始化
string b("hi"); // 直接初始化
string c(3, 'i'); // 直接初始化,c 的内容是 iii
在初始化标准库容器的时候,如果不提供显式的初始值,则编译器会创建一个元素初值,并把它赋给容器中的所有元素,初值的选取与默认初始化是一致的。但要注意将其与默认初始化区分开来:值初始化相当于容器上的默认初始化。
头文件:
#include
vector<int> a; // a 保存 int 类型的对象
vector<vector<int>> b; // b 保存 vector 类型的对象
上面的代码中,编译器根据模板 vector 生成了两种不同的类型:vector 和 vector
初始化 vector 的方法 | |
---|---|
vector |
a 是一个 vector,元素类型为 T,采用默认初始化 |
vector |
b 是一个 vector,包含 a 中所有元素的副本 |
vector |
等价于上一条语句 |
vector |
c 包含 n 个重复的元素,值都为 val |
vector |
d 包含了 n 个重复地执行了值初始化的对象 |
vector |
e 是一个vector,用 1, 2, 3…等进行初始化 |
vector |
等价于上一条语句 |
vector |
v 是一个长度为 n 的 vector,它每个元素都是一个 vector |
用圆括号时,提供的值是用来构造 vector 对象的,表明 vector 对象的容量。提供两个值时前一个表明 vector 对象的容量,后一个表明所有元素的初值。
用花括号时,提供的值是用来列表初始化 vector 对象的,提供的每一个值只初始化 vector 对象中的一个元素。
注意,想要列表初始化 vector 对象,花括号的值必须与元素类型相同,否则编译器就会将其当做圆括号进行相应的初始化。如:
vector<string> a{"hi"}; // 列表初始化,a 有一个元素 hi
vector<string> b("hi"); // 此处是圆括号,是错误的用法,不能使用字符串字面值构造 vector 对象
vector<string> c{10}; // 花括号的值与 string 类型不同,初始化为 c 有10个默认初始化的元素
vector<string> d{10, "hi"}; // d 有10个值为 "hi" 的元素
三个注意点:
vector<string> a{"b", "c", "d"}; // 列表初始化
vector<string> a("b", "c", "d"); // 错误,不能使用圆括号
vector<T> a;
a.push_back(i); // 将整数 i 放到 a 的末端
不要在范围 for 循环中向 vector 对象添加元素。
vector 类似于一个长度可以灵活变化的数组。
vector a; | |
---|---|
a.empty() | 如果 a 中不含任何元素返回真,否则返回假 |
a.size() | 返回 a 中元素的个数 |
a[n] | 返回 a 中第 n 个位置上元素的引用 |
vector b = a; | 将 a 中的元素拷贝给 b |
a = {1, 2, 3…} | 用列表中的值拷贝替换 a 中的元素的值,若其多于 a 中元素个数,则直接在 a 的末尾插入新的元素 |
a == b | a 和 b 元素个数和对应位置上的元素完全相同时返回真,否则返回假 |
a != b | a 和 b 不相等时返回真,否则返回假 |
<, <=, >, >= | 以字典序进行比较 |
类似于指针,迭代器可以实现对对象的间接访问。其对象是容器中的元素或者 string 对象中的字符。迭代器可以访问对象中的某个元素,或者从一个元素移动到另一个元素。当迭代器有效时,其指向某个元素,或者指向容器尾元素的下一个位置;其他情况都属于无效迭代器。
有迭代器的类型同时拥有返回迭代器的成员,比如 begin 返回指向第一个元素(字符)的迭代器;end 返回指向容器(或 string 对象)尾元素的下一个位置的迭代器,也被称为尾后迭代器或尾迭代器。如果容器为空,则 begin 和 end 返回的都是尾后迭代器。
iterator b = v.begin(), c = v.end();
标准容器迭代器的运算符 | |
---|---|
*iter | 返回迭代器 iter 所指元素的引用 |
iter -> mem | 解引用 iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem(括号不能少) |
++iter | 令 iter 指向容器(或 string 对象)的下一个元素(字符) |
- -iter | 令 iter 指向容器(或 string 对象)的上一个元素(字符) |
iter1 == iter2 | 两个迭代器指向的元素(字符)相同,或者都是同一个容器的尾后迭代器时相等,返回 true |
iter1 != iter2 | 两个迭代器指向的元素(字符)不相同时返回 true |
迭代器的类型是 iterator 和 const_iterator。后者和常量指针差不多,能读取但不能修改它所指的元素值。begin 和 end 返回的具体类型由对象是否是常量决定,对象是常量的话返回的就是 const_iterator,对象不是常量就返回 iterator。
vector<int> a;
auto iter1 = a.begin(); // iter1 的类型是 iterator
const vector<int> b;
auto iter2 = b.begin(); // iter2 的类型是 const_iterator
但通常来说,要想获取 const_iterator,一般使用 cbegin 和 cend。
auto iter3 = a.cbegin(); // iter3 的类型是 const_iterator
凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
vector 和 string 迭代器支持的运算 | |
---|---|
iter + n 或 iter += n | 迭代器向后移动 n 个元素(字符),指向容器(或 string 对象)的另一个元素(字符),或者变成尾迭代器 |
iter - n 或 iter -= n | 迭代器向前移动 n 个元素(习惯),指向容器(或 string 对象)的另一个元素(字符),或者变成尾迭代器 |
iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离(是一个数),这两个迭代器必须指向同一个容器 |
>, >=, <, <= | 迭代器的关系运算符,这两个迭代器必须指向同一个容器 |
由上面可知,迭代器中间只有减法操作,没有加法操作,所以不要写出形如:iter1 + iter2 的语句。
头文件:
#include
下表是定义 string 类对象的方法,假定 s 是一个 C 风格的字符串(末尾含有字符 ‘\0’),而 str 是一个已定义的 string 类型对象:
string 类构造函数 | |
---|---|
string s1 | 默认初始化,s1 是一个空串 |
string s1(s) | 用 s 显式初始化 s1,s1 不包含 s 末尾的 ‘\0’ |
string s2(str) 或 string s2 = str | 用 str 来初始化 s2 |
string s3(“value”) 或 string s3 = “value” | 用一个字符串常量显式初始化 s3,s3 是 |
string s4(n, c) | 把 s4 初始化为由 n 个连续的字符 c 组成的字符串 |
string s5 = s4 + “!!!” | 把字面值和 string 对象相加得到的结果拷贝给 s5 |
string s6(s, n) | 把 s 指向的字符串的前 n 个字符拷贝给 s6;如果 n 大于 s 的长度,则 s6 的大小等于 n,且末尾会用空字符 ‘\0’ 补充 |
string s7(str, pos, n) | 把 str 中从位置 pos 开始的 n 个字符拷贝给 s7;最多拷贝到 str 的末尾,即末尾不会用空字符补充 |
string s8(initializer_list |
将 s8 初始化为初始化列表 il 中的字符 |
要注意几点:
os << s | 将 s 写到输出流 os 当中,返回 os |
is >> s | 从 is 中读取字符串赋给 s,字符串以空白分隔,返回 is |
getline(is, s) | 从 is 中读取一行赋给 s,返回 is |
s.empty() | s 为空返回 true |
s.size() | 返回 s 中字符的个数,返回的是一个无符号整型数 |
s[n] | 返回 s 中第 n 个字符的引用,可以理解为数组下标 |
s1 + s2 | 返回 s1 和 s2 连接后的结果 |
s1 = s2 | 用 s2 中的内容替换 s1 中的内容 |
s1 == s2 | 如果 s1 和 s2 中的字符完全一样(包括大小写)返回 true,否则返回 false |
s1 != s2 | 如果 s1 和 s2 中的字符不完全一样(包括大小写)返回 true,否则返回 false |
<, <=, >, >= | 利用字典序进行比较,区分大小写 |
可直接通过 cin 和 cout 来读取和打印 C 风格字符串或 string 对象。
char s[100];
cin >> s;
string str;
cin >> str;
cout << str << endl;
但是要注意的是,使用标准输入 cin 读取 C 风格字符串或string 对象时会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个可见字符开始读起,直到遇见下一处空白为止。比如你输入“ Hello World!”,那么输出的就是 “Hello”,输出中没有任何空格。
如果想读取字符串中的空白,可以使用 getline 函数。对于 C 风格字符串,getline 函数的参数是一个指向字符串的指针和字符串的最大大小。对于 string 对象,getline 函数的参数是一个输入流和一个 string 对象。
getline 函数从输入流中读入内容,直到遇到换行符为止(换行符也读入);接着把所读的内容存入到字符串里(不存入换行符)。getline 也会返回它的流参数,可以用作判断的条件,当然也可以直接用 getline 的结果作为条件。
char s[100];
cin.getline(s, 100); // 读取一行字符串并存入 s,抛弃输入末尾的换行符
cin.get(s, 100); // 读取一行字符串并存入 s,输入末尾的换行符依然在输入流中
string str;
while (geline(cin, str))
{ // 每次读入一整行,直至到达文件末尾
cout << str << endl;
}
对于 C 风格字符串的 getline 函数而言,其最大的特点是需要指定读入字符串的最大大小,如果输入的字符超过了100,则最多只会存入前100字符。但是如果直接使用标准输入 cin 来读取 C 风格字符串,如果你输入了超过100个字符,编译器会自动扩充 s 的大小以保存你输入的字符,类似于读取 string 对象。这就是 C++ 新标准对于 C 风格字符串做出的一个适配。
仔细分辨两者对于 getline 函数的使用可以发现,读取 C 风格字符串的 getline 函数是 istream 类下的方法,cin 作为调用对象调用 getline;而读取 string 对象的 getline 函数是独立的函数,cin 作为 getline 的参数传入。两者设计上的不同也就导致了使用方法上的区别。
除此之外,getline 函数还有第三个可选参数(C/C++ 的 getline 版本都有),该参数可以指定使用哪个字符来确定输入的边界:
char s[MAXLEN];
cin.getline(s, MAXLEN, ':');
cout << s;
string line;
getline(cin, line, ':');
cout << line;
如果你输入的是 “nihao:ya”,最后的输出结果将会是 “nihao”。
需求头文件 #include< cctype> | |
---|---|
isalnum(a) | 当 a 是字母或数字时为真 |
isalpha(a) | 当 a 是字母时为真 |
iscntrl(a) | 当 a 是控制字符时为真 |
isdigit(a) | 当 a 是数字时为真 |
isgraph(a) | 当 a 不是空格但可打印时为真 |
islower(a) | 当 a 是小写字母时为真 |
isprint(a) | 当 a 是可打印字符时为真(空格或具有可视形式) |
ispunct(a) | 当 a 是标点符号时为真(非控制字符、数字、字母、可打印空白等) |
isspace(a) | 当 a 是空白时为真(空格、横向或纵向制表符、回车符、换行符或进纸符) |
isupper(a) | 当 a 是大写字母时为真 |
isxdigit(a) | 当 a 是是十六进制数字时为真 |
tolower(a) | 当 a 是大写字母,输出对应的小写字母;否则原样输出 c |
toupper(a) | 当 a 是小写字母,输出对应的大写字母;否则原样输出 c |
int a[] = {0, 1, 2, 3}; // 忽略了 [] 中的维度
int a[] = {0, 1, 2};
int b[] = a; // 错误
int &a[10];
可以用字符串字面值初始化字符数组,但要注意结尾的空字符需要占用一个空间。
char str1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char str2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
char str3[] = "C++"; // 自动添加空字符
char str4[6] = "nihao!"; // 错误:没有空间可以存放空字符
str4 字符数组的大小至少是7才行。
int *a[10]; // a 是含有 10 个整型指针的数组
int (*b)[10] = &arr; // b 指向一个含有 10 个整数的数组
int (&c)[10] = arr; // c 引用一个含有 10 个整数的数组
对于数组声明的理解,最好的办法是由内向外阅读。
以第二条语句为例,*b 表明 b 是一个指针,[10] 表明 b 指向了一个大小为 10 的数组,int 表明该数组是一个 int 型的数组,&arr 表明 b 中的地址就是数组 arr 的地址。综上,b 是一个指针,指向一个名为 arr 的整形数组,数组里有10个元素。第三条语句同理,c 是一个引用,他引用了一个名为 arr 的整形数组,数组里有10个元素。
但是对于第一条语句而言,由于 *a 不是在括号内的,所以它表明整个数组都是指针,所以 a 是一个大小为10的数组,数组内存放的是指向 int 型的指针。
[ ] 运算符的优先级要比 * 高。
对数组的元素使用取地址符就能得到指向该元素的指针:
int a[] = {1, 2, 3};
int *p = &a[0]; // p 指向 a 的第一个元素
数组有一个特性,在很多用到数组名字的地方,编译器会自动地将其替换为一个指向数组首元素的指针,使用数组类型的对象其实是用一个指向该数组首元素的指针:
int *q = a; // 等价于 q = &a[0]
如果希望数组能像迭代器一样的进行遍历,就需要得到一个“尾后指针”,类似于尾后迭代器。可以使用 begin 和 end 函数,begin 函数返回指向 a 首元素的指针,end 函数返回指向 a 尾元素的下一个位置的指针。它们定义在 iterator 头文件中,使用时与迭代器中的稍有不同:
#intclude<iterator>
int a[] = {0, 1, 2, 3, 4, 5};
int *pbeg = begin(a), *pend = end(a);
while (pbeg != pend)
cout << *pbeg++ << endl; // 逐个输出 a 中的所有元素
指向数组元素的指针可以执行迭代器的所有运算。
最后补充一点,尽管标准库类型 string 和 vector 都能执行下标运算,但是它们的下标必须是无符号类型。而内置的下标运算符(数组的下标)可以处理负值,只要结果依然指向数组中的元素即可。如:
int a[] = {1, 2, 3, 4};
int *p = &a[2]; // 令 p 指向数组的第3个元素,即3
int k = p[-2]; // p[-2] 即为 p[0],所以 k 的值是1
只需指明要拷贝区域的首元素地址和尾后地址就可以(左闭右开的区间):
int a[] = {0, 1, 2, 3, 4, 5};
vector<int> v1(begin(a), end(a)); // 使用 begin 和 end 函数来进行拷贝
vector<int> v2(a + 1, a + 4); // 拷贝其中某部分的元素:a[1], a[2], a[3]
int a[2][3] = 0;
int cnt = 0;
for (auto &row : a) // 对于外层数组的每一个元素
for (auto &col : row) // 对于内层数组的每一个元素
{
col = cnt; // 将 cnt 的值赋给每一个元素
++cnt;
}
使用范围 for 语句的话,就把管理数组索引的任务交给了系统来完成。在上述语句中,row 的类型就是含有3个整数的数组的引用。使用引用的原因并不仅仅只为了修改元素的值,事实上,使用范围 for 语句处理多维数组,除了最内层的循环,其他所有循环的控制变量都应该使用引用。原因如下:
for (auto row : a)
for (auto col : row)
如果不使用引用,上述语句将无法通过编译。因为在第一个 for 内,经过 auto 处理后 row 的类型会是指针,显然第二个 for 内就不合法了(因为不能遍历指针)。
在 C 中一般用 typedef 来给各种类型取别名,C++ 新标准下可以使用关键字 using 取别名,using 作为别名声明的开始,其后紧跟别名和等号,作用是把等号左侧的名字规定成等号右侧类型的别名。
typedef int int_array[4]; // 为类型“4个整数组成的数组”取个别名 int_array
using int_array = int[4]; // 上面的等价声明
编写3个不同版本的程序,令其均能输出一个3*3数组的元素。版本1使用范围 for 语句管理迭代过程;版本2和版本3都使用普通的 for 语句,其中版本2要求使用下标运算符,版本3要求用指针。此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype 关键字。
#include
using namespace std;
int main()
{
int a[3][3] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
// 版本1
for (int (&row)[3] : a) // row 引用了一个三维数组
{
for (int col : row) // 对 row 引用的数组进行遍历
cout << col << ' ';
cout << endl;
}
// 版本2
for (int row = 0; row < 3; ++row) // 利用传统的下标遍历
{
for (int col = 0; col < 3; ++col)
cout << a[row][col] << ' ';
cout << endl;
}
// 版本3
for (int (*row)[3] = a; row != a + 3; ++row) // 详见图解
{ // int *col = *row 实际上是令 col 指向了 row 当前所指数组的首元素
for (int *col = *row; col != *row + 3; ++col)
cout << *col << ' ';
cout << endl;
}
return 0;
}
对于版本3的解释如下:由于使用数组名时,编译器会自动转换成首元素的地址,因此 a[0][0] 的地址就传给了 row。但由于 row 是一个指向三维数组的指针,相当于它所指向的数据类型的大小是三个数组元素,所以每次执行 ++row 的操作后 row 便指向了下一行的首元素。
其次,对于 int * 类型,对 int * 取值之后,可以获得 int,也就是指针指向的数据。而 row 的类型为 int ( * )[3],对 int (*)[3] 取值之后,可以获得 int[3]。所以语句 int *col = *row 相当于令 col 指向一个3维数组(首元素)。我们可以使用 sizeof 测量各部分大小来证明这一点。
int (*row)[3] = a;
cout << " a : " << sizeof(a) << endl;
cout << " row : " << sizeof(row) << endl;
cout << " *row : " << sizeof(*row) << endl;
结果如图。我是64位机器,所以指针的大小为8字节,int 型数据的大小为4字节。a 的类型为 int[3][3],是一个二维数组,空间大小是9个 int,即 36个字节。row 的类型为 int(*)[3],是一个指针,所以结果为8个字节。*row 的类型为 int[3],是一个数组,空间大小为3个 int,即12个字节。
希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。