C 数组 全面剖析
数组名
数组名的值是一个指针常量,也就是数组第一个元素的地址.它的类型取决于数组元素的类型.
只有在两种场合下,数组名并不是用指针常量来表示-------
①sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组.sizeof返回整个数组的长度.
②&数组名,取出的是数组的地址.&数组名,数组名表示整个数组.取一个数组名的地址所产生的是一个指向数组的指针.
//代码理解
int a //a是一个整型
int b[10] //b是一个整型数组,长度为10 数组名b 为数组首元素 int 类型的地址, 即一个指向整型的指针 int *p
int c[3][6]//c是一个二维数组,3行6列 数组名c 为数组首元素 int [6] 类型的地址, 即一个指向 数组的指针 int (*p)[6]
//关于常量的理解
p=&arr[0] == p=arr
arr[5] == *(arr+5) == *(p+5)
//注意 在++操作符中 p++可以实现, 但是arr++无法实现,因为数组名arr是一个指针型常量
多维数组在内存中也是连续存储的!所以多维数组传参实际上也可以用 一级指针来接收.但是绝不可以用二维指针来接收.详细后面会进行解释.
多维数组的理解: 实现方框表示第一维的3个元素,虚线用于划分第二维的6个元素,
一维数组名的值是一个指针常量,它的类型是"指向元素类型的指针",它指向数组的第1个元素.多维数组也差不多简单.唯一的区别是多维数组**第1维元素实际上是另外一个数组 **
如 int c [3] [6] 它可以看作是一个一维数组, 包含3个元素,只是每个元素恰好包含6个整型元素的数组.
数组名 c 的值指向它第1个元素的地址,即一个指向包含 10个整型元素的数组的指针 即 c [6]
在进行数组名指针运算的时候 ,指针会跳过 首元素的大小即
//代码示例
int mat[3][6]; //定义一个二维数组
mat[1][5] //访问 二维数组中 第一行第五列的 元素
*(mat+1) // 一个指向数组第二行 的数组的指针 等价于 mat[1]
*(mat+1)+5 //它是一个指针 .指向第1行第6个元素 即 &mat[1][5]
*(*(mat+1)+5) // 完全等价于 a[1][5]
下标
除优先级外,下标引用和间接访问完全相同,在使用下标的地方,完全可以使用对等的指针来代替.例如下面2个表达式是等同的
array[i];
*(array+i);
//甚至下面的表达式也是合法的,但并不建议这么写,因为可读性实在太差
2[array];
*(2+(arry));//对等的转换
*(array+2);//对等的转换
指针的效率有时会比下标更有效率,但效率的提升是微乎其微的,这里就不展开细说,感兴趣的大佬们可以翻阅<<C和指针>>这本书,本篇绝大部分内容也是从书中汲取的.
指针数组
除类型之外,指针变量和其他变量很相似.正如可以创建整型数组一样,也可以声明指针数组.例如
int *api[10];
为了弄清这个复杂的声明,我们假定它是一个表达式,并对他进行求值
下标引用的优先级高于间接访问,所以这个表达式中,先执行下标引用.因此 api 是某种类型的数组(顺便说一下,它包含元素个数为10个).在取得一个数组元素后随即执行的是间接访问操作符.这个表达式不再有其他操作符,所以它的结果是一个整型值.
那么 api 到底是什么东西呢?对数组的某个元素执行间接访问操作后,我们得到一个整型值,所以api 肯定是一个数组,它的元素类型是指向整型的指针.
//代码示例
char const *keyword[]={
"good",
"morning",
"afterno",
"evening",
NULL
};
int sz=sizeof(keyword)/sizeof(keyword[0]) //计算数组元素中的个数,
//sz=sizeof(keyword) 计算整个数组所占用的字节 ,sizeof(keyword[0])计算数组每个元素所占用的字节
//特别注意 keyword[0] 的类型 char* ,即字符串常量,在32位下默认占4个字节
//比较代码
char const keyword[][10]={
"good",
"morning",
"afternoom",
"evening",
NULL
};
//虽然数组元素个数相同,但是如果这样定义,
//但是sizeof(keyword[0])计算数组每个元素所占用的字节 便是数组第二维度, 即char [10] 占10个字节
//这样便产生了空间的浪费
所以,人们往往更喜欢选择指针数组方案.
数组传参
一维数组传参
通过学习,我们现在应该已经清楚的知道数组的值就是一个指向数组第一个元素的地址,所以很容易明白此时传递给函数的是一份该指针的拷贝.
所以数组形参既可以声明为数组,也可以声明为指针.注意:这两种声明形式只有当它们作为函数的形参时才是相等的
//代码示例
int arr[10];
void test(int arr[])//
{}
void test(int arr[10])//实际上,由于数组传参,传只是数组的地址,c在函数传参的时候并不会真正关心数组数组的长度,也无法通过参数得知数组的长度
{}
void test(int *arr)//
{}
//这三种声明方式都是可以的
多维数组传参
常规方法
作为函数参数的多维数组名的传递方式和一维数组名相同-----实际传递的是个指向数组第一个元素的指针.
但是两者之间的区别在于,多维数组的的每个元素本身就是另外一个数组,编译器需要知道它的维度,以便函数下标表达式进行求值
总结:当一个多维数组名作为参数传递给一个函数时,它的形参设计只能省略第一维度,只有这样编译器才能计算
切记:万万不可以把数组形参声明成一个二级指针
//代码示例
int arr[3][5];
void test(int (*arr)[5]);
void test(int arr[][5]);
//错误代码
void test(int **arr);//这个例子把arr声明为一个指向整型的指针的指针,它和指向整型数组的指针并不是一回事
特殊方法(压扁数组(“flattening the array”))
虽然这种方法编译器可能会报警告,但是多维数组每个元素的本质便是它的基本数据类型,并且多维数组在内存中也是连续存放的,所以我们可以直接声明成一个指向它基本数据类型的一级指针作为形参用来接收它的地址,这种技巧被称为压扁数组,
如果需要给函数传递一个二维数组,又不想在函数原型中给出第二维的长度,那便可以使用这种方法.
int arr[3][5];
void test(int *arr,int row,int col); //使用一维指针进行接收
*(Mat+(i*col)+j) ===Mat[i][j] //访问数组元素 i*row 跳到指定行 +j跳到指定列
//ps 虽然编译器和书中可能不推荐此方法,但是我认为这种方法只要熟练的掌握指针和数组的关系,使用起来更方便
//代码示例
//有一个数字矩阵,矩阵的每行从左到右是递增的,矩阵从上到下是递增的,请编写程序在这样的矩阵中查找某个数字是否存在。
//要求:时间复杂度小于O(N);
//常规方法
int findnum(int a[][3], int x, int y, int f) //第一个参数的类型需要调整
{
int i = 0, j = x - 1; //从右上角开始遍历
while (j >= 0 && i < y)
{
if (a[i][j] < f) //比我大就向下
{
i++;
}
else if (a[i][j] > f) //比我小就向左
{
j--;
}
else
{
return 1;
}
}
return 0;
}
//特殊方法(压扁数组)
int search(int *Mat ,int Des , int row, int col){
int i = 0, j = (col - 1); //从右上角开始寻找
while (j >= 0 && i < row){
if( *(Mat+(i*col)+j)<Des){ //*(Mat+(i*col)+j) 相等于Mat[i][j]
i++;
}
else if (*(Mat+(i * col) + j) > Des){
j--;
}
else return 1;
}
return 0;
}
//测试函数
int main(){
int mat[4][5] = { { 1, 2, 3,4,5 },
{5,6,7,8,9},
{10,11,12,13,14},
{15,16,17,18,19} };
if (search(mat , 19, 4, 5)) printf("yes\n");
else printf("no\n");
return 0;
}