本文采用了《C Primer Plus》、《C陷阱与缺陷》以及比特科技的教学视频。
对C语言数组、指针和数组的关系进行了详细讲解;并将初学者在使用数组时遇到的问题进行了总结,希望对各位读者有所帮助。
目录
1.一维数组
一维数组的创建
数组就是由数据类型相同的一系列元素组成,创建普通变量时的数据类型,在创建数组中均适用。
一维数组创建的一般格式:元素类型 数组名 [常量表达式];
示例1:
#include<stdio.h>
int main()
{
int arr1[10];//10个int类型元素的数组
char arr2[20];//10个char类型元素的数组
double arr3[30];//10个double类型元素的数组
return 0;
}
这里arr1、arr2、arr3都是数组名,[]中的10、20、30均是常量,很好理解。
示例2:
#include<stdio.h>
#define n 10
int main()
{
int arr1[n];//10个int类型元素的数组
return 0;
}
通过用define定义常量,也可以实现数组的创建。
示例3:
#include<stdio.h>
int main()
{
int n = 10;
char a[n];
return 0;
}
以上方法为C99标准所支持的变长数组(VLA)的概念,在C99标准之前[]中必须赋予一个常量。
值得注意的是:并不是所有编译器都支持变长数组(VS编译器就不支持),并且变长数组存在很多弊端,不建议使用。
数组的创建和使用都是静态的,无论用那种方式创建数组,在使用中不可将其空间大小改变。若是想动态的改变空间存放数据,则需要学到动态规划的知识。
一维数组的初始化
(一)传统方法
数组的初始化是指:在创建数组的同时给数组的内容一些合理初始值(初始化)。
int arr1[5] = {1,2,3,4,5};//完全初始化
int arr2[10] = {1,2,3};//不完全初始化
int arr3[] = {1,2,3,4};//编译器按输入内容自行定义数组大小
- 完全初始化:为数组内的每一元素都赋予其值。
- 不完全初始化:未赋值的元素,编译器自动初始化为0。
- 省略[]中的常量:编译器会自动根据输入的内容来合理分配空间。
需要区分以下两种初始化形式
char arr1[] = "abc";
char arr2[4] = {'a','b','c','\0'};
字符串的末尾默认为‘\0’,以字符串形式初始化不用输入‘\0’;若是以单个字符输入时,需要在结尾单独输入‘\0’,并且‘\0’占一个字节的空间。
如果不进行初始化会怎样?
#include<stdio.h>
int main()
{
char arr1[10];
printf("%s\n", arr1);
}
如上述代码所示,创建了arr1数组,其中包含10个char类型的数据,在创建之初并没有对其进行初始化,打印arr1数组。
这是因为没有对数组进行初始化,数组中存放的都是垃圾信息,并且arr1数组是char类型的,编译器找不到终止符‘\0’,打印时会一直打印,超出数组的大小限制。
(二)指定初始化器(C99)
传统方法中,若是想初始化一个数组中的最后一个元素,必须将该元素以及之前的元素全部初始化。而在C99标准中,支持针对某一个元素进行初始化,而其他未被初始化的元素默认初始化为0。
int arr1[5]={0,0,0,0,1};//将第五个元素初始化为1
int arr2[5] = {[4]=1};//数组下标从0开始,4表示第五个元素
arr1和arr2的初始化结果是一样的。
示例1:
int main()
{
int arr1[10] = {[2]=1,[4]=2,3,4,[2]=10};
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
通过以上示例可以明白指定初始化器的两个特性:
- 如果初始化器后面有更多的值:[4]=2,3,4,那么这些值将赋予指定初始化元素后面的元素。arr[4]被初始化为2后,紧接着arr[5]被初始化为3,arr[6]被初始化为4。
- 如果再次初始化指定元素,那么最后一次初始化将会取代前一次初始化。例如[2]=1,最开始将1的值赋给arr1[2],但是arr1[2]又被后来的[2]=10初始化为10。
不指定数组大小会怎样?
int main()
{
int arr1[] = {[2]=1,[4]=2,3,4};
return 0;
}
编译器会根据指定初始化的内容合理分配空间,初始化的最后一个元素为arr[6],所以编译器为其分配7个元素的空间。
一维数组的使用
声明完数组后,借助数组的下标可对其进行赋值。
#define num 5
int main()
{
int arr1[num] = {0};
int i = 0;
for (i = 0; i < num; i++)
{
arr1[i] = i;
}
return 0;
}
C语言中不允许把数组作为一个值赋给另一个数组,并且除了初始化外,不允许以花括号的形式进行赋值,以下为错误示例:
#define num 5
int main()
{
int arr1[num] = { 1,2,3,4,5 };
int arr2[num] = { 0 };
arr2 = arr1;//不允许
arr1[num] = arr2[num];//数组下标越界
arr2[num] = { 1,2,3,4,5 };//无效
return 0;
}
- arr1[num] = arr2[num]理解为:将arr2中下标为num的元素,将其值赋给arr1中下标为num的元素,但是arr1和arr2中的元素下标最大为num-1,访问越界。
- 若将其改成arr1[num-1] = arr2[num-1],仅为下标是[num-1]元素的赋值操作,并不是数组赋值操作。
数组边界
在使用数组时,需要防止数组下标超出边界,必须确保数组下标是有效值。
例如:
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
putchar('\n');
return 0;
}
arr1数组中有效下标为0~4,访问越界时,打印的就是无意义的数。但是编译器并不会报错,可以正常运行。这是因为C语言信任程序员的原则,不检查边界。为了C语言可以运行的更快,编译器没有必要捕获所有的下标错误。为了防止访问出界,在声明数组时用符号常量来表示数组的大小。
指定数组的大小
在前文创建数组时,举了三种例子来指定数组的大小,这里更加详细的介绍指定数组大小的各种方法:
#define size 5
int main()
{
int a1[size];//可以,整数符号常量
int a2[5];//可以,整数字面常量
int a3[5 * 2 - 1];//可以,整数常量表达式
int a4[0];//不可以,数组大小必须大于0
int a5[-1];//不可以,数组大小必须大于0
int a6[2.5];//不可以,数组大小必须是整数
int a7[(int)2.5];//可以,强制类型转换成整型
int a8[sizeof(int)];可以,sizeof表达式被视为整型常量
int n = 5;
int a9[n]; //变长数组VLA,C99之前不允许
return 0;
}
值得注意的是,const修饰的常变量,虽然具有常量的属性,但本质还是变量,C99标准前不允许用const修饰的常变量指定数组的大小。
一维数组在内存中的存储
创建一个整型数组,打印其地址(以X86环境为例)
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
结果如下:
通过观察可以得知:数组元素在存放时是连续存放的,随着下标的增长由低地址指向高地址,本例中arr为整型数组,所以每一个元素之间间隔4个字节。
2.二维数组
二维数组的创建
一维数组创建的一般格式:元素类型 数组名 [常量表达式1][常量表达式2];表达式1为数组的行,表达式2为数组的列。
int arr1[3][3];
char arr1[3][3];
double arr1[3][3];
二维数组的初始化
int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
int arr[3][3] = { 1,2,3,4,5,6,7,8,9 };//只要对应的数值正确,也可以省略内部的小括号
int arr[][3] = { 1,2,3,4,5,6,7,8,9 };//初始化时,行可以省略,列不能省略
例1:
下述代码初始化数组的结果是什么?
int arr4[][3] = { {1,2,3},{4,5},{7} };
结果:
与一维数组一样,未初始化的元素默认为0。
例2:
下述代码初始化数组的结果是什么?
int arr5[][3] = { {1,2,3},{(4,5),5},{7}};
结果:
(4,5)为逗号表达式,赋值时取后者5。
二维数组的使用
与一维数组一样,二维数组的使用也是用for循环遍历,区别在于二维数组需要用到两个for循环嵌套使用。
打印一个二维数组:
int arr[][3] = {1,2,3,4,5,6,7,8,9};
int i = 0, j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
二维数组在内存中的存储
打印二维数组中每一位的地址
#include <stdio.h>
int main()
{
int arr[3][4];
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
}
}
return 0;
}
结果:
通过观察发现,二维数组的存储也是连续的,先存储第一行的元素,然后再存储第二行的,以此类推。
3.指针和数组
指针提供一种以符号形式使用地址的方法,使用指针可以更加有效率的存储数据,而数组则是变相的使用指针?
数组名是什么?
对于一维数组来说,数组名就是首元素的地址。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%d\n", *arr);
return 0;
}
打印arr的地址和arr首元素arr[0]的地址,发现二者是相同的。通过对arr地址的解引用*,可以找到该数组的首元素1。
对于二维数组来说,数组名就是第一行元素的地址。
#include <stdio.h>
int main()
{
int arr[][3] = { 1,2,3,4,5,6,7,8,9 };
printf("%p\n", arr);
printf("%p\n", &arr[0][0]);
printf("%d\n", (*arr)[0]);
return 0;
}
结果:
第一行元素的地址又等于其第一行中首元素的地址,打印arr的地址和第一行第一个元素的地址,发现其二者相同。(*arr)[0]表示为:通过对arr进行解引用找到第一行,再从第一行中找到下标为0的元素,即1。
通过地址对数组进行操作
打印一维数组的每一位
#include <stdio.h>
#define n 5
int main()
{
int arr[n] = { 1,2,3,4,5};
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
打印二维数组的每一位
#include <stdio.h>
#define n 3
int main()
{
int arr[][n] = { 1,2,3,4,5,6,7,8,9};
int i = 0, j = 0;
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
printf("%d ", *( * (arr + i)+j));
}
putchar('\n');
}
return 0;
}
数组参数、指针参数
设计一个函数,返回值是数组内所有整数的和。
一维数组传参的几种形式:
数组形式:
#include <stdio.h>
int text1(int arr[],int sz)//int arr[3]亦可,arr[]里有没有数都一样
{
int i = 0;
int sum = 0;
for (i = 0; i < sz; i++)
{
sum += arr[i];
}
return sum;
}
int main()
{
int arr[3] = {1,2,3};
int sz = sizeof(arr) / sizeof(arr[0]);
return 0;
}
因为数组名是首元素的地址,传入到text函数中,看似arr还是数组,其实只是首元素的地址,还需要将数组的大小sz也传入text函数中。
指针形式:
int text(int* arr, int sz)
{
int i = 0;
int sum = 0;
for (i = 0; i < sz; i++)
{
sum += *(arr+i);
}
return sum;
}
二维数组传参的几种形式:
数组形式:
#include <stdio.h>
#define ROW 3
#define COL 3
int text(int arr[][ROW], int row,int col)//int arr[][ROW]接收时,可以省略行,不能省略列
{
int i = 0, j = 0;
int sum = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
sum += arr[i][j];
}
}
return sum;
}
int main()
{
int arr[ROW][COL] = {1,2,3,4,5,6,7,8,9};
printf("%d\n", text(arr, ROW,COL));
return 0;
}
指针形式:
#include <stdio.h>
#define ROW 3
#define COL 3
int text(int (*arr)[ROW], int row, int col)
{
int i = 0, j = 0;
int sum = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
sum += *(*(arr+i)+j);
}
}
return sum;
}
int main()
{
int arr[ROW][COL] = { 1,2,3,4,5,6,7,8,9 };
printf("%d\n", text(arr, ROW, COL));
return 0;
}
数组形式和指针形式的本质都是地址的使用,二者在实质上没有区别。