C语言速成笔记
第一章 分置于循环
一、选择结构
1. if 语句
条件判断:符合其中一个进入,如果同时符合两个条件,则只会进入第一个条件
if (条件)
{
//内容
}
else if (条件)
{
//内容
}
else
{
//内容
}
2. switch 语句
如果 switch 接收到的数字在下面的 case 里,则进入,否则进入 default 里面,注意不要忘记 break。
switch (int 类型的数字) {
case 1: //如果数字为 1 则进入 case 1:
//内容
break;
case 2: //如果数字为 2 则进入 case 2:
//内容
break;
case 3: //如果数字为 3 则进入 case 3:
//内容
break;
case 4: //如果数字为 4 则进入 case 4:
//内容
break;
default: //如果以上数字均没有 则进入 default:
//内容
break;
}
3. 条件操作符
条件 ? expTrue : expFalse
二、循环结构
1. while 循环
如果条件成立就进入循环,不成立就进不去循环
while (条件)
{
//内容
}
2. do while 循环
判断循环在末尾,先运行一遍,如果条件成立就进入循环,不成立就进不去循环
do
{
// 内容
}
while (condition);
3. for 循环
初始化只在遇到 for 循环市执行一次,后续循环不再执行。条件判断与 while 相同。调整是每次循环结束后,下一次条件判断之前执行。
for (初始化; 条件判断; 调整)
{
// 内容
}
第二章 函数
一、函数的概念
ret_type fun_name (para1, *)
返回类型 函数名 函数参数(形参)
1.实参与形参
实参:
真实传给函数的参数,叫做实参,实参可以是变量,常量,表达式,函数等。无论实参是何种类型的量,在函数调用时,他们必须有确定的值,以便把这个值传给形参。
形参:
形实参数是指函数名后括号中的变量,因为形参只有在函数被调用的过程中才实例化,(分配内存单元) 形式参数在函数调用玩之后就自动销毁了,因此形式参数只在函数中有效
2.传值调用与传址调用
传值调用:
函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。
传址调用:
是把函数外部创建变量的内存地址,传递给函数参数的一种调用函数的方式,这种传入方式可以让函数和函数外部的变量建立起真正的联系,也就是说函数内部可以直接操作函数外部的变量 (传入数组时,传入的仅仅是数组第一个元素的地址,大小为4/8个字节, 地址++ == arr[i++])
二、库函数 <string.h>
strcat
char *strcat(char *dest, const char *src);
strcat:把 src 所指向的字符串追加到 dest 所指向的字符串的结尾
原理详解:
- 两个字符串必须以
'\0'
结束 - 目标空间必须有足够的大,能容纳下源字符串的内容
- 目标空间必须可修改
- 不可追加自己
- 可通过修改传入src的地址改变追加的起始位置
strncat
char *strncat(char *dest, const char *src, size_t n);
strncat:把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止
原理详解:
- 两个字符串必须以
'\0'
结束 - 目标空间必须有足够的大,能容纳下源字符串的内容
- 目标空间必须可修改
- 不可追加自己
- 可通过修改传入src的地址改变追加的起始位置
strchr
char *strchr(char *s, char c);
strchr:在参数 s 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的地址
原理详解:
- 遍历整个字符串查找指定字符
- 如果找到返回该字符的地址
- 如果没找到返回空指针NULL
strstr
char *strstr(char const *s1, char const *s2);
strstr:在字符串 haystack 中查找第一次出现字符串 needle(不包含空结束字符)的位置
原理详解:
- 遍历整个字符串查找指定字符串
- 如果找到对应的字符,则以该字符为起点的一段字符进行字符串比较
- 如果相等返回这段字符串起点的地址
- 如果不想等则继续遍历直到结束
- 若最终未找到对应的字符串则返回空指针NULL
strcmp
int strcmp(const char *str1, const char *str2);
strcmp:把 str1 所指向的字符串和 str2 所指向的字符串进行比较
原理详解:
- 比较ASCII码值
- 第一个字符串 > 第二个字符串,则返回 大于0 的数字
- 第一个字符串 = 第二个字符串,则返回 0
- 第一个字符串 < 第二个字符串,则返回 小于0 的数字
strncmp
int strncmp(const char *str1, const char *str2, size_t n);
strncmp:把 str1 和 str2 进行比较,最多比较前 n 个字节
原理详解:
- 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完
- 第一个字符串 > 第二个字符串,则返回 大于0 的数字
- 第一个字符串 = 第二个字符串,则返回 0
- 第一个字符串 < 第二个字符串,则返回 小于0 的数字
strcpy
char *strcpy(char *dest, const char *src);
strcpy:把 src 所指向的字符串复制到 dest
原理详解:
- 源字符串必须以’\0’结束
- 会将源字符串中的’\0’拷贝到目标空间
- 目标空间必须足够大,以确保能存放源字符串(不够大也依然能全部拷贝,但会报错)
- 目标空间必须可变(不能是常量字符串)
strncpy
char *strncpy (char *dest, const char *src, size_t n);
strncpy:把 src 所指向的字符串复制到 dest,最多复制 n 个字符
原理详解:
- 拷贝num个字符从源字符串到目标空间
- 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个
strlen
size_t strlen(const char *str);
strlen:计算字符串的长度
原理详解:
- 字符串以’\0’作为结束标志,strlen函数返回的是在字符串中’\0’前面出现的字符个数(不包含’\0’)
- 参数指向的字符串必须要以’\0’结束
- 注意函数的返回值为size_t,是无符号的
strtok
char *strtok (char * str, const char * sep);
strtok:用于分隔字符串
str
,并删除str
中含有sep
的字符
原理详解:
sep
参数是个字符串,定义了用作分隔符的字符集合- 第一个参数指定一个字符串,它包含了0个或者多个由
sep
字符串中一个或者多个分隔符分割的标记 strtok
函数找到str
中的下一个标记,并将其用‘\0’
结尾,返回一个指向这个标记的指针。- 注:
strtok
函数会改变被操作的字符串,所以在使用strtok
函数切分的字符串一般都是临时拷贝的内容并且可修改。 strtok
函数的第一个参数不为NULL
,函数将找到str中第一个标记,strtok
函数将保存它在字符串中的位置。strtok
函数的第一个参数为NULL
,函数将在同一个字符串中被保存的位置开始,查找下一个标记。- 如果字符串中不存在更多的标记,则返回
NULL
指针。
memcpy
void *memcpy(void *dest, const void *src, size_t n);
memcpy:从 src 复制 count 个字节到 dest
原理详解:
- 内存复制一个一个字节拷贝
- 将每个字节在内存中存储的值进行拷贝
- 不再局限于字符串的拷贝
memmove
void *memmove (void *destination, const void *source, size_t num);
memmove:从 src 复制 count 个字节到 dest
原理详解:
- 与memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的
- 如果源空间和目标空间出现重叠,就得使用memmove函数处理
memset
void *memset(void *str, int num, size_t len);
memset:对每个字节进行初始化
原理详解:
- 以字节为单位设置内存
- 每个字节设置为一个值
- 设置len个字节
memcmp
int memcmp(void const *s1, void const *s2, int len);
memcmp:比较从 s1 和 s2 指针开始的 num 个字节
原理详解:
- 内存比较一个字节一个字节比较
- 判断每个字节在内存中存储的值是否相等
- 第一个字符串 > 第二个字符串,则返回 大于0 的数字
- 第一个字符串 = 第二个字符串,则返回 0
- 第一个字符串 < 第二个字符串,则返回 小于0 的数字
三、递归
递归:
- 什么是递归:在c语言中,一个调用自身(不管是直接地还是间接地)的函数被称为是递归的(recursive)。
- 栈溢出:调用函数时,函数会向栈申请空间,当递归陷入死循环时,栈的空间被耗尽,称为栈溢出(stak overflow)
- 递归的必要条件:
- 存在限制条件,使得满足限制条件时,递归不再继续
- 每次递归调用后越来越接近这个限制条件
四、内存
内存:
- 栈区(临时变量):局部变量,函数参数,调用函数时的返回值
- 堆区():动态内存分配
- 静态区:全局变量,静态变量;全局的int x = 0; static int y = 0;
第三章 数组
一、一维数组
1.调用格式
type_t arr_name [const_n] = {};
- type_t:数组的元素类型
- arr_name:数组的元素名
- const_n:是一个常量表达式,用来指定数组大小
2.变长数组
C99支持-> 变长数组
int n = 10;
int arr[n];
3.初始化
//完全初始化
int arr1[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
//不完全初始化
int arr2[10] = {0, 1, 2, 3, 4, 5};
int arr3[/*6*/] = {0, 1, 2, 3, 4, 5};
//sizeof 字节长度总合
char num0[] = {'a', 'b', 'c'};
//strlen 以'\0'为查询标志
char num1[/*4*/] = "abc";
char num2[10] = "abc";
char num3[/*4*/] = {'a', 'b', 'c', '\0'};
char num4[10] = {'a', 'b', 'c'};
二、二维数组
1.调用格式
int arr[3][4];//越靠左位次越高
2.初始化
int arr1[3][4] = {1,2,3,4,5,6,7};
//[1] [2] [3] [4]
//[5] [6] [7] [0]
//[0] [0] [0] [0]
int arr2[3][4] = {{1,2,3,4},{5,6},{7,8}};
//嵌套,将打括号所有内容当做一个元素
//[1] [2] [3] [4]
//[5] [6] [0] [0]
//[7] [8] [0] [0]
三、数组的应用
应用:
- 数组名是首元素的地址
- 2.sizeof(数组名):数组名表示整个数组 - 计算的使整个数组的大小,单位是字节
- &数组名:数组名表示整个数组 - 取出的是整个数组的地址
int arr[10] = {0};//结果都相等,但意义不同
printf("%p\n", arr);//arr代表首元素的地址
printf("%p\n", &arr[0]);//&arr[] == arr
printf("%p\n", &arr);//arr本身就是地址,取地址的地址就是取整个数组的地址
第四章 操作符
一、位移操作符
1.左移操作符
左移操作符(<< 左移),移动的是二进制位,先化成二进制再移位
例如1的二进制表示为0001,向左移位变成0010,最后化成十进制,用法与四则运算相同
int num1 = 1;
int num2;
num2 = num1 << 2;//num1化成二进制向左移动2位,最左边的数丢弃,最右边补一个零
printf("%d\n", num2);
num1 = num2 >> 2;//num1化成二进制向右移动2位,最右边的数丢弃,最左边补一个零
printf("%d\n\n", num1);//num2化成二进制向右移动2位
2.右移操作符
右移操作符(右移 >>),移动的是二进制位,先化成二进制再移位
- 算数右移:右边丢弃,左边补原符号位(0/1) — 常用
- 逻辑右移:右边丢弃,左边补0 — 不常用
二、位操作符
1.按位与 &
int a = 4;//0 1 0 0
int b = 5;//0 1 0 1
int num1 = a & b;//取交集为 0 1 0 0
printf("4 & 5 = %d\n", num1);
int c = 3;//0 0 1 1
int d = 5;//0 1 0 1
int num2 = c & d;//取交集为 0 0 0 1
printf("3 & 5 = %d\n", num2);
2.按位或 |
int e = 4;//0 1 0 0
int f = 5;//0 1 0 1
int num3 = e | f;//取并集为 0 1 0 1
printf("4 | 5 = %d\n", num3);
int g = 3;//0 0 1 1
int h = 5;//0 1 0 1
int num4 = g | h;//取并集为 0 1 1 1
printf("3 | 5 = %d\n", num4);
3.按位异或 ^
int i = 4;//0 1 0 0
int j = 5;//0 1 0 1
int num5 = i ^ j;//取非交集为 0 0 0 1
printf("4 ^ 5 = %d\n", num5);
int k = 3;//0 0 1 1
int l = 5;//0 1 0 1
int num6 = k ^ l;//取非交集为 0 1 1 0
printf("3 ^ 5 = %d\n", num6);
三、逻辑操作符
1.逻辑与
&& 逻辑与 - 两边同时成立才成立才行
if (a == 2 && b == 3) {
printf("true\n");
} else {
printf("false\n");
}
2.逻辑或
|| 逻辑或 - 其中一个成立即成立即可
if (a == 2 || b == 3) {
printf("true\n");
} else {
printf("false\n");
}
四、常用操作符
1.取反 !
scanf("%d", &input);
if (input)//input 为真即不等于0
{
printf("%d为真", input);
}
else if(!input)//input为假即等于0
{
printf("%d为假", input);
}
2. 字节长度 sizeof
括号里的内容不参与运算,但它求的值实际是-推算-之后的值的类型和长度
int a = 5;
short b = 8;
int arr[10] = { 0 };
printf("%lu\n", sizeof(arr));
printf("%lu\n", sizeof(int [10]));
printf("%lu\n", sizeof a );
printf("%lu\n", sizeof(a));
printf("%lu\n", sizeof(int));
printf("%lu", sizeof(b = a + 2));//依然为short型,放到谁里面谁的类型说了算
printf("%d", b);//sizeof在编译期间算出2执行完毕,在运行期间将不再执行
3.强制类型转换 ()
int num = (int) 3.14;
//强制转换成int类型
第五章 指针
一、指针的定义
指针类型决定了,指针解引用的权限有多大
//char *权限详解
int a = 0x11223344;
char *pc = &a;
*pc = 0;
printf("%x",a);//结果为11223300
//说明char *的解引用权限为1个字节
//int *权限详解
a = 0x11223344;
int *pi = &a;
*pi = 0;
printf("%x",a);//结果为0
//说明int *的解引用权限为4个字节
指针类型决定了,指针走一步,能走多远(步长)
int arr[3] = {1,2,3};
char *pc = arr; //地址+1
int *pi = arr; //地址+4
printf("%d\n", *pc); //结果为1
printf("%d\n", *(pc + 1));//结果为0
printf("%d\n", *pi); //结果为1
printf("%d\n", *(pi + 1));//结果为2
*(pc + 1) = 9;
printf("%d\n", *pc); //结果为1
printf("%d\n", *(pc + 1));//结果为9
printf("%d\n", *pi); //发生了固定变化,结果为2305
printf("%d\n", *(pi + 1));//结果为2
*(pc + 1) = 0;
printf("%d\n", *pc); //结果为1
printf("%d\n", *(pc + 1));//结果为0
printf("%d\n", *pi); //结果为1
printf("%d\n", *(pi + 1));//结果为2
二、野指针
野指针:
- 未被定义指针
- 超出数组的范围-指针的越界访问
- 指针指向的空间释放
三、指针的运算
运算:
- 指针的关系运算,用指针直接比较大小
- 指针加减整数
- 指针-指针:得到的是两个指针之间的元素个数(两个指针必须指向同一块空间)
- 允许指向数组的指针与指向数组最后一个元素的下一个元素的指针进行比较但是不允许与指向第一个元素的前一个元素的指针进行比较
int ret = 0;
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
ret = &arr[9] - &arr[0];
printf("%d", ret);
四、指针与数组
arr[9] == 9[arr]
运算符[],满足交换律
数组运算时转化为*(arr + 9),因此 arr[9] == 9[arr]
arr[9] == *(arr + 9) == *(p + 9) == *(9 + p) == *(9 + arr) == 9[arr]
五、二级指针
int a = 10;
int *p = &a;// *说明是指针类型,int说明指向的是int类型的量
int **pp = &p;// 第二个*说明是指针类型,int *说明指向的是int *类型的量
int ***ppp = &pp;// 第三个*说明是指针类型,int **说明指向的是int **类型的量
第六章 结构体
一、建立结构声明
结构声明( structure declaration )描述了一个结构组织布局
struct PeoInfo {
char Name[8];
char Sex[4];
int Age;
char Telephone[12];
char Address[12];
};
该声明描述了一个由四个字符数组和一个整型变量组成的结构。该声明并未创建实际的数据,只是描述了该对象由什么组成。
从本质上看,结构声明创建了一个名为 struct PeoInfo 的新类型( int double 等类型等价) 。
struct:
- 它表明其后的是一个结构
- struct PeoInfo 则是这个结构的名称
二、创建结构变量
结构变量定义了一个 struct PeoInfo 类型的变量
struct PeoInfo Mail;
此时此刻才向堆栈申请了一块sizeof(struct PeoInfo)
大小的空间(这里是为了便于理解所以恰好设置了40个字节的大小布局,后面会有结构体大小的计算详解)。
三、初始化结构体
struct PeoInfo 类型也可以直接进行初始化
struct PeoInfo Mail = {"小明", "男", 18, "11111111111", "某小区"};
struct PeoInfo和int等其他类型一样,可以直接进行赋值,它的内容需要由花括号括起来,各初始化项由逗号进行分隔。
四、访问结构成员
结构体成员的基本访问( 对象
.
访问成员 或 指针->
访问成员 )
printf("%s", Mail.Name);
printf("%s", Mail.Sex);
printf("%d", Mail.Age);
printf("%s", Mail.Telephone);
printf("%s", Mail.Address);
本质上Name、Sex、Age、Telephone、Address都是 struct PeoInfo 的下标,就像数组的下标0、1、2、3一样,数组是用[ ]
来访问所在下标位置的元素, 而结构体则是用.
和->
来访问成员。
第七章 动态内存管理
一、 malloc
void *malloc (size_t size);
malloc:对任意类型都可以动态内存开辟的函数
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个
NULL
指针,因此 malloc 的返回值一定要做检查。 - 返回值的类型是
void*
,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size
为0,malloc 的行为是标准是未定义的,取决于编译器。
二、 calloc
void *calloc (size_t num, size_t size);
calloc:对任意类型都可以动态内存开辟的函数,并且把空间的每个字节初始化为0
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针,并且把空间的每个字节初始化为0。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个
NULL
指针,因此 malloc 的返回值一定要做检查。 - 返回值的类型是
void*
,所以 calloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size
为0,calloc 的行为是标准是未定义的,取决于编译器。 - 函数的功能是为
num
个大小为size
的元素开辟一块空间,并且把空间的每个字节初始化为0。 - 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
三、 realloc
void *realloc (void* ptr, size_t size);
realloc:函数就可以对动态开辟内存大小的调整
- realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存, 我们往往会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
ptr
是要调整的内存地址。size
是调整后的新大小。- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc 在调整内存空间的是存在两种情况:
-
原有空间之后有足够大的空间:要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
-
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来 使用。这样函数返回的是一个新的内存地址。
四、 free
void free (void* ptr);
free:函数用来释放动态开辟的内存
- 如果参数
ptr
指向的空间不是动态开辟的,那 free 函数的行为是未定义的。 - 如果参数
ptr
是NULL指针,则函数什么事都不做。
第八章
一、文件打开与关闭
fopen
FILE *fopen("文件名.后缀", "打开方式");
返回一个指针,需要判断是否为空指针
fclose
int fclose(文件地址);
关闭成功返回0,否则返回EOF(-1)
FILE *P_File = fopen("文件名.后缀", "打开方式");
if (P_File == NULL) {
perror("打开文件失败:");
}
fclose(P_File);
2.文件的打开方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 出错 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
二、文件的顺序读写
1.字符输入输出(fputc & fgetc)
fputc
int fputc(int , FILE *stream);
向文件写入数据(从末尾开始写入)
fputc('1', P_File);
fputc('2', P_File);
fputc('3', P_File);
fgetc
int fgetc(FILE *stream);
向文件读数据(从开头开始按顺序读,读一次向后走一格)
- 读取结束返回EOF
- 正常读取返回ASCII码值
int ret = fgetc(P_File);
printf("%c", ret);
2.文本行输入输出函数(fputs & fgets)
fputs
int fputs(const char *str, File *stream);
向文件写入数据(从末尾开始写入)
fputs("abcdef\n", P_File);
fputs("123456\n", P_File);
fgets
char *fgets(char *str, int n, FILE *stream)
读取数 n 包括
'/0'
的位置,所以最多读 n - 1 个
fgets(arr, 5, P_File);
printf("%s\n", arr);
fgets(arr, 20, P_File);
printf("%s\n", arr);
3.格式化输入输出函数(fprintf & fscanf)
fprintf
int fprintf(File *stream, 后面与printf一致);
向文件写入数据(从末尾开始写入)
fprintf(P_File, "%s\n", "hello world!");
fscanf
int fscanf(File *stream, 后面与scanf一致);
读取(从开始位置)
fscanf(P_File, "%s", arr);
printf("%s\n", arr);
4.二进制输入输出函数(fwrite & fread)
fwrite
int fwrite(要写入的元素的地址, 每个元素的大小, 元素的数量, File *stream);
以二进制的方式写入(字符串以二进制和文本写入结果一样)
fwrite(arr, sizeof(char), 20, P_File);
fread
int fread(要读取的元素的地址, 每个元素的大小, 元素的数量, File *stream);
返回读取到的完整元素的个数,如果读取到的个数 < 实际要读的个数,则为最后一次读取
fread(arr, sizeof(char), 20, P_File);
三、文件的随机读写与结束判定
1.文件的随机读写
fseek
int fseek("文件", 偏移量, 偏移的起始位置);
确定书写的位置
- SEEK_CUR - 当前位置
- SEEK_SET - 开头位置
- SEEK_END - 结尾位置(当指向结尾时,偏移量只能为负)
fwind
int fwind("文件");
让文件指针回到起始位置
2.文件的结束判定
feof
int feof(文件地址);
是判断读取失败结束,还是遇到文件结尾结束
- 文本文件读取是否结束,判断返回值是否为EOF,或者是否为NULL等。每个函数有每个特定的结束标志。
- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
第九章 预处理
一、预定义符号
符号 | 作用 |
---|---|
FILE | 进行编译的源文件 |
LINE | 文件当前的行号 |
DATE | 文件被编译的日期 |
TIME | 文件被编译的时间 |
STDC | 如果编译器遵循ANSI C,其值为1,否则未定义 |
这些预定义符号都是语言内置的
printf("file:%s line:%d\n", __FILE__, __LINE__);
二、预定义
1. #define 定义标识符
1.语法结构
#define name stuff
2.调用方式
调用 | 作用 |
---|---|
#define MAX 1000 | 定义一个全局常量MAX,大小为1000 |
#define reg register | 为 register这个关键字,创建一个简短的名字 |
#define do_forever for( ; ; ) | 用更形象的符号来替换一种实现 |
#define CASE break; case | 在写case语句的时候自动把 break写上 |
3.注意事项
在define定义标识符的时候不建议加上
;
#define MAX 100;
#define MIN 10;
int ret = MAX * MIN;
由于 #define 定义的内容是在预处理时将其替换到代码中,此时此刻就会出问题。因此强烈不建议在定义标识符时加上;
2. #define 定义宏
1.语法结构
#define name( parament-list ) stuff
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
2.调用示例
#define SQUARE(x) x * x
传参 | 替换 | 结果 |
---|---|---|
SQUARE( 5 ); | 5 * 5 | 25 |
SQUARE( 5 + 1 ) | 5 + 1 * 5 + 1 | 11 |
SQUARE( 5 + 2 + 1 ) | 5 + 2 + 1 * 5 + 2 + 1 | 15 |
#define SQUARE(x) (x) * (x)
传参 | 替换 | 结果 |
---|---|---|
SQUARE( 5 ); | ( 5 ) * ( 5 ) | 25 |
SQUARE( 5 + 1 ) | ( 5 + 1 ) * ( 5 + 1 ) | 36 |
SQUARE( 5 + 2 + 1 ) | ( 5 + 2 + 1 ) * ( 5 + 2 + 1 ) | 64 |
#define SQUARE(x) (x) + (x)
传参 | 替换 | 结果 |
---|---|---|
2 * SQUARE( 5 ); | 2 * ( 5 ) + ( 5 ) | 15 |
2 * SQUARE( 5 + 1 ) | 2 * ( 5 + 1 ) + ( 5 + 1 ) | 18 |
2 * SQUARE( 5 + 2 + 1 ) | 2 * ( 5 + 2 + 1 ) + ( 5 + 2 + 1 ) | 24 |
#define SQUARE(x) ((x) + (x))
传参 | 替换 | 结果 |
---|---|---|
2 * SQUARE( 5 ); | 2 * ( ( 5 ) + ( 5 ) ) | 20 |
2 * SQUARE( 5 + 1 ) | 2 * ( ( 5 + 1 ) + ( 5 + 1 ) ) | 24 |
2 * SQUARE( 5 + 2 + 1 ) | 2 * ( ( 5 + 2 + 1 ) + ( 5 + 2 + 1 ) ) | 32 |
3.注意事项
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或 邻近操作符之间不可预料的相互作用
3. #define 使用规则
1.替换规则
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
2.命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写
3.注意事项
在程序中使用 #define 定义符号和宏时,需要注意:
- 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
三、宏和函数对比
1.宏的优势
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
宏相比函数优势的地方:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序 的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可 以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
2.宏的劣势
宏相比函数劣势的地方:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏无法进行调试的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
- 宏有时候可以做函数做不到的事情。
3.详细对比表
属 性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏 之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使 用这个函数时,都调用那个地方的同 一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销, 所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加 上括号,否则邻近操作符的优先级可能会产生不可预料的后 果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一 次,它的结果值传递给函数。表达式 的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参 数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次, 结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可 以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参 数的类型不同,就需要不同的函数, 即使他们执行的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
四、命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要 编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果 机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
#include <stdio.h>
int main() {
int array[ARRAY_SIZE];
int i = 0;
for (i = 0; i < ARRAY_SIZE; i++) {
array[i] = i;
}
for (i = 0; i < ARRAY_SIZE; i++) {
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
编译指令:
gcc -D ARRAY_SIZE=10 programe.c