乐趣区

关于c:C程序设计-09-自定义数据类型

一、定义和应用构造体变量

1. 定义构造体类型

后面定义应用的变量根本是互相独立、五无在分割的,在内存中的地址也是互不相干的。但在理论生存和工作中,有些数据是有内在联系的、成组呈现的。例如,一个学生的学号、姓名、性别、年龄等,是属于同一个学生的。如果将这些变量别离定义为互相独立的简略变量,难以反映他们之间的内在联系,而数组又只能寄存同一类型的数据。

C 语言容许用户本人建设又不同类型数据组成的数据结构,称为 构造体(structure)。其余一些高级语言中也称为“记录”(record)。申明一个构造体类型的个别模式为:

struct 构造体名 {成员表列};

其中,struct 是申明构造体类型的关键字,不可省略。构造体名由用户指定,又称为构造体标记 (structure tag),以区别于其余构造体类型。成员表列(member list) 也称为域表(field list),每一个成员是构造体中的一个域。成员名命名规定与变量名雷同。构造体的成员也能够是另一个构造体。

也成为下面的例子申明构造体类型如下:

struct Student {char id[10];
    char name[20];
    char sex;
    int age;
}

构造体类型和零碎提供的规范类型具备类似作用,都能够用来定义变量。

2. 定义构造体类型变量

定义了构造体类型后,将来在程序中应用构造体类型的数据,应该定义构造体类型变量,并在其中存放数据。能够采取 3 种办法定义构造体类型变量。

先申明构造体类型,再定义该类型的变量

能够应用下面定义的构造体类型 struct Student 来定义变量:

struct Student student1, student2;

这种模式和定义其余根本类型的变量是类似的。下面定义了 student1student2struct Student 类型的变量。

这种形式是申明类型和定义变量拆散,在申明类型后能够随时定义变量,比拟灵便。

在申明类型的同时定义变量

这种定义方法的个别模式为:

struct 构造体名 {成员表列} 变量名表列;

例如:

struct Student {char id[10];
    char name[20];
    char sex;
    int age;
} student1, student2;

小型程序中,该办法能够间接看到构造体的构造,比拟直观不便。但在大型程序中,往往要求对构造体类型的申明和对变量的定义别离放在不同中央,以使程序结构清晰,便于保护。

不指定类型名而间接定义构造体类型变量

其个别模式为:

struct {成员表列} 变量名表列;

该办法指定了一个无名的构造体类型,因为没有构造体名,因而不宜再次以此构造体类型去定义其余变量。

留神:

  1. 构造体类型与构造体变量是不同的概念,编译时对类型不调配空间,只对变量调配空间。
  2. 构造体类型中的成员名能够和程序中的变量名雷同,但二者不代表同一个对象。
  3. 构造体变量中的成员 (即“域”) 能够独自应用,它的作用和位置相当于一般变量。

3. 构造体变量的初始化和援用

定义构造体时,能够对其进行初始化,而后能够援用该变量。

#include <stdio.h>

int main() {
    struct Student {char id[10];
        char name[20];
        char sex;
        int age;
    } a = {"20220629", "liuyuxin", 'M', 19};  // 定义构造体变量 a 并初始化
    printf("NO.: %s\nName: %s\nSex: %c\nAge: %d", a.id, a.name, a.sex, a.age);
    return 0;
}

// NO.: 20220629
// Name: liuyuxin
// Sex: M
// Age: 19

对构造体变量初始化时,初始化列表是用花括号括起来的一些常量,这些常量顺次赋值给构造体变量中的各成员。C 99 规范容许支队某一成员初始化,如:

struct Student b = {.name = "liuyuxin"};  // 成员名前由成员运算符 .

.name 代表构造体变量 b 中的成员 b.name。其余未被初始化的数值类型成员被零碎初始化为 0,字符型成员被初始化为 \0,指针型成员被初始化为 NULL

独自援用构造体变量中某一成员的值,其办法为:

构造体变量名. 成员名

如下面程序中的 a.ida.name 等。也能够在程序中对这些成员赋值:

b.age = 19;

留神:不同希图通过输入构造体变量名来达到输入构造体变量所有成员的值!只能逐个对各个成员进行输入输出。

如果成员自身又是一个构造体类型,则要用若干个成员运算符,一级一级找到最低一级的成员。只能对最低一级的成员进行赋值、存取和运算。但同类的构造体变量能够互相赋值,如:

student1 = student2;  // student1 和 student2 都为 struct Student 类型变量

