文章目录记录一下学习C语言的过程,分享好的学习资料。
- 学习资料
- 学习环境
- 数据类型
- 变量命名规范
- 基本数据类型
- 进制表示
- 类型转换
- 类型大小
- 生存周期&作用域
- 类型定义
- 表达式
- 控制语句
- 数组
- 一维数组
- 二维数组
- 变长数组 感觉是个假的:)
- 字符串
- 初始化
- 常用函数
- 函数
- 函数声明 & 函数定义
- 作用范围
- 变长参数
- 指针
- 指针运算
- 指针与数组
- 指针与多维数组
- 内存分布
- int** 与 int (*)[3]的关系
- 二维数组参数传递
- 宏
- 条件编译
- 多文件
- 结构、联合和枚举
- 结构
- 联合
- 枚举
- 内存管理
- IO
- 转译字符
- 输出格式
- 控制台IO
- 格式化输入:scanf
- 格式化输出:printf
- 字符IO:getchar、putchar
- 标准流和重定向
- 文件IO
| 名称 | 类型 | 描述 | 链接 |
|---|---|---|---|
| C Primer Plus | 书籍 | 面面俱到,但内容较多,适合系统学习 | 百度云,u46k |
| C 语言程序设计 | 书籍 | 简练一些 | 链接 |
本次学习主要也是参考以上资源。
学习环境学习阶段推荐使用:小熊猫devc
.c文件经过预处理、编译、汇编和链接后才能生成可执行程序,而集成环境(小熊猫devc)可以自动帮我们处理这些过程。
- 预处理:指删除注释、替换宏和处理预处理指令(#include)
- 编译:指将预处理后的c语言代码翻译成汇编码
- 汇编:指将汇编码翻译成机器码(占位置,并不知道去哪里调用外部的函数和全局变量)
- 链接:根据机器码链接,找到具体位置
可以使用大小写字母、下划线和数字,但第一个字符不能是数字,感觉是为了方便编译器设计:)
此外,不能使用关键字和保留字作为变量名。
- 常量:全部大写,用下划线分隔
- 变量名:全部小写,用下划线分隔
- 整型:short、int、long、long long
- 浮点型:float、double、long double
- 字符型:char
| 进制 | 数值 | 十进制数 | IO符号 |
|---|---|---|---|
| 二进制 | 0b11 | 3 | - |
| 八进制 | 011 | 9 | %o |
| 十六进制 | 0x11 | 17 | %x |
- 隐式类型转换(向大范围转换不会有问题,向小范围转换会丢失)
- 算术转换:提升到表达式中的最高级
- 赋值转换、参数传递和返回值:浮点数到整数丢弃小数部分;大范围⇒小范围可能溢出;
- 显示类型转换:用于防止溢出((long long)1000000*1000000)
sizeof value; // 作用在变量上 sizeof(char); // 作用在类型上要加括号生存周期&作用域
- 局部变量:生存周期为代码块
- 静态变量:始终存在,但对于代码块外不可见
- 全局变量:始终存在,整个.c可见
typedef int ID; // 定义新的类型表达式
运算符:+、 -、 *、 /、 %
比较算法:<、 >、<=、 >=、==、!=
逻辑算符:||、 &&
自增、自减:a++ ++a a-- --a
三元组运算符:表达式1? 表达式2: 表达式3
短路运算:对于逻辑运算符 && 和 ||有短路的效果
逗号表达式:常用于声明变量
if体系、swtich、for、while和do-while
数组 一维数组#define N 1000 // 配合数组使用
// 不初始化
int a[N];
// 初始化
int a[N] = {0}; // 不足的部分会用0填充
int a[] = {0, 1, 2, 3}; // 由{}中的值确定数组大小
二维数组
#define N 100
// 不初始化
int a[N][N];
// 初始化
int a[2][2] = {{0, 1}, {2, 3}};
int a[2][2] = {0, 1, 2, 3}; // 一维的形式初始化(不推荐)
int a[2][2] = {0} // 不足的补0(推荐这个清0)
变长数组 感觉是个假的:)
int n = 10; int a[n]; // 可以用变量来指定长度字符串 初始化
char s[] = "ni hao"; // 相当于 char s[] = {'n', 'i', ' ', 'h', 'a', 'o', ' '},可以修改
char *s = "ni hai"; // s指向字符串常量,不能修改
常用函数
int strlen(char *s); int flag = strcmp(char *s0, char *s1); strcpy(char *s0, char *s1); strcat(char *s0, char *s1);函数 函数声明 & 函数定义
// 函数声明
int add(int a, int b);
// 函数定义
int add(int a, int b)
{
return a + b;
}
作用范围
当函数被static修饰时,说明该函数只能被本.c文件访问,可以减少名字污染。
变长参数需要编译器配合,单参数列表为
#include指针#include void show(int num, ...){ va_list ap; va_start(ap, num); for(int i=0; i show(4, 1, 2, 3, 4); return 0; }
保存变量的地址,可用于构建链表和树,此外还能在函数调用时减少参数传递开销。
int a = 1;
// 定义和赋值
int *p = &a;
// 可以通过指针赋值 ⇒ a = 2
*p = 2;
// 指针大小(64位--> 8)
printf("size: %dn", sizeof (int*));
指针运算
指针能进行加、减运算(包括自增、自减),编译器在编译时将改变运算的数值:对于char *,编译器判断出一个单位是一个字节,所以+1在编译后还是+1;而对于int *,编译器判断出一个单位是四个字节,所以+1在编译成汇编码后变成了+4。
char *p_c = 0;
int *p_i = 0;
printf("%pn", p_c); // 0
printf("%pn", p_c+1); // 1
printf("%pn", p_i); // 0
printf("%pn", p_i+1); // 4
指针与数组
数组相当于指向不可变的初始化了内存区域的指针。
a[1] 相当于 *(a+1),为了准确获得+1的便宜,必须要知道该指针的类型。
int a[10];
int *p = a;
// 指针可以按数组的格式使用
a[0] = 1;
printf("%dn", *p);
printf("%dn", p[0]); // p[0] == *(p+0) == *(a+0) == a[0]
// 不可以改变数组的指向
int b[10];
a = b; // wrong, 所以a就相当于 int * const q = (int*)malloc(sizeof(int) * 10);
指针与多维数组
想要准确理解多维数组和指针的关系,就要理清楚数组在内存中的分布以及int** 与 int(*)[3]的关系。
内存分布- python的list是多级引用的结构,顶层的list指向子层的list;
- C中的数组在内存中则是线性的。
如上图,虽然a、a[0]指向相同的地方,但两种的数据类型不同。a是int(*)[3],a[0]是int *,在提取数值时:**a == *a[0],从这个角度看 int** 好像就等于 int(*)[3]。
int a[2][3]; int (*p)[3] = a; // a的数据类型是int (*)[3],所以接收时也要是该类型 int *q = a[0]; // 而a[0]则是int *的类型 int b = **p; // int (*)[3]是二级指针的一种特例,所以取值时要** int c = *q; // q就只需要一个*
然而,两者还是有质的不同的,int**是真正的二级指针,指向int的指针;而int (*)[3]多少就有点冒牌的意味了。
int a[2][3] = {0, 1, 2, 3, 4, 5};
int (*p)[3] = a;
printf("%pn", p); // 000000000066FE00
printf("%pn", *p); // 000000000066FE00,与上面的一致,即*p并没有去访问p指向的内存
printf("%pn", **p); // 0000000000000000,这里才真正去做了访问
观察汇编码可以发现,在数值方面,*p和p的结果是完全一致的,可以说*p只是做了数据类型的改变,没有按照一般指针的形式访问数据。这点与前面提到的a与a[0]地址值相同呼应(a[0] == *(a+0)==*a)。在面对指向数组的指针时,*的作用是改变类型,而随着类型的改变,地址+1所增加的偏移也随之改变。具体来说就是a[1][1]中第一个1和第二个1带来的偏移量不同。而对于int**来说,则是真正通过两次寻址。
由于上述不同,当二维数组作为参数时,不能简单的形参定义为int **,而实际传递的又是int(*)[3]之类的二维数组。
#include宏void show(int **a, int n, int m) { int (*p)[m] = (int (*)[m]) a; for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ printf("%d ", p[i][j]); } putchar('n'); } } int main() { int a[2][3] = {0, 1, 2, 3, 4, 5}; show((int **)a, 2, 3); int b[3][4] = {{0, 1, 2, 3},{4, 5, 6, 7}, {9, 8, 7, 6}}; show((int**)b, 3, 4); return 0; }
#define PI 3.14 // 定义常数 #define MAX(x, y) ((x)>(y)?x:y) // 宏函数: 参数列表必须紧紧贴合宏名条件编译
// 适合找debug #define DEBUG 1 #if DEBUG // do #endif // 适合debug #define DEBUG #if defined DEBUG // do #endif // 简写 #ifdef DEBUG // do #endif // 取反(适合排除互相包含) #ifndef __MATH_H__ #define __MATH_H__ 暴露给外界的全局变量和函数声明 extern float PI; int add(int a, int b); #endif多文件
掌握共享全局变量和函数的原理和实际操作。
// people.c:有一个全局变量 num_people和函数greet()
int num_people = 0;
void greet()
{
printf("Hello, boy!n");
}
// main.c:需要访问num_people并且使用greet
extern int num_people; // 声明变量类型,即可使用该变量
void greet(); // 声明函数原型,即可调用该函数
结构、联合和枚举
结构
// 定义了struct student的类型
struct student {
int id;
char name[32];
};
struct student stu1; // 定义变量:不能省略struct
// 用typedf化简
typedef struct student {
int id;
char name[32];
} Student;
Student stu1 = {12, "fyy"}; // 定义变量
printf("%dn", stu1.id); // 通过.访问
Values * p = &stu1;
printf("%dn", p->id); 对于指针,通过->访问, 就相当于: (*p).id
联合
typedef union values{
int int_v;
double double_v;
}Values;
Values v; // 注意初始化方式
v.double_v = 1.234; // 通过.访问
枚举
typdef enum size {
BIG, MIDDLE, SMALL
} Size;
Size = BIG; // 直接用
内存管理
// 不会堆申请区域初始化 int *arr = (int *) malloc(sizeof(int) * 100); // 申请 free(arr); // 释放 // 对申请区域初始化 int *arr = (int *) calloc(100, sizeof(int)); // 初始化为0 // 调整大小 arr = (int*) realloc(arr, sizeof(int) * 200); // 调整大小IO 转译字符
常用转译字符: t制表、n换行
空白符:t、 n、 空格
输出%:需要连续的%%
输出格式:根据%[标志][最小字段宽度][.精度][转换说明符]控制
- 常见标志:-表示左对齐
- 最小字段宽度:表示输出至少占多少宽度(不足补空格)
- 精度:表示至少有多少位(不足补0),整数考虑所有位数;浮点数只考虑小数位数
- 转换说明符
| 类型 | scanf | printf |
|---|---|---|
| int | %d | %d |
| long | %ld | %ld |
| long long | %lld | %lld |
| float | %f | %f |
| double | %lf | %f or %lf |
| char | %c | %c |
输入匹配:将控制台的输入理解成流(拼接成串),根据输入格式串去匹配。
- 匹配转换说明符时:刚开始遇到空白符时,跳过直到遇上非空白符,当遇到匹配不上的符号时,将该符号留在流中,结束匹配。
- 匹配空白符:可以匹配输入中任意数量的空白符(可以是0个)。
- 匹配非空白符:直接匹配,如果匹配不上直接退出,并将其留给下一次匹配。
scanf("%d%d", &a, &b);
// 输入: 10nn20n ⇒ a = 10, b = 20,剩余输入串: n
// 输入:10-20n ⇒ a = 10, b = -20,剩余输入串:n
// 输入:10a20n ⇒ a = 10, b 原始值,剩余输入串:a20n
scanf("%d %d", &a, &b);
// 输入: 10nn20n ⇒ a = 10, b = 20,剩余输入串: n
// 输入:10-20n ⇒ a = 10, b = -20,剩余输入串:n
// 输入:10a20n ⇒ a = 10, b = 原始值,剩余输入串:a20n
scanf("%da%d", &a, &b);
// 输入: 10n20n ⇒ a = 10, b = 原始值,剩余输入串: n20n
// 输入:10a-20n ⇒ a = 10, b = -20,剩余输入串:n
// 输入:10b20n ⇒ a = 10, b = 原始值,剩余输入串:b20n
scanf("/%d", &a);
// 输入:/1234n ⇒ a = 1234, 剩余输入串:n
// 输入:nn/1324n ⇒ a = 原始值, 剩余输入串:nn/1324n
scanf(" /%d", &a);
// 输入:/1234n ⇒ a = 1234, 剩余输入串:n
// 输入:nn/1324n ⇒ a = 原始值, 剩余输入串:n
格式化输出:printf
printf("%dn", 520); // 根据输出格式,直接输出即可。
字符IO:getchar、putchar
char c = getchar(); // 输入一个字符 putchar(c); // 输出一个字符标准流和重定向
C有三个标准流,不需要对其打开关闭即可使用。
| 名称 | 含义 | 设备 |
|---|---|---|
| stdin | 标准输入 | 键盘 |
| stdout | 标准输出 | 屏幕 |
| stderr | 标准错误 | 屏幕 |
当用cmd运行exe文件时,可以将文件重定向为上述的三个标准流。在cmd中输入./run.exe
- 打开文件:FILE * fp = fopen(path, mode);
- 关闭文件:fclose(fp);
- 向文件输出:fprintf(fp, 格式化串, ...);
- 由文件输入:fscanf(fp, 格式化串, ...);
- 到结尾:feof(fp);
#include#include int main() { // 写文件 FILE *fp; if((fp = fopen("test.txt", "w")) == NULL){ printf("fail to open!n"); exit(1); } for(int i=0; i < 10; i++) fprintf(fp, "%dn", i); fclose(fp); // 读取文件 if((fp = fopen("test.txt", "r")) == NULL){ printf("fail to open!n"); exit(1); } int num; while(fscanf(fp, "%d", &num) != EOF){ printf("%dn", num); } fclose(fp); return 0; }



