《C++ Primer》(第5版) Chapter03-Strings,Vectors and Arrays


本章:字符串、向量和数组

命名空间的 using 声明

  • 每个名字都需要独立的using声明。使用某个命名空间:例如 using std::cin表示使用命名空间std中的名字cin
  • 头文件中不应该包含using声明。这样使用了该头文件的源码也会使用这个声明,会带来风险。

标准库类型string

  • 标准库类型string表示可变长的字符序列。

  • 代码

    #include <string>
    #include <iostream>
    using std::string;
    using std::cin;
    using std::cout;
    
  • string对象:注意,不同于字符串字面值。


定义和初始化string对象

初始化string对象的方式:

方式解释
string s1默认初始化,s1是个空字符串
string s2(s1)s2s1的副本
string s2 = s1等价于s2(s1)s2s1的副本
string s3("value")s3是字面值“value”的副本,除了字面值最后的那个空字符外
string s3 = "value"等价于s3("value")s3是字面值"value"的副本
string s4(n, 'c')s4初始化为由连续n个字符c组成的串
  • 拷贝初始化(copy initialization):使用等号=将一个已有的对象拷贝到正在创建的对象。
  • 直接初始化(direct initialization):通过括号给对象赋值。

string对象上的操作

string的操作:

操作解释
os << ss写到输出流os当中,返回os
is >> sis中读取字符串赋给s,字符串以空白分割,返回is
getline(is, s)is中读取一行赋给s,返回is
s.empty()s为空返回true,否则返回false
s.size()返回s中字符的个数
s[n]返回s中第n个字符的引用,位置n从0计起
s1+s2返回s1s2连接后的结果
s1=s2s2的副本代替s1中原来的字符
s1==s2如果s1s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感
s1!=s2同上
<, <=, >, >=利用字符在字典中的顺序进行比较,且对字母的大小写敏感
  • string io:
    • 执行读操作>>:忽略掉开头的空白(包括空格、换行符和制表符)并从第一个真正的字符开始,直到遇到下一处空白为止。
    • getline:string 对象会从输入流中读取字符(包括空白符),直到遇见换行符为止。
  • 字符串字面值和 string 是不同的类型。

代码示例:

// 读取未知数量的string对象
int main() {
    string word;
    while (cin >> word) {
        cout << word << endl;
    }
    return 0;
}
// 使用getline读取一整行
int main() {
    string line;
    while (getline(cin, line)) {
        if (!line.empty()) { cout << line << endl; }
    }
    return 0;
} // 触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。

字面值和string对象相加

标准库允许把字符字面值字符串字面值转换成 string对象,所以在需要string对象的地方就可以使用这两种字面值来替代。
当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是 string。涉及连续加法的,最初运算的两个对象至少有一个是 string。

#include <string>
#include <iostream>
using std::string;
using std::cin;
using std::cout;
using std::endl;

int main() {
    string s1 = "hello";
    string s2 = "world";
    string s3 = s1 + ", " + s2 + "\n";

    string s4 = s1 + ", ";  // 把一个string对象和一个字面值相加
//    string s5 = "hello" + ", ";  两个运算符都不是string
// Invalid operands to binary expression ('const char [6]' and 'const char [3]')

    string s6 = s1 + "hello" + ", ";  // 每个加法运算否有一个运算对象是string
//    string s7 = "hello" + ", " + s1;  // 字面量不能直接相加
}

处理string对象中的字符

ctype.h vs. cctype:C++修改了c的标准库,名称为去掉.h,前面加c

函数解释
isalnum(c)c是字母或数字时为真
isalpha(c)c是字母时为真
iscntrl(c)c是控制字符时为真
isdigit(c)c是数字时为真
isgraph(c)c不是空格但可以打印时为真
islower(c)c是小写字母时为真
isprint(c)c是可打印字符时为真
ispunct(c)c是标点符号时为真
isspace(c)c是空白时为真(空格、横向制表符、纵向制表符、回车符、换行符、进纸符)
isupper(c)c是大写字母时为真
isxdigit(c)c是十六进制数字时为真
tolower(c)c是大写字母,输出对应的小写字母;否则原样输出c
toupper(c)c是小写字母,输出对应的大写字母;否则原样输出c

