Day 2

Day 2

c程序的执行过程

源文件到可执行文件,打造顾客为两个步骤:

  1. 广义上的编译过程:
  2. 链接过程

预处理过程

预处理过程主要就两个作用:

  1. 执行预处理指令
  2. 丢弃代码中的注释,注释就不参与后续的任何过程;

#define 用以定义宏常量(符号常量)

其实就是给字面值常量起个名字,本质是文本替换,没有数据类型、取值范围的限制

注:

1.宏常量名字必须大写,如果有多个单词,下划线隔开

2.宏定义是没有分号的

预处理阶段会将代码中的宏常量进行文本替换

#define定义函数宏

// 定义了一个函数宏,用于求正方形的面积
#define SQUARE_AREA(length) ((length) * (length))
// 定义了一个函数宏,用于求两个数的平方差
#define SQUARE_DIFF(x, y) (((x) * (x)) - ((y) * (y)))

注:

1.宏函数名字全部大写 下划线隔开 见名知意

2.在写宏函数定义表达式时,要增加上必要的小括号(),增强表达式的可读性**<建议>**

3.在宏函数定义的表达式中,每一个参数都必须要用小括号括起来()

4..在宏函数定义的表达式中,整个表达式必须用()括起来

#include <stdio.h>

int main(void) {
// 调用函数宏
printf("边长是10的正方形,其面积是:%d\n", SQUARE_AREA(10));
printf("3和2的平方差是:%d\n", SQUARE_DIFF(3, 2));

return 0;
}

#inlcude

包含头文件

<>表示表示去标准库中寻找对应的头文件,也就是说使用C语言标准库头文件,使用<>

“ ”表示先去当前目录下寻找头文件,找不到再去找标准库

scanf函数

用于从键盘录入读取数据,也是两个单词的缩写:scan formatted

scanf(“%d”,&num)其中注意:变量名的前面需要加一个"&",这是必须的语法,不能忘记写。

然后需要注意的是 他表示将读取到的数据存入目标地址,所以它的参数后面不能直接写变量,而是需要写变量的地址,这一般需要添加取地址符号&

编译过程(狭义)

将预处理后的源代码转换成汇编语言,也就是将.i文件转换成.s文件

汇编过程

编译器完成编译后,**汇编器(Assembler)**会将这些汇编指令转换为目标代码(机器代码),生成了一个.o(或者.obj)文件。从这一步开始,文件中的内容就不是程序员能够肉眼看懂的文本代码了,而是二进制代码指令。

所以汇编过程的主要作用就是将汇编语言代码转换成机器语言,即转换成机器可以执行的指令,也就是生成.o文件。

在链接阶段,链接器(Linker)会把项目中,经过汇编过程生成的多个(至少有1个).o文件和程序所需要的其它附加代码整合在一起,生成最终的可执行程序。比如你在代码中调用了标准库函数,那么链接器会将库中的代码包含到最终的可执行文件中。

链接过程

合并外部的代码

生成可在执行的文件

总结编译和链接

=========================================================

使用VS调试程序

编译错误和链接错误

编译错误(Compile-time Errors)出现在代码的编译阶段,表示编译失败。大多数的编译错误,实际上就是疏忽导致的语法错误,比如:

  1. 忘记在语句结尾加上分号。
  2. 小括号、中括号或大括号不匹配。
  3. 类型不匹配,尝试将一个字符串赋值给一个整数类型的变量等。
  4. 使用未声明的变量。

链接错误(Linking Errors)出现在代码的链接阶段,表示链接失败。链接错误大多和函数调用有关,比如:

  1. 调用函数时,把函数的名字写错了。比如想调用printf函数,但是写成了print。
  2. 忘记包含头文件。比如使用printf函数但忘记写#include语句。
  3. 没有定义一个函数却使用它。

调用一个函数,他有两个参数,但我只给一个,这属于语法错误,属于编译错误

格式化输入输出

输入输出模型

scanfprintf函数看起来一个是从键盘接收数据输入,一个是将数据输出打印到显示器,但计算机内部处理的过程却没有那么简单。

我们通过输入/输出模型来简单了解一下这两个函数大体上的执行原理。

冯诺依曼体系计算机(也叫存储程序控制型计算机),主要包含三大核心组件:CPU、IO设备以及存储器,而存储器当中最重要的则是内存储器,也就是内存。直到今天,硬件设备的发展日新月异,但现代计算机仍没有脱离此体系。

冯诺依曼体系计算机,一个核心问题是CPU、内存以及IO设备三者之间的速度差异从而导致的性能瓶颈,这就是常说的"冯·诺依曼瓶颈”。

