3.11笔记

1. 从源代码生成可执行程序包含哪几个步骤,请用自己的话描述每个步骤的作用。

生成可执行程序通常包括以下几个步骤:

  1. 编写源代码:首先,需要编写源代码,即实现所需功能的程序代码。源代码可以使用文本编辑器或集成开发环境(IDE)等工具编写。

  2. 预处理器处理:在编译之前,源代码会被预处理器处理。预处理器主要负责处理以 # 开头的预处理指令,如宏定义、文件包含等。预处理器会对源代码进行一些文本替换和处理,生成经过预处理后的代码。

  3. 编译器编译:预处理后的代码会被编译器编译成机器码。编译器会将高级语言代码转换成特定平台的汇编语言代码或直接转换成机器码。编译器会检查代码语法和语义错误,并生成目标文件。

  4. 链接器链接:编译生成的目标文件需要通过链接器进行链接,生成最终的可执行程序。链接器会将目标文件与所需的库文件进行链接,解决外部函数和变量的引用关系,生成可执行文件。

  5. 生成可执行程序:最后,链接器生成的可执行程序包含了所有必要的代码和数据,可以在相应的操作系统上运行。

这些步骤共同完成了从源代码到可执行程序的转换过程,使得计算机可以理解和执行我们编写的程序。

2. 预处理阶段会执行预处理指令,哪几种预处理指令?编写带参数的宏 (宏函数) 应该注意哪些事项?宏函数优于普通函数的地方在哪里?

预处理阶段会执行预处理指令,常见的预处理指令包括:

  1. 宏定义:使用 #define 定义宏,将一个标识符替换为一个值或一段代码。

  2. 文件包含:使用 #include 将其他文件的内容包含到当前文件中。

  3. 条件编译:使用 #if#ifdef#ifndef#elif#else#endif 控制代码的编译条件。

  4. 行连接:使用 \ 将多行代码连接成一行。

  5. 注释:使用 ///* */ 进行注释。

编写带参数的宏(宏函数)时,应注意以下事项:

  • 参数在宏定义中应该用括号括起来,以避免参数被周围的表达式所影响。
  • 参数应该在宏定义中使用括号来确保参数传递的正确性,特别是对于参数的表达式。
  • 避免在宏定义中使用带副作用的表达式,以免出现意外的行为。

宏函数优于普通函数的地方在于:

  • 宏函数在编译时展开,避免了函数调用的开销,提高了程序的执行效率。
  • 宏函数可以接受可变数量的参数,提高了灵活性。
  • 宏函数可以实现一些编译时计算,避免了运行时的计算开销。

但宏函数也有一些缺点,比如可读性较差,容易出现错误,并且不易调试。因此,在使用宏函数时应该权衡利弊,谨慎使用。

3. 什么是进程?进程的虚拟内存空间包含哪几部分?

进程是操作系统中的一个概念,它是程序执行时的一个实例。进程是一个具有一定独立功能的程序关于某个数据集合上的一次运行活动,它是操作系统进行资源分配和调度的一个独立单位。

进程的虚拟内存空间包括以下几部分:

  1. 代码段(Text Segment):存储程序的执行代码,通常是只读的,存放CPU执行的机器指令。

  2. 数据段(Data Segment):存储程序中已初始化的全局变量和静态变量,包括全局变量、静态变量和常量。

  3. 堆(Heap):用于存放动态分配的内存,即运行时可变大小的内存块,由程序员分配和释放。

  4. 栈(Stack):用于存放函数的参数值、局部变量等,以及函数调用时的返回地址和相关信息,栈是向低地址方向生长的,由系统自动分配和释放。

4. 下面是 Hello World 程序对应的汇编代码 (Intel 格式),请阅读下面的汇编代码,看是否能找到和 C 语言代码的对应关系。

// helloworld.c
#include <stdio.h>

int main(void) {
    printf("Hello world.\n");
    return 0;
}
.LC0:
        .string "Hello world."
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        mov     eax, 0
        pop     rbp
        ret

这段汇编代码是将 C 语言的 Hello World 程序编译成的对应汇编代码。下面是汇编代码和 C 语言代码的对应关系:

  1. .LC0 标签对应 C 语言中字符串 “Hello world.”。
  2. main: 标签对应 C 语言中的 main 函数。
  3. push rbpmov rbp, rsp 分别对应 C 语言中的函数开头的压栈和设置栈基址。
  4. mov edi, OFFSET FLAT:.LC0 将字符串地址传递给 puts 函数的参数 edi
  5. call puts 调用 puts 函数打印字符串。
  6. mov eax, 0 将返回值设置为 0。
  7. pop rbpret 分别对应 C 语言中的函数结束的出栈和返回。

因此,这段汇编代码对应了 C 语言中的 Hello World 程序的功能实现。

5. 生成可执行程序过程中发生的错误可分为编译错误和链接错误。请用代码演示一下编译错误和链接错误

