Linux C 编程学习第四天_结构体&数据抽象

结构体

由于我水平十分有限,这个系列的随笔也只是对C语言进行查漏补缺,结构体接触的十分少,依稀记得大学里教的C语言到指针就结束了,结构体只停留在知道它是个混合类型的类似数组的东西,所以先进行基础知识的恶补。

C数组允许定义可存储相同类型数据项的变量,结构式 C 编程中另一种用户可以自己定义的可用的数据类型,它允许存储不同类型的数据项。

结构体的定义必须使用 struct 语句,格式如下:

 

 tag 是结构体标签,member-list 是标准的变量定义,variable-list  结构变量,定义在末尾

long time ago................

刚看完菜鸟的 C 结构体,有些许一知半解,但是还好,我接着看linux C

我们所使用的大多数类型,具有单一的值,例如整数、字符、布尔值、浮点数,这些称之为基本数据类型(Primitive Type)。字符串是一个例外,它由很多字符组成,这样由基本类型组成的数据称之为复合数据类型(Compound Type)由基本类型组成符合类型也有组合规则,结构体、数组、字符串。一方面组合数据类型可以当作一个数据来使用,也可以单独访问各自组成单元。复合数据类型这种两面性体提供了一种数据抽象(Data Abstraction)的方法。

学习一门语言特别要注意以下3个方面:

 

 开始定义结构体:

 首先我们定义了 my_game 这个标识符,它的命名规则和变量一样,但是它不是一个变量,它表示一个类型,自己定义的复合类型,它在C中称之为 Tag ,{ } 括起来整体可以看作是一个复合类型名字,就像是int,char这样,我们用它定义了  L4D2   PUBG    FGO  三个变量,只要定义了Tag,以后就可以直接这样定义其他的变量了:

这个定义可以写在函数外面,这样就可以在任何函数中使用,就和全局变量一样。 

结构体变量可以在定义时初始化

如果在一开始就定义变量,就可以不写 Tag 了:

 但是这样意味着,以后没办法再次引用这个结构体类型,因为它没有名字。

结构体初始化的时候可以直接赋值:

如果 { } 中的数据比结构体的成员多,就会报错,如果数据比结构体成员少,那么未指定的成员就会用0来代替。

虽然可以在定义的时候直接赋值,但是定义之后就不能全体赋值了。

如下图14行的方法就会报错。

 

 

结构体访问:

 使用   .   这个运算符来访问结构体成员:

 

 赋值可以在定义完结构体后单独给结构体成员赋值

 

 但是需要注意的是,单独给结构体成员赋值必须写在函数里,写在函数外会报错。结构体初始化可以写在函数外,但是结构体赋值不能写在函数外,这是结构体和一般变量不同的地方。

结构体类型的值在表达式中有很多限制,不像基本数据那么自由,+ - * /  && || !都不能用于结构体类型,if  while 的控制表达式的值也不能是结构体类型。

可以做运算的的类型称为算术类型(Arithmetic Type)算数类型包括整型和浮点。

可以做逻辑与或非的操作数或者if for while  的控制表达式的类型称为标量类型(Scalar Type)标量类型包括算数类型和指针类型。

结构体之间使用赋值运算时允许的,可以用一个结构体去初识化另外一个结构体:

 

 赋值也可以:

结构体可以有返回值,也可以当函数的参数,也可以用结构体去定义结构体:

 1 #include <stdio.h>
 2 
 3 struct complex_struct
 4 {
 5     int i;
 6     int j;
 7 }z1 = {1, 2};
 8 
 9 struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2)
10 {
11     z1.i = z1.i + z2.i;
12     z1.j = z1.j + z2.j;
13     return z1;
14 }
15 
16 int main(int argc, char const *argv[])
17 {
18     complex_struct z2 = {2, 4};
19     complex_struct z3;
20 
21     z3 = add_complex(z1,z2);
22     printf("%d\n%d\n", z3.i, z3.j);
23     return 0;
24 }