具体的说,这个瓶颈指的是:

CPU 的处理速度远远快于内存和 I/O 设备,导致在等待数据处理和传输时,CPU 大部分时间处于空闲等待的状态。这种速度差异造成了显著的性能瓶颈,限制了整个系统的效率。

为了平衡这三者之间的速度鸿沟,一个简单有效的手段是引入缓冲区技术,下面我们简单介绍一下缓冲区技术。

缓冲区

缓冲区本质上是一块临时存储数据的内存区域(一般是在内存中分配的),它在速度较慢的内存和 I/O 设备与速度较快的 CPU 之间起到桥梁的作用。

printf函数和scanf函数:

  1. printf函数将程序(内存)中的数据打印到外部设备(显示器)上
  2. scanf函数代表从外部设备(键盘)中读取数据到程序(内存)中

这些都是非常典型的I/O操作过程。

为了更深入地解释缓冲的工作原理,以printfscanf函数为例,可以更直观地解释其运作机制:

  1. 当你使用 printf 输出数据时,数据并不是立刻写入到输出设备(如屏幕)。它首先被放置在一个stdout缓冲区(标准输入流)中,然后在满足特定条件时,数据会被刷新到输出设备。
  2. 当你使用 scanf 输入数据时,数据也不是直接从输入设备(如键盘)读取的。它首先被加载到一个stdin缓冲区中,然后 scanf 从这个缓冲区中获取数据。
  3. 不管是stdin还是stdout缓冲区都是内存的一部分。

格式化输入输出-缓存模型

我们都知道,I/O的过程效率很低。除了硬件性能本身的差异外,I/O操作的复杂性也是非常重要因素。每次进行I/O操作都会带来一些固定的开销,比如:

  1. 每次 I/O 操作都需要设备初始化和响应等待等。
  2. 操作系统管理 I/O 请求,涉及中断处理和上下文切换,这些都消耗了大量时间。
  3. 应用从用户态切换到内核态的系统调用也会带来额外的时间开销。(I/O操作普遍涉及系统调用)

总之,如果每输入或输出一个字符都要进行一次完整的I/O操作,那么这些固定的开销就会迅速积累,导致性能显著下降。

硬件层面的效率低下,我们没有办法通过软件层面的优化去解决。但对于这些大量的固定开销,我们可以通过缓冲区来进行效率优化。

缓冲区的主要目的是暂时存储数据,然后在适当的时机一次性进行大量的I/O操作。这样,多个小的I/O请求可以被组合成一个大的请求,有效地分摊了固定开销,并显著提高了总体性能。

缓冲区的分类

从上述内容中,我们可以明确地看到缓冲区的一个显著特点:当满足特定的条件时,程序会开始对缓冲区内的数据执行输入或输出操作。

这种**“满足条件即触发数据传输”的行为,被我们称为“缓冲区的自动刷新”**机制。

基于这种自动刷新的触发条件的不同,我们可以将常见缓冲区划分为以下三种类型:

  1. **全缓冲区,也叫满缓冲区。**顾名思义,仅当缓冲区达到容量上限时,缓冲区才会自动刷新,并开始处理数据。否则,数据会持续积累在缓冲区中直到缓冲区满触发自动刷新。文件操作的缓冲区便是这种类型的经典例子。

    实际上所有缓冲区满了以后都会自动刷新

  2. **行缓冲区。**缓冲区一旦遇到换行符,缓冲区就会自动刷新,所有数据都会被传输。stdin和stdout缓冲区都是行缓冲区。

  3. **无缓冲区,不缓冲。**在此模式下,数据不经过中间的缓冲步骤,每次的输入或输出操作都会直接执行。这种方法适用于需要快速、实时响应的场合。例如,stderr(标准错误输出)就是这种方式,它经常被用来即时上报错误信息。

补充和注意事项:

关于缓冲区,有以下几点需要特别注意:

  1. 无论是哪种类型的缓冲区,当缓冲区满了时,都会触发自动刷新。
    1. 全缓冲区:唯一的自动刷新条件是缓冲区满。
    2. 行缓冲区:除了缓冲区满导致的自动刷新,还有遇到换行符的自动刷新机制。
  2. 手动刷新。大多数缓冲区提供了手动刷新的机制,比如使用fflush函数来刷新stdout缓冲区。
  3. 当程序执行完毕(如main函数返回)时,缓冲区通常会自动刷新,除此之外,还有一些独特的机制也可以刷新缓冲区。但这些机制可能因不同的编译器或平台而异,不能作为常规手段。强烈建议依赖手动或者常规自动刷新的机制来完成缓冲区的刷新。
  4. 不同的编译器和开发环境可能会对输出缓冲进行特殊设置,尤其是在调试模式下,以便提供更好的调试体验。比如在VS的Debug模式下,即使没有换行符,printf函数的输出通常也会立即显示在控制台上。这种行为是为了帮助程序员更有效地调试程序,即时看到他们的输出,而不需要固定等待缓冲区刷新条件。

