1. 程序结构
- 预处理器指令:#include
- 函数
- 变量
- 语句 & 表达式
- 注释:
- 单行注释://
- 多行注释:/* */
2. 数据类型
- 基本类型:
- 整数类型
- 默认为10进制 ,10 ,20。
- 以0开头为8进制,045,021。
- 以0b开头为2进制,0b11101101。
- 以0x开头为16进制,0x21458adf。
- 浮点类型
- 单精度常量:2.3f 。
- 双精度常量:2.3,默认为双精度。
- 整数类型
- 枚举类型
- void类型
- 函数返回空:void exit (int status);
- 函数参数空: int rand(void);
- 指针指向空:void *malloc( size_t size );
- 派生类型
- 指针类型
- 数组类型
- 结构类型
- 共用体类型
- 函数类型
sizeof():可以获得类型大小
size_t,近似于无符号整型,但容量范围一般大于 int 和 unsigned
sizeof的返回值是size_t
3. 变量
extern int i; //声明,不是定义
int i; //声明,也是定义
定义:为变量分配存储空间,为变量指定初始值
声明:用于向程序表明变量的类型和名字
声明可以多次,定义只能一次
如果声明有初始化式,那么它可被当作是定义,即使声明标记为extern。(必须在函数外部,内部会错误)
对于函数:声明不加{},定义加
extern关键字,此变量/函数是在别处定义的,要在此处引用,外面没有,则此处声明为全局变量,可以在该文件与外部文件全局使用
4. 常量(字面量)
4.1. 整数常量
可以加一个后缀,U是无符号整数,L表示长整数,大小写随意。不仅仅可以在常量中使用
4.2. 浮点常量
只能使用,小数形式或者带E的指数形式
4.3. 字符常量
单引号字符对应ASCII 表中的序列值
字符串会被存储到一个数组中,这个字符串代表指向这个数组起始字符的指针
转义序列码:
转义序列 | 含义 |
---|---|
\\ | \ 字符 |
\‘ | ‘ 字符 |
\“ | “ 字符 |
\? | ? 字符 |
\a | 警报铃声 |
\b | 退格键 |
\f | 换页符 |
\n | 换行符 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ooo | 一到三位的八进制数 |
\xhh . . . | 一个或多个数字的十六进制数 |
4.4. 定义方式
- 使用 #define 预处理器。 #define identifier value
- 定义的是不带类型的常数,只进行简单的字符替换。在预编译的时候起作用,不存在类型检查。
- 使用 const 关键字。 const type variable = value; // 必须定义时赋值
- 本质不是去定义一个常量,而是去改变一个变量的存储类,把该变量所占的内存变为只读!
两者区别:
- 编译器处理方式不同:
- #define 宏是在预处理阶段展开。
- const 常量是编译运行阶段使用。
- 类型和安全检查不同
- #define 宏没有类型,不做任何类型检查,仅仅是展开。
- const 常量有具体的类型,在编译阶段会执行类型检查。
- 存储方式不同
- #define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)编译期间会进行替换从而为变量分配内存
- const常量会在内存中分配(可以是堆中也可以是栈中)。不会重复分配内存,所以效率很高
5. 存储类
- auto:所有局部变量默认的存储类。只能用在函数内,即 auto 只能修饰局部变量。
- register:用于定义存储在寄存器中而不是 RAM 中的局部变量,但并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。并且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
- static
- 修饰全局变量,默认
- 修饰局部变量,函数内部,不同调用,会记住每次调用结果,不会重置
- extern
6. 运算符
6.1. 算数运算符
6.2. 关系运算符
6.3. 逻辑运算符
6.4. 位运算符
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与操作,按二进制位进行”与”运算。运算规则: | (A & B) 将得到 12,即为 0000 1100 |
| | 按位或运算符,按二进制位进行”或”运算。运算规则: | (A | B) 将得到 61,即为 0011 1101 |
^ | 异或运算符,按二进制位进行”异或”运算。运算规则:`` | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 取反运算符,按二进制位进行”取反”运算。运算规则: | (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
6.5. 赋值运算符
6.6. 杂项运算符
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
7. 判断
8. 循环
9. 函数
9.1. 函数定义:包括方法体
9.2. 函数声明:没有方法体分号结尾
内部函数:static,只能在本文件中使用
外部函数:extern,可以被外部函数调用(默认),在被外部文件调用时也需要加extern声明
内联函数:inline,内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益。
递归函数不能定义为内联函数
内联函数一般适合于不存在while和switch等复杂的结构且只有1~5条语句的小函数上,否则编译系统将该函数视为普通函数。
内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。
对内联函数不能进行异常的接口声明。
关于main函数
int main( int argc, char *argv[] )
如果 main 函数要带参数,就是这两个类型的参数;否则main函数就没有参数。
C程序在编译和链接后,都生成一个exe文件,执行该exe文件时,可以直接执行;也可以在命令行下带参数执行,命令行执行的形式为:可执行文件名称 参数1 参数2 … … 参数n。
- (1) 可执行文件名称和所有参数的个数之和传递给 argc;
- (2) 可执行文件名称(包括路径名称)作为一个字符串,首地址被赋给 argv[0],参数1也作为一个字符串,首地址被赋给 argv[1],… …依次类推。
函数声明和函数原型的参数名可以不一样,编译器他想知道的是函数参数的类型,与函数参数的名字没有关系。同理,定义时相同函数名,不同参数列表也是合法的,甚至参数列表没有参数名也可以(声明时)
9.1. main 函数前面为什么要加上数据类型,比如: int void ?
main 函数的返回值是返回给主调进程,使主调进程得知被调用程序的运行结果。标准规范中规定 main 函数的返回值为 int,一般约定返回 0 值时代表程序运行无错误,其它值均为错误号,但该约定并非强制。在一些严格的编译器中必须使用int作为返回类型
9.2. C 语言 int main() 和 int main(void) 的区别?
- int main(void) 指的是此函数的参数为空,不能传入参数,如果你传入参数,就会出错。
- int main() 表示可以传入参数。
函数调用
程序是从上向下执行,所以函数要先声明,后调用。
10. 作用域规则
- 局部变量
- 全局变量
全局变量与局部变量在内存中的区别:
- 全局变量保存在内存的全局存储区中,占用静态的存储单元;
- 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
初始化的区别:当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化。char的初始值’\0’
形参与实参
- 在定义函数中制定的形参,在没有出现函数调用时不占用内存中的存储单元。在函数调用时才分配内存
- 将实参的值传递给形参
- 在执行函数时,由于形参已经有值。可以用形参进行运算。
- 通过return语句将函数值返回,若无返回值,则无return
- 调用结束后,形参被释放掉,实参保留原值(单向传值)
11. 数组
数组名是一个指向数组中第一个元素的常量指针。
11.1. 数组声明
type arrayName [ arraySize ];
11.2. 初始化数组
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
11.3. 多维数组
11.4. 传递数组到函数
- 形参是一个指针
- 形参是一个已定义大小的数组
- 形参是一个未定义大小的数组
11.5. 从函数返回数组
使用指针:
int* myFunction() { int r[]; return r; }
注意:C 不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。
11.6. 指向数组的指针
double *p;
double balance[10];
p = balance;
*(balance+4) //访问balance[4]
// p[4] == balance[4]
12. 枚举
enum 枚举名 {枚举元素1,枚举元素2,……};
注意:第一个枚举成员的默认值为0,后续在前面加1,枚举的内容都是常量不能被赋值。
12.1. 枚举变量
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;
枚举变量值只能是枚举值,不能是整型或其他类型。但是可以使用强制转换,
day = (enum week)1;
12.2. 枚举与宏
宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
12.3. 枚举的遍历
必须使用强制转换,并且枚举值连续
for (enum week day = Mon; day <= Sun; day = (enum week)(day + 1))
{
printf("%d", day);
}
13. 指针
13.1. 指针声明
type *var-name;
未被初始化的指针被称为野指针,如果实在不知道初始成多少,就指向为空。
指针参数的传递本质仍然是值传递,只不过值是地址值
13.2. 指针地址访问:&取得该变量的地址
13.3. 指针值访问:*访问地址所指向的值
13.4. 指针的算术运算
指针的每一次递增,它其实会指向下一个元素的存储单元。
指针的每一次递减,它都会指向前一个元素的存储单元。
即指针地址增加其增加长度为指针类型单元长度
指针是可以进行比较的
13.5. 指针数组
指针数组,指针类型的数组,即数组元素是指针
数组指针,指针指向的是一个数组
int a[5]={ 1,2,3,4,5 }; //定义一个一维数组 a int (*prt)[5]; // 步长为 5 的数组指针,即 prt 指向的数组里有 5 个元素 prt=&a; // 把数组 a 的地址付给 prt,则 prt 为数组 a 的地址,*prt 表示数组 a 本身 prt[ 0 ]; //表示数组首元素的地址 *prt[ 0 ]; //表示数组的首元素的值,即为数组 a 的 1 **prt; //表示数组的首元素的值,即为数组 a 的 1 *prt[ 1 ] ; //表示指向数组的下一行元素的首元素的值,但是a是一维数组,只有一行,所以指向的地址中的值不确定
int *a[3]:为什么这里是指针数组,[] 的优先级高于 ***** ,所以这是一个数组,而 ***** 修饰数组,所以是指针数组,数组的元素是整型的指针。
13.6. 函数指针数组
数组内容为指针,指针指向的是函数
13.7. 指向指针的指针
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。
13.8. 传递指针给函数
13.9. 从函数返回指针
14. 函数指针与回调函数
14.1. 函数指针(指向函数的指针)
double func(int a,char c);
double (*p)(int a,char c);
p=&func;
//调用函数
double s1=func(100,'x');
double s2=(*p)(100,'x');
14.2. 指针函数(返回值为指针的函数)
14.3. 回调函数(函数指针作为某个函数的参数)
其中一个参数值为函数指针,传入某个实例函数。有点类似于java的接口
15. 字符串
字符串本质就是字符数组(以\0结尾),所以字符串又可这样定义:
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
char site[] = "RUNOOB";
printf("%s") // 输出RUNNOOB
注意若没有\0结尾,会输出错误,不定长字符串数组会默认以\0结尾
15.1. 字符串操作函数
函数 | 目的 |
---|---|
strcpy(s1, s2); | 复制字符串 s2 到字符串 s1。 |
strcat(s1, s2); | 连接字符串 s2 到字符串 s1 的末尾。 |
strlen(s1); | 返回字符串 s1 的长度。 |
strcmp(s1, s2); | 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。 |
strchr(s1, ch); | 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 |
strstr(s1, s2); | 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
- sizeof计算会包括\0
- sizelen计算不包括\0
16. 结构体
struct tag {
member-list
member-list
member-list
...
} variable-list ;
结构体相互包含,则需要对其中一个结构体进行不完整声明:struct B; //对结构体B进行不完整声明
16.1. 结构体变量初始化
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};
16.2. 访问结构体成员(使用.)
16.3. 结构体作为函数参数
void printBook( struct Books book );
16.4. 指向结构体的指针
struct Books *struct_pointer;
为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符
struct_pointer->title;
16.5. 位域
所谓”位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作
struct 位域结构名
{
类型说明符 位域名: 位域长度;
类型说明符 位域名: 位域长度;
};
- 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
- 位域可以是无名位域(空域),这时它只用来作填充或调整位置。无名的位域是不能使用的。
16.6. 结构体数组
struct book library[MAVXBKS]; //book类型的结构体数组
16.7. 结构体内存大小对齐原则
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
- 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。即结构体成员的末地址减去结构体首地址(第一个结构体成员的首地址)得到的偏移量都要是对应成员大小的整数倍。
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在成员末尾加上填充字节。
17. 共用体:允许您在相同的内存位置存储不同的数据类型
17.1. 共用体定义
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
//共用体大小应该能够容下最大变量
17.2. 访问共用体成员(.)
共用体只能存下一个成员,不同存入依最后一次取出为准
17.3. 共用体应用场景
通信中的数据包会用到共用体:因为不知道对方会发一个什么包过来,用共用体的话就很简单了,定义几种格式的包,收到包之后就可以直接根据包的格式取出数据。
18. typedef
可以使用它为类型取一个别名
18.1. typedef与#define的异同
- typedef由编译器执行解释,仅限于为类型定义符号名称
- #define 语句是由预编译器进行处理的,为任意字符,与编译阶段,原样替换
18.2. typedef的四种用法
为基本数据类型定义新的类型名
typedef unsigned int COUNT;
为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct tagPoint { double x; double y; double z; } Point;
为数组定义简洁的类型名称
为指针定义简洁的名称
typedef char* PCHAR;
int strcmp(const PCHAR,const PCHAR); // char* const p
//形成的是一个常量指针,指向常量的指针(指针可以修改,指向内容不可改)
typedef const char* PCHAR;
int strcmp(PCHAR, PCHAR); // 这才是指针常量(指针不可改,但指向内容可改)
19. 输入和输出
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 您的屏幕 |
19.1. printf和scanf
printf("Number = %d", testInteger);
//格式化输出到屏幕,%d整型
scanf("%f",&f);//返回输入长度【】 ·
//从键盘输入 %f 匹配浮点型数据
//%s对应字符串
//%c对应字符
指定数据宽度和小数位数,用 %m.nf
输出的数据相左对齐,用 %-m.nf
在 m.n 前加一个负号,其作用与 %m.nf 形式作用基本相同,但当数据长度不长过 m 时,数据向左靠,右端补空格。
19.2. getchar和putchar
int c;
c = getchar();//从屏幕读取一个字符,就算输入多个也只读第一个,返回一个整数(对应ascll码)
putchar(c);//将整数(ascll码)转成字符
19.3. gets和puts
char str[100];
gets(str);//从键盘获取字符串输入到str缓冲区,换行符转换为null即\0,可能会越界
fget(str);//不会越界,自动截断,最后一个字符会自动转为\0
puts(str);//从str缓冲区获取字符串,缓冲区不变
20. 文件读写
20.1. 打开文件
FILE *fopen( const char * filename, const char * mode );
模式 | 描述(必须小写) |
---|---|
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
19.2. 关闭文件
int fclose( FILE *fp );//返回0成功,返回EOF(studio.h中的常量)失败
19.3. 写入文件
int fputc( int c, FILE *fp );//写入字符
int fputs( const char *s, FILE *fp );//写入字符串
int fprintf(FILE *fp,const char *format, ...)//写入格式化的字符串
19.4. 读取文件
int fgetc( FILE * fp );//读取单个字符
char *fgets( char *buf, int n, FILE *fp );
//从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。
fscanf(fp, "%s", buff);//读取文件到缓冲区,遇到空格换行符会停止
19.5. 二进制I/O函数
size_t fread(void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
//用于存储块的读写 - 通常是数组或结构体。
19.6. 文件指针移动
int fseek(FILE *stream, long offset, int whence);
//fseek 设置当前读写点到 offset 处, whence 可以是 SEEK_SET,SEEK_CUR,SEEK_END 这些值决定是从文件头、当前点和文件尾计算偏移量 offset。
20. 预处理器
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
20.1. #include
- <>从系统库寻找文件,即编译器的类库路径里的头文件
- ”“从本地目录中获取文件,即程序的相对路径中的头文件,没找到就到类库路径的目录下
20.2. 预定义宏
宏 | 描述 |
---|---|
DATE | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
TIME | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
FILE | 这会包含当前文件名,一个字符串常量。 |
LINE | 这会包含当前行号,一个十进制常量。 |
STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
20.3. 预处理器运算符
宏延续运算符(\)
字符串常量化运算符(#)
标记粘贴运算符(##)
defined() 运算符,确定一个标识符是否已经定义过了,配合#if使用
20.4. 参数化宏
#define square(x) ((x) * (x))
#define square_1(x) (x * x) //必须将参数用()括起来,否则会失败
#define SWAP1(x,y) {x=x+y;y=x-y;x=x-y;}
#define SWAP2(x,y) {x=x^y;y=x^y;x=x^y;} //不建议使用
21. 头文件
21.1. 只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
21.2. 有条件引用
21.3. .c 和 .h
- .c文件,以c为扩展名,一般存储具体功能的实现;
- .h文件,称为头文件,一般存储类型的定义,函数的声明等。
22. 错误处理
- perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
- strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
23. 可变参数
==需要引入#include <stdarg.h>==
int func(int , ...);
// int表示有几个参数 最后面必须接...
va_list valist;
double sum = 0.0;
int i;
/* 为 num 个参数初始化 valist */
va_start(valist, num);
/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++)
{
/* 每次调用 va_arg 都会修改用 va_list 声明的对象,
从而使该对象指向参数列表中的下一个参数; */
sum += va_arg(valist, int);
}
/* 清理为 valist 保留的内存 */
va_end(valist);
- 函数参数的传递存储在栈中,从右至左压入栈中,压栈过程为递减。
- 参数的传递以4字节对齐,float/double这里不讨论。
24. 内存管理
==<stdlib.h>==
| 函数|描述 |
|:———————————————————– |
| void *calloc(int num, int size); |在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。 |
| void free(void *address); |该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。 |
| void *malloc(int num); |在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。 |
| void *realloc(void *address, int newsize); |该函数重新分配内存,把内存扩展到 newsize。 |