文章目录
3.5 数组
数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活.
如果不清楚元素的确切个数,请使用vector
3.5.1 定义和初始化内置数组
数组中元素的个数也属于数组类型的一部分, 编译的时候维度应该是已知的. 也就是说, 维度必须是一个常量表达式.
unsigned cnt = 42; //不是常量表达式
constexpr unsigned sz = 42; //常量表达式
string bad[cnt]; //错误:cnt不是常量表达式
string strs[get_size()]; //当get_size是constexpr时正确,否则错误
定义数组的时候必须指出数组的类型, 不允许使用auto关键字由初始值的列表推断类型.
数组的元素应该为对象,所以不存在引用的数组.
显式初始化数组元素
如果在声明时没有指明维度, 编译器会根据初始值的数量计算并推测出来; 如果指明了维度, 那么初始值的总数量不应该超出指定的大小. 如果维度比提供的初始值数量大, 则用提供的初始值初始化靠前的元素, 剩下的元素被初始化成默认值.
字符数组的特殊性
我们可以用字符串字面值对字符数组初始化. 当使用这种方式时, 一定要注意到字符串字面值的结尾处还有一个空字符, 这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
char a1[] = {'c','+','+'}; //没有空字符
char a2[] = {'c','+','+','\0'};//含有显式的空字符
char a3[] = "C++"; //自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; //错误,没有空间可存放空字符
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组, 也不能用数组为其他数组赋值
int a[] = {0,1,2};
int a2[] = a; //错误,不允许使用一个数组初始化另一个数组
a2 = a; //错误, 不能把一个数组直接赋值给另一个数组
理解复杂的数组声明
int *ptrs[10]; //ptrs是含有10个整型指针地数组
int &refs[10] = /*?*/ //错误, 不存在引用地数组
int (*Parray)[10] = &arr;//Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
默认情况下, 类型修饰符从右向左依次绑定.
对于ptrs来说, 从右向左理解其含义比较简单: 首先知道我们定义的是一个大小为10 的数组, 他的名字是ptrs, 然后知道数组中存放的是指向int的指针.
对于数组来说, 由内向外阅读要比从右向左好多了.
对于Parray: *Parray意味着Parray是个指针, 观察右边, 可知Parray是个指向大小为10的数组的指针, 观察左边, 可知数组中元素为int.
Parray是个指针, 它指向一个int数组, 数组中包含10个元素.
3.5.2 访问数组元素
数组的元素可以使用范围for语句或下标运算符来访问. 数组的索引从0开始
在使用数组下标的时候, 通常将其定义为size_t类型. size_t是一种机器相关的无符号类型, 它被设计得足够大以便能表示内存中任意对象的大小.
检查下标的值
下标应该大于等于0而且小于数组的大小.
3.5.3 指针和数组
使用数组时编译器一般会把它转换成指针
通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:
string nums[] = {"one","two","three"};
string *p = &nums[0]; //p指向nums的第一个元素
在很多用到数组名字的地方, 编译器都会自动地将其替换为一个指向数组首元素的指针
string *p2 = nums; //等价于 p2 = &nums[0]
在一些情况下数组的操作实际上是指针的操作. 当使用数组作为一个auto变量的初始值时, 推断得到的类型是指针而非数组
尽管ia是由是个整数构成的数组, 但当使用ia作为初始值时, 编译器实际执行的初始过程类似于下面的形式
auto ia2(&ia[0]);
当使用decltype关键字时上述转换不会发生, decltype(ia)返回的类型是由10个整数构成的数组
指针也是迭代器
vector和string的迭代器支持的运算,数组的指针全都支持.
int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; //p指向arr的第一个元素
++p; //p指向arr[1]
标准库函数begin和end
这两个函数与容器中的两个同名成员功能类似, 但数组不是类类型, 所以这两个函数不是成员函数, 正确的使用形式是将数组作为他们的参数
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); //指向ia首元素的指针
int *last = end(ia); //指向ia尾元素的下一位置的指针
指针运算
给一个指针加上(减去)某整数值, 结果仍是指针. 新指针指向的元素与原来的指针相比前进了(后退了)该整数值个位置.
constexpr size_t sz = 5;
int arr[sz] = {1,2,3,4};
int *ip = arr; // int *ip = &arr[0]
int *ip2 = ip + 4;//ip2 指向arr的元素arr[4]
跟迭代器一样, 两个指针相减的结果是它们之间的距离. 参与运算的两个指针必须指向同一个数组当中的元素
auto n = end(arr) - begin(arr);//n等于5,就是arr中元素的数量
两个指针相间的结果的类型是一种名为ptrdiff_t的标准库类型, 是一种定义在cstddef头文件中的机器相关的类型, 因为差值可能为负值, 所以ptrddef_t是一种带符号类型.
只要两个指针指向同一个数组的元素, 或者指向该数组的尾元素的下一位置, 就能利用关系运算符对其进行比较
int *b = arr, *e = arr + sz;
while(b < e){
cout<< *b <<endl;
++b;
}
解引用和指针运算的交互
指针加上一个整数所得到的结果还是一个指针. 假设结果指针指向了一个元素, 则允许解引用该结果指针
int ia[] = {1,2,3,4,5};
int last = *(ia + 4); //把last初始化成5,也就是ia[4]的值
如果表达式含有解引用运算符和点运算符, 最好在必要的地方加上圆括号
last = *ia + 4; // 等价于 ia[0] + 4
下标和指针
int ia[] = {0,2,4,6,8};
此时, ia[0]是一个使用了数组名字的表达式, 对数组执行下标运算其实是对指向数组元素的指针执行下标运算
int i = ia[2]; //ia转换成指向数组首元素的指针,ia[2]得到(ia + 2)所指的元素
int *p = ia;
i = *(p + 2); //等价于i = ia[2];
只要指针指向的是数组中的元素(或者数组中尾元素的下一位置), 都可以执行下标运算
int *p = &ia[2];
int j = p[1];
int k = p[-2]; //p[-2]是ia[0]表示的那个元素
虽然标准库类型string 和 vector也能执行下标运算, 但是数组与它们相比还是有所不同. 标准库类型限定使用的下标必须是无符号类型, 而内置的下标运算无此要求, 上面的最后一个例子很好地说明了这一点. 内置的下标运算符可以处理负值, 当然, 结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置).
3.5.4 C风格字符串
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串. C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束. 以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(’\0’). 一般利用指针来操作这些字符串。
C标准库String函数
头文件: cstring
上表所列函数不负责验证其字符串参数
传入此类函数的指针必须指向以空字符作为结束的数组
char ca[] = {'C','+','+','\0'}; //不以空字符结束
cout<< strlen(ca) <<endl; //错误, ca没有以空字符结束
ca虽然也是一个字符数组但它不是以空字符作为结束的, 因此上述程序将产生未定义的结果. strlen函数将有可能沿着ca在内存中的位置不断向前寻找, 直到遇到空字符才停下来.
比较字符串
比较标准库string对象的时候, 用的是普通的关系运算符和相等性运算符
string s1 = "A string example";
string s2 = "A different string";
if(s1 < s2) // s2 < s1
如果把这些运算符用在两个C风格字符串上, 实际比较的将是指针而非字符串本身
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if(ca1 < ca2) //未定义的, 试图比较两个无关地址
谨记, 当使用数组的时候其实真正用的是指向数组首元素的指针. 因此, 上面的if条件实际上比较的是两个 const char *的值. 这两个指针指向的并非同一对象, 所以将得到未定义的结果.
比较C风格字符串需要调用strcmp函数. 如果两个字符串相等, 返回0. 如果前面的字符串大, 返回正值; 如果后面的字符串大, 返回负值.
目标字符串的大小由调用者指定
3.5.5 与旧代码的接口
混用string对象和C风格字符串
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值
- 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是); 在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象.
上述性质反过来就不成立了
不能用string对象直接初始化指向字符的指针. 为了完成该功能, string专门提供了一个名为c_str的成员函数
string s("Hello world");
char *str = s; //错误, 不能用string对象初始化char*
const char *str = s.c_str(); //正确
c_str函数的返回值是一个C风格的字符串. 函数的返回结果是一个指针, 该指针指向一个以空字符结束的字符数组, 而这个数组所存的数据恰好与那个string对象的一样. 结果指针的类型是const char*, 从而确保我们不会改变字符数组的内容
我们无法保证c_str函数返回的数组一直有效, 事实上, 如果后续的操作改变了s的值就可能让之前返回的数组失去效用
使用数组初始化vector对象
不允许使用一个数组为另一个内置类型的数组赋初值, 也不允许使用vector对象初始化数组. 相反的, 允许使用数组来初始化vector对象. 要实现这一目的, 只需指明拷贝区域的首元素地址和尾后地址就可以了
int int_arr[] = {0,1,2,3,4,5};
// ivec有6个元素, 分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr),end(int_arr));
在上述代码中, 用于创建ivec的两个指针实际上指明了用来初始化的值在数组int_arr中的位置,其中第二个指针应指向待拷贝区域尾元素的下一位置. 此例中,使用标准库函数 begin和end来分别计算int_arr的首指针和尾后指针. 在最终的结果中,ivec将包含6个元素,它们的次序和值都与数组int_arr完全一样.
用于初始化vector对象的值也可能仅是数组的一部分
vector<int> subVec(int_arr + 1, int_arr + 4);