至此,我们已经对输入输出的基本概念有了全面的了解。接下来,我们将深入探讨具体的函数如何使用。

当涉及到函数调用时,虽然查阅文档是重要的学习步骤,但真正的关键在于实践:亲自编写和执行代码。

printf函数

printf函数的核心作用是将各种类型的数据转换为字符形式并输出到stdout缓冲区中。

格式字符串包含两个主要部分:

  1. 普通字符,printf函数会将普通字符原封不动的进行显示。比如上面代码中的"i = , j = "。
  2. 转换说明,以字符% 开头,它为后续对应位置的表达式提供了一个占位符。在上述示例中,“%d"和”%f"就是转换说明。

理解转换说明的含义和用法是掌握printf函数的关键。

转换说明printf函数中起到了关键的角色,允许开发者对输出格式进行精细的控制。它主要有以下几个作用:

  1. 占位符的作用。
  2. 控制输出的格式,比如宽度,精度等。
  3. 指示被转换成字符数据的对应参数的类型。

系统的讲,转换说明的组成公式如下:

%[标志][字段宽度][.精度][长度]说明符
scanf函数

scanf函数的核心作用是从stdin缓冲区中读取字符形式的数据,并将其转换为特定类型的数据。

从实际效果看,scanf函数会根据格式字符串读取输入的内容,并将这些内容赋值给指定的变量。

调用scanf函数时,首要参数也是格式字符串,紧随其后的参数是变量的地址,表示将读取到的值存放在哪个位置。调用形式如下:

scanf(格式字符串, &变量1, &变量2, ...);

scanf函数的格式字符串中可能包含:

  1. 普通字符,比如空格和其他字符,scanf函数会期望输入中有与之匹配的字符。一般来说,格式字符串不需要普通字符。
  2. 转换说明,以字符"%" 开头,它告诉scanf函数应该如何解释输入中的数据并如何存储它。在上述示例中,“%d"和”%f"就是转换说明。

值得注意的是,scanf函数在调用时填入的变量前面要加符号"&",它是取地址运算符,意思是告诉scanf函数将数据存储到某个地址。它一般是必须的,但有些情况下可以省略。

scanf函数本质上是一个**“模式匹配"函数,试图把"stdin缓冲区”**中的字符与格式字符串匹配。

scanf函数会从左到右依次匹配格式字符串中的每一项:

  1. 如果匹配数据项成功,那么scanf函数会继续处理格式串的剩余部分;
  2. 如果匹配不成功,那么scanf函数将不再处理格式串的剩余部分,而会立刻返回。

除此之外,scanf函数的转换说明符大都默认忽略前置的空白字符,这样的设计让输入对用户更好友好,比如:

  1. %d: 忽略前置的空白字符 (包括空格符、水平和垂直制表符、换页符和换行符),然后匹配十进制的有符号整数。
  2. %f: 忽略前置的空白字符,然后匹配浮点数。

练习,下列代码的执行结果是什么?

代码块 8. scanf函数-练习题1

int i, j;
float x, y;
scanf("%d%d%f%f", &i, &j, &x, &y);

分别键盘录入以下数据:

100 200 0.1 0.2

1-20.3-4.0e3

100a2000.10.2

1002000.10.2

结果是什么呢?为什么?

转换说明scanf的说明符和printf是不同的,i是可以自动匹配十进制 八进制 十六进制的整数,d只能匹配十进制

注意事项:

scanf 函数用 %c 格式化字符串来读取单个字符时,并不会跳过空白字符,%c 会读取输入的下一个字符,无论它是什么,包括空白字符。

所以在录入字符时,尤其是一行录入多个数据且包含输入字符时,一定要在转换说明前面留出一个空格,以匹配可能的空格:

char ch;
int num;
printf("请输入一个数字以及一个字符: ");
scanf("%d %c", &num, &ch); // 注意 %c 前的空格
printf("你输入的数字是: %d\n", num);
printf("你输入的字符是: %c\n", ch);

上述代码运行,键盘录入:

100 a

程序打印结果:

你输入的数字是: 100

你输入的字符是: a

普通字符

scanf函数的格式字符串串中也可以包含普通字符("%"之前的部分是普通字符),和printf函数不同的是,scanf函数的普通字符也用来表示匹配规则。

