C++ Primer学习纪录(五)
内置数据类型——数组和C风格的字符串
数组
数组是类似于标准库vector
的数据结构,但性能和灵活程度上与vector
有所区别。通俗来说数组的运行性能优于vector
,但数组缺乏灵活性。
数组的定义和初始化
数组定义一般写为elem_type arr_name[const_expression]
, 其中const_expression
为常量表达式,以下列举了一些数组初始化的方式。
constexpr unsigned size = 42;
int arr[10]; // 含有10个整数
int arr[size]; // 含有42个整数的数组
constexpr int get_size(){
return 10;
}
int arr[get_size()];
数组可以使用列表初始化来显示确定数组的值。
constexpr unsigned sz = 3;
int ial[sz] = {0, 1, 2}; // 列表初始化 拷贝初始化形式
int a2[sz]{0, 1, 2}; // 列表初始化 直接初始化形式
int a3[]{0, 1, 2}; // 不指定数组大小的初始化形式
int a4[sz] = {0, 1}; // a4的前两个元素被初始化为0,1, 最后一个被初始化为0
数组返回的其实是指向数组头元素的指针, 即
int *ptr = &arr[0]
, 因此无法直接给数组赋值。该语句
int a2[] = arr;
是错误的赋值语句; 同样int a2[10]; a2 = arr;
也是行不通的。
复杂数组声明
arr[] = {0 ... 9};
int *ptrs[10]
ptrs
是大小为10的数组,其中的元素为int*
类型(指针类型)
int (*ptrs)[10] = &arr;
ptrs
是指针类型,它指向了含有十个指针的数组。由于数组名实质是指针,因此ptrs
是指向了指针的指针
int (&arrRef)[10] = arr;
arrRef
是引用类型(必须初始化), 它是一个含有10个整数元素数组的别名(arr
的别名),因此代码arrRef[0]
与arr[0]
是一样的
数组元素的访问
数组元素可以使用下标和指针来进行访问。数组的下标被定义为size_t
类型,目的和size_type
是一样的。其定义在cstddef
文件中。
数组的下标
数组的下表和vector
与string
的下标相同,依旧是[0 ~ length)
的左闭右开区间。编译器不会检查下标越界,所以即使能通过编译也不一定能顺利运行。
指针和数组
对于编译器而言,数组通常被看为指针。以下定义会返回指针类型。
int num[] = {0, 1, 2, 3};
// 以下两句是一样的
int *ptr = &num[0]; // ptr指向num的第一个元素
int *ptr2 = num; // ptr2依旧指向数组的第一个元素
// 以下两句是一样的
auto ptr3(num); // ptr3依旧指向数组的第一个元素(指针类型)
auto ptr4(&num[0]); // ptr4依旧指向数组的第一个元素
但对于decltype
而言,情况有点特殊,decltype
直接返回了一个数组而非指针。
decltype(num) num_2 = {3, 4, 5, 6}; // num_2是一个指向4个整数的数组
cout << num_2[2];
output: 2;
指针和数组的区别:
虽然数组能够被看成指针,而且大部分时候数组被隐式转化为了指向首地址的指针类型右值,但实际上他们是有区别的。
数组在对其用
sizeof
和&
操作符时不会转化为指针类型。对于数组名而言,数组名实际就是一块内存的地址,相当于某一块内存的别名;因此数组名是一个地址常量或常量指针(没有分配内存),其地址是无法改变的,无法使用增量运算符(
++arr
是不被允许的), 但可以使用*(arr + 1)
的形式。对于指针而言,除了要为数组开辟一段空间以外,还需要开辟一个额外的空间用来指向数组的首地址(即指针自身所需的空间)。对于指针而言,指针内所保存的地址是可以改变的,因此可以使用增量运算符。
指针访问数组元素
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = arr; // 等同于int *p = &arr[0];
++p;
cout << *p;
output: 1;
上述代码中, int *p = arr
获取了arr
的首地址(当然也可以使用int *p = &arr[0];
来获取),通过同样的方式,可以获取尾元素的下一位置指针;
int *end = &arr[10]
虽然arr[10]
并不存在,但依旧可以通过&arr[10]
来获取地址,但是千万不要对其进行解引用,即*end
操作,这会得到无用数据。
C++11中引入了begin()和end()两个函数来获取数组的首地址与末尾元素后一个位置的地址。
使用前需要包含头文件
#include <iterator>
使用方法如下
#include <iterator> int arr[] = {0, 1, 2, 3, 4}; int *pbeg = begin(arr), *bend = end(arr); while(pbeg != bend){ ... ++pbeg; }
对于数组中元素个数的统计,可以使用
auto n = end(arr) - begin(arr)
这里返回一个
ptrdiff_t
的数据类型,定义在cstddef
中
指针下标
对指针下标进行操作时,编译器会自动转化为指针运算的操作。
例如:
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = arr;
// 以下两句是等价的
p[5] += 7;
*(p + 5) += 7;
因此有个有趣的现象,指针的下标可以是负数(小心指向无意义的内存区域)。
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = &arr[3];
cout << p[-2];
output: 1;
C风格的字符串
C风格的字符串用C的char
类型数组来表示,并且在数组末尾添加\0
用来表示字符串的结束。因此字符串实际占用了 字符串长度+1 的内存空间大小。
当结尾处没有\0
或者开辟空间不足时,都会造成难以估计的错误。
例如:
char str[] = {'C', '+', '+'};
cout << strlen(str);
由于忘记添加\0
,strlen
会一直寻找\0
,直到遇到\0
才会停下来。
由于数组可以近似理解为指针,因此对于两个字符串的一系列操作(拷贝、比较、连接等),无法直接使用运算符来完成,例如
str1 < str2
这样的比较是根本没有意义的, 这比较的是两个指针指向的内存地址的大小,而非字符串大小。因此C语言中有标准库来替我们完成这些操作。
与STL标准库的接口
-
string
允许使用C语言风格的字符串来进行初始化操作,反之不行string str = "123456"; // "123456"是C语言风格的字符串 char str2[] = "123456"; string str3 = str2; map<string, int> mp; mp[str2] = 123456;
-
vector
允许使用数组来进行初始化操作,反之不行int arr[] = {1,2,3,4,5,6}; vector<int> v1(begin(arr), end(arr)); vector<int> v2(begin(arr) + 1, end(arr) - 2);
多维数组
多维数组其实就是数组的数组。
int a[3][4];
int b[3][4][5];
a
是一个长为3
的数组,其中数组的每个元素又是一个长为4
的数组,该数组中每个元素为整数类型。
b
是一个长为3
的数组,其中数组的每个元素又是一个长为4
的数组,该数组中每个元素为长为5
的数组,该数组中元素为整形类型。
范围for
语句处理数组
一维数组
int arr[] = {0, 1, 2, 3, 4};
for(auto &it: arr){
it += 5;
}
多维数组(二维为例)
int arr[3][4] = {0 ... 11};
for(auto &row: arr){
// 加上引用符号,row就是array[4]的一个别名
for(auto col: row){
cout << col << endl;
}
}
for(auto row : arr){
// 不加&时row是指针类型, 为 int* 类型指针
for(auto col = row; col != row + 4; ++col){
cout << *col;
}
}
for(auto row = arr; row != end(arr); ++row){
// row = arr 此时row为int (*row)[4]类型指针
for(auto col = *row; col != end(*row); ++col){
cout << *col << ' ';
}
}