遍历字符串:使用范围for(range for)语句: for (auto c: str),或者 for (auto &c: str)使用引用直接改变字符串中的字符。 (C++11)

int main() {
    string str("some string");
    for (auto c:str) {
        cout << c << endl;
    }
    return 0;
}
int main() {
    string str("some string, hello!!");
    decltype(str.size()) punct_cnt = 0;
    for (auto c:str) {
        if (ispunct(c)) {
            ++punct_cnt;
        }
    }
    cout << punct_cnt << " punctuation characters in " << str << endl;
    return 0;
}

如果想要改变 string 对象中字符的值,必须把循环变量定义成引用类型。

int main() {
    string str("some string, hello!!");
    decltype(str.size()) punct_cnt = 0;
    for (auto &c:str) {
        c = toupper(c);
    }
    cout << str << endl;
    return 0;
}

只处理一部分字符

下标运算符([ ])接收的输入参数是 string::size_type 类型的值,表示要访问的字符的位置,返回值是该位置上字符的引用。string 对象的下标必须大于等于0而小于 s.size()。

int main() {
    string str("some string.");
    if (!str.empty()) {
        str[0] = toupper(str[0]);
        cout << str << endl;
    }
    return 0;
}

使用下标进行迭代

int main() {
  	// 把str的第一个词改成大写形式
    string str("some string, hello!!");
    for (decltype(str.size()) index = 0; index != str.size() && !isspace(str[index]); ++index)
        str[index] = toupper(str[index]);
    cout << str << endl;
    return 0;
}

使用下标执行随机访问

int main() {
    const string str("0123456789ABCDEF");
    cout << "Enter a serial of numbers between 0 and 15 separated by spaces:" << endl;
    string result;
    string::size_type n;
    while (cin >> n) {
        if (n < str.size()) {
            result += str[n];
        }
    }
    cout << result << endl;
    return 0;
}

标准库类型vector

标准库类型 vector 表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。vector “容纳着”其他对象,所以它也常被称作容器(container)。

#include <vector>
using std::vector;

vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,例如 vector<int>


定义和初始化vector对象

初始化vector对象的方法

方法解释
vector<T> v1v1是一个空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...}
  • 拷贝

    int main() {
        vector<int> iv;   // 初试状态为空
        vector<int> iv2(iv);   // 把iv的元素拷贝给iv2
        vector<int> iv3 = iv2;
        return 0;
    }
    
  • 列表初始化: vector<string> v{"a", "an", "the"}; (C++11)

  • 创建指定数量的元素

    vector<int> ivec(10, -1);  // 10个int类型的元素,每个都被初始化为-1
    vector<string> svec(10, "hi");  // 10个string类型的元素,每个都被初始化为“hi”
    
  • 值初始化

    可以只提供 vector 对象容纳的元素数量而不用略去初始值。此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由 vector 对象中元素的类型决定。

    vector<int> ivec(10);   // 10个元素都初始化为0
    vector<string> svec(10); // 10个元素都是空string对象
    

括号区分列表初始值还是元素数量:

  • 圆括号,提供的值是用来构造 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 对象,花括号里的值必须与元素类型相同。不能用int初始化string对象,所以v7和v8提供的值不能作为元素的初始值,编译器尝试用默认值初始化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<vector<int>> ivec;         // 在C++11当中合法
vector<string> svec = ivec;       // 不合法,类型不一样
vector<string> svec(10, "null");  // 合法
vector<int> v1;         // size:0,  no values.
vector<int> v2(10);     // size:10, value:0
vector<int> v3(10, 42); // size:10, value:42
vector<int> v4{ 10 };     // size:1,  value:10
vector<int> v5{ 10, 42 }; // size:2,  value:10, 42
vector<string> v6{ 10 };  // size:10, value:""
vector<string> v7{ 10, "hi" };  // size:10, value:"hi"

向vector对象中添加元素等操作

高效快速地添加元素:先定义一个空的 vector 对象,再在运行时向其中添加具体值。

