C语言知识点梳理 | 自用

271 阅读23分钟

前言

小记:2024年1月14日至2024年2月24日,在B站看了比特鹏哥的《C语言程序设计从入门到进阶》,根据笔记以及课件总结此C语言知识点梳理,以备后续复习使用。 在这里插入图片描述


一、数据类型

1.1整型家族

char    //字符数据类型  1byte
short    //短整型  2byte
int    //整形  4byte
long    //长整型  4/8byte
long long    //更长的整形  8byte
unsigned XXX    //无符号XXX
singned XXX    //有符号XXX

1.1.1整形在内存中的存储

整形在内存中存放的是补码。

  • 原码:将数值按照正负数的形式翻译成二进制
  • 反码:将原码的符号位不变,其他位依次按位取反
  • 补码:反码+1

(负数的原码与补码之间的快速转换:从右往左一直到第一个1不变,之前的除了符号位其他全部按位取反。)

1.1.2大小端

  • 大端(存储)模式:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。(方便人类阅读)
  • 小端(存储)模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。(方便机器操作)

1.2浮点型家族

float    //单精度浮点数  4byte
double    //双精度浮点数  8byte
long double    //

1.2.1浮点型在内存中的存储

  1. 根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(1)SM2E (-1)^S * M * 2^E

(1)S(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。 M表示有效数字,大于等于1,小于2。 2E2^E表示指数位。

  1. IEEE 754规定:
  • 对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。
  • 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
  1. 指数E:一个无符号整数(unsigned int) IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数 是127;对于11位的E,这个中间数是1023。
  • E不全为0或不全为1:指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
  • E全为0:浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于 0的很小的数字。
  • E全为1:如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。

1.3构造类型(自定义类型)

1.3.1数组类型

数组:一组相同类型元素的集合,下标从0开始,在内存中连续存放,注意检查越界问题。

  1. 一维数组:如果初始化,[ ]内数字可以省略
int arr[ ] = {1,2,3};
char ch1[ ] = {‘a’,’b’,’c’};
char ch2[ ] = “abc”;    //默认结尾为’\0’
int sz = sizeof(arr) / sizeof(arr[0]);    //计算数组大小
  1. 二维数组:[ ]内数字行可以省略,列不可以省略
int arr[3][4] = {1,2,3,4,2,3,4,5};
int arr[3][4] = {{1,2},{3,4},{5,6}};
int arr[ ][4] = {{1,2},{3,4}};
int row = sizeof(arr) / sizeof(arr[0])    //计算行
int column = sizeof(arr[0]) / sizeof(arr[0][0])    //计算列
  1. 数组作为函数参数

数组名是数组首元素的地址。(有两个例外)

  • ①sizeof(数组名),数组名表示整个数组。
  • ②&数组名,数组名表示整个数组。&arr+1会跳过整个数组。

二维数组数组名表示的首元素地址是第一行一整行的地址。

1.3.2结构体

typedef struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}Stu;//分号不能丢 
  1. 零碎概念
  • 结构的成员可以是标量、数组、指针,甚至是其他结构体。
  • 结构体成员访问:结构体对象.成员名;结构体指针->成员名。
  • 函数传参需要压栈,为减小系统开销,结构体传参最好传地址。
  • 匿名结构体类型:声明结构的时候不完全的声明。(一次性)
  1. 内存对齐规则 (拿空间来换取时间的做法)
  • ①第一个成员在与结构体变量偏移量为0的地址处。
  • ②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8 Linux中没有默认对齐数,对齐数就是成员自身的大小
  • ③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • ④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

1.3.3位段

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
}
  1. 零碎概念
  • 相对于结构体,(缝缝补补勤俭持家型结构体)位段可以很好的节省空间,但是存在跨平台的问题。一般应用于网络的数据报中。
  • 位段的成员必须是 int、unsigned int 或signed int 或整型家族(char)。
  • 成员名后边有一个冒号和一个数字(比特位,不能超过原类型的大小)。
  1. 内存分配
  • ①位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  • ②位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  • ③位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

1.3.4枚举

enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
};

一一列举。不占空间,只有创建变量才申请空间。

  • 优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加>严谨。
  3. 便于调试
  4. 使用方便,一次可以定义多个常量

1.3.5联合(共用体)

union Un
{
 char c;
 int i;
}; 成员公用同一块空间。

大小的计算:联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。(数组看的是类型的大小,而不是数组的大小)

1.4指针类型

int* pi;
char* pc;
float* pf;
void* pv;

