目录
- 一、命名空间
- 1. 创建命名空间
- 2. 访问命名空间
- 2.1 using 编译指令
- 2.2 using 声明
- 2.3 直接使用全名
- 3. 嵌套命名空间
- 4. 匿名命名空间
- 5. 命名空间的注意事项
- 5.1 头文件中不应该包含 using 声明和 using 编译指令。
- 5.2 最好使用 using 声明而不是 using 编译指令
- 二、标准库类型 string
- 1. 定义和初始化 string 对象
- 2. string 对象上的操作
- 2.1 string 的输入输出
- 2.2 使用 getline 读取一行
- 2.3 string 的 empty() 和 size() 方法
- 2.4 string 对象的比较
- 2.5 string 对象的拷贝和拼接
- 2.6 string 和 c 风格字符串的区别
- 3. 处理 string 对象中的字符
- 三、标准库类型 vector
- 1. 定义和初始化 vector 对象
- 2. vector 对象上的操作
- 2.1 push_back() 尾插
- 2.2 empty() 和 size()
- 2.3 通过下标访问 vector
- 四、迭代器
- 1. 迭代器的使用
- 2. 迭代器能进行的操作
- 3. 迭代器的类型
- 4. 迭代器的运算
- 5. 迭代器的注意事项
- 6. 范围 for
- 五、数组
- 1. 定义和初始化数组
- 1.1 字符数组的特殊性
- 2. 访问数组元素
- 2.1 下标运算符
- 2.2 范围 for
- 3. 指针和数组
- 4. 让指针想迭代器一样访问数组元素
- 5. 标准库函数 begin() 和 end()
- 6. 下标运算符的差异
- 7. 使用数组初始化 vector 对象
- 7. 多维数组
- 7.1 多维数组的初始化
- 7.2 多维数组的使用
- 六、C 风格字符串
- 1. C 标准库 String 函数
- 2. 混用 string 对象和 C 风格
- 3. 使用字符串的建议
一、命名空间
使用命名空间是为了在大型程序中防止各个模块或代码中的标识符冲突。如:小王和小李的模块中各有一个 play() 函数,使用命名空间就不会造成冲突,如:wang::play() 和 li::play() 。
1. 创建命名空间
使用关键字 namespace 来创建命名空间。如下代码:
// name1 命名空间的声明
namespace name1
{
int a = 10;
void play();
}
// name1 命名空间函数的定义
void name1::play()
{
// ...
}
上述代码创建了命名空间 name1,其内包含 int 变量 a 和函数 play() 及其实现。
2. 访问命名空间
有三种方式访问命名空间中的成员,using 编译指令,using 声明,直接显示使用全名。
2.1 using 编译指令
using namespace + 命名空间的名称。导入命名空间中所有的名称。如下代码:
using namespace name1;
a = 10; // 使用的是 name1 中的变量 a
上述代码导入命名空间 name1 中的所有名称,代码 a = 10 相当于 name1::a = 10 。
2.2 using 声明
using + 命名空间的名称 + 需要使用的名称。导入命名空间中的该单个名称。如下代码:
using name1::a;
a = 10;
上述代码导入了命名空间 name1 的变量 a,代码 a = 10 相当于 name1::a = 10 。
2.3 直接使用全名
如下代码:
name1::a = 10;
上述代码直接使用作用域运算符(::)显式说明变量 a 是命名空间 name1 中的变量 a 。
3. 嵌套命名空间
命名空间里面可以存放命名空间,形成嵌套。如下代码:
// 嵌套命名空间
namespace qcx
{
namespace play
{
int a = 10;
}
}
如果想要使用上述代码中的变量 a,则需要使用两次作用域运算符(::)。
如下:
(1)using 编译指令
using namespcae qcx::play;
(2)using 声明
using qcx::play::a;
(3)直接使用全名
cout << qcx::play::a << endl;
4. 匿名命名空间
如果定义命名空间时没有指定命名空间的名称,则称为匿名命名空间。在同一个编译单元内(文件),匿名命名空间的标识符具有内部链接性,类似于具有 static 修饰的变量和函数。如下代码:
5. 命名空间的注意事项
5.1 头文件中不应该包含 using 声明和 using 编译指令。
倘若头文件中包含上述两种访问命名空间的方法,则每个包含该头文件的文件都会有上述代码。可能会和当前文件中创建的标识符产生冲突。
5.2 最好使用 using 声明而不是 using 编译指令
使用 using 声明可以逐个导入你所需要使用的标识符,不易和自身所创建的标识符产生冲突。而使用 using 编译指令直接导入整个命名空间,增加了标识符冲突的机会。而且使用 using 声明时,如果标识符冲突,编译器会发出警告。而使用 using 编译指令时,自身创建的标识符会隐藏命名空间里面的同名标识符。
二、标准库类型 string
标准库类型 string 表示可变长的字符序列——字符串。使用该类型必须包含头文件 string,且其位于标准命名空间 std 中。如若使用 string 类型,需包含如下两行代码:
// 头文件
#include <string>
// using 声明
using std::string;
1. 定义和初始化 string 对象
string 是标准库定义的类类型,类对象的创建都需要调用该类的构造函数,以下是一些创建 string 对象的方式:
string s1; // 默认初始化,s1 是一个空串
string s2(s1); // s2 是 s1 的副本
string s2 = s1; // 等价于第二条语句,s2 是 s1 的副本
string s3("value"); // s3 是字符串字面值"value"的副本
string s3 = "value"; // 等价于第四条语句,s3 是字符串"value"的副本
string s4(n, 'c'); // 把 s4 初始化为由连续 n 个字符 c 组成的串
一般而言,还没学类之前,使用这几条语句:string s1,string s2 = s1,string s3 = “value” 。学习了类之后,就习惯使用 string s2(s1),string s3(“value”) 。上述初始化代码中,如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化。直接初始化需要对应相关的构造函数,拷贝初始化当涉及动态内存分配时需要重新定义复制构造函数。
2. string 对象上的操作
一般而言,如果想对类进行操作,需要自己去定义相关操作的函数。而标准库中的 string 类已经设计的足够完美,下面是一些常用的操作:
// 创建 string 对象
string s;
// 可以执行的操作
cin >> s; // 输入
cout << s; // 输出
getline(cin, s); // 读取一行输入
s.empty(); // s 为空返回 true,不为空返回 false
s.size(); // 返回 s 的字符个数
s[n]; // 返回 s 中第 n 个字符的引用,从 0 开始计数
s1 + s2; // 返回 s1 和 s2 拼接后的结果
s1 = s2; // 把 s2 中的内容拷贝到 s1 中
s1 == s2; // s1 和 s2 相同返回 true,否则返回 false
s1 != s2 // 和 上一条语句相反
<,<=,>,>= // 利用字符在字典中的顺序进行比较
上述内容均对字母的大小写敏感。
2.1 string 的输入输出
输入 string 对象和输入 c 风格字符串一样,都是跳过前面的空白直到遇到第一个非空白字符,再次遇到空白字符停止输入,可以理解为从标准输入读取一个单词。输出也是一致的。如下代码:
输入时跳过前面的空白,只读取单词"hello"。和 int 等内置类型一样,string 也支持连续输入和连续输出。
2.2 使用 getline 读取一行
该函数遇到换行符就结束(‘\n’),哪怕是一开始就遇到换行符也是如此。如果一开始就输入换行符,那么得到的就是一个空 string 。getline 函数读取该换行符后丢弃,并不存入该 string 对象中,最终的到的 string 对象并不包含换行符。如下代码:
和 cin 一样,getline 函数也能检测文件结尾(EOF)。
2.3 string 的 empty() 和 size() 方法
empty() 方法检测该 string 对象是否为空,为空返回 true,否则返回 false 。如下代码:
类中的方法一般都是类对象通过成员运算符(.)进行调用。
size() 方法返回该 string 对象的字符个数。如下代码:
2.4 string 对象的比较
如下代码:
通过代码可以看到,string 对象之间进行比较是从第一个字符开始按照字典顺序进行比较,直到有一对字符出现不同。若其中一个字符串是另一个字符串的子串,则长度大的字符串大。c 风格字符串使用函数 strcmp() 进行比较。而上述代码中,string 对象能直接进行赋值操作,而 c 风格字符串一旦初始化,就不能直接赋值,因为 c 风格字符串使用字符数组来存储字符串,数组名实际上是该字符串第一个字符的地址。
2.5 string 对象的拷贝和拼接
string 对象可以直接进行拷贝与拼接,而 c 风格字符串需要使用函数 strcpy() 和 strcat() 分别进行拷贝与拼接。如下代码:
这里分别使用 string 对象和 c 风格字符串进行拷贝和拼接操作,结果都是输出字符串"123456"。明显 string 对象使用起来更加简单。
2.6 string 和 c 风格字符串的区别
(1)c 风格字符串创建时必须指定容量,且后续不能修改。而 string 对象不需要指定容量,它可以根据存储的字符串长度自动调节容量。
(2)c 风格字符串不能直接进行赋值、比较、拼接等操作,需要通过相关函数间接实现。而 string 对象可以通过其类方法直接进行相关操作。
(3)string 的操作通过会进行边界检查,更加安全。而 c 风格字符串进行访问时容易产生越界访问。
综上所述,string 提供了更加高级、方便和安全的字符串处理方式,一般使用 string 来处理字符串较好。而 c 风格字符串在某些特定的场景仍有用途(如和 c 语言交互)。
3. 处理 string 对象中的字符
在C++中可以通过 cctype 头文件中的函数对字符串中的字符进行处理。如下所示:
// 头文件
#include <cctype>
// 字符变量
char ch;
// 字符处理函数
isalnum(ch); // 当 ch 为字母或者数字时返回 true,否则返回 false
isalpha(ch); // 当 ch 为字母时返回 true
iscntrl(ch); // 当 ch 为控制字符时返回 true
isdigit(ch); // 当 ch 为数字时返回 true
isgraph(ch); // 当 ch 不为空格但可打印时返回 true
islower(ch); // 当 ch 为小写字母时返回 true
isprint(ch); // 当 ch 为可打印字符时(即 ch 为空格或具有可视形式)返回 true
ispunct(ch); // 当 ch 为标点符号时返回 true
isspace(ch); // 当 ch 为空白字符时返回 true
isupper(ch); // 当 ch 为大写字母时返回 true
isxdigit(ch); // 当 ch 为十六进制数字时返回 true
tolower(ch); // 如果 ch 是大写字母,则输出其小写字符,否则原样输出
toupper(ch); // 如果 ch 是小写字母,则输出其大写字母,否则原样输出
不管是 c 风格字符串还是 string 对象都可以通过下标运算符来获取其中的字符。如下代码:
上述代码通过下标运算符和 cctype 头文件中的函数,输出字符串 temp 中的大写字母。
三、标准库类型 vector
vector 与数组相似,它可以容纳多个类型相同的对象,也可以通过下标对每个元素进行访问。由于 vector “容纳着” 其他对象,所以也被称为容器。C++中既有函数模版也有类模版,而实际上 vector 是一个类模板。vector 在头文件 vector 中,且定义在命名空间 std 中,若想要使用 vector 需要包含如下代码:
// 头文件
#include <vector>
// using 声明
using std::vector;
1. 定义和初始化 vector 对象
定义 vector 对象必须显式指出其元素的类型,如下代码:
vector<T> v1; // v1 是一个空的 vector,其元素个数为 0,但是其元素类型为 T
vector<T> v2(v1); // v2 是 v1 的副本,两个 vector 一模一样
vector<T> v2 = v1; // 和上一条语句一致
vector<T> v3(n, val); // v3 包含 n 个元素,元素的类型为 T,每个元素的值均为 val
vector<T> v4(n); // v4 包含 n 个元素,元素类型为 T
vector<T> v5{a, b, c...}; // v5 包含该初始化列表的元素
vector<T> v5 = {a, b, c...}; // 和上一条语句一致
上述代码中,元素个数可以不指出,但是元素类型必须指出。
2. vector 对象上的操作
// 创建 vector 对象
vector<T> v;
// 对 vector 对象的操作
v.empty(); // 如果 v 中没有任何元素返回 true,否则返回 false
v.size(); // 返回 v 中当前的元素个数
v.push_back(t); // 把元素 t 插入 v 的末端(尾插)
v[n]; // 返回 v 中第 n 个位置上的元素的引用
v1 = v2; // 把 v2 拷贝到 v1 中,v1 和 v2 完全相同
v1 = {a, b, c...}; // 把列表中的元素拷贝到 v1 中,v1 和列表中的元素完全相同
v1 == v2; // v1 和 v2 完全相同返回 true,否则返回 false
v1 != v2; // 和上一条语句相反
<,<=,>,>= // 以字典的顺序进行比较
上述是一些 vector 对象可能会用到的操作,下面介绍一些常用的操作。
2.1 push_back() 尾插
当不知道初始化 vector 对象容量为多大时,一般可以创建一个容量为 0 的 vector 对象,然后往里面插入元素。如下代码:
vector 和 string 一样,可以自动处理存储空间的问题,当容量不够它们可以自动增容。
2.2 empty() 和 size()
这两个函数的用法和 string 差不多,上代码:
默认初始化的 vector 中没有元素,待插入元素。
2.3 通过下标访问 vector
通过下标拿到 vector 中元素的引用,然后对其进行访问。
上述代码通过下标对 vector 对象 vt_i 中的元素的值变为了原来的 2 倍并显示改变后的值。
四、迭代器
除了使用下标来对 string 对象和 vector 对象中的元素进行访问之外,迭代器这种更通用的机制也可以实现同样的目的。前面介绍了 vector 属于容器,除了 vector 之外,标准库还定义了其他容器,所有的容器都支持使用迭代器,但只有少数容器支持下标运算符。
迭代器与指针类似,迭代器也可以对对象进行间接访问。对迭代器而言,其对象是容器中的元素(string 中的字符)。迭代器可以访问某个元素,也可以从一个元素移动到另一个元素。迭代器有有效和无效之分,指针也是如此。有效迭代器指向某个元素或容器中尾元素的下一个元素。其他情况均为无效迭代器。
1. 迭代器的使用
使用迭代器需要使用到容器的两个方法,即 begin() 和 end() 。其中,begin() 方法返回指向容器第一个元素的迭代器,end() 方法返回指向容器尾部元素的下一个元素,该位置实际没有什么意义,只是做一个标记,一般称该元素为尾后元素,该迭代器为尾后迭代器。如果该容器为空,则 begin() 和 end() 均返回尾后迭代器。虽然现在不知道迭代器的类型,但是可以使用 auto 来定义迭代器变量,如下代码:
vector<int> vt_i;
// 插入元素
for (int i = 0; i < 10; ++i)
vt_i.push_back(i);
// 使用迭代器
auto begin = vt_i.begin(); // 首元素迭代器
auto end = vt_i.end(); // 尾后迭代器
2. 迭代器能进行的操作
假设 iter 是一个迭代器,下面是可以对其进行的操作:
*iter // 返回迭代器 iter 所指向元素的引用
iter->mem // 通过指针获取 iter 指向元素的成员 mem,等价于 (*iter).mem
++iter // 指向容器中 iter 指向元素的下一个元素
--iter // 指向容器中 iter 指向元素的上一个元素
iter1 == iter2 // 判断两个迭代器是否相等,如果两个迭代器指向同一个元素或者同一个尾后元素则相等,否则不相等
iter1 != iter2 // 和上一条语句相反
有了上述操作,便可以通过迭代器来访问容器的元素了。如下代码:
这里需要注意尾后迭代器不动,首元素迭代器向后移动。当处理完当前容器最后一个元素,两个迭代器均为尾后迭代器,跳出循环。
3. 迭代器的类型
拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型。顾名思义,两种迭代器都可以访问容器中的元素,但是前者可以对容器中的元素进行修改,而后者不能修改。和前面讲的 const 类似。每个容器类都定义了属于自身的迭代器,当我们使用时需声明该迭代器属于哪个容器。如下代码:
上述代码使用了普通迭代器,可以修改容器中元素的值。如果把上述迭代器改成 const 迭代器编译器会报错,如下代码:
4. 迭代器的运算
iter 是一个迭代器,如下是 iter 能进行的运算:
iter + n // 返回 iter 向后移动 n 个元素的迭代器
iter - n // 返回 iter 向前移动 n 个元素的迭代器
iter += n // iter 向后移动 n 个元素,相当于 iter = iter + n
iter -= n // iter 向前移动 n 个元素,相当于 iter = iter - n
iter1 - iter2 // 返回两个迭代器之间的距离(可以是负的)
>、>=、<、<=
上述运算均需要有效迭代器,且涉及两个迭代器之间的运算时,两个迭代器须指向同一个容器。
5. 迭代器的注意事项
(1)不能对无效迭代器进行解引用操作。
上述代码中试图对尾后迭代器进行解引用操作,导致程序崩溃。
(2)不能在使用迭代器的循环体中对容器进行添加元素的操作,不然会导致迭代器失效。
简单解释一下,使用迭代器之后,该迭代器标就记了这块空间。然后对该容器进行添加元素的操作,会导致该容器进行增容,释放原来的空间,寻找新的足够的空间,那么迭代器原来标记的位置就失效了。
6. 范围 for
范围 for 实际上就是迭代器,只不过在使用上面更加方便,在编译过程中,编译器会将其替换成对应的迭代器。如下代码:
这里通过变量 val 依次拷贝得到 vt_i 容器中各个元素的值。若想通过 val 来改变容器中的元素的值,需要使用引用,如下代码:
五、数组
数组和 vector 类似,也是存放类型相同的对象的容器。与 vector 不同的是,数组的大小确定不变,不能随意向数组中增加元素。
1. 定义和初始化数组
如下代码:
int arr_i[10]; // 包含 10 个元素的 int 数组
int arr_i[10] = {1, 2, 3}; // 包含 10 元素并初始化的 int 数组
int arr_i {1, 2, 3}; // 和上一条语句等价
string arr_str[10]; // 包含 10 个元素的 string 数组
上述代码中,方括号在定义的时候表示该标识符是一个数组,其内的数字表示数组的大小,且只能为整型常量表达式。创建数组若不初始化,则按照其内元素的类型进行默认初始化,如 string 对象默认初始化为空字符串,而局部变量 int 默认为随机值。也可以只初始化一部分,如第二条初始化语句,后面 7 个未初始化的元素均被编译器设置为 0 。
1.1 字符数组的特殊性
字符数组可以单纯是一个字符数组,若其末尾有空字符(‘\0’)则其为一个字符串。如下代码:
char c[3] = {'a', 'b', 'c'}; // 纯字符数组
char s[4] = {'a', 'b', 'c', '\0'}; // 字符串
上述的区别在于,字符串 s 比字符数组多存储一个空字符(‘\0’)。用字符数组存储字符串时,需在原来空间基础上增加一个空字符。特别是想下面这种方式初始化字符串:
char s[4] = "abc";
在编写上述代码时很容易忘记末尾的空字符,导致初始化字符串 s 时的空间为 3 。
2. 访问数组元素
通常使用下标运算符来对数组元素进行访问,也可以使用范围 for 对数组元素进行访问。
2.1 下标运算符
C++中的下标都是从 0 开始的。如下代码:
上述代码,也说明了部分初始化其中未初始化的元素被编译器设置为 0 。
2.2 范围 for
和前面 vector 的范围 for 使用类似,如下代码:
3. 指针和数组
数组名实际上是数组首元素的地址,如下代码:
在大多数表达式中,数组名都是代表数组首元素的地址。以下两种情况数组名代表整个数组:
(1)对数组名取地址得到的是整个数组的地址。
如下代码:
可以看到就算对数组名取地址,其值还是数组首元素的地址。但是对其加 1 ,增加了整个数组的地址,而不是单个元素。
(2)对数组名使用 sizeof 运算符得到的是整数数组的地址。
如下代码:
4. 让指针想迭代器一样访问数组元素
使用两个指针,一个指针指向数组首元素,另一个指针指向数组尾元素的下一个元素。尾后元素的位置可以通过 sizeof 运算符进行计算。如下代码:
5. 标准库函数 begin() 和 end()
虽然可以像上面一样计算得到尾后指针,但是这种方法容易出错。为了使之更简单、安全,C++11 新标准引入了 begin() 和 end() 两个函数。这两个函数和容器中的两个成员函数类似,但是数组不是类类型,只能通过调用函数来进行使用。且这两个函数定义在头文件 iterator 中,使用的时候需要包含该头文件。如下所示:
begin() 和 end() 定义在标准命名空间 std 中,使用的时候需要 using 声明。
6. 下标运算符的差异
标准库类型限定使用的下标必须是无符号类型,而内置类型的下标没有要求。这就说明内置类型可以处理负数的下标,当然前提是该下标有效。
7. 使用数组初始化 vector 对象
如下代码:
上述代码通过头文件 iterator 中的 begin() 和 end() 两个函数返回数组的迭代器来初始化 vector 对象。
7. 多维数组
多维数组实际上就是元素是数组的数组。二维数组就是一个数组,只不过其每个元素为一维数组。
7.1 多维数组的初始化
多维数组的初始化和一维数组差不多,就是多维数组的元素变成了数组而已。以二维数组为例,如下代码:
int arr1[3][4]; // 如果为局部变量,则 arr1 中元素的值均为随机值
int arr2[3][4] = {
{1, 2},
{3, 4}
} // arr2 中第一行和第二行前面两个元素初始化,其余元素均被编译器设置为 0
int arr3[3][4] = {1, 2, 3}; // 按照顺序初始化,未初始化的元素均被编译器设置为 0
上述代码中等号可以省略。如果初始化列表中包含初始化列表,则一行一行初始化,否则按照顺序一个一个元素初始化。
7.2 多维数组的使用
以二维数组为例,如下代码:
二维数组把它看成一个长方形队列,其第一个元素就是第一行,然后逐个访问第一行的元素,然后第二行。以此类推,多维数组就是逐层循环,直到拿到最里面的一维数组,然后开始访问。
六、C 风格字符串
C 字符串存储在字符数组中,通过结尾的空字符(‘\0’)进行标识。
1. C 标准库 String 函数
在头文件 cstring 中有一些与C风格字符串相关的函数,C 风格字符串通过这些函数对字符串进行操作:
strlen(str); // 返回 str 的长度,不计算空字符
strcmp(str1, str2); // 按照字典顺序比较两个字符串中的字符,str1 大于 str2 返回 true,否则返回 false
strcat(str1, str2); // 把字符串 str2 拼接到 str1 中
strcpy(str1, str2); // 把字符串 str2 的内容拷贝到 str1中
上述函数均不对传入其中的字符串进行验证,且使用者还需要考虑其容量是否足够。
2. 混用 string 对象和 C 风格
(1)可以使用 C 风格字符串来初始化 string 对象,如:string s1 = “abc”;
(2)在 string 对象的加法运算中允许使用 C 风格字符串,但其中必须有一个是 string 对象;如:s1 + “def” 。
(3)可以使用 string 的成员函数 c_str() 来返回一个 C 风格字符串(即 char 指针),但是如果改变了该字符串的值,该 C 风格字符串可能会失效。
3. 使用字符串的建议
尽管 C++ 支持 C 风格字符串,但是在 C++ 程序中最好不要使用它们。这是因为 C 风格字符串不仅使用起来不太方便,而且极容易引发程序漏洞,是诸多安全问题的根本原因。