v.push_back(e) 在尾部增加元素。

#include <vector>

using namespace std;
using std::vector;

int main() {
    vector<int> v;
    for (int i = 0; i < 100; ++i) {
        v.push_back(i);
    }

    string word;
    vector<string> text;
    while (cin >> word) {
        text.push_back(word);
    }
    return 0;
}

vector支持的操作:

操作解释
v.emtpy()如果v不含有任何元素,返回真;否则返回假
v.size()返回v中元素的个数
v.push_back(t)v的尾端添加一个值为t的元素
v[n]返回v中第n个位置上元素的引用
v1 = v2v2中的元素拷贝替换v1中的元素
v1 = {a,b,c...}用列表中元素的拷贝替换v1中的元素
v1 == v2v1v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
v1 != v2同上
<,<=,>, >=以字典顺序进行比较
  • 范围for语句内不应该改变其遍历序列的大小。
  • vector对象(以及string对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能用于添加元素。
  • 举例

    int main() {  // for处理vector对象中的所有元素
        vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};
        for (auto &i :v) {
            i *= i;
        }
        for (auto i:v) {
            cout << i << " ";
        }
        cout << v[8] << endl;
        return 0;
    }
    

迭代器

  • 所有标准库容器都可以使用迭代器。
  • 类似于指针类型,迭代器也提供了对对象的间接访问。

使用迭代器
  • vector<int>::iterator iter
  • auto b = v.begin();返回指向第一个元素的迭代器。
  • auto e = v.end();返回指向最后一个元素的下一个的迭代器。
  • 如果容器为空, begin()end()返回的是同一个迭代器,都是尾后迭代器。
  • 使用解引用符*访问迭代器指向的元素。
  • 养成使用迭代器和!=的习惯(泛型编程)。
  • 容器:可以包含其他对象;但所有的对象必须类型相同。
  • 迭代器(iterator):每种标准容器都有自己的迭代器。C++倾向于用迭代器而不是下标遍历元素。
  • const_iterator:只能读取容器内元素不能改变。
  • 箭头运算符: 解引用 + 成员访问,it->mem等价于 (*it).mem
  • 谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

标准容器迭代器的运算符:

运算符解释
*iter返回迭代器iter所指向的元素的引用
iter->mem等价于(*iter).mem
++iteriter指示容器中的下一个元素
--iteriter指示容器中的上一个元素
iter1 == iter2判断两个迭代器是否相等
int main() {
    string str("some thing");
    if (str.begin() != str.end()) {
        auto it = str.begin();   // 声明了一个迭代器变量it
        *it = toupper(*it);
        cout << *it << endl;
        cout << str << endl;
    }
    for (auto it = str.begin(); it != str.end() && !isspace(*it); ++it) {
        *it = toupper(*it);
    }
    cout << str << endl;
    return 0;
}

从其他语言转C++后,会对for循环中使用!=而非<进行判断有点儿奇怪,C++程序员习惯性地使用!=,原因同更愿意使用迭代器而非下标的原因一样:这种编程风格在标准库提供的所有容器上都有效。只有 string 和 vector 等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,只要养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。

拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型:

#include <vector>
using std::vector;

int main() {
    vector<int>::iterator it;  // it能读写vector<int>的元素
    string::iterator it2;   // it能读写string对象中的字符

    vector<int>::const_iterator it3;    // it3只能读元素,不能写元素
    string::const_iterator it4;     // it4只能读字符,不能写字符
    return 0;
}
int main() {
    const vector<string> str{"some thing", "two", "", "three"};
    for (auto it = str.cbegin(); it != str.cend() && !it->empty(); ++it) {
        cout << *it << endl;
    }
    return 0;
}

迭代器运算

vectorstring迭代器支持的运算:

运算符解释
iter + n迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。
iter - n迭代器减去一个整数值仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。
iter1 += n迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1
iter1 -= n迭代器减法的复合赋值语句,将iter2减n的加过赋给iter1
iter1 - iter2两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
>>=<<=迭代器的关系运算符,如果某迭代器
  • difference_type:保证足够大以存储任何两个迭代器对象间的距离,可正可负。

