0. typedef
typedef
关键字是用来定义类型别名的关键字。
使用typedef
,可以让我们的代码条理清晰、易于阅读。例如:
// 可以表示二维网格的位置
typedef unsigned int ipos[2];
ipos pos = {2, 3};
除了使用typedef
,还可以使用using
,这是C++11新引入的方法:
using ipos = unsigned int[2];
实际就是你的大名叫起来有些拗口,领导通常喜欢叫你“小张”、“老王”。
使用typedef,需要注意如下几点:
- typedef使用方法
- typedef与复合数据类型
1. typedef使用方法
基本内置类型的typedef很简单:
typedef unsigned int idx;
idx
的实际类型就是unsigned int
。
对于复合数据类型,理解typedef
,我们首先考虑去掉typedef
:
unsigned int ipos[2];
这时声明了一个数组变量,名字叫做ipos
。这时我们加上typedef
,告诉编译器这句话不是声明变量,而是声明类型别名。
// 告诉编译器,我声明的不是变量
// 是类型别名
typedef unsigned int ipos[2];
此时编译器知道,ipos
表示的就是包含2个元素类型为unsigned int
的数组类型。
2. typedef与复合数据类型
声明基本内置类型、自定义数据类型的类型别名比较简单:
typedef double wages; // 用一个double存储工资wages
class MassachusettsInstituteOfTechnology;
typedef MassachusettsInstituteOfTechnology MIT;
复合类型的声明,以及使用别名的复合类型,就容易出错了。
2.1 数组
我们可以为数组定义了一个别名:
// 可以表示二维网格的位置
typedef unsigned int ipos[2];
使用ipos
,若不赋初始值,则执行默认初始化,对于上例,是一个未定义的值。
ipos pos = {3, 4};
cout << pos[0] << ", " << pos[1]; // 输出3,4
也可以在声明类型别名的时候,不指定数组的长度:
// 可以表示二维网格的位置
// 也可以表示三维、四维
typedef unsigned int ipos[];
但是在使用ipos
时,必须初始化,告诉编译器数组的长度,这是数组的要求:
ipos pos = {2, 3, 4};
// 此时是三维网格的坐标
注意:数组是不能赋值和拷贝的,不要使用另一个ipos初始化一个新的ipos,类型别名并不改变原类型的行为,只是别名,好比不能因为领导叫你“老王”你就必须做一些绿色的事情。所以上述定义ipos
的方案并不是很好的方案,因为我们直观理解二维网格的位置的赋值应该是允许的。
可以定义数组别名的指针、数组别名的引用:
ipos pos = {3, 4};
ipos *p_ipos = &pos;
// unsigned int ipos[2] = {3, 4};
// unsigned int (*p_ipos)[2] = &ipos;
很明显使用类型别名更方便阅读。
2.2 引用
可以将引用类型声明为别名:
typedef MyClass &rmc;
MyClass my_class;
rmc mc = my_class;
此时注意,我们似乎可以定义引用的引用了。
rmc &ref_mc = mc;
请读者注意,某些编译器上述代码是可以编译通过的。C++标准虽然规定了不可以定义引用的引用,但是编译器有自己的实现。
而且上述代码,是通过一种间接的方式定义了引用的引用,从字面角度看,依旧是某种类型的引用,这个类型是程序员自己声明的类型,所以也不算是引用的引用。
2.3 指针
可以将指针类型声明为别名:
typedef char *pstring;
pstring ps = "hello world";
通常使用类型别名声明函数指针(前方高能):
typedef string name;
typedef bool (*pCompare)(const name &, const name &);
bool CompareLength(const name &s1, const name &s2) {
return s1.size() < s2.size();
}
bool CompareAlpha(const name &s1, const name &s2) {
return s1[0] < s2[0];
}
int main() {
const string s1 = "Matt", s2 = "Jimmy";
vector<pCompare> compares = {CompareLength, CompareAlpha};
bool flag = false;
for (const pCompare &pcom : compares) {
flag = flag || pcom(s1, s2);
}
cout << (flag ? "true" : "false") << endl;
return 0;
}
首先,将string
取别名name
,表示这个数据类型用来表示名字。
然后声明了一个函数指针pCompare
类型,表示那些比较两个const name &
,并返回bool
类型的函数。
接下来定义的CompareLength
和CompareAlpha
就是这样的函数。
在main
函数中,我们定义了一个类型为vector
的compares
容器,容纳所有比较操作。
遍历容器中的所有操作,将结果或||
在flag上,输出flag
。
这样的代码,通过字面意义就明白程序在做什么。
这里有一个地方,在范围for循环中:
const pCompare &pcom : compares
仔细推敲一下pcom
的类型,它是一个函数指针的常量引用。
若我们不使用typedef,该如何定义呢?
bool (* const &rCom)(const name &, const name &) = &CompareAlpha;
理解上述声明,要使用机器思维:从内向外,从右向左。
从标识符rCom
开始。
①rCom
向右是右括号,向左;看到&
,rCom
是引用类型。什么样的引用?
②向左看到const
,常量引用;向左看到*
,指针的常量引用。什么样的指针?
③看到左括号,向外;看到函数相关的定义,函数指针的常量引用。什么样的函数?
④接下来观察一下函数的定义,就知道是指向什么样的函数的指针了。
因为是引用,所以必须初始化。
2.4 const限定符
使用const限定符修饰类型别名,容易出错的地方就是对指针的理解。
const + 引用,只有常量引用(实际上是指向常量的引用)一种情况。
const + 指针,有顶层const和底层const之分。
如示例:
typedef string *pname;
const pname n1 = new string("Matt");
请问上例const
是顶层cosnt还是底层const?
答案:顶层const,即我们不能改变n1
的值。
若把原类型带入,很可能得到:
const string *n1 = new string("Matt");
// 这是一个底层const
这是错误的理解。
实际上,pname
已经是一个独立的类型了,const修饰的目标是对给定的类型的修饰,就是表示n1不允许被改变。
但是:
const pname *p_n1 = &n1;
// 这是一个底层const
又回到了原貌。
3. 总结
typedef
关键字是用来定义类型别名的关键字,使用typedef
,可以让我们的代码条理清晰、易于阅读。