目录
预处理详解
#define
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
x+1;//不带副作用
x++;//带有副作用
例如:
int main()
{
int a = 10;
int b = a + 1;//b=11, a=10
int b = ++a;//b=11 a=11 - 表达式有副作用
return 0;
}
#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 3;
int b = 5;
int m = MAX(a++, b++);
//int m = ((a++) > (b++) ? (a++) : (b++));
// 3 > 5
// a=4 b=6 no 6
// 6 b=7
printf("%d\n", m);
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
宏和函数对比
例如:
//1.宏
#define MAX(x, y) ((x)>(y)?(x):(y))
//2.函数
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 10;
int b = 20;
int m1 = MAX(a, b);
printf("%d\n", m1);
int m2 = Max(a, b);
printf("%d\n", m2);
return 0;
}
宏通常被应用于执行简单的运算;
如在两个数中找出较大的一个
#define MAX(a, b) ((a)>(b)?(a):(b))
为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多;
所以宏比函数在程序的规模和速度方面更胜一筹
2. 更为重要的是函数的参数必须声明为特定的类型;
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
用于>来比较的类型;
宏是类型无关的
宏的缺点:当然和函数相比宏也有劣势的地方
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度;
- 宏是没法调试的;
- 宏由于类型无关,也就不够严谨;
- 宏可能会带来运算符优先级的问题,导致程容易出现错;
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到;
例如:
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
//int*p = (int*)malloc(10 * sizeof(int));
//malloc(10, int);
//malloc(5, double);
int*p = MALLOC(10, int);
if (p == NULL)
{
//...
}
return 0;
}
使用:MALLOC(10, int);//类型作为参数;
预处理器替换之后:(int *)malloc(10 * sizeof(int));
宏和函数的一个对比
建议:
如果逻辑比较简单,可以使用宏来实现;
但是如果计算逻辑比较复杂,就是用函数;
命名约定
一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者;
那我们平时的一个习惯是:
把宏名全部大写;
函数名不要全部大写;
#undef
这条指令用于移除一个宏定义;
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
例如:
#include<stdio.h>
#define M 100
int main()
{
int m = M;
printf("m = %d\n", m);
#undef M//移除宏定义
#define M 1000//重新定义
int n = M;
printf("n = %d\n", n);
return 0;
}
命令行定义
许多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;
}
编译指令:
//linux 环境演示
gcc -D ARRAY_SIZE=10
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令;
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译;
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)——如果是UNIX的话,就执行下面两条语句
#ifdef OPTION1——如果它定义了就看下一条语句
unix_version_option1();——那这句代码就参与编译
#endif——结束
#ifdef OPTION2——如果它定义了就看下一条语句
unix_version_option2();——那这句代码就参与编译
#endif
#elif defined(OS_MSDOS)——如果是这个定义的话,就走这个
#ifdef OPTION2——如果它定义了就看下一条语句
msdos_version_option2();——那这句代码就参与编译
#endif
#endif
头文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样;
头文件被包含的方式:
1.本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误;
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
注意按照自己的安装路径去找;
2.库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误;
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以;
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了
嵌套文件包含
comm.h和comm.c是公共模块;
test1.h和test1.c使用了公共模块;
test2.h和test2.c使用了公共模块;
test.h和test.c使用了test1模块和test2模块;
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复;
如何解决这个问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif
或者:
#pragma once
就可以避免头文件的重复引入;
头文件中的 ifndef/define/endif是干什么用的?——防止头文件被多次使用
#include <filename.h> 和 #include "filename.h"有什么区别?——查找一次和查找两次的区别
其他预处理指令
#error
#pragma
#line
...
还有很多之类的,可以自己去了解以下;
到这里,C语言的知识都介绍完了,感谢大家的支持,谢谢大家!!!