1.4.1零碎概念

  • 指针就是地址,是内存中一个最小单元的编号,口语中说的指针通常指的是指针变量,指针变量,用来存放地址的变量。
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节。
  • 二级指针:用来存放一级指针变量的地址

1.4.2指针运算

  • 指针的类型决定了指针被解引用时访问几个字节以及+1/-1操作跳过几个字节。
  • 指针-指针的绝对值是两个指针之间元素的个数。

1.4.3野指针

指针指向了不可知的位置。

  • 成因:1. 指针未初始化 2. 指针越界访问 3. 指针指向的空间释放
  • 规避:1. 指针初始化 2. 小心指针越界 3. 指针指向空间释放,及时置NULL 4. 避免返回局部变量的地址 5. 指针使用之前检查有效性

1.4.4指针和数组

arr[i] 等价于 *(arr+i)
int arr[10];    //整型数组
int* p1[10];    //指针数组:存放指针的数组
int (*p2)[10];    //数组指针:指向数组的指针    类型:int (*)[ ]
int (*parr3[10])[5];     //存放数组指针的数组    存放了10个数组指针(int(*)[5])

1.4.5数组传参

  • 一维数组传参
#include <stdio.h>
void test(int arr[])//1
void test(int arr[10])//2
void test(int *arr)//3

void test2(int *arr[20])//1
void test2(int **arr)//2
int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};
 test(arr);
 test2(arr2);
}
  • 二维数组传参
void test(int arr[3][5])//1
void test(int arr[][5])//2
void test(int (*arr)[5])//3
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

1.4.6函数指针

指向函数的地址。函数名/&函数名都是函数的地址。

Int Add(int x, int y)
{
 return x+y;
}
int main()
{
 int (*pf)(int, int) = &Add;
 int ret = (*pf)(2, 3);    //*可以不用写    pf(2, 3)
}
函数指针数组:int(*pfArr[ ])(int , int) = { 0, Add, Sub, Mul, Div }; //转移表
指向函数指针数组的指针:int(*(*pfArr)[ ])(int , int) = &pfArr;
void test(const char* str)
{
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0;
}

1.4.7回调函数

一个通过函数指针调用的函数。 例如:qosrt函数的使用者得实现一个比较函数

1.5空类型

void 表示空类型(无类型)。通常应用于函数的返回类型、函数的参数、指针类型。


二、操作符

2.1算术操作符

+    -   *   /   %  
1 / 2 = 0;    //整型的除法
1.0 / 2 = 0.5    //浮点型的除法
% 两端必须是整数

2.2移位操作符

<<    //左移操作符, 左边抛弃、右边补0(相当于乘二)
>>    //右移操作符. 1. 逻辑移位 左边用0填充,右边丢弃 2. 算术移位 左边用原该值的符号位填充,右边丢弃

注:移位操作符的操作数只能是整数。

2.3位操作符

&    //按位与
|    //按位或
^    //按位异或

注:位操作符的操作数必须是整数。

2.4赋值操作符

=    +=    -=    *=    /=    %=    >>=    <<=    &=    |=    ^=

表达式如果不能通过操作符的属性确定唯一的计算路径,则该表达式存在问题。

2.5单目操作符

!           逻辑反操作
-           负值
+           正值
&           取地址
~           对一个数的二进制按位取反
--          前置、后置--
++          前置、后置++
*           间接访问操作符(解引用操作符)
(类型)       强制类型转换
sizeof      操作数的类型长度(以字节为单位)不关注存放的具体内容

而strlen是一个库函数,专门求字符串长度,只能针对字符串,从给定的地址向后找‘\0’,统计‘\0’之前字符的个数

2.6关系操作符

>
>=
<
<=
!=   用于测试“不相等”
==      用于测试“相等”

2.7逻辑操作符

&&     逻辑与    左边为假右边就不用计算了
||          逻辑或    左边为真右边就不用计算了

2.8条件操作符(三目操作符)

exp1 ? exp2 : exp3    //条件满足执行exp2,不满足执行exp3,只会执行其中一个

2.9逗号表达式

exp1, exp2, exp3, …expN    //从左向右依次执行,结果为最后一个表达式的结果。

2.10下标引用、函数调用和结构成员

  • ①[ ] 下标引用操作符 操作数:一个数组名 + 一个索引值
  • ②( ) 函数调用操作符
  • ③访问一个结构的成员: 结构体.成员名 -> 结构体指针->成员名

2.11表达式求值

  1. 隐式类型转换

  • 整型提升: C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型。整形提升是按照变量的数据类型的符号位来提升的。
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
  • 整型提升的意义:
  • ①表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
  • ②因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
  • ③通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
  1. 算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类 型,否则操作就无法进行。

  1. 操作符的属性

