指针与数组
一、概述
数组是由数据结构相同的一系列元素所组成。需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。
数组的声明: 在变量后面添加一个方括号,如
people[100]
。
数组的访问: 如果要访问数组中的元素,可以使用数组的下标来访问,如访问数组的第2个元素perple[1]
。
二、数组的特性
2.1 数组的初始化
数组的初始化方式有如下4种:
int main() {
//初始化方式1:指定数组大小,同时初始化数组内每一个元素。
int powers1[5] = {1, 2, 3, 4, 5};
//初始化方式2:不指定数组大小,编译器会根据元素的个数来初始化。
int powers2[] = {1, 2, 3, 4, 5};
//初始化方式3,被const修饰的数组内元素不能被修改
const int powers3[] = {1, 2, 3, 4, 5};
//初始化方式4(指定初始化器)
int powers4[5] = {1}; //除了下标为0的元素被赋值为1,其他4个元素都被初始化为0
int powers5[] = {[1] = 2}; //除了下标为1的元素被赋值为2,其他4个元素都被初始化为0
}
2.2 数组的赋值
给数组内元素赋值,可以通过数组下标来实现。
#define SIZE 5
int main() {
int count;
int arr[SIZE];
for (count = 0; count < SIZE; count++) {
arr[count] = count * 2; //通过下标给数组赋值
}
}
2.3 数组边界
访问数组时,要防止数组下标越界的问题。即数组下标不能小于0,也不能大于数组的大小。因为编译器通常无法检测出这种错误。
假设有个数组的大小为 size,则数组下标的区间在:
0<= index < size
。
2.4 指定数组的大小
int main() {
//方式1:声明数组时,指定数组大小。
int powers[5]; //数组还没有初始化,所以访问数组时访问到的可能是垃圾数据。
//方式2:声明数组时,指定数组大小
int powers[sizeof(int) + 1];
//方式3:变长数组。C99之前不允许
int powers[n];
}
2.5 二维数组
如下展示了二维数组的声明、初始化、赋值、取值等操作。
int main() {
// 二维数据的定义:我们可以使用行/列的概念来定义二维数组。第一个[]代表行数,第二个[]代表列数。
int arrs[3][4];
// const 修饰的二维数组,不可修改二维数组内的元素值。
const int arrs[3][4];
// 二维数组的行支持动态指定大小,列数在声明时必须指定,这一点可以从内存分配的特点上来理解。
const int arrs[n][4];
// 二维数组的定义及初始化,带有const时,数组内元素不可变
const int arrs[3][4] = {
{1, 2, 3, 4},
{2, 4, 6, 8},
{3, 6, 9, 12}
};
// 将二维数组第2行第3个元素设置为99
arrs[1][2] = 99;
// 访问二维数组第2行第3个元素
int value = arrs[1][2];
printf("%d", value); //结果:6
}
三、指针和数组
除了使用数组变量来访问数组外,我们还可以通过指针来访问数组内的元素。指针提供了一种以符号形式使用地址的方法。
3.1 一维数组与指针
数组与地址是如何关联的呢?
数组名是数组首元素的地址(即
arr == &arr[0]
,两者都是常量)。
通过指针如何访问指定位置元素的地址,如访问 arr 中第2个元素的地址?
通过数组获取元素地址:
&arr[1]
通过指针获取元素地址:pi + 1
或者pi++
(pi 是指向arr的指针,pi++指向当前pi内存地址所在元素的下个元素,同时会导致pi指向的内存地址也发生改变)
通过指针如何访问指定位置的元素,如访问 arr 中第2个元素的值?
通过数组获取元素值:
arr[1]
通过指针获取元素值:*(pi + 1)
间接运算符(*)的优先级高于加号(+)。
指针与数组的不同点:
arr 是数组首元素的地址,是一个常量,不能通过 arr++ 来获取下一个元素的内存地址。
指针是一个变量,可以使用pi++
来指向下一个元素所在的内存地址。
示例:
int main(void) {
// 声明并初始化一个长度为5的数组。
int arr[] = {1, 2, 3, 4, 5};
int isEqual = arr == &arr[0]; // 数组名是数组首元素的地址,因此这个等式成立。
printf("地址相同返回1:%d\n", isEqual);
int * pi; // 声明一个int类型的指针
pi = arr; // 将arr数组指向指针pi,然后就可以通过指针pi来访问数组了。
//由于指针pi指向数组的首元素,因此直接通过 *pi取值,取到的是数组中第一个元素。
printf("*pi = %d\n", *pi);
printf("************\n");
for (int i=0; i<5; i++) {
printf("arr[%d] = %d\n", i, *(pi + i));
}
}
输出结果如下:
地址相同返回1:1
pi = 1
***********
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
3.2 二维数组与指针
前面我们讲过了二维数组的声明及初始化,也讲过了一维数组与指针之间的关系,下面我们来深入了解一下二维数组与指针之间的关系。
int main() {
const int arrs[3][4] = {
{1, 2, 3, 4},
{2, 4, 6, 8},
{3, 6, 9, 12}
};
int (*pi)[4]; //声明指向二维数组的指针
pi = arrs;
// 二维数组名arrs指向的地址等于二维数组第一个元素arrs[0]所在的地址。
printf("arrs地址 = %p\n", arrs);
printf("arrs[0]地址 = %p\n", &arrs[0]);
printf("===========\n");
// 元素地址移动指定单位大小
printf("&arrs 地址 = %p\n", &arrs); //&arrs取到的是整个数组起始地址
printf("(&arrs+1) 地址 = %p\n", &arrs + 1); //(&arrs + 1) 移动了整个二维数组字节大小的内存地址
// 获取二维数组里的第2行元素的地址,即 &arrs[1]
printf("arrs[1] 地址 = %p\n", &arrs[1]);
printf("(arrs + 1) 地址 = %p\n", arrs + 1); //数组名+1表示行数加1
printf("(pi + 1) 地址 = %p\n", arrs + 1);
printf("===========\n");
// 访问第m行元素里的第n个元素,如:arrs[1][1].
printf("arrs[1][1]地址 = %p\n", &arrs[1][1]); //数组访问方式获取元素地址
printf("(*(arrs + 1) + 1) = %p\n", (*(arrs + 1) + 1)); //使用数组名获取对应元素的地址
printf("(*(pi + 1) + 1) = %p\n", (*(pi + 1) + 1)); //使用指针获取对应位置的地址
printf("===========\n");
// 访问指定位置的元素值,如:arrs[1][1].
printf("arrs[1][1]的值 = %d\n", arrs[1][1]); //数组访问方式获取元素值
printf("*(*(arrs + 1) + 1) = %d\n", *(*(arrs + 1) + 1)); //使用数组名获取对应元素的地址
printf("*(*(pi + 1) + 1) = %d\n", *(*(pi + 1) + 1)); //使用指针获取对应位置的地址
printf("===========\n");
}
输出结果如下:
结论1:
arrs 地址 = 0x7ffeefbff4a0
arrs[0] 地址 = 0x7ffeefbff4a0
===========
结论2:
&arrs 地址 = 0x7ffeefbff4a0
(&arrs+1) 地址 = 0x7ffeefbff4d0 // 对比0x7ffeefbff4a0 地址值,移动了 12 * 4 个字节(12个int)。
arrs[1] 地址 = 0x7ffeefbff4b0
(arrs + 1) 地址 = 0x7ffeefbff4b0
(pi + 1) 地址 = 0x7ffeefbff4b0
===========
结论3:
arrs[1][1]地址 = 0x7ffeefbff4b4
((arrs + 1) + 1) = 0x7ffeefbff4b4
((pi + 1) + 1) = 0x7ffeefbff4b4
===========
结论4:
arrs[1][1]的值 = 4
*(*(arrs + 1) + 1) = 4
*(*(pi + 1) + 1) = 4
===========
小结:
- 二维数组名指向的地址
arrs
与二维数组第一个元素的地址arrs[0]
相同,与对二维数组名取地址值&arrs
后获得的值相等 (即arrs == arrs[0] == &arrs
)。 &arrs + 1
得到的地址值基于&arrs
地址移动整个二维数组的内存大小。arrs + 1
得到的地址值基于arrs[0]
地址移动二维数组1行的内存大小 (即 4 * 4 个字节)。*(arrs + 1)
即为获取二维数组第二行元素,获取到的是一个数组{2, 4, 6, 8}
,所以可以把*(arrs + 1)
看做是指向数组{2, 4, 6, 8}
的首元素地址,因此(*(arrs + 1) + 1)
即为取二维数组第二行第2个元素的地址,*(*(arrs + 1) + 1)
即为取二维数组第二行第2个元素的值。(*(pi + 1) + 1)
与(*(arrs + 1) + 1)
类似,*(*(arrs + 1) + 1)
与*(*(pi + 1) + 1)
类似。
四、函数与数组
在实际项目中,也常常将数组作为参数传递到函数中。而上面我们讲到,可以使用指针来操作数组,因此函数的入参也就存在两种表示方法。
数组:使用数组作为函数参数:
//函数原型:使用数组
int sum(const int arrNew[], int n);
int main(void) {
int arr[] = {1, 2, 3, 4, 5};
int total = sum(arr, 5);
printf("求1到5的总和:%d\n", total);
}
int sum(const int arrNew[], int n) {
int total = 0;
for (int i=0; i<n; i++) {
total += arrNew[i];
}
return total;
}
指针:使用指针作为函数参数:
//函数原型:使用指针
int sum(const int * arrNew, int n);
int main(void) {
int arr[] = {1, 2, 3, 4, 5};
int total = sum(arr, 5);
printf("求1到5的总和:%d\n", total);
}
// 细节:添加const修饰指针,表示不可以用指针来修改数组内的元素。
int sum(const int * arrNew, int n) {
int total = 0;
for (int i=0; i<n; i++) {
total += *(arrNew + i);
//total += *(arrNew++); //
}
return total;
}
小结:
参数类型 | 优点 | 缺点 |
---|---|---|
数组 | 函数内对数组进行操作不会影响到外面的数组 | 会拷贝一份数组到方法栈中,增加内存的消耗 |
指针 | 拷贝的是指向数组arr的指针地址,内存消耗小 | 函数内对参数 arrNew 进行操作时,会影响到外面的数组 |