程序员C语言快速上手——基础篇(四)

2019-06-26 19:19:25 浏览数 (1)

  • 基础语法
    • 简单数组
      • 声明数组
      • 初始化数组
      • 下标访问
      • 计算数组长度
      • 数组使用小结
    • 字符与字符串
      • char 字符
      • 宽字符
      • 字符串 (String)
        • 字符串与普通数组的区别
      • 小拓展:
      • 字符串的常用函数
        • 字符串长度
        • 比较字符串内容
        • 字符串的复制
        • 字符串的拼接

基础语法

简单数组

把具有相同类型的若干个数据按一定顺序组织起来,这些同类数据元素的集合就称为数组。数组元素可以是基本数据类型,也可以是结构体类型。注意,C语言中的数组与其他编程语言的数组或列表有相似性,但本质上又有不同。

声明数组

代码语言:javascript复制
1  // 声明格式:类型 数组变量名[长度]
2  // 声明数组时需指明元素类型和长度(元素个数),且[]中的长度必须为常量
3  int arr[10];

初始化数组

C语言数组在使用前应当初始化,否则数组中的数据是不确定的,由此会造成一些不可预知的问题。

代码语言:javascript复制
 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开始的。

代码语言:javascript复制
1  int a[6] = {12,4,5,6,7,8};
2
3  // 打印数字中的元素。使用: 数组变量[下标]的格式获取元素
4  printf("%d n",a[0]);
5  printf("%d n",a[1]);

在这里插入图片描述 遍历数组

代码语言:javascript复制
 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运算符,该运算符可以获取类型或变量的内存大小,那么我们可以使用它获得数组总内存大小(即数组占用多少内存),然后用总内存大小除以每一个元素占用的内存大小,就可以获得数组的长度了。由于数组存放的都是同一种类型数据,因此每一个元素占用的内存大小都是固定且相等的。

代码语言:javascript复制
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的声明大小,其他地方不需做任何修改。

数组使用小结

  1. 声明数组时,数组长度必须使用常量指定
  2. 数组应当先初始化再使用
  3. 数组的下标(序号)是从0开始的
  4. 访问数组时必须做边界检查。例如数组a的长度为5,则使用a[5]访问是错误的。a[5]表示的是数组的第6个元素,访问超出数组长度的元素会导致程序异常退出。如果数组长度是n,则当a[i]访问时,应当保证i &lt; n

字符与字符串

如果对于字符、字符编码这些不是非常清楚,或者说是一知半解,建议先看看博主的另一篇科普文章,对与字符与字符编码有了更深入的理解再学习以下内容。

《字符编码的前世今生——一文读懂字符编码》

char 字符

C语言中字符是非常简单的,同时也意味着非常原始!

代码语言:javascript复制
1  // 声明一个字符变量
2  char s = 'a';

在C语言中,字符类型的字面量是单引号括起来的一个字符,注意,字符不是字符串,它只能写一个。且char类型的字符只能表示ASCII表中的字符。实际上,C语言的char就是一个整数,它的范围是0~127

代码语言:javascript复制
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。发现这个规律,我们就能非常简单的实现大小写字母的转换了。

代码语言:javascript复制
1    char s1 = 'c';
2    char s2 = 'G';
3
4    printf("%c n", s1-32); //小写转大写
5    printf("%c n", s2 32); //大写转小写

打印结果

代码语言:javascript复制
1  C 
2  g 

由于char本质上是整数类型,因此可以直接进行算术运算。

宽字符

有些朋友已经发现了,char类型是C语言发展的早期,未考虑地区性字符的产物。简单说就是不能表示中文。直接char s1 = '中';这样写编译会报错的,后续当然是要出台补救措施,宽字符就是补救措施的产物。需要注意,这里宽字符概念仅作为知识拓展,这种解决方案基本被时代所遗弃,仅部分陈旧项目或某些系统内部编码使用。

代码语言:javascript复制
 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

打印结果:

代码语言:javascript复制
1  size is 2 
2  code = 20013

可以看到,这里宽字符的编号是20013,显然一个字节是存不了这么大的整数的,因此宽字符使用两个字节来存字符的编号。这就是为什么被称为宽字符的原因,它比char要宽,使用两个字节16位表示。

在中国大陆区的Window系统中,默认使用的编码表是GBK,并且Windows还使用一种页的概念来表示编码表,而GBK编码表对应的就是page 936,也就是第936页表示GBK编码。如要查看GBK编码表,可将page 936的内容下载下来查看,链接地址 复制该连接地址,选择目标另存为即可下载该txt文件

打印输出宽字符,比直接打印char要麻烦

代码语言:javascript复制
 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

字符串 (String)

所谓字符串,顾名思义,就是将许多单个字符串成一串。既然要把多个字符串起来,当然就需要用到上面说的数组了,存放char类型元素的数组,被称为字符数组。由于C语言没有专门为字符串提供单独的类型,因此只能使用字符数组的方式来表示字符串,这是与其他编程语言很大不同的地方,也是比较繁琐的地方,如果说其他高级语言是自动挡的小轿车,那么C语言就是手动挡的轿车。

声明并初始化字符串

代码语言:javascript复制
 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语言中,虽说字符串是用字符数组来表示的,但是字符串和普通字符数组仍然是不同的,这两者的区别可以简单总结为如下三点

  1. C语言字符串规定,结尾必须包含一个特殊字符'',我们查询一下ASCII表可知,该字符属于控制字符,即无法打印显示出来的字符,它在ASCII表中的编号是0,即表中的第一个字符NUL
  2. 字符串的实际长度(即字符的个数)比字符数组的长度小1。
  3. 声明的同时,数组只能使用花括号初始化,而字符串可以使用双引号括起来的字面量初始化。

