c++基础:3-字符串、向量和数组

156 阅读5分钟

标准库类型string

1. string对象的初始化

  • 当初始值只有一个时,使用拷贝初始化或者直接初始化都行;
  • 当有多个初始值时,一般只能使用直接初始化;若非要使用拷贝初始化,则需要显式创建临时变量用于拷贝;
// 默认初始化, s1为空串
string s1; 

// 拷贝初始化:使用=号初始化一个变量
string s2 = "value";
string s2 = s1;
string s2 = string(10, 'c'); // string temp(10,'c'); string s2 = temp;

// 直接初始化:使用()号初始化一个变量
string s3("value");
string s3(10,'c'); // 10个c

2. string对象的操作

  • string对象的读取会自动忽略开头的空白,直到下一处空白为止;

    #include <iostream>
    #include <string>
    using std::string;
    
    int main() {
      string s1, s2;
    
      std::cin >> s1 >> s2;
    
      std::cout << "s1: " << s1 << std::endl;
      std::cout << "s2: " << s2 << std::endl;
    
      return 0;
    }
    // input
        hello       world
    // output
    s1:hello
    s2:world
    
  • getline只要一遇到换行符就结束读取操作并返回结果

    #include <iostream>
    #include <string>
    using std::string;
    
    int main() {
      string line;
      while (std::getline(std::cin, line)) {
        std::cout << line << std::endl;
      }
    
      return 0;
    }
    
  • string::size_type是字符串的长度类型,类似此类型具有与机器无关的特性,是一个无符号类型的值;

    #include <iostream>
    #include <string>
    using std::string;
    
    int main() {
      string line;
      while (getline(std::cin, line)) {
        if (line.empty()) {
          std::cout << "input is empty" << std::endl;
        } else {
          string::size_type sz = line.size();
          std::cout << "input size is " << sz << std::endl;
        }
      }
      return 0;
    }
    
  • string对象相加: 得到一个新的string对象

    • 字符串字面值与string是不同的类型
    • 字符字面值和字符串字面值可以转换为string对象, 但
    • 加法运算符的两侧至少有一个必须是string对象!!!
    #include <iostream>
    #include <string>
    using std::string;
    
    int main() {
      string s1 = "hello", s2 = "world";
    
      string s3 = s1 + "," + s2 + '\n'; // ok
      string s4 = "hello" + "world";    // not ok
      string s5 = s1 + "," + "world";   // ok
      string s6 = "hello" + "," + s2;   // not ok
    
      return 0;
    }
    
  • cctype头文件中定义了一组标准库函数用来处理字符

    • 如果想要改变string对象中的字符,要么把循环变量定义为引用类型,要么使用下标迭代
    #include <cctype>
    #include <iostream>
    #include <string>
    using std::cout;
    using std::endl;
    using std::string;
    
    int main() {
      string s = "123...Hello, world!";
    
      for (auto c : s) {
        if (std::isalpha(c)) {
          cout << c << " 是一个字母 ";
          if (std::islower(c)) {
            char c_upper = std::toupper(c);
            cout << "大写为 " << c_upper << endl;
          }
          if (std::isupper(c)) {
            char c_upper = std::tolower(c);
            cout << "小写为 " << c_upper << endl;
          }
        }
    
        if (std::isdigit(c)) {
          cout << c << " 是一个数字" << endl;
        }
    
        if (std::ispunct(c)) {
          cout << c << " 是一个标点符号" << endl;
        }
      }
    
      // 使用范围迭代
      for (auto &c : s) {
        c = std::toupper(c);
      }
      cout << "全大写 " << s << endl;
    
      // 使用下标迭代
      for (decltype(s.size()) i = 0; i != s.size() && !std::isspace(s[i]); i++) {
        s[i] = std::tolower(s[i]);
      }
      cout << "一半小写 " << s << endl;
    
      return 0;
    }
    

标准库类型vector

Vector: 相同类型对象的集合

1. vector对象的初始化

// 默认初始化
vector<int> v1;  // 空vector,元素类型为int

// 拷贝初始化
vector<int> v2(v1); // v2包含v1中所有元素的副本
vector<int> v2 = v1; // 同上

// 直接初始化
vector<int> v3(10, 1); // v3包含10个1
vector<int> v4(10); // v4包含10个0, 10个元素执行了默认值初始化

// 列表初始化
vector<int> v5{1,3,5}; // v5包含1,3,5
vector<int> v5 = {1,3,5}; // 同上

