C 程序员如何使用 D 编程
每个有经验的 C 程序员都积累了一系列的习惯和技术,这几乎成了第二天性。有时候,当学习一门新语言时,这些习惯会因为太令人舒适而使人看不到新语言中等价的方法。所以下面收集了一些常用的 C 技术,以及如何在 D 中完成同样的任务。
因为 C 没有面向对象的特征,所以有关面向对象的论述请参见 C++ 程序员如何使用 D 编程 。
C 的预处理程序在 C 的预处理程序 vs D 中讨论。
- 获得一个类型的大小
- 获得一个类型的最大值和最小值
- 基本类型
- 特殊的浮点值
- 浮点除法中的余数
- 在浮点比较中处理 NAN
- 断言
- 初始化数组的所有元素
- 遍历整个数组
- 创建可变大小数组
- 字符串连接
- 格式化打印
- 函数的前向引用
- 无参数的函数
- 带标号的 break 和 continue
- goto 语句
- 结构标记名字空间
- 查找字符串
- 设置结构成员对齐方式
- 匿名结构和联合
- 声明结构类型和变量
- 获得结构成员的偏移量
- 联合的初始化
- 结构的初始化
- 数组的初始化
- 转义字符串文字量
- Ascii 字符 vs 宽字符
- 同枚举相应的数组
- 创建一个新的 typedef 类型
- 比较结构
- 比较字符串
- 给数组排序
- 访问易失性内存
- 字符串文字量
- 遍历数据结构
- 无符号右移
获得一个类型的大小
C 的方式
() ( *) () ( Foo)
D 的方式
使用 sizeof 属性:
.sizeof ( *).sizeof .sizeof Foo.sizeof
获得一个类型的最大值和最小值
C 的方式
CHAR_MAX CHAR_MIN ULONG_MAX DBL_MIN
D 的方式
.max .min .max .min
基本类型
D 中与 C 类型对应的类型
bool => bit char => char signed char => byte unsigned char => ubyte short => short unsigned short => ushort wchar_t => wchar int => int unsigned => uint long => int unsigned long => uint long long => long unsigned long long => ulong float => float double => double long double => extended _Imaginary long double => imaginary _Complex long double => complex
尽管 char 是一个无符号 8 bit 的类型,而 wchar 是一个无符号 16 bit 的类型,它们还是被划为独立的类型以支持重载解析和类型安全。
在 C 中各种整数类型和无符号类型的大小不是固定的(不同的实现的值可以不同);在 D 中它们的大小都是固定的。
特殊的浮点值
C 的方式
NAN INFINITY DBL_DIG DBL_EPSILON DBL_MANT_DIG DBL_MAX_10_EXP DBL_MAX_EXP DBL_MIN_10_EXP DBL_MIN_EXP
D 的方式
.nan .infinity .dig .epsilon .mant_dig .max_10_exp .max_exp .min_10_exp .min_exp
浮点除法中的余数
C 的方式
f = fmodf(x,y); d = fmod(x,y); e = fmodl(x,y);
D 的方式
D 支持浮点操作数的求余(‘%’)运算符:
f = x % y; d = x % y; e = x % y;
在浮点比较中处理 NAN
C 的方式
C 对操作数为 NAN 的比较的结果没有定义,并且很少有 C 编译器对此进行检查(Digital Mars C 编译器是个例外,DM 的编译器检查操作数是否是 NAN)。
(isnan(x) || isnan(y)) result = FALSE; result = (x < y);
D 的方式
D 的比较和运算符提供对 NAN 参数的完全支持。
result = (x < y);
断言是所有防卫性编码策略的必要组成部分
C 的方式
C 不直接支持断言,但是它支持 __FILE__ 和 __LINE__ ,可以以它们为基础使用宏构建断言。事实上,除了断言以外,__FILE__ 和 __LINE__ 没有什么其他的实际用处。
assert(e == 0);
D 的方式
D 直接将断言构建在语言里:
(e == 0);
[注记:追踪函数?]
初始化数组的所有元素
C 的方式
array[ARRAY_LENGTH]; (i = 0; i < ARRAY_LENGTH; i++) array[i] = value;
D 的方式
array[17]; array[] = value;
遍历整个数组
C 的方式
数组长度另外定义,或者使用笨拙的 sizeof() 表达式。
array[ARRAY_LENGTH]; (i = 0; i < ARRAY_LENGTH; i++) func(array[i]);
或:
array[17]; (i = 0; i < (array) / (array[0]); i++) func(array[i]);
D 的方式
可以使用“length”属性访问数组的长度:
array[17]; (i = 0; i < array.length; i++) func(array[i]);
或者使用更好的方式:
array[17]; ( value; array) func(value);
创建可变大小数组
C 的方式
C 不能处理这种数组。需要另外创建一个变量保存长度,并显式地管理数组大小:
array_length; *array; *newarray; newarray = ( *) realloc(array, (array_length + 1) * ()); (!newarray) error(); array = newarray; array[array_length++] = x;
D 的方式
D 支持动态数组,可以轻易地改变大小。D 支持所有的必需的内存管理。
[] array; array.length = array.length + 1; array[array.length - 1] = x;
字符串连接
C 的方式
有几个难题需要解决,如什么时候可以释放内存、如何处理空指针、得到字符串的长度以及内存分配:
*s1; *s2; *s; free(s); s = ( *)malloc((s1 ? strlen(s1) : 0) + (s2 ? strlen(s2) : 0) + 1); (!s) error(); (s1) strcpy(s, s1); *s = 0; (s2) strcpy(s + strlen(s), s2); hello[] = ; *news; size_t lens = s ? strlen(s) : 0; news = ( *)realloc(s, (lens + (hello) + 1) * ()); (!news) error(); s = news; memcpy(s + lens, hello, (hello));
D 的方式
D 为 char 和 wchar 数组分别重载了‘~’和‘~=’运算符用于连接和追加:
[] s1; [] s2; [] s; s = s1 ~ s2; s ~= ;
格式化打印
C 的方式
printf() 是通用的格式化打印例程:
printf(, ntimes);
D 的方式
我们还能说什么呢?这里还是 printf() 的天下:
stdio; printf(, ntimes);
(译注:其实现在标准库 phobos 中已经有了 write 和 writeln 这两个函数,但是尚未定型。)
函数的前向引用
C 的方式
不能引用尚未声明的函数。因此,如果要调用源文件中尚未出现的函数,就必须在调用之前插入函数声明。
forwardfunc(); myfunc() { forwardfunc(); } forwardfunc() { ... }
D 的方式
程序被看作一个整体,所以没有必要编写前向声明,而且这也是不允许的!D 避免了编写前向函数声明的繁琐和由于重复编写前向函数声明而造成的错误。函数可以按照任何顺序定义。
myfunc() { forwardfunc(); } forwardfunc() { ... }
无参数的函数
C 的方式
function();
D 的方式
D 是强类型语言,所以没有必要显式地说明一个函数没有参数,只需在声明时不写参数即可。
function() { ... }
带标号的 break 和 continue
C 的方式
break 和 continue 只用于嵌套中最内层的循环或 switch 结构,所以必须使用 goto 实现多层的 break:
(i = 0; i < 10; i++) { (j = 0; j < 10; j++) { (j == 3) Louter; (j == 4) L2; } L2: ; } Louter: ;
D 的方式
break 和 continue 语句后可以带有标号。该标号是循环或 switch 结构外围的,break 用于退出该循环。
Louter: (i = 0; i < 10; i++) { (j = 0; j < 10; j++) { (j == 3) Louter; (j == 4) Louter; } }
Goto 语句
C 的方式
饱受批评的 goto 语句是专业 C 程序员的一个重要工具。有时,这是对控制流语句的必要补充。
D 的方式
许多 C 方式的 goto 语句可以使用 D 中的标号 break 和 continue 语句替代。但是 D 对于实际的程序员来说是一门实际的语言,他们知道什么时候应该打破规则。所以,D 当然支持 goto !
结构标记名字空间
C 的方式
每次都要将 struct 关键字写在结构类型名之前简直是烦人透顶,所以习惯的用法是:
ABC { ... } ABC;
D 的方式
结构标记名字不再位於单独的名字空间,它们同普通的名字共享一个名字空间。因此:
ABC { ... };
查找字符串
C 的方式
给定一个字符串,将其同一系列可能的值逐个比较,如果匹配就施行某种动作。该方法的典型应用要数命令行参数处理。
dostring( *s) { Strings { Hello, Goodbye, Maybe, Max }; *table[] = { , , }; i; (i = 0; i < Max; i++) { (strcmp(s, table[i]) == 0) ; } (i) { Hello: ... Goodbye: ... Maybe: ... : ... } }
该方法的问题是需要维护三个并行的数据结构:枚举、表和 switch-case 结构。如果有很多的值,维护这三种数据结构之间的对应关系就不那么容易了,所以这种情形就成了孕育 bug 的温床。另外,如果值的数目很大,相对于简单的线性查找,采用二叉查找或者散列表会极大地提升性能。但是它们需要更多时间进行编码,并且调试难度也更大。典型地,人们会简单地放弃实现这些高效而复杂的数据结构。
D 的方式
D 扩展了 switch 语句的概念,现在它能像处理数字一样处理字符串。所以,字符串查找的实现变得直接:
dostring([] s) { (s) { : ... : ... : ... : ... } }
添加新的 case 子句也变得容易起来。可以由编译器为其生成一种快速的查找方案,这样也就避免了由于手工编码而消耗的时间及引入的 bug 。