编译错误和链接错误是在生成可执行程序过程中可能遇到的两种常见错误类型。下面分别用代码演示这两种错误。

  1. 编译错误:编译错误指的是在编译源代码时发现的错误,通常是语法错误、类型错误等。例如,下面的示例代码中演示了一个常见的编译错误,即缺少分号的语法错误。
#include <stdio.h>

int main(void) {
    printf("Hello world.\n")
    return 0;
}

在这个示例中,printf 语句缺少了分号,这是一个编译错误。编译器会提示类似以下的错误信息:

test.c: In function ‘main’:
test.c:5:5: error: expected ‘;’ before ‘return’
     return 0;
     ^~~~~~
     ;
  1. 链接错误:链接错误指的是在将目标文件链接成可执行程序时发现的错误,通常是找不到函数定义、重复定义等。例如,下面的示例代码演示了一个链接错误,即找不到 add 函数的定义。
#include <stdio.h>

int add(int a, int b);

int main(void) {
    int result = add(3, 4);
    printf("Result: %d\n", result);
    return 0;
}

在这个示例中,add 函数的声明没有对应的定义,因此在链接时会出现类似以下的错误信息:

/tmp/ccn4oEqH.o: In function `main':
test.c:(.text+0x1f): undefined reference to `add'
collect2: error: ld returned 1 exit status

这两个示例演示了编译错误和链接错误的情况,编程时应该注意避免这些常见错误。

6. 程序运行过程中也会出现错误,这时候就需要我们自己调试程序 (Debug)了。请描述一下在 VS 中下面各个按钮的作用:F5、shift + F5、F10、F11、shift + F11、继续。

在 Visual Studio 中,调试程序时常用的按钮和对应的作用如下:

  1. F5 (开始调试):启动调试器并开始调试当前项目。如果项目尚未编译,则会先编译项目。

  2. Shift + F5 (停止调试):停止当前正在运行的调试会话,关闭调试器。

  3. F10 (逐过程跟踪):执行当前行的代码,并且不进入函数体内部。如果当前行是一个函数调用,将会执行整个函数。

  4. F11 (逐语句跟踪):执行当前行的代码,并进入函数体内部。如果当前行是一个函数调用,将会跳转到该函数的第一行。

  5. Shift + F11 (逐出跟踪):退出当前函数的调试,跳回到调用该函数的地方。

  6. 继续 (Continue):在遇到断点时继续执行程序,直到遇到下一个断点或程序结束。

这些按钮可以帮助开发人员在调试过程中控制程序的执行流程,逐步跟踪代码的执行情况,帮助定位和解决程序中的错误。

7. 调试程序的关键在于打断点,请描述一下如何在 VS 中打断点?

在 Visual Studio 中,可以通过以下步骤在代码中设置断点:

  1. 打开要调试的项目,并打开需要调试的源文件。

  2. 在源文件中找到想要设置断点的代码行。

  3. 单击代码行的行号区域(或按下 F9 键),在该行前面会出现一个红色圆圈,表示已经设置了一个断点。

  4. 如果需要,可以在设置断点后对其进行进一步配置。例如,右键单击断点,可以设置条件断点、日志断点等。

  5. 启动调试器(按下 F5 键),程序将在设置的断点处停止执行,等待进一步调试操作。

通过设置断点,可以在程序执行过程中暂停程序,查看变量的值、程序状态等信息,有助于更快地定位和解决程序中的问题。


补充
在 Linux 上使用 GDB(GNU 调试器)来调试程序并设置断点。以下是在 Linux 上使用 GDB 设置断点的基本步骤:

  1. 安装 GDB:如果系统中没有安装 GDB,可以使用包管理器进行安装。例如,在 Ubuntu 上可以使用以下命令安装:

    sudo apt-get install gdb
    
  2. 编译程序:使用 -g 参数编译程序,以包含调试信息。例如,假设要调试的程序为 example.c,可以使用以下命令编译:

    gcc -g -o example example.c
    
  3. 启动 GDB:在终端中使用以下命令启动 GDB,并指定要调试的可执行文件:

    gdb ./example
    
  4. 设置断点:在 GDB 中,可以使用 break 命令设置断点。例如,要在 main 函数的第一行设置断点,可以输入:

    break main
    

    这将在程序执行到 main 函数时暂停执行。

  5. 运行程序:在 GDB 中使用 run 命令来运行程序。程序将在设置的断点处停止执行。

  6. 查看变量和执行状态:在程序暂停时,可以使用 print 命令查看变量的值,使用 step 命令逐行执行代码,使用 continue 命令继续执行程序直到下一个断点。

  7. 退出 GDB:在调试完成后,可以使用 quit 命令退出 GDB。

通过在 Linux 上使用 GDB 设置断点和调试程序,可以有效地查找和解决程序中的问题。

8.判断 f 和 g 两个函数属于哪种情况: f = O ( g ) , f = Ω ( g ) , 还是  f = Θ ( g ) f = O(g), f = \Omega(g), \text{还是 } f=\Theta(g) f=O(g),f=Ω(g),还是 f=Θ(g)