二、构造体数组

一个构造体变量中能够寄存一组有关联的数据,如果有多组这样的数组须要加入运算,显然应该应用数组,这就是 构造体数组。构造体数组的每个元素都是一个构造体类型的数据,都别离包含各个成员项。

定义构造体数组的个别模式为:

struct 构造体名 {成员表列} 数组名[数组长度];

对构造体数组的初始化能够在定义数组前面加上一个初值表列。

struct Student {char name[20];
    int age;
} class[3] = {"liu", 19, "yuxin", 20};

三、构造体指针

1. 指向构造体变量的指针

构造体指针就是指向构造体变量的指针,即构造体变量的起始地址。指向构造体对象的指针变量能够既能够指向构造体变量,也能够指向构造体数组中的元素,其基类型必须与构造体变量的类型雷同。

struct Student* p;  // p 能够指向 struct Student 类型的变量或数组元素

p 指向一个构造体变量 stu 时,以下 3 种用法等价:

  1. stu. 成员名 如:stu.name
  2. (*p). 成员名 如:(*p).name
  3. p-> 成员名 如:p->name

2. 指向构造体数组的指针

能够用指针变量指向构造体数组的元素。

#include <stdio.h>

struct Student {char name[20];
    int age;
};
struct Student class[3] = {{"liu", 19}, {"yu", 20}, {"xin", 21}};

int main() {
    struct Student* p;
    printf("Name     Age\n");
    for (p = class; p < class + 3; p++)
        printf("%5s%8d\n", p->name, p->age);
    return 0;
}

//  Name     Age
//   liu      19
//    yu      20
//   xin      21

3. 用构造体变量和构造体变量的指针做函数参数

将一个构造体变量的值传递给一个函数由 3 个办法:

  1. 用构造体变量的成员做实参,传递给形参。用法与一般变量做形参统一,属于“值传递”的形式。
  2. 用构造体变量做实参,形参也必须是同类型的构造体变量,也属于“值传递”形式。这种形式在工夫和空间上的开销较大,且如果在执行被调用函数过程中该变量形参的值,该值不能返回主调函数,往往造成应用上的不便,因而较少应用。
  3. 用指向构造体变量的 (或数组元素) 的指针做实参,将构造体变量 (或数组元素) 的地址传给形参。

上面的程序实现了输出 3 个学生 3 门课程的问题,输入均匀问题最高的学生信息:

#include <stdio.h>
#define N 3 // 学生数为 3

struct Student { // 建设构造体类型
    char name[20];
    float score[3];
    float aver;
};

int main() {void input(struct Student stu[]);
    struct Student max(struct Student stu[]);
    void print(struct Student stud);
    struct Student stu[N], *p = stu;
    input(p);
    print(max(p));
    return 0;
}

void input(struct Student stu[]) { // 输出学生姓名、3 门课问题
    int i;
    for (i = 0; i < N; i++) {scanf("%s %f %f %f", stu[i].name, &stu[i].score[0], &stu[i].score[1],
              &stu[i].score[2]);
        stu[i].aver =
            (stu[i].score[0] + stu[i].score[1] + stu[i].score[2]) / 3.0;
    }
}

struct Student max(struct Student stu[]) { // 求均匀问题最高的学生
    int i, m = 0;
    for (i = 0; i < N; i++) {if (stu[i].aver > stu[m].aver)
            m = i;
    }
    return stu[m];
}

void print(struct Student stud) { // 输入问题最高的学生的信息
    printf("Name: %s\nScore: %2.lf, %2.lf, %2.lf\nAver: %2.lf\n", stud.name,
           stud.score[0], stud.score[1], stud.score[2], stud.aver);
}

四、用指针解决链表

1. 链表

链表 是一种常见的重要的数据结构,它是动静地进行存储调配的一种构造。用数组存放数据时,必须当时定义固定的数组长度 (即元素个数),这样有时会节约内存。链表则没有这种毛病,它依据须要开拓内存单元。下图示意最简略的一种链表(单向链表) 的构造。

链表有一个“头指针”变量(图中的 head),它寄存一个地址,该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应包含两个局部:

  1. 用户须要用的理论数据;
  2. 下一个结点的地址。

能够看出,head 指向第 1 个元素,第 1 个元素又指向第 2 个元素……直到最初一个元素,该元素不再指向其余元素,它称为“表尾”,它的地址局部放一个空地址 NULL,链表到此结束。

