##的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作用
x++;//带有副作用
宏和函数对比
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错
宏也可以这样使用
命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
#undef
这条指令用于移除一个宏定义
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
命令行定义
#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; }
在编译期间可以对一些参数进行定义
//linux 环境演示 gcc -D ARRAY_SIZE=10 programe.c
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令——条件满足就编译,不满足就不编译
#include <stdio.h> #define __DEBUG__ int main() { int i = 0; int arr[10] = { 0 }; for (i = 0; i < 10; i++) { arr[i] = i; #ifdef __DEBUG__ //如果定义了__DEBUG__,就执行下面语句 printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 #endif //__DEBUG__ } return 0; }
常见条件编译指令
1.
#if 常量表达式
..
#endif //常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
如果if后面表达式为真就进行编译,否则不参与编译
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol) //如果条件为真
#ifdef symbol //如果条件为真
#if !defined(symbol) //如果条件为假
#ifndef symbol //如果条件为假
4.嵌套指令
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
跟if和else嵌套很像
文件包含
若头文件包含次数过多,预编译产生的文件里面会出现好多次头文件的内容
要解决这个问题,在每个头文件开头写
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif //__TEST_H__
或者
#pragma once
就可以避免头文件的重复引入
#inlcude<stdio.h>和#include"test.h"区别 <>:
查找策略,直接去库目录下查找
" ":
1.先去代码所在路径下查找
2.如果找不到再去库里面查找
其他预处理指令
#error #pragma #line ...
实现offsetof函数
offsetof(type, member-designator)
type -- 这是一个 class 类型,其中,member-designator 是一个有效的成员指示器。
member-designator -- 这是一个 class 类型的成员指示器
该宏返回类型为 size_t 的值,表示 type 中成员的偏移量。
思路:把数字0-n强制转换为地址,然后再转换为整形,得到的结果就是偏移量
(struct S*)0 //把0强制转换为地址,让结构体地址从0开始 ((struct S*)0)->m_name ,找到相对于的变量名,就是拿到这个结构体成员 &((struct S*)0)->m_name,取出该成员地址 (int)&((struct S*)0)->m_name//强制转换为整形
#include <stdio.h> #include<stddef.h> struct S { char c1; int i; char c2; }; #define OFFSETOF(type,m_name) (int)&((type*)0)->m_name int main() { printf("%d\n", OFFSETOF(struct S, c1)); printf("%d\n", OFFSETOF(struct S, i)); printf("%d\n", OFFSETOF(struct S, c2)); return 0; }
习题1
C语言头文件中的 ifndef/define/endif 的作用?( )
A.防止头文件重复引用
B.规范化代码
C.标志被引用文件内容中可以被共享的代码
D.以上都不正确
概念性问题,这个做法本就是解决同一文件重复包含相同头文件的,选A
习题2
设有以下宏定义:
#define N 4
#define Y(n) ((N+2)*n) /*这种定义在编程规范中是严格禁止的*/
则执行语句:z = 2 * (N + Y(5+1));后,z的值为( )
A.出错
B.60
C.48
D.70
Y(5+1)经过替换后是((N+2)*5+1),其中N是4,所以结果是31,2 * (4 + 31)结果为70,选D。
习题3
下面代码执行的结果是:( )
#define A 2+2 #define B 3+3 #define C A*B int main() { printf("%d\n", C); return 0; }
A.24
B.11
C.10
D.23
宏C预处理后的代码是:2+2*3+3,即2+6+3,等于11,选B
习题4
下面哪个不是宏和函数的区别?( )
A.函数可以递归,宏不能递归
B.函数参数有类型检查,宏参数无类型检查
C.函数的执行速度更快,宏的执行速度慢
D.由于宏是通过替换完成的,所以操作符的优先级会影响宏的求值,应该尽量使用括号明确优先级
宏不存在执行速度,它是查找替换,选C。A中宏是查找替换,无法设定递归跳出条件,自然无法递归。B中宏是查找替换,都没有执行,类型更是无从谈起。D中直接说了宏的本质。所以只要知道了宏是查找替换,其他问题也就不是问题了。
习题5
下面哪个是条件编译指令( )
A.#define
B.#ifdef
C.#pragma
D.#error
A是宏定义,C是一个比较复杂的预编译语句,但跟条件肯定扯不上关系,D是报错用的,条件编译指令包括#if、#ifdef,#ifndef,#else,#elif、#endif等。除此之外还有#if defined(xxx)的用法。故选B。
习题6
以下关于头文件,说法正确的是( )
A.#include,编译器寻找头文件时,会从当前编译的源文件所在的目录去找
B.#include“filename.h”,编译器寻找头文件时,会从通过编译选项指定的库目录去找
C.多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯
D.在大型项目开发中,把所有自定义的数据类型、函数声明都放在一个头文件中,各个源文件都只需要包含这个头文件即可,省去了要写很多#include语句的麻烦,是好的编程习惯。
AB说反了,尖括号是直接去库找,双引号是先从当前目录找,再去库里找。C选项头文件不能定义全局变量,否则如果有多个文件,那链接时会冲突。故选D。D也不是十全十美,在大型项目的开发中,这也并不是一个很好的编程习惯,分类放在不同的头文件并根据特点命名是更好的选择,因为这样更加方便代码的管理和维护,就目前而言,算是一个好习惯吧。
习题7 宏实现offsetof
offsetof宏
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察: offsetof宏的实现
#define offsetof(StructType, MemberName) (size_t)&(((StructType *)0)->MemberName)
习题8 交换奇偶位
#define SwapIntBit(n) (((n) & 0x55555555) << 1 | ((n) & 0xaaaaaaaa) >> 1)
交换奇偶位,需要先分别拿出奇偶位。既然是宏,分别拿出用循环不是很现实,那就用&这些位的方式来做。奇数位拿出,那就是要&上010101010101……,偶数位拿出,就是要&上101010101010……,对应十六进制分别是555……和aaa……,一般我们默认是32位整数,4位对应一位16进制就是8个5,8个a。通过& 0x55555555的方式拿出奇数位和& 0xaaaaaaa的方式拿出偶数位。奇数位左移一位就到了偶数位上,偶数位右移一位就到了奇数位上,最后两个数字或起来,就完成了交换。
※这个宏只能完成32位以内的整形,要想完成64位的,那就将5和a的数量翻倍即可。