Notes

类型

隐式类型转换

  • 水平方向:自动转换
  • 垂直方向:低->高(短->长,有符号->无符号)

整型提升

有符号数与无符号数混合运算

运算符

优先级

函数

静态局部变量

double calc(int n) {
	static int count = 1;
	++count;
	return n == 1 ? 1 : calc(n - 1) * n;
}
  • 静态局部变量如果在定义时没有赋初值,系统将自动赋 0(类似与全局变量)。
  • 赋初值只在函数第一次调用时起作用,以后都不会自动修改他的值。
  • 静态变量与全局变量一样,生命周期贯穿整个程序执行过程。

数组

静态全局变量与外部变量

// file1.c
int x;
// file2.c
extern int x; // 声明在file1.c中定义的变量x,而不是重新定义
static int y; // 定义仅在file2.c中使用的变量y

指针

动态内存分配

指针数组

指针数组的应用

  1. 给字符串 / 二维数组排序
  2. 二维数组,但是第二维的空间大小不固定,需要动态分配

指针数组 vs 二维数组

char ccolor[ ][7] = {"red", "blue", "yellow", "green", "black"};
const char *pcolor[ ] = {"red", "blue", "yellow", "green", "black"};
// ccolor[0] = "white" 不可以但 pcolor[0] = "white" 可以
// ccolor[0][0] = 'x' 可以但 pcolor[0][0] = 'x' 不可以

命令行参数

int main(int argc, char **argv) {
  ...
}

