故事背景:
最近在看邓俊辉老师的书《数据结构(C++语言版》。不得不说这本书写的太好了,强烈推荐大家看看。
我以前也学过C++,基础的语法还是知道的,也知道C++里模板的用法。所以我满以为凭这点底子看这本书的示例代码应该是没问题的。我还特地找了一个C++在线编译器wandbox。这个在线编译器支持多种版本的C++语法,还支持多文件。
在看到第三章列表节点模板类的示例代码时,我看不懂了。代码是这样的:
typedef int Rank; //秩
#define ListNodePosi(T) ListNode<T>* //列表节点位置
template <typename T> struct ListNode { //列表节点模板类(以双向链表形式实现)
// 成员
T data; ListNodePosi(T) pred; ListNodePosi(T) succ; //数值、前驱、后继
// 极造函数
ListNode() {} //针对header和trailer的构造
ListNode( T e, ListNodePosi(T) p = NULL, ListNodePosi(T) s = NULL)
: data(e), pred(p), succ(s) {} //默认构造器
// 操作接口
ListNodePosi(T) insertAsPred(T const& e); //紧靠当前节点之前插入新节点
ListNodePosi(T) insertAsSucc(T const& e); //紧随当前节点之后插入新节点
};
看不懂的有两句(凭什么可以这样写?为什么我写不出来?):
#define ListNodePosi(T) ListNode<T>*
ListNodePosi(T) pred;
先来看看微软msdn对#define的语法解释:
#define Directive (C/C++)
#define identifier token-stringopt
#define identifier ( identifieropt,...,identifieropt)token-stringopt
The
#define
directive causes the compiler to substitute token-string for each occurrence of identifier in the source file. The identifier is replaced only when it forms a token. That is, identifier is not replaced if it appears in a comment, in a string, or as part of a longer identifier. For more information, see Tokens.
define指令使得编译器把源文件每个标识符都替换成token-string。只有当标识符构成一个token的时候才会被替换。那就是,标识符出现在注释,字符串,或者是更长标识符的一部分时不会被替换。
什么是token?(这个词很常见哟,尤其是涉及到分词的时候经常用到这个词)
A token is the smallest element of a C++ program that is meaningful to the compiler. The C++ parser recognizes these kinds of tokens: identifiers, keywords, literals, operators, punctuators, and other separators. A stream of these tokens makes up a translation unit.
一个token就是C++编程中对编译器有意义的最小元素。C++解析器会识别这些token:标识符,关键字,字面量,操作符,标点符号和其它分隔符。一连串这样的token构成了一个翻译单元。
比如定义一个变量, int a;
这就是一个token。因为它是有意义的,编译器能够编译它。
字面量就是“硬编码”。比如String str = "abcd"
。这个"abcd"就是一个字面量。
现在明白了,define会把能构成token的identifier替换成token-stringopt。但不是简单粗暴的全部替换(比如注释里的就不会替换)
小结一下#define就两种用法:
- #define 标识符 token字符串选项
- #define 标识符 (标识符选项,...,标识符选项) token字符串选项
第二种用法省略号表示可以传入多个标识符选项。标识符就是变量的意思。
opt
就是选项的缩写,表示这个token字符串是可选的(optional)。可选的意思就是说可以有,也可以没有。identifer
翻译过来是标识符的意思,变量名,函数名,类名等都是一个标识符。
啰嗦了这么久,再来看看#define ListNodePosi(T) ListNode<T>*
这一句。这里identifier的地方有一个圆括号,显然是第二种用法。我们再来仔细看看第二种用法的语法:
The second syntax form defines a function-like macro with parameters. This form accepts an optional list of parameters that must appear in parentheses. After the macro is defined, each subsequent occurrence of identifier( identifieropt, ..., identifieropt ) is replaced with a version of the token-string argument that has actual arguments substituted for formal parameters.
第二种语法形式定义了一个类似函数的带参数的宏。这种形式接受一个可选的必须出现在圆括号里的参数列表。宏被定义之后,接下来出现的每个标识符(标识符选项,...标识符选项)都会被替换成拥有正式参数(实参)的token-string。 (言外之意如果opt-string里 有参数的话,参数会被替换成标识符里参数的值)
上面这句话看起来很容易,也很容易懂,但是我还是写不出这样的语句:
#define ListNodePosi(T) ListNode<T>*
ListNode<T>*
:这的确是一个正确的token。这是一个指针类型,就像 int *一样。只不过这个类型带有泛型的参数。单独写我没意见。ListNodePosi(T)
:一个标识符后面跟上参数,根据第二种写法,这样是合法的。
根据定义,凡是代码里出现ListNodePosi(T)
的地方都要替换成ListNode<T>*
。 并且会用标识符参数T
替换 ListNode<T>
里的参数T
。
我真正不理解的地方就在这里:ListNode<T>
里的T
是参数吗?它跟标识符里参数T
是同一个概念吗?能替换成功吗?
好吧,来看看C++模板的定义。
A template is a construct that generates an ordinary type or function at compile time based on arguments the user supplies for the template parameters.
模板是根据用户为模板参数提供的参数,在编译时生成普通类型或函数的句法结构。
这里明确说明,模板里的T是一个参数。跟普通函数里的参数是一样的。如果不一样文档里不会继续说成是parameter
。不一样的话它一定会用另一个(新)词来代替。
所以真相大白了,模板里的参数是参数,跟标识符里的参数是一个概念,那么用ListNodePosi(T)
里的T
替换ListNode<T>*
里的T
没毛病,可以替换成功。
啰嗦了这么久,其实就是为了搞清楚一个问题。
宏定义里标识符的参数能替换右边token里模板的参数吗?
我的理解,文档里都把它们叫成parameter
,所以是可以替换的。从编译结果来看也确实如此。而且这里的叫成parameter
是非常准确的,parameter
指的是形参。真正替换的时候其实是用标识符里的实参(argument)替换token里模板的实参(argument)。看上面define第二种语法的定义,人家说的就是argument
。
parameter
和argument
的区别就是:前者多指形参,后者多指实参。
宏定义和模板里所谓的参数可以这么理解:
宏定义和模板里的参数T里理解成一个变量。编译器在编译的时候会给这个变量赋上具体的值。给模板的参数赋值,就实现了所谓的模板实例化。
#define ListNodePosi(T) ListNode<T>*
这一句终于搞懂了,我可真能钻牛角尖。
那么ListNodePosi(T) pred;
这一句呢。一般我们在使用(调用)宏的时候都要传入实参的对不对?这里为什么还可以继续写成形参T呢?
写个demo试一下:
#include <iostream>
#include <cstdlib>
#define sum(a, b) a+b
int abc(int a, int b){
return sum(a, b);
}
int main()
{
int c = abc(1, 2);
std::cout<<c<<std::endl; // 输出3
}
额,我的理解是:
①目前只是在函数abc的定义里调用了宏,只是定义,程序并没有跑起来,所以可以继续使用形参。
②也可以理解为我们已经传入了“实参”,把函数abc的参数传给了宏。程序跑起来的时候在函数sum里,a和b已然是实际的值了。
好像第二种说法更有说服力一点。
总结:
以上是C++.0x标准(或者称C++11或者C++0B)出来以前的写法,有点难以理解,不过感觉很高大上啊。
从C++0x标准开始,模板别名(template alias)有了新的写法:
template <typename T> typedef ListNode<T>* ListNodePosi;
显然这种写法更简洁,更直观。因为ListNodePosi(T)
感觉很别扭,总感觉是伪代码。