5.1一维数组
5.1.1数组的定义
数组特点:
- 具有相同的数据类型
- 使用过程中需要保存原始数据
C语言为了方便操作这些数据,提供了一种构造数据类型——数组,数组是指一组具有相同数据类型的数据的有序集合。
一维数组的定义格式为
数据类型 数组名[常量表达式];
例如:
int a[10]; //定义一个整型数组,数组名为a,长度为10个元素
声明数组时要遵循以下规则:
- 数组名的命名规则和变量名的规则相同,即遵循标识符命名规则。
- 在定义数组时,需要指定数组中元素的个数,方括号[ ]中的常量表达式用来表示元素的个数,即数组长度。
- 常量表达式中可以包含常量和符号常量,但不能包含变量,也就是说,C语言不允许对数组的大小做动态定义,即数组的大小不依赖于程序运行过程中变量的值。
以下是错误的声明示例(最新的C标准支持,但最好不要这么写):
int n;
scanf("%d",&n); //在程序中临时输入数组的大小
int a[n];
数组声明的其他常见错误如下:
float a[0]; //数组大小为0没有意义
int b(2)(3); //不能使用圆括号
int k = 3,a[k]; //不能用变量说明数组大小
5.2一维数组在内存中的存储
一维数组初始化方法:
(1)在定义数组时对数组元素赋初值,例如:
int a =[10] = {0,1,2,3,4,5,6,7,8,9};
不能写成
int a[10];
a[10] = {0,1,2,3,4,5,6,7,8,9};
(2)可以只给一部分元素赋值,例如:只给前5个元素赋初值,后5个元素值为0
int a =[10] = {0,1,2,3,4};
(3)如果要使一个数组中全部元素的值为0,可以写成:
int a =[10] = {0,0,0,0,0,0,0,0,0,0};
int a[10] = {0}; //更推荐这种写法
(4)在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组的长度(编译器具有自动计算有几个元素的功能)。(考研初试不建议使用,因为改卷老师需要手动数有几个数)
int a[]={1,2,3,4,5};
5.3数组的访问越界
5.3.1访问越界例子
【访问越界演示】
#include <stdio.h> int main() { int arr[5] = {1, 2, 3, 4, 5}; //定义数组 int j = 20; int i = 10; arr[5] = 6; //访问越界 arr[6] = 7; printf("i=%d\n", i); //i=7 i并没有赋值,但是值却变化了 return 0; }
【运行结果】
5.3.2查看内存视图
在内存视图依次输入&arr,&j,&i 来查看整型数组arr,整型变量j,整型变量i的地址。
5.3.3总结
- 访问越界是很危险的,C语言并没有对访问越界进行检查。
- CLion当中的地址都是8字节64位的,考研当中给的大多数为4个字节,即Windows32位控制台应用程序为0x00 00 00 00 到 0xFF FF FF FF,总计为2的32次方,大小为4G。
- 数组注意点:编译器并不检查程序对数组下标的引用是否在数组的合法范围内:
好处:不用浪费时间对有些已知正确的数组下标进行检查
坏处:无法检测出无效的下标引用
一个良好的经验法则:
- 如果下标值是通过那些已知正确的值计算得来则无需检查
- 如果下标值是由用户输入产生,那么使用它们之前必须进行检查,以确保在有效范围内
5.4数组的传递
【数组传递反例】
#include <stdio.h> //一维数组传递 //子函数是把一个常用的功能封装起来 //数组名传递到子函数后弱化成指针(8字节),子函数的形参接收到的是数组的起始地址,因此不能把数组的长度传递给子函数 void print(int a[]) { //数组传递不需要写数字,因为数组长度传递不过来 int i; for (int i = 0; i < sizeof(a) / sizeof(int); i++) { //sizeof(a)/sizeof(int) //数组整个的长度/int的长度 即20个字节/int 4个字节 = 5 printf("%d\n", a[i]); } } //main函数就是主函数 int main() { int a[5] = {1, 2, 3, 4, 5}; print(a); //调用子函数 return 0; }
【运行效果】
【单步调试】
【正确的数组传递例子】
#include <stdio.h> //一维数组传递 //子函数是把一个常用的功能封装起来 //数组名传递到子函数后弱化成指针(8字节),子函数的形参接收到的是数组的起始地址,因此不能把数组的长度传递给子函数 void print(int b[], int length) { //多定义一个变量length作为数组的长度 //数组传递不需要写数字,因为数组长度传递不过来 int i; for (int i = 0; i < length; i++) { printf("%3d", b[i]); } printf("\n");//换行 b[4] = 10; } //main函数就是主函数 int main() { int a[5] = {1, 2, 3, 4, 5}; print(a, 5); //调用子函数 printf("b[4]=%d\n",a[4]); //发现b[4]值发生改变 return 0; }
【运行效果】
总结:
- 在调用函数时,一维数组的长度传递不过去,所以通过length来传递数组中的元素个数。
- 实际数组名中存储的数组的首地址,在调用函数传递时,是将数组的首地址赋给了变量b。
- 在b[ ]的方括号中填写任何数字是没有意义的,因为不能将数组长度传递给子函数。
- 此时在print函数内修改元素b[4]=10可以看到数组b的起始地址和main函数中数组a的起始地址相同,即二者在内存中位于同一位置,当函数执行结束时数组a中元素a[4]就得到了修改。
5.5字符数组与scanf读取字符串
5.5.1字符数组初始化及传递
字符数组的定义方法与前面的5.5.1一维数组类似,例如:
char c[10]; //定义一个长度为10的字符数组c
字符数组的初始化方式
(1)对每个字符单独赋值进行初始化
c[0] = '1';c[1] = ' ';c[2] = 'a'; c[3] = 'm';c[4] = ' ';c[5] = 'h';c[6] = 'a'; //初始化速度慢
(2)对整个数组进行初始化
char c[10] = {'1','a','m','h','a','p','y'}; //初始化速度慢
(3)常用初始化形式:会自动将字符串里的每个字符都放入数组c里面
char c[10] = "Iamhappy"; //编写速度更快
char a[6] = "hello";
【内存视图】
注意:
- C语言规定字符串的结束标志是 '\0' ,而系统会对字符串常量自动加一个 '\0' 。
- 为保证出力方法一致,一般人会人为在字符数组中添加 '\0' ,所以字符数组中存储的字符串长度必须比字符数组少1个字节,例如char c[10]最长存储9个字符,剩余的一个字符用来存储 '\0'
【字符数组初始化及传递的正确例子】
#include <stdio.h> int main() { char c[10] = "Iamhappy"; printf("%s\n", c);//使用%s来输出一个字符串,直接吧字符数组名放在printf后面 //%s内部其实也有一个循环,能将字符数组输出 return 0; }
【运行效果】
【字符数组初始化及传递的错误例子】
#include <stdio.h> int main() { //若字符串长度超过字符数组长度则会输出乱码 char c[5] = "hello"; //正确写法是 char c[6] = "hello"; //输出字符串乱码时,要去查看字符数组中是否存储了结束符'\0' printf("%s\n", c);//使用%s来输出一个字符串,直接吧字符数组名放在printf后面 return 0; }
【运行效果】
【内存视图查看】
【总结】
- 若字符串长度超过字符数组的长度则会输出乱码
- 输出字符串乱码时,要去查看字符数组中是否存储了结束符'\0'
【自定义函数print模拟printf("%s\n",c);操作】
#include <stdio.h> //定义一个print函数模拟printf("%s\n", c);操作 void print(char d[]) { int i = 0; while (d[i]) { //当走到结束符'\0',时循环结束 printf("%c", d[i]); i++; } printf("\n"); //换行 d[0] = 'H';//也可以修改字符数组中某一元素的值 } int main() { //若字符串长度超过字符数组长度则会输出乱码 char c[6] = "hello"; char d[5] = "how"; printf("%s\n", c);//使用%s来输出一个字符串,直接吧字符数组名放在printf后面 print(d); //调用函数print printf("%c\n", d[0]); //d[0]的值已经修改 return 0; }
【运行效果】
5.5.2scanf读取字符串
scanf读取字符串时用%s
【scanf读取字符串例子】
#include <stdio.h> int main() { char c[10]; scanf("%s", c); //字符数组名c中存储了数组的起始地址,因此这里不用写取地址&c //使用scanf读取字符操作时,会自动往数组中填充一个结束符'\0' printf("%s\n", c); return 0; }
【运行效果】
【若数组中已提前填充数据】
#include <stdio.h> int main() { char c[10] = {"1234567"}; //若是提前在c中填充一些数据 scanf("%s", c); //字符数组名c中存储了数组的起始地址,因此这里不用写取地址&c //使用scanf读取字符操作时,会自动往数组中填充一个结束符'\0' printf("%s\n", c); return 0; }
【运行效果】
【单步调试内存视图:scanf语句运行前】
【单步调试内存视图:scanf语句运行后】
【sacnf读取字符串中带有空格】
【若需要读取空格如何操作】
定义两个字符数组,用scanf同时读取两个字符串
#include <stdio.h> int main() { char c[10]; char d[10]; scanf("%s%s", c, d); //字符数组名c中存储了数组的起始地址,因此这里不用写取地址&c //使用scanf读取字符操作时,会自动往数组中填充一个结束符'\0' //scanf使用%s会自动忽略空格和回车(与%d和%f类似) printf("c=%s,d=%s\n", c, d); return 0; }
【运行效果】