设置和结构成员对齐方式
C 的方式这是使用命令行选项完成的,而且该效果会影响整个程序,并且如果有模块或者库没有重新编译,结果会是悲剧性的。为了解决这个问题,需要用到 #pragma : #pragma pack(1)
struct ABC
{
...
};
#pragma pack()
但是,无论在理论上还是实际上,#pragma 在编译器之间都是不可移植的。
D 的方式很显然,设置对齐的主要目的是使数据可移植,因此需要一种表述结构的可移植的方式。 struct ABC
{
int z; // z 按照默认方式对齐
align (1) int x; // x 按 byte 对齐
align (4)
{
... // {} 中的声明按照 dword 对齐
}
align (2): // 从现在开始按照 word 对齐
int y; // y 按照 word 对齐
}
匿名结构和联合有时,有必要控制嵌套在结构或联合内部的结构的分布。
C 的方式C 不允许出现匿名的结构或联合,这意味着需要使用傀儡标记名和傀儡成员: struct Foo
{ int i;
union Bar
{
struct Abc { int x; long y; } _abc;
char *p;
} _bar;
};
#define x _bar._abc.x
#define y _bar._abc.y
#define p _bar.p
struct Foo f;
f.i;
f.x;
f.y;
f.p;
这样做不仅笨拙,由于使用了宏,还使符号调试器无法理解程序究竟做了什么,并且宏还占据了全局作用域而不是结构作用域。
D 的方式匿名结构和联合用来以一种更自然的方式控制分布: struct Foo
{ int i;
union
{
struct { int x; long y; }
char* p;
}
}
Foo f;
f.i;
f.x;
f.y;
f.p;
声明结构类型和变量
C 的方式可以在一条以分号结尾的语句中完成: struct Foo { int x; int y; } foo;
或者分为两条语句: struct Foo { int x; int y; }; // 注意结尾处的‘;’
struct Foo foo;
D 的方式结构的定义和声明不能在一条语句中完成: struct Foo { int x; int y; } // 注意结尾处没有‘;’
Foo foo;
这意味着结尾的‘;’可以去掉,免得还要区分 struct {} 和函数及语句块的 {} 之间在分号用法上的不同。
获得结构成员的偏移量
C 的方式很自然,又用了一个宏: #include <stddef>
struct Foo { int x; int y; };
off = offsetof(Foo, y);
D 的方式偏移量只是另一个属性: struct Foo { int x; int y; }
off = Foo.y.offset;
联合的初始化
C 的方式联合的初始化采用“首个成员”规则: union U { int a; long b; };
union U x = { 5 }; // 初始化成员 a 为 5
为联合添加成员或者重新排列成员的结果对任何的初始化语句来说都是灾难性的。
D 的方式在 D 中,初始化那个成员是显式地指定的: union U { int a; long b; }
U x = { a:5 }
还避免了误解和维护问题。
结构的初始化
C 的方式成员按照它们在 {} 内的顺序初始化: struct S { int a; int b; };
struct S x = { 5, 3 };
对于小结构来说,这不是什么问题,但当成员的个数变得很大时,小心地排列初始值以同声明它们的顺序对应变得很繁琐。而且,如果新加了或者重新排列了成员的话,所有的初始化语句都需要进行适当地修改。这可是 bug 的雷区。
D 的方式可以显式地初始化成员: struct S { int a; int b; }
S x = { b:3, a:5 }
这样意义明确,并且不依赖于位置。
数组的初始化
C 的方式C 初始化数组时依赖于位置: int a[3] = { 3,2,2 };
潜逃的数组可以使用 {} ,也可以不使用 {}: int b[3][2] = { 2,3, {6,5}, 3,4 };
D 的方式D 也依赖于位置,但是还可以使用索引,下面的语句都产生同样的结果: int[3] a = [ 3, 2, 0 ];
int[3] a = [ 3, 2 ]; // 未提供的初始值被视为 0 ,如同 C 一样
int[3] a = [ 2:0, 0:3, 1:2 ];
int[3] a = [ 2:0, 0:3, 2 ]; // 如果未提供,索引为前面的索引加一
如果数组的下标为枚举的话,这会很方便。而且枚举的顺序可以变更,也可以加入新的枚举值: enum color { black, red, green }
int[3] c = [ black:3, green:2, red:5 ];
必须显式地初始化嵌套数组: int[2][3] b = [ [2,3], [6,5], [3,4] ];
int[2][3] b = [[2,6,3],[3,5,4]]; // 错误
转义字符串文字量
C 的方式C 在 DOS 文件系统中会遇到问题,因为字符串中的‘\’是转义符。如果要使用文件 c:\root\file.c : char file[] = "c:\\root\\file.c";
如果使用这则表达式的话,会让人很难高兴起来。考虑匹配引号字符串的转义序列: /"[^\\]*(\\.[^\\]*)*"/
在 C 中,令人恐怖的表示如下: char quoteString[] = "\"[^\\\\]*(\\\\.[^\\\\]*)*\"";
D 的方式字符串本身是 WYSIWYG(所见即所得)的。转义字符位于另外的字符串中。所以: char[] file = 'c:\root\file.c';
char[] quoteString = \" r"[^\\]*(\\.[^\\]*)*" \";
著名的 hello world 字符串变为: char[] hello = "hello world" \n;
Ascii 字符 vs 宽字符
现代的程序设计工作需要语言以一种简单的方法支持 wchar 字符串,这样你的程序就可以实现国际化。
C 的方式C 使用 wchar_t 并在字符串前添加 L 前缀: #include <wchar.h>
char foo_ascii[] = "hello";
wchar_t foo_wchar[] = L"hello";
如果代码需要同时兼容 ascii 和 wchar 的化,情况会变得更糟。需要使用宏来屏蔽 ascii 和 wchar 字符串的差别: #include <tchar.h>
tchar string[] = TEXT("hello");
D 的方式字符串的类型由语义分析决定,所以没有必要用宏调用将字符串包裹起来: char[] foo_ascii = "hello";// 字符串使用 ascii
wchar[] foo_wchar = "hello";// 字符串使用 wchar
同枚举相应的数组
C 的方式考虑: enum COLORS { red, blue, green, max };
char *cstring[max] = {"red", "blue", "green" };
当项的数目较小时,很容易保证其正确。但是如果数目很大,当加入新的项时就会很难保证其正确性。
D 的方式 enum COLORS { red, blue, green }
char[][COLORS.max + 1] cstring =
[
COLORS.red : "red",
COLORS.blue : "blue",
COLORS.green : "green",
];
虽不完美,但却更好。
创建一个新的 typedef 类型
C 的方式C 中的 typedef 是弱的,也就是说,他们并不真正引入一个类型。编译器并不区分 typedef 类型和它底层的类型。 typedef void *Handle;
void foo(void *);
void bar(Handle);
Handle h;
foo(h);// 未捕获的编码错误
bar(h);// ok
C 的解决方案是创建一个傀儡结构,目的是获得新类型才有的类型检查和重载能力。(译注:这里捎带的涉及了 C++ 中的重载问题)struct Handle__ { void *value; }
typedef struct Handle__ *Handle;
void foo(void *);
void bar(Handle);
Handle h;
foo(h);// 语法错误
bar(h);// ok
如果要给这个类型定一个默认值,需要定义一个宏,一个命名规范,然后时刻遵守这个规范:#define HANDLE_INIT ((Handle)-1)
Handle h = HANDLE_INIT;
h = func();
if (h != HANDLE_INIT)
...
对于采用结构的那种解决方案,事情甚至变得更复杂:struct Handle__ HANDLE_INIT;
void init_handle()// 在开始处调用这个函数
{
HANDLE_INIT.value = (void *)-1;
}
Handle h = HANDLE_INIT;
h = func();
if (memcmp(&h,&HANDLE_INIT,sizeof(Handle)) != 0)
...
需要记住四个名字:Handle、HANDLE_INIT、struct Handle__ 和 value 。
D 的方式不需要上面那样的习惯构造。只需要写:typedef void* Handle;
void foo(void*);
void bar(Handle);
Handle h;
foo(h);
bar(h);
为了处理默认值,可以给 typedef 添加一个初始值,可以使用 .init 属性访问这个初始值:typedef void* Handle = cast(void*)(-1);
Handle h;
h = func();
if (h != Handle.init)
...
之需要记住一个名字:Handle 。
比较结构
C 的方式尽管 C 为结构赋值定义了一种简单、便捷的方法:struct A x, y;
...
x = y;
却不支持结构之间的比较。因此,如果要比较两个结构实例之间的相等性: #include <string.h>
struct A x, y;
...
if (memcmp(&x, &y, sizeof(struct A)) == 0)
...
请注意这种方法的笨拙,而且在类型检查上得不到语言的任何支持。
memcmp() 中有一个潜伏的 bug 。结构的分布中,由于对齐的原因,可能会有“空洞”。C 不保证这些空洞中为何值,所以两个不同的结构实例可能拥有所有成员的值都对应相等,但比较的结果却由于空洞中的垃圾的存在而为“不等”。
D 的方式D 的方式直接而显然:A x, y;
...
if (x == y)
...
比较字符串
C 的方式库函数 strcmp() 用于这个目的:char string[] = "hello";
if (strcmp(string, "betty") == 0)// 字符串匹配吗?
...
C 的字符串以‘\0’结尾,所以由于需要不停地检测结尾的‘\0’,C 的方式在效率上先天不足。
D 的方式为什么不用 == 运算符呢?char[] string = "hello";
if (string == "betty")
...
D 的字符串另外保存有长度。因此,字符串比较的实现可以比 C 的版本快得多(它们之间的差异就如同 C 的 memcmp() 同 strcmp() 之间的差异一样)。
D 还支持字符串的比较运算符:char[] string = "hello";
if (string < "betty")
...
这对于排序/查找是很有用的。
数组的排序
C 的方式尽管许多的 C 程序员不厌其烦地一遍一遍实现着冒泡排序,C 中正确的方法却是使用 qsort() : int compare(const void *p1, const void *p2)
{
type *t1 = (type *)p1;
type *t1 = (type *)p2;
return *t1 - *t2;
}
type array[10];
...
qsort(array, sizeof(array)/sizeof(array[0]), sizeof(array[0]), compare);
必须为每种类型编写一个 compare() 函数,而这些工作极易出错。
D 的方式这恐怕是最容易的排序方式了:type[] array;
...
array.sort;// 适当地为数组排序
访问易失性内存
C 的方式如果要访问易失性内存,如共享内存或者内存映射 I/O ,需要一个易失性的指针:volatile int *p = address;
i = *p;
D 的方式D 有一种易失性语句,而不是一种类型修饰符:int* p = address;
volatile { i = *p; }
字符串文字量
C 的方式C 的字符串文字量不能跨越多行,所以需要用‘\’将文本块分割为多行: "This text spans\nmultiple\nlines\n"
如果有很多的文本的话,这种做法是很繁琐的。
D 的方式字符串文字量可以跨越多行,如下所示:"This text spans
multiple
lines
"
所以可以简单用剪切/粘贴将成块的文字插入到 D 源码中。
遍历数据结构
C 的方式考虑一个遍历递归数据结构的函数。在这个例子中,有一个简单的字符串符号表。数据结构为一个二叉树数组。代码需要穷举这个结构以找到其中的特定的字符串,并检测它是否是唯一的实例。
为了完成这项工作,需要一个辅助函数 membersearchx 递归地遍历整棵树。该辅助函数需要读写树外部的一些上下文,所以创建了一个 struct Paramblock ,用指针指向它的以提高效率。 struct Symbol
{char *id;
struct Symbol *left;
struct Symbol *right;
};
struct Paramblock
{ char *id;
struct Symbol *sm;
};
static void membersearchx(struct Paramblock *p, struct Symbol *s)
{
while (s)
{
if (strcmp(p->id,s->id) == 0)
{
if (p->sm)
error("ambiguous member %s\n",p->id);
p->sm = s;
}
if (s->left)
membersearchx(p,s->left);
s = s->right;
}
}
struct Symbol *symbol_membersearch(Symbol *table[], int tablemax, char *id)
{
struct Paramblock pb;
int i;
pb.id = id;
pb.sm = NULL;
for (i = 0; i < tablemax; i++)
{
membersearchx(pb, table[i]);
}
return pb.sm;
}
D 的方式这是同一个算法的 D 版本,代码量大大少于上一个版本。因为嵌套函数可以访问外围函数的变量,所以就不需要 Paramblock 或者处理它的簿记工作的细节了。嵌套的辅助函数完全处于使用它的函数的内部,提高了局部性和可维护性。
这两个版本的性能没什么差别。 class Symbol
{char[] id;
Symbol left;
Symbol right;
}
Symbol symbol_membersearch(Symbol[] table, char[] id)
{ Symbol sm;
void membersearchx(Symbol s)
{
while (s)
{
if (id == s.id)
{
if (sm)
error("ambiguous member %s\n", id);
sm = s;
}
if (s.left)
membersearchx(s.left);
s = s.right;
}
}
for (int i = 0; i < table.length; i++)
{
membersearchx(table[i]);
}
return sm;
}
无符号右移
C 的方式如果左操作数是有符号整数类型,右移运算符 >> 和 >>= 表示有符号右移;如果左操作数是无符号整数类型,右移运算符 >> 和 >>= 表示无符号右移。如果要对 int 施行无符号右移,必须使用类型转换:int i, j;
...
j = (unsigned)i >> 3;
如果 i 是 int ,这种方法会工作得很好。但是如果 i 是一个 typedef 类型,myint i, j;
...
j = (unsigned)i >> 3;
并且 myint 恰巧是 long int ,则这个类型转换会悄无声息地丢掉最重要的那些 bit ,给出一个不正确的结果。
D 的方式D 的右移运算符 >> 和 >>= 的行为同它们在 C 中的行为相同。但是 D 还支持显式右移运算符 >>> 和 >>>= ,无论左操作数是否有符号,都会执行无符号右移。因此, myint i, j;
...
j = i >>> 3;
避免了不安全的类型转换并且对于任何整数类型都能如你所愿的工作。