结构体

UDT(用户自定义类型):分为结构体、共用体和枚举类型等;

结构体类型

声明一个结构体类型的一般形式如下,使用时必须对各成员都进行类型声明即类型名 成员名;

1
2
3
struct 结构体类型名{    //类型名用于作结构体类型的标志
成员表 //又称为域表,每一个成员称为结构体中的一个域
};

在C++中,结构体的成员既可以包括数据也可以包括函数;

当我们对结构体类型作出定义时,相当于定义了一个模型,系统并不对之分配实际内存单元;

在使用时要定义结构体类型的变量,并在其中存放具体的数据;

[!NOTE]

结构体变量必须先定义、赋值,然后使用。

定义方法:

  1. 先声明类型再定义(推荐

    便于修改和使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //#include<string>,如果换为string类型,要引入这个头文件
    #include<cstring> //引用strcpy等函数
    using namespace std;//C++会将标准库中提供的类放到这个std文件夹里
    struct Student{
    int num; //占4字节
    char name[20];//char数组类型,占20字节
    char sex; //占1字节
    int age; //按照数据成员对齐原则,在这之前要填充3字节,从28开始存,占4字节
    float score; //占4字节
    char addr[30];//char数组类型,如果换为string类型:string addr;
    /*根据结构体整体对齐原则,在addr之后要填充2字节,使整体大小为4的整数倍(数组不看)*/
    };
    Student student1, student2; //定义变量,定义后系统才会分配内存单元
    student1.num = 1001;
    /*这里的student1.name是数组首地址,为常量指针(不可修改)*/
    strcpy(student1.name,"Zhang Liang");//用 strcpy 函数把字符串的每个字符逐个复制到数组里
    student1.sex = 'M';
    student1.age = 19;
    student1.score = 90;
    strcpy(student1.addr,"Hainan");//改为string类型后可直接赋值,student1.addr = "Hainan"

    【号外:内存对齐原理】

    数据成员对齐:每个数据成员的起始地址必须是它自身大小的整数倍;

    结构体整体对齐:结构体的总大小必须是结构体中最大数据成员大小的整数倍

    若调换上例中结构体中的成员顺序,结构体整体大小也会变化,实际上,在分配存储单元时,以字为单位进行分配(4B),因此如果通过调整顺序减少了内存空间(提高内存利用率),整体所占大小也是64,而不是原本的63。

  2. 声明类型同时定义

    适用于程序简单,结构体只用于本文件

    1
    2
    3
    4
    5
    6
    7
    8
    struct  Student         //声明结构体类型Student
    { int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
    } student1, student2; //定义两个结构体类型变量,这里放着变量名表,后续可补充
  3. 直接定义结构体类型变量

    结构体类型仅限于本语句情况;后续无法再用这个结构体类型创建新变量;

    1
    2
    3
    4
    5
    6
    7
    8
    struct                //无类型名,匿名类型
    { int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
    } student1, student2; //只能在这定义,后续不能复用

结构体中的成员也可以是一个结构体变量;

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Date   //声明一个结构体类型Date
{ int month;
int day;
int year;
};
struct Student //声明一个结构体类型Student
{ int num;
char name[20];
char sex;
int age;
Date birthday; //将成员birthday指明为Date类型
char addr[30];
}student1,student2;

使用时可以将一个结构体变量的值赋给另一个具有相同结构的结构体变量;

对结构体数据不能整体性输出,必须一个一个成员地输出(输入也是如此)

结构体数组:每个数组元素都是一个结构体的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <cstring>
using namespace std;
struct Person {
char name[20]; // 候选人姓名(C风格字符数组,最多存19个字符+1个结束符'\0')
//string name;可以把这换成字符串类型
int count; // 候选人得票数
};

int main()
{
// 6. 定义Person类型的数组leader,包含3个元素(对应3个候选人)
// 初始化:每个元素的name赋值为候选人姓名,count(得票数)初始化为0
Person leader[3] = {{"Li",0},{"Zhang",0},{"Sun",0}};

int i,j; // 7. 定义循环变量i(选票次数)、j(候选人索引)
char leader_name[20]; // 8. 定义字符数组,存储投票人输入的候选人姓名
//string leader_name;//同步改为字符串变量
// 9. 循环10次,模拟10张选票的输入与统计
for(i=0; i<10; i++) {
cin >> leader_name; // 10. 输入票上填写的候选人姓名

// 11. 内层循环:遍历3个候选人,匹配输入的姓名
for(j=0; j<3; j++) {
// strcmp比较两字符串:相等返回0,不相等返回非0
// 若输入的姓名与第j个候选人姓名匹配,该候选人得票+1
//leader_name == leader[j].name 改成字符串之后条件就可以变成这样了
if(strcmp(leader_name, leader[j].name) == 0) {
leader[j].count++;
}
}
}
cout << endl; // 13. 输出空行,分隔输入与结果,提升可读性
// 14. 循环遍历3个候选人,输出最终得票结果
for(i=0; i<3; i++) {
// 15. 输出第i个候选人的姓名和得票数
cout << leader[i].name << ":" << leader[i].count << endl;
}

return 0;
}

结构体变量的地址就是该变量所占据的内存段的起始地址,可以用一个指针变量来指向一个结构体变量,如Student *p= &stu;

此时对于结构体成员的操作可分为:

  • 结构体变量.成员名
  • (*p).成员名 【成员运算符“ . ” 优先于“ * ”运算符,所以必须带括号】
  • p->成员名

区分运算:

  • p->n++ :先得到p指向的成员n的值,使用完后使值加1
  • ++p->n : 先得到p指向成员n的值,加1后使用

结构体链表

用结构体变量和指向结构体变量的指针构成链表(包含两种成员:一种是用户需要用的实际数据,另一种是用来存放下一结点地址的指针变量)

链表有一个“头指针”变量以head表示,用于存放链表中第一个元素的地址;

链表中的每一个元素称为“结点”,每个结点对应两个部分,即数据加下一个结点地址;

最后一个结点无指向,因此称为“表尾”,其地址部分存放一个“NULL”空地址,表示链表到此结束;

链表中各元素在内存的存储单元中是可以不连续的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*----这里是一个静态链表的示例(所有结点都在程序定义而不是运行时临时开辟)--------*/
//动态链表则是指各结点是在运行时可以随时插入和删除的
struct Student
{ int num; //学号
float score; //成绩
Student *next; //next指向Student结构体数据,即下一个结点
};
int main(){
Student a,b,*head,*p;
a.num = 2001;
a.score = 88.0;
b.num = 2002;
b.score = 89.2;

head = &a;
a.next = &b;
b.next = &c;
c.next = NULL;
p = head; //使p指针指向头结点
do{
cout<<(*p).num<<" "<<p->score<<endl;
}while(p!=NULL); //输出完c结点后p的值会变为NULL
return 0;
}

程序设计者不必具体知道各结点的具体地址,只要保证能将下一个结点的地址放到前一结点的成员next中即可。

结构体类型数据作为函数参数

(1) void print(Student stu) //用结构体变量作函数参数 【直观,但要单独为形参开辟内存单元,效率不高】

(2) void print(Student *p) //用结构体类型的指针变量作函数参数 【只将stu的起始地址传给形参】

(3) void print(Student &stud) //用结构体变量的引用作函数参数 【实参和形参代表同一对象,传递stu地址】、

动态分配/撤销内存空间

C语言利用库函数malloc和free来分配和撤销内存空间;

C++提供了较简便而功能较强的运算符new和delete。

[!WARNING]

撤销由指针变量指向的动态内存空间后,不会撤销指针变量本身。指针变量依然存在,值也不变,只是指向的值未知了

【例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main( ) {
int *p , *pt, *head;
p = new int; //用new进行指定,空间的大小由系统根据Student类型自动算出
*p = 100;
head = new int[5];
pt = head;
*pt = 1; //为数组元素赋值
pt++;
*pt = 2; //为数组元素赋值
cout <<*p <<" "<< *head<<" "<< *pt << endl ;
delete p; //撤销整数空间
delete [ ]head; //撤销整型数组内存空间
cout <<*p <<" "<< *head<< endl ;
//撤销内存空间后,指向的值不确定了
head = NULL; //指针用完后应及时赋值为NULL
return 0;
}
/*
在代码中,虽然执行了delete []head;来释放动态分配的整型数组空间,但后续cout<<*head;依然能输出看似有效的值,这是因为内存并未立即被操作系统回收且访问已释放内存的行为是未定义的,可能导致数据错误(得到不可预测结果)、程序崩溃、看似正常工作等情况。
*/

【动态建立链表】

1
2
3
4
5
6
7
8
9
10
11
12
 do {
cout<< "num: "; cin>> n;
if(n<=0) break; //提前结束
temp = new Student; //生成新的结点
temp->num = n;
cout<< "score: "; cin>> temp->score;
temp->next = NULL;
p->next = temp; //将新结点加入到链表
p= temp; //p始终指向链尾结点
i++; //计数
}while(i<10);
p= head; //把指针拉回链首

共用体类型

允许在同一个存储空间中存放多个不同类型的变量

声明共用体类型:union 共用体类型名 {成员列表};【”成员列表”是用基本类型声明的多个变量】

【相同类型】

1
2
3
4
5
6
7
8
9
10
union Data {  //声明共用体类型
int num;
int id; //声明的相同类型,两个变量共用同一存储空间
};
Data a; //定义共用体变量
a.num = 1001;
cout<< a.num << endl; //输出1001
a.id = 1010;
cout<< a.id << endl; //输出1010
cout<< a.num << endl; //输出1010

【不同类型】

不过共用体里只能放基本数据类型,因此string这种就不可以;

共用体的大小为最大类型的大小。

1
2
3
4
5
6
7
8
9
10
union Data {  //声明共用体类型
int num;
char id[20]; //存储另一个类型的变量后,原来的变量就恢复为未初始化的状态
}hah; //hah就是声明的成员变量名
Data a; //定义共用体变量
a.num = 1001;
cout<< a.num << endl; //输出1001
strcpy(a.id, "1010"); //对于字符数组的类型都要通过strcpy往里逐个复制
cout<< a.id << endl; //输出1010
cout<< a.num << endl; //输出808529969

对于匿名共用体,一般用于直接定义共用体变量,只在本文件中使用;

在struct结构体中使用union共用体时,将union共用体作为结构体中的一个成员(此时可省略类型名);

当union声明了成员变量名时,引用union的成员需要逐级引用

枚举类型

指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内,用 enum 开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;

/*--今天星期几?----*/
// 1. 定义枚举类型:weekday(表示“星期”的枚举类型)
// 枚举元素:sun/mon/tue...sat 对应 周日到周六,默认从0开始赋值(sun=0, mon=1...sat=6)
enum weekday {sun, mon, tue, wed, thu, fri, sat};

// 2. 自定义函数:根据输入的n和m,计算并返回对应的星期几
// 参数:n和m是整数(比如n是基准星期数,m是偏移天数)
// 返回值:weekday类型(枚举类型,只能是sun/mon...sat中的一个)
weekday getWeekday(int n, int m)
{
// 3. 定义枚举类型变量result,用于存储计算结果
weekday result;

// 4. 核心计算逻辑:
// (n+m)%7:保证结果在0~6之间(对应周日到周六)
// weekday(...):强制类型转换,把整数结果转成weekday枚举类型
// 比如:(0+1)%7=1 → 转成mon(周一);(1+6)%7=0 → 转成sun(周日)
result = weekday( (n + m) % 7 );

// 5. 返回枚举类型的结果
return result;
}

int main() {
// 6. 定义枚举类型变量week,用于接收函数返回的“星期几”
weekday week;

// 7. 测试:假设n=sun(0,周日),m=3(加3天),计算结果
int n = 0, m = 3;
// 8. 调用函数,把计算出的枚举值赋值给week
week = getWeekday(n, m);

// 9. 打印结果(枚举值本质是整数,可直接输出;也可转成文字)
cout << "计算出的星期数(枚举对应整数):" << week << endl;
// 10. 把枚举值转成直观的文字(新手必学:枚举的实际应用方式)
switch (week) {
case sun: cout << "对应的星期:周日" << endl; break;
case mon: cout << "对应的星期:周一" << endl; break;
case tue: cout << "对应的星期:周二" << endl; break;
case wed: cout << "对应的星期:周三" << endl; break;
case thu: cout << "对应的星期:周四" << endl; break;
case fri: cout << "对应的星期:周五" << endl; break;
case sat: cout << "对应的星期:周六" << endl; break;
}

return 0;
}

枚举元素按常量处理,枚举元素作为常量,它们是有值的,其值是一个整数

枚举值可以用来做判断比较,按整数比较规则进行比较(默认第1个枚举元素的值为0)

不能把一个整数直接赋给一个枚举变量,枚举变量只能接受枚举类型数据。

用typedef声明新的类型名

用typedef声明一个新的类型名来代替已有的类型名也可以对结构体去声明一个新的名字;

习惯上常把用typedef声明的类型名用大写字母表示

不能用来定义变量【如 typedef int a; //试图用typedef定义变量a, 非法】

步骤:

(1)按定义变量的方法写出定义语句;

(2)将变量名换成新类型名

(3)在最前面加typedef

(4)可以用新类型名去定义变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//若在一个程序中i,j是用来专门计数的
typedef int COUNT; //指定用标识符COUNT代表int类型
INTEGER i, j;

//给结构体声明一个新名字
typedef struct Date
{ int month;
int day;
int year;
} DATE; //此处DATE是新类型名,而不是结构体变量名,对匿名的也行
DATE birthday;
DATE *p;//p为指向此结构体类型数据的指针

//声明一个新的类型名
typedef int NUM[100]; //声明NUM为整型数组类型,包含100个元素
NUM n; //定义n为包含100个整型元素的数组

//声明字符指针类型
typedef char * STRING; //声明STRING为char * 类型,即字符指针
STRING p, s[10]; // //定义p为char * 型指针变量,s为char *类型的指针数组(有10个元素)

//声明函数指针
typedef int (*POINTER)( )//声明POINTER为指向函数的指针类型,函数返回整型值
POINTER p1, p2;