记录一下学习C语言的过程,分享好的学习资料。
文章目录
学习资料
名称 | 类型 | 描述 | 链接 |
---|---|---|---|
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', '\0'},可以修改
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 <stdio.h>
#include <stdarg.h>
void show(int num, ...){
va_list ap;
va_start(ap, num);
for(int i=0; i<num; i++)
printf("%d\n", va_arg(ap, int));
va_end(ap);
}
int main()
{
show(4, 1, 2, 3, 4);
return 0;
}
指针
保存变量的地址,可用于构建链表和树,此外还能在函数调用时减少参数传递开销。
int a = 1;
// 定义和赋值
int *p = &a;
// 可以通过指针赋值 ⇒ a = 2
*p = 2;
// 指针大小(64位--> 8)
printf("size: %d\n", sizeof (int*));
指针运算
指针能进行加、减运算(包括自增、自减),编译器在编译时将改变运算的数值:对于char *
,编译器判断出一个单位是一个字节,所以+1在编译后还是+1;而对于int *
,编译器判断出一个单位是四个字节,所以+1在编译成汇编码后变成了+4。
char *p_c = 0;
int *p_i = 0;
/**************************************************
指针中携带有指向目标的大小信息:+1对于不同的指针结果不同
**************************************************/
printf("%p\n", p_c); // 0
printf("%p\n", p_c+1); // 1
printf("%p\n", p_i); // 0
printf("%p\n", p_i+1); // 4
指针与数组
数组相当于指向不可变的初始化了内存区域的指针。
a[1]
相当于 *(a+1)
,为了准确获得+1
的便宜,必须要知道该指针的类型。
int a[10];
int *p = a;
// 指针可以按数组的格式使用
a[0] = 1;
printf("%d\n", *p);
printf("%d\n", 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中的数组在内存中则是线性的。
int** 与 int (*)[3]的关系
如上图,虽然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("%p\n", p); // 000000000066FE00
printf("%p\n", *p); // 000000000066FE00,与上面的一致,即*p并没有去访问p指向的内存
printf("%p\n", **p); // 0000000000000000,这里才真正去做了访问
观察汇编码可以发现,在数值方面,*p
和p
的结果是完全一致的,可以说*p
只是做了数据类型的改变,没有按照一般指针的形式访问数据。这点与前面提到的a
与a[0]
地址值相同呼应(a[0] == *(a+0)==*a
)。在面对指向数组的指针时,*
的作用是改变类型,而随着类型的改变,地址+1
所增加的偏移也随之改变。具体来说就是a[1][1]
中第一个1和第二个1带来的偏移量不同。而对于int**
来说,则是真正通过两次寻址。
由于上述不同,当二维数组作为参数时,不能简单的形参定义为int **
,而实际传递的又是int(*)[3]
之类的二维数组。
二维数组参数传递
#include <stdio.h>
/********************************************************************************
这里的 int** a表示传递进来的参数是一个int** 指针,这样最接近 int(*)[]的类型,然而即使将
其转换成 void *,也是可以的,各种指针的大小都是一致的,不同的只是其附带的类型信息,然而通
过强制类型转换,可以消除该影响。
********************************************************************************/
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) // 宏函数: 参数列表必须紧紧贴合宏名
/************************
特殊的宏
__FILE__: 函数名
__LINE__: 第几行
************************/
条件编译
// 适合找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(); // 声明函数原型,即可调用该函数
/*
声明变量类型和声明函数原型主要是为了在单文件编译时提供类型检查,要实际获取变量地址需要链接操作
- extern 关键字是为了和变量定义区别;而声明函数原型时就不需要了
- 在每个.c文件中声明类型不方便,所以有了存放声明的.h文,通过include拷贝.h文件信息
*/
结构、联合和枚举
结构
// 定义了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("%d\n", stu1.id); // 通过.访问
Values * p = &stu1;
printf("%d\n", 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 |
控制台IO
格式化输入:scanf
输入匹配:将控制台的输入理解成流(拼接成串),根据输入格式串去匹配。
- 匹配转换说明符时:刚开始遇到空白符时,跳过直到遇上非空白符,当遇到匹配不上的符号时,将该符号留在流中,结束匹配。
- 匹配空白符:可以匹配输入中任意数量的空白符(可以是0个)。
- 匹配非空白符:直接匹配,如果匹配不上直接退出,并将其留给下一次匹配。
scanf("%d%d", &a, &b);
// 输入: 10\n\n20\n ⇒ a = 10, b = 20,剩余输入串: \n
// 输入:10-20\n ⇒ a = 10, b = -20,剩余输入串:\n
// 输入:10a20\n ⇒ a = 10, b 原始值,剩余输入串:a20\n
scanf("%d %d", &a, &b);
// 输入: 10\n\n20\n ⇒ a = 10, b = 20,剩余输入串: \n
// 输入:10-20\n ⇒ a = 10, b = -20,剩余输入串:\n
// 输入:10a20\n ⇒ a = 10, b = 原始值,剩余输入串:a20\n
scanf("%da%d", &a, &b);
// 输入: 10\n20\n ⇒ a = 10, b = 原始值,剩余输入串: \n20\n
// 输入:10a-20\n ⇒ a = 10, b = -20,剩余输入串:\n
// 输入:10b20\n ⇒ a = 10, b = 原始值,剩余输入串:b20\n
scanf("/%d", &a);
// 输入:/1234\n ⇒ a = 1234, 剩余输入串:\n
// 输入:\n\n/1324\n ⇒ a = 原始值, 剩余输入串:\n\n/1324\n
scanf(" /%d", &a);
// 输入:/1234\n ⇒ a = 1234, 剩余输入串:\n
// 输入:\n\n/1324\n ⇒ a = 原始值, 剩余输入串:\n
格式化输出:printf
printf("%d\n", 520); // 根据输出格式,直接输出即可。
字符IO:getchar、putchar
char c = getchar(); // 输入一个字符
putchar(c); // 输出一个字符
标准流和重定向
C有三个标准流,不需要对其打开关闭即可使用。
名称 | 含义 | 设备 |
---|---|---|
stdin | 标准输入 | 键盘 |
stdout | 标准输出 | 屏幕 |
stderr | 标准错误 | 屏幕 |
当用cmd运行exe
文件时,可以将文件重定向为上述的三个标准流。在cmd中输入./run.exe <in.txt >out.txt
,则在运行时将in.txt中的数据当作键盘输入,out.txt当作屏幕输出。
文件IO
- 打开文件:
FILE * fp = fopen(path, mode);
- 关闭文件:
fclose(fp);
- 向文件输出:
fprintf(fp, 格式化串, ...);
- 由文件输入:
fscanf(fp, 格式化串, ...);
- 到结尾:
feof(fp);
#include <stdio.h>
#include <stdlib.h>
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, "%d\n", 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("%d\n", num);
}
fclose(fp);
return 0;
}