C++ 学习笔记(一)(标准库类型 vector、string 篇)

前言:主要是自己学习过程的积累笔记,所以跳跃性比较强,建议先自学后拿来作为复习用。

1 初始化的相关知识

1.1 默认初始化和显式初始化

  • 定义变量时直接给出初始值称为显式初始化
  • 内置类型定义变量时如果没有指定初值,则变量会被默认初始化默认值由变量类型和位置来决定。例如 int 型默认值为0,string 型默认值为空串。
  • 一个例外就是,内置类型的变量未被显式初始化时,若它是定义在函数体的内部(包括 main 函数) 将不会被默认初始化,此时对它进行拷贝或者访问将会出错。一般地,只有全局变量才会进行默认初始化(在一些特定的语句中也可以执行默认初始化)。
  • 对于类来说,其自行决定初始化对象的方式。

1.2 普通初始化和列表初始化

定义一个 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}; 	// 错误:存在丢失信息的风险,列表初始化不执行

1.3 拷贝初始化和直接初始化

使用赋值号(=)初始化一个变量即为拷贝初始化,不用则为直接初始化:

string a = "hi";	// 拷贝初始化
string b("hi");		// 直接初始化
string c(3, 'i');	// 直接初始化,c 的内容是 iii

1.4 值初始化

在初始化标准库容器的时候,如果不提供显式的初始值,则编译器会创建一个元素初值,并把它赋给容器中的所有元素,初值的选取与默认初始化是一致的。但要注意将其与默认初始化区分开来:值初始化相当于容器上的默认初始化

2 标准库容器 Vector

2.1 vector 的定义

头文件:

#include<vector>
  • vector 是类模板,如下:
vector<int> a;			// a 保存 int 类型的对象
vector<vector<int>> b;	// b 保存 vector<int> 类型的对象

上面的代码中,编译器根据模板 vector 生成了两种不同的类型:vector 和 vector<vector<int>> 。

初始化 vector 的方法
vector<T> a;a 是一个 vector,元素类型为 T,采用默认初始化
vector<T> b(a);b 是一个 vector,包含 a 中所有元素的副本
vector<T> b = a;等价于上一条语句
vector<T> c(n, val);c 包含 n 个重复的元素,值都为 val
vector<T> d(n);d 包含了 n 个重复地执行了值初始化的对象
vector<T> e{1, 2, 3…};e 是一个vector,用 1, 2, 3…等进行初始化
vector<T> e = {1, 2, 3…};等价于上一条语句
vector<T> v[n];v 是一个长度为 n 的 vector,它每个元素都是一个 vector<int>

用圆括号时,提供的值是用来构造 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");	// 错误,不能使用圆括号

2.2 vector 的相关操作

  • 向 vector 中添加元素:
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 == ba 和 b 元素个数和对应位置上的元素完全相同时返回真,否则返回假
a != ba 和 b 不相等时返回真,否则返回假
<, <=, >, >=以字典序进行比较

2.3 迭代器

类似于指针,迭代器可以实现对对象的间接访问。其对象是容器中的元素或者 string 对象中的字符。迭代器可以访问对象中的某个元素,或者从一个元素移动到另一个元素。当迭代器有效时,其指向某个元素,或者指向容器尾元素的下一个位置;其他情况都属于无效迭代器。

begin 和 end

有迭代器的类型同时拥有返回迭代器的成员,比如 begin 返回指向第一个元素(字符)的迭代器;end 返回指向容器(或 string 对象)尾元素的下一个位置的迭代器,也被称为尾后迭代器尾迭代器。如果容器为空,则 begin 和 end 返回的都是尾后迭代器。

iterator b = v.begin(), c = v.end();

2.4 迭代器运算符

标准容器迭代器的运算符
*iter返回迭代器 iter 所指元素的引用
iter -> mem解引用 iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem(括号不能少)
++iter令 iter 指向容器(或 string 对象)的下一个元素(字符)
- -iter令 iter 指向容器(或 string 对象)的上一个元素(字符)
iter1 == iter2两个迭代器指向的元素(字符)相同,或者都是同一个容器的尾后迭代器时相等,返回 true
iter1 != iter2两个迭代器指向的元素(字符)不相同时返回 true