时间复杂度和空间复杂度是衡量算法性能的重要指标。时间复杂度描述了算法的运行时间随输入规模增长的趋势,而空间复杂度描述了算法的额外空间需求随输入规模增长的趋势。

根据定义:

  • 如果 f ( n ) = O ( g ( n ) ) f(n) = O(g(n)) f(n)=O(g(n)),则表示存在一个常数 c > 0 c > 0 c>0 和一个 n 0 n_0 n0,使得对所有 n > n 0 n > n_0 n>n0,有 0 ≤ f ( n ) ≤ c ⋅ g ( n ) 0 \leq f(n) \leq c \cdot g(n) 0f(n)cg(n)
  • 如果 f ( n ) = Ω ( g ( n ) ) f(n) = \Omega(g(n)) f(n)=Ω(g(n)),则表示存在一个常数 c > 0 c > 0 c>0 和一个 n 0 n_0 n0,使得对所有 n > n 0 n > n_0 n>n0,有 0 ≤ c ⋅ g ( n ) ≤ f ( n ) 0 \leq c \cdot g(n) \leq f(n) 0cg(n)f(n)
  • 如果 f ( n ) = Θ ( g ( n ) ) f(n) = \Theta(g(n)) f(n)=Θ(g(n)),则表示存在两个常数 c 1 > 0 c_1 > 0 c1>0 c 2 > 0 c_2 > 0 c2>0,以及一个 n 0 n_0 n0,使得对所有 n > n 0 n > n_0 n>n0,有 0 ≤ c 1 ⋅ g ( n ) ≤ f ( n ) ≤ c 2 ⋅ g ( n ) 0 \leq c_1 \cdot g(n) \leq f(n) \leq c_2 \cdot g(n) 0c1g(n)f(n)c2g(n)

现在我们来判断给定的函数 f f f g g g 分别属于哪种情况:

  1. f ( n ) = n − 100 , g ( n ) = n − 200 f(n) = n - 100, g(n) = n - 200 f(n)=n100,g(n)=n200

    f ( n ) = O ( g ( n ) ) f(n) = O(g(n)) f(n)=O(g(n)),因为 n − 100 ≤ n − 200 n - 100 \leq n - 200 n100n200 对于所有 n ≥ 200 n \geq 200 n200 成立。

  2. f ( n ) = n , g ( n ) = n 2 / 3 f(n) = \sqrt{n}, g(n) = n^{2/3} f(n)=n ,g(n)=n2/3

    f ( n ) = O ( g ( n ) ) f(n) = O(g(n)) f(n)=O(g(n)),因为 n ≤ n 2 / 3 \sqrt{n} \leq n^{2/3} n n2/3 对于所有 n ≥ 1 n \geq 1 n1 成立。

  3. f ( n ) = n log ⁡ n , g ( n ) = 10 n log ⁡ 10 n f(n) = n \log n, g(n) = 10n \log 10n f(n)=nlogn,g(n)=10nlog10n

    f ( n ) = Θ ( g ( n ) ) f(n) = \Theta(g(n)) f(n)=Θ(g(n)),因为 n log ⁡ n n \log n nlogn 10 n log ⁡ 10 n 10n \log 10n 10nlog10n 是同阶的。

  4. f ( n ) = log ⁡ 2 n , g ( n ) = log ⁡ 3 n f(n) = \log 2n, g(n) = \log 3n f(n)=log2n,g(n)=log3n

    f ( n ) = O ( g ( n ) ) f(n) = O(g(n)) f(n)=O(g(n)),因为 log ⁡ 2 n ≤ log ⁡ 3 n \log 2n \leq \log 3n log2nlog3n 对于所有 n ≥ 2 n \geq 2 n2 成立。

  5. f ( n ) = 2 n , g ( n ) = 2 n + 1 f(n) = 2^n, g(n) = 2^{n+1} f(n)=2n,g(n)=2n+1

    f ( n ) = O ( g ( n ) ) f(n) = O(g(n)) f(n)=O(g(n)),因为 2 n ≤ 2 n + 1 2^n \leq 2^{n+1} 2n2n+1 对于所有 n ≥ 0 n \geq 0 n0 成立。

  6. f ( n ) = ∑ i = 1 n i k , g ( n ) = n k + 1 f(n) = \sum_{i=1}^{n} i^k, g(n) = n^{k+1} f(n)=i=1nik,g(n)=nk+1

    f ( n ) = Θ ( g ( n ) ) f(n) = \Theta(g(n)) f(n)=Θ(g(n)),因为 ∑ i = 1 n i k \sum_{i=1}^{n} i^k i=1nik n k + 1 n^{k+1} nk+1 是同阶的。

所以,对于上述函数 f f f g g g

  • f ( n ) = O ( g ( n ) ) f(n) = O(g(n)) f(n)=O(g(n)) 的情况有:1、2、3、4、5;
  • f ( n ) = Ω ( g ( n ) ) f(n) = \Omega(g(n)) f(n)=Ω(g(n)) 的情况有:3、5;
  • f ( n ) = Θ ( g ( n ) ) f(n) = \Theta(g(n)) f(n)=Θ(g(n)) 的情况有:3、6。

