把具有相同类型的若干个数据按一定顺序组织起来,这些同类数据元素的集合就称为数组。数组元素可以是基本数据类型,也可以是结构体类型。注意,C语言中的数组与其他编程语言的数组或列表有相似性,但本质上又有不同。
1 // 声明格式:类型 数组变量名[长度]
2 // 声明数组时需指明元素类型和长度(元素个数),且[]中的长度必须为常量
3 int arr[10];
C语言数组在使用前应当初始化,否则数组中的数据是不确定的,由此会造成一些不可预知的问题。
1 // 声明的同时,使用字面量初始化。即大括号初始化
2 int arr[10] = {0,1,2,3,4,5,6,7,8,9};
3
4 // 可以只指定部分元素的值,剩下的元素将自动使用0值初始化
5 int arr[10] = {0,1,2,3,4}; //数组元素:0,1,2,3,4,0,0,0,0,0
6
7 // 使用大括号初始化时,中括号中的长度可以省略,编译器将按照实际的个数来确定数组长度
8 int arr[] = {0,1,2,3,4,5,6,7,8,9};
9
10 // 不需要指定每个元素具体值,仅做零值初始化时,可以使用如下写法
11 int arr[10] = {0}; // 数组的每个元素都会被初始化为0
需要注意,使用大括号初始化数组时,大括号中不能为空,至少要写一个值。如int arr[10] = {};
语法错误!
要访问数组中的任意一个元素,都可以通过数组下标访问。因为数组是有顺序的,下标就是元素的序号。但是要注意,数组的第一个元素的序号是0,也就是说下标是从0开始的。
1 int a[6] = {12,4,5,6,7,8};
2
3 // 打印数字中的元素。使用: 数组变量[下标]的格式获取元素
4 printf("%d \n",a[0]);
5 printf("%d \n",a[1]);
在这里插入图片描述 遍历数组
1 int a[6] = {12,4,5,6,7,8};
2
3 // 使用for 循环来访问数组中的每一个元素
4 for(int i=0;i<6;i++){
5 printf("%d \n",a[i]);
6 }
7
8 // 使用for循环修改数组元素
9 for(int i=0;i<6;i++){
10 a[i] = i+2;
11 printf("%d \n",a[i]);
12 }
要注意,在访问数组元素时,[]括号中的下标可以是整型变量。
虽然我们可以明确的知道数组的长度,但有时候我们需要编写更友好更易于维护的代码,例如数组的长度经常修改,则我们需要修改每一处使用数组长度的地方,不易于维护,因此我们需要能动态的计算出数组长度,而不是将长度写死。
前面我们已经多次使用过sizeof
运算符,该运算符可以获取类型或变量的内存大小,那么我们可以使用它获得数组总内存大小(即数组占用多少内存),然后用总内存大小除以每一个元素占用的内存大小,就可以获得数组的长度了。由于数组存放的都是同一种类型数据,因此每一个元素占用的内存大小都是固定且相等的。
1 int a[6] = {12,4,5,6,7,8};
2
3 // 计算数组长度。数组总内存大小/每个元素内存大小
4 int len = sizeof(a)/sizeof(int);
5 for(int i=0;i<len;i++){
6 printf("%d \n",a[i]);
7 }
如上例,当修改数组大小时,只需要修改数组a
的声明大小,其他地方不需做任何修改。
a[5]
访问是错误的。a[5]
表示的是数组的第6个元素,访问超出数组长度的元素会导致程序异常退出。如果数组长度是n
,则当a[i]
访问时,应当保证i < n
如果对于字符、字符编码这些不是非常清楚,或者说是一知半解,建议先看看博主的另一篇科普文章,对与字符与字符编码有了更深入的理解再学习以下内容。
《字符编码的前世今生——一文读懂字符编码》
C语言中字符是非常简单的,同时也意味着非常原始!
1 // 声明一个字符变量
2 char s = 'a';
在C语言中,字符类型的字面量是单引号括起来的一个字符,注意,字符不是字符串,它只能写一个。且char
类型的字符只能表示ASCII
表中的字符。实际上,C语言的char
就是一个整数,它的范围是0~127
1 char s = 'a';
2 char s1 = 97;
3
4 // 可以看到,s和s1打印的结果完全相同
5 printf("%c \n",s);
6 printf("%c \n",s1);
7
8 // 以整数形式打印字符`a`
9 printf("%d \n",s);
char
保存的这个整数也就是每个字符对应的编号,具体的内容我们可以查看ASCII
表
在这里插入图片描述
仔细观察这张表,我们可以发现一个好玩的规律,所有大写字母的编号都比它对应的小写字母小32。例如a
的编号是97,则A
的编号是97-32=65
。发现这个规律,我们就能非常简单的实现大小写字母的转换了。
1 char s1 = 'c';
2 char s2 = 'G';
3
4 printf("%c \n", s1-32); //小写转大写
5 printf("%c \n", s2+32); //大写转小写
打印结果
1 C
2 g
由于char
本质上是整数类型,因此可以直接进行算术运算。
有些朋友已经发现了,char
类型是C语言发展的早期,未考虑地区性字符的产物。简单说就是不能表示中文。直接char s1 = '中';
这样写编译会报错的,后续当然是要出台补救措施,宽字符就是补救措施的产物。需要注意,这里宽字符概念仅作为知识拓展,这种解决方案基本被时代所遗弃,仅部分陈旧项目或某些系统内部编码使用。
1 #include <stdio.h>
2
3 // 使用宽字符,需包含头文件
4 #include <wchar.h>
5
6 int main(){
7 // 声明宽字符,字面量前需加上大写L
8 wchar_t s = L'中';
9
10 printf("size is %d \n",sizeof(wchar_t));
11 printf("code = %d \n",s);
12 }
13
打印结果:
1 size is 2
2 code = 20013
可以看到,这里宽字符中
的编号是20013,显然一个字节是存不了这么大的整数的,因此宽字符使用两个字节来存字符的编号。这就是为什么被称为宽字符的原因,它比char
要宽,使用两个字节16位表示。
在中国大陆区的Window系统中,默认使用的编码表是GBK,并且Windows还使用一种页的概念来表示编码表,而GBK编码表对应的就是page 936
,也就是第936页表示GBK编码。如要查看GBK编码表,可将page 936
的内容下载下来查看,链接地址 复制该连接地址,选择目标另存为即可下载该txt文件
打印输出宽字符,比直接打印char
要麻烦
1 #include <stdio.h>
2 #include <wchar.h>
3
4 // 使用setlocale需包含头文件
5 #include <locale.h>
6
7 int main(){
8 wchar_t s = L'中';
9
10 // 需先设置本地的语言环境,第二个参数传"",表示使用本机默认字符集
11 setlocale(LC_ALL, "");
12
13 // 两种打印宽字符的方式,其中wprintf为宽字符专用函数
14 wprintf(L"%lc \n",s);
15 printf("%lc \n",s);
16 }
17
所谓字符串,顾名思义,就是将许多单个字符串成一串。既然要把多个字符串起来,当然就需要用到上面说的数组了,存放char
类型元素的数组,被称为字符数组。由于C语言没有专门为字符串提供单独的类型,因此只能使用字符数组的方式来表示字符串,这是与其他编程语言很大不同的地方,也是比较繁琐的地方,如果说其他高级语言是自动挡的小轿车,那么C语言就是手动挡的轿车。
声明并初始化字符串
1 //1. 与普通数组相同,用花括号初始化
2 char str1[30] = {'h','e','l','l','o','w','o','r','l','d'};
3 char str2[20] = {"hello world"}; //字符数组的特殊方式
4
5 //2. 字符数组特有的方式。使用英文双引号括起来的字符串字面量初始化
6 char str3[20] = "hello world";
7
8 //3. 省略数组长度
9 char str4[] = {"hello world"};
10
11 //4. 省略数组长度,并使用字符串字面量初始化
12 char str5[] = "hello world";
在C语言中声明字符串,推荐以上第4种方式,它具有简洁且能避免出错的优点。
在C语言中,虽说字符串是用字符数组来表示的,但是字符串和普通字符数组仍然是不同的,这两者的区别可以简单总结为如下三点
'\0'
,我们查询一下ASCII
表可知,该字符属于控制字符,即无法打印显示出来的字符,它在ASCII
表中的编号是0,即表中的第一个字符NUL
。现在通过代码验证以上结论
1 // 请注意,以下代码会造成无法预知的错误。不可为!
2 char s1[3] = {'a','b','c'};
3 printf(" %s \n",s1);
4
5 // 手动添加字符串结束符'\0'或整数0。正确
6 char s2[4] = {'a','b','c','\0'};
7 printf(" %s \n",s2);
8
9 //只要预留结束符的位置,编译器会自动帮我们添加,无需手动
10 char s3[4] = {'a','b','c'};
11 char s4[4] = "abc";
12
13 printf("s3=%s s4=%s \n",s3,s4);
通过以上代码验证,我们就会发现,使用char str5[] = "hello world";
方式声明并初始化字符串是最好的做法,既简洁,也无需操心是否预留了字符串结束符的位置,因为编译器会自动帮我们计算好。最后再强调一次,由于字符串末尾会自动添加\0
结束符,因此字符串的实际长度会比字符数组的长度小1。
声明时不初始化
1 char str[20];
2 /*
3 错误的赋值方式!!!
4 str = "abc";
5 str = {"abc"};
6 */
7
8 // 不规范的使用方式
9 str[0]='a';
10 str[1]='b';
11 str[2]='c';
12
13 printf("%s",str);
以上代码是不规范的使用方式。当我们声明字符数组时未初始化就使用了,则编译器不会自动为我们添加结束符\0
,使用微软的VC编译器进行编译后,直接出现了乱码情况,虽然GCC不会出乱码,但也存在不可预知的问题。
1abc烫烫烫烫烫烫烫烫烫烫特3臋H?
正确的做法是在未初始化的情况下,使用字符串数组应手动添加结束符
1 char str[20];
2
3 str[0]='a';
4 str[1]='b';
5 str[2]='c';
6 str[3]='\0';
7
8 printf("%s\n",str);
当然,除了手动添加结束符号,还可以使用C语言标准库的函数来自动初始化数组。这是一种更常用的做法
1 #include <stdio.h>
2 #include <string.h> // 需要包含string.h头文件
3
4 int main(){
5 char str[20];
6 // 将数组初始化化为指定的值,这里指定0,第三个参数是数组的内存大小
7 memset(str, 0, sizeof(str));
8
9 str[0] = 'a';
10 str[1] = 'b';
11 str[2] = 'c';
12
13 printf("%s", str);
14
15 return 0;
16 }
使用VC编译器,未初始化的数组为什么会出现“烫烫烫”?
因为VC编译器默认会干一件事情,将未初始化的字符数组,使用十六进制数0xcc
进行填充
在这里插入图片描述
观察以上内存布局图,可知前三个元素分别是十六进制0x61
、0x62
、0x63
,转换成十进制就是97、98、99,正好是a、b、c的ASCII码编号,剩余数组元素则默认都是0xcc
,而它的十进制则是204,显然已经超出了ASCII码表的范围,Windows默认使用GBK码表,用两个字节表示一个汉字。这时候我们去查询page 936
表,可发现两个cc
字节合起来就是汉字烫
还可以查GBK的表,首字节cc
的平面表如下,然后根据尾字节去查具体对应的汉字,这里尾字节也是cc
在这里插入图片描述
除了被填充成cc
,乱码还与数组越界有关。因为没有字符串结束符\0
,使用printf打印的时候,它并不知道应该在哪儿结束,因为内存都是连成一片的,超过str[20]
的20个元素范围,后面还有内存空间,因此乱码 abc烫烫烫烫烫烫烫烫烫烫特3臋H?
明显超出了20个char的范围,将其他的内存内容也打印了。这就好比你家住18号,你不仅把18号的门打开了,还把隔壁19号的门也撬开了。
C语言虽然是手动挡的,但也为我们提供了一些不太完美的标准库函数,虽然这些函数多多少少都存在一些坑,但也聊胜于无,总比我们事事躬亲要强许多。要想使用字符串库函数,需要包含string.h
头文件。
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void){
5 char str[]= "hello world!";
6
7 // 动态计算str数组的长度
8 printf("array size is %d\n",sizeof(str)/sizeof(char));
9
10 // 获取字符串的长度
11 int len = strlen(str);
12 printf("string size is %d\n",len);
13
14 return 0;
15 }
打印结果:
1 array size is 13
2 string size is 12
可见str
数组共用13个元素,但只有12个有效字符,最后一个为\0
结束符
当我们要判断两个字符串是否相同时,是不能直接使用比较运算符==
操作的
1 char str1[]= "hello";
2 char str2[]= "hello";
3
4 // ==比较的是两个数组的地址,而不是内容,结果与预期不符
5 printf("%d\n",str1 == str2);
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void){
5 char str1[]= "hello";
6 char str2[]= "hello";
7
8 // strcmp的返回值等于0时,表示两个字符串内容相同,否则不同
9 if (strcmp(str1,str2) == 0){
10 printf("str1 == str2\n");
11 }else{
12 printf("str1 != str2\n");
13 }
14
15 char str3[]= "bruce";
16 char str4[]= "hello";
17
18 if (strcmp(str3,str4) == 0){
19 printf("str1 == str2\n");
20 }else{
21 printf("str1 != str2\n");
22 }
23
24 return 0;
25 }
打印结果:
1 str1 == str2
2 str3 != str4
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void){
5 char str1[100];
6
7 // 将字符串复制到指定的字符数组中,并自动复制结束符。第一个参数就是目的地
8 // 第三个参数需指定复制的长度,这里指定目标数组的大小,表示如果超过这个长度则以这个长度为止
9 strncpy(str1,"Greetings from C",sizeof(str1));
10 printf("str1=%s\n",str1);
11
12 // 将str1的内容复制到str2中
13 char str2[50];
14 strncpy(str2,str1,sizeof(str2));
15 printf("str2=%s\n",str2);
16 return 0;
17 }
暗坑
strncpy
函数存在一个问题,如果被复制的字符串长度太长,超过了目的数组的长度,则将目的数组填充满为止,但是这种情况下就不会添加\0
结束符,导致存在不可预知的问题。
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void){
5 char str1[10];
6
7 // 字符串超过str1的长度,导致str1没有结束符
8 strncpy(str1,"Greetings from C", sizeof(str1));
9 printf("str1=%s\n",str1); // 乱码
10
11 char str2[10];
12
13 // 更安全合理的做法,始终为结束符预留一个位置
14 strncpy(str2,"Greetings from C", sizeof(str2)-1);
15 printf("str2=%s\n",str2); // 字符串虽被截断,但是有结束符,安全!
16 return 0;
17}
在其他语言中,通常只需要简单的使用+
号就能拼接字符串,但是C语言就显得繁琐
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void){
5 char str1[100] = "hello";
6
7 // 将第二个参数的内容追加到第一个参数的后面,相当于将两者拼接
8 // 第三个参数为拷贝的长度,类似strncpy,
9 // 这里计算数组的总长度减去字符串的长度,求得str1剩余空间的长度
10 strncat(str1," world!",sizeof(str1)/sizeof(char)-strlen(str1));
11 printf("str1=%s\n",str1);
12
13 return 0;
14 }
同strncpy
函数相似,这里的暗坑也是目的地数组的空间不足导致丢失结束符的问题,因此应当预留结束符的位置
1 strncat(str1," world!",sizeof(str1)/sizeof(char)-strlen(str1) - 1);