// 使用了花括号,但无法执行列表初始化,将尝试使用直接初始化
vector<string> v6{10}; //v6包含10个空字符串
vector<string> v7{10,"hi"}; // v7包含10个"hi"

2. vector对象的操作

与其他语言(c、java、golang)不同,定义vector对象时设定其大小,性能可能更差

#include <iostream>
#include <vector>
using std::vector;

int main() {
  vector<int> ivec;

  if (ivec.empty()) {
    for (int i = 0; i < 10; ++i) {
      ivec.push_back(i);
    }
  }

  for (auto &v : ivec) {
    v *= v;
  }

  for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix) {
    std::cout << ivec[ix] << std::endl;
  }

  return 0;
}

迭代器iterator

  • 和指针类似,可以通过解引用来获取迭代器指示的值;

  • 如果vector对象或者string对象是常量,则只能使用const_iterator

    vector<int>::iterator it; //it可读写vector<int>的元素
    string::iterator strit; // strit可读写string中的字符
    
    vector<int>::const_iterator cit; // cit只能读vector<int>的元素,不能写
    string::const_iterator strcit; // strcit只能读string中字符,不能写
    
  • 函数begin()end()返回vector<T>::iterator类型, 相应的cbegin()cend()则返回vector<T>::const_iterator类型;

  • 但凡使用了迭代器的循环体,则不能向迭代器所属容器添加元素;

  • 迭代器支持+、-、>=、<=等算数运算操作。

    #include <iostream>
    #include <string>
    using std::string;
    
    int main() {
      const string text = "a b c d e f g h i j k l m n o";
      const char target = 'h';
      string::const_iterator beg = text.cbegin(), end = text.cend();
      string::difference_type half_len = (end - beg) / 2;
      string::const_iterator mid = beg + half_len;
    	
      // 二分查找
      while (mid != end && *mid != target) {
        if (target < *mid) {
          end = mid;
        } else {
          beg = mid + 1;
        }
        mid = beg + (end - beg) / 2;
      }
    
      std::cout << *mid << std::endl;
      // 如果mid==end则没有找到目标元素
      std::cout << (mid == end) << std::endl;
    
      return 0;
    }
    

内置类型array

array: 存放相同类型对象的容器,大小确定,运行时性能较好,灵活性欠佳。

当不清楚元素确切个数时,使用vector

1. array对象的初始化

  • 数组的维度必须是常量表达式

  • 数组的元素被默认初始化

  • 数组的元素应为对象,不存在引用的数组

    const unsigned n = 3;
    int arr1[n] = {0,1,2}; // 含三个元素的数组
    int arr2[] = {0,1,2}; // 同上
    int arr3[5] = {0,1,2}; // 含5个元素的数组, 0,1,2,0,0
    string arr4[3] = {"hello", "world"}; // 含三个字符串元素, "hello"、"world",""
    
    int *ptrarr[10]; // 含有10个int*的数组
    int &refarr[10]; // not ok. 不存在引用的数组
    int (*arrptr)[3] = &arr1; // 数组指针,arrptr指向一个数组, 该数组含有3个int 
    int (&arrref)[3] = arr1; // 数组引用,arrref是数组arr1的引用,是arr1的别名
    
  • 使用字符串字面值初始化一个数组时,含有一个空字符

    char arr5[] = "c++";
    char arr5[] = {'c','+','+','\0'}; // 同上
    
  • 数组不允许拷贝和赋值

    int arr6[] = {0,1,2};
    int arr7[] = arr6; // not ok. 不允许使用一个数组初始化另一个数组
    arr7 = arr6; // not ok. 不能把一个数组直接赋值给另一个数组
    

2. array对象的操作

指针和数组

  • 通常用到数组名字的地方,编译器会自动将其替换为指向数组首元素的指针;

    string nums[] = {"one","two","three"};
    string *p = nums; // 等同于
    string *p = &nums[0];
    
    auto pnums(nums); // 等同于指针拷贝
    auto pnums(&nums[0]); // pnums是一个string*,指向nums的第一个元素
    
  • 当使用decltype关键字时,数组不会被转换为首元素地址;

    decltype(nums) nums2 = {"four","five","six"};
    
  • 使用标准库函数begin()end()可以获得数组的首元素指针和尾后指针;

    string *beg = begin(nums);
    string *end = end(nums);
    
  • 对数组执行下标运算等价于对指向数组元素的指针执行下标运算;

    标准库类型(如string、vector)使用的下标必须是无符号类型,内置类型(如array)的下标无此要求。

    string third_num = nums[2]; // 等价于
    string *numsprt = nums;
    third_num = *(numsprt + 2);
    
    string *second = &nums[1];
    string third = second[1];  // 等价于 *(second+1)
    string first = second[-1]; // 等价于 *(second-1)
    

