目录

《C++ Primer》 字符串,向量和数组


C++ Primer

字符串,向量和数组

标准库类型string

  • 标准库类型string表示可变长的字符序列,使用string类型必须先包含头文件,string定义在命名空间std
1
2
#include <string>
using std::string;
  • 定义和初始化string对象
初始化string对象的方式
string s1 默认初始化,s1是一个空串
string s2(s1) s2是s1的副本
string s2 = s1 等价于s2(s1),s2是s1的副本
string s3("value") s3是字面值"value"的副本,除了字面值最后的那个空字符外
string s3 = "value" 等价于s3("value"),s3是字面值"value"的副本
string s4(n, 'c') 把s4初始化为由连续n个字符c组成的串
  • 直接初始化和拷贝初始化

    • 使用等号(=) $\rightarrow$ 拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去
    • 不使用等号,执行的是直接初始化(direct initialization)
  • 如何使用

    • 初始化需要一个值:拷贝初始化和直接初始化都行
    • 初始化需要多个值
      • 尽量用直接初始化
      • 拷贝初始化需要显式地创建一个(临时)对象来进行拷贝:string s8 = string(10, 'c');(可读性差,也没有任何补偿优势)
  • string对象上的操作

string的操作
os << s 将s写到输出流os当中,返回os
is >> s 从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is, s) 从is中读取一行赋给s,返回is(换行符也被读了进来,但是不会存入读入的字符串中去)
s.empty() s为空返回true,否则返回false
s.size() 返回s中字符的个数
s[n] 返回s中第n个字符的引用
s1 + s2 返回s1和s2连接后的结果
s1 = s2 用s2的副本替代s1中原来的字符
s1 == s2 如果s1个s2中所含的字符完全一样,则返回true,否则返回false
s1 != s2 等性判断对字母的大小写敏感
<, <=, >, >= 利用字典序进行比较,对字母大小写敏感
  • 技巧:读取未知数量的string对象
1
2
3
4
5
6
int main() {
    std::string word;
    while (std::cin >> word)
        std::cout << word << std::endl;
    return 0;
}

原理:CTRL+Z/读到文件末尾 -> 引发异常 -> 记录异常 -> fail() 返回true-> operator void*() 返回 0 -> while结束,参考文章:C++ 深入探究 while(cin)

  • 如果是按行读
1
2
3
4
5
6
int main() {
    std::string line;
    while(std::getline(std::cin, line))
        std::cout << line << std::endl;
    return 0;
}
  • string::size_type类型

    • string类的size函数返回的是是一个string::size_type类型的值,无符号类型,足以存放下任何string对象的大小,下标运算符参数类型也是如此

    • string类以及其他大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性,类型size_type即是其中的一种。标准库会根据运行的机器型号分配适当的类型,我们不需过多关心,如果要看类型在C++11以后可以借助autodecltype来推断变量类型

      1
      
      auto len = line.size();
      
    • 由于string::size_type是一个无符号类型,所以我们在对字符串进行有关size的相关操作时,尽量避免int与unsigned混用,否则会由于反补码机制出现意想不到的一些bug,正确做法

      1
      2
      3
      4
      5
      6
      7
      
      for (auto i = 0; i < line.size(), i++) {
          ...
      }
      
      for(decltype(str.size()) i = 0;i < str.size();i++) {
          ............
      }
      
  • 字典序相关比较机制

  1. 较短的string对象小于较长的string对象(前面的内容都完全相同)
  2. 如果两个string对象在某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果
  • 字面值和string对象相加

    • 通过重载+和+=运算符实现,使用时一定要注意返回值的问题,不要两边类型不匹配(例如两个字面值相加)

      1
      
      std::string s3 = s1 + ", " + s2 + '\n';
      
    • 防止上述问题:必须确保每个加法运算符(+)的两侧运算对象至少有一个是string

  • 处理string对象中的字符

    • 主要利用到了头文件<cctype>
<ccytpe>中的常用函数
isdigit(c) 当c是数字的时候为真
islower(c) c是小写字母的时候为真
isupper(c) c是大写字母的时候为真
isspace(c) c是空格的时候为真
tolower(c) 如果c是大写字母,则输出对应的小写字母,否则原样输出c
toupper(c) 如果c是小写字母,则输出对应的大写字母,否则原样输出c

为了向后兼容C,C++保留了C的标准库,但是为了更好地符合C++的要求,C++自己又开发出了一套新的标准库,去掉C库中的.h,前面加上c,即<name.h> $\rightarrow$ <cname>,上面<cctype>就是由C中的<ctype.h>衍生过来的,C++库中的函数为了防止与C标准库中的内容发生命名冲突,都在std命名空间中

  • 基于范围的for语句
    • C++11的新特性
1
2
3
4
5
6
7
8
for (declaration : expression)
    statement

//example    
std::string str("Some string");

