目录
1. 数组的概念
假设我要存放100个整型类型的值,那么我就要创建100个整型变量来存放100个整型类型的值,显然是比较麻烦的,为了能够存放多个相同类型的值,就有了数组。
数组是一组相同类型元素的集合,从这个概念中我们就可以发现两个有价值的信息。
- 数组中存放的是1个或多个数据,但是数组元素个数不能为0.
- 数组中存放的是多个数据,类型是相同的。
数组分为一维数组和多维数组,多维数组一般比较多见的是二维数组。
2. 一维数组的创建和初始化
2.1 数组创建
一维数组创建的基本语法:
type arr_name[常量值];
存放在数组的值被称为数组的元素,数组在创建的时候可以指定数组的大小和数组的元素类型。
- type指定的是数组中存放数据的类型,可以是:char、short、int、float等,也可也自定义类型。
- arr_name指的是数组名的名字,这个名字根据实际情况,起的有意义就行。
- [ ]中的常量值是哟过来指定数组的大小的,这个数组大小根据实际的需求指定就行。
比如:我们现在想存储某个班级的20人的数学成绩,那我们就可以创建一个数组。
//一维数组的创建
int math[20];
当然我们也可以根据需要创建其他类型和大小的数组:
//一维数组的创建
char ch[10];
double score[10];
2.2 数组的初始化
数组在创建的时候,我们需要给定一些初始值,这种就被称为初始化。
数组的初始化一般使用大括号,将数据放在大括号中。
int main()
{
//完全初始化
int math[10] = {0,1,2,3,4,5,6,7,8,9};
//不完全初始化
char ch[5] = { 'a'};//第一个元素初始化为1,剩余的默认为0
char ch2[5] = "abc";//也可以拿字符串初始化
//错误的初始化
double score[3] = { 1.0,2.0,3.0,4.0 };//初始化项太多 - err
return 0;
}
2.3 数组的类型
数组也是有类型的,数组算是一种自定义类型,去掉数组名留下的就是数组的类型。
#include <stdio.h>
int main()
{
int a = 100;
char b = 'a';
double d = 0.0;
//上面这些变量的类型就是int,char,double。
int arr[10] = { 0 };
//数组的元素类型是int
//数组的类型就是去掉数组名就是类型,int[10]
int arr2[5];//int[5]
return 0;
}
数组的类型跟数组元素的类型和数组元素的个数是有关系的。
3. 一维数组的使用
知道了一维数组的基本语法,一维数组是可以存放数据的,存放数据的目的就是对数据进行操作,那么我们就应该了解了解一维数组的使用。
3.1 数组下标
C语言规定数组是有下标的,下标是从0开始的,假设数组有n个元素,最后一个元素的下标是n-1,下标就相当于数组元素的编号。
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
在C语言中数组的访问提供了一个操作符[ ],这个操作符叫:下标引用操作符。
有了下标访问操作符,我们就可以轻松的访问到数组的元素了,比如我们访问下标为5的元素,我们就可以使用arr[7],想要访问下标是3的元素,就可以使用arr[3]。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组名后面的10是指定数组元素个数的
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
//在数组创建的时候,数组元素个数也可以不指定
//但前提是需要初始化数组
//当对数组初始化的时候,数组的大小可以省略的
//编译器会根据数组的初始化内容自动计算数组的元素个数
printf("%d\n", arr[5]);//[ ] -- 下标引用操作符
//这里的5是数组元素的下标
printf("%d\n", arr[9]);
return 0;
}
输出结果:
3.2 数组元素的打印
如果想要访问整个数组的内容,只要我们产生数组所有元素的下标就可以了,那我们使用for循环产生0~9的下标,接下来使用下标访问就行了。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
输出结果:
3.3 数组的输入
我们可以打印数组的值,那么肯定也可以根据需求,自己给数组输入想要的数据。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++)
{
scanf("%d", &arr[i]);
}
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
输入输出:
4. 一维数组在内存中的存储
依次打印数组元素的地址:
//一维数组在内存中的存储
#include <stdio.h>
//%d - 整型
//%c - 字符
//%s - 字符串
//%p = 地址
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++)
{
printf("&arr[%d] = %p\n",i, &arr[i]);//& - 取地址操作符
}
return 0;
}
输出:
如果你打印的地址比我打印的地址长的话那你可以把你的vs环境修改为x86环境。如果你的环境是x64的话表示64位环境,所有地址打印出来就是64个比特位,就会比较长,为了观察方便我们就修改为x86,32位的环境,其实64位和32位地址都是一样的。
从输出的结果我们分析,数组随着下标的增长,地址是由小到大变化的,并且我们发现两个相邻的元素之间相差4(因为一个整型是4个字节)。所有我们得出结论:数组在内存中是连续存放的。
5. sizeof计算数组元素个数
在遍历数组的时候,我们经常想知道元素的个数,那C语言中就可以使用sizeof来计算元素的个数。
sizeof是C语言的一个关键字,是可以计算类型或者变量大小的。
sizeof计算的是变量或者类型的长度,单位是字节。
其实sizeof也是可以计算数组的大小。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zd\n", sizeof(arr));
return 0;
}
这里计算的40是因为素组有10个元素,每个元素是4个字节,所有是40个字节。 所以sizeof中放数组名的时候,计算的是数组的大小,40个字节。
数组中所有元素的类型都是相同的,那么只要计算出一个元素所占的字节个数,数组的元素个数就可以计算出来。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zd\n", sizeof(arr)/sizeof(arr[0]));
return 0;
}
这⾥的结果是:10,表⽰数组有10个元素。
以后在代码中需要数组元素个数的地⽅就不⽤固定写死了,使⽤上⾯的计算,不管数组怎么变化,计算出的大小也就随着变化了。
那么我们打印数组的时候for循环的条件我就可以不用写死了。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr)/sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行结果:
现在就算我的数组增加元素,下面的代码也不用改。
6. 二维数组的创建
6.1 二维数组的概念
前面了解的数组是一维数组,数组的元素都是内置类型的,如果我们把一维数组作为 数组的元素,这时候就是二维数数组,二维数组作为数组元素的数组被称为三维数组,二维数组以上的数组统称为多维数组。
6.2 二维数组的创建
定义二维数组的语法:
type arr_name[常量值1][常量值2];
例如:
int arr[3][5];
double data[2][8];
- 3表示数组有三行
- 5表示每一行有5个元素
- int表示数组的每个元素是整型类型
- arr是数组名,可以根据自己的需要指定名字
data数组基本一致
假设我现在要创建一个二维数组,来保存每个班的成绩。
#include <stdio.h>
int main()
{
float score[5][30];
//一共5个班,每个班最多保存30个成绩,而且
//成绩有可能有小数,所有是float类型
return 0;
}
7. 二维数组的初始化
在创建数组的时候,给定一些初始值,被称为初始化,那么二维数组和一维数组一样,也是使用大括号初始化。
7.1 不完全初始化
#include <stdio.h>
int main()
{
int arr1[3][5] = { 1,2 };
int arr2[3][5] = { 0 };
return 0;
}
7.2 完全初始化
#include <stdio.h>
int main()
{
//int arr3[3][5] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
int arr3[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
//这两种初始化都可以
return 0;
}
7.3 按照行初始化
#include <stdio.h>
int main()
{
int arr4[3][5] = { {1,2},{6,7,8},{11,12,13,14} };
return 0;
}
7.4 初始化时省略行,但不能省略列
#include <stdio.h>
int main()
{
int arr5[][5] = { 1,2,3 };
int arr6[][5] = { 1,2,3,4,5,6,7 };
int arr7[][5] = { {1,2}, {3,4}, {5,6} };
return 0;
}
8. 二维数组的使用
8.1 二维数组的下标
当我们掌握了二维数组的创建和初始化,那么也应该掌握二维数组的使用。
其实二维数组访问也是使用下标的形式,二维数组也是有行和列的,只要锁定行和列就能唯一锁定数组中的一个元素。
C语言规定,二维数组的行是从0开始,列也是从0开始。
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
return 0;
}
图中最左侧绿色的数字表示行号,第一行蓝色的数字表示列号,都是从0开始的,比如,第一行第一列,我们就可以找到7。
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
printf("%d\n", arr[1][1]);
return 0;
}
运行结果:
8.2 二维数组的输入和输出
访问整个二维数组的话,其实我们只要能够按照一定的规律产生所有的行和列的数字就行,上面代码中的数组行的范围是0~2,列的范围是0~4,所以我们可以借助循环实现生成所有的下标。
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
int hang = sizeof(arr) / sizeof(arr[0]);//计算行
int lie = sizeof(arr[0]) / sizeof(arr[0][0]);//计算列
//输入
for (int i = 0; i < hang; i++)
{
for (int j = 0; j < lie; j++)
{
scanf("%d", &arr[i][j]);
}
}
//输出
for (int i = 0; i < hang; i++)
{
for (int j = 0; j < lie; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
输入和输出
9. 二维数组在内存中的存储
和一维数组一样,如果想要研究二维数组在内存中的存储方式的,我们也是可以打印出数组所有元素的地址。
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
int hang = sizeof(arr) / sizeof(arr[0]);//计算行
int lie = sizeof(arr[0]) / sizeof(arr[0][0]);//计算列
for (int i = 0; i < hang; i++)
{
for (int j = 0; j < lie; j++)
{
printf("arr[%d][%d] = %p\n",i,j, &arr[i][j]);
}
}
return 0;
}
运行结果:
使用32位环境打印。
从输出的结果来看,每一行内部的元素都是相邻的,地址之间相差4个字节,跨行位置处的两个元素之间也是差4个字节,所以二维数组中的每个元素在内存中也是连续存放的。
二维数组中的每个元素就是一维数组。
前面的都是一行一行打印,其实也可以一列一列的打印。
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15} };
int hang = sizeof(arr) / sizeof(arr[0]);//计算行
int lie = sizeof(arr[0]) / sizeof(arr[0][0]);//计算列
//1 2 3 4 5
//6 7 8 9 10
//11 12 13 14 15
for (int i = 0; i < lie ; i++)
{
for (int j = 0; j < hang; j++)
{
printf("%d ", arr[j][i]);
}
printf("\n");
}
return 0;
}
10. C99中的变长数组
在C99标准之前,C语言在创建数组的时候,数组大小的指定只能使用常量、常量表达式、或者我们初始化数组的时候可以省略大小。
#include <stdio.h>
int main()
{
int arr1[10];
int arr2[2 + 2];
int arr3[] = { 1,2,3 };
return 0;
}
正因为有这样的语法限制,让我们创建数组就不够灵活,有时候数组大了浪费空间,有时候数组小了不够用。
但是C99中给了一个变长数组(variable-length array,简称VLA)的新特性,允许我们使用变量指定数组的大小。
#include <stdio.h>
int main()
{
int n = 10;
int arr[n];
return 0;
}
上⾯示例中,数组 arr 就是变⻓数组,因为它的长度取决于变量 n 的值,编译器没法事先确定,只有运行时才能知道n是多少。
变长数组的根本特性,就是数组长度只有运行时才能确定,所以变长数组不能初始化。它的好处是程序员不必在开发时随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。变长数组的意思时数组的大小是可以使用变量来指定的,在程序运行的时候,根据变量的大小来指定数组的元素个数,而不是说数组的大小是可变的。数组的大小一旦确定就不能在变化了。
但是在VS2019上,虽然支持大部分C99的语法,但是并没有C99中的变长数组,没法测试,下面是在gcc编译器上测试。
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d",&n);
int arr[n];//边长数组不能初始化
for(int i = 0;i < n;i++)
{
scanf("%d",&arr[i]);
}
for(int i = 0;i < n;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
两次测试,第一次输入10,第二次输入5,并正常输出。
11. 数组练习
11.1 多个字符从两端移动,向中间汇聚。
编写代码,演示多个字符从两端移动,向中间汇聚。
#include <stdio.h>
#include <string.h>
#include <Windows.h>
/*
hello wang!!!
#############
h###########!
*/
int main()
{
char ch1[] = "hello wang!!!";
char ch2[] = "#############";
int sz = strlen(ch1);
printf("%s\n", ch2);
for (int i = 0; i <= sz/2; i++)
{
ch2[i] = ch1[i];
ch2[sz - i-1] = ch1[sz - i-1];
Sleep(1000);//睡眠函数 - 睡眠1s
system("cls");//清屏函数
printf("%s\n", ch2);
}
return 0;
}
12.2 二分查找
在一个升序的数组中查找指定的数字n,很容易想到的方法就是遍历数组,但是这种方法的效率比较低。
比如我买了一双鞋,你好奇问我多少钱,我说不超过300元。你还是好奇,你想知道到底多少,我就让你猜,你会怎么猜?你会1,2,3,4....这样猜吗?显然很慢,一般你就会猜中间数字,比如:150,然后看大了还是小了,这就是二分查找,也叫折半查找。
#include <stdio.h>
int main()
{
int k = 9;
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;//这样写为了防止溢出
if (arr[mid] == k)
{
printf("找到了,下标是:%d\n", mid);
break;
}
else if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
}
if (left > right)
printf("没找到!!\n");
return 0;
}
建议在计算中间下标的时候不要使用int mid = (left + right)/2来计算,因为当left是最大值,right是最大值的时候,相加除2就不合适了,会溢出。
#include <stdio.h>
int main()
{
int a = 2147483646;//_CRT_INT_MAX - 1
int b = 2147483646;
int avg = (a + b) / 2;
printf("%d\n", avg);
return 0;
}
两个相同的值相加再除2不应该还是原来的值吗,可这里偏偏就是-2,这就不是平均值,所以这里存在潜在问题。所以我们在求平均值的时候可以用下面的思路。
#include <stdio.h>
int main()
{
int a = 2147483646;//_CRT_INT_MAX - 1
int b = 2147483646;
int avg = a + (b - a) / 2;//小的加上大的和小的差的一半
printf("%d\n", avg);
return 0;
}
那如果a大的话就小的减大的,成负的了,然后除2,然后再给a减去m,也是一样的。
所以求中间下标的时候就可以用上面的思路。
int mid = left + (right - left) / 2;