数组

数组是一种类似于标准库类型 vector 的数据结构,也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问,数组的大小固定。

数组是一种复合类型,数组的声明形如a[d],其中a是数组的名字,d是数组的维度,维度必须是一个常量表达式。

constexpr unsigned sz = 10;
int arr[10];  // 含10个证书的数组
int *parr[sz];	// 含10个整型指正的数组

定义数组的时候必须指定数组的类型,不允许用 auto 由初始值的列表推断类型。和 vector 一样,数组的元素应为对象,不存在引用的数组。

显式初始化数组元素:

constexpr unsigned sz = 3;
int a1[sz] = {1, 2, 3};
int a2[] = {1, 2, 3};
int a3[5] = {1, 2, 3};
string a4[3] = {"one", "two"};

字符串字面值对数组初始化:

char a1[] = {'a', 'b', 'c'};
char a2[] = {'a', 'b', 'c', '\0'};  // a2,a3的维度都是4,a1维度为3
char a3[] = "abc";

数组的内容不能被拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

测试:

// 假设txt_size是一个无参函数,它的返回值是int。
unsigned buf_size = 1024;
(a) int ia[buf_size];   // 非法,维度必须是一个常量表达式。
(b) int ia[4 * 7 - 14];	 // 合法。
(c) int ia[txt_size()];	 // 非法。txt_size() 的值必须要到运行时才能得到。
(d) char st[11] = "fundamental";  // 非法。数组的大小应该是12。

string sa[10];
int ia[10];
int main() {
	string sa2[10];
	int ia2[10];
}
// 数组的元素会被默认初始化。 
// sa的元素值全部为空字符串,ia 的元素值全部为0。 
// sa2的元素值全部为空字符串,ia2的元素值全部未定义。

复杂的数组声明
int *ptrs[10]; // ptrs是含有10个整型指针的数组
int (*Parray)[10] = &arr;  // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr;  // arrRef引用一个含有10个整数的数组
int *(&arry)[10] = ptrs;  // arry是数组的引用,该数组含有10个指针

默认情况下,类型修饰符从右向左依次绑定。对于 ptrs 来说,从右向左理解比较简单:首先定义的是一个大小为10的数组,它的名字是 ptrs,然后数组中存放的是指向 int 的指针。

对于 Parray,由内向外的顺序可以更好地理解 Parray 的含义:首先是圆括号括起来的部分,*Parray 意味着 Parray 是个指针,接下来观察右边,可知道 Parray 是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素是 int,于是最终的含义:Parray 是一个指针,它指向一个 int 数组,数组中包含10个元素。同理,(&arrRef)表示 arrRef 是一个引用,它引用的对象是一个大小为10的数组,数组中元素的类型是 int。

对于 arry,由内向外的顺序阅读,首先 arry 是一个引用,然后观察右边,arry 引用的对象是一个大小为10的数组,最后观察左边,数组的元素类型是指向 int 指针。这样,arry 就是一个含有10个 int 型指针的数组的引用。

小结:理解数组声明的含义,最好从数组的名字开始、按照由内向外的顺序阅读。


访问数组元素
  • 数组下标的类型:size_t
  • 字符数组的特殊性:结尾处有一个空字符,如 char a[] = "hello";
  • 用数组初始化 vectorint a[] = {1,2,3,4,5}; vector<int> v(begin(a), end(a));

指针和数组
  • 使用数组时,编译器一般会把它转换成指针
  • 对数组的元素使用取地址符就能得到指向该元素的指针
  • 标准库类型限定使用的下标必须是无符号类型,而内置的下标可以处理负值
  • 指针访问数组:在表达式中使用数组名时,名字会自动转换成指向数组的第一个元素的指针
  • vector 和 string 的迭代器支持的运算,数组的指针全都支持
#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::endl;

