C语言入门(八)——数组(二)

简介: C语言入门(八)——数组

字符串


之前我一直对字符串避而不谈,不做详细解释,现在已经具备了必要的基础知识,可以深入讨论一下字符串了。字符串可以看做一个数组,它的每个元素都是字符型的,例如字符串"Hello,world!\n"图示如下:

image.png

注意每个字符末尾都有一个字符'\0‘做结束符,这里的\0是ASCll码的八进制表示,也就是ASCII码为0的NUII字符,在C语言中这种字符串也称为以零结尾的字符串。数组元素可以用过数组名加下标的方式访问,而字符串字面值也可以像数组名一样使用,可以加下标访问其中的字符:

char c = "Hello, world.\n"[0];

但是通过下标修改其中的字符是不允许的:

"Hello, world.\n"[0] = 'A';

这行代码会产生编译错误,说字符串字面值是只读的,不允许修改的。字符串字面值还有一点和数组明类似,做右值使用时自动转换成指向首元素的指针,在形参和实参中我们看到printf原型的第一个参数是指针类型,而printf("hello world");其实就是传一个指针参数给printf.前面讲过数组可以像结果提一样初始化,如果是字符数组,也可以用一个字符串字面值来初始化:

char str[10] = "Hello";

相当于:

char str[10] = { 'H', 'e', 'l', 'l', 'o', '\0' };

str的后四个元素没有定,自动初始化为0,即NUII字符。注意,虽然字符串字面值"Hello"是只读的,但用它初始化的数组str却是可读可写的。 数组str中保存了一串字符,以'\0’结尾,也可以叫字符串。在本书中只要是以NUII字符结尾的一串字符都叫字符串,不管是像str这样的数组,还是像"Hello"这样的字符串字面值。

如果用于初始化的字符串字面值比数组还长,比如:

char str[10] = "Hello, world.\n";

则数组str只包含字符串的前10个字符,不包含NUII字符,这种情况编译器会给出警告。如果要用一个字符串字面值准确地初始化一个字符数组,最好的办法是不指定数组长度,让编辑器自己计算:

char str[] = "Hello, world.\n";

字符串字面值的长度包括 Null 字符在内一共 15 个字符,编译器会确定数组 str 的长度为 15 。

有一种情况需要特别注意,如果用于初始化的字符串字面值比数组刚好长出一个 Null 字符的长度,

比如:

char str[14] = "Hello, world.\n";

则数组str不包含NUII字符,并且编译器不会给出警告,说这样规定是为了程序员方便,以前地很多编辑器都是这样实现的,不管它有理没理,C标准既然这么规定了我们也没办法,只能自己小心了。


补充一点,printf函数地格式化字符串中可以用%s表示字符串地占位符。在学字符数组以前,我们用%s没什么意义,因为

printf("string: %s\n", "Hello");

还不如写成

printf("string: Hello\n");

但现在字符串可以保存在一个数组里面,用%s来打印就很有必要:

printf("string: %s\n", str);

printf会从数组str的开头一直打印到NUII字符为止,NUII字符本身是Non-printable字符,不打印。这其实是一个危险的信号:如果数组str中没有NUII字符,那么printf函数就会访问数组越界,后果可能会很诡异;有时候打印出乱码,有时候看起来没错误,有时候引起程序崩溃。

多维数组


就像结构体可以嵌套一样,数组也是可以嵌套的,一个数组的元素可以是另外一个数组,这样就构成了一个多维数组。例如定义并初始化一个二维数组:

int a[3][2] = { 1, 2, 3, 4, 5 };

数组a有三个元素,a[0],a[1],a[2]。每个元素也是一个数组,例如a[0]是一个数组,它有两个元素是a[0][0],a[0][1],这两个元素的类型是int,值分别是1,2.同理,数组a[1]的两个元素是3,4.数组a[2]的两个元素是5,0。如下图所示:

image.png

从概念模型上看,这个二维数组是三行两列的表格,元素的两个下标分别是行号和列号。从物理模型上看,这六个元素在存储器中仍然是连续存储的,就像一维数组一样,相当于把概念模型的表格一行行接起来拼成一串,C语言的这种存储方式称为Row_major方式,而有些编程语言是把概念模型的表格一列一列的接起来拼成一串存储的,称为Column_major方式。


多维数组也可以像嵌套结构体一样用嵌套初始化,例如上面的二维数组也可以这样初始化:

int a[][2] = { { 1, 2 },
               { 3, 4 },
               { 5, } };

注意,除了第一维的长度可以由编译器自动计算而不需要指定,其余各维都必须明确指定长度。利用C99的新特性也可以做Memberwise Initialization,例如:

int a[3][2] = { [0][1] = 9, [2][1] = 8 };

image.png

况也可以做Memberwise Initalization,例如:

struct complex_struct {
 double x, y;
} a[4] = { [0].x = 8.0 };
struct {
 double x, y;
 int count[4];
} s = { .count[2] = 9 };

image.png

如果是多维字符数组,也可以嵌套使用字符串字面值做Initalizer,例如:

#include <stdio.h>
void print_day(int day)
{
 char days[8][10] = { "", "Monday", "Tuesday",
 "Wednesday", "Thursday", 
"Friday",
 "Saturday", "Sunday" };
 if (day < 1 || day > 7)
 printf("Illegal day number!\n");
 printf("%s\n", days[day]);
}
int main(void)
{
 print_day(2);
 return 0;
}

