本文已参与「新人创作礼」活动,一起开启掘金创作之路。
第一章 导言
1.1 入门
\n 表示一个字符,是换行的意思;
\t 表示制表符;
\b 表示回退符;
\“ 表示双引号;
\\ 表示反斜杠本身;
2.3会给出完整列表。
1.2 变量与算数表达式
#include<stdio.h>
// 当fahr为0,20,40...300时,分别打印华氏温度与摄氏温度的对照表
int main() {
int fahr, celsius;
int lower, upper, step;
lower = 0; //温度下限
upper = 300; //温度上限
step = 20; //步长
fahr = lower;
printf("Fahr\tCelsius\n");
while (fahr <= upper)
{
celsius = 5 * (fahr - 32) / 9;
printf("%d\t\t%d\n", fahr, celsius);
fahr += step;
}
}
int表示其后面所列变量为正数,与之相应的还有float表示浮点数。对于int类型,通常为16位,其取值范围在-32768~32767之间,也有用32位表示的int类型。float类型通常是32位,它至少有6位有效数字,取值范围一般在之间。
除了这两个之外还有一些基本数据类型,包括:
char 字符-一个字节
short 短整型
long 长整型
double 双精度浮点型
这些数据类型对象的大小也取决于具体的机器。另外,还存在这些基本数据类型的数组、结构、联合,指向这些类型的指针以及返回这些类型的函数。后面会陆续介绍。
printf函数本身不属于C语言,C语言本身并没有定义输入/输出功能。printf仅仅是标准函数库中一个有用的函数而已,这些标准函数在C语言程序中通常都可以使用。但是ANSI标准定义了printf函数的行为,因此,对每个符合该标准的编译器和库来说,该函数的属性都是相同的。
由于输出的数不是右对齐,所以输出的结果不是很美观,这个问题可以通过printf语句的第一个参数%d中指明打印宽度,例如:
printf(" %3d %6d\n", fahr, celsius);
打印结果如下所示:
另一个较为严重的问题是,我们使用的是整型,会造成数据不准确,所以需要改成浮点型:
#include<stdio.h>
// 当fahr为0,20,40...300时,分别打印华氏温度与摄氏温度的对照表
int main() {
float fahr, celsius;
float lower, upper, step;
lower = 0; //温度下限
upper = 300; //温度上限
step = 20; //步长
fahr = lower;
printf("Fahr\tCelsius\n");
while (fahr <= upper)
{
celsius = 5 * (fahr - 32) / 9;
printf("%3.0f\t\t%6.1f\n", fahr, celsius);
fahr += step;
}
}
输出中的%3.0f的3是保留3个占位符,且不留小数点后的小数部分,f表示float。可以省略宽度与精度,比如%6f表示待打印的浮点数至少有6个字符宽;%.2f表示指定待打印的浮点数的小数点后有两位小数,但宽度没限制;%f则仅仅要求按照浮点数打印该数。
%d 按照十进制整数打印
%6d 按照十进制整数打印,至少6个字符宽
%f 按照浮点数打印
%6f 按照浮点数打印,至少6个字符宽
%.2f 按照浮点数打印,小数点后有两位小数
%6.2f 按照浮点数打印,至少6个字符宽,小数点后有两位小数
此外,printf函数还支持下列格式说明:%o表示八进制数;%x表示十六进制数;%c表示字符;%s表示字符串;%%表示百分号(%)本身。
1.3 for语句
对于某个特定任务我们可以采用多种方法来编写程序,下面代码也可以实现前面的温度转换程序的功能:
#include<stdio.h>
// 当fahr为0,20,40...300时,分别打印华氏温度与摄氏温度的对照表
int main() {
float fahr, celsius;
float lower, upper, step;
lower = 0; //温度下限
upper = 300; //温度上限
step = 20; //步长
fahr = lower;
printf("Fahr\tCelsius\n");
for (int i = 0; i <= upper; i+=step)
{
celsius = 5 * (fahr - 32) / 9;
printf("%3.0f\t\t%6.1f\n", fahr, celsius);
fahr += step;
}
}
在实际编程过程中,可以选择while和for中的任意一种循环语句,主要要看使用哪一种更清晰。for语句比较适合初始化和增加步长都是单条语句并且逻辑相关的情形,因为它将循环控制语句集中放在一起,且比while语句更紧凑。
练习1-5 修改温度转换程序,要求以逆序(从300度到0度的顺序)打印温度转换表。
#include<stdio.h>
// 当fahr为0,20,40...300时,分别打印华氏温度与摄氏温度的对照表
int main() {
float fahr, celsius;
float lower, upper, step;
lower = 0; //温度下限
upper = 300; //温度上限
step = 20; //步长
fahr = upper;
printf("Fahr\tCelsius\n");
for (int i = upper; i >= 0; i-=step)
{
celsius = 5 * (fahr - 32) / 9;
printf("%3.0f\t\t%6.1f\n", fahr, celsius);
fahr -= step;
}
}
1.4 符号常量
最后看下符号常量,在程序中使用300、20等类似的“幻数”并非好习惯,它们几乎没办法告诉后人该程序要提供什么信息,而且使程序的修改变得更加困难。处理这种幻数的一种方法是赋予它们有意义的名字。#define指令可以把符号名(或称为符号常量)定义为一个特定的字符串:
#define 名字 替换文本
替换文本可以是任何字符序列,不限于数字。
#include <stdio.h>
#define LOWER 0
#define UPPER 300
#define STEP 20
int main() {
float fahr, celsius;
for(fahr = LOWER; fahr <= UPPER; fahr+=STEP) {
celsius = 5 * (fahr - 32) / 9;
printf("%3.0f\t\t%6.1f\n", fahr, celsius);
}
}
其中,LOWER、UPPER与STEP都是符号常量,而非变量,因此不需要出现在声明中。符号常量名通常用大写字母拼写,这样可以很容易与小写字母拼写的变量名相区别。注意#define指令行的末尾没有分号。
1.5 字符输入/输出
接下来看一组跟字符型数据处理有关的程序。读者将会发现,许多程序只不过是这里所讨论的程序原型和扩充版本而已。
标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。
标准库提供了一次读/写一个字符的函数,其中最简单的是getchar和putchar两个函数。每次调用时,getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回。也就是说,在执行语句
c = getchar()
之后,变量c中将包括输入流中的下一个字符。这种字符通常是通过键盘输入的,从文件输入字符的方法将在第七章进行讨论。
每次调用putchar函数时将打印一个字符。例如,语句
putchar()
将把整型变量c的内容以字符的形式打印出来,通常显示在屏幕上。putchar与printf这两个函数可以交替调用,输出的次序与调用的次序一致。
1.5.1 文件复制
借助于getchar与putchar函数,可以在不了解其他输入/输出知识的情况下编写出数量惊人的有用的代码。最简单的例子就是把输入一次一个字符地复制到输出,其基本思想如下:
读一个字符
while(该字符不是文件结束指示符)
输出刚读入的字符
读下一个字符
#include<stdio.h>
int main() {
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}
字符在键盘、屏幕或其他任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。char类型专门用于存储这种字符型数据,当然任何整型(int)也可以用于存储字符型数据。因为某些潜在重要原因,这里使用int。
这里需要解决如何区分文件中有效数据与输入结束符的问题。C语言采取的解决方法是:在没有输入时,getchar函数将返回一个特殊值,这个特殊值与任何实际字符都不同,这个值叫EOF(end of file)。我们在声明变量c的时候,必须让它大到足以存放getchar函数返回的任何值。这里之所以不把c声明成char,就是因为它必须要足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF。因此,将c声明成int。
EOF定义在头文件<stdio.h>中,是个整型数,其具体数值是什么不重要,只要它与任何char类型的值都不同即可。其实可以更加精炼一下代码:
#include<stdio.h>
int main() {
int c;
c = getchar();
while ((c = putchar(c)) != EOF) {
c = getchar();
}
}
这里!=的优先级大于=,所以c会被赋值为0或者1,
c = getchar() != EOF
等价于
c = (getchar() != EOF)
练习1-6 验证表达式getchar() != EOF的值是0还是1
#include<stdio.h>
int main() {
int c;
while ((c = getchar()) != EOF) {
printf("%d", c);
}
}
可以看到只要不是ctrl+c就都是1,否则是0。
练习1-7 编写一个打印EOF值的程序
#include<stdio.h>
int main()
{
printf("%d", EOF);
}
看出EOF是一个预定义的int值,会输出-1
1.5.2 字符计数
下列程序用于对字符进行计数,它与上面的复制程序类似。
#include<stdio.h>
int main() {
long nc = 0;
while (getchar() != EOF)
{
++nc;
}
printf("%ld", nc);
}
其中++nc是新引进的运算符,其功能是自增1的操作,nc = nc + 1和它有相同效果。类似的,自减可以用--nc进行操作。
该字符计数程序使用long类型的变量存放计数值,而没有使用int类型的变量。long整型数(长整型)至少要占用32位存储单元。在某些机器上int与long类型的长度相同,但在一些机器上,int可能只有16位存储单元的长度(最大值为32767),这样,容易导致很小的输入都造成计数变量溢出。转换说明%ld告诉printf函数其对应的参数是long类型。
使用double(双精度浮点型)可以处理更大的数字。我们在这里不使用while,而用for循环语句来展示另一种写法:
#include<stdio.h>
int main() {
double nc = 0;
for (nc; getchar() != EOF; nc++)
{
;
}
printf("%.0f", nc);
}
对于float和double类型,printf函数都使用%f进行说明。%.0f强制不打印小数点和小数部分,因此小数部分为0。
该程序中,for循环语句是空的,因为所有操作都在for的括号里完成了,但是C语言语法规则又要求必须有一个循环体,所以里面用了一个分号来代替。把它单独放在一行是为了醒目。
还有一个问题就是第一次调用getchar()的时候,while或者for从一开始就为假,程序会输出0,这也是正确的结果。这一点很重要。while和for的优点是在执行循环体之前就会检查条件是否合格。有可能一次循环都不执行。在出现边界条件时,while语句与for语句有助于确保程序执行合理的操作。
1.5.3 行计数
接下来的这个程序用于统计输入中的行数,我们在上面提到过,标准库保证输入文本流以行序列的形式出现,每一行都以换行符结束,因此,统计行数等价于统计换行符的个数。
#include<stdio.h>
int main() {
int n, c1 = 0;
while ((n = getchar()) != EOF)
{
if (n == '\n')
{
c1++;
}
}
printf("%d", c1);
}
双等号是关系运算法,用来判断用的,表逻辑用,一个等号是赋值运算,要加以区分。如果在for中只写了一个等号依然合法,程序不会报错,需要特别注意。
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,它不过是整型数的另一种写法,比如'A'是一个字符常量,在ASCII字符集中值为65(即字符A的内部表示值为65),用‘A’比65更好,因为它的可读性更强。字符常量中使用的转义字符序列也是合法的,比如'\n'表示换行符的值,在ASCII中值为10,我们应注意到'\n'是一个仅包含一个字符的字符串常量。有关字符串和字符之间的关系将在第2章讨论。
练习1-8 编写一个统计空格、制表符与换行符个数的程序
#include<stdio.h>
int main() {
int space = 0;
int table = 0;
int next = 0;
int c;
while ((c = getchar()) != EOF)
{
if (c == ' ')
++space;
else if (c == '\t')
++table;
else if (c == '\n')
++next;
}
printf("the number of Space is %d, Table is %d, next is %d", space, table, next);
}
练习1-9 编写一个将输入复制到输出的程序,并将其中连续的多个空格用一个空格代替。
#include<stdio.h>
int main() {
int last = 0, now;
for(; (now = getchar()) != EOF;) {
if (now == ' ') {
if (last == 0)
{
last++;
putchar(now);
}
else
{
last++;
}
}
else{
putchar(now);
last = 0;
}
}
}
练习1-10 编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,把回退符替换为\b,把反斜杠替换为\\。这样可以将制表符和回退符以可见的方式显示出来。
#include<stdio.h>
int main() {
int back;
int table;
int next;
int c;
while ((c = getchar()) != EOF)
{
if (c == '\b')
{putchar('\\');putchar('b');}
else if (c == '\t')
{putchar('\\');putchar('t');}
else if (c == '\\')
{putchar('\\');putchar('\\');}
else
putchar(c);
}
}
1.5.4 单词计数
接下来介绍第四个实用的程序,用于统计行数、单词数与字符数。
#include<stdio.h>
#define IN 1 /* inside a word */
#define OUT 0 /* outside a word */
int main()
{
int c, nl, nw, nc, state;
state = OUT;
nl = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++nl;
if (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf("%d %d %d\n", nl, nw, nc);
}
每次遇到单词的第一个字符,就作为一个新单词加以统计,state变量记录现在是不是在一个单词中,初值是不在单词中。
1.6 数组
在这部分,我们编写程序统计各个数字、空白符(包括空格符、制表符及换行符)以及所有其他字符出现的次数。
#include<stdio.h>
/* count digits, white space, others */
int main() {
int i, c, nwhite, nothers;
int ndigit[10];
nwhite = nothers = 0;
for (i = 0; i < 10; ++i)
ndigit[i] = 0;
while((c = getchar()) != EOF){
if (c >= '0' && c <= '9')
++ndigit[c-'0'];
else if (c == ' ' || c == '\t' || c == '\n')
++nwhite;
else
++nothers;
}
printf("digits =");
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
printf(", white space = %d, other = %d\n",
nwhite, nothers);
}
输出结果为:digits = 9 3 0 0 0 0 0 0 0 1, white space = 215, other = 354
c-'0'表示的是0-9的具体值,'0'和c都表示ASCII码,不能确定其在数组中的位置。
1.7 函数
下面是函数power(m, n)的定义及调用它的主程序, 这样我们可以看到一个完整的程序结构:
#include <stdio.h>
int power(int m, int n);
/* test power function */
int main()
{
int i;
for (i = 0; i < 10; ++i)
printf("%d %d %d\n", i, power(2,i), power(-3,i));
return 0;
}
/* power: raise base to n-th power; n >= 0 */
int power(int base, int n)
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
在开头需要声明函数,如果 没有声明某个参数的类型,则默认为int类型。
1.8 参数-传值调用
C语言是值传递,只会修改被调用函数的参数值,其被存放在一个临时变量中。因此参数可以看作是便于初始化的局部变量,因此额外使用的变量更少。
int power(int base, int n)
{
int p;
for (p = 1; n > 0; --n)
p = p * base;
return p;
}
上面的n用作临时变量,并通过随后执行的for循环语句递减,直到其值为0,这样就不需要引入变量i,power函数内部怎么折腾这个n,也不会影响主函数中n的值,这就是值传递的好处。
如果我非要改power函数的n同时也要改到主函数怎么办?调用者需要向被调用函数提供变量的地址(地址就是指向变量的指针),被调用函数则需要声明对应的参数为指针变量。
1.9 字符数组
字符数组很常见,通过一个例子表示,该程序读入一组文本,把最长的文本行打印出来,该算法的基本框架非常简单:
while (还有未处理的行)
if (该行比已处理的最长行还要长)
保存该行为最长行
保存该行的长度
打印最长的行
#include<stdio.h>
#define MAX 1000
// read a line and return the length.
int getline(char line[], int limit);
// copy array 'from' into 'to'.
void copy(char to[], char from[]);
int main() {
int len; //current length
int max = 0; //maimum seen so far
char line[MAX];
char longest[MAX];
while ((len = getline(line, MAX)) > 0)
{
if (len > max) {
max = len;
copy(longest, line);
}
}
if(max > 0) {
printf("%s", longest);
}
return 0;
}
int getline(char line[], int limit) {
int c, i;
for(i = 0; i < limit - 1 && (c=getchar()) != EOF && c != '\n'; ++i) {
line[i] = c;
}
if(c == '\n') {
line[i] = c;
i++;
}
line[i] = '\0';
return i;
}
void copy(char to[], char from[]) {
int i = 0;
while ((to[i] = from[i]) != '\0')
{
i++;
}
}
getline函数把字符'\0'(即空字符,其值为0)插入到它创建的数组末尾,以标记字符串的结束。这一约定已被C语言采用,当在C语言程序中出现类似于
“hello\0"
的字符串常量时,它将以字符数组的形式存储,数组的各元素分别存储字符串的各个字符,并以'\0'标志字符串的结束。
有看不懂看不清的可以评论私聊我哦