内容简单介绍
1、课程大纲
2、第二部分第四课: 字符串
3、第二部分第五课预告: 预处理
课程大纲
我们的课程分为四大部分,每个部分结束后都会有练习题,并会发布答案。还会带大家用C语言编写三个游戏。
C语言编程基础知识
-
什么是编程?
-
工欲善其事。必先利其器
-
你的第一个程序
-
变量的世界
-
运算那点事
-
条件表达式
-
循环语句
-
实战:第一个C语言小游戏
-
函数
-
练习题
-
习作:完好第一个C语言小游戏
-
模块化编程
-
进击的指针,C语言王牌
-
数组
-
字符串
-
预处理
-
创建你自己的变量类型
-
文件读写
-
动态分配
-
实战:“悬挂小人”游戏
-
安全的文本输入
-
练习题
-
习作:用自己的语言解释指针
-
安装SDL
-
创建窗体和画布
-
显示图像
-
事件处理
-
实战:“超级玛丽推箱子”游戏
-
掌握时间的使用
-
用SDL_ttf编辑文字
-
用FMOD控制声音
-
实战:可视化的声音谱线
-
练习题
-
链表
-
堆,栈和队列
-
哈希表
-
练习题
第二部分第四课:字符串
好了。这课我不说废话。直接进入主题了。(但又好像不是我的风格...)
《字符串》。这是一个编程的术语。用来描写叙述“一段文字”,非常easy。
一个字符串,就是我们能够在内存中以变量的形式储存的“一段文字”。比方,username是一个字符串,“程序猿联盟”是一个字符串。可是我们之前的课说过,呆萌的电脑兄仅仅认得数字,“众里寻他千百度,电脑就爱穿秋裤”(不是“穿秋裤”,是“认得数”。说好不废话的,小编你这么顽皮你妈妈知道么...)。
所以说电脑实际是不认得字母的,可是“古灵精怪”的计算机先驱们是怎么设计使电脑能够“识别”字母呢?
接下来我们会看到。他们事实上还是非常聪明的。
字符类型
在这个小部分,我们把注意力先集中在字符类型上。
假设你还记得,之前的课程中我们说过: 有符号字符类型(char)是用来储存范围从-128到127的数的; unsigned char(无符号字符类型)用来储存范围从0到255的数.
注意: 尽管char类型能够用来储存数值。可是在C语言中却鲜少用char来储存一个数. 通常,即使我们要表示的数比較小,我们也会用int类型来储存,当然了,用int来储存比用char来储存在内存上更占空间,可是今天的电脑基本是不缺那点内存的,“有内存任性嘛”.
char类型一般用来储存一个字符,注意,是 一个 字符.
前面的课程也提到了。由于电脑仅仅认得数字,所以计算机先驱们建立了一个表格(比較常见的有ASCII表, 更完整一些的有Unicode表)。用来约定字符和数字之间的转换关系。比如字母A(大写)相应的数字是65.
C语言能够非常easy地转换字符和其相应的数值. 为了获取到某个字符相应的数值(电脑底层事实上都是数值),仅仅须要把该字符用单引號括起来,像这样:
'A'
在编译的时候,'A'会被替换成实际的数值: 65
我们来測试一下:
int main(int argc, char *argv[])
{
char letter = 'A';
printf("%d
", letter);
return 0;
}
程序输出:
65
所以,我们能够确信大写字母A的相应数值是65. 类似地。大写字母B相应66, C相应67。 以此类推. 假设我们測试小写字母,那你会看到a和A的数值是不一样的,小写字母a的数值是97, 实际上,在大写字母和小写字母之间有一个非常easy的转换公式,就是 “小写字母的数值 = 大写字母的数值 + 32”. 所以电脑是区分大写和小写的. 看似呆萌的电脑兄还是能够的么.
大部分所谓“基础”的字符都被编码成0到127之间的数值了. 在ASCII表(发音【aski】)的官网 www.asciitable.com 上,我们能够看到大部分经常使用的字符的相应数值. 当然这个表我们也能够在其它站点上找到,比方维基百科,百度百科。等等.
显示字符
要显示一个字符,最经常使用的还是printf函数啦。这个函数真的非常强大,我们会经经常使用到.
上面的样例中。我们用%d格式,所以显示的是字符相应的数值(%d是整型),假设要显示字符实际的样子。须要用到%c格式(c是英语character[字符]的首字母):
int main(int argc, char *argv[])
{
char letter = 'A';
printf("%c
", letter);
return 0;
}
程序输出:
A
哇,我们知道怎样输出一个字符了,可喜可贺。(小编你也该吃药了...)
当然我们也能够用常见的scanf函数来请求用户输入一个字符,而后用printf函数打印:
int main(int argc, char *argv[])
{
char letter = 0;
scanf("%c", &letter);
printf("%c
", letter);
return 0;
}
假设我输入C,那我将看到:
C
C
第一个字母C是我输入给scanf函数的,第二个C是printf函数打印的.
以上就是对于字符类型char我们大致须要知道的,请牢记下面几点:
-
有符号字符类型(signed char)是用来储存范围从-128到127的数的; unsigned char(无符号字符类型)用来储存范围从0到255的数. (C语言中,假设你没写signed或unsignedkeyword,默认情况下是表示有符号signed)
-
计算机先驱们给电脑规定了一个表,电脑能够遵照里面的转换原则来转换字符和数值。一般这个表是ASCII表
-
char类型仅仅能储存一个字符
-
'A'在编译时会被替换成实际的数值:65 。
因此,我们使用单引號来获得一个字符的值
字符串事实上就是字符的数组
这一部分的内容,就如这个小标题所言.
其实: 一个字符串就是一个“字符的数组”,仅此而已.
到这里。你是否对字符串有了更直观的理解呢?
假设我们创建一个字符数组:
char string[5];
然后我们在数组的第一个成员上储存‘H’,就是string[0] = 'H'。第二个成员上储存'E'(string[1] = 'H'),第三个成员上储存'L'(string[2] = 'L'),第四个成员储存'L' (string[3] = 'L'),第五个成员储存'O'(string[4] = 'O'),那么我们就是构造了一个字符串啦:
下图对于字符串在内存中是怎么存储的。能够给出一个比較直观的印象(注意: 实际的情况比这个图演示的要稍微复杂一些,待会会解释):
上图中,我们能够看到一个数组,拥有5个成员,在内存上连续存放。构成一个字符串 "HELLO". 对于每个储存在内存地址上的字符。我们用了单引號把它括起来。是为了突出实际上储存的是数值。而不是字符. 在内存上。储存的就是此字符相应的数值.
实际上,一个字符串可不是就这样结束了,上面的图示事实上不完整. 一个字符串必须在最后包括一个特殊的字符。称为“字符串结束符”,它是' '。相应的数值是0.
“为什么要在字符串结尾加这么一个多余的字符呢?”
问得好。
那是为了让电脑知道一个字符串到哪里结束. ' '用于告诉电脑:“停止。字符串到此结束了,不要再读取了,先退下吧”。
因此,为了在内存中存储字符串"HELLO"(5个字符),用5个成员的字符数组是不够的。须要6个!
因此每次创建字符串时,须要记得在字符数组的结尾留一个字符给' '。
忘记字符串结束符是C语言中一个常见的错误。
因此,以下才是正确展示我们的字符串"HELLO"在内存中实际存放情况的示意图:
如上图所见,这个字符串包括6个字符,而不是5个。
也多亏了这个字符串结束符' ',我们就无需记得字符串的长度了。由于它会告诉电脑字符串在哪里结束。因此,我们就能够将我们的字符数组作为參数传递给函数,而不须要传递字符数组的大小了。
这个优点仅仅针对字符数组,你能够在传递给函数时将其写为 char *或者char[]类型。对于其它类型的数组,我们总是要在某处记录下它的长度。
字符串的创建和初始化
假设我们想要用“Hello”来初始化字符数组string。我们能够用下面的方式来实现,当然有点没效率:
char string[6]; // 六个char构成的数组,为了储存: H-e-l-l-o +
string[0] = 'H';
string[1] = 'e';
string[2] = 'l';
string[3] = 'l';
string[4] = 'o';
string[5] = ' ';
尽管是笨办法,但至少行得通。
我们用printf函数来測试一下。
要使printf函数能显示字符串格式。我们须要用到%s这个符号(s就是英语string的首字母):
#include <stdio.h>
int main(int argc, char *argv[])
{
char string[6]; // 六个char构成的数组。为了储存: H-e-l-l-o +
string[0] = 'H';
string[1] = 'e';
string[2] = 'l';
string[3] = 'l';
string[4] = 'o';
string[5] = ' ';
// 显示字符串内容
printf("%s ", string);
return 0;
}
程序输出:
Hello
假设我们的字符串内容多起来,上面的方法就更显拙劣了。事实上啊,初始化字符串还有更简单的一种方式(小编你好奸诈,不早讲,害我写代码这么辛苦...):
int main(int argc, char *argv[])
{
char string[] = "Hello"; // 字符数组的长度自己主动计算了
printf("%s ", string);
return 0;
}
以上程序的第一行,我们写了一个char []类型的变量,事实上也能够写成 char * 相同是能够执行的:
char *string = "Hello";
这样的方法就比之前一个字符一个字符初始化的方法高大上多了。由于仅仅须要在双引號里输入你想要创建的字符串,C语言的编译器就非常智能地为你计算好了字符串的大小。就是说它计算你输入的字符的数目,然后再加上一个' '的长度(是1)。它就把你的字符串里的字符一个接一个写到内存某个地方,在最后加上' '这个字符串结束符。就像我们刚才用第一种方式自己一步步做的。
可是简便也有缺陷,我们会发现,对于字符数组来说。这样的方法仅仅能用于初始化。你在之后的程序中就不能再用这样的方式来给整个数组赋值了,比方你不能这样:
char string[] = "Hello";
string = "nihao"; --> 出错!
仅仅能一个字符一个字符地改。比如:
string[0] = 'j'; --> 能够!
可是问题又来了。对于用char *来声明的字符串,我们能够在之后整个又一次赋值,可是不能够单独改动某个字符:
char *string = "Hello";
string = "nihao"; --> 能够!
这样是能够的,可是假设改动当中的一个字符,就不能够:
string[1] = 'a'; --> 出错!
非常有意思吧。
大家能够亲自己主动手试试。所以这里就引出了一个话题:
指针和数组根本就是两码事!
为什么会出现上述的情况呢?
那是由于:
1.
char string[] = "Hello";
这样声明的是一个字符数组,里面的字符串是储存在内存的变量区。是在栈上。所以能够改动每一个字符的内容,可是不能够单单通过数组名总体改动:
string = "nihao"; --> 出错。
仅仅能一个个单独改:
string[0] = 'a'; --> 能够。
由于之前的课程里说过,string这个数组的名字表示的是数组首元素的首地址。
2.
char *string = "Hello";
这样声明的是一个指针,string是指针的名字。指针变量在32 位系统下。永远占4 个byte(字节)。其值为某一个内存的地址。所以string里面仅仅是存放了一个地址,这个地址上存放的字符串是常量字符串。存在内存的静态区。不能够更改。
和上面的字符数组情况不一样。上面的字符数组是本身存放了那一整个字符串。
string[0] = 'a'; --> 出错!
可是能够改变string指针的指向:
string = "nihao"; --> 能够!
(由于能够改动指针指向哪里)
大家能够自己測试一下:
char *n1 = "it";
char *n2 = "it";
printf("%p %p ", n1, n2); //用%p查看地址
会发现二者的结果是一样的,指向同一个地址!
再进一步測试(生命在于折腾):
char *n1="it";
char *n2="it";
printf("%p %p ",n1,n2);
n1 = "haha";
printf("%p %p ",n1,n2);
你会发现以上程序,指针n2所指向的地址一直没变。而n1在经过
n1 = "haha";
之后,它所指向的地址就改变了。
经过上面地分析,可能非常多朋友还是有点晕,特别是可能不太清楚内存各个区域的差别。
假设有兴趣深入探究,既能够自己去看相关的C语言书籍。也能够參考下表和一些解释,假设临时不想把自己搞得更晕,能够跳过。以后讲到相关内容时自然更好理解:
名称 |
内容 |
代码段 | 可运行代码、字符串常量 |
数据段 | 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据 |
BSS段 | 未初始化全局变量,未初始化全局静态变量 |
栈 | 局部变量、函数參数 |
堆 | 动态内存分配 |
普通情况下。一个可运行二进制程序(更确切的说。在Linux操作系统下为一个进程单元。在UC/OSII中被称为任务)在存储(没有调入到内存运行)时拥有3个部分,各自是代码段(text)、数据段(data)和BSS段。
这3个部分一起组成了该可运行程序的文件。
(1)代码段(text segment):存放CPU运行的机器指令。通常代码段是可共享的,这使得须要频繁被运行的程序仅仅须要在内存中拥有一份拷贝就可以。代码段也一般是仅仅读的。这样能够防止其它程序意外地改动其指令。另外,代码段还规划了局部数据所申请的内存空间信息。
代码段(code segment/text segment)一般是指用来存放程序执行代码的一块内存区域。
这部分区域的大小在程序执行前就已经确定,而且内存区域通常属于仅仅读, 某些架构也同意代码段为可写,即同意改动程序。在代码段中,也有可能包括一些仅仅读的常数变量,比如字符串常量等。
(2)数据段(data segment):或称全局初始化数据段/静态数据段(initialized data segment/data segment)。该段包括了在程序中明白被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据。
(3)未初始化数据段:亦称BSS(Block Started by Symbol)。该段存入的是全局未初始化变量、静态未初始化变量。
而当程序被载入到内存单元时,则须要另外两个域:堆域和栈域。
(4)栈(stack):存放函数的參数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。
(5)堆(heap):用于动态内存分配,即使用malloc/free系列函数来管理的内存空间。
在将应用程序载入到内存空间运行时,操作系统负责代码段、数据段和BSS段的载入,并将在内存中为这些段分配空间。栈也由操作系统分配和管理。而不须要程序猿显示地管理;堆由程序猿自己管理,即显示地申请和释放空间。
非常多C语言地刚開始学习的人弄不清指针和数组究竟有什么样的关系。
如今就告诉大家:它们之间没有不论什么关系!它们是清白的...
-
指针就是指针。指针变量在32 位系统下。永远占4 个byte(字节)。其值为某一个内存的地址。
指针能够指向不论什么地方。可是不是不论什么地方你都能通过这个指针变量訪问到。
-
数组就是数组,其大小与元素的类型和个数有关。
定义数组时必须指定其元素的类型和个数。数组能够存不论什么类型的数据,但不能存函数。
推荐大家去看《C语言深度解剖》这本仅仅有100多页的pdf,是国人写的,里面对于指针和数组分析得非常全面。
不禁感叹,C语言果然是博(shi)大(fen)精(keng)深(die)。
从scanf函数取得一个字符串
我们能够用scanf函数获取用户输入的一个字符串。也要用到%s符号。
可是有一个问题:就是你不能知道用户到底会输入多少字符。假如我们的程序是问用户他的名字是什么。
那么他可能回答Tom。仅仅有三个字符,或者Bruce LI。就有8个字符了。
所以我们仅仅能用一个足够大的数组来存储名字,比如 char [100]。你会说这样太浪费内存了,可是前面我们也说过了,眼下的电脑一般不在乎这点内存。
所以我们的程序会是这样:
int main(int argc, char *argv[])
{
char name[100];
printf("请问您叫什么名字 ? ");
scanf("%s", name);
printf("您好, %s, 非常高兴认识您! ", name);
return 0;
}
执行程序:
请问您叫什么名字?Oscar
您好。Oscar。非常高兴认识您!
操纵字符串的一些经常使用函数
字符串在C语言里是非经常常使用的,其实,此刻你在电脑或手机屏幕上看到的这些单词、句子等,都是在电脑内存里的字符数组。
为了方便我们操纵字符串,C语言的设计者们在 string 这个标准库中已经写好了非常多函数,可供我们使用。
当然在这曾经。须要在你的.c源文件里引入这个头文件
#include <string.h>
以下我们就来介绍它们之中最经常使用的一些吧:
strlen:计算字符串的长度
strlen函数返回一个字符串的长度(不包含 )。
函数原型是这样:
size_t strlen(const char* string);
注意:size_t是一个特殊的类型,它意味着函数返回一个相应大小的数目。不是像int,char,long。double之类地基本类型。而是一个被“创造”出来的类型。在接下来的课程中我们就会学到怎样创建自己的变量类型。临时说来,我们先满足于将strlen函数的返回值存到一个int变量里(电脑会把size_t自己主动转换成int),当然严格来说应该用size_t类型,可是我们这里临时不深究了。
函数地參数是 const char *类型。之前的课程中我们学过,const(仅仅读的变量)表明此类型的变量是不能被改变的,所以函数strlen并不会改变它的參数的值。
写个程序測试一下strlen函数:
int main(int argc, char *argv[])
{
char string[] = "Hello";
int stringLength = 0;
// 将字符串的长度储存到stringLength中
stringLength = strlen(string);
printf("字符串 %s 中有 %d 个字符 ", string, stringLength);
return 0;
}
程序执行,显示:
字符串 Hello 中有5个字符
当然了,这个strlen函数。事实上我们自己也能够非常easy地实现。
仅仅须要用一个循环。从開始一直读入字符串中的字符,计算数目,一直读到' '字符结束循环。
我们就来实现我们自己的strlen函数好了:
int stringLength(const char *string);
int main(int argc, char *argv[])
{
char string[] = "Hello";
int length = 0;
length = stringLength(string);
printf("字符串 %s 中有 %d 个字符 ", string, length);
return 0;
}
int stringLength(const char *string)
{
int charNumber = 0;
char currentChar = 0;
do
{
currentChar = string[charNumber];
charNumber++;
}
while(currentChar != ' '); // 我们做循环,直到遇到' ',跳出循环
charNumber--; // 我们将charNumber减一,使其不包括' '的长度
return charNumber;
}
程序输出:
字符串 Hello 中有 5 个字符
strcpy:把一个字符串地内容拷贝到还有一个字符串里
函数原型:
char* strcpy(char* targetString, const char* stringToCopy);
这个函数有两个參数:
-
targetString:这是一个指向字符数组的指针,我们要复制字符串到这个字符数组里。
-
stringToCopy:这是一个指向要被复制的字符串的指针。
函数返回一个指向targetString的指针。通常我们不须要获取这个返回值。
用下面程序測试此函数:
int main(int argc, char *argv[])
{
/* 我们创建了一个字符数组string,里面包括了几个字符。我们又创建了还有一个字符数组copy。包括100个字符,为了足够容纳拷贝过来的字符 */
char string[] = "Text", copy[100] = {0};
strcpy(copy, string); // 我们把string拷贝到copy中
// 假设一切顺利,copy的值应该和string是一样的
printf("string 是 %s ", string);
printf("copy 是 %s ", copy);
return 0;
}
程序输出:
string 是 Text
copy 是 Text
假设我们的copy数组的长度小于6。那么程序会出错。由于string的总长度是6(最后有一个' '字符串结束符)。
strcpy的原理图解例如以下:
strcat:连接两个字符串
strcat函数的作用是连接两个字符串,就是把一个字符串接到还有一个的结尾。
函数原型:
char* strcat(char* string1, const char* string2);
由于string2是const类型,所以我们就想到了,这个函数肯定是将string2的内容接到string1的结尾,改变了string1所指向的字符指针。然后返回指向string1所指字符数组的指针。稍微有点拗口,但不难理解吧。
写个程序測试一下:
int main(int argc, char *argv[])
{
/* 我们创建了两个字符串,字符数组string1须要足够长,由于我们要将string2的内容接到其后 */
char string1[100] = "Hello ", string2[] = "Oscar!";
strcat(string1, string2); // 将string2接到string1后面
// 假设一切顺利,那么string1的值应该会变为"Hello Oscar!"
printf("string1 是 %s ", string1);
// string2没有变
printf("string2 始终是 %s ", string2);
return 0;
}
程序输出:
string1 是 Hello Oscar!
string2 始终是 Oscar!
函数的原理例如以下:
当strcat函数将string2连接到string1的尾部时,它须要先删去string1字符串最后的' '。
strcmp:比較两个字符串
函数原型:
int strcmp(const char* string1, const char* string2);
能够看到,strcmp函数不能改变string1和string2,由于它们都是const类型。
这次,函数地返回值实用了。
strcmp返回:
-
0:当两个字符串相等时
-
非零的整数(负数或正数):当两个字符串不等时
用下面程序測试strcmp函数:
int main(int argc, char *argv[])
{
char string1[] = "Text of test", string2[] = "Text of test";
if (strcmp(string1, string2) == 0) // 假设两个字符串相等
{
printf("两个字符串相等 ");
}
else
{
printf("两个字符串不相等 ");
}
return 0;
}
程序输出:
两个字符串相等
sprintf:向一个字符串写入
当然,这个函数事实上不是在string.h这个头文件中,而是在stdio.h头文件中。可是它也与字符串的操作有关,所以我们也介绍一下,并且这个函数是非经常常使用的。
看到sprintf函数的名字,大家是否想到了printf函数呢?
printf函数是向标准输出(通常是屏幕)写入东西。而sprintf是向一个字符串写入东西。
前面的s就是英语string的首字母。
写个程序測试一下此函数:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char string[100];
int age = 18;
// 我们向string里写入"你18岁了"
sprintf(string, "你%d岁了", age);
printf("%s ", string);
return 0;
}
程序输出:
你18岁了
其它经常使用的另一些函数。如 strstr(在字符串中查找一个子串),strchr(在字符串里查找一个字符)。等等,我们就不一一介绍了。
总结
-
电脑不认识字符,它仅仅认识数字(0和1),为了解决问题,计算机先驱们用一个表格规定了字符与数值的相应关系,最经常使用的是ASCII表
-
字符类型char用来存储一个字符,且仅仅能存储一个字符. 实际上存储的是一个数值。可是电脑会在显示时转换成相应的字符
-
为了创建一个词或一句话,我们须要构建一个字符串,我们用字符数组来实现
-
全部的字符串都是以' '结尾。这个特殊的字符' '标志着字符串的结束
-
在string这个C语言标准库中,有非常多操纵字符串的函数,仅仅须要引入头文件 string.h就可以
第二部分第五课预告:
今天的课就到这里,一起加油咯。
下一次我们学习第二部分第五课:预处理
程序猿联盟社区
眼下有一个微信群和一个QQ群(都已经破百人),凡是对编程感兴趣的朋友都能够加。大家能够交流,学习,互动。分享写的程序的源码,等。
微信群(程序猿联盟),加群请私信我(微信群人数超过100之后,不能通过扫描二维码增加了,仅仅能私信我,谢谢)
QQ群(程序猿联盟),群号是 413981577
QQ群共享里有N多编程PDF。
扫描以下二维码加QQ群:
我们还建立了一个公共的百度云盘,2TB容量,供大家上传优秀编程资源。链接加群之后会发送。
我的微信号能够在文章最后看到,QQ和邮箱也在最后。
也创建了《程序猿联盟》的微社区,方便大家提问和互动。能够关注一下。
微社区地址和二维码例如以下:
http://m.wsq.qq.com/264152148
谢谢!