image.png

多维字符数组

image.png

这个程序中定义了一个多维字符数组char days[8][10];为了使1-7刚好映射到days[1]-days[7],我们把days[0]空出来不用,所以第一维的长度是8,为了使最长的字符串"Wednesday"能够保存在一行,末尾还能多出个NULL字符位置,所以第二维长度是10。这个程序和前面switch语句的功能其实是一样的,但是代码简洁太多了。简洁的代码不仅可读性强,而且维护成本也低。像switch语句那样一堆case,printf和break,如果漏写一个break就要出现Bug。这个程序之所以简洁,是因为用数据代替了代码。具体来说,通过下标访问字符串组成的数组可以代替一堆case分支判断,这样就可以把每个case里重复的代码(printf调用)提取出来,从而又一次达到了"提取公因式"的效果。这种方法称为数据驱动的编程,写代码最重要的是选择正确的数据结构来组织信息,设计控制流程和算法尚在其次,只要数据结构选择的正确,其他代码自然而然就变得容易理解和维护了,就像这里的printf自然而然就被提取出来了。


最后,综合本章知识,写一个简单的小游戏--剪刀石头布:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
  char gesture[3][10] = { "scissor","stone","cloth" };
  int man, computer, result, ret;
  srand((unsigned)time(NULL));
  while (1)
  {
    computer = rand() % 3;
    printf("Input your gesture(0-scissor 1-stone 2-cloth):\n");
    ret = scanf("%d", &man);
    if (ret != 1 || man < 0 || man>2)
    {
      printf("Invalid input! Please input 0,1or2\n");
      continue;
    }
    printf("Your gesture:%s\tComputer's gesture:%s\n", gesture[man], gesture[computer]);
    result = (man - computer + 4) % 3 - 1;
    if (result > 0)
    {
      printf("You win!\n");
    }
    else if (result == 0)
    {
      printf("Draw!\n");
    }
    else
    {
      printf("You lose!\n");
    }
  }
  return 0;
}

运行结果:

image.png

0,1,2三个整数分别是剪刀石头布在程序中的内部表示,用户也要求输入0,1,2。然后和计算机随机生存的0,1,2比胜负。这个程序的主体是一个死循环,需要按Ctrl-c退出。以往我们写的程序只能打印输出,在这个程序中我们第一次碰到处理用户输入的情况。我们简单介绍一下scanf函数的用法,到格式化I/O函数中再详细解释。scanf("%d",&man)这个调用的功能是等待用户输入一个整数并回车,这个整数会被scanf函数保存在man这个整型变量里。如果用户输入合法(输入的确实是数字而不是别的字符),而scanf函数返回1.表示成功读入一个数据。但即使用户输入的是整数,我们还需要进一步检查是不是在0-2的范围内,写程序时对用户输入要格外小心,用户有可能输入任何数据,他才不管游戏规则是什么.


和print类似,scanf也可以用%c,%f,%s等转换说明。如果在传给scanf的第一个参数中用%d,%f或%c表示读入一个整数,浮点数或字符,则第二个参数的形式应该是&运算符加相应类型的变量名,表示读进来的数保存到这个变量中,&运算符的作用是得到一个指针类型,到后面指针的基本概念再详细解释;如果在第一个参数中用%s读入一个字符串,则第二个参数应该是数组名,数组名前面不加&,因为数组类型做右值自动转换成指针类型,在端点章节中有scanf读入字符串的例子。

思考:(man-computer+4)%3-1这个神奇的表达式是如何比较出0,1,2这三个数字在"剪刀石头布"意义上的大小?

胜 负 平 胜 负

man-computer -2 -1 0 1 2

man-computer+4 2 3 4 5 6

(man-computer+4)%3 2 0 1 2 0

(man-computer+4)%3-1 1 -1 0 1 -1

刀石头布相生相克,形成一个环,凡是具有环的特性的数学模型都可以考虑用取模运算,首先确定了man-computer和%3,然后再调整其它常数得到normalized的结果。


相关文章
|
14天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
66 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
14天前
|
传感器 算法 安全
【C语言】两个数组比较详解
比较两个数组在C语言中有多种实现方法,选择合适的方法取决于具体的应用场景和性能要求。从逐元素比较到使用`memcmp`函数,再到指针优化,每种方法都有其优点和适用范围。在嵌入式系统中,考虑性能和资源限制尤为重要。通过合理选择和优化,可以有效提高程序的运行效率和可靠性。
56 6
|
17天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
38 5
|
17天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
21天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
21天前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。
|
25天前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
44 4
|
2月前
|
存储 编译器 C语言
【c语言】数组
本文介绍了数组的基本概念及一维和二维数组的创建、初始化、使用方法及其在内存中的存储形式。一维数组通过下标访问元素,支持初始化和动态输入输出。二维数组则通过行和列的下标访问元素,同样支持初始化和动态输入输出。此外,还简要介绍了C99标准中的变长数组,允许在运行时根据变量创建数组,但不能初始化。
46 6
|
2月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
2月前
|
存储 C语言
C语言:一维数组的不初始化、部分初始化、完全初始化的不同点
C语言中一维数组的初始化有三种情况:不初始化时,数组元素的值是随机的;部分初始化时,未指定的元素会被自动赋值为0;完全初始化时,所有元素都被赋予了初始值。