注意:本文并不是教你学 C++,这只是笔者学习 C 过程中记录的相关知识,适用于已经对 C 有所了解的读者进行复习和查询,读者至少对 C 和 C++ 有所了解。
# 小记:
有符号和无符号一起用,有符号转无符号。
double(一般 16 有效位)一般和 float(一般 7 有效位)运算代价相同,用 double 即可。
char16_t 和 char32_t 为 Unicode 字符,包含所有自然语言字符。
泛化的转义序列:
\x
后跟 1 或多个十六进制数字或者\
后跟多个八进制数字:\12
(换行符)\x4d
(字符 M)初始化不是赋值,定义变量时若未指定初始值,变量被默认初始化,“默认值” 由变量类型和定义位置决定。定义在任何函数体之外的内置类型变量会被默认初始化为 0,定义在函数体内部的内置类型变量不被初始化,它的值是未定义的。每个类各自决定初始化对象的方式。
声明使得名字为程序所知,定义负责创建于名字关联的实体。变量能且只能被定义一次,但是可以被多次声明。任何包含了显式初始化的声明即成为定义。多个文件使用同一变量,只能有一个定义,其他文件必须声明才能使用,决不能定义该变量。
extern int i; // 声明i而非定义i int j; // 声明并定义j extern double pi = 3.1415926; // 定义,extern作用被抵消
引用只是一个已经存在的对象的另一个名字,它和它的初始值(一个对象)一直绑定在一起,必须被初始化。对引用的操作就是对于它绑定的对象的操作(可以视为替换)。引用本身不是对象,无法定义引用的引用和指针。
int ival = 1024; int &refVal = ival; refVal = 2;
::
作用域运算符,使用::reuserd
的方式可以访问被局部变量覆盖的全局变量,全局作用域本身没有名字,所以::
左侧为空时会向全局作用域发起请求。把 int 变量直接赋给指针是错误的操作,即使他的值为 0。
对指针的引用:
int i = 21;
int *p;
int *&r = p;
r = &i; // 令p指向i
*r = 0; // 将p指向的变量i的值改为0
当以编译时初始化的方式定义一个 const 对象时,例如:
const int bufSize = 512;
,编译器会将在编译过程中用到该变量的地方都替换成对应的值。默认状态下,const 对象被设定为仅在文件内有效。当多个文件中出现同名 const 变量时,等同于在不同文件中分别定义了独立的变量
当希望 const 变量初始值不是常量表达式,但需要在文件间共享,即和其他对象一样,只在一个地方定义 const 变量,而在其他多个文件中声明并使用它,则不管是声明还是定义都添加 extern 关键字,这样只需要定义一次就可以了。
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问 extern const int bufSize = fcn(); // file_1.h 头文件 extern const int bufSize; // 与file_1.cc 中定义的bufSize是同一个
对 const 对象的引用称之为对常量的引用(常量引用)。不能让一个非常量引用指向一个常量对象。
一般来说引用的类型应该与其所引用的对象的类型一致,但有下面两个例外:
对 const 的引用可能引用一个并非 const 的对象。
int i = 42; int &r1 = i; const int &r2 = i; // r2绑定对象i但是不允许通过r2修改i的值。
初始化常量引用时允许使用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可,尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式:
double i = 3.14; const int &r1 = i; // 正确,double可以转换成int,常量引用可以绑定非常量对象 const int &r2 = 42; // 正确:r2是一个常量引用,可以绑定字面值 const int &r3 = r1 * 2; // 正确:r3是一个常量引用,可以绑定表达式 int &r4 = r1 *2 // 错误:r4是一个普通的非常量引用
以上面第 1、2 行代码为例,当一个常量引用被绑定到另一种类型上时,编译器把 i 转化一个整形常量 3,这个 3 是一个临时量对象,是编译器需要的一个用来暂存表达式求值结果时临时创建的一个未命名的对象。
一般来说指针的类型应该与其所指对象的类型一致,但有下面两个例外:
- 允许一个指向常量的指针指向另一个非常量的对象。
- 常量指针可以指向非常量对象。
顶层 const 表示指针本身是一个常量,底层 const 表示指针所值的对象是一个常量。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转换成常量,反之则不行。
将变量声明为 constexptr 类型,让编译器来验证变量的值是否是一个常量表达式(值不会改变且在编译过程就能得到计算结果的表达式)。
constexpr =int mf = 20;
定义类型别名:
typedef double wages; // wages是double的同义词 typedef wasges base, *p; // base是double的同义词,p是double*的同义词 using SI = Sales_item; // SI是Sales_item的同义词
简单的把类型别名替换成它原本样子可能是错误的,后两句如果进行简单的文本替换,基本数据类型变为 const char .
const char *cstr = 0; // 声明的是一个指向 const char 的指针 typedef char *pstring; // pstring的类型是指向char的指针 const pstring cstr = 0; // cstr是指向char的常量指针 const pstring *ps; // ps是一个指针,他指向的对象是一个指向char的常量指针
auto 一般会忽略掉顶层 const ,同时底层 const 会被保留。想保留,需要用 const atuo 。设置一个类型为 auto 的引用时,初始值中的顶层常量属性仍然被保留。
decltype ( ) 返回()中表达式的类型。如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内)。如果表达式的内容是解引用操作,则 decltype 将得到引用类型。decltype 使用的表达式是变量,该变量是否有括号会影响结果,如果无括号,得到的类型是该变量的类型,如果有括号,得到的是该变量对应的引用类型。
decltype (f()) sum = x; // sum的类型就是函数f的返回类型,编译器并不实际调用f() // decltype的结果可以是引用类型 int i = 42, *p = &i, &r = i; decltype(r + 0) b; // 正确:加法的结果是int,因此b是一个(未初始化的)int decltype(*p) c; // 错误:*p的类型是int,但他是解引用操作,所以c的类型是int&,必须初始化 decltype((i)) d; // 错误:使用(i)得到的是 int& 类型,必须初始化
头文件保护符
#ifndef
、#ifdef
、#endif
应该包含在头文件中,即使头文件(目前还)没有被包含在任何其他头文件中。头文件不应该使用 using 声明。string 字符串使用字符串字面值初始化时,不包含字符串字面值最后隐藏的空字符,字符串字面值实际上包含写出来的字符和隐藏的空字符。
cin 在读取 string 时,string 对象会自动忽略开头的空白(空格符、换行符、制表符等),并从第一个真正的字符开始读起,直到遇见下一次处空白为止。
getline(cin, line)
读取一整行,参数是一个输入流和一个 string 对象,函数从输入流中读入内容,直到遇到换行符为止(换行符也被读进来了),然后把所读的对象存入到 string 对象中(但是不存读入的换行符)。如果一开始就输入换行符,那么得到空 string。触发 getline 函数返回的换行符实际上被丢掉了。sting 操作中的 size () 等函数,返回的数字类型配套的,
size()
返回string:: size_type
类型,一个无符号整型,使用auto
或者decltype()
推断变量类型来存比较好。注意无符号整型和有符号整型混用的问题。string 比较规则:返回两个 string 对象第一个不同字符的比较结果,若全相同,则长的大。
当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是 string,字符串字面值不是 string 对象。
string 的下标运算符接收的参数是 string::size_type 类型的值,返回值是该位置上字符的引用。在范围 for 语句中也需要使用引用来修改 string 中的字符,使用 char 的话没有改变原 string 对象。
string:
// 初始化 string s1; // 默认初始化,空字符串 string s2 = ""; // 拷贝初始化,空字符串 string s3 = "hello"; // 拷贝初始化 string s4("world"); // 拷贝初始化,使用构造函数定义并初始化字符串 string s5(10,'c'); // 直接初始化,是十个c的字符串 string s6 = string(10,'c') // 拷贝初始化,创建了一个临时对象然后拷贝给s6 // 成员函数 s[n] // 返回n位置上的字符的引用 s.empty() // s是否为空,空为真,不空为假 s.length() // 或者 s.size(),返回s中字符个数 s.at(n); // 返回下标为n的字符 s.find("ld"); // 返回 "ld" 在字符串中的位置n s.replace(6, 5, "there"); // 把 "world" 替换成 "there" s.substr(0, 5); // 返回i字符串的前5个字符组成的字符串 s.substr(6); // 返回从字符串的第7个字符到末尾的字符组成的字符串 s.insert(6, "there"); // 在下标为6的位置插入there“,后面字符顺延 s.insert(6, 1, '-'); // 在第6个位置插入 '-',后面字符顺延
使用花括号初始化 vector 对象时,会优先使用列表初始化,提供的值必须与元素类型相同,如果不同,无法执行列表初始化,编译器会尝试使用默认初始化 vector 对象。
一般创建一个空的 vector ,再向里面添加元素更好,直接初始化可能性能更差,同时,如果改变了 vector 容量,不应使用范围 for 循环,范围 for 语句内不应该改变其所遍历序列的大小。不能使用下标形式为 vector 添加元素,只能对已存在的元素执行下标操作。
** 类模板 vector **:
vector 定义在头文件中,需要包含,并位于 std 命名空间中。
vector<double> v; // 创建空容器 vector<string> v1{10}; // 10不是string,使用默认初始化,初始化有10个string的vectoer vector<double> v(20, 1.0); // 直接初始化有20个double的vectoe,初始值均为1.0,没有第二个参数初始值默认均为0,使用花括号也可 vector<int> values2{1,2,3,4,2,1}; // 指定元素个数和初始值 vector<int> values3(values2); // 创建和alces相同的容器 vector<int> values4(begin(value2,begin(v。alue2)+3)) // 使用指针或者迭代器来指定初始值范
vector 容器包含的成员函数:
begin() // 返回指向容器中第一个元素的迭代器。 end() // 返回指向容器最后一个元素所在位置后一个位置的迭代器。 front() // 返回第一个元素的引用。 back() // 返回最后一个元素的引用。 data() // 返回指向容器中第一个元素的指针。 assign() // 用新元素替换原有内容。 push_back() // 在序列的尾部添加一个元素。 pop_back() // 移出序列尾部的元素。 insert() // 在指定的位置插入一个或多个元素。 erase() // 移出一个元素或一段元素。 clear() // 移出所有的元素,容器大小变为 0。 rbegin() // 返回指向最后一个元素的迭代器。 rend() // 返回指向第一个元素所在位置前一个位置的迭代器。 cbegin() // 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 cend() // 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 crbegin() // 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 crend() // 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 size() // 返回实际元素个数 类型是vector<T>::size_type,T为vector存储的元素类型。 max_size()// 返回元素个数的最大值。这通常是一个很大的值,一般是 2^32-1,所以我们很少会用到这个函数。 resize() // 改变实际元素的个数。 capacity()// 返回当前容量。 empty() // 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 reserve() // 增加容器的容量。 shrink _to_fit() // 将内存减少到等于当前元素实际所使用的大小。 operator[ ] // 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。 at() // 使用经过边界检查的索引访问元素。 swap() // 交换两个容器的所有元素。 emplace() // 在指定的位置直接生成一个元素。 emplace_back() // 在序列尾部生成一个元素。
如果容器为空,begin 和 end 都返回尾后迭代器。
标准容器迭代器的部分运算符:
*it // 返回迭代器it所指元素的引用 (*it).empty() // it指示一个strng类型并判断string是否为空,必须加(),否则先进行点运算 it->empty() // 解引用it并获取该元素的名为mem的成员,等价于(*it).empty() ++it // 令it指示容器中的下一个元素 --it // 令it指示容器中的上一个元素 it1 == it2 // 判断两个选代器是否相等,如果两个选代器指示的是同一个元素或者它们是同一个容器的尾后选代器,则相等;反之,不相等 +、-、+=、-=、<、>、<=、>=
某些对 vector 的操作(任何可能改变 vertor 对象容量的操作)会使迭代器失效,比如 push_back,不能在范围 for 循环中向 vector 对象添加元素。
遍历一个 vector :
vector<int> vec = {1,2,3,4}; // 范围for循环 for(const auto& element : vec){ cout << element << endl; } // 迭代器遍历 for(vector<int>::iterator it = vec.begin(); it != vec.end(); ++it){ cout << *it << end; } // 索引遍历 for(size_t i = 0; i < vec.size(); ++i){ cout << vec[i] << endl; }
使用迭代器的二分搜索:
// 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; // 新end为旧mid,beg不变 else // 否则,我们要找的元素在后半部分 beg = mid + 1 // 新beg为旧mid+1,end不变 mid = beg + (end-beg)/2; // 同样方法计算新的中间点
数组的维度(元素个数)必须是常量表达式。默认情况下数据元素北默认初始化。字符数组使用字符串字面值初始化时,后面隐藏的 '\0' 会被包含进去,空间不够时报错。数组不允许拷贝和赋值。
数组本身是对象,存放对象,不存在引用的数组,但存在数组的引用。
unsigned cnt = 42; //不是常量表达式 constexpr unsigned sz = 42;//常量表达式 int *parr[sz]; //含有42个整型指针的数组 string bad[cnt];; //错误:cnt不是常量表达式 string strs[get_size()]; //当get_size是constexpr时正确;否则错误
当数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组;使用 decltype 关键字时返回的类型是数组。
标准库函数 begin () 和 end () 获取数组的首元素指针和尾元素下一指针。两个指针相减的结果类型是 ptrdiff_t 的标准库类型,是带符号类型。如果两个指针分别指向不相关的对象,不能比较他们,未定义行为。
使用数组下标等同于使用指针,同时内置的下标运算(数组下标)可以处理负值,但标准库类型 string 和 vector 的下标运算必须是无符号类型。
尽量少使用 C 风格字符串,内存管理麻烦,多用 string,string 的成员函数 c_str () 可以把 string 转化成 char*。
数组无法拷贝和赋值,不能用 vector 初始化数组,但可以用数组初始化 vector ,只需之名拷贝区域的首元素地址和尾后地址:
int int_arr[] = {0, 1, 2, 3, 4, 5}; // ivec有6个元素,分别是intarr中对应元素的副本 vector<int> ivec(begin(int_arr),end(int_arr));
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
# 运算符
有 4 种运算符明确规定了运算对象的求值顺序。逻辑与(&&)运算符和逻辑或(||)运算符规定先求左侧运算对象的值,只有当左侧运算对象的值为真 / 假时才继续求右侧运算对象的值。条件( ?: )运算符和逗号( , )运算符。其他运算符,如 + 、>> 等未规定左右运算对象的求值顺序,先求左右表达式中的哪一个都有可能。如果表达式影响同一对象,求值先后顺序对结果有影响,那么它是一个错误的表达式,将产生未定义行为。
布尔值不应该参与运算,大多数运算符会把布尔值提升为 int 类型,true 为 1,求负后为 -1 ,再转回布尔值时仍为 1 ,还是 true。
求商向 0 取整(直接切除小数部分)。取余时,除了 -m 导致溢出的情况,m % (-n) 等于 m % n,( -m ) % n 等于 -( m % n ),即左右都为负,结果是两个正数取余后结果取反,一正一负,结果等于两个正数取余。
位运算符的运算对象是 “小整数”,它的值会被自动提升成较大的整数类型”,如何处理负数对象的 “符号位” 依赖于机器,而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。左移在右侧插入 0。右移时,无符号类型左侧插入 0,带符号类型插入符号位的副本或者值为 0 的二进制位,如何选择视具体情况而定。
sizeof 运算符对解引用指针进行运算不会验证指针是否有效,对指针进行运算返回指针本身所占空间大小,对数组返回整个数组所占空间大小。对 sring 或者 vector 运算返回该类型固定部分的大小。sizeof 返回值是一个常量表达式。
逗号( , )运算符按照从左向右的顺序求值,运算结果是右侧表达式的值。
无符号和有符号数一起运算时,无符号类型所占位数大于等于有符号类型,则有符号类型转为无符号类型;如果无符号类型所占位数小于有符号类型,比如 long 和 unsigned int ( long 大于 int 时),则 unsigned int 转为 long
命名的强制类型转换具有如下形式:
cast-name(expression)
,static_cast 是一种只要类型不包含底层 const 都可用。const_cast 只能改变运算对象的底层 const, 不能进行类型转换。switch case 语句中,后面的 case 标签可以使用前面的 case 标签定义过的变量,但是该变量的初始化无效(如果前 case 标签被跳过的话),包括类似 string 的默认初始化。即:不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
# 常用函数
// cctype 头文件中的函数
isalnur(c) // 当c是字母或数字时为真
isalpha(c) // 当c是字母时为真
iscntrl(n) // 当c是控制字符时为真
isdigit(c) // 当c是数字时为真
isgraph(c) // 当c不是空格但可打印时为真
islower(c) // 当c是小写字母时为真
isprint(c) // 当c是可打印字符时为真(即c是空格或c具有可视形式)
ispunct(c) // 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种)
isspace(c) // 当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种)
isupper(c) // 当c是大写字母时为真
isxdigit(c) // 当c是十六进制数字时为其
tolower(c) // 如果c是大写字母,输出对应的小写字母:否则原样输出c
toupper(c) // 如果c是小写字母,输出对应的大写字母:否则原样输出c
# 头文件:
#include <initializer_list>
#include <iostream>
#include <istream>
#include <ostream>
#include <stdexcept>
#include <string>
#include <cstdarg> // 使用va_list可变参数列表
#include <system_error>
#include <vector> // 使用vector容器
#include <string>
#include <cstdlib> // 预处理变量NULL
#include <cctype> // 使用nullptr,isalnum(),isalpha()等
#include <cstddef> // 使用size_t,ptrdiff_t
#include <iterator> // 使用begin(),end()
#include <stdexcept> // 异常类,如runtime_error,p176
# 全局区
#include <cassert> // assert,预处理宏
using std::cin; // using声明,当我们使用cin时,意味着使用的std::cin
using namespace std;// 头文件内容会拷贝到所有引用他的文件里,一般不应包含using声明(易造成名字冲突)
// 如果定义了预处理变量MIN,就继续到#endif,#indef和#indef叫预处理百年
#ifdef MIN
#define MKSTR(x) #x // "x"
#endif
// 如果没有定义预处理变量MIN,就继续到#endif,一般头文件的预处理变量定义都需要这样写
#ifndef MIN
#define MIN(a, b) (a < b ? a : b)
#define concat(a, B) a##b // xy
#define NDEBUG // 定了他 assert什么都不做,没定义则指执行运行时检查
#endif
extern const int i = 123; // a文件里定义并初始化i,加extern使其能被其他文件使用
# 数据结构
stack 容器
头文件,c 在 std 命名空间。stack 容器适配器的模板有两个参数。第一个参数是存储对象的类型,第二个参数是底层容器的类型。stack 的底层容器默认是 deque 容器,因此模板类型其实是 stack。通过指定第二个模板类型参数,可以使用任意类型的底层容器,只要它们支持 back ()、push_back ()、pop_back ()、empty ()、size () 这些操作。下面展示了如何定义一个使用 list 的堆栈:
stack<string> words1; // 省略第一个参数,用默认的底层容器deque<T>实现 stack<string, list<string>> words2 // 底层容器使用list<T>
创建堆栈时,不能用对象来初始化,但是可以用另一个容器来初始化,只要堆栈的底层容器类型和这个容器的类型相同,且必须使用圆括号。例如:
list<double> values {1.414, 3.14159265, 2.71828}; stack<double, list<double>> my_stack (values);
第二条语句生成了一个包含 value 元素副本的 my_stack。这里不能在 stack 构造函数中使用初始化列表,必须使用圆括号。如果没有在第二个 stack 模板类型参数中将底层容器指定为 list,那么底层容器可能是 deque,这样就不能用 list 的内容来初始化 stack;只能接受 deque。
stack 模板定义了拷贝构造函数,因而可以复制现有的 stack 容器:
stack copy_stack {my_stack}
堆栈操作:(T 为栈中元素类型)
push (const T& obj) 可以将对象副本压入栈顶。这是通过调用底层容器的 push_back () 函数完成的。
- pop () 弹出栈顶元素但没有返回它。
top () 返回一个栈顶元素的引用但没有弹出,类型为 T&。如果栈为空,返回值未定义。
size () 返回栈中元素的个数。
empty () 在栈中没有元素的情况下返回 true。
emplace () 用传入的参数调用构造函数,在栈顶生成对象。
swap (stack & other_stack) 将当前栈中的元素和参数中的元素交换。参数所包含元素的类型必须和当前栈的相同。对于 stack 对象有一个特例化的全局函数 swap () 可以使用。
类模板
vector 定义在头文件中,需要包含,并位于 std 命名空间中。
vector<double> values; // 创建空容器 vector<double> values1(20, 1.0); // 开始就有20个int,初始值均为1.0,没有第二个参数初始值默认均为0 vector<int> values2{1,2,3,4,2,1}; // 指定元素个数和初始值 vector<int> values3(values2); // 创建和alces相同的容器 vector<int> values4(begin(value2,begin(v。alue2)+3)) // 使用指针或者迭代器来指定初始值范围
vector 容器包含的成员函数:
begin() // 返回指向容器中第一个元素的迭代器。 end() // 返回指向容器最后一个元素所在位置后一个位置的迭代器。 front() // 返回第一个元素的引用。 back() // 返回最后一个元素的引用。 data() // 返回指向容器中第一个元素的指针。 assign() // 用新元素替换原有内容。 push_back() // 在序列的尾部添加一个元素。 pop_back() // 移出序列尾部的元素。 insert() // 在指定的位置插入一个或多个元素。 erase() // 移出一个元素或一段元素。 clear() // 移出所有的元素,容器大小变为 0。 rbegin() // 返回指向最后一个元素的迭代器。 rend() // 返回指向第一个元素所在位置前一个位置的迭代器。 cbegin() // 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 cend() // 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 crbegin() // 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 crend() // 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 size() // 返回实际元素个数。 max_size()// 返回元素个数的最大值。这通常是一个很大的值,一般是 2^32-1,所以我们很少会用到这个函数。 resize() // 改变实际元素的个数。 capacity()// 返回当前容量。 empty() // 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 reserve() // 增加容器的容量。 shrink _to_fit() // 将内存减少到等于当前元素实际所使用的大小。 operator[ ] // 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。 at() // 使用经过边界检查的索引访问元素。 swap() // 交换两个容器的所有元素。 emplace() // 在指定的位置直接生成一个元素。 emplace_back() // 在序列尾部生成一个元素。
哈希表
头文件
<unordered_map>
,在 std 名命名空间中。它将数据存储为键值对,其中键是唯一的。 声明和初始化:unordered_map<string, int> map1