C++ Primer 第三章 字符串、向量和数组

命名空间

#include <iostream>
// using 声明,当我们使用名字 cin 时,从命名空间 std 中获取它
using std::cin;

int main() {

    int i;
    cin >> i; // 正确:cin 和 std::cin 含义相同
    //cout << i; // 错误:没有对应的 using 声明,必须使用完整的名字
    std::cout << i; // 正确:显式地从 std 中使用 cout

    return 0;
}

标准库类型 string

  • 需要包含头文件 #include <string>
  • string 定义在命名空间 std 中。using std::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 组成的串
  • 拷贝初始化:编译器把等号右侧的初始值拷贝到新建的对象中去
  • 直接初始化:与之相反,不使用等号
    string s5 = "hiya"; // 拷贝初始化
    string s6("hoya"); // 直接初始化
    string s7(10, 'c'); // 直接初始化,s7 的内容是 cccccccccc

    string s8 = string(10, 'c'); // 拷贝初始化,s8 的内容是 cccccccccc
    //等价于
    string temp(10, 'c');
    string s8 = temp;
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 个字符的引用,位置 n 从 0 计起
    s1 + s2         返回 s1 和 s2 连接后的结果
    s1 = s2         用 s2 的副本代替 s1 中原来的字符
    s1 == s2        如果 s1 和 s2 中所含的字符完全一样,则它们相等,string 对象的相
    s1 != s2        等性判断对字母的大小写敏感
    <, <= ,>, >=    利用字符在字典中的顺序进行比较,且对字母的大小写敏感
读写 string 对象
int main() {

    string s;
    cin >> s;
    cout << s << endl;

    return 0;
}

在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等),并从一个真正的字符开始读起,直到遇见下一处空白为止。

例如:输入" Hello World! ", 输出为"Hello"。

getline
  • getline 只要一遇到换行符就结束读取操作并返回结果(注意换行符也被读进来了),哪怕输入的一开始就是换行符也是如此。(字符串中不存换行符)
string::size_type
  • size() 函数返回的是一个 string::size_type 类型,是无符号类型的值,足够存放下任何 string 对象的大小
int main() {

    string s;
    cin >> s;

    string::size_type len = s.size();
    cout << len << endl;

    return 0;
}
字面值和 string 对象相加
  • 当把 string 对象和字串字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是 string
    string s4 = s1 + ", "; // 正确:把一个 string 对象和一个字面值相加
    string s5 = "hello" + ", "; // 错误:两个运算对象都不是 string
    // 正确:每个加法运算符都有一个运算对象是 string
    string s6 = s1 + ", " + "world";
    string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加

因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。

cctype 头文件中的函数
isalnum(c)		当 c 是字母或数字时为真
isalpha(c)		当 c 是字母时为真
iscntrl(c)		当 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

例:将第一个单词大写

    string s("some string");

    for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++ index) {
        s[index] = toupper(s[index]);
    }

    cout << s << endl;

标准库类型 vector

  • 需要包含头文件 #include <vector>
  • 需要 using 声明 using std::vector

编译器根据模板创建类或函数的过程成为实例化

    vector<int> ivec; // ivec 保存 int 类型的对象
    vector<Sales_item> Sales_vec; // 保存 Sales_item 类型的对象
    vector<vector<string> > file; // 该向量的元素是 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...}
值初始化
  • 如果用的是圆括号,可以说提供的值是用来构造 vector 对象的。
  • 如果用的是花括号,可以表述成我们想列表初始化该 vector 对象。
    vector<int> v1(10); // v1 有 10 个元素,每个的值都是 0
    vector<int> v2{10}; // v2 有 1 个元素,该元素的值是 10

    vector<int> v3(10, 1); // v3 有 10 个元素,每个的值都是 1
    vector<int> v4{10, 1}; // v4 有 2 个元素,值分别是 10 和 1
  • 如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造 vector 对象了。
    vector<string> v5{"hi"}; // 列表初始化:v5 有一个元素
    vector<string> v6("hi"); // 错误:不能使用字符串字面值构建 vector 对象
    vector<string> v7{10}; // v7 有 10 个默认初始化的元素
    vector<string> v8{10, "hi"}; // v8 有 10 个值为 "hi" 的元素
vector 比较重要的操作
v.empty()                 如果 v 不含有任何元素,返回真:否则返回假
v.size()                  返回 v 中元素的个数
v.push_back(t)            向 v 的尾端添加一个值为 t 的元素
v[n]                      返回 v 中第 n 个位置上元素的引用
v1 = v2                   用 v2 中元素的拷贝替换 v1 中的元素
v1 = {a, b, c...}         用列表中元素的拷贝替换 v1 中的元素
v1 == v2                  v1 和 v2 相等当且仅当它们的元素数量相同且对应位置的元素值都相同
v1 != v2
<, <=, >, >=              顾名思义,以字典序进行比较
  • size 的返回值类型为:vector<int>::size_type

只能对确知已存在的元素执行下标操作

vector<int> ivec; // 空 vector 对象
cout << ivec[0]; // 错误:ivec 不包含任何元素