int main() {
    string nums[] = {"one", "two", "three"};
    string *p = &nums[0];  // p指向数组nums的第一个元素
    cout << "*p:" << *p << endl;  // *p:one

    string *p2 = nums;
    // p2 = &nums[0]; 等价
    cout << "*p2:" << *p2 << endl;  // *p2:one

    auto p3(nums);  // p3是一个整型指针,指向nums的第一个元素
    cout << *p3 << endl;
    auto p4(&nums[0]); // 等价于
    cout << *p4 << endl;

    // 指针也是迭代器
    int array[] = {0, 1, 2, 3, 4, 5};
    int *ptr = array;
    cout << *(++ptr) << endl;

    int *endptr = &array[6]; // 尾后指针也不指向具体的元素,不能对尾后指针执行解引用或递增操作。
    for (int *i = array; i != endptr; ++i) {
        cout << *i << " ";
    }

    return 0;
}

标准库函数 begin 和 end

尾后指针不能执行解引用和递增操作。

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::begin;
using std::end;

int main() {
    int iarr[] = {0, 1, 2, 3, 4, 5};
    int *beg = begin(iarr);  // 指向iarr首元素的指针
    int *last = end(iarr);  // 指向iarr尾元素的下一位置的指针
    cout << *beg << endl;
    cout << *(--last) << endl;

    return 0;
}

指针运算

两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,和 size_t 一样,ptrdiff_t 也是一种定义在 cstddef 头文件中的机器相关的类型。因为差值可能为负值,所以 ptrdiff_t 是一种带符号类型。

如果两个指针分别指向不相关的对象,则不能比较它们。

int main() {
    int iarr[] = {0, 1, 2, 3, 4, 5, -1};
    int *beg = begin(iarr);
    int *last = end(iarr);
    auto n = end(iarr) - begin(iarr);
    cout << n << endl;  // 7

    constexpr size_t sz = 5;
    int *ptr = iarr + sz;
    cout << *ptr << endl;
    return 0;
}

虽然标准库类型 string 和 vector 也能执行下标运算,但是与数组不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。

int main() {
    int iarr[] = {0, 2, 4, 6, 8};
    int *p = &iarr[2];  // 指向索引为2的元素
    int j = p[1];  // 等价于 *(p+1), 就是iarr[3]
    int k = p[-2]; // 等价于 *(p-2), 就是iarr[0]
    return 0;
}

C标准String函数
  • 从C继承来的字符串
  • 用空字符结束:\0
  • 对大多数应用来说,使用标准库string比使用C风格字符串更安全、更高效
  • 获取 string 中的 cstringconst char *str = s.c_str();

C标准库String函数,定义在<cstring> 中:

函数介绍
strlen(p)返回p的长度,空字符不计算在内
strcmp(p1, p2)比较p1p2的相等性。如果p1==p2,返回0;如果p1>p2,返回一个正值;
p1<p2,返回一个负值。
strcat(p1, p2)p2附加到p1之后,返回p1
strcpy(p1, p2)p2拷贝给p1,返回p1
int main() {
    char ok[] = {'c', 'p', 'p', '\0'};
    cout << strlen(ok) << endl;  // 3

    char err[] = {'c', 'p', 'p'};
    cout << strlen(err) << endl;  // 严重错误,err没有以空字符结尾
	// 虽然err也是一个字符数组但不是以空字符结尾,因此程序将产生未定义的结果。strlen函数将有可能沿着err在内存中的位置不断向前寻找,直到遇到空字符才停下来。
    return 0;
}

比较字符串

比较标准库 string 对象的时候,

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::strcmp;

bool compare(const string &s1, const string &s2) {
    if (s1 < s2) {
        cout << "s2 more larger." << endl;
    } else {
        cout << "s1 more larger." << endl;
    }
    return true;
}

int main() {
    // 用普通的关系运算符和相等性运算符:
    string s1 = "a string";
    string s2 = "a diff string";
    compare(s1, s2);

    // 如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身:
    const char c1[] = "a string example";
    const char c2[] = "a diff string";
    compare(c1, c2);  // 未定义的:试图比较两个无关的地址

    // 比较两个C风格字符串,和两个string对象的比较s1<s2效果一样
    cout << strcmp(c1, c2) << endl;
    return 0;
}

