What & How & Why

这是本文档旧的修订版!


数组、Vector 与字符串

第 3 章笔记


数组

Intro

  • 数组是一种类型,由多个相同对象组成的序列
定义数组
  • 数组的类型与包含的对象类型不同:数组的类型由对象的类型和自身的长度组成

int a; //int
int b[10]; // int[10]

  • 数组的长度必须是常量表达式
    • 长度需要大于 0(负数是不行的,)
    • 长度需要能转换为 size_t(converted expression,比如浮点就不行)

int x;
std::cin >> x;
//不合法
//类型由编译器决定,但长度要在运行期才能得到
//编译器无法推断出b[x] 的类型
int b[x];
//合法
constexpr short y = 3;
int c[y];

  • C++ 标准不支持运行期数组长度;但在编译器实现中, gcc 和 clang 都可以通过去掉 –pedantic-errors tag 来支持 variable length array。但这样做是有风险的,因为该功能是 complier denpend。除此之外,某些必须在编译器完成的功能也不能使用该类类型做为参数,比如 decltype(b)
数组的初始化
  • 默认初始化:元素都会按照默认的方式来创建(根据C++ 的方式)
  • 聚合初始化
    • 编译器可以自行推断数组长度
    • 给出的元素不足,剩余元素会进行默认初始化

int b[3]; //default int
int c[3] = {1, 2, 3}; // int[3], aggregate init
int cn[] = {1,2, 3}; //int[3]
int d[3] = {1,2} // partially aggregate init, 1,2,0

  • 不能使用 auto:auto 会将 {} 视作 initialization_list
  • 不能复制数组:
    • 初始化不能通过赋值构造
    • 拷贝赋值的结果是拷贝的指向数组首元素的指针
  • 数组作为右值会退化为指针。
  • 使用指针访问数组性能更好
字符串数组的特殊性

//char[6] 简化定义写法,会自动添加 ''\0'' 表示结束
char str[] = "hello";
//char[5] 完整定义写法,但不会为末尾添加 ''\0''
char str[] = {‘h’, 'e', 'l', 'l', '0'};

数组的复杂声明

  • 指针为元素的数组

//a[3] 是有3个元素为指针的数组
int* a[3];

  • 指向数组的指针

//注意括号
//a 是指针,指向 int[3]
int (*a) [3];

  • 数组的引用

//a 是引用,指向 int[3]
int b[3];
int (&a) [3] = b;

引用不支持聚合初始化。C++ 中不支持元素为引用的数组。从概念上来说,引用不是对象。数组要求元素使对象,因此没有元素为引用的数组。

数组中的元素访问

  • 数组中的第一个元素下标为 0
  • 通过下标操作符访问:a[0]
数组访问的细节
  • C++ 中,左值的含义被修改为了 locator value,也就是带地址信息的值(而不是按等号的左右来决定)。
    • 数组是左值,但不能放在等号左边
    • 在等号右部使用(作为右值)时,数组的类型会隐式转换为指向数组头元素的指针。也就是说,指针也可以使用 [] 操作符
    • 当使用 x[y],且 x, y 都是 Built-in 类型时,x[y] 被解析为 *(x+y),也就是转换为指针,再解引用。
    • 需要注意下标越界

int a[3] = {1,2,3};
auto b = a;
//int(&)
decltype(b)
//int*
a;
//int*
b;
//int, *(b+0)
b[0]

可以使用 decltype 来判断表达式是否是左值。

数组和指针

数组到指针的隐式转换

  • 使用数组的时候通常数组会被转化为指针
    • 会丢失一部分信息(比如长度,编译器无法检查,这也是下标越界的原因)

// decay, b 是 int* 类型
int a[3];
auto b = a;
//不会 decay 的范例
//decltype 返回的是数组的类型
// int[3]
decltype(a);
// int size * 3
sizeof(a);

  • 可以通过声明避免隐式转换

// b 是 Int(&)[3] 类型
// 此时长度信息没有丢失
auto& b = a;

  • 不要使用 extern 指针声明数组

//声明数组
//不能使用 extern 指针
extern int array[4];
//illegal
//runtime error

extern int* array;

为什么不能这么做?

很多情况下,长度信息需要反复的修改。如果在声明的时候带上长度信息,那么每次更改 array 的长度都必须同时去声明中改变 array 的长度。因此很多情况下会使用指针替代数组。但 extern 不能直接使用指针代替数组。来看看下面的例子:

//source.cpp
int array[4] = {1,2,3,4};
void fun()
{
    std::cout << array << std::endl;
}
//main.cpp
extern int* array;
void fun();

int main()
{
    fun();
    std::cout << array << std::endl;
}

// output
// 两个 array 的输出不一样
0x7ff718ee3000
0x200000001

问题出在哪里?

  • 编译阶段没有问题,array 会被翻译为 *(array + step)
  • 链接过程中:无论是 main.cpp 和 source.cpp 中的 array 是没有类型的,只有名称。这是因为类型只在编译期有效,如果加了更多的类型信息可能会导致不同翻译单元之间的链接问题。
  • 运行过程中:
    • source.cpp 中的打印结果是正确的,因为 source.cpp 中的 array 代表的数组可以被编译器识别,此时编译器认为是指向 array 头元素的指针
    • main.cpp 中的 array,实际上不是一个地址:
      • array[4] 中存储的类型是 int(32字节)一个16进制数为 $2^4$,占 4 bits,那么每个数组成员都需要 32 / 4 = 8 个 16 进制数来表示,比如 1 就是 00 00 00 01
      • 按小端法,int 会从低地址到高地址保存: 01 00 00 00
      • 数组中的数是连续存储的,因此该数组在内存中表现为: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
      • 而此时我们此时以指针的方式来解析该段数据,那么就会将前 8 个子节的内容拼成一个类似地址的内容,也就是 0x 2 00 00 00 01
      • 该内容并不是 array 的首元素地址,而是 01 00 00 00 02 00 00 00 按内存地址的方式解析出来的结果。这个结果反应的是数组的内容,是固定的,
      • 可见数组和指针还是存在区别的

应该怎么做?

extern int array[];
这么写合法。int array[] 是数组的合法声明。定义已经在 source.cpp 中完成了。

  • 该方式被称为 Unkonwn Bounded Array Declaration
  • int array[] 被称为 incomplete type,这种类型大多数用于共享定义的场景中
指向数组开头和结尾的指针
  • 开头指针:头元素
  • 结尾指针:尾元素后一位

计算的两种方法:

int a[3] = {1,2,3};
//开头指针
a; //数组名即为指向数组首地址的指针
&(a[0]) // 取数组首元素的地址(注意括号)
std::begin(a); std::cbegin(a); //标准库提供的取地址函数,int* 和 const int*

//结尾指针
a + 3;
&(a[3]);
std::end(a); std::cend(a);
需要注意的是,std::begin()std::end() 适用于数组类型而不是指针类型,任何导致数组信息的丢失(转换,Unkonwn bounded)都会导致调用失败:
b = a;
//error, begin() & end() are not applicable to a pointer.
std::begin(b); std::end(b); 
//之前的共享 array 也不能使用 begin 和 end,因为边界未知
extern int array[];
std::begin(b); std::end(b);
对于 incomplete type 的获取,如需使用 std::begin()std::end(),需要在声明的时候声明为 complete type,也就是提供其数组的长度:
//ok
extern int array[4];
std::begin(b); std::end(b);

std::begin(); std::end(); 是泛型函数,可以应用到各种类型的容器中。

指针的其他算术运算

int a[3] = {1,2,3};
auto ptr = a;
auto ptr2 = a;
//指针的前进
ptr = ptr + 1;
//比较
ptr == ptr2;
//关系运算(不推荐,除非两个指针在同数组中)
ptr > ptr2;
//求距离
ptr2 - ptr

数组的其他操作

求数组元素个数
  • sizeof():使用 sizeof 获取数组总大小,再除以元素的宽度
  • std::size():使用标准库方法
  • std::end() - std::begin()

/* 必须都是对数组类型进行操作 */
int b[10];
//sizeof,[C method]
sizeof(b) / sizeof(int);
//std::size, [recommand]
std::size(b);
//end - begin, [run time method],通常使用 const 版本。
std::end(b) - std::begin(b);

遍历

int c[3] = {1,2,3};
//使用 std::size() 和 while
size_t index = 0;
while(index < std::size(c))
{
    std::cout << c[index] << '\n';
    index++;
}

//cbeign, cend
auto bPtr = std::cbegin(c);
while(bPtr != std::cend(c))
{
    std::cout << *bPtr << '\n';
    bPtr++;
}
//range for ,语法糖,基于 cbegin / cend 实现
for (auto elem : c)
{
    std::cout << elem << '\n';
}

C 字符串