C 语言中对于数组有一些相关点需要注意:
-
C 语言中只有 1 维数组, 且 数组的大小必须在编译期间就作为一个
常数确定下来. -
C 语言的数组中可以存放任意类型的数据, 也可以嵌套数组实现多维数组.
-
对一个数组, 只能做 2 件事:
a. 确定数组的大小
b. 获得指向该数组下标为 0 的元素的指针, (其他有关数组的操作, 即使是数组下标进行计算, 实际上都是通过 指针进行的.)
关于指针的理解:
- 任何指针 都是指向某一种数据类型的变量
- 给一个指针加上 1 个整数 与 将该指针的 2 进制表示加上同样的整数的含义不同 !
声明数组
int arr[6] {0, 1, 2, 3, 4, 5};
实际上是 把数组 arr 中下标为 0 的元素赋值给 p
int *p = arr;
// 初始化进行隐式类型转换
int brr[5] = {'a', 'b', 'c', 'd', 'e'};
brr[4] 和 4[brr] 等效
数组名只有在作为 sizeof() 运算符的操作数时, 计算的才是整个数组的大小, 其他情况下退化为指针, 代表的是指向数组中 下标为 0 的元素的指针
所以可以理解为: *a 是数组总下标为 0 的元素的引用, 所以 *a = 100 可以改变对应的值
*(a + i) 是数组中下标为 i 的元素的引用, 可以简写为 a[i]
实际上: 由于 a + i 和 i + a 的含义一样, 所以 a[i] 和 i[a] 也是完全等价的, 只是用方括号的下标形式简化了 指针的表达,
不建议使用
i[a]这种写法, 公式:a[b] = *(a + b)
2 维数组
int aa[3][4] {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}};
std::cout << aa[2][2] << std:: endl;
// 其实相当于
std::cout << *(aa[2] + 2) << std:: endl;
// 也相当于
std::cout << *(*(aa + 2) + 2) << std:: endl;
// 声明指向 数组的指针,
int (*parr)[4];
实际意义是: 声明 *parr 是一个拥有 4 个整型数据的 数组, 因此 parr 就是一个指向 每一个元素都是 int [4] 的数组的指针,
或者理解为, 对 parr 解引用的每一个数据都是 int[4]
所以 parr 类型(形参类型)为 int **,
// 遍历 2 维数组
// a. 下标形式
// e.g. 清空数组
int aa[3][4] {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}};
int count;
for (count = 0; count < 3; count ++) {
int m;
for (m = 0; m < 4; m ++) {
aa[count][m] = 0;
}
}
// b. 指针形式
int (*pointer)[4];
std::cout << "&aa[0] : " << &aa[0] << std::endl;
std::cout << "&aa[2] : " << &aa[2] << std::endl;
std::cout << "&aa[3] : " << &aa[3] << std::endl;
for (pointer = aa; pointer < &aa[3]; pointer ++) {
std::cout << "address pointer start : " << pointer << std::endl;
// 处理 int 数组元素
int *ps;
for (ps = *pointer; ps < &( (*pointer)[4]); ps ++) { // 地址偏移
std::cout << "address : " << ps << " ,val : " << *ps << std::endl;
}
}
// 数组中最后一个元素(12) 的首地址: *(aa + 3) - 0x04
std::cout << "address of end : " << ( *(cc + 3) - 0x4 ) << std::endl;
// 如果要取出元素 7, aa[1][2] 或者 *( *(cc + 1) + 2)
&aa[0] : 0x7ffc6233ff80
&aa[2] : 0x7ffc6233ffa0
&aa[3] : 0x7ffc6233ffb0
address pointer start : 0x7ffc6233ff80
address : 0x7ffc6233ff80 ,val : 1
address : 0x7ffc6233ff84 ,val : 2
address : 0x7ffc6233ff88 ,val : 3
address : 0x7ffc6233ff8c ,val : 4
address pointer start : 0x7ffc6233ff90
address : 0x7ffc6233ff90 ,val : 5
address : 0x7ffc6233ff94 ,val : 6
address : 0x7ffc6233ff98 ,val : 7
address : 0x7ffc6233ff9c ,val : 8
address pointer start : 0x7ffc6233ffa0
address : 0x7ffc6233ffa0 ,val : 9
address : 0x7ffc6233ffa4 ,val : 10
address : 0x7ffc6233ffa8 ,val : 11
address : 0x7ffc6233ffac ,val : 12
address of end : 0x7ffc6233ffa0
int arr2[3] = {0, 1, 2};
std::cout << *( &(arr2[1] + 1)) << std::endl; // 2
1. 数组并非指针
// 声明 x 是一个 int 型的指针
extern int *x;
// 声明 y 是一个 int 型的数组
// 长度尚未确定 (所以是不完整的类型), 其存储在别处定义
extern int y[];
什么是声明 ? 什么是定义 ?
C 语言中的对象(这里指的是 函数和变量之类的) 必须有且只有一个 定义, 但是可以有多个 extern 声明,
定义是一种特殊的声明, 它创建了一个对象
声明 简单地说明了在其他地方创建的对象的名字, 它允许你使用这个名字
// 定义, 只能出现在一个地方
// 确定对象的类型 并 分配内存, 用于创建新的对象, 如:
int my_array[100];
// 声明, 可以多次出现,
// 用来 描述对象的类型, 用于指代 其他地方定义的对象, 例如在其他的文件里, 如:
extern int my_arr[];
extern 对象声明告诉编译器对象的类型 和 名字, 具体对象的 内存分配则在别处进行
由于 并未在声明中为数组分配内存, 所以并不需要 提供关于数组的长度等信息,
但是, 对于多维数组, 声明时需要 提供除最左边一维之外的其他维的长度 ---> 需要给编译器足够的信息产生对应的代码
同时还需要明确一个概念: 地址 x 和 地址 x 的内容 的区别
x = y;
符号 x 的含义是 x 所代表的地址
这 被称为 左值
左值在编译期可知, 左值实际上表示的是 存储结果的地方
y 的含义是 y所代表的地址的 内容
这 被称为 右值
右值是 直到运行时才知的, 如果没有特别说明, 右值表示 y 的内容
出现在赋值运算符 = 左边的称为 左值 (右边称为 右值),
编译器为每个变量分配一个地址(左值), 这个地址在编译期可知, 而且该变量在运行时 一直保存在这个地址,
相反,
存储于变量中的值(它的右值), 只有在运行时才可知, 如果需要用到变量中存储的值, 编译器就会发出指令从 指定的地址中读入变量值 并 将它存储到寄存器中.
关键之处在于:
- 每一个符号的地址都是在编译期可知, 如果编译器需要一个地址(可以添加偏移量)来执行某操作, 就可以直接进行操作, 并不需要增加指令来先获取具体的地址
- 对于指针, 必须首先在
运行时取得它的当前值, 然后才能对它进行解引用操作
对数组下标的引用
这就是为什么 extern char arr[] 与 extern char arr[100] 等价的原因.
这两个声明都提示 arr 是一个数组, 也就是一个 内存地址, 数组内的字符可以从这个地址找到, 编译器并不需要知道数组一共有多长, 因为它只产生 偏离起始地址的偏移地址, 如: 从数组中提取一个字符, 只需要简单的从符号表显示的 arr 的地址 + 下标即可,
对指针的引用
如果声明为 extern char *ptr; 将会告诉编译器这是一个指针, 指向的对象是一个字符,
为了取得这个字符, 必须得到地址 ptr 的内容, 把它作为字符的地址并从这个地址中取得这个字符
指针的访问更加灵活, 但是 需要增加一次额外的提取
定义为指针, 引用为数组的问题
test1.cpp
// 定义为指针
int *mango;
test2.cpp
// 引用为数组
extern int mango[100];
编译器对内存 mango 进行直接引用 (把 mango 当作符号从符号表中直接取地址), 但是实际上操作的是指针 mango, 进行的是间接取地址,
声明为什么, 编译器就会将其解释成什么
如果定义为数组, 引用为指针: test1.cpp
// 定义为数组
int mango[100];
// 引用为指针
extern int *mango;
这种情况下, 如果使用 mango[i] 这种形式提取这个声明中的内容, 实际上得到的 是一个字符 (因为原先定义 mango 是数组名),
但是参考上述对指针的引用, 编译器会需要从 mango 中读取地址, 然后拿着这个地址去取对应地址的内容, 但是在这里 mango 取出的内容是字符, 所以出错
把 ascii 字符解释为地址 存在风险 !, 会污染程序地址空间的内容, 将来会出现莫名的错误
如果引用使用 extern int *mango; 会出现段错误
正确的声明为 extern int mango[];
结论:
如果声明为指针, 只有原来定义为指针时才是合法的 ~
数组和指针的区别
| 指针 | 数组 |
|---|---|
| 保存数据的地址 | 保存数据 |
| 间接访问数据, 首先取得地址的内容, 把该内容作为一个地址, 然后从新地址中获取数据 | 直接访问数据, a[i] 只是简单的以 a+i 为地址获取数据, 等价于 *(a + i) |
如果 指针有一个下标[], 即 ptr[i] 表示直接把 指针的内容 加上 i 作为新地址, 并从中提取数据 | |
| 通常用于动态数据结构 | 通常用于存储 固定数目 且 数据类型相同的元素 |
相关函数, malloc(), free() | 隐式分配 和 删除 |
| 通常指向 匿名数据 | 自身就是数据名 |
注意点
数组和指针都可以在 定义时用 字符串常量 进行初始化, but 底层机制不同:
定义指针时, 编译器不会为指针所指向的对象分配空间, 只是分配指针本身的空间, 除非在指针定义同时 给它赋予一个字符串常量进行初始化, 例如:
char *p = "hello"; // 程序会为字符串常量分配内存
// 但是仅仅局限于 字符串常量
float *ptr = 1.23; // 编译错误
// 初始化指针所创建的字符串常量 被 定义为只读
// 如果尝试通过指针修改这个字符串的值, 程序会出现为定义行为
// 数组也可以用 字符串常量进行初始化, 由字符串常量初始化的数组是可以修改的
char arr[] = "helloworld";
测试:
// 验证数组 和 指针使用字符串常量进行初始化的区别
char *ptr1 = "helloworld1";
char ptr2[] = "helloworld2";
printf("ptr1[10] = %c\r\n", ptr1[10]); // 读取 ok
// ptr1[10] = 3; // 段错误
printf("ptr2[10] = %c\r\n", ptr2[10]); // 读取 ok
ptr2[10] = 114; // 修改 ok, 这里会进行隐式类型转船, int -> char
printf("ptr2[10] = %c\r\n", ptr2[10]); // 读取 ok