1 操作符的优先级 2. 操作符的结合性 3. 是否控制求值顺序。

  • 操作符优先级 在这里插入图片描述 在这里插入图片描述

三、控制语句

  • C语句可分为以下五类: 1. 表达式语句 2. 函数调用语句 3. 控制语句 4. 复合语句 5. 空语句。

3.1分支语句

  • ①if语句
语法结构:
if(表达式)
    语句;
if(表达式)
    语句1;
else
    语句2;
//多分支    
if(表达式1)
    语句1;
else if(表达式2)
    语句2;
else
    语句3;
  • ②switch语句
switch(整型表达式)
{
    case 整形常量表达式:
    语句;
        break;
    case 整形常量表达式:
    语句;
        break;
    default:
    语句;
        break; 
}

3.2循环语句

  • ①while循环 //表达式为真(非零),重复执行循环语句;为假(零)跳出
while(表达式)
 循环语句;
  • ②for循环 //表达式1为初始化部分, 表达式2为条件判断部分, 表达式3为调整部分
for(表达式1; 表达式2; 表达式3)
 循环语句;
  • ③do...while()循环 //至少执行一次循环语句
do
 循环语句;
while(表达式);
  • ④goto语句 //尽量少用,只能在一个函数里使用,常见用法终止深度嵌套的结构
// goto语言真正适合的场景如下:
for(...)
    for(...)
   {
        for(...)
       {
            if(disaster)
                goto error;
       }
   }
    …
error:
 if(disaster)
         // 处理错误情况

四、函数

4.1函数定义

  • ①C语言中函数的分类:1. 库函数 2. 自定义函数

  • ②函数的参数:1. 实际参数(实参) 2. 形式参数(形参)

  • ③函数的调用:1. 传值调用 2. 传址调用

  • ④函数的嵌套调用和链式访问

  • ⑤函数的声明和定义

  • ⑥函数递归 把大事化小 两个必要条件:1. 存在限制条件 2. 每次递归调用之后越来越接近这个限制条件。
  • ⑦递归与迭代

4.2字符函数和字符串函数

  • ①求字符串长度
strlen    //求字符串长度,‘\0’结束    size_t为无符号整型
size_t strlen ( const char * str );
  • ②长度不受限制的字符串函数
strcpy    //字符串拷贝,包括’\0’
char* strcpy(char * destination, const char * source );
strcat    //字符串追加
char * strcat ( char * destination, const char * source );
strcmp    //字符串比较,相等返回0;大于返回大于0的数字;小于返回小于0的数字
int strcmp ( const char * str1, const char * str2 );    //比较的是内容,不是长度
  • ③长度受限制的字符串函数
strncpy
char * strncpy ( char * destination, const char * source, size_t num );
strncat
char * strncat ( char * destination, const char * source, size_t num );
strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
  • ④字符串查找
strstr    //查找子串,返回子串地址
char * strstr ( const char *str1, const char * str2);
strtok    //切割字符串,会改变被操作的字符串,第二次之后传NULL指针
char * strtok ( char * str, const char * sep );
  • ⑤错误信息报告
strerror    //返回错误码,所对应的错误信息。
char * strerror ( int errnum );
errno    C语言设置的一个全局的错误码存放的变量
  • ⑥字符操作 在这里插入图片描述

  • ⑦内存操作函数

memcpy    //两块独立空间的内存拷贝
void * memcpy ( void * destination, const void * source, size_t num );
memmove    //重叠内存的拷贝
void * memmove ( void * destination, const void * source, size_t num );
memcmp    //内存比较,一对字节一对字节的比较
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
memset    //内存设置

五、动态内存管理

5.1动态内存函数

  • ①malloc //按照字节开辟内存,返回起始地址指针,失败返回NULL(在堆区申请的)
void* malloc (size_t size);
  • ②free //释放
void free (void* ptr);
//用法
free(p);
p = NULL;
  • ③calloc //开辟空间后会把每个字节初始化为0
void* calloc (size_t num, size_t size);
  • ④realloc //对动态开辟内存大小 的调整。
void* realloc (void* ptr, size_t size);

5.2常见的动态内存错误

  • ①对NULL指针的解引用操作(申请完先检测)
