C和指针:第十三,十四章
第13章 高级指针话题
1. 高级指针
1.) int *f() // 由于()优先级高于*,f是一个函数,函数返回一个指向整型的指针;
2.) int (*f) () // 第二个圆括号表示函数,第一个圆括号,是聚组的作用,这里f是指向函数返回值的指针,函数返回一个整形值;
3.) int *(*f) () // (*f)部分和上面表达式2.)作用是一样的,它是个指向函数返回值的指针,但这里的函数返回的是整形指针。
4.) int f() [] // 首先f()代表是个函数,返回值是个数组。但是函数只能返回标量值,而不能返回数组。所以此表达式是非法的。
5.) int f[] () // f[] 是个数组,元素类型是函数。由于数组元素的长度必须一致,而不同的函数可能长度不同,所以此表达式也是非法的。
6.) int (*f[]) () // 根据左右法则,先找到f[],它是个数组,再看左边的*,去与(*f[])外面的()匹配,得出f是个数组的元素是指向函数返回值的指针。
7.) int *(*f[]) () // 此表达式和上面的6.)的区别是多了个间接访问,所以f是个指针数组,元素是指向函数返回值的指针。
结合上面的表达式后,就不难看出下面两个表达式代表的意义了:
8.) int (*f) (int ,float) // f是一个指向函数返回值的指针,函数有两个参数
9.) int *(*g[]) (int, float) // g是一个数组,数组元素为指向函数返回值的指针
上面两个函数原型是ANSI C风格,尽管增加了函数声明的复杂度,但仍应提倡这种风格,因为它向编译器提供了一些额外的信息。
2. 函数指针
1.) 初始化
int f(int); // 函数指针初始化前具有f的原型是必须的,否则编译器就无法检查f函数类型是否与pf所指向的类型一致
int (*pf) (int) = &f; // &是可选的,因为器会将函数名转换为函数指针,&操作符只是显式的说明了编译器将隐式执行的任务
2.) 编写与类型无关的链表查找代码,解决方案是使用函数指针。
调用函数指针的方法是,调用者编写一个函数,用于比较两个值,再把一个指向这个函数的指针作为参数传递给查找函数。然后,查找函数调用这个比较函数来执行值的比较。使用这种方法,任何类型的值都可以进行比较。代码实现如下:
a
3. 命令行参数,处理命令行参数是指向指针的另一个用武之地
1.) 传递命令行参数
main函数有两个形参,第1个称为argc,它表示命令行参数的数目;第2个称为argv,它指向一组参数值。由于参数的数目并没有内在的限制,所以arv指向这组参数值的第一个元素。这些元素的每个都是指向一个参数文本的指针。如果程序要访问命令行参数,main函数在声明时就要加上以下参数:
int main(int argc, char **argv) // argc与argv的名字可以是其它的
int main(int argc, char **argv)
{
while(*++argv != NULL)
printf(“%s\n”, *argv); // %s要求参数是一个指向字符的指针
return EXIT_SUCCESS;
}
2.) 处理命令行参数
命令行每个参数可能包含多个选项,可以用一个循环来处理:
while ((opt = *++*argv) != ‘\0’)
{
switch(opt)
{
case ‘a’:
option_a = TRUE;
break;
}
}
4. 字符串常量指针
1.) “xyz” + 1 // 字符串常量实际上是个指针,表达式计算“指针值加1”的值,结果是个指针,它指向字符串中的第2个字符:y
2.) *“xyz” // 字符串常量类型是“指向字符的指针”,对指针执行间接访问结果是指针所指向的内容,表达式结果是它的第一个字符:x
3.) “xyz”[2] // 表达式结果是:z
4.) *(“xyz” + 4) // 表达式越界,出现非法访问
第14章 预处理器
C程序处理的第一步被称为预处器(preprocessing)阶段,C预处理器在源代码编译前对其进行一些文本性质的操作。主要任务包括删除注释,插入被#include指令包含的文件的内容,定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
1. 预定义符号如下:
符号 | 样例值 | 含义 |
__FILE__ | "name.c" | 进行编译的源文件名 |
__LINE__ | "25" | 文件当前行的行号 |
__DATE__ | "Jan 21 1997" | 文件被编译的日期 |
__TIME__ | "18:04:30" | 文件被编译的时间 |
__STDC__ | 1 | 如果编译器遵循ANSI C,其值就为1,否则未定义 |
注:__是两个连续的下划线
2. #define
使用#define可以把任何文本替换到程序中,如:
#define reg register
#define do_forever for (; ;) // 用符合来代替实现无限循环的for语句
#define CASE break; case // 自动把每个break放在每个case之前
如果定义的内容非常长,可以分成几行,并在每行的末尾都要加一个反斜杠,如:
#define DEBUG_PRINT printf(“File %s line %d:” \
“x = %d, y= %d, z = %d, \
__FILE__, __LINE__, \
x, y, z)
3. 宏
#define 允许把参数替换到文本中,通常把这种称为宏(macro)或定义宏,下面是宏的声明方式:
#define name(parameter-list) stuff
parameter-list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中,参数列表的左括号必须与name紧邻,如果两者有任何空白存在,参数列表就会被解释为stuff的一部分。
#define SQUARE(x) x * x
不过上面的代码有个副作用,就是当x是一个表达式时,结果不是预期的,如:
SQUARE(5 + 1) // 实际替换为 5 + 1 * 5 + 1,结果为11
解决这一问题,只要将stuff部分的x用括号括起来就可以:
SQUARE(x) (x) * (x)
再有另外一有宏定义:
#define DOUBLE(x) (x) + (x)
int a = 5;
printf(“%d”, 10 * DOUBLE (a)); // 10 * DOUBLE(a) 实际替换为 10 * 5 + 5,结果为55
而解决这个问题只要在整个表达式的两边加上一对括号就可以了:
#define DOUBLE(x) ((x) + (x))
其实,所以有数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时,由于参数中的操作符或邻近的操作符之间不可预料的相互作用。
4. 宏与函数
宏经常用于执行简单的计算,如求两个表达式中的最大值:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
不用函数而用宏的原因有两个:一是,定义和调用的函数代码要比宏的大,所以使用宏比使用函数在程序的规模和速度上都更胜一筹;二是,函数参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用,而宏与类型无关,这也是最为重要的一点。
但和函数相比,宏也有不利之处,每次使用宏时,一个宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
5. 带副作用的宏参数
当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果。如:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
z = MAX(x++, y++); // 将替换为:((x++) > (y++) ? (x ++) : (y++)),这个表达式,较小的值只增值了一次,但较大的却增值了两次,第一次是在比较时,第二次是在执行?后面的表达式出现。
另外,getchar()作宏参数时,也有副作用。调用这个函数将“消耗”输入的一个字符,如果用户意图不想“消耗”输入的字符,就不能重复调用这个函数。
6. 命名约定,宏名字全部大写
7. 宏和函数的区别:
属性 | #define宏 | 函数 |
代码长度 | 每次使用时,宏代码都被插入到程序中,除了非常小的宏之外,程序的长度将大幅增长 | 函数代码只出现在一个地方,每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用/返回的额外开销 |
操作符优级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果 | 函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
参数求值 | 参数每次用于宏定义时,它们都将重新求值。由于多次求值 ,具有副作用的参数可能会产生不可预料的结果 | 参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多种求值过程,参数的副作用并不会造成任何特殊的问题 |
参数类型 | 宏与类型无关。只要对参数的操作合法,它可以使用任何参数类型 | 函数的参数与类型有关,如果参数类型不同,就需要使用不同的函数,即使它们执行任务是相同的 |
8. #undef 用于移除一个宏定义。如果出现已存在的宏名字要被重新定义,就要用#undef移除此前的旧定义
9. 条件编译(conditional compilation)是指,你可以选择代码的一部分是正常编译还是完全忽略。用于支持条件编译的基本结构是#if指令和与其匹配的#endif指令。语法结构为:
#if constant-expression
statements
#endif
另外,#if指令还具体可先的#elif和#else子句,语法结构为:
#if constant-expression
statements
#elif constant-expression
other statements
#else
other staements
#endif
上面的#elif子句出现的次数是不限的,每个constant-expression(常量表达式)只有当前面所有常量表达式的值为假时才会被编译。
10. 测试符号是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
每对定义的两条语句是等价的,但#if形式功能更强。
11. 文件包含
1.) 函数库文件的包含,格式:
#include <filename>
2.) 本地文件的包含,格式:
#include “filename” // filename可以是包含路径的文件名
处理本地文件包含时,通常在源文件当前所在的目录查找,如果该头文件未找到,编译器就按查找函数库文件一样在标准位置查找本地头文件。所以,你可以在所有的#include 语句中使用双引号而不是尖括号。但对有些编译器来说,在查找函数库头文件时,可能会浪费少许时间。
12. 嵌套文件包含
在大型程序中,可能出现多重包含的情况,如:
#include “a.h”
#include “b.h”
但a.h和b.h两个头文件中都包含一个嵌套的x.h头文件,那么就出现了多重包含。要解决这个问题,可以使用条件编译:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1 // 当头文件第一次被包含时,被正常处理,如果头文件再次包含,此时符合_HEADERNAME_H被定义为1,内容就会被忽略
…
#endif
但即使这个文件所有内容被忽略,但会手慢编译速度,如果可能,应避免多重包含。