浙大翁恺《C语言程序设计》课程笔记
1.1 计算机与编程语言
- 设计算法 -> 编写程序 -> 计算机执行
- 程序执行的两种方式
1.解释:借助一个程序(解释器),那个程序能试图理解你的程序,然后按照你的要求让计算机执行
2.编译:借助一个程序(编译器),把你的程序翻译成机器语言,然后让计算机执行
编程语言本身没有解释型和编译型之分,任何一段程序既可以解释执行,也可以编译执行
1.2 C语言
- C语言标准:C89 -> C95 -> C99 -> C11 -> C17
2.1 变量
- 整数的运算结果还是整数
- 整数int的输入输出:printf("%d",xxx);scanf("%d",&xxx);
- 浮点数double的输入输出:printf("%f",xxx);scanf("%lf",&xxx);
2.2 表达式
- 程序就是数据加计算
- 赋值运算也是有结果的:a=6表示a被赋予了6这个值,同时也表示a=6这个表达式本身的运算结果为6
- 赋值运算是自右向左结合的:a=b=6等价于a=(b=6),a和b的值都为6
3.1 判断
- 关系运算的结果只有0和1
- 所有关系运算符的优先级低于算数运算符,但是高于赋值运算符
3.2 分支
- if语句只要求()里的值是零或非零
- switch语句的控制表达式只能是整数型的结果
4.1 循环
- 循环体内要有改变条件的机会
4.2 循环应用
- x%n的结果是[0,n-1]的一个整数
- 对整数做%10的操作,就得到了它的个位数;对整数做/10的操作,就去掉了它的个位数
5.3 循环应用
- 求两个数的最大公约数:辗转相除法(两个整数的最大公约数等于其中较小那个数和两数相除的余数的最大公约数)
6.1 数据类型
- C语言类型
整数:char/short/int/long/long long
浮点数:float/double/long double
逻辑:bool
指针
自定义类型
- 所表达的数的范围:char<short<int<float<double
- 内存中所占据的大小:1个字节到16个字节
- 内存中的表达形式:二进制补码(整数)/编码(浮点数)
- sizeof:计算某种类型或某个变量在内存中占据的字节数
- int就是用来表达一个寄存器的大小,也叫一个字长
- unsigned的作用:做纯二进制的运算,主要是为了移位
- 整数的输入输出:只有int/long long
%d:int
%u:unsigned
%ld:long long
%lu:unsigned long long
- 浮点数的输入输出
float:scanf("%f") printf("%f")
double:scanf("%lf") printf("%f")
- 当char/short传给printf时,会将char/short转换成int
- float类型字面量要加f/F,否则就是double类型
- char类型的变量可以直接当作整数处理
- 当运算符两边类型不一致时,会自动转成较大的类型:char -> short -> int -> long -> long long int -> float -> double
- 对于printf,任何小于int的类型都会被转成int;float会转成double
- 对于scanf,不会自动转换
- bool类型实际上还是int,可以当作int来计算,只能通过int来输入输出
- 强制类型转换不会改变原来的变量,会计算得到一个新的变量
6.2 其他运算:逻辑/条件/逗号
- 优先级:算数运算符 > 关系运算符 > 逻辑运算符
- 逗号运算符用来连接两个表达式,并以其右边的表达式的值作为逗号运算的结果
7.2 函数的参数和变量
- 函数原型:函数头+分号(函数原型的作用就是告诉编译器这个函数的名称/参数/返回类型)
- 调用函数时给的值和参数类型不匹配是C语言传统上最大的漏洞
- C语言在调用函数时,永远只能传值给函数
- 函数的每次运行都会产生一个独立的变量空间,在这个空间里的变量是函数的这次运行所独有的,称作本地变量;定义在函数内部的变量和函数的参数都是本地变量
8.1 数组
- 定义数组:<类型> 变量名称[元素数量];
- 数组中的所有元素具有相同的数据类型
- 数组一旦创建,不能改变大小
- 数组中的元素在内存中是连续依次排列的
- 编译器不会检查数组下标是否越界
8.2 数组运算
- 如何计算数组大小:sizeof(arr)/sizeof(arr[0])
- 不能将一个数组变量arr1赋值给另一个数组变量arr2;只能使用遍历
- 数组作为函数的参数时,往往必须再用另一个参数来传入数组的大小,因为数组作为函数的参数时:不能在[]中给出数组的大小;不能再利用sizeof计算数组的元素个数
- 二维数组初始化时,列数必须指定,行数可以省略
- 多维数组作为函数参数时,只能省略第一维的大小,后面的维度大小必须指定:void f1(int a[][5]);
9.1 指针
- &是运算符,作用是获取变量的地址,操作数必须是变量
- 地址的大小(所占字节数)是否与int相同取决于编译器;不要直接把地址当作整数
- 输出地址使用%p
- 对于数组变量arr来说,使用%p输出&arr和arr的结果是一样的
- 指针类型的变量就是保存地址的变量,指针变量的值是内存的地址
- int* p和int *p都是表达p是一个指针
- 普通变量的值是实际的值;指针变量的值是具有实际值的变量的地址
- *用来访问指针的值所表示的地址上的变量
- 指针变量未被赋值前,不要使用*访问指针变量指向的实际变量
- 函数参数表中的数组实际上是指针
- 数组变量是特殊的指针,因此无需用&取地址:int arr[10]; int *p = arr;
- int const * p表示不能通过*p=xxx改变指针所指向的变量的值;int * const p表示p不能再指向其他变量
- 数组变量可以看作const的指针,所以不能被赋值:int arr[] <==> int *const arr
- const int arr[]表明数组的每个元素都是const int,因此必须通过初始化进行赋值 const int arr[] = {1,2,3,4,5,6};
- 为了保护数组不被函数修改,可以设置数组参数为const:void f(const int[] arr)
9.2 指针运算
- 指针变量+1等价于指针变量所指的地址+sizeof(指针变量所指的变量的类型)(如果指针不是指向一片连续分配的空间,则这种运算没有意义)
- 两个指针相减的结果是两个地址的差/sizeof(指针变量所指的变量的类型),而不是直接将两个地址进行相减
- *p++:取出p所指的那个数据,然后再让p指向下一个位置
- 指针不应该具有0值,NULL表示0地址
- 无论指向什么类型,所有指针的大小都是一样的,因为都是地址:sizeof(int *)==sizeof(char *)==sizeof(double *);但是指向不同类型的指针是不能相互赋值的
- void*表示不知道指向什么类型的指针
- 动态内存分配:int* a = (int*)malloc(number * sizeof(int)); free(a);
- malloc的单位是字节,返回类型是void*
- free必须是申请的空间的首地址,不能是变化后的地址
- 不要在函数中动态申请内存然后,因为调用者会忘记free
10.1 字符串
- 字符串是指以整数0结尾的一串字符(0和'\0'是一样的,但和'0'不同)
- 0标志字符串的结束,但它不是字符串的一部分,计算字符串长度的时候不包含这个0
- 字符串以字符数组的形式存在,以数组或指针的形式访问
- 字符串变量就是字符数组变量:char *str = "Hello"; char str[] = "Hello"; char str[10] = "Hello";
- "hello"是一个字符串常量,会被编译器变成一个字符数组放在某处,这个数组的长度是6,因为结尾还有一个表示结束的'\0'
- 字符串常量放在内存中的一块只读区域,所以当一个指针被初始化为指向一个字符串常量时,无法通过这个指针去改变字符串常量的内容:char* s = "hello"; s[0]='a'会使程序崩溃
- 如果要修改字符串,应该使用数组:char s[] = "hello";
- 如果要构造一个字符串,使用数组;如果要处理一个字符串,使用指针
- 字符串可以表达为char的形式;char不一定是字符串,它可以指向单个字符,也可以指向一个字符数组,只有当它指向字符数组并且有结尾的0时,才能说它指向了一个字符串
- scanf读入一个字符串,直到碰到空格/tab/回车为止
- 常见错误:误以为char*类型就是字符串类型
- 空字符串:char str[10] = ""(这时str[0]=='\0')
- char str[] = ""(这个数组的长度只有1)
- 字符串数组:char a[][10]表示a是一个二维数组,a[x]是一个char[10];而char *a[]表示a是一个一维数组,a[x]是一个char*
10.2 字符串函数
- int putchar(int c); 向标准输出写一个字符,返回值是写了几个字符,返回EOF(-1)表示写入失败
- int getchar(void); 从标准输入读一个字符,返回值是读入的字符,返回EOF表示输入结束
- 键盘/显示器和程序代码之间隔着shell,shell负责将键盘的输入处理后给程序、执行程序、将程序的输出处理后显示到命令行
- size_t strlen(const char* s); 返回s的字符串长度
- int strcmp(const char* s1, const char* s2); 比较两个字符串,s1==s2返回0,s1>s2返回1,s1<s2返回-1
- char* strcpy(char *restrict dst, const char *restrict src); 把src的字符串拷贝到dst,restrict表示src和dst不重叠,返回值为dst
- char* strcat(char *restrict s1, const char *restrict s2); 把s2拷贝到s1的后面,然后返回s1
- strcpy和strcat都可能出现安全问题(如果目的地没有足够的空间)
- 安全版本:char* strncpy(char *restrict dst, const char *restrict src, size_t n); 最多能拷贝n个字符
- 安全版本:char* strncat(char *restrict s1, const char *restrict s2, size_t n); 最多能连接n个字符
- 安全版本:int strncmp(const char* s1, const char* s2, size_t n); 最多能比较n个字符
- char* strchr(const char* s, int c); 字符串中找字符(从左往右),返回null表示没有找到
- char* strrchr(const char* s, int c); 字符串中找字符(从右往左),返回null表示没有找到
- char* strstr(const char* s1, const char* s2); 字符串中找字符串
- char* strcasestr(const char* s1, const char* s2); 字符串中找字符串(忽略大小写)
11.1 枚举
- 枚举是一种用户定义的数据类型,它用关键字enum来声明:enum 枚举类型名字 {名字0,名字1,...,名字n};
- 在枚举类型声明中,大括号中的名字就是常量符号,它们的类型是int,值依次从0到n
- 定义枚举的意义就是给一些顺序排列的常量值起名字
- 枚举类型的变量实际上都是当作int来做计算和输入输出
- 声明枚举类的时候可以指定值:enum color {red=1,yellow,green=5};
11.2 结构
- 和本地变量一样,在函数内部声明的结构类型只能在函数内部使用;所以通常在函数外部声明结构类型,以便被多个函数使用
- 声明结构的方式
// 方式1
struct point {
int x;
int y;
};
struct point p1,p2;
// 方式2
struct {
int x;
int y;
} p1,p2;
// 方式3
struct point {
int x;
int y;
} p1,p2;
- 结构变量的初始化
struct date {
int month;
int day;
int year;
};
struct date today = {07,31,2014};
struct date thismonth = {.month=7,.year=2014};
- 结构变量可以相互赋值(数组变量不可以相互赋值)
- 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符:struct date *pDate = &today;
- 当函数的参数为结构类型的变量时,会在函数内部新建一个结构变量,并复制调用者的结构变量的值
- 指向结构变量的指针
struct date {
int month;
int day;
int year;
} mydate;
struct date *p = &mydate;
(*p).month=12;// 方式1
p->month=12;// 方式2
- 如果需要保护传入的结构变量不被函数修改,则函数的参数可以声明为const struct point *p
11.3 联合
- 自定义数据类型
typedef int Length; // Length是int的别名
typedef struct point {
int x;
int y;
} Point; // Point是struct point的别名
- 联合
union AnElt{
int i;
char c;
}elt1,elt2;
- 所有成员共享一块内存空间
- 同一时刻只有一个成员是有效的
- union的大小取决于其最大的成员:sizeof(union) == sizeof(每个成员)的最大值
12.1 全局变量
- 定义在函数外部的变量是全局变量
- 全局变量具有全局的生存期和作用域,在任何函数内部都可以使用它们
- 没有初始化的全局变量默认为0值(指针变量默认为null)
- 只能用编译时刻已知的值来初始化全局变量(不能用全局变量a去初始化全局变量b,除非全局变量a是const)
- 全局变量的初始化发生在main函数之前
- 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏
- 本地变量在定义时使用static进行修饰就成为静态本地变量
- 当函数执行结束时,静态本地变量会继续存在并保持其值
- 静态本地变量的初始化只发生在函数第一次执行时,之后每次执行函数时静态本地变量都是函数上次执行结束时的值
- 静态本地变量实际上是特殊的全局变量,它们位于相同的内存区域
- 静态本地变量具有全局的生存期、函数内的局部作用域(static在这里的意思是局部作用域)
- 如果一个函数的返回类型是指针,那么:返回本地变量的地址是危险的;返回全局变量或者静态本地变量的地址是安全的;最好的做法是返回传入的指针
- 不要使用全局变量来在函数间传递参数和结果
- 尽量避免使用全局变量
- 使用全局变量和静态本地变量的函数是线程不安全的
12.2 编译预处理和宏
- #开头的是编译预处理指令
- #define用来定义一个宏(#define <名字> <值>)
#define PI 3.14159
#define FORMAT "%f\n"
- 在C语言的编译器开始编译之前,编译预处理程序会把程序中的名字换成值(直接文本替换)
- 一个宏的值中有其他宏的名字,也是会被替换
- 如果宏的值超过一行,需要在行末加上\
- 可以定义一个没有值的宏(#define <名字>),作用是进行条件编译:如果某个宏已经被定义过,则编译这部分代码;如果未被定义过,则编译另一部分代码
- 预定义的宏:__LINE__/__FILE__/__DATE__/__TIME__/__STDC__
- 宏可以带参数:#define cube(x) ((x)*(x)*(x)) (整个值要有括号,每个参数要有括号)
- 定义宏时结尾不要带分号
12.3 大程序结构
- 两个独立的.c文件(源代码文件)不能编译形成.exe文件(可执行程序),只有在同一个项目中的源代码文件才会被编译并链接形成.exe文件
- 一个.c文件就是一个编译单元,编译器每次编译只处理一个编译单元
- 一个.c文件编译(compile)后得到一个.o文件(目标代码文件),然后链接器将多个.o文件链接起来(build)得到一个.exe文件(可执行程序)
- 把函数原型放到一个头文件(.h)中,在需要调用这个函数的源代码文件(.c)中#include这个头文件,就能让编译器在编译的时候知道函数的原型
- #include是一个编译预处理指令,在编译之前就处理了,它把那个文件的全部文本内容原封不动地插入到它所在的地方,所以并不是一定要在.c文件的最前面#include
- #include有""和<>两种形式:""要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有再到编译器指定的目录去找;<>让编译器只在指定的目录去找
- 编译器自己知道自己的标准库头文件在哪里;环境变量和编译器命令行参数也可以指定寻找头文件的目录
- #include不是用来引入库的,只是将.h文件的内容复制到.c文件中
- stdio.h里只有printf函数的原型,而printf函数的源代码在另外的地方(某个.lib(windows)或.a(unix)中),#include <stdio.h>只是为了让编译器知道printf函数的原型,保证在调用时给出的参数是正确的类型
- 现在的C语言编译器默认会引入所有标准库的源代码,并将标准库的代码和自己开发的源代码链接到一起
- 在使用和定义函数的地方都应该#include该函数的头文件
- 一般的做法是任何.c文件都有对应的同名的.h文件(除了main.c),把所有对外公开的函数的原型和全局变量的声明都放进去
// max.h
double max(double a,double b);
extern int g;
// int i; //这是变量的定义
// extern int i; //这是变量的声明
- 全局变量可以在多个.c文件中共享
- 在函数或者全局变量前面加上static就使得该函数/全局变量只能在它所在.c文件中被使用
- 声明不会产生代码(函数原型/变量声明/结构声明/枚举声明/宏声明);定义会产生代码
- 只有声明可以被放在.h文件中,否则会造成一个项目中多个编译单元里有重名的实体
- 通过标准头文件结构来避免重复声明的情况
// max.h
// 使用条件编译和宏,保证max.h在一个编译单元(.c文件)中只会被#include一次
#ifndef __MAX_H__
#define __MAX_H__
double max(double a,double b);
extern int g;
struct data{
int a;
int b;
};
#endif
13.1 文件
- 格式化输出printf
- 格式化输入scanf
- printf的返回值是输出的字符数,scanf的返回值是读入的项目数(通过判断调用scanf和printf时的返回值来了解程序运行中是否存在问题)
- 文件输入输出:fopen/fclose/fprintf/fscanf
- 打开文件的标准代码
FILE* fp = fopen("filename","r");
if(fp) {
fscanf(fp,...);
fclose(fp);
} else{
//...
}
- fopen函数的第二个参数说明
- 文本文件的优点是方便人类读写,而且跨平台,缺点是程序输入输出要经过格式化,开销大;二进制文件的优点是程序读写快,缺点是人类读写困难,而且不跨平台
- 二进制读写:fread函数和fwrite函数
- 在文件中定位:ftell函数和fseek函数
- 二进制文件不具有可移植性(在int为32位的机器上写成的数据文件无法直接在int为64位的机器上正确读出)
解决方案之一是放弃使用int,而是typedef具有明确大小的类型
更好的方案是用文本
13.2 位运算
- 异或:对一个变量用同一个值异或两次,等于什么也没做:xyy -> x
- 左移后右边填0:x<<n 等价于 x *= 2的n次方
- 右移后左边填入原来的最高位(对于signed类型)或者0(对于unsigned类型):x>>n 等价于 x /= 2的n次方
- 左移和右移的运算结果都是int
- 移位时位数不能为负数,这是没有定义的行为
- 位段
struct U0 {
unsigned int leading : 3;
unsigned int FLAG1 : 1;
unsigned int FLAG2 : 1;
int trailing : 27;
}
- 利用位运算输出一个数的二进制形式
void printBin(unsigned int num) {
unsigned int mask = 1u << 31;
for (; mask; mask >>= 1) {
printf("%d", num & mask ? 1 : 0);
}
}
14.1 可变数组
- 如何实现一个可变数组
// array.h
#ifndef ARRAY_H
#define ARRAY_H
typedef struct {
int *array;
int size;
} Array;
Array array_crete(int init_size);
void array_free(Array *array);
int array_size(const Array *array);
int *array_at(Array *array, int index);
void array_inflate(Array *array, int more_size);
#endif
// array.c
#include "array.h"
#include "stdlib.h"
#include "stdio.h"
//typedef struct {
// int *array;
// int size;
//} Array;
Array array_crete(int init_size) {
Array a;
a.size = init_size;
a.array = (int *) malloc(init_size * sizeof(int));
return a;
}
void array_free(Array *a) {
free(a->array);
a->array = NULL;
a->size = 0;
}
// 封装
int array_size(const Array *a) {
return a->size;
}
int *array_at(Array *a, int index) {
return a->array + index;
// return &(a->array[index]);
}
void array_inflate(Array *a, int more_size) {
// 扩容本质是重新分配内存+复制
int *p = (int *) malloc(sizeof(int) * (a->size + more_size));
for (int i = 0; i < a->size; ++i) {
p[i] = a->array[i];
}
free(a->array);
a->array = p;
a->size += more_size;
}
int main(int argc, char const *argv[]) {
Array a = array_crete(10);
printf("%d\n", array_size(&a));// 10
*array_at(&a, 0) = 123;
printf("%d\n", *array_at(&a, 0));// 123
array_inflate(&a,10);
printf("%d\n", array_size(&a));// 20
array_free(&a);
return 0;
}