例如:

  1. 空白字符:对应输入中的任意数量的空白字符(如空格、制表符或换行)。
  2. 非空白字符:要求输入中精确地匹配该字符。(是什么字符就匹配什么,写几个该字符就匹配几个)

因此,scanf中的普通字符不仅仅是装饰或分隔,它们也参与到输入数据的匹配中,确保数据的格式正确。

int i, j;
scanf("%d/%d", &i, &j);

分别键盘录入以下数据:

5/ 96

5 / 96

结果是什么?

其它不变,把转换说明改完"%d /%d",再次录入数据,有区别吗?为什么?

基本数据类型

基本数据类型主要包括:整数类型,浮点数类型和字符类型,其中字符类型也可以看作是整数类型中的一种。

所以你可以认为C语言的基本数据类型只有两类:

  1. 整数类型
  2. 浮点数类型
sizeof

值的注意的是,我们经常在VS的监视窗口使用sizeof运算符。

整数

代码中最常见的十进制整数字面值,默认情况下是int类型的,如果它超出了 int 的表示范围,那么它的类型是 long 和 long long 中能表示该字面值的 “最小” 类型。

对八进制和十六进制整数字面值来说,整数的字面值首先尝试被视为 int 类型,如果超出 int 范围,依次尝试 unsigned intlongunsigned longlong longunsigned long long,直到找到可以容纳该字面值的最小类型。

所以在有必要的情况下,我们可以通过一系列的操作,使得整数字面值改变它的默认类型。比如:

如果要指明某个整数字面值为一个 long 类型,只需要在字面值最后面加字母 L (禁止加小写的L)

整数类型编码 (拓展)

整数类型分为两类:

  1. 无符号整数
  2. 将所有的二进制位都作为数值位,在内存中它的编码表示就是这个整数的二进制表示,非常简单。
  3. 取值范围就是[ 0, 2该整数类型位长-1],比如unsigned short一般占2个字节,位长是16,它的取值范围就是[0, 65535]
  4. 有符号整数,关于有符号整数在内存中的存储表示,可以直接参考文档补充_有符号整数。这部分内容属于《计算机组成原理》的一部分,建议每一位C程序员都要掌握。
有符号整数规则的总结

如果你实在懒得看文档,那么也请至少记住以下结论,会非常有用:

  1. 原码
    1. 其有效数字是该数绝对值的二进制表示。
    2. 最高位是符号位,如果是负数符号位是1,正数符号位是0。
    3. 有效数值和最高位之间如果有空隙,加0。
    4. 最高位是符号位,其它位都是数值位。
  2. 反码
    1. 正数的反码与原码相同。
    2. 负数的反码是对其原码逐位取反,但符号位除外。
  3. 补码
    1. 正数的补码与原码相同。
    2. 负数的补码是在其反码的基础上再加1。

计算机当中存储整数都是以补码的形式存储的,目的是为了统一加减法为加法,降低CPU设计的复杂性,提高性能。

补码有以下两个重要的特性(假设x是一个n位整数):

  1. x + (-x) = 1, 000…02进制补码(其中0一共有n个,高位的1溢出被舍掉) = 0
  2. x + (~x) = 111…12进制补码(其中1一共有n个) = -1
字符型

由于C语言自身char类型设计的局限性,尽量不要使用char类型去表示和操作非ASCII码表中的字符,这会带来很多麻烦。

C语言的char类型在绝大多数时候,可以直接作为整数类型进行处理。(当然,下面我们会讲不能作为整数处理的情况)

虽然字符可以直接作为整数处理,但还是尽量在处理字符时使用char类型,而不是滥用char类型,以至于写出一些很奇葩的代码:

'a' * 'b' / 'c';   /* 这个表达式的结果是96,但明显过于奇葩,不要这么用)
字符处理函数

首先,使用字符处理函数,需要包含头文件<ctype.h> 。

这些字符处理函数,又可以分为两大类:

  1. 检查字符类型函数
  2. 字符大小写转换函数

// 检查字符类型函数
int isalnum(int c); /* Is c alphanumeric? 检查字符c是否是一个字母或数字 */

int isalpha(int c); /* Is c alphabetic? 检查字符c是否是一个字母 */

int isblank(int c); /* 检查字符c是否是一个空白字符(只包括空格、制表)*/ int isspace(int c); /* 检查字符c是否是一个空白字符(包括空格、制表、换行、换页等)*/

int isdigit(int c); /* Is c a decimal digit? 检查字符c是否是一个十进制整数 */