这里 argc 表示接受的参数个数,argv 表示接受的参数列表(注意指令名本身也算一个参数,存储在 argv[0]

二级指针

T **p 定义一个指向 T * 类型的指针(指向另一个指针的指针)。

使用二级指针

char color[][7] = {"red", "blue", "yellow", "green", "black"};
char **pc = color;
*pc <=> color[0]
*pc+i <=> color[0]+i
*(pc+i) <=> color[i]
**pc <=> *color[0] <=> color[0][0]
**(pc+i) <=> *color[i] <=> color[i][0]

指针数组 vs 数组指针

指针数组vs数组指针
char *color1[5]
一个数组,包含 5 个 char*
([] 的优先级高于 *char (*color2)[5]
一个指针,指向 5 个 char 元素的数组

注意这里的声明方式比较反直觉

如何理解 [] 的优先级比 * 高?char *color1[5] 说明了 color1 一定是一个数组,char (*color2)[5] 说明了 color2 一定是一个指针。

int a[3][4];
a <=> &a[0] int (*)[4]
a+i <=> &a[i] int (*)[4]
*(a+i) <=> a[i] <=> &a[i][0] int *
*(a+i)+j <=> a[i]+j <=> &a[i][j] int *
**(a+i) <=> *a[i] <=> a[i][0] <=> int
*(*(a+i)+j) <=> *(a[i]+j) <=> a[i][j] <=> int

将二维数组作为函数参数

编译器改写规则:将(最外层的)数组改写为指针。

int a[10][20];
void func(int **a);       // CE: int (*)[20]与int **类型不匹配
void func(int a[][]);     // CE: 数组不能包含此类型(int[])的元素
void func(int a[10][]);   // CE: 数组不能包含此类型(int[])的元素
void func(int a[][20]);   // √
void func(int a[10][20]); // √
void func(int (*a)[20]);  // √

指向函数的指针

定义方式:返回值类型 (*指针名)(参数类型表)

调用方式:指针名(参数表)(*指针名)(参数表)

文件操作

打开文件

  • 使用 FILE *fopen(char *文件名, char *模式) 打开文件,返回对应的文件指针。如果返回值为 NULL 说明文件打开失败。
  • 使用 int fclose(FILE *文件指针) 关闭文件,如果返回值非 0 说明文件关闭失败。

文件读写

  • 使用 char fgetc(FILE* file) 读取单个字符
  • 使用 void fputc(char c, FILE* file) 写入单个字符
// 示例代码:一直读入字符直到文件结束
// 这样写法的前提是文件本身没有存储EOF字符,否则应用feof函数判断
while ((ch = fgetc(file1)) != EOF) {
	fputc(ch, file2);
}
  • 使用 char *fgets(char *s, int n, FILE* file) 读取字符串
    • 连续读取至多 n-1 个字符并将其保存到 s 中。
    • 如果遇到换行符(会保留换行符)或 EOF(不保留)则截止,并自动往末尾添加 \0
    • 执行成功时返回读取到的字符串,否则返回 NULL。
  • 使用 char *fputs(char *s, FILE *file) 写入字符串

还可以使用 fscanffprintf,第一个参数是文件指针,后面参数和 scanfprintf 的使用方法相同。

二进制文件读写

  • 使用 fread(void* 数据块(首)地址, size_t 单个数据块字节数, size_t 要读入的数据块个数, FILE* 文件指针) 来从以二进制模式打开的文件中读取数据。
  • 使用 fwrite(void* 数据块(首)地址, size_t 单个数据块字节数, size_t 要读入的数据块个数, FILE* 文件指针) 来往以二进制模式打开的文件中写入数据。

其他相关函数

  • 使用 int feof(FILE* 文件指针) 判断文件尾是否已经被尝试读取。(如 fgetc 已经返回 EOF 而不是将要返回 EOF)
  • 使用 rewind(FILE* 文件指针) 使得文件读写指针恢复到打开文件时所指向的位置(字节数)。
  • 使用 fseek(FILE* 文件指针, size_t 偏移量, int 起始位置) 来移动读写指针,其中起始位置可选值为 0(SEEK_SET)、1(SEEK_CUR)、2(SEEK_END) 分别对应文件头、当前位置、文件尾。
  • 使用 ftell(FILE* 文件指针) 获取文件指针的位置(字节数),即相对于文件开头的偏移量。

多文件编程

文件包含 #include

Usage 1:#include <filename> 仅检索库文件夹
Usage 2:#include "filename" 检索库文件夹和当前文件夹

在编译期编译器会把要包含的源代码直接拼接到对应为止。

头文件保护

在编写头文件时,常用宏来避免头文件被重复包含。否则会引起链接期的编译错误。

// student.h
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <stdio.h>
struct student {
	int num;
	char name[10];
	int computer, english, math;
	double average;
}
...
#endif

文件模块间的通信

static int x; // 静态全局变量,只能在当前文件中访问
extern int y; // 外部全局变量,可以在其他文件中访问
static void f(); // 静态全局函数,只能在当前文件中访问
extern void g(); // 外部全局函数,可以在其他文件中访问

一些零散知识点

面经

  1. C 语言的优点:结构化语言、简洁紧凑方便灵活、易于移植、处理能力强、效率高。
  2. C 语言的缺点:数据类型检查不严格、表达式有二义性、不检测数据越界、运算符的优先级与结合性相对复杂

格式化输入输出函数

类型格式字符串说明
整型%d
%o
%x
%u
有符号十进制整数
无符号八进制整数
无符号十六进制整数
无符号十进制整数
浮点型%f
%e
%g
以小数形式输出,默认小数位数为 6 位
以指数形式输出,数字部分默认为 6 位
以优化的小数或指数形式输出(去掉无意义的零后所占宽度较小的)
字符型%c
%s
输出字符
输出字符串

abs() 函数与 C++ 不同

  • C 中在 stdlib.h 中提供了整数的 abs(),在 math.h 中提供了浮点的 fabs()。所以如果在 C 中调用 abs(-0.1) 会被隐式类型转换。
  • 而 C++ 中还在 cmath 中重载了支持浮点数的 abs() 函数,不存在这个问题。

一些细节

多个 if-else 的匹配问题

省略长度的数组声明方式

初始化列表

变量的存储类型

对于局部变量存储类型的缺省值是 auto,对于全局变量存储的缺省值是 extern

变量类型初始化作用范围生命周期
函数的局部变量 auto register随机值函数内部函数调用时分配,函数调用结束时自动回收
函数的静态全局变量 static默认 0,函数第一次调用时赋初值函数内部程序开始直到程序结束
全局变量 extern默认 0,main 函数被调用前赋初值文件内部,可以跨文件使用程序开始直到程序结束
静态全局变量 static默认 0,main 函数被调用前赋初值文件内部,不能跨文件使用程序开始直到程序结束

一些坑点

  1. 按照课本要求,可变长的数组为语法错误(int n=5; int a[n]; ×)。
  2. 带参数的宏没给参数加括号可能带来隐患。
  3. typedef 不是简单的类型替换。如 typedef int *int_p; int_p p1, p2; 等价于 int *p1, *p2;

Comments