第二种比较,当使用数组时其实真正用的是指向数组首元素的指针。因此,上面的 if 条件实际上比较的是两个const char * 的值。这两个指针指向的并非同一对象,所以将得到未定义的结果。

若比较两个C风格字符串需要调用 strcmp 函数,此时比较的就不再是指针了。如果两个字符串相等时返回0;如果前面的字符串较大,返回正值;如果后面的字符串较大,返回负值:

连接或拷贝C风格字符串也与标准库string对象的同类操作差别很大:

string s1 = "a string";
string s2 = "a diff string";
string s12 = s1 + s2;

const char c1[] = "a string";
const char c2[] = "a diff string";
// string c12 = c1 + " " + c2;  // 非法

推荐使用标准库 string ,要比使用C风格字符串更安全、更高效。


使用数组初始化 vector 对象

允许使用数组来初始化 vector 对象。不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用 vector 对象初始化数组。

int main() {
    int iarr[]{1, 2, 3, 4, 5, 6};
    vector<int> invec(begin(iarr), end(iarr));
    for (auto v:invec) {
        cout << v << " ";
    }
    vector<int> ivec2(iarr + 1, iarr + 4);
    for (auto v:ivec2) {
        cout << v << " ";
    }
    return 0;
}

建议:尽量使用标准库类型而非数组。

使用指针和数组很容易出错。一部分原因是概念上的问题:指针常用于底层操作,因此容易引发一些与烦琐细节有关的错误。其他问题则源于语法错误,特别是声明指针时的语法错误。

现代的C++程序应当尽量使用 vector 和迭代器,避免使用内置数组和指针;应该尽量使用 string,避免使用C风格的基于数组的字符串。


多维数组

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组

对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

int main() {
    int ia[3][4]{{0, 1, 2,  3},
                 {4, 5, 6,  7},
                 {8, 9, 10, 11}};
    // 等价于
    int ia2[3][4]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

    // 显式地初始化每行的首元素
    int ia3[3][4]{{0},
                  {4},
                  {8}};
    // 显式地初始化第一行
    int ia4[3][4]{1, 2, 3, 4};
    
    // 把row定义成一个含有4个整数的数组的引用,然后将其绑定到ia的第2行
    int (&row)[4] = ia[1];
    return 0;
}
使用范围 for语句处理多维数组

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

int main() {
    constexpr size_t rowCnt = 3, conCnt = 4;
    int ia[rowCnt][conCnt];
    for (size_t i = 0; i != rowCnt; ++i) {
        for (size_t j = 0; j != conCnt; ++j) {
            ia[i][j] = i * conCnt + j;
        }
    }

    // 简化:
    size_t cnt = 0;
    for (auto &row:ia) {
        for (auto &col:row) {
            col = cnt;
            ++cnt;
        }
    }
    return 0;
}

将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转成指针。

假设不用引用类型,程序将无法通过编译。因为,像之前一样第一个循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历,这显然和程序的初衷相去甚远。


指针和多维数组

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。

int iarr[3][4] = {{0, 1, 2,  3},
                  {4, 5, 6,  7},
                  {8, 9, 10, 11}};
int (*p)[4] = iarr;  // p指向含有4个整数的数组,iarr的首行
p = &iarr[2];   // p指向iarr的第3行

(*p)意味着p是一个指针,接着观察右边,指针p所指的是一个维度为4的数组;再观察左边,数组中的元素是整数,因此,p就是指向含有4个整数的数组的指针。

int main() {
    int iarr[3][4] = {{0, 1, 2,  3},
                      {4, 5, 6,  7},
                      {8, 9, 10, 11}};
    for (auto p = begin(iarr); p != end(iarr); ++p) {
        for (auto q = begin(*p); q != end(*p); ++q) {
            cout << *q << " ";
        }
    }
    return 0;
}

小结

string 和 vector 是两种最重要的标准库类型。 string 对象是一个可变的字符序列,vector 对象是一组同类型对象的容器。

迭代器允许运行对容器中的对象进行间接访问,对于 string 对象和 vector 对象来说,可以通过迭代器访问元素或者在元素间移动。

指针。这样得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历,这显然和程序的初衷相去甚远。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值