上述代码中,第九行定义了一个函数,函数的参数为两个结构体 z1 z2,同时函数返回一个结构体 z1 ,在函数中,实现了两个结构体的 i 相加,j 相加。

也就是一个复数的加法,实部和实部相加,虚部和虚部相加。

 

 

数据抽象:

这是一种全新的编程思维,我从来没接触过,看完了又是一波恍然大悟,之后我消化结束就从新整理。

看了3遍这一章节,觉得,结构体实在是太厉害了,是什么样的人才能想出这样的方法,学习这样编程的思维和方法简直就是一种享受

进入正题:

在前面学到了使用结构体构造一个复数:

现在我们将这个struct定义理解为定义了一个拥有两个 double 变量的变量,一定要贯彻这个思想,将 struct complex_struct  看成类似于int double 这样的变量,这很重要。

接着像是构建返回整型的函数 int XXX() 一样构建一个返回类型为  struct complex_struct  的函数:

 

 

 这是一个名为:make_from _real_img 的函数,返回类型为 struct complex_struct 

函数的作用是构建一个  a + bi 类型的复数:

 

 

 当输入  x , y  (实部,虚部)就会构建一个  x + yi  这样的复数

可能你会觉得多此一举,一开始定义了结构体,又要定义复数,直接使用 struct complex_struct z = { 2, 3 }  这比调用 struct complex_struct z = make_from_real_img( 2 ,  3 ) 方便多了,根据前面的编程思维,不断调用之前写过的代码的原则,这样不久更麻烦了吗?

我们继续往下看:

我们知道复数有两种写法,一种是上面那样 a + bi,还有一种是  模 + 幅角 z=r*(cosA + i sinA) 这样的写法,接着我们需要一个输入 r 和 A 来构建复数的函数,那么怎么写呢,很显然直接使用一开始构建的结构体难以做到,但是采用上面的构建 实部+虚部 复数函数的方法:

 

 

 这样就解决了这个问题

接着根据复数的定义给出下面四个定义:

//虚数实部:
double real_part(struct complex_struct z)
{
    return z.x;
}

//虚数虚部:
double img_part(struct complex_struct z)
{
    return z.y;
}

//虚数的模
double magnitude(struct complex_struct z)
{
    return sqrt(z.x * z.x, z.y * z.y);
}

//虚数的辅角
double angle(struct complex_struct z)
{
    return atan2(z.y, z.x);
}

现在我们可以单独使用某个复数的实部、虚部、模、幅角,接着就可以根据定义构建复数基本运算函数了:

//虚数加法:
struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_real_img(real_part(z1) + real_part(z2), img_part(z1) + img_part(z2));    
}

//虚数减法:
struct complex_struct sub_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_real_img(real_part(z1) - real_part(z2), img_part(z1) - img_part(z2));    
}

//虚数的乘法:
struct complex_struct mul_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_mag_angle(magnitude(z1) * magnitude(z2), angle(z1) + angle(z2))
};

//虚数的除法:
struct complex_struct div_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_mag_angle(magnitude(z1) / magnitude(z2), angle(z1) - angle(z2))
};

是不是觉得这样是可行的,也能理解,以上所有代码都是将复数计算返回 a + bi 这样的格式

那如果我现在要让他们计算之后返回 极坐标 形式输出呢?这么一想,完了,要改最开始的结构体定义,好像所有的函数都要做修改,既不是很麻烦,但是是这样吗?我们为什么采取上述看似更加麻烦的编程方式,接下来就更改。(千万不要嫌麻烦,编程就是这样的过程,不断去更改,将考虑到的新事物添加进去,修改测试,又有想法,再添加修改,直到最后成品)

接着给出所有更改后的函数

//结构体定义
struct complex_struct
{
    double r, A;
};

//复数的实部
double real_part(struct complex_struct z)
{
    return z.r * cos(z.A)
}