9. 证明:c 是一个正实数,且 f(n) = 1 + c + c2 + … + cn ,则有:

在这里插入图片描述

我们需要分别证明三种情况:

  1. c < 1 c < 1 c<1 时, f ( n ) = Θ ( 1 ) f(n) = \Theta(1) f(n)=Θ(1)
  2. c = 1 c = 1 c=1 时, f ( n ) = Θ ( n ) f(n) = \Theta(n) f(n)=Θ(n)
  3. c > 1 c > 1 c>1 时, f ( n ) = Θ ( c n ) f(n) = \Theta(c^n) f(n)=Θ(cn)

情况 1: c < 1 c < 1 c<1

我们知道等比数列的前 n 项和公式为:
S n = a ( 1 − c n ) 1 − c S_n = \frac{a(1 - c^n)}{1 - c} Sn=1ca(1cn)
其中,a 是首项,c 是公比。

对于 f ( n ) = 1 + c + c 2 + … + c n f(n) = 1 + c + c^2 + \ldots + c^n f(n)=1+c+c2++cn,首项 a=1,公比 c,所以:
f ( n ) = 1 − c n + 1 1 − c f(n) = \frac{1 - c^{n+1}}{1 - c} f(n)=1c1cn+1
由于 c < 1 c < 1 c<1,所以 c n → 0 c^n \to 0 cn0 n → ∞ n \to \infty n。因此, f ( n ) f(n) f(n) 在 n 趋近无穷大时趋近于一个常数,即 f ( n ) = Θ ( 1 ) f(n) = \Theta(1) f(n)=Θ(1)

情况 2: c = 1 c = 1 c=1

c = 1 c = 1 c=1 时, f ( n ) = 1 + 1 + 1 2 + … + 1 n = n + 1 f(n) = 1 + 1 + 1^2 + \ldots + 1^n = n + 1 f(n)=1+1+12++1n=n+1。显然, f ( n ) = Θ ( n ) f(n) = \Theta(n) f(n)=Θ(n)

情况 3: c > 1 c > 1 c>1

对于 f ( n ) = 1 + c + c 2 + … + c n f(n) = 1 + c + c^2 + \ldots + c^n f(n)=1+c+c2++cn,我们可以将其表示为几何级数的形式:
f ( n ) = c n + 1 − 1 c − 1 f(n) = \frac{c^{n+1} - 1}{c - 1} f(n)=c1cn+11
由于 c > 1 c > 1 c>1,所以 c n c^n cn 随着 n 的增大增长非常迅速,因此 f ( n ) = Θ ( c n ) f(n) = \Theta(c^n) f(n)=Θ(cn)

综上所述,根据不同的 c 值, f ( n ) f(n) f(n) 分别是 Θ ( 1 ) \Theta(1) Θ(1) Θ ( n ) \Theta(n) Θ(n) Θ ( c n ) \Theta(c^n) Θ(cn)

10. 什么是变量?变量的本质是什么?变量的三要素是什么?

变量是计算机编程中用于存储和表示数据的一种概念。在程序中,变量可以被赋予不同的值,并且可以在程序执行过程中改变其值。

变量的本质是在内存中分配的一块存储空间,用于存储数据。当我们定义一个变量时,计算机会分配一块内存空间来存储该变量的值,并为该内存空间分配一个标识符(即变量名),以便在程序中引用该变量。

变量的三要素是:

  1. 变量名:用于标识变量的名称,是程序中引用变量的方式。

  2. 数据类型:用于定义变量可以存储的数据类型,例如整数、浮点数、字符等。

  3. 存储地址:变量在内存中的存储位置,程序通过存储地址来访问变量的值。

总的来说,变量是程序中用于存储和表示数据的一种抽象概念,它包含了变量名、数据类型和存储地址这三个要素。

11. 什么是常量?请举例说明一下 define N 5const int N = 5 有何不同?

常量是程序中固定不变的值,它们在程序执行过程中不会改变。常量可以是数值、字符、字符串等。在C语言中,常量可以使用 #define 宏定义或 const 关键字定义。

#define N 5 是一种宏定义方式,它在预处理阶段被替换为 5。在编译时,所有的 N 都会被替换为 5。这种方式没有类型检查,只是简单的文本替换。例如:

#define N 5
int x = N; // 在编译时被替换为 int x = 5;

const int N = 5 则是使用 const 关键字定义的常量,它会在内存中分配空间存储值 5。这种方式有类型检查,变量 N 的类型为 const int。例如:

const int N = 5;
int x = N; // x 的值为 5,N 不会被替换为具体的值

因此,主要区别在于 #define 是在预处理阶段进行简单文本替换,而 const 是在编译时进行类型检查并分配内存空间。使用 const 更安全,因为它具有类型检查的特性。

12. 什么是标识符?取一个好的标识符应该遵循怎样的规范?

