C进阶详解预处理指令
Posted syseptember
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C进阶详解预处理指令相关的知识,希望对你有一定的参考价值。
文章目录
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
MSVC编译器不是完全遵循ANSIC标准
gcc编译器严格遵循ANSIC标准
#define
#define定义标识符
语法:#define name stuff
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\\tline:%d\\t \\
date:%s\\ttime:%s\\n" ,\\
__FILE__,__LINE__ , \\
__DATE__,__TIME__ )
注意:转义字符是让计算机识别的,计算机看见
\\Enter
不会认为这是分开的两行,因此不存在语法问题
在#define定义标识符时,不要加上;
否则可能出问题,看下面这个例子
#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)
或定义宏(define macro)。
定义宏的语法:
#define macro_name(pramaent-list) stuff
pramament-list是一个由逗号隔开的参数列表,它们可能出现在stuff中
tips:参数列表和宏名字中间必须紧邻,不能有任何空白,否则宏会被解释为表示符
由于宏是先替换在进行运算,所以定义宏时stuff的每一部分最好用()括起来,防止因为优先级而产生问题请看下面示例
将上述num+num改成((num) + (num)),num * num 改成((num) * (num))即可
#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
#和##
C语言的字符串具有自动连接的特点
所以我们可以利用这个特点来实现一些特定的功能,比如将宏的参数插入到字符串中
总结:#操作符
可以在宏中将宏参数转换为字符串
##操作符
可以在宏体中完成拼接操作(拼接后的字符一定要是被定义过的)
将Class和C拼接在一起ClassC,ClassC是已经被定义过的标识符
注意:使用##
拼接后的结果一定要是已定义的
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
x+1;//不带副作用
x++;//带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。
我们只看这个代码会认为x和y分别++了一次,但是由于这是宏,所以x和y其中有一个++了两次,所以这会导致意外的结果
tips:宏的命名往往是全字母大写,这是为了方便区分宏和函数
宏和函数对比
宏和函数的用法是一样的,都是名字紧跟参数列表,我们下面来看看它们的区别
用宏和函数实现求最大值
可以看见,两者都可以完成求最大值的功能,但是在这串代码中使用宏比函数要合适,原因有下:
- 宏是直接替换的,不需要开辟额外的空间,而函数调用需要开辟栈帧
- 宏比函数执行速度要快,函数调用时需要
为形参压栈、为函数体开辟栈帧、执行函数体、销毁栈帧
,执行速度小于宏 - 函数的定义必须要指名参数类型,而宏不用,所以宏和以比所有类型的数据大小,而函数不行
那是不是所有情况下都应该用宏而不是函数呢?
答案是否定的,宏的缺点有如下几点:
- 每次使用宏时,宏的代码会插入到当前代码块中,除非宏的代码块很短,否则会增加代码的长度
- 使用宏是不会做类型检查,这有可能是不安全的
- 使用宏可能存在优先级的问题
- 宏没有办法调试的
宏和函数对比👇
#undef
这条指令用于移除一个宏定义
#undef NAME
//如果存在一个名字需要重新被定义,那么它应该先移除之前的定义
这是没有移除之前的定义
这是移除之前的定义
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
下面演示Linux环境下的命令行编译
👇
命令行
gcc -D ARRAY_SIZE=10 test.c
表示在test.c文件中将ARRAY_SIZE定义为10
条件编译
在编译一个程序时,如果我们可以选择某条语句或某组语句进行翻译或者被忽略,常常会显得很方便。只用于调试程序的语句就是一个明显的例子。它们不应该出现在程序的产品版本中,但是你可能并不想把这些语句从源代码中物理删除,因为如果需要一些维护性修改时,你可能需要重新调试这个程序,还需要这些语句。
条件编译(conditionalcompilation)就是用于实现这个目的。使用条件编译,你可以选择代码的-部分是被正常编译还是完全忽略。用于支持条件编译的基本结构是#if
指令和与其匹配的#endif
指令。下面显示了它最简单的语法形式。
#if constant-expression
statements
#endif
constant-expression(常量表达式)
由预处理器
进行求值,如果它的值是非0,an那么statements会被编译,如果它的值是0,那么statements不会被编译
条件编译都是预处理指令,是在程序运行前进行的操作,如果此时出现了预处理时无法得知的数据(比如变量),那么程序会报错
注意:枚举常量和全局变量都是预处理之后、运行之前才知道的值
👇常见的条件编译指令👇
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
注意:每一条
#if
语句一定要有对应的#endif
演示预处理指令#if #elif #else
演示嵌套指令
文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
头文件被包含的方式
- 本地文件包含
#include "filename"
查找策略:先在源文件目录底下找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件(标准位置是由编译器所决定的)如果找不到就会显示编译错误。
- 库文件包含
#include <filename>
查找策略:只在标准位置查找该文件,找不到就会显示编译错误.
所以理论上来说使用本地文件包含既可以引入自定义文件,又可以引入包含库函数的头文件,只是在引入库函数头文件时速度会慢一点
嵌套文件包含
在编写多文件时可能会出现重复包含一个文件的情况,虽说这样的问题不是很大,但我们还是需要尽量避免嵌套文件包含
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
演示重复包含头文件
我们需要条件编译来避免这个问题
头文件中加上这句话
#ifndef __TEST_H__
#defince __TEST_H__
//头文件内容
...
#endif //__TEST_H__
演示结果
也可以使用预处理指令#pragma once
避免头文件的重复引用
演示结果
可以看见预处理指令和条件编译的结果是一样的
其他预处理指令
#error text of error message
用于编译时生成错误信息
#line number "string"
"string"部分是可选的,将#line这一行的行数更改为number,该源文件名称更改为string
没有使用#line指令
使用#line指令
这条指令最常用于把其他语言的代码转换为 C代码的程序。C 编译器产生的错误信息可以引用源文件而不是翻译程序产生的 C 中间源文件的文件名和行号。
#pragma
#progma 指令是另一种机制,用于支持因编译器而异的特性。它的语法也是因编译器而异。有些环境可能提供一些#pragma 指令,允许一些编译选项或其他任何方式无法实现的一些处理方式。例如,有些编译器使用#pragma 指令在编译过程中打开或关闭清单显示,或者把汇编代码插入到C程序中。从本质上说,#pragma 是不可移植的。预处理器将忽略它不认识的#pragma 指令,两个不同的编译器可能以两种不同的方式解释同一条#pragma 指令。
总结
- 程序编译时第一步就是预处理,预处理器支持5个预处理符号
#define
指令将一个符号和任意的字符序列联系在一起,这个字符序列可以是字面量、也可以是一段程序,如果字符序列太长需换行,则需要在除开最一行的每一行加上\\
- 宏就是一个被定义的序列,它的参数将被替换。当一个宏被调用时,它的每个参数都会被具体的值替换,宏完整表达式的两边应该加上
#argument
会被预处理器看作"argument"
,argumen1t##argument2
会被预处理器会将argument1和argument2粘贴为一个文本- 宏的命名习惯是全字母大写
- 宏与函数相比不会做类型检查,运行速度比函数快,带副作用的宏参数可能会产生意料之外的结果
- 可以使用
#undef name
来取消对name的定义 - 使用条件编译可以从单一的源文件创建不同程序的版本
#include
指令可以实现头文件的包含。如果文件名位于尖括号之中,那么只会在编译器定义的标准路径下查找;如果文件名位于双引号之中,那么编译器优先到当前文件路径下查找,如果没找到再去标准路径下查找- 编写头文件时,记得加上防止重复包含的预处理指令
#pragma once
或者条件编译指令
#error
在编译时产生一条错误信息,错误信息是#error后面的文本,#line
告诉编译器当前行号,如果加上可选分"string"会告诉编译器源文件名字。#pragma
允许编译器提供不标准的处理过程
AngularJS指令进阶 -- ngModelController详解
前言
在Angular应用中,ng-model指令时不可缺少的一个部分,它用来将视图绑定到数据,是双向绑定魔法中重要的一环。ngModelController则是ng-model指令中所定义的controller。这个controller包含了一些用于数据绑定,验证,CSS更新,以及数值格式化和解析的服务。它不用来进行DOM渲染或者监听DOM事件。与DOM相关的逻辑都应该包含在其他的指令中,然后让这些指令来试用ngModelController中的数据绑定功能。
注意:本篇文章不是对NgModelController文档的说明,而是更偏向实践。下面我将全程带领大家去实现一个自定义指令,并且利用ng-model属性来做双方的数据绑定。
示例
我们的app中使用了一个自定义的指令,名字叫做timeDruation
如下
<div ng-app="HelloApp" ng-controller="HelloController"> <h1>自定义指令</h1> <time-duration ng-model="test"></time-duration> <h1>默认指令</h1> <input ng-model="test">second </div>
JS代码如下,
angular.module(‘HelloApp‘, []) .directive(‘timeDuration‘, TimeDurationDirective); .controller(‘HelloController‘, function($scope) { $scope.test = 1; });
我们的示例指令可以做这样一件事,可以指定几个常见的时间单位,并且能够输入数据。最终我们将得到对应的秒数。其功能的截图如下,
这里我们特意将test变量分别绑定到我们的自定义指令和默认指令中,以观察其效果。
自定义指令
闲话少叙,下面来看代码
先上指令的模板。从上图中可以看出,指令包含一个输入框一个下拉选择框。
<div class="time-duration"> <input ng-model=‘num‘> <select ng-model=‘unit‘> <option value="seconds">Seconds</option> <option value="minutes">Minutes</option> <option value="hours">Hours</option> <option value="days">Days</option> </select> </div>
模板其实很简单,这里就不多说了。下面我们来看看这个指令的逻辑部分。
function TimeDurationDirective() { var tpl = ‘....‘; // 指令模板代码就是上面的内容,这里就不复制了。 return { restrict: ‘E‘, replace: true, template: tpl, require: ‘ngModel‘, scope: {}, link: function(scope, element, attrs, ngModelController) { var multiplierMap = { seconds: 1, minutes: 60, hours: 3600, days: 86400 }; var multiplierTypes = [‘seconds‘, ‘minutes‘, ‘hours‘, ‘days‘]; // TODO } }; }
指令的link方法我们暂时TODO了它。后面会逐步完善。
我先来看看这个指令的定义,其中用到了require声明。简单来说,require的作用就是为这个directive声明一个依赖关系,表明此directive依赖另一个指令的controller属性。
这里稍微说明一下require的衍生用法。
我们可以在require前加上修辞量词,比如,
return { require: ‘^ngModel‘ } return { require: ‘?ngModel‘ } return { require: ‘?^ngModel‘ }
1、^前缀修饰表示允许查找当前指令的父级指令,如果找不到对应指令的controller则抛出一个错误。
2、?则表示将这个require动作变成一个可选项,意思就是找不到对应指令的controller就算了,不会抛出错误。
3、当然,我们也可以联合使用这两个前缀修饰。
相对?ngModel,^ngModel我们使用的频率要更加高一点。
比如
<my-directive ng-model="my-model"> <other-directive></other-directive> </my-directive>
这时,我们在other-directive中使用require: ^ngModel
,它将会自动查找my-directive指令声明中的controller属性。
使用NgModelController
当我们声明了require: ‘ngModel‘
之后,在link方法中会注入第四个参数,这个参数就是我们require的那个指令对应的controller。这里就是内置指令ngModel的指控器ngModeController了。
link: function (scope, element, attrs, ngModelCtrl) { // TODO }
$viewValue和$modelValue
在ngModelController中有两个很重要的属性,一个叫做$viewValue,一个叫做$modeValue。
这两者的含义官方的解释如下
$viewValue: Actual string value in the view.
$modelValue: The value in the model, that the control is bound to.
如果你对上面的官方解释有疑惑的话,我这里给出一种我个人的解释。
$viewView就是指令渲染模板所用的值,而$modelView是在控制器中流通的值。很多时候,这两个值可能是不一样的。
比如你在页面上展示了一个日期,它显示的可能是“Oct. 20 2015”这样的字符串,但是呢,这个字符串在控制器中对应的值可能是一个Javascript的Date对象的实例。
再比如,我们的这个time-duration示例中,$viewValue其实指的是指令模板中num和unit组合出来的值,而$modelValue是HelloAppController中test变量对应的值。
$formatters和$parses
除了$viewValue和$modelValue这两个属性之外,还有两个用来处理他们的方法。分别是$parses和$formatters。
前者的是作用是将$viewValue->$modelValue
,后者的作用恰好相反,是将$modelValue->$viewValue
。
time-duration指令与外部控制器以及其内部的运作如下图,
1、在外部控制器中(即这里的HelloApp的controller),我们通过ng-model="test"
将test变量传入指令time-duration中,并建立绑定关系。
2、在指令内部,$modelValue其实就是test值的一份拷贝。
3、我们通过$formatters()
方法将$modelValue转变成$viewValue。
4、然后调用$render()
方法将$viewValue渲染到directive template中。
5、当我们通过某种途径监控到指令模板中的变量发生变化之后,我们调用$setViewValue()
来更新$viewValue。
6、与(4)相对应,我们通过$parsers方法将$viewValue转化成$modelValue。
7、当$modelValue发生变化后,则会去更新HelloApp的UI。
完善指令逻辑
按照上面的流程,我们先来将$modelValue转化成$viewValue,然后在指令模板中进行渲染。
// $formatters接受一个数组 // 数组是一系列方法,用于将modelValue转化成viewValue ngModelController.$formatters.push(function(modelValue) { var unit = ‘minutes‘, num = 0, i, unitName; modelValue = parseInt(modelValue || 0); for (i = multiplierTypes.length-1; i >= 0; i--) { unitName = multiplierTypes[i]; if (modelValue % multiplierMap[unitName] === 0) { unit = unitName; break; } } if (modelValue) { num = modelValue / multiplierMap[unit]; } return { unit: unit, num: num }; });
最后返回的对象就是$viewValue的value。(当然$viewValue还会有其他的一些属性。)
第二步,我们调用$render方法将$viewValue渲染到指令模板中去。
// $render用于将viewValue渲染到指令的模板中 ngModelController.$render = function() { scope.unit = ngModelCtrl.$viewValue.unit; scope.num = ngModelCtrl.$viewValue.num; };
第三步,我们通过$watch来监控指令模板中num和unit变量。当其发生变化时,我们需要更新$viewValue。
scope.$watch(‘unit + num‘, function() { // $setViewValue用于更新viewValue ngModelController.$setViewValue({ unit: scope.unit, num: scope.num }); });
第四步,我们通过$parsers将$viewValue->$modelValue。
// $parsers接受一个数组 // 数组是一系列方法,用于将viewValue转化成modelValue ngModelController.$parsers.push(function(viewValue) { var unit = viewValue.unit; var num = viewValue.num; var multiplier; multiplier = multiplierMap[unit]; return num * multiplier; });
总结
好了,到这一个双方的数据绑定逻辑就建立了。不知道大家都学会了吗?希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
以上是关于C进阶详解预处理指令的主要内容,如果未能解决你的问题,请参考以下文章