for (auto c : str)
    std::cout << c << std::endl;
  • 重载的下标运算符
    • operator[] -> 接收输入是std::string::size_type类型的值,返回string对应位置字符的引用
    • 超出范围下标将引发不可预知的结果(内存非法越界访问或者访问到nullptr $\rightarrow$ 空字符串,C++为了性能并不会像Java那样在下标访问时进行检查,会有很大风险)

标准库类型vector

  • vector本质是一种变长数组

  • vector也是一种容器(container)

  • vector包含于头文件<vector>中,定义在std命名空间下

  • vector是类模板,使用时必须声明其类型(其实在Modern C++中编译器也能自己推导一部分了),编译器根据模板创建类或函数函数的过程称为实例化(instantiation),由于C++的分离式编译,自己写模板的时候一定要将声明与实现都写在.h里,不要将实现分离到.cpp中,否则会导致编译失败

/img/C++ Primer/chapter3-1.jpg
由于模板的声明与实现分离导致编译失败
/img/C++ Primer/chapter3-2.jpg
全部写入头文件后编译成功
  • vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector

  • 定义和初始化vector对象
初始化vector对象的方法
vector<T> v1 v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1) v2中包含有v1所有元素的副本
vector<T> v2 = v1 等价于v2(v1),v2中包含有v1所有元素的副本
vector<T> v3(n, val) v3包含了n个重复元素,每个元素的值都是val
vector<T> v4(n) v4重复包含了n个重复地执行了值初始化的对象(默认初始化)
vector<T> v5{a, b, c, ...} v5包含了初始值个数的元素,每个元素都被赋予了相应的初始值(注意使用花括号)
vector<T> v5 = {a, b, c, ...} 等价于v5{a, b, c, ...}
  • 圆括号与花括号

    • 圆括号是提供的值用来构造(construct)vector对象的
    • 花括号是用来进行列表初始化的(list initialize),初始化过程会尽可能的把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式 $\Longrightarrow$ 列表内数据与所需元素类型不同
1
2
3
4
vector<int> v1(10); // -> {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 
vector<int> v2{10}; // -> {10}
vector<int> v3(10, 1); // -> {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
vector<int> v4{10, 1}; // -> {10, 1}
  • 列表初始化构造失败转为直接构造
1
2
vector<string> v5{10}; //10个默认初始化string
vector<string> v8{10, "hi"}; //10个"hi"

  • vector有关操作
vector支持的操作
v.empty() 返回元素个数是否为空
v.size() 返回元素个数
v.push_back(t) 向v的尾端添加一个值为t的元素
v[n] 下标访问,返回第n个位置上元素的引用
v1 = v2 用v2中元素的拷贝替换v1中的元素
v1 = {a, b, c...} 用列表中的元素拷贝替换v1中的元素
v1 == v2; v1 != v2 判等条件:元素数量相等且对应位置的元素值相等(对象需要重载==和!=运算符)
<, <=, >, >= 字典序比较,对象需要重载相应运算符
  • 有关使用push_back的建议
  1. 不需要在创建vector时确定其中的元素及其大小,后续使用push_back即可
  2. 在循环体内部包含向vector对象添加元素的操作时,不应该使用foreach循环
  • size()返回的类型为vector::size_type,同string::size_type,由头文件和机器来决定类型,下标运算符参数类型也是如此(同string)

  • 虽然vector可以扩容,但是不能通过下标来添加元素,通过下标访问不存在的元素会产生非常严重的后果,例如缓冲区溢出(buffer overflow),会导致出现安全问题

迭代器介绍与使用

  • 目的:访问对象中的元素
  • 迭代器类似于指针类型,也提供了对对象的间接访问,使用迭代器可以访问某个元素,也可以从一个元素移动到另一个元素
  • 有效与无效迭代器
    • 有效:迭代器指向某个元素或者容器中尾元素的下一位置
    • 无效:除了上述指向的迭代器都无效

  • 获取迭代器:获取迭代器不能使用取地址符,有专门的方法进行获取,容器一般都支持beginend方法
    • begin返回指向第一个元素的迭代器
    • end返回指向容器“尾元素的下一位置(one past the end)”的迭代器,这样的迭代器没有什么实际意义,只是一个标记,表示我们已经处理完了容器中的所有元素。end成员返回的迭代器通常被称为尾后迭代器(off-the-end iterator)或尾迭代器(end iterator)
    • 如果容器为空,begin和end返回的是同一个迭代器,都是尾后迭代器(可以作为容器的判空手段)
1
2
auto b = v.begin();
auto e = v.end();
标准容器迭代器的运算符
*iter 返回迭代器iter所指元素的引用
iter->men 解引用iter并获得该元素的名为men的成员,等价于(*iter).men
++iter 令iter指示容器中的下一个元素
--iter 令iter指示容器中的下一个元素
iter1 == iter2; iter1 != iter2 判等条件:两个迭代器指示的是同一个元素或者他们是同一个容器的尾后迭代器,则相等;反之则不等
  • 试图解引用一个非法迭代器或者尾后迭代器也是危险行为!