2.5 迭代器的类型

迭代器的类型是 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

凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

2.6 迭代器的运算

vector 和 string 迭代器支持的运算
iter + n 或 iter += n迭代器向后移动 n 个元素(字符),指向容器(或 string 对象)的另一个元素(字符),或者变成尾迭代器
iter - n 或 iter -= n迭代器向前移动 n 个元素(习惯),指向容器(或 string 对象)的另一个元素(字符),或者变成尾迭代器
iter1 - iter2两个迭代器相减的结果是它们之间的距离(是一个数),这两个迭代器必须指向同一个容器
>, >=, <, <=迭代器的关系运算符,这两个迭代器必须指向同一个容器

由上面可知,迭代器中间只有减法操作,没有加法操作,所以不要写出形如:iter1 + iter2 的语句。

3 标准库容器 String

3.1 string 的定义

头文件:

#include<string>

下表是定义 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<char> il) string s8 = {initializer_list<char> il}将 s8 初始化为初始化列表 il 中的字符

要注意几点:

  • string 对象不会像 C 语言的字符数组在末尾存储字符 ‘\0’。
  • 定义字符数组时,数组大小至少要比字符串长度大1。例如 char s[5] = “nihao”; 是错误的,因为末尾的 ‘\0’ 字符也占要用数组的一个单位,因此字符数组的长度至少得是6。但是,在使用 strlen(s) 打印字符串长度时得到的是5,因为末尾的字符虽然占用一个单位,但不计入字符串的长度
  • 关于 initializer_list<char> il 的知识可参考C++ 学习笔记(四)(函数篇)2.6 可变形参

3.2 string 的相关操作

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
<, <=, >, >=利用字典序进行比较,区分大小写

3.3 读写 C 风格字符串或 string 对象

可直接通过 cin 和 cout 来读取和打印 C 风格字符串或 string 对象。

char s[100];
cin >> s;

string str;
cin >> str;
cout << str << endl;

但是要注意的是,使用标准输入 cin 读取 C 风格字符串或string 对象时会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个可见字符开始读起,直到遇见下一处空白为止。比如你输入“ Hello World!”,那么输出的就是 “Hello”,输出中没有任何空格。

3.4 用 getline 读取空白

如果想读取字符串中的空白,可以使用 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”。

3.5 常用的 string 对象处理函数

需求头文件 #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

4 C++ 中的数组

  • 只有对数组进行列表初始化时才可以忽略数组的维度。
int a[] = {0, 1, 2, 3}; // 忽略了 [] 中的维度
  • 不能把数组拷贝给其他数组(类似于 vector)的操作,比如:
int a[] = {0, 1, 2};
int b[] = a; // 错误
  • 数组的元素都是对象,所以不存在引用的数组(即数组元素是引用),即:
int &a[10];
  • 不允许使用 auto 关键字定义数组。

4.1 字符数组的特殊性

可以用字符串字面值初始化字符数组,但要注意结尾的空字符需要占用一个空间。

char str1[] = {'C', '+', '+'};         // 列表初始化,没有空字符
char str2[] = {'C', '+', '+', '\0'};   // 列表初始化,含有显式的空字符
char str3[] = "C++";                   // 自动添加空字符
char str4[6] = "nihao!";               // 错误:没有空间可以存放空字符

str4 字符数组的大小至少是7才行。

4.2 如何理解数组声明

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 型的指针。

[ ] 运算符的优先级要比 * 高。

4.3 指针和数组

对数组的元素使用取地址符就能得到指向该元素的指针:

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

4.4 使用数组初始化 vector

只需指明要拷贝区域的首元素地址和尾后地址就可以(左闭右开的区间):

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]

4.5 使用范围 for 语句处理多维数组

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 内就不合法了(因为不能遍历指针)。

4.6 类型别名

在 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<iostream>
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个字节。

在这里插入图片描述


希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值