这是我在学习c语言的时候记的笔记,用作备份与分享(或许也能用作期末复习(笑)),若有错误请在评论区指正,感激不尽!
持续更新中~
C语言文件类型:
filename.h 头文件
filename.c 源文件
filename.i 预处理文件
filename.s 汇编文件
filename.o 目标文件
filename.gch 头文件的编译结果,会优于.h文件调用,建议直接删除
filename.so 共享库文件
filename.a 静态库文件
C语言数据类型:
内建数据类型:
整型数据:
有符号整型:
最高位二进制用于表示正负符号,0代表正数,1代表负数 。
signed char 1字节 [-128,127]
signed short 2 [-32768,32767]
signed int 4 2开头的十位整数
signed long 4/8 //32位系统是4/64位系统是8
singed long long 8 9开头的19位整数
占位符:%hhd、%hd、%d、%ld、%lld(自上而下)
注意: 有符号类型数据,signed默认加上。
无符号整型:
所有二进制位都用于表示数据,只能表示整数。
unsigned char 1字节 [0,256]
unsigned short 2 [0,65535]
unsigned int 4 [0,4开头的10位整数]
unsigned long 4/8
unsinged long long 8 [0,1开头的20位整数]
占位符:%hhu、%hu、%u、%lu、%llu(自上而下)
**注意:**在定义无符号类型数据时,关键字unsigened不能省略。
浮点数据:
单精度浮点:
float 4字节 %f
双精度浮点:
double 8字节 %lf
高精度浮点:
long double 12/16 %LF
浮点型采用科学计数法存储,存储格式为符号位+指数+尾数,运算速度比整型慢。
模拟类型:
char 字符型 在计算机中以整数形式存储,当显示时再根据ASCII码对应关系显示,占位符是%c;
bool 布尔型 包含stdbool.h头文件才可使用
#include <stdbool.h>
bool flag = ture;
bool flag1 = false;
复合数据类型:
结构
由多种内建数据类型组合而成的一个整体,用于描述一个事物的各项数据。由于相同类型的结构体变量数据顺序相同,结构体变量可以直接给结构体变量赋值。
设计结构体:
Struct Typename
{
类型 成员名;
...
};
注意: 此时只是完成了数据类型的设计,一般结构体设计在头文件或函数外,方便其他函数使用(通常结构体Typename首字母大写)。
定义结构变量:
Struct typename 结构变量名;定义时struct不能省略 Struct typename* p = malloc(sizeof(struct typename));使用堆内存定义结构体,使用这种方式,由于编译时堆内存还没有分配,不能进行初始化,只能批量或单个赋值。(p = stu;或scanf(“%s%d%g”,p -> name,&p -> ip,&p -> score)(第一个变量是name,是数组,不取地址)) 初始化成员 Struct typename 结构变量名 = {数据1,数据2…} 注意要按照成员顺序进行初始化 Struct typename 结构变量名 = { .成员名1 = 数据1, .成员名2 = 数据2 }; 可以无视成员顺序,但要指定成员名 访问成员: 结构变量.成员名; 结构指针 -> 成员名; 例如:typedef struct A{int num;}A; A a = malloc(sizeof(A)*100) a[5].num或(a+5) -> num; 结构体类型重定义: 由于在C语言中struct关键字无法省略,导致使用时麻烦,可以使用typedef给结构重新定义一个类型。 Typedef struct typename { … }typename; 结构体字节数计算 系统为了快速的访问结构体的成员,会对结构的成员在内存排列时进行补齐和对齐,因此结构的成员顺序会影响结构的总字节数,一般结构的总字节数会大于等于所有成员的字节数和 对齐:假定第一个成员使用零地址,所有成员所使用的内存地址,必须能被他的字节数整除 补齐:结构体的总字节数必须是它最大成员的整数倍,若不够,则补空字节 在linux系统下,若成员最大字节超过4,则按4计算进行对齐、补齐(windows正常计算)跟系统有关,跟系统位数无关。 Char ch1 : 4将ch以四位二进制(半字节)进行储存,但最终还会进行补齐操作 联合 由程序员设计的一种数据类型,使用语法与结构一样,只是成员的排列方式不同,所有成员共用一个起始位置,共用一块内存,同一时间只能使用一个,一个成员的值发生变化,其他成员的值也会发生变化,可以用一块内存对应多个标识符,达到节约内存的目的,目前已基本不用。没有对齐,有补齐,规则同结构。 Union typename { 类型 成员名; … }; 考点: 成员天然是对齐的,但是有补齐 使用联合判断大小端系统 枚举:一种特殊的整型数据,把一个整型变量可能出现的值全部列出来,并取一个有意义的名字(枚举常量,可以直接放在case里),除此之外不允许使用其他的值(C语言编译器为了提高速度不会检查枚举变量的赋值,但c++会检查)以此来保证数据安全,提高程序的可读性。 Enum direction {up,down,right,lift};默认{0,1,2,3},最好指定初始枚举值。 可以定义匿名枚举,只使用枚举值
运算符
算术运算符 + - * / % 关系运算符 < > <= >= == != 计算结果为0/1(逻辑值,可以继续作为整数参与计算) 使用时常量写左边,变量写右边防止失误将写作=导致不报错但程序出错。 逻辑运算符 && || ! 会先把运算对象转换为逻辑值,0为假,非0为真,结果也是逻辑值 !A 求反,计算对象只有一个,也叫单目运算符,运算级别比&&||高 &&、||具有短路特性,当左边的值可以确定运算结果时,算符右边不进行计算 自变运算符 前自变++i、–i变量值会立即加一或减一 后自变i++、i-- 变量在当前语句不变,下一条语句前会变 只能变量使用,不能过多使用,不同编译器对自变运算符的运算规则不同 三目运算符 [a]?[b]:[c] 运算对象有三个 字节运算符 sizeof() 用于计算数据类型在内存中占用的字节数,仅对一个单位使用时可省略(),如果()内是表达式,不会计算表达式只推算表达式运行结果的数据类型字节数(取可能结果的最大值)。 位运算符 << >> & | ~ ^ 类型转换 只有相同类型的数据才能在一起运算 自动类型转换规则:以不丢失数据为基础,字节少的向字节多的转换,有符号向无符号转换,整型向浮点数转换, char和short优先向int转换 强制类型转换: (目标类型)数据,有丢失数据风险 If分支语句 if else else if Switch语句 Switch(整型) gcc编译器特殊语法:case 1 … 3: => case 1: case 2: case 3: for循环语句 for([1];[2];[3]) 1:只执行一次 2:条件判断 3:循环体 while循环
随机函数:
#include <stdlib.h>
#include <time.h>
Srand(time(NULL));
Rand()%100+1;(1-100随机数)
数组与函数
数组
一个数组所有数据类型相同所占内存空间大小相同,内存地址连续 数组是最简单最重要的线性结构–顺序结构(特点:内存地址连续) 语法规则: dataType arrayName[arrayLength] 数组长度是静态的,一旦定义不可改变。const是只读,定义的是变量,C99之前不能使用变量定义长度 如果数组下标超过[0,长度-1]会数组越界,程序不会检查,不会报错但可能改变其他数据的值导致系统出错!!! 变长数组不能直接初始化,需要用循环进行初始化 二维数组 定义 类型 数组名 [行][列] Int arr[3][5]; 初始化语法: 数组名[行数][列数] = {{v00,v01,v02…},{v10,v11,v12…}};
函数
是C语言管理代码的最小单位,是一个代码模块,命名空间、栈空间是独立的。 函数声明: 在C语言中函数名全部小写,用下划线分隔 在函数声明时,形参列表可以只写类型,但在函数定义是必须写全,没有参数时必须写void 不需要返回值时要写void 函数定义 返回值类型 函数名(形参列表) 尽量不要超过50行 传参 普通变量是值传递 数组参数是地址传递,数组会变成指针,需要额外增加一个参数来传递数组长度 不要返回局部变量的地址 函数的隐式声明 如果函数没用显式声明,系统会猜测函数类型 猜测格式 根据实参的个数和类型猜测 返回值int类型 函数的返回值 函数一旦定义的返回值类型,编译器就会划分内存用于存储返回值,而调用者可以访问这部分内存(获取返回值) return仅能将数据存入返回值的内存,不写return会有随机返回值 函数递归 函数自己调用自己的行为叫递归,没有合适的出口该行为可能导致死循环,有合适的出口可以解决复杂的问题:树形结构问题,带状态的循环,是分治算法的具体实现方式之一 优点: 代码简洁 容易解决复杂问题 缺点: 浪费内存 运行速度慢 递归函数的一般结构 设计出口 解决问题 函数递归
程序的内存分段
Text 代码段:C语言代码会翻译成二进制指令,就存储在该内存中,还有常量(字符串字面值),该内存是只读(可执行)的。一旦强行修改会触发段错误 Data 数据段:存储初始化过的全局变量和静态变量(包括全局和局部) Bss 静态数据段:未初始化的全局变量和静态变量(包括全局和局部)该内存段在程序执行前会被清理一次(初始化为0) Heap 堆:由程序员手动管理,范围足够大能存储大量数据,且施放时间可控,该内存无法与标识符建立映射关系(无法取名字),必须与指针配合使用。使用麻烦,容易产生内存泄漏和内存碎片 Stack 栈:存储局部变量使用 size ./a.out可以查看内存大小,由系统自动管理,随函数被调用,会自动分配内存,函数执行结束后,自动释放内存。 优点:使用方便,采用栈结构方式管理,安全,不会产生内存泄漏、内存碎片 缺点:大小有限,一次使用过多可能产生段错误,分配和释放不可控不适合长期保存数据。 变量的分类和属性 (静态)全局变量 生命周期:整个程序运行周期 作用域:任何位置都可以用,但在其他源文件中需要声明(静态全局只能在目标文件内部使用) (静态)局部变量 生命周期:函数开始执行到结束 作用域:函数内 块变量 定义在for while if等语句块内 变量的属性 作用域:可使用的范围 存储位置:变量存储的内存段 生命周期:定义到释放的时间段 全局变量和局部变量可以重名,局部变量会屏蔽相同名称的全局变量。
变量的类型限制符
Auto 定义自动分配内存、自动翻译内存的变量(默认添加)全局变量不可使用 Const 对变量进行保护,被const修饰的局部变量不能显式修改,可以通过指针隐式修改 特殊情况:存储在data段(初始化后的全局变量)的变量被const修饰后会存储到text段(变量变成常量,无法修改),此时变量被强行修改会出现段错误 Static 限制作用域,改变存储位置,延长生命周期,可以修饰变量也可以修饰函数 限制作用域:限制函数、全局变量的作用域,只能在源文件中使用 延长局部变量的生命周期,改变局部变量的存储位置 Volatile 默认情况下,编译器会优化变量取值过程,使用该类型会使编译器放弃优化(用于变化性高(常被隐式修改)的变量,多线程共享变量,软硬件共享变量) Register 申请把变量的存储位置从内存改为寄存器,大大提高程序的运行速度,该类型变量不能取地址,但寄存器是有限的,不一定百分百成功 Extern 声明变量,但只是在编译阶段暂时通过,链接时若无法链接到正确的全局变量,会产生未定义错误 Typedef 类型重定义,可以给较长的数据类型重新取一个简短的名字
指针
指针定义:指针是一种数据类型,使用它可以定义指针变量,这种变量存储的是整数,这种整数代表了内存的编号,每个数字对应一个字节,使用指针变量可以访问对应的内存。 指针访问的字节是起始位置,根据指针类型,系统会自动向后访问相应字节的数据,大部分设备是小端设备,即指针起始位置时数据的低位,读取数据时从低位开始读取。 适合使用指针的情况 从理论角度来说,指针可以访问任何位置的内存,但大部分内存没有权限访问,因此非常容易产生段错误,建议仅在合适时(现阶段)使用指针。 函数之间共享局部变量,由于全局变量浪费内存,还可能造成命名冲突,所以全局变量不适合大量共享,而函数传参默认值传递,所以无法共享,使用指针是共享局部变量的好方法。 提高函数的传参效率,函数传参默认值传递(内存拷贝),当变量的字节数较多时,使用指针传递变量的地址效率更高(4(32位系统)或8(64位系统)字节),但这样就带来了变量被修改的风险,可以使用const配合指针保护变量。 配合堆内存使用,堆内存无法取名,无法与标识符建立映射关系,必须与指针配合使用。 指针使用方法 定义指针变量:类型* 指针变量_p; 由于指针变量用法与普通变量不同,为了与普通变量区分,在命名时在结尾加p 指针变量不能连续定义 //int* p1,p2 //p1是指针,p2是int变量 //int *p1,*p2 //二者均是指针 指针变量与普通变量一样,默认值是随机的(野指针),为了安全一定要初始化,可以赋值NULL(空指针) 指针的类型决定了访问内存时的字节数
Char arr[10][10];
Char (*arr)[10];
指针变量的赋值 用变量的地址赋值:p = # 用堆内存地址赋值:p = malloc(sizeof(*p)); 指针变量的解引用(访问内存):(*p )指针变量 通过指针变量中存储的内存编号去访问内存,具体访问多个字节由指针变量的类型决定,该过程可能产生段错误,需要从指针赋值的步骤寻找原因 小技巧:当程序崩溃时,内核有可能把该程序当前内存映射到core文件里,方便程序员找到出问题的地方,而ubuntu系统默认不输出core文件,需要执行ulimit -c unlimited,打开bashrc文件(vi ~/.bashrc)在文件末尾添加ulimit -c unlimited然后保存文件运行source ~/.bashrc,可以长期有效。 当出现段错误时,调试时运行gcc -g file.c(出错的文件)指令,然后运行./a.out,gdb ./a.out core,在gdb模式下运行run即可跳转到出错代码段。 退出gdb:运行q 指针的运算 指针变量中存储的是整数,理论上整型能使用的运算符,指针变量都可以使用,但仅有以下运算有意义: 指针 + n;等价于指针+(n乘指针类型字节数) 指针 - n; 指针 - 指针;(指针-指针)/指针类型字节数;不同类型的指针不能相减 使用指针要注意的问题: 空指针:指针变量的值为NULL的指针被称为空指针,大多数系统NULL是0地址,可以把指针变量当逻辑值使用(不建议),不可解引用,否则会产生段错误.为了避免空指针导致的段错误,当使用来历不明的指针时应该判断是否是空指针。 判断空指针 if(NULL == p){ } 野指针:指针变量的值是是随机的、不确定的,这种指针称为野指针,使用野指针不一定会出错、不确定什么时候出错,无法判断一个指针变量是否是野指针,与空指针相比危害性更大。为了避免使用野指针导致的bug,在编程时应避免产生野指针: 定义指针变量一定要初始化 指针变量所指向的内存被释放或销毁后,指针变量要及时赋值为空 函数不返回局部变量的地址 指针与const
const int *p 保护目标,*p不被修改
int const *p 同上
int* const p 保护指针变量p不被修改
const int* const p 既保护p也保护*p
int const * const p 同上
const int const *p 错误写法
指针数组与数组指针 指针数组:由指针变量组成的数组,他的身份是数组,成员是指针变量 int* arr[10]; 数组指针:身份是指针,专门用来存放数组的地址 int (*arr)[5]; 数组指针移动以数组字节宽度为一个单位 例:
int arr[] = {1,2,3,4,6};
int* p = (int*)(&arr + 1)-1;
printf(“%d”,*p);
Void指针: 以1字节为单位移动 不能解引用 可以与任何类型的指针自动类型转换,被称作万能指针(C语言限定) 一般被用作函数的参数、返回值 函数指针 函数会被翻译成二进制指令存储到代码段,而函数名就是这段指令的首地址 可以定义特殊的指针变量存储函数的首地址,这样就可以把函数当成数据进行传递 我们把存储函数地址的指针变量叫做函数指针 定义函数指针: 复制函数的声明语句,给函数名加上小括号并在函数名前加*,末尾加p(给函数起一个函数指针名) Bool (is_prime_p)(int num) = is_prime; 调用is_prime_p(100); 当我们把函数作为数据传递时,被调用的函数可以通过函数指针调用我们以参数形式提供的函数,这种模式叫回调模式。比如:qsort、bsearch 由于函数指针比较长,一般会选择使用typedef进行类型重定义 Typedef int (*FP)(int,int); FP fp;(fp即是函数指针名,FP相当于函数指针fp的格式) 二级指针 一级指针用于存储普通变量的地址,二级指针用于存储指针变量地址的指针 定义二级指针:类型 ** 变量名_pp; Int num; int p = # int** pp = &p; 跨函数共享一级指针时使用二级指针 二级指针解引用:*pp <=> p; **pp <=> *p; 指针与数组的关系 数组名就是数组空间的首地址,是个特殊的指针,是个常量,可以看作常指针,所以可以使用解引用的语法访问成员,指针也可以使用[]进行解引用,所以 Arr[n] <=> *(arr+n) 数组与普通指针区别 数组名是常量,普通指针是变量 普通指针变量有自己的存储空间,用来存储内存编号,它与目标内存是指向关系 数组名没有自己的存储空间,本身就代表数组空间的首地址,它与数组内存是映射关系 对数组名取地址,结果还是数组名的值 int arr[5] arr<=>&arr arr类型int* &arr类型int (*)[5]
堆内存管理
1:C语言中没有管理堆内存的语句,而是由标准库提供的一套函数来管理堆内存:calloc、free、malloc、realloc
Void *malloc(size_t size);
功能:向系统申请一块堆内存
Size:内存块字节数
返回值:
成功 返回内存块首地址
失败 返回NULL
当首次使用malloc申请内存时,malloc会向系统申请内存,而操作系统会一次性给malloc 33页(每页4096字节)内存给malloc管理,之后再使用malloc申请内存时,就会从这33页中分配。
Malloc分配内存时,两个相邻内存块之间存在空隙(至少是4字节,通常4-12)。这些空隙中存在一个指针,这个指针所指向的空间记录着malloc的管理信息,一旦破坏就会影响接下来malloc和free的使用。不要越界,认识内存越界导致的错误。
Void free (void *ptr);
功能:释放内存
可以释放空指针
一块内存不可以重复释放
只释放使用权,而不会清理内存(会破坏一部分数据,并可能监控入口)
操作系统对堆内存的管理决定了是否产生段错误,而malloc、free只是在管理堆内存的使用权限,如果越界访问,会影响malloc和free的后续使用,也可能产生脏数据
Void *calloc(size_t nmemb, size_t size);
功能:按块分配堆内存
Nmemb:要申请的块数
Size:每块的字节数
返回值:内存块的首地址
内存块间无空隙
该函数其实底层调用的malloc,分配的是一整块内存,只是nmemb、size让代码可读性更强,二者调换位置不影响使用结果
Calloc会把分配的内存进行初始化为0,因此速度较慢
Void *realloc(void *ptr, size_t size)
功能:调整已经申请到的内存块的大小,可以变大也可以变小
Ptr:申请到的内存块首地址
Size:要调整到的目标字节数
返回值:
调整后的内存块首地址,如果在原位置基础上无法调整,函数会重新申请一块新的内存块,并把原数据拷贝过去,并释放原内存块
使用malloc申请的内存,里面的内存是随机的、不确定的,如果需要对内存进行初始化,可以使用以下函数
#include <strings.h>
Void bzero(void *s,size_t n)
功能:把一块内存的所有字节赋值为0
S:内存首地址
N:内存块字节数
/**********************************************************************
Void *memset(void *s,int c,size_t n)
功能:把一块内存的所有字节数赋值为c,范围:0-255(以字节为单位)
#include <stdio.h>
int main(int argc,const char* argv[])
{
int* p = malloc(4);
int i = 0;
for(;;)
{
printf("%d\n",i);
p[i] = i++;
}
}
为什么可以越界那么多,到33790才越界
Malloc向系统申请内存时,系统会分配给它33页4096字节的空间,其中由4字节用于记录malloc的维护信息,剩下的空间有337914字节的空间,计数从0-33790.
为什么这么设计malloc函数
为了不要频繁访问操作系统,提高效率。
***********************************************************************/
字符:
字符就是符号或团,在c代码是以整数形式模拟的,当需要显示时在根据ascii表中的对应关系显示出对应的符或图案
‘0’ 48
‘A’ 65
‘a’ 97
‘\0’ 0
字符处理相关函数
Int isalnum(int ch);
功能:ch是数字或字母字符,返回非零值,否则返回零
Int isalpha(int ch);
功能:ch是字母,函数返回非零值,否则返回零值
字符串:有字符组成的串型结构(串型结构:由若干个相同类型的数据组成,有一个确定的结束标志,对数据的处理是连续的,直到遇见结束标志) %s
字符串的结束标志是’\0’
字符串字面值:由双引号包含的若干个字符,存储在代码段,以地址形式呈现。
虽然是地址,但与数组名相似,可以计算出字符数量,末尾有隐藏的’\0’,使用sizeof对字面值进行计算可以看出。
可以用 const char* 指针变量来存储它们的地址,长期使用,不能强行修改,否则会产生段错误。
在linux下汉字占三字节,windows下,汉字占两字节
相同的字符串字面值在代码段只存储一份
字符数组:
只要有’\0’结尾,字符数组也就可以当字符串使用。
如果没有’\0’,把字符数组当字符串使用,可能会出现乱码,严重甚至产生段错误
可以使用字符串字面值对字符数组初始化,不需要设置数组长度,编译器会自动计算,并会给’\0’预留位置
字符数组被初始化完成后,程序中就有了两份相同字符串,其中一份存放在代码段,另一份在栈内存,可读可写
字符串的输入:
使用Scanf,占位符%s存储到char*地址,scanf函数会自动在末尾存储’\0’,不会检查字符串存储空间的大小,直到遇见enter,因此输入字符串过长可能会产生段错误。Scanf遇到空格就会停止,无法接受带空格的字符串
Gets(char *s)专门用于接受字符串的函数,同样无法限制输入字符串的长度,当超出存储空间的范围时就可能会产生段错误
Fgets(char *s,int size,FILE *stream)该函数可以从指定的文件中读取不超过size-1个字符,当达到size-1个字符时会自动停止,强行为’\0’预留位置。//终端就是特殊的文件(stdin)
若输入的字符不足size-1,会连同\n一同接收,
如果输入的字符超过size-1个,剩余的数据会残留在数据缓冲区,影响后续数据的输入。解决办法:stdin -> _IO_read_ptr = stdin -> _IO_read_end;
void get_str(char* str,size_t size)
{
fgets(str,size,stdin);
size_t len = strlen(str);
if('\n' == str[len-1])
{
str[len-1] = '\0';
}
else
{
stdin -> _IO_read_ptr = stdin -> _IO_read_end;
}
}
输出字符串:
printf()
Puts()功能:输出字符串,自动添加’\n’。返回值输出字符个数
对字符串操作:
标准库中的string.h函数几乎都是处理字符串的函数
常用的有:size_t strlen(const char *s) 计算字符串长度
Char *strcpy(char *dest,const char *src) 拷贝字符串相当于赋值,右边赋值给左边,不负责检查存储空间长度,可能出现段错误
Char *strcat(char *dest,const char *src) 把字符串src追加到dest末尾
Int strcmp(const char *s1,const char *s2) 比较,按照字典顺序排序,谁在前谁小,s1>s2返回正值,s1 < s2 返回负值,s1 == s2 返回0;以上四个函数要求能手写。
Char *strncpy(char *dest,const char *src,size_t n)拷贝n个字符
Char *strncat(char *dest,const char *src,size_t n)把n个字符添加到末尾
Char *strncmp(const char *s1,const char *s2,size_t n)比较前n个字符
Char *strchr(const char *str,int ch) 在str中查找ch首次出现的位置,如果找不到,返回NULL。
Char *strstr(const char *str1,const char *str2)在str1中查找str2首次出现的位置,如果找不到,返回NULL。
Int atoi(const char *nptr)把字符串转换成int整数
long atol(const char *nptr)把字符串转换成long整数
Long long atoll(const char *nptr)把字符串转换成long long整数
预处理指令:
程序员所编写的C代码,并不能被直接编译,而是需要一段程序预先翻译成标准的C代码,负责处理的程序叫做预处理器,处理的过程叫预处理,被翻译的代码叫做预处理指令。所有预处理指令都以#开头 #include 把头文件的内容插入到当前的文件中。 Include <>从系统指定的路径下查找头文件,并插入当前文件 Include”” 先从当前路径下查找头文件,如果没有,再从系统指定的路径下查找头文件,然后插入当前文件。 #define 宏定义指令 定义宏常量: #define 宏名 (字面值)(末尾没有分号) 给字面值增加一个有意义的标识符,增强可读性和可扩展性 代码在预处理过程中会直接把宏名替换成字面值 定义宏函数 #define 宏名(a,b,c) (a+b+c) 不是真正的函数,而是带参数的宏,预处理时将代码替换成宏名后面的语句,所提供的参数会被替换到语句中的位置 不是真正的函数调用,没有参数传递过程,也没有返回值,运行速度快 不限制参数类型,任何类型都可以使用,代码通用性强 没有类型检查,安全性低 过多使用会造成代码冗余,导致代码段增大,浪费内存 可能会产生二义性 定义宏函数的时候给函数的整体和每一个参数加(),避免产生二义性。 宏函数不能换行,可以使用续行符 \代码 使用宏函数时不要使用自变运算符++/–等 宏函数不能换行,可以使用续行符 \代码\ 使用宏函数时不要使用自变运算符++/–等 3)编译时定义宏 Gcc -D DEBUG 4)编译器预定义的宏: __LINE__计算当前代码的行号 __func__获取当前函数名 __FILE__获取当前文件的名字 __DATE__获取当前日期 __TIME__获取当前时间 _cplusplus 分辨出C语言编译器和C++编译器 ③删除宏 #undef ④条件判断 1)#if,#ifdef,#ifndef,#else,#elif,#endif 2)头文件卫士 a.#ifndef #define 内容 #endif 防止头文件重复调用(写头文件的固定格式) 3)代码注释 a.//单行注释,使用麻烦,移植性差早期编译器不支持 b./* */不能嵌套使用 c.#if 0或1 内容 #endif 适合注释大段代码 d.判断代码的编译运行环境 a)#ifdef _cplusplus b)Printf(“C++编译器”) c)#else d)Printf(“C编译器”) e)#endif 4)调试函数 a.#ifdef DEBUG b.#define debug(fmt,…) c.{ d.Printf(“File:%s Line:%d Func:%s Info:”,FILE,LINE,func); e.Printf(“\033[01;34m”); f.Printf(fmt,##VA_ARGS); g.Printf(“\033[00m\n”); h.} i.#else j.#define debug(fmt,…) if(0) k.#endif ⑤其他预处理指令 1)#error “字符串” 产生一条编译错误信息(不生成可执行文件),与#if系列指令配合使用才有意义。 2)#warning ”字符串” 产生一条编译警告(生成可执行文件),与#if系列指令配合使用才有意义。 3)#line line_number “filename”修改当前代码的行号和文件名 4)#pragma 让编译器执行某些指令,柑橘编译器不同,效果可能不同 a.#pargma pack(n) n=2的n次方才有意义,用于修改结构体内存对齐补齐的最大字节数,n<默认最大字节数。 b.#pragma GCC poison goto/”sta” 禁用某变量或标识符
多文件编程:
1、当程序的业务逻辑越来越复杂,代码量越来越多,所有代码写在一个文件中会影响代码的编写、阅读、团队合作,因此为了避免这个问题,可以按功能把代码编写到不同的源文件中,然后给每个源文件编写一个辅助说明的头文件。 2、头文件应该写什么: (1)头文件卫士 (2)结构、联合、枚举类型设计 (3)全局变量的声明,在对应的源文件中定义 (4)函数声明,在对应的源文件中实现 (5)宏常量和宏函数 (6)注意:头文件中的内容能在不同的源文件中重复出现而不会导致冲突。源文件一定要包含自己的头文件,为了检查头文件中的函数声明与源文件中的函数定义是否匹配 3、多文件编译 (1)单独把每个源文件生成目标文件,gcc -c file.c会生成file.o (2)把生成的目标文件合并在一起生成可执行文件,gcc a.o b.o … -o name。 4、Makefile编译脚本 (1)Makefile编译脚本中记录的时项目编译指令的集合,通过make指令批量执行 (2)一般文件中有两大部分内容 ①定义变量 1)CC = gcc 2)FLAG = -Werror -Wall 3)OBJS=tools.o cons.o main.o 4)TARGE=cons ②定义编译目标 1)All: a.$(CC) $(OBJS) -o $(TARGE) 2)Tools.o: $(CC) $(FLAG) tools.c -c 3)Cons.o: 4)Main.o: ③注意使用tab进行缩进,编译目标可以调用其他编译目标,也可以依赖文件,根据目标最后修改时间判断是否需要重新编译.c文件,以此来达到节约编译时间的目的 ④可以通过make命令手动调用编译目标,默认只执行第一个编译目标 5、文件读写 (1)文件类型: ①从变成角度,把文件分成两大类:文本文件、二进制文件 ②二进制文件:存储数据的补码,无法用文本文件直接打开,好处是读取到文件就可以直接使用 ③文本文件:把数据以字符形式存储进去,可以被直接打开,检查数据是否正确,但读写时都需要数据转换 (2)文件的打开操作: ①Fopen(const char* path,const char* mode)功能是打开、创建文件 1)Path文件路径 2)Mode打开模式 a.“r”以只读方式打开文件,如果文件不存在则打开失败 b.“r+”在”r”基础上增加写权限 c.“w”以只写方式打开文件,如果文件不存在则创建,如果文件已经存在则清空内容 d.“w+”在w的基础上增加读权限 e.“a”也是以写权限打开文件,但不会清空文件,如果文件不存在则创建,若存在则将新写入的内容加入到末尾。 f.“a+”在”a”的基础上增加读权限 3)返回值: a.FILE结构指针,该结构里记录着被打开文件的相关数据,是接下来操作文件的凭证 b.若文件打开失败,则返回NULL。 c.如果文件本身没有相关权限则会打开失败 ,终端下ls -l查看权限 (3)文本文件的读写 ①Fprintf(FILE* stream,const char* format,…)功能:以文本格式写入数据到文件 1)Fprintf(fwp,”%s %d %f”,stu.name,stu.id,stu.score); ②Stream:要写入的文件,fopen的返回值 ③Format:格式,占位符 ④…:要写入的变量 ⑤返回值是成功写入的字节数 ⑥Fscanf(FILE* stream,const char* fomat,…) ⑦随机读写:每个打开的文件都有一个指针,记录着该文件读写到哪了,该指针被称为位置指针。 1)“r””r+”方式打开的文件,位置指针指向开头,”a””a+”指向末尾,该指针会随文件的读写操作自动移动 2)Fseek(FILE* stream,long offset,int where) a.功能:设置文件的位置指针 b.Stream:要设置的文件 c.Offset:偏移值 d.Where:基础位置 a)SEEK_SET开头 b)SEEK_END结尾 c)SEEK_CUR当前 e.返回值:成功返回0,失败-1 3)Rewind(FILE *stream)把文件指针指向开头 4)Ftell(FILE stream),获取文件位置指针所指向的字节数。 (4)文件的关闭 ①Fclose(FILE stream)关闭文件。
|