标识符是在程序中用来标识变量、函数、类、对象或其他用户定义的项目的名称。在大多数编程语言中,标识符可以包含字母、数字和下划线,但必须以字母或下划线开头,且不能是语言的关键字或保留字。

取一个好的标识符应该遵循以下规范:

  1. 具有描述性:标识符应该能够清晰地描述其代表的变量、函数或对象的用途和含义,以便于其他人阅读和理解代码。

  2. 简洁明了:标识符应该尽可能简洁明了,避免过长或过于复杂的命名。

  3. 遵循命名规范:不同编程语言有不同的命名规范,如驼峰命名法、下划线命名法等,应根据语言的规范选择合适的命名方式。

  4. 避免使用缩写:除非是广为接受的缩写,否则应尽量避免使用缩写,以提高代码的可读性。

  5. 避免使用保留字:标识符不能是语言的关键字或保留字,否则会导致编译器报错。

  6. 一致性:在整个项目中应该保持标识符的一致性,以便于统一管理和维护。

例如,在命名一个代表学生姓名的变量时,一个好的标识符可以是 studentName(驼峰命名法),而不是 sn(过于简短)或 student_name(不符合命名规范)。

13. printf 函数的格式是怎样的?它的作用是什么?

printf 函数是C语言和C++语言中用于格式化输出的函数,其格式如下:

int printf(const char *format, ...);

其中,format 是格式控制字符串,用来指定输出的格式,可以包含普通字符和格式说明符;... 表示可变参数,用来传递要输出的数据。

printf 函数的作用是按照指定的格式将数据输出到标准输出设备(通常是屏幕)。它可以输出各种类型的数据,如整数、浮点数、字符、字符串等,并且可以通过格式控制字符串控制输出的格式,如指定输出的宽度、精度、对齐方式等。

printf 函数在C语言和C++语言中被广泛应用于输出调试信息、结果展示等场景。

14. printf 函数的格式串中可以包含普通字符和转换说明,printf 对这两者的处理有何不同?转换说明的作用是什么?

printf 函数的格式串中,普通字符和转换说明具有不同的处理方式:

  1. 普通字符:普通字符直接输出到标准输出,不进行任何格式转换。例如,格式串中的普通字符 A 会直接输出为 A

  2. 转换说明:转换说明以 % 开头,用来指定输出的格式。转换说明包括转换说明字符和可选的宽度、精度、长度修饰符等。例如,%d 用来输出十进制整数,%f 用来输出浮点数,%s 用来输出字符串等。

转换说明的作用是告诉 printf 函数如何格式化输出相应的数据。通过转换说明,可以指定输出数据的类型、宽度、精度等信息,以及控制输出的对齐方式等。例如,%d 指定输出整数,%f 指定输出浮点数,%s 指定输出字符串。

转换说明在格式化输出中起到非常重要的作用,它使得 printf 函数可以根据需要输出各种类型的数据,并且可以控制输出的格式,使得输出结果更加清晰、易读。

15. scanf 函数的格式是怎样的?它的作用是什么?

scanf 函数是C语言和C++语言中用于输入的函数,其格式如下:

int scanf(const char *format, ...);

其中,format 是格式控制字符串,用来指定输入的格式,可以包含转换说明符;... 表示可变参数,用来接收输入的数据。

scanf 函数的作用是从标准输入设备(通常是键盘)读取数据,并按照指定的格式进行解析和赋值。它可以读取各种类型的数据,如整数、浮点数、字符、字符串等,并且可以通过格式控制字符串控制输入的格式,如跳过空白字符、指定最大输入字符数等。

scanf 函数在C语言和C++语言中被广泛应用于从用户输入获取数据的场景。

16. scanf 函数的格式串中可以包含普通字符、空白字符和转换说明,scanf 对这三者的处理有何不同?

scanf 函数的格式串中,普通字符、空白字符和转换说明具有不同的处理方式:

  1. 普通字符:普通字符在格式串中需要与输入流中的字符完全匹配。scanf 会尝试从输入流中读取字符,并与格式串中的普通字符进行匹配。如果匹配成功,则继续读取下一个字符;如果匹配失败,则停止读取,并将失败的字符留在输入流中。

  2. 空白字符:空白字符(空格、制表符、换行符等)在格式串中可以用来表示跳过输入流中的空白字符。在 scanf 中,空白字符会导致 scanf 在读取输入时跳过所有空白字符,直到遇到非空白字符为止。

  3. 转换说明:转换说明以 % 开头,用来指定要读取的数据类型。scanf 会根据转换说明从输入流中读取相应类型的数据,并将其存储到指定的变量中。转换说明可以指定整数、浮点数、字符串等类型的输入数据格式。

因此,在 scanf 函数中,普通字符用于匹配输入流中的字符,空白字符用于跳过空白字符,转换说明用于读取指定类型的数据,并将其存储到变量中。三者结合使用可以实现对输入数据的灵活处理。

17. 写一个程序,实现分数相加。用户以分子/分母的形式输入分数,程序打印相加后的结果。