链表中各元素在内存种的地址能够是不间断的。要找到某一元素,必须先找到上一个元素,依据它提供的地址能力找到下一个元素。如果不提供头指针 head,则整个链表都无法访问。

显然,链表这种数据结构,必须应用指针能力实现,即一个结点种应蕴含一个指针变量,用它寄存下一结点的地址。用构造体变量去建设链表是最合适的。在构造体变量中应用指针型成员来寄存下一个结点的地址(一个指针类型的成员既能够指向其余类型的构造体数据,也能够指向本人所在的构造体类型数据)。如:

struct Student {char name[20];
    int age;
    struct Student* next;  // 指针变量,指向构造体变量
};

下面的程序中,nameage 用来寄存结点中的有用数据。nextstruct Student 类型中的成员,而它又指向本人所在的构造体类型的数据。用这种办法就建设了一个链表。

2. 建设简略的动态链表

上面的程序建设和输入了一个简略链表:

#include <stdio.h>
#include <string.h>

struct Student {char name[20];
    int age;
    struct Student* next;
};

int main() {
    struct Student a, b, c, *head, *p;
    strcpy(a.name, "liu");
    a.age = 18;
    strcpy(b.name, "yu");
    b.age = 19;
    strcpy(c.name, "xin");
    c.age = 20;
    head = &a;
    a.next = &b;
    b.next = &c;
    c.next = NULL;
    p = head;
    do {printf("Name: %s\tAge: %d\n", p->name, p->age);
        p = p->next;
    } while (p != NULL);
    return 0;
}

// Name: liu       Age: 18
// Name: yu        Age: 19
// Name: xin       Age: 20

3. 建设动静链表

建设动静链表是指在程序执行过程中从无到有地建设起一个链表,即一一地开拓结点和输出各结点数据,并建设起前后相链的关系。这须要用到 08 – 指针中介绍的动态内存调配及其无关函数。

#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct Student)

struct Student {
    long id;
    int age;
    struct Student* next;
};

int n;

struct Student* creat(void) {  // 建设链表
    struct Student *head, *p1, *p2;
    n = 0;
    p1 = p2 = (struct Student*)malloc(LEN);
    scanf("%ld %d", &p1->id, &p1->age);
    head = NULL;
    while (p1->id != 0) {
        n = n + 1;
        if (n == 1)
            head = p1;
        else
            p2->next = p1;
        p2 = p1;
        p1 = (struct Student*)malloc(LEN);
        scanf("%ld %d", &p1->id, &p1->age);
    }
    p2->next = NULL;
    return (head);
}

void print(struct Student* head) {  // 输入链表
    struct Student* p = head;
    if (head != NULL) {
        do {printf("\nID: %ld\tAge: %d", p->id, p->age);
            p = p->next;
        } while (p != NULL);
    }
}

int main() {
    struct Student* head;
    head = creat();
    print(head);
    return 0;
}

// 001 18
// 002 19
// 003 20
// 0 0
// 
// ID: 1   Age: 18
// ID: 2   Age: 19
// ID: 3   Age: 20

五、共用体类型

1. 共用体类型

有时候想用一段内存单元寄存不同类型的数据,这些数据占用的字节数可能不同,但都从同一地址开始寄存,也就是应用笼罩技术,后一个数据笼罩前一个数据。这种使几个不同的变量共享同一段内存的构造,称为 共用体类型构造。也有译为“联结”。

定义共用体类型的个别构造为:

union 共用体名 {成员表列} 变量表列;

与构造体类型类似,共用体类型也能够定义类型的同时定义变量、先定义类型后定义变量、间接定义变量。

// 定义类型的同时定义变量
union Data {
    int i;
    char j;
    float k;
} a, b, c;

// 先定义类型后定义变量
union Data {
    int i;
    char j;
    float k;
};
union Data a, b, c;

// 间接定义变量
union {
    int i;
    char j;
    float k;
} a, b, c;

留神:构造体变量所占内存长度是各成员所占内存长度之和;共用体变量所占内存长度等于最长成员的长度。

2. 援用共用体变量

共用体变量只有定义之后能力援用,但不能引用它自身,只能援用其中的成员。援用办法构造体变量类似:

共用体变量名. 成员名

3. 共用体类型的特点