//复数的虚部
double real_part(struct complex_struct z)
{
    return z.r * sin(z.A)
}

//复数的模
double magnitude(struct complex_struct z)
{
    return z.r
}

//复数的辐角
double angle(struct complex_struct z)
{
    return z.A
}

//构建a + bi类型复数
struct complex_struct make_from_real_img(double x, double y)
{
    struct complex_struct z;
    z.A = atan2(y, x);
    z.r = sqrt(x * x + y * Y);
    return z;
}

//构建 极坐标型 复数
struct complex_struct make_from_mag_ang(double r, double A)
{
    struct complex_struct z;
    z.A = A;
    z.r = r;
    return z;
}

上述代码进行运算的时候,使用的都是极坐标的 r 和 A,那么是否我们也要对运算函数进行更改?

答案是不用,虽然我们更改了 complex_struct 的格式,但是加减乘除四个运算均不需要进行任何改动,依然可以使用。

仔细观察运算的函数:

//虚数加法:
struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_real_img(real_part(z1) + real_part(z2), img_part(z1) + img_part(z2));    
}

//虚数减法:
struct complex_struct sub_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_real_img(real_part(z1) - real_part(z2), img_part(z1) - img_part(z2));    
}

//虚数的乘法:
struct complex_struct mul_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_mag_angle(magnitude(z1) * magnitude(z2), angle(z1) + angle(z2))
};

//虚数的除法:
struct complex_struct div_complex(struct complex_struct z1, struct complex_struct z2)
{
    return make_from_mag_angle(magnitude(z1) / magnitude(z2), angle(z1) - angle(z2))
};

发现这些函数中,并没有对结构体成员的直接访问,都是将结构体当作整体来使用,可以说,输入的是 a+bi 格式的复数,运算结束还是 a+bi 格式,输入极坐标格式的复数,运算之后还是极坐标格式,本体运算不需要任何更改,有种适应性很强的感觉。

发现了吗,这就是抽象的思想,复数运算中,有可能出现直角坐标和极坐标两种格式,我们将复数抽象为一种全新的类型,接着基于这个类型编写运算函数,那么无论复数采用的是直角坐标还是极坐标,都属于这种类型,都使用于我们基于这个类型编写的运算函数。

 

 我们将上述代码分为三层:

构建复数类型的为一层:real_part、img_part、magnitude、angle、make_from_real_img、make_from_mag_ang

进行复数运算的为二层:add_complex、sub_complex、mul_complex、div_complex

进行其他运算的为三层:cos,sin,atan2,  sqrt

这些层中,第一层无论成员是 x, y,还是 r , A 和第二层的函数接口并没有发生改变,所以调用这一层函数接口的复数运算层也不需要改变,进行复数运算的第二层从第一层获取的数据是抽象的全新类型,它不管是极坐标还是直角坐标,在它眼里都是一个类型,是一个抽象的“复数”的概念,只知道它可以进行运算,根本不用在意它是直角坐标还是极坐标。

这里第一层和第二层称为抽象层(Abstraction Layer)从下向上看,“复数”这个类型越来越抽象,把这些层组合在一起就是一个完整的系统,系统可以任意复杂,任何改动只是在某一层进行修改,只要不改变接口,任意修改都不会波及整个系统(在这里是整个代码段)。

到这里我们又有新的思想了,这样既不是还是很麻烦,要是我输入是直角坐标,但是运算结束之后我想要极坐标怎么办?虽然每次进行更改虽然只是一层,但是代码量还是很多,有没有那种可以在输入的时候就告诉代码,我输入的是什么类型,要进行什么运算,输出的值我要什么类型,这样更加方便可控,将这些代码整合在一起,变得更加简洁和方便,当然是有,这就是——数据类型标志,详解见下一章。

posted @ 2021-05-20 02:20  哿与银冰  阅读(217)  评论(0)    收藏  举报