现在通过代码验证以上结论

代码语言:javascript复制
 1    // 请注意,以下代码会造成无法预知的错误。不可为!
 2    char s1[3] = {'a','b','c'};
 3    printf(" %s n",s1);
 4
 5    // 手动添加字符串结束符''或整数0。正确
 6    char s2[4] = {'a','b','c',''};
 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";方式声明并初始化字符串是最好的做法,既简洁,也无需操心是否预留了字符串结束符的位置,因为编译器会自动帮我们计算好。最后再强调一次,由于字符串末尾会自动添加结束符,因此字符串的实际长度会比字符数组的长度小1。

声明时不初始化

代码语言:javascript复制
 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);

以上代码是不规范的使用方式。当我们声明字符数组时未初始化就使用了,则编译器不会自动为我们添加结束符,使用微软的VC编译器进行编译后,直接出现了乱码情况,虽然GCC不会出乱码,但也存在不可预知的问题。

代码语言:javascript复制
1abc烫烫烫烫烫烫烫烫烫烫特3臋H?

正确的做法是在未初始化的情况下,使用字符串数组应手动添加结束符

代码语言:javascript复制
1   char str[20];
2
3   str[0]='a';
4   str[1]='b';
5   str[2]='c';
6   str[3]='';
7
8   printf("%sn",str);

当然,除了手动添加结束符号,还可以使用C语言标准库的函数来自动初始化数组。这是一种更常用的做法

代码语言:javascript复制
 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进行填充

在这里插入图片描述 观察以上内存布局图,可知前三个元素分别是十六进制0x610x620x63,转换成十进制就是97、98、99,正好是a、b、c的ASCII码编号,剩余数组元素则默认都是0xcc,而它的十进制则是204,显然已经超出了ASCII码表的范围,Windows默认使用GBK码表,用两个字节表示一个汉字。这时候我们去查询page 936表,可发现两个cc字节合起来就是汉字

还可以查GBK的表,首字节cc的平面表如下,然后根据尾字节去查具体对应的汉字,这里尾字节也是cc

在这里插入图片描述 除了被填充成cc,乱码还与数组越界有关。因为没有字符串结束符,使用printf打印的时候,它并不知道应该在哪儿结束,因为内存都是连成一片的,超过str[20]的20个元素范围,后面还有内存空间,因此乱码 abc烫烫烫烫烫烫烫烫烫烫特3臋H?明显超出了20个char的范围,将其他的内存内容也打印了。这就好比你家住18号,你不仅把18号的门打开了,还把隔壁19号的门也撬开了。

字符串的常用函数

C语言虽然是手动挡的,但也为我们提供了一些不太完美的标准库函数,虽然这些函数多多少少都存在一些坑,但也聊胜于无,总比我们事事躬亲要强许多。要想使用字符串库函数,需要包含string.h头文件。

字符串长度
  • strlen
代码语言:javascript复制
 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 %dn",sizeof(str)/sizeof(char));
 9
10    // 获取字符串的长度
11    int len = strlen(str);
12    printf("string size is %dn",len);
13
14    return 0;
15  }

打印结果:

代码语言:javascript复制
1  array size is 13
2  string size is 12

可见str数组共用13个元素,但只有12个有效字符,最后一个为结束符

比较字符串内容

当我们要判断两个字符串是否相同时,是不能直接使用比较运算符==操作的

代码语言:javascript复制
1    char str1[]= "hello";
2    char str2[]= "hello";
3
4    // ==比较的是两个数组的地址,而不是内容,结果与预期不符
5    printf("%dn",str1 == str2);
  • strcmp
代码语言:javascript复制
 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 == str2n");
11    }else{
12        printf("str1 != str2n");
13    }
14
15    char str3[]= "bruce";
16    char str4[]= "hello";
17
18    if (strcmp(str3,str4) == 0){
19       printf("str1 == str2n");
20    }else{
21        printf("str1 != str2n");
22    }
23
24    return 0;
25  }

打印结果:

代码语言:javascript复制
1  str1 == str2
2  str3 != str4
字符串的复制
  • strncpy 还可使用该函数为字符数组进行初始化
代码语言:javascript复制
 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=%sn",str1);
11
12    // 将str1的内容复制到str2中
13    char str2[50];
14    strncpy(str2,str1,sizeof(str2));
15    printf("str2=%sn",str2);
16    return 0;
17  }

暗坑 strncpy函数存在一个问题,如果被复制的字符串长度太长,超过了目的数组的长度,则将目的数组填充满为止,但是这种情况下就不会添加结束符,导致存在不可预知的问题。

代码语言:javascript复制
 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=%sn",str1);   // 乱码
10
11    char str2[10];
12
13    // 更安全合理的做法,始终为结束符预留一个位置
14    strncpy(str2,"Greetings from C", sizeof(str2)-1);
15    printf("str2=%sn",str2); // 字符串虽被截断,但是有结束符,安全!
16    return 0;
17}
字符串的拼接

在其他语言中,通常只需要简单的使用 号就能拼接字符串,但是C语言就显得繁琐

  • strncat
代码语言:javascript复制
 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=%sn",str1);
12
13    return 0;
14  }

strncpy函数相似,这里的暗坑也是目的地数组的空间不足导致丢失结束符的问题,因此应当预留结束符的位置

代码语言:javascript复制
1  strncat(str1," world!",sizeof(str1)/sizeof(char)-strlen(str1) - 1);

0 人点赞