共用体类型数据有以下特点:

  1. 同一个内存段能够用来寄存几种不同类型的成员,但在每一刹时只能寄存其中一个成员,而不是同时寄存几个。

    union Data {
        int i;
        char j;
        float k;
    } a;
    a.i = 97;
    printf("%d %c %f", a.i, a.j, a.k);
    
    // 97 a 0.000000
  2. 能够对共用体类型初始化,但初始化表中只能有一个常量。

    union Data {
        int i;
        char j;
        float k;
    } a = {1, 'a', 1.5};  // 谬误
    union Data a = {16}  // 正确
    union Data a = {.j='a'}  // 正确
  3. 共用体变量中起作用的成员是最初一次被赋值的成员,在对共用体变量中的一个成员赋值后,原有变量存储单元中的值就被取代。
  4. 共用体变量的地址和它各成员的地址都是同一地址。
  5. 不能对共用体变量名赋值,也不能希图援用变量名来失去一个值。C 99 容许同类型的共用体变量互相赋值。
  6. C 99 容许应用共用体变量及指向共用体变量的指针做函数参数。
  7. 共用体类型能够呈现在构造体类型定义中,也能够定义共用体数组。反之,构造体也能够呈现在共用体类型定义中,数组也能够作为共用体的成员。

六、枚举类型

如果一个变量只有几种可能的值,则能够定义为 枚举 (enumeration) 类型,所谓“枚举”就是指把可能的值一一列举进去,变量的值只限于列举进去的值的范畴内。

定义枚举类型的个别模式为:

enum 枚举名 {枚举元素};

枚举元素也称为枚举常量。

能够先定义枚举类型,再定义枚举变量,如:

enum Weekday {sun, mon, tue, wed, thu, fri, sat};
enum Weekday workday, weekend;

也能够不申明枚举类型名,间接定义枚举变量:

enum {sun, mon, tue, wed, thu, fri, sat} workday, weekend;

枚举变量的值只能限于定义中的枚举元素的值:

workday = mon;  // 正确
workday = monday;  // 谬误

须要留神的是:

  1. C 编译对枚举类型的枚举元素按常量解决,故称枚举常量。不能因为它们是标识符 (有名字) 而把它们看作变量,不能对它们赋值。
  2. 每一个枚举元素都代表一个整数,C 语言编译按定义时的程序默认它们的值为 0,1,2,3,4……。在下面的定义中,sun 的值主动设为 0,mon 的值为 1,…… sat 的值为 6。因而上面两个语句等价:

    workday = mon;
    workday = 1;
  3. 枚举常量能够援用、输入、判断,还能够人为指定枚举常量的数值:

    enum Weekdaty {sun = 7, mon = 1, tue, wed, thu, fri, sat} workday;
    workday = sun;
    if (workday > mon)
        printf("%d", workday);
    
    // 7

七、申明新类型名

除了应用 C 提供的规范类型和自定义的构造体、共用体、枚举类型外,还能够应用 typedef 指定新的类型名来代替已有的类型名。

1. 用新类型名代替原有类型名

typedef int Integer;  // 指定用 Integer 代表 int
typedef float Real;  // 指定用 Real 代表 float

2. 用简略类型名代替简单类型名

定义一个简略类型名代替简单类型名的办法是:按定义变量的形式,把变量名换为新类型名,并且在最后面加 typedef,就申明了一个新类型名代表原来的类型。

命名一个新的类型名代表构造体类型:

typedef struct {char name[20];
    int age;
} Data;  // 指定用 Data 代表构造体类型
Data stu1, stu2;  // 用新类型名定义变量

命名一个新的类型名代表数组类型:

typedef int Num[100];  // 申明 Num 为整型数组类型名
Num a;  // 用新类型名定义一个整形数组

命名一个新类型名代表指针类型:

typedef char* String;  // 申明 String 为字符指针类型
String p, s[10];  // 定义一个字符指针变量和一个字符指针数组

命名一个新类型名代表指向函数的指针类型:

typedef int (*Pointer)();  // 申明 Pointer 为指向函数的指针类型
Pointer p1, p2;  // 定义 p1,p1 为 Pointer 类型的指针变量

不同源文件中用到同一类型数据 (尤其是像数组、指针、构造体、共用体等类型数据) 时,罕用 typedef 申明一些数据类型。能够把所有的 typedef 名称申明独自放在一个头文件中,而后在须要用到它们的文件中用 #include 指令把它们蕴含到文件中。这样编程者就不须要在各文件中本人定义 typedef 名称了。该办法有利于程序的通用与移植,升高了程序对硬件个性的依赖性。


Reference:

谭浩强《C 程序设计(第五版)》

退出移动版