#include <stdio.h>

// 求两个整数的最大公约数
int gcd(int a, int b) {
    if (b == 0) {
        return a;
    } else {
        return gcd(b, a % b);
    }
}

// 将分数化简为最简形式
void simplify(int *numerator, int *denominator) {
    int common_divisor = gcd(*numerator, *denominator);
    *numerator /= common_divisor;
    *denominator /= common_divisor;
}

int main() {
    int numerator1, denominator1, numerator2, denominator2;
    printf("Enter the first fraction (numerator/denominator): ");
    scanf("%d/%d", &numerator1, &denominator1);
    
    printf("Enter the second fraction (numerator/denominator): ");
    scanf("%d/%d", &numerator2, &denominator2);
    
    // 计算分数相加后的分子和分母
    int result_numerator = numerator1 * denominator2 + numerator2 * denominator1;
    int result_denominator = denominator1 * denominator2;
    
    // 化简分数为最简形式
    simplify(&result_numerator, &result_denominator);
    
    // 打印结果
    printf("The sum is: %d/%d\n", result_numerator, result_denominator);
    
    return 0;
}

18. C 语言是如何对无符号整数编码的?如果对有符号整数编码的?请简述一下补码的原理。

在C语言中,无符号整数(unsigned int)和有符号整数(signed int)都是以二进制补码的形式进行编码的。

  1. 无符号整数编码:无符号整数只能表示非负数,因此它的编码范围是从0到2n-1(n为整数位数)。例如,一个8位的无符号整数可以表示的范围是从0到255(28-1)。

  2. 有符号整数编码:有符号整数可以表示正数、负数和0。对于有符号整数,采用二进制补码表示法。其中,正数的二进制补码与无符号整数相同,而负数的二进制补码是其对应正数的二进制反码加1。例如,一个8位的有符号整数的范围是从-128到127。

补码的原理是,用一个数的补码来表示这个数的负数,这样可以将加法和减法统一为加法操作。补码的计算方法如下:

  1. 对于正数,其补码就是它本身的二进制表示。
  2. 对于负数,先计算其绝对值的二进制表示(取反加1),然后再取反得到其补码。

补码的优势在于可以方便地进行加法运算,同时只需要一个零表示正零和负零。

19. C 语言是如何对浮点数编码的?请计算 double 类型能表示的最大正数和最小正数。

C语言对浮点数采用IEEE 754标准进行编码。在IEEE 754标准中,双精度浮点数(double)使用64位来表示,其中包括1位符号位(S)、11位指数位(E)和52位尾数位(M)。其编码格式如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

双精度浮点数能表示的最大正数和最小正数分别是:

  • 最大正数: 1.111...1 × 2 1023 1.111...1 \times 2^{1023} 1.111...1×21023,即尾数部分全为1,指数部分为最大值1023,其值约为 1.7976931348623157 × 1 0 308 1.7976931348623157 \times 10^{308} 1.7976931348623157×10308
  • 最小正数: 1.0 × 2 − 1022 1.0 \times 2^{-1022} 1.0×21022,即尾数部分为0,指数部分为1,其值约为 2.2250738585072014 × 1 0 − 308 2.2250738585072014 \times 10^{-308} 2.2250738585072014×10308

这里的计算过程是将尾数部分全部置为1或全部置为0,然后根据指数部分的取值计算得到实际的十进制值。

20. C 语言是如何对字符数据编码的?如何表示空白字符和控制字符?转义序列有哪几种表示方式?

在C语言中,字符数据使用ASCII编码(或扩展的ASCII编码)进行表示。ASCII编码使用7位来表示字符,可以表示128个字符,包括标准的打印字符、控制字符和扩展字符。

空白字符包括空格符(space)、制表符(tab)、换行符(newline)、回车符(carriage return)等。这些字符在ASCII编码中有对应的整数值,分别为32(空格符)、9(制表符)、10(换行符)、13(回车符)。

控制字符是ASCII编码中的一部分,表示一些无法直接显示的字符,如退格符(backspace)、换页符(form feed)、垂直制表符(vertical tab)等。这些字符在ASCII编码中有对应的整数值,例如退格符对应的整数值为8。

转义序列是一种表示无法直接输入的字符的方法,以反斜杠(\)开头,后面跟着一个或多个字符。常见的转义序列包括:

  • \n:换行符(newline)
  • \t:制表符(tab)
  • \r:回车符(carriage return)
  • \\:反斜杠符号(backslash)
  • \':单引号(single quote)
  • \":双引号(double quote)

这些转义序列可以在字符串常量中使用,用于表示特殊字符。例如,printf 函数中的格式控制字符串中就可以使用转义序列来表示换行、制表等特殊字符。

21. 为什么要发生类型转换?什么情况下会发生类型转换?如何进行类型转换 (有哪几种方式)?

