C语言入门(15)——结构体与数据抽象

时间:2023-12-26 10:55:49

大多数的计算机运算是对现实世界的模拟,如果想用计算机来模拟现实世界需要用到数据抽象的方法。所谓抽象是从实际的人、物、事和概念中抽取所关心的共同特征,,忽略非本质的细节,吧这些特征用各种概念精确的加以描述,从而使这些概念构成某种对现实世界进行描述的模型。

下面以数学中的复数为实例,通过结构体讲解数据类型的组合和抽象。至于过程抽象我们已经见过最简单的形式,就是把一组语句用一个函数名封装起来,当作一个整体使用。

现在我们用C语言表示一个复数。如果从直角座标系来看,复数由实部和虚部组成,如果从极座标系来看,复数由模和辐角组成,两种座标系可以相互转换。如下图所示

C语言入门(15)——结构体与数据抽象

比如用实部和虚部表示一个复数,我们可以采用两个double型组成的结构体:

struct complex_struct {
doublex, y;
};

这样定义了complex_struct这个标识符,既然是标识符,那么它的命名规则就和变量一样,但它不表示一个变量,而表示一个类型,struct complex_struct { double x, y; }整个可以看作一个类型名,就像int或double一样,只不过它是一个复合类型,如果用这个类型名来定义变量,可以这样写:

struct complex_struct {
doublex, y;
} z1, z2;

这样z1和z2就是两个变量名,变量定义后面要带个;号。这点一定要注意,结构体定义后面少;号是初学者很常犯的错误。不管是用上面两种形式的哪一种形式定义了complex_struct这个标识符,以后都可以直接用struct complex_struct来代替类型名了。例如可以这样定义另外两个复数变量:

struct complex_struct z3, z4;

结构体变量也可以在定义时初始化,例如:

struct complex_struct z = { 3.0, 4.0 };

复数加法的运算法则是实部与实部相加,虚部与虚部相加。复数相加运算的函数代码如下:

struct complex_struct add_complex(structcomplex_struct z1, struct complex_struct z2)
{
z1.x= z1.x + z2.x;
z1.y= z1.y + z2.y;
returnz1;
}

此外,我们还提供一个函数用来构造复数变量:

struct complex_struct make (double x,double y)
{
structcomplex_struct z;
z.x= x;
z.y= y;
returnz;
}

现在我们来实现一个完整的复数运算的程序。在上一节我们已经定义了复数的结构体,现在需要围绕它定义一些函数。复数可以用直角座标或极座标表示,直角座标做加减法比较方便,极座标做乘除法比较方便。如果我们定义的复数结构体是直角座标的,那么应该提供极座标的转换函数,以便在需要的时候可以方便地取它的模和辐角:

struct complex_struct {
doublex, y;
}; double real_part(struct complex_struct z)
{
returnz.x;
} double img_part(struct complex_struct z)
{
returnz.y;
} double magnitude(struct complex_struct z)
{
returnsqrt(z.x * z.x + z.y * z.y);
} double angle(struct complex_struct z)
{
doublePI = acos(-1.0); if(z.x > 0)
returnatan(z.y / z.x);
else
returnatan(z.y / z.x) + PI;
}

此外,我们再提供一个可以提供极座标的函数用来构造复数变量,在函数中自动做相应的转换然后返回构造的复数变量:

struct complex_structmake_from_mag_ang(double r, double A)
{
structcomplex_struct z;
z.x= r * cos(A);
z.y= r * sin(A);
returnz;
}

在此基础上就可以实现复数的加减乘除运算了:

struct complex_struct add_complex(structcomplex_struct z1, struct complex_struct z2)
{
returnmake_from_real_img(real_part(z1) + real_part(z2),
img_part(z1) + img_part(z2));
} struct complex_struct sub_complex(structcomplex_struct z1, struct complex_struct z2)
{
returnmake_from_real_img(real_part(z1) - real_part(z2),
img_part(z1) - img_part(z2));
} struct complex_struct mul_complex(structcomplex_struct z1, struct complex_struct z2)
{
returnmake_from_mag_ang(magnitude(z1) * magnitude(z2),
angle(z1) + angle(z2));
} struct complex_struct div_complex(structcomplex_struct z1, struct complex_struct z2)
{
returnmake_from_mag_ang(magnitude(z1) / magnitude(z2),
angle(z1) - angle(z2));
}

可以看出,复数加减乘除运算的实现并没有直接访问结构体complex_struct的成员x和y,而是把它看成一个整体,通过调用相关函数来取它的直角座标和极座标。这样就可以非常方便地替换掉结构体complex_struct的存储表示,例如改为用极座标来存储:

struct complex_struct {
doubler, A;
}; double real_part(struct complex_struct z)
{
returnz.r * cos(z.A);
} double img_part(struct complex_struct z)
{
returnz.r * sin(z.A);
} double magnitude(struct complex_struct z)
{
returnz.r;
} double angle(struct complex_struct z)
{
returnz.A;
} struct complex_structmake_from_real_img(double x, double y)
{
structcomplex_struct z;
doublePI = acos(-1.0);
z.r= sqrt(x * x + y * y);
if(x > 0)
z.A= atan(y / x);
else
z.A= atan(y / x) + PI; returnz;
} struct complex_structmake_from_mag_ang(double r, double A)
{
structcomplex_struct z;
z.r= r;
z.A= A;
returnz;
}

虽然结构体complex_struct的存储表示做了这样的改动,add_complex、sub_complex、mul_complex、div_complex这几个复数运算的函数却不需要做任何改动,仍可以使用,原因在于这几个函数只把结构体complex_struct当作一个整体来使用,而没有直接访问它的成员,因此也不依赖于它有哪些成员。我们结合下图具体分析一下。

C语言入门(15)——结构体与数据抽象

这里要介绍的编程思想称为抽象。其实“抽象”这个概念并没有那么抽象,简单地说就是“提取公因式”:ab+ac=a(b+c)。如果a变了,ab和ac这两项都需要改,但如果写成a(b+c)的形式就只需要改其中一个因子。

在我们的复数运算程序中,复数有可能用直角座标或极座标表示,我们把这个有可能变动的因素提取出来组成复数存储表示层:real_part、img_part、magnitude、angle、make_from_real_img、make_from_mag_ang。这一层看到的是数据是结构体的两个成员x和y,或者r和A,如果改变了结构体的实现就要改变这一层函数的实现,但函数接口不改变,因此调用这一层函数接口的复数运算层也不需要改变。复数运算层看到的数据只是一个抽象的“复数”的概念,知道它有直角座标和极座标,可以调用复数存储表示层的函数得到这些座标。再往上看,其它使用复数运算的程序看到的数据是一个更为抽象的“复数”的概念,只知道它是一个数,像整数、小数一样可以加减乘除,甚至连它有直角座标和极座标也不需要知道。

这里的复数存储表示层和复数运算层称为抽象层,从底层往上层来看,“复数”这种数据越来越抽象了,把所有这些层组合在一起就是一个完整的系统。组合使得系统可以任意复杂,而抽象使得系统的复杂性是可以控制的,任何改动都只局限在某一层,而不会影响整个系统。

我们通过一个复数存储表示抽象层把complex_struct结构体的存储格式和上层的复数运算函数隔开,complex_struct结构体既可以采用直角座标也可以采用极座标存储。但有时候需要同时支持两种存储格式,比如先前已经采集了一些数据存在计算机中,有些数据是以极座标存储的,有些数据是以直角座标存储的,如果要把这些数据都存到complex_struct结构体中怎么办?一种办法是complex_struct结构体采用直角座标格式,直角座标的数据可以直接存入complex_struct结构体,极座标的数据先用make_from_mag_ang函数转成直角座标再存,但转换总是会损失精度的。这里介绍另一种办法,complex_struct结构体由一个数据类型标志和两个浮点数组成,如果数据类型标志为0,那两个浮点数就表示直角座标,如果数据类型标志为1,那两个浮点数就表示极座标。这样,直角座标和极座标的数据都可以适配(Adapt)到complex_struct结构体中,无需转换和损失精度:

enum coordinate_type { RECTANGULAR, POLAR};
struct complex_struct {
enumcoordinate_type t;
doublea, b;
};

enum关键字的作用和struct关键字类似,把coordinate_type这个标识符定义为一个类型,只不过struct complex_struct表示一个结构体类型,而enum coordinate_type表示一个枚举(Enumeration)类型。枚举类型的成员是常量,它们的值编译器自动分配,例如定义了上面的枚举类型之后,RECTANGULAR就表示常量0,POLAR就表示常量1。如果不希望从0开始分配,可以这样定义:

enum coordinate_type { RECTANGULAR = 1,POLAR };

这样,RECTANGULAR就表示常量1,而POLAR就表示常量2,这些常量的类型就是int。有一点需要注意,结构体的成员名和变量名不在同一命名空间,但枚举的成员名和变量名却在同一命名空间,所以会出现命名冲突。例如这样是不合法的:

int main(void)
{
enumcoordinate_type { RECTANGULAR = 1, POLAR };
intRECTANGULAR;
printf("%d%d\n", RECTANGULAR, POLAR);
return0;
}

complex_struct结构体的格式变了,就需要修改复数存储表示层的函数,但只要保持函数接口不变就不会影响到上层函数。例如:

struct complex_structmake_from_real_img(double x, double y)
{
structcomplex_struct z;
z.t= RECTANGULAR;
z.a= x;
z.b= y;
returnz;
}

struct complex_structmake_from_mag_ang(double r, double A)

{

structcomplex_struct z;

z.t= POLAR;

z.a= r;

z.b= A;

returnz;

}