int islower(int c); /* Is c a lower-case letter? 检查字符c是否是一个小写字母 */ int isupper(int c); /* Is c an upper-case letter? 检查字符c是否是一个大写字母 */

int ispunct(int c); /* Is c punctuation? 检查字符c是否是一个标点符号 */

int isxdigit(int c); /* Is c a hexadecimal digit? 检查字符c是否是一个十六进制数 */

// 字符大小写转换函数
int tolower(int c); // 如果字符 c 是大写字母,则转换为对应的小写字母;否则,返回原字符。
int toupper(int c); // 如果字符 c 是小写字母,则转换为对应的大写字母;否则,返回原字符。

这些函数都不要死记硬背,你只需记住C语言的标准库提供了这些功能就可以了。

到了实际需要用的时候,如果记不起具体的函数名和调用方式,也没有关系。只要你还大体上记得有这个函数能实现xx功能,就可以通过查询的方式很快找到并使用它。

读/写字符

总体上而言,对char类型字符数据进行读/写,我们更推荐使用以下两个标准库函数。即getchar和putchar函数!

putchar函数用于将单个字符打印到显示器上,其函数的声明如下:

int putchar(int c);

其作用是向标准输出缓冲区(stdout)写入一个字符,并且直接返回该字符作为返回值,如果写入过程发生意外错误,该函数会返回EOF(End of File)

这就意味着此字符会暂存缓冲区,直到缓冲区满、遇到换行符或者程序结束等场景时,缓冲区刷新,内容才会显示在屏幕上。

getchar函数用于从键盘录入中读取读单个字符,其函数声明如下:

int getchar(void);

此函数调用不需要任何参数,它会从**标准输入缓冲区(stdin)**中读取一个字符,并且把读取到的字符作为返回值返回。如果已经读取到流(数据源)的末尾或者发生意外错误,此函数会返回EOF。

注意:

  1. getchar函数在读字符时,仍然不会自动跳过空白字符。
  2. putchar 和 getchar 函数是针对char类型字符处理专门优化的函数,它们的效率要高于 printf 和 scanf,处理char数据请优先使用它们。
一个重要的惯用法

在编程中,惯用法是指一种常用的编码模式或表达方式,类似汉语中的成语,是程序员前辈们的智慧结晶。惯用法往往代表了处理某种场景的最佳手段。

在C语言中,我们经常需要从流中读取数据,比如getchar函数就是从标准输入流(stdin, 缓冲区)中读取数据,为了确保将流中的数据读完,我们经常会使用以下惯用法:

int ch;   // 使用int来存储getchar的返回值,因为它可能返回EOF
int count = 0;  // 统计总字符数
while ((ch = getchar()) != '\n') {  // 读取到流中的换行符就结束循环
  count++;
  printf("第%d次读到的字符是%c\n", count, ch);
}
printf("输入的字符一共有%d个\n", count);

注意:

  1. EOF在大多数平台下实际上就等于-1,但它并不是任何字符,只是一个表示流已经读到末尾的标记、特殊值。所以不应该用char类型作为getchar()函数的返回值,一般我们习惯使用int类型作为函数返回值。
  2. ch = getchar()赋值运算符组成的赋值表达式是有值的,就是"="右边的表达式的取值,也就是getchar()函数的返回值。
  3. 在读文件流时,只需要把"!="的条件从换行符改成EOF,就是一个通过循环将文件数据读取完的惯用法。(后面文件流会讲)

实际上,不仅是C语言,C++和Java也有几乎一模一样的惯用法,只不过是结束读取流的条件不同。

//ch = getchar()
//主要作用:表达式要返回的值,这就是getchar的返回值
 //副作用:赋值,改变了ch变量的取值

`

  count++;
  printf("第%d次读到的字符是%c\n", count, ch);
}
printf("输入的字符一共有%d个\n", count);

注意:

  1. EOF在大多数平台下实际上就等于-1,但它并不是任何字符,只是一个表示流已经读到末尾的标记、特殊值。所以不应该用char类型作为getchar()函数的返回值,一般我们习惯使用int类型作为函数返回值。
  2. ch = getchar()赋值运算符组成的赋值表达式是有值的,就是"="右边的表达式的取值,也就是getchar()函数的返回值。
  3. 在读文件流时,只需要把"!="的条件从换行符改成EOF,就是一个通过循环将文件数据读取完的惯用法。(后面文件流会讲)

实际上,不仅是C语言,C++和Java也有几乎一模一样的惯用法,只不过是结束读取流的条件不同。

//ch = getchar()
//主要作用:表达式要返回的值,这就是getchar的返回值
 //副作用:赋值,改变了ch变量的取值
  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如是我闻艺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值