操作示例:

#include <cstddef>
#include <iostream>
#include <iterator>
#include <string>
using std::string;

int main() {
  const unsigned n = 3;
  string nums[n] = {"one", "two", "three"};
  string *beg = std::begin(nums);
  string *end = std::end(nums);

  // 移动首元素指针
  string *second_num = nums + 1;
  std::cout << "nums第二个元素: " << *second_num << std::endl;

  // 指针相减,ptrdiff_t是带符号类型
  std::ptrdiff_t sz = end - beg;
  std::cout << "nums含有" << sz << "个元素" << std::endl;

  // 指针遍历
  while (beg != end) {
    std::cout << *beg << std::endl;
    ++beg;
  }

  // 数组下标操作等价于指针操作
  string third_num = nums[2]; // 等价于
  string *numsprt = nums;
  third_num = *(numsprt + 2);
  std::cout << "nums第三个元素: " << third_num << std::endl;

  string *second = &nums[1];
  string third = second[1];  // 等价于 *(second+1)
  string first = second[-1]; // 等价于 *(second-1)
  std::cout << "first: " + first << std::endl;
  std::cout << "second: " + *second << std::endl;
  std::cout << "third: " + third << std::endl;

  return 0;
}

c风格字符串

c风格字符串本质是char[],数组会转换为指向首元素的指针,因此

  • c风格字符串函数的参数实质都是指针,且
  • 指针参数指向的数组必须以空字符结尾
strlen(p); // p的长度
strcmp(p1,p2); // 比较
strcat(p1,p2); // p2附加到p1,返回p1
strcpy(p1, p2); // 将p2拷贝给p1,返回p1
#include <cstring>
#include <iostream>
#include <string>

int main(int argc, const char **argv) {
  char arr1[] = {'h', 'e', 'l', 'l', 'o', '\0'};
  std::cout << "len(arr)=" << strlen(arr1) << std::endl;

  char arr2[] = " world";
  // arr2拼接到arr1
  std::strcat(arr1, arr2);

  // arr1 拷贝给 arr3
  char arr3[strlen(arr1)];
  std::strcpy(arr3, arr1);

  // 比较
  int cmp = std::strcmp(arr1, arr3);
  std::cout << "cmp: " << cmp << std::endl;

  if (cmp == 0) {
    std::cout << "arr1 == arr3" << std::endl;
  } else if (cmp > 0) {
    std::cout << "arr1 > arr3" << std::endl;
  } else {
    std::cout << "arr1 < arr3" << std::endl;
  }

  return 0;
}
  • 允许以空字符结尾的字符数组来初始化string对象或为string对象赋值

  • 允许string对象加法运算中其中一个是以空字符结尾的字符数组

  • 反过来,如果想要用string对象来初始化一个c风格字符串,则必须使用c_str()成员函数;

    char cstr[] = {'h', 'e', 'l', 'l', 'o', '\0'};
    string str = cstr;
    string addstr = cstr + str;
    const char *c_addstr  = addstr.c_str();
    

多维数组

// 多维数组内层嵌套的花括号并非必需
int aa[3][4] = {
  {0, 1, 2, 3},
  {4, 5, 6, 7},
  {8, 9, 10, 11},
}; // 等价于
int aa[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
int aa[3][4] = {{0}, {4}, {8}}; // 显式初始化每行首元素
int aa[3][4] = {0, 4, 8, 12}; // 显式初始化第一行

操作示例:

#include <iostream>
#include <iterator>

int main() {
  // 三行四列
  int aa[3][4] = {
      {0, 1, 2, 3},
      {4, 5, 6, 7},
      {8, 9, 10, 11},
  };

  // p指向类型为int[4]
  int(*p)[4] = aa; // p指向aa的首元素
  p = &aa[1];      //  p指向aa的第二个元素

  // 多维数组遍历
  // 范围遍历——外层for需要使用引用类型,否则row为首元素指针,内层遍历指针是不允许的
  for (auto &row : aa) {
    for (auto &col : row) {
      std::cout << col << " ";
    }
    std::cout << std::endl;
  }

  // 数组指针遍历
  for (auto p = std::begin(aa); p != std::end(aa); ++p) {
    for (auto q = std::begin(*p); q != std::end(*p); ++q) {
      std::cout << *q << " ";
    }
    std::cout << std::endl;
  }

  return 0;
}

参考书籍:《C++ Primer》第5版