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