void test()
{
 int *p = (int *)malloc(100);
 if(NULL != p)    //检测
 {
 *p = 20;
 }
}
  • ②对动态开辟空间的越界访问
  • ③对非动态开辟内存使用free释放
  • ④使用free释放一块动态开辟内存的一部分(p++的话p已经指向后面,应该用p[i]/*(p+i))
  • ⑤对同一块动态内存多次释放
  • ⑥动态开辟内存忘记释放(内存泄漏)

5.3柔性数组(flexible array)

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
 int a[];//柔性数组成员,有些编译器会报错无法编译可以改成这样
}type_a;
//使用
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
  • 特性: 1.结构中的最后一个元素允许是未知大小的数组。 2.柔性数组成员前面必须至少一个其他成员。 3.sizeof 返回的这种结构大小不包括柔性数组的内存。 4.使用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。 5.好处:方便内存释放;有利于访问速度。

六、文件操作

6.1文件

  • ①程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。
  • ②数据文件 文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件。
  • ③文件名:文件路径+文件名主干+文件后缀

6.2文件的打开和关闭

  • ①文件指针(文件类型指针)
FILE* pf;    //文件指针变量,通过文件指针变量能够找到与它关联的文件。
  • ②文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。

//打开文件

FILE * fopen ( const char * filename, const char * mode );

//关闭文件

int fclose ( FILE * stream );

打开方式: 在这里插入图片描述

6.3文件的顺序读写

  • ①顺序读写函数 在这里插入图片描述

  • ②函数对比

scanf    //从标准输入(键盘)读取格式化的数据。
int scanf( const char *format [,argument]... );
fscanf    //从所有的输入流读取格式化的数据。
int fscanf ( FILE * stream, const char * format, ... );
sscanf    //从字符串中读取一个格式化的数据。
int sscanf ( const char * s, const char * format, ...);
printf    //把格式化的数据到标准输出(屏幕)上。
int printf( const char *format [, argument]... );
fprintf    //把格式化的数据输出到所有输出流(屏幕/文件)上。
int fprintf ( FILE * stream, const char * format, ... );
sprintf    //把格式化的数据输出转换成字符串。
int sprintf ( char * str, const char * format, ... );

6.4文件的随机读写

  • ①fseek //根据文件指针的位置和偏移量来定位文件指针。
int fseek ( FILE * stream, long int offset, int origin );
  • ②ftell //返回文件指针相对于起始位置的偏移量。
long int ftell ( FILE * stream );
  • ③rewind //让文件指针的位置回到文件的起始位置。
void rewind ( FILE * stream );

6.5文本文件和二进制文件

  • ①文本文件:求在外存上以ASCII码的形式存储,则需要在存储前转换。
  • ②二进制文件:数据在内存中以二进制的形式存储,不加转换的输出到外存。

6.6文件读取结束的判定

①被错误使用的feof 牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。 feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。判断读取失败而结束用ferror。

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。(fread判断返回值是否小于实际要读的个数。)

6.7文件缓冲区

  • ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序 中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装 满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓 冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根 据C编译系统决定的。

七、程序环境和预处理

7.1程序的翻译环境和执行环境

  • 翻译环境:在这个环境中源代码被转换为可执行的机器指令。
  • 执行环境:用于实际执行代码。

在这里插入图片描述

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

7.2预处理详解

7.2.1预定义符号

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

7.2.2#define

  • ①#define 定义标识符
语法:
 #define name stuff

在define定义标识符的时候,建议不要加上 ; ,这样容易导致问题。

  • ②#define 定义宏
#define name( parament-list ) stuff

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro)。(宏名习惯全大写) 注意:

  1. 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。(多加括号)。 2.参数列表的左括号必须与name紧邻。
  • ③#define 替换规则
  • 步骤:

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
  • ④#和## #:把一个宏参数变成对应的字符串。
int i = 10;
#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);

##:把位于它两边的符号合成一个符号。

#define ADD_TO_SUM(num, value) \
 sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
  • ⑤注意带副作用的宏参数
  • ⑥#undef //这条指令用于移除一个宏定义。
#undef NAME    //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
  • ⑦宏和函数对比
  • 宏的优点:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。 3. 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
  • 宏的缺点:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
  • 宏和函数的对比 在这里插入图片描述

7.3命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

编译指令:
//linux 环境演示
gcc -D ARRAY_SIZE=10 programe.c

7.4条件编译

常见的条件编译指令:

  • 1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
  • 2.多个分支的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
  • 3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
  • 4.嵌套指令
#if defined(OS_UNIX)
 #ifdef OPTION1
 unix_version_option1();
 #endif
   #ifdef OPTION2
 unix_version_option2();
 #endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
 msdos_version_option2();
 #endif
#endif

7.5文件包含

  • #include "filename" //本地文件包含 查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。
  • #include < filename > //库文件包含 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。库文件也可以用“”,但是会降低效率。
  • #pragma once //防止头文件被多次包含