1
2
3
4
5
6
string s("Some thing");

if (s.begin() != s.end()) { // -> is empty?
    auto it = s.begin();
    *it = toupper(*it);
}

一般循环

1
2
3
for (auto it = s.begin(); ii != s.end(); ++it) {
    //......
}
  • 大多数标准库容器的迭代器都定义了==!=,但是它们中的大多数都没有定义<运算符,所以我们使用迭代器是尽量使用==!=

  • 迭代器类型

    • iterator可读可写
    • const_iterator只读
1
2
vector<int>::iterator it;
vector<int>::const_iterator it;
  • 实际使用中使用auto让编译器决定即可

  • begin()end自己决定返回的是否为const,如果必须要用const_iterator,使用cbegin()cend()即可C++ 11

  • 注意,凡是使用了迭代器的循环体,都不要向迭代器所属的容器中添加元素,否则可能会造成迭代器失效


  • 迭代器运算
迭代器运算
iter + n 迭代器加上了一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干元素
iter - n 迭代器减去了一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干元素
iter += n iter = iter + n
iter -= n iter = iter - n
iter1 - iter2 两个迭代器相减的结果是它们之间的距离,两个迭代器必须是同一个容器的有效迭代器,返回值类型为difference_type(带符号类型)
>, >=, <, <= 关系运算符,注意两个迭代器必须是同一个容器的有效迭代器

数组

  • 编译的时候数组的维度应该是已知的,维度必须是一个常量表达式
1
2
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42;  //常量表达式
  • 默认情况下,数组中的元素会被默认初始化 $\Longrightarrow$ 这里注意如果遇到了newdelete相关的一些操作,一定要防止默认初始化改变指针最后导致delete释放内存的时候程序崩溃
/img/C++ Primer/chapter3-3.png
HEAP CORRUPTION DETECTED报错说明及解决方案
  • 显式初始化 $\Longrightarrow$ 通过列表初始化数组
1
2
3
4
5
const unsigned sz = 3;
int ia1[sz] = {0, 1, 2};
int a2[] = {0, 1, 2}; //维度为3
int a3[5] = {0, 1, 2}; // {0, 1, 2, 0, 0} 后面会默认初始化
int a5[2] = {0, 1, 2}; //错误,初始值过多
  • 字符数组
    • 字符数组可以用字符串字面值进行赋值,但是要注意最后还有一个终止符
1
2
char a[] = "Hello world";
char b[6] = "Daniel"; //错误,忽略了最后的'\0'
  • 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值
1
2
3
int a[]{0, 1, 2};
int a2[] = a; //错误
a2 = a; //错误

本质是指针的拷贝,最后容易导致出现内存泄漏和野指针问题,虽然部分编译器支持上述行为,但是为了安全性和通用性,尽量不要出现上述代码

  • 阅读数组声明含义:由内向外读

  • 数组也可以用基于范围的for循环

  • 访问数组元素使用的下标用的类型为size_t,也是一种机器相关类型,头文件C:<stddef.h> $\Longrightarrow$ C++:<cstddef>,但是内置下标运算符也可以使用负数(不同于vector和string)

  • 数组的下标访问同样只由程序员负责检查,一定要防止非法访问

  • auto返回的是指针,decltype返回的是数组

1
2
3
4
5
6
//指针迭代器用法
int arr[10]{};
int *e = &arr[10];
for (int *b = arr; b != e; b++) {
    do_something(*b);
}
  • C++11后引入了begin和end函数(定义于<iterator>,可以通过这两个函数获得的指针来模拟迭代器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

int main() {
    int arr[]{0, 1, 2, 3, 4, 5};
    int *begin = std::begin(arr);
    int *end = std::end(arr);
    for (; begin != end; begin++)
	    std::cout << *begin << std::endl;
    return 0;
}
  • 两个指针相减的结果类型是ptrdiff_t的机器相关有符号类型,定义于<cstddef>文件中

  • 同一数组的两个有效指针可以进行比较

  • C风格的字符串处理函数位于头文件<cstring>中,但是为了方便性和安全性,C++程序中尽量使用std::string

  • C风格的字符串比较实际是在比较字符串数组首地址

  • 旧代码接口

1
2
3
string s {"Hello world"};
const char *str = s.c_str();
//但是在后续由于扩容等机制c_str()的指针可能会失去作用,最好通过s.c_str()拷贝复制一份
1
2
int *arr[]{0, 1, 2, 3, 4, 5};
vector(begin(arr), end(arr)); // -> {0, 1, 2, 3, 4, 5}
  • 建议:尽量使用标准库而非指针和数组

多维数组基于范围的for循环,除了最内层,其他所有循环的控制变量都应该是引用(转化为数组,如果不是引用会转化为指针)

for (auto &i : arr)

​ for (auto &j : i)

​ for (auto k : j) {

​ do_something(k);

}

Ending