vector<int> ivec2(10); // 含有 10 个元素的 vector 对象
cout << ivec2[10]; // 错误:ivec2 元素的合法索引是从 0 到 9
迭代器
  • 如果容器为空,则 begin 和 end 返回的是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符
*iter                  返回迭代器 iter 所指元素的引用
iter -> mem            解引用 iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem
++ iter                令 iter 指示容器中的下一个元素
-- iter                令 iter 指示容器中的上一个元素
iter1 == iter2         判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元
iter1 != iter2         素或者它们是同一个容器的尾后迭代器,则相等:反之,不相等
  • 拥有迭代器的标准库类型使用iteratorconst_iterator来表示迭代器的类型
vector<int>::iterator it; // it 能读写 vector<int> 的元素
string::iterator it2; // it2 能读写 string 对象中的字符

vector<int>::const_iterator it3; // it3 只能读元素,不能写元素
string::const_iterator it4; // it4 只能读字符,不能写字符
  • begin 和 end 返回的具体类型由对象是否是常量决定,如果对象是常量,begin 和 end 返回 const_iterator; 如果不是常量,返回 iterator
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1 的类型是 vector<int>::iterator
auto it2 = cv.begin(); // it2 的类型是 vector<int>::const_iterator
  • 为了便于专门得到 const_iterator 类型的返回值,C++11新标准引入了两个新函数,分别是 cbegin 和 cend;
auto it3 = v.cbegin(); // it3 的类型是 vector<int>::const_iterator

谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素,任何一种可能改变 vector 对象容量的操作,都会使该 vector 对象的迭代器失效

vector 和 string 迭代器支持的运算
iter + n                迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比
                        向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指
                        示容器尾元素的下一位置
iter - n                迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比
                        向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指
                        示容器尾元素的下一位置
iter1 += n              迭代器加法的复合赋值语句,将 iter1 加 n 的结果赋给 iter1
iter1 -= n              迭代器减法的复合赋值语句,将 iter1 减 n 的结果赋给 iter1
iter1 - iter2           两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭
                        代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器
                        必须指向的是同一个容器中的元素或者尾元素的下一位置
>, >=, <, <=            迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指
                        位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一
                        个容器中的元素或者尾元素的下一位置
  • 两个迭代器的距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为 difference_type的带符号整数。

数组

定义和初始化内置数组
  • 数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 含有 10 个整数的数组
int *parr[sz]; // 含有 42 个整型指针的数组
string bad[cnt]; // 错误:cnt 不是常量表达式
string strs[get_size()]; // 当 get_size() 是 constexpr 时正确,否则错误
显式初始化数组元素
const unsigned sz = 3;
int ial[sz] = {0, 1, 2};        // 含有 3 个元素的数组,元素值分别是 0, 1, 2
int a2[] = {0, 1, 2};           // 维度是 3 的数组
int a3[5] = {0, 1, 2};          // 等价于 a3[] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"};   // 等价于 a4[] = {"hi", "bye", ""}
int a5[2] = {0, 1, 2};          // 错误:初始值过多
字符数组的特殊性
  • 用字符串字面值初始化数组时,记得结尾还有一个空字符。
char a1[] = {'c', '+', '+'};           // 列表初始化,没有空字符
char a2[] = {'c', '+', '+', '\0'};     // 列表初始化,没有空字符
char a3[] = "C++";                     // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel";           // 错误:没有空间可存放空字符!
复杂的数组声明
int *ptrs[10];                 // ptrs 是含有 10 个整型指针的数组
int &refs[10] = /* ? */;       // 错误:不存在引用的数组
int (*Parray)[10] = &arr;      // Parray 指向一个含有 10 个整数的数组
int (&arrRef)[10] = arr;       // arrRef 引用一个含有 10 个整数的数组
int *(&arry)[10] = ptrs;       // arry 是数组的引用,该数组含有 10 个指针
访问数组元素
  • 使用数组下标的时候,通常将其定义为 size_t 类型,它被设计得足够大以便能表示内存中任意对象的大小,在cstddef头文件中
指针和数组
string nums[] = {"one", "two", "three"}; // 数组的元素是 string 对象
string *p = &nums[0];                    // p 指向 nums 的第一个元素

/*
在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针
*/
string *p2 = nums; // 等价于 p2 = &nums[0];
  • 在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。
    int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // ia 是一个含有 10 个整数的数组
    auto ia2(ia); // ia2 是一个整型指针,指向 ia 的第一个元素
    ia2 = 42; // 错误:ia2 是一个指针,不能用 int 值给指针赋值
    
    /*
    编译器执行的初始化过程类似下面的形式
    auto ia2(&ia[0]); // 显然 ia2 的类型是 int*
    */
  • 当使用 decltype 关键字时,上述转换不会发生,decltype(ia) 返回的类型是由 10 个整数构成的数组。
// ia3 时一个含有 10 个整数的数组
decltype(ia) ia3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
ia3 = p; // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确:把 i 的值赋给 ia3 的一个元素

指针也是迭代器

  • vector 和 string 的迭代器支持的运算,数组的指针全都支持。
    如:
int *e = &arr[10]; // 指向 arr 尾元素的下一位置的指针
for (int *b = arr; b != e; ++ b) cout << *b << endl;
标准库函数 begin 和 end

C++引入两个名为 begin 和 end 的函数*,在iterator 头文件中,声明 std

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // ia 是一个含有 10 个整数的数组
int *beg = begin(ia); // 指向 ia 首元素的指针
int *last = end(ia); // 指向 ia 尾元素的下一位置的指针
  • 两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,定义在 cstddef头文件中的机器相关类型,是一种带符号类型
  • 如果 p 是空指针,允许给 p 加上或减去一个值为 0 的整型常量表达式。两个空指针也允许彼此相减,结果当然是 0。
下标和指针
  • 只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算
int *p = &ia[2]; // p 指向索引为 2 的元素
int j = p[1]; // p[1] 等价于 *(p + 1) 就是 ia[3] 表示的那个元素
int k = p[-2]; // p[-2] 是 ia[0] 表示的那个元素

C风格字符串

  • 操作C风格字符串的函数定义在cstring头文件中
strlen(p)                 返回 p 的长度,空字符不计算在内
strcmp(p1, p2)            比较 p1 和 p2 的相等性。如果 p1 == p2, 返回 0,如果 p1 > p2,
                          返回一个正值,如果 p1 < p2,返回一个负值
strcat(p1, p2)            将 p2 附加到 p1 之后,返回 p1
strcpy(p1, p2)            将 p2 拷贝给 p1,返回 p1
    char ca[] = {'c', '+', '+'}; // 不以空字符结束
    cout << strlen(ca) << endl; // 严重错误:ca 没有以空字符结束
  • 传入此类函数的指针必须指向以空字符作为结束的数组,无空字符可能沿着 ca 在内存中的位置不断向前寻找,直到遇到空字符才停下来。

与旧代码的接口

混用 string 对象和 C 风格字符串
  • 不能用 string 对象直接初始化指向字符的指针。为了完成该功能,string 专门提供了一个名为 c_str 的成员函数
char *str = s; // 错误:不能用 string 对象初始化 char*
const char *str = s.c_str(); // 正确
// 我们无法保证 c_str 函数返回的数组一直有效,事实上,如果后续的操作改变了 s 的值就可能让之前返回的数组失去效用。

如果执行完 c_str() 函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份

使用数组初始化 vector 对象
  • 只需指明要拷贝区域的说元素地址和尾后地址就可以了
    int int_arr[] = {0, 1, 2, 3, 4, 5};
    // ivec 有 6 个元素,分别是 int_arr 中对应元素的副本
    vector<int> ivec(begin(int_arr), end(int_arr));

用于初始化 vector 对象的值也可能仅是数组的一部分

// 拷贝三个元素:int_arr[1]、int_arr[2]、int_arr[3]
vector<int> subVec(int_arr + 1, int_arr + 4);

多维数组

严格来说没有多维数组,通常所说的多维数组其实是数组的数组

多维数组的初始化
int ia[3][4] = {
    {0, 1, 2, 3}, // 第一行的初始值
    {4, 5, 6, 7}, // 第二行的初始值
    {8, 9, 10, 11} // 第三行的初始值
}

int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; // 等价于上面

int ia[3][4] = {{ 0 }, { 4 }, { 8 }}; // 初始化每行的首元素

int ix[3][4] = {0, 3, 6, 9}; // 初始化第一行
多维数组的下标引用
// 用 arr 的首元素为 ia 最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1]; // 把 row 绑定到 ia 的第二个 4 元素数组上
使用范围 for 语句处理多维数组
    size_t cnt = 0;
    for (auto &row : ia)
        for (auto &col : row) {
            col = cnt;
            ++ cnt;
        }

    for (const auto &row : ia) 
        for (auto col : row) 
            cout << col << endl;

要使用范围 for 语句处理多维数组,除了最内层的循坏外,其他所有循环的控制变量都应该是引用类型。

指针和多维数组
int ia[3][4]; 
int (*p)[4] = ia; // p 指向含有 4 个整数的数组
p = &ia[2]; // p 指向 ia 的尾元素

在上述声明中,圆括号必不可少

int *ip[4]; // 整型指针的数组
int (*p)[4]; // 指向含有 4 个整数的数组

其他遍历方法

    for (auto p = ia; p != ia + 3; ++ p) {
        for (auto q = *p; q != *p + 4; ++ q)
            cout << *q << ' ';
        cout << endl;
    }

    for (auto p = begin(ia); p != end(ia); ++ p) {
        for (auto q = begin(*p); q != end(*p); ++ q)
            cout << *q << ' ';
        cout << endl;
    }
类型别名简化多维数组的指针
using int_array = int[4];
typedef int int_array[4]; // 等价

for (int_array *p = ia; p != ia + 3; ++ p) {
    for (int *q = *p; q != *p + 4; ++ q)
        cout << *q << ' ';
    cout << endl;
}
posted @ 2023-01-01 16:16  HuiPuKui  阅读(49)  评论(0)    收藏  举报