数组和指针
数组
数组由数据类型相同的一系列元素组成。下面是一些数组声明:
int main(void)
{
int Month[12]; //内含12个整型元素
float salary[25]; //内含25个浮点型元素
char id[12]; //内含12个字符元素
}
要访问数组中的元素,通过使用数组下标数(也成为索引)表示数组中的各元素。
数组元素编号从0开始,所以Month[0]是第一个元素。
初始化数组
只存储单个值的变量有时也称为标量变量,如:
int a = 1;
float b = 1.0f;
数组的初始化为:
int num[5] = {1, 2, 3, 4, 5};
用以逗号分隔的值列表来初始化数组。
注意
有时需要把数组设置为只读,这样程序只能从数组中检索值,不能把新值写入数组。示例:
const int days[MONTHS] = { 31, 28, 31,30,31,30,31,31,30,31,30,31 };
一但声明为const,便不能再给它赋值。
给数组元素赋值
声明数组后, 可以借助数组下标给数组元素赋值,示例:
//给数组元素赋值
#include<stdio.h>
#include<stdlib.h>
#define SIZE 10
int main(void)
{
int ar[SIZE] = {}; //声明一个数组,这里其实把所有元素初始化为0了
for (int i = 0; i < SIZE; ++i)
{
ar[i] = rand() % 100;
}
for (int i = 0; i < SIZE; ++i)
{
printf("%5d", ar[i]);
}
printf("\n");
return 0;
}
输出结果:
这段程序充分体现了如何利用数组下标来访问数组元素的。
C不允许把数组作为一个单元赋给另一个数组。
错误示例:
#define SIZE 5
int main(void)
{
int ar[SIZE] = { 1,2,3,4,5 };
int br[SIZE];
br = ar; //不允许
br[SIZE] = ar[SIZE]; //数组下标越界
}
数组边界
在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。
假设有: int ar[20];
那么在使用该数组时,要确保程序中使用的数组下标在0~19的范围内。
指定数组的大小
在C99标准之前,声明数组时只能在方括号内使用整型常量表达式。
另外,表达式的值必须大于0。
示例:
int main(void)
{
int n = 5;
int m = 8;
float a1[5]; //合法
float a2[5 * 2 + 1]; //合法
float a3[sizeof(int) + 1]; //合法
float a4[-4]; //不可以,数组大小必须大于0
float a5[0]; //不可以,数组大小必须大于0
float a6[2.5]; //不可以,数组大小必须是整数
float a7[(int)2.5]; //可以,已被强制转换为整型
float a8[n]; //VS2019不可以,表达式必须是常量,n是变量
}
多维数组
二维数组
示例:
若要计算五年内每个月的降水量,这些数据分开存储是要比创建一个内含60个元素的数组好。
可以这样创建数组:
float Rain[5][12];
主数组有五个元素(每个元素表示一年),每个元素是内含12个元素的数组(每个元素表示一个月)。
Rain的首元素Rain[0]是一个内含12个float类型值的数组。
该图可以方便理解二维数组的两个下标。
初始化二维数组
方便理解,可以这样初始化二维数组:
#include<stdio.h>
#define ROW 4
#define COL 4
int main(void)
{
int arr[ROW][COL] =
{
{1, 2, 3, 4},
{5, 6, 7, 8 },
{9, 10, 11, 12},
{13, 14, 15, 16}
};
}
访问二维数组中的元素
若要依次访问二维数组中的元素,可以通过循环的嵌套来实现:
#include<stdio.h>
#define ROW 4
#define COL 4
int main(void)
{
int arr[ROW][COL] =
{
{1, 2, 3, 4},
{5, 6, 7, 8 },
{9, 10, 11, 12},
{13, 14, 15, 16}
};
for (int i = 0; i < ROW; ++i)
{
for (int j = 0; j < COL; ++j)
{
printf("%4d", arr[i][j]);
}
printf("\n");
}
return 0;
}
输出结果:
指针和数组
首先要知道:
数组名是数组首元素的地址。即:
假设ar是一个数组,则有
ar == &ar[0];
两者都是常量,可以把他们赋值给指针变量。
在C中,指针加一指的是增加一个存储单元。对数组而言,这意味着加1后的地址是下一个元素的地址,而不是下一个字节的地址。
- 指针的值是它所指向对象的地址。
- 在指针前面使用*运算符可以得到该指针所指向对象的值。
- 指针加1,指针的值递增它所指向类型的大小(以字节为单位)。
下面的等式体现了C语言的灵活性:
date + 2 = &date[2]; //相同的地址
*(date + 2) = date[2]; //相同的值
从本质上看,同一个对象有两种表示法。定义ar[n]的意思是 *(ar + n)。
函数,数组和指针
声明数组形参
因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。
只有在这种情况下,C才会把int ar[]和int * ar解释成一样。
下面4种原型都是等价的:
int sum1(int* ar, int n);
int sum2(int*, int);
int sum3(int ar[], int n);
int sum4(int[], int);
使用指针形参
函数要处理数组必须知道何时开始,何时结束。
介绍两个办法:
一是函数使用一个指针形参标识数组开始,再用一个整数形参表面待处理数组的元素个数。
二是传递两个指针,第一个指针指明数组的开始处,第二个指针指向数组的结束处。
示例:
#include<stdio.h>
#include<assert.h>
//第一种
int Sum_1(int* ar, int n)
{
assert(ar != nullptr);
int sum = 0;
for (int i = 0; i < n; ++i)
{
sum += ar[i];
}
return sum;
}
//第二种
int Sum_2(int* start, int* end)
{
assert(start != nullptr && end != nullptr);
int sum = 0;
while (start < end)
{
sum += *start;
start++; //让指针指向下一个元素
}
return sum;
}
int main(void)
{
const int SIZE = 10;
int ar[SIZE] = { 1,2,3,4,5,6,7,8,9,10 };
printf("sum1 = %d\n", Sum_1(ar, SIZE));
printf("sum2 = %d\n", Sum_2(ar, ar + SIZE));
return 0;
}
运行结果:
指针运算中的优先级问题
程序:
#include<stdio.h>
int main(void)
{
int data[2] = { 100, 200 };
int moredata[2] = { 300, 400 };
int* p1, * p2, * p3;
p1 = p2 = data;
p3 = moredata;
printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++, *++p2, (*p3)++);
printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
return 0;
}
运行结果:
第二行中,*p1++, p1先和++结合,但它是后置++,所以解引用符号提出了当前p1中的值,在这个表达式运行结束后p1再++,指向了后一个元素。
指针操作
- 赋值:可以把地址赋给指针。例如数组名,带地址运算符(&)的变量名,另一个指针。
- 解引用:*运算符给出指针指向地址上存储的值
- 取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
- 指针与整数相加:整数会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。例如:指针ptr + 4与&ptr[4]等价。
- 递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
- 指针减去一个整数:指针必须是第一个运算对象,整数是第二个运算对象。该数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。
- 递减指针:除了递增指针还可以递减
- 指针求差:计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算得出两元素之间的距离。插值的单位与数组类型的单位相同。假如:两个指针指向一个整型数组,ptr1 - ptr2得2,意思是这两个指针所指向的两个元素相隔两个int。
- 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
保护数组中的数据
对形参使用const
如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形参时应使用关键字const。
像这样:
*int sum(const int ar, int n);
这里使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
const的其他内容
例如:
#define MONTH 12
...
const int days[MONTH] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
1.如果程序尝试改变数组元素的值,就会报错
days[2] = 29; //编译错误
2.若有:
int days[MONTH] = {};
const int* ptr1 = days;
则有:
*ptr1 = 30; //不允许,指针被const限定
ptr1[2] = 30; //不允许
days[2] = 30; //允许,这个数组没有被限定
//可以让指针指向别处:
ptr1++; //允许
3,关于指针赋值和const 需要注意的规则:
把const数据或非const数据的地址初始化为指向const的指针或为其赋值时合法的。
示例:
int ar[5] = { 1,2,3,4,5 };
const int br[5] = { 1,2,3,4,5 };
const int* ptr1 = ar; //合法
ptr1 = br; //合法
ptr1 = &ar[2]; //合法
但是,只能把非const数据的地址赋给普通指针:
int* ptr2 = ar; //可以
ptr2 = br; //无效
ptr2 = &ar[2]; //可以
4,可以声明并初始化一个不能指向别处的指针,例如:
int ar[5] = { 1,2,3,4,5 };
int* const ptr = ar; //ptr指向数组的开始
ptr = &ar[2]; //不允许,该指针不能指向别处
*ptr = 10; //可以,可以更改ar[0]的值
可以用这种指针修改它所指向的值,但是只能指向初始化时设置的地址。
5,还可以在创建指针时使用两次const,该指针既不能更改它所指向的地址,也不能修改指向地址上的值:
int ar[5] = { 1,2,3,4,5 };
const int* const ptr = ar;
ptr = &ar[3]; //不允许
*ptr = 10; //不允许
指针和二维数组
假设有以下声明:
int arr[2][4];
数组名arr是该数组首元素地址。arr的首元素是一个内含四个int值的数组,所以arr是这个内涵四个int值得数组的地址。
- 所以arr的值和&arr[0]的值相同。而arr[0]本身是一个数组,所以arr[0]的值和它首元素的地址(即&arr[0][0]的值)相同。
- 解引用一个指针:*arr代表数组首元素arr[0]的值,但arr[0]表示arr[0][0]的地址; *arr[0]表示arr[0][0]上的值;*arr与&arr[0][0]等价。
- 地址的地址或指针的指针就是双重间接。
指针表示法示例:
int arr[2][4];
arr; //二维数组首元素的地址
arr + 1; //二维数组第二个元素的地址
*(arr + 1); //二维数组第二个元素的首元素的地址
*(arr + 1) + 1; //二维数组第二个元素的第二个元素的地址
*(*(arr + 1) + 1); //二维数组第二个一维数组的第二个int类型的值,即arr[1][1];
结束
还有更多的操作需要大家在今后的学习中不断发现,谢谢。