类型转换是将一个数据类型的值转换为另一个数据类型的过程。在编程中,类型转换通常发生在以下情况下:

  1. 表达式中的类型不匹配:当表达式中的操作数具有不同的数据类型时,编译器会根据一定的规则进行类型转换,以使它们具有相同的数据类型。

  2. 赋值操作:将一个数据类型的值赋给另一个数据类型的变量时,可能需要进行类型转换。

  3. 函数调用:在函数调用中,如果函数的参数类型与调用时提供的参数类型不匹配,需要进行类型转换。

  4. 强制类型转换:有时候需要显式地将一个数据类型转换为另一个数据类型,这种情况下使用强制类型转换。

C语言中有以下几种类型转换方式:

  1. 隐式类型转换:在表达式中,如果操作数的类型不匹配,编译器会自动进行隐式类型转换。例如,将一个整数赋值给一个浮点数变量时,编译器会将整数隐式转换为浮点数。

  2. 强制类型转换:也称为显式类型转换,使用强制转换运算符(())来显式地将一个数据类型转换为另一个数据类型。例如,(float) 5 将整数 5 转换为浮点数。

  3. 整数提升:当对较小的整数类型进行运算时,它们会被自动提升为较大的整数类型。例如,char 类型会被提升为 int 类型。

  4. 截断:将一个较大的整数类型赋值给一个较小的整数类型时,会发生截断,即高位的位数会被丢弃。

  5. 自动转换:在一些情况下,编译器会自动进行类型转换,例如将一个整数赋值给一个指针类型时,会自动将整数转换为指针类型。

类型转换在编程中是一个常见的操作,但需要注意避免精度丢失和数据溢出等问题。

22. 隐式类型转换很方便,为何我们还需要强制类型转换?请举例说明。

虽然隐式类型转换很方便,但有时候我们仍然需要使用强制类型转换,主要是为了明确表达我们的意图,并确保程序的正确性。以下是一些需要使用强制类型转换的情况:

  1. 提高可读性:强制类型转换可以使代码更加清晰,明确地表达出数据类型的转换,有助于他人理解代码的意图。

    double d = 3.14;
    int i = (int)d; // 明确表示将浮点数转换为整数
    
  2. 避免精度丢失:在将高精度数据类型转换为低精度数据类型时,可能会发生精度丢失。通过强制类型转换,我们可以明确知道可能发生精度丢失,并决定是否要进行转换。

    double d = 3.999;
    int i = (int)d; // i 的值为3,明确知道小数部分会被丢弃
    
  3. 强制改变数据类型:有时候我们需要将一个数据类型转换为另一个完全不同的数据类型,这时候就需要使用强制类型转换。

    char c = 'A';
    int i = (int)c; // 将字符转换为对应的ASCII码值
    
  4. 调用函数时的参数匹配:有些函数需要特定类型的参数,如果实际参数类型不匹配,就需要使用强制类型转换来匹配函数的参数类型。

    double d = 3.14;
    printf("%d\n", (int)d); // printf函数需要int类型参数,所以需要将d转换为int
    

总之,虽然隐式类型转换提供了方便,但在一些情况下,使用强制类型转换可以更加明确地表达我们的意图,并确保程序的正确性。

23. 我们可以使用 #definetypedef 给类型起别名,它们之间的区别是什么?为什么有了宏定义,C 语言还提供 typedef 呢?

#definetypedef 都可以用来给类型起别名,但它们之间有一些区别。

  1. #define

    • #define 是C语言中的预处理指令,用来定义宏。通过#define定义的宏可以替换为指定的文本内容,包括类型名。
    • #define 定义的别名是简单的文本替换,不会进行类型检查。因此,可以用来定义任何类型的别名,包括基本类型、复杂类型和表达式。
    • 由于是简单文本替换,可能会导致代码可读性较差,容易出错。
  2. typedef

    • typedef 是C语言中的关键字,用来为类型定义新的名称。它不是预处理指令,而是在编译阶段处理的。
    • typedef 只能用来定义类型别名,不能用来定义常量或表达式。
    • typedef 定义的别名是真正的类型别名,会进行类型检查,提高了代码的可读性和安全性。

为什么有了宏定义,C语言还提供 typedef 呢?主要是因为 typedef 定义的类型别名更接近于真正的类型,可以提高代码的可读性和可维护性。宏定义虽然灵活,但容易造成代码可读性差,容易出错。因此,在需要定义类型别名的情况下,推荐使用 typedef

24. 如何使用sizeof 运算符?它是在哪一个阶段进行计算的?

sizeof 运算符用于计算数据类型或变量的大小(以字节为单位)。在使用时,可以有两种形式:

  1. sizeof(type):计算指定类型的大小。
  2. sizeof(expression):计算表达式的结果类型的大小。

sizeof 运算符是在编译时计算的,它返回的是数据类型或变量在内存中占用的字节数。因此,sizeof 运算符在运行时并不会对表达式进行实际的计算,而是根据数据类型或变量的类型来计算大小。

例如,以下是一些使用 sizeof 运算符的示例:

int a;
printf("Size of int: %zu\n", sizeof(int)); // 输出 int 类型的大小
printf("Size of a: %zu\n", sizeof(a)); // 输出变量 a 的大小

struct {
    int x;
    char y;
} s;

printf("Size of struct: %zu\n", sizeof(s)); // 输出结构体的大小
printf("Size of expression: %zu\n", sizeof(s.x + s.y)); // 输出表达式 s.x + s.y 的大小

在这些示例中,sizeof 运算符根据指定的类型或变量来计算其大小,并在编译时进行计算,而不是在运行时。

25. 什么是表达式?运算符的作用是什么?运算符有哪两个属性?(请同学们自己熟悉优先级表)

表达式是由运算符和操作数组成的符号集合,表示一个计算过程或值。在C语言中,表达式可以是一个简单的变量、常量,也可以是一个复杂的组合,包括算术表达式、逻辑表达式、关系表达式等。

运算符是用来进行特定操作的符号,作用是对操作数进行计算或操作,生成一个结果。C语言中的运算符包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等,每个运算符都有特定的功能和优先级。

运算符有两个重要的属性:

  1. 优先级:不同的运算符有不同的优先级,优先级高的运算符先于优先级低的运算符进行计算。如果表达式中有多个运算符,可以根据优先级确定计算顺序。需要注意的是,如果表达式中有括号,则括号中的部分先于其他部分进行计算。

  2. 结合性:结合性指的是当表达式中有多个具有相同优先级的运算符时,运算符的计算顺序是从左到右还是从右到左。不同运算符的结合性可能不同,例如赋值运算符 = 是右结合的,而逻辑与 && 是左结合的。

了解运算符的优先级和结合性可以帮助我们正确理解和书写复杂的表达式,避免出现不必要的错误。

26. 算术运算符有哪些注意事项?我们能像下面这样判断整数的奇偶性吗?为什么?

bool is_odd(int n) {
    return n % 2 == 1;
}

算术运算符有以下几个注意事项:

  1. 除数不能为0:除法运算符(/)中除数不能为0,否则会产生错误。

  2. 整数除法:两个整数相除时,结果仍为整数,小数部分会被截断。例如,5 / 2 的结果为2而不是2.5。

  3. 取模运算符:取模运算符(%)返回除法的余数。例如,5 % 2 的结果为1。

关于判断整数的奇偶性,通常可以使用取模运算符来判断。对于上面的代码,可以稍作修改来正确判断整数的奇偶性:

bool is_odd(int n) {
    return n % 2 != 0;
}

这段代码中,n % 2 的结果如果不等于0,则表示 n 为奇数,因为奇数除以2的余数一定是1。这样的判断是正确的,因为C语言中规定取模运算符的结果在除数为正数时与被除数的符号相同。

27. ++ii++ 的区别是什么?类似 i = i++a[i] = b[i++] 这样的表达式存在什么问题?

++ii++ 都是递增操作符,用于增加变量 i 的值。它们的区别在于递增操作的时机不同:

  • ++i 是前缀递增操作符,表示先将 i 的值加1,然后再使用加1后的值。即先加后用。
  • i++ 是后缀递增操作符,表示先使用 i 的值,然后再将 i 的值加1。即先用后加。

例如,考虑以下代码:

int i = 5;
int a = ++i; // i 先加1,然后赋值给 a,此时 a 的值为 6,i 的值也为 6
int b = i++; // i 的值先赋值给 b,然后再加1,此时 b 的值为 6,i 的值为 7

关于类似 i = i++a[i] = b[i++] 这样的表达式,存在问题的原因是这些表达式中包含了对同一个变量的多次修改或者同时对同一个变量进行读取和修改操作,导致结果是未定义的。在C语言中,对同一个变量进行多次修改或者同时读取和修改的行为是不确定的,可能会导致程序出现奇怪的行为,所以应该尽量避免这种写法。

28. 什么是逻辑运算符的短路原则?这样设计有什么好处?

逻辑运算符的短路原则是指,在逻辑运算中,如果逻辑表达式的结果可以确定的情况下,后面的表达式将不会被执行。主要有两种情况:

  1. 对于逻辑与运算符(&&),如果第一个操作数为假(false),则不会再计算第二个操作数,整个表达式的值就是假(false)。
  2. 对于逻辑或运算符(||),如果第一个操作数为真(true),则不会再计算第二个操作数,整个表达式的值就是真(true)。

这样设计的好处在于可以提高程序的性能和效率。如果在逻辑表达式中,后续的操作不影响整个表达式的结果,就可以避免不必要的计算,节省了时间和资源。

例如,考虑以下代码:

int a = 5;
int b = 0;

if (a > 0 && b / a > 0) {
    // ...
}

在这个例子中,如果逻辑与运算符不具有短路原则,那么在计算 b / a 时会发生除以零的错误。但由于逻辑与运算符具有短路原则,当 a 的值大于0时,就不会计算后面的表达式,避免了除以零的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不好,商鞅要跑

谢谢咖啡

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

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

打赏作者

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

抵扣说明:

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

余额充值