C程序设计语言第一章笔记

307 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

第一章 导言

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位有效数字,取值范围一般在1038103810^{-38}\sim10^{38}之间。

​ 除了这两个之外还有一些基本数据类型,包括:

char 字符-一个字节

short 短整型

long 长整型

double 双精度浮点型

​ 这些数据类型对象的大小也取决于具体的机器。另外,还存在这些基本数据类型的数组、结构、联合,指向这些类型的指针以及返回这些类型的函数。后面会陆续介绍。

​ printf函数本身不属于C语言,C语言本身并没有定义输入/输出功能。printf仅仅是标准函数库中一个有用的函数而已,这些标准函数在C语言程序中通常都可以使用。但是ANSI标准定义了printf函数的行为,因此,对每个符合该标准的编译器和库来说,该函数的属性都是相同的。

​ 由于输出的数不是右对齐,所以输出的结果不是很美观,这个问题可以通过printf语句的第一个参数%d中指明打印宽度,例如:

printf(" %3d %6d\n", fahr, celsius);

打印结果如下所示:

image.png

​ 另一个较为严重的问题是,我们使用的是整型,会造成数据不准确,所以需要改成浮点型:

#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;
    }
}

image.png

输出中的%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'标志字符串的结束。

image.png 有看不懂看不清的可以评论私聊我哦