1. 为什么要引入结构类型?
结构体是将不同类型的数据组合成一个有机整体以便于引用,解决了数组只能对同类型的数据进行处理的缺陷。
2. 结构类型
- 结构类型的声明:系统不会为结构类型分配内存空间,只有在定义了结构体变量后,系统才会为之分配内存单元。
#include <stdio.h>
#include <string.h>
// C++ 中的定义方式
struct 结构类型标识符(eg: Contact) // 一种新的构造类型 Contact, 地位和 int、double 之类的相同
{
结构成员1; (eg: int id;) // 结构成员定义形式:type varName1;
// 成员变量的定义和普通变量的定义方式是一样的
结构成员2; (eg: char name[16];)
┆
结构成员n; (eg: char phone[16];)
}; // 请注意,别忘记这个小不点^_^!
// C 中的定义方式,一般包含在头文件中
typedef struct 结构类型别名 // struct 的别名
{
结构成员1; (eg: int id;) // 结构成员定义形式:type varName1;
// 成员变量的定义和普通变量的定义方式是一样的
结构成员2; (eg: char name[16];)
┆
结构成员n; (eg: char phone[16];)
}Contact; // 一种新的构造类型 Contact, 地位和 int、double 之类的相同
// 新类型的使用和基本类型差不多,它只是基本数据类型的组合
// 定义时进行初始化
Contact c = {201501, "John", "18601011223"};
Contact c; // 定义一个变量,不给初始值,用的时候再赋初值,注意此时字符类型的数组必须用 strcpy 来赋值
c.id = 201501;
strcpy(c.name, "John");
strcpy(c.phone, "18601011223");
// 定义数组并进行初始化
Contact cs[4] =
{
{201501, "John", "18601011223"},
{201502, "Jennifer", "13810022334"},
{201503, "AnXi", "18600100100"},
{201504, "Unnamed", "18601011223"} // 最后一个元素/字段后面不需要加逗号
};
cs[2].id = 201503
cs[2].name = "AnXi"
cs[2].phone = "18600100100"
// 作为指针
Contact* pc = &c;
pc->id = 201501
pc->name = "John"
pc->phone = "18601011223"
// 作为函数参数类型
void test(Contact c); // 传值,使用 . 来获取结构体变量的值,缺点:使用了更多内存空间,花费较多 CPU 进行值拷贝
void test(Contact* p); // 传地址,使用 -> 来获取结构体变量的值
void test(const Contact* who); // 只是输入参数,加上 const 修饰
// 作为函数的返回值,直接返回一个 Contact 对象
Contact find(int id);
-
结构变量的定义:先声明结构类型,再定义该类型的变量;形式为:
结构类型标识符 结构变量;
-
结构变量的初始化:
结构变量 = {结构成员1初始化值, ... , 结构成员n初始化值};
- 结构数组初始化时按数组初始化原则所有元素组织在一对花括号中,元素间逗号分隔,最后一个元素/字段后面不需要加逗号
-
结构变量的引用:
结构变量名.成员名 or 指向结构的指针->成员名(最常用) or (*指向结构的指针).成员名
-
一个结构类型的变量可以作为另外一个结构类型的成员。
-
结构体的体积较大,占用的内存空间较多,往往不使用传值方式。
-
结构体作为函数的参数时,总是用**“传地址”**方式。如果只是输入参数,则加上const修饰。
3. 动态分配内存
防止因对象体积过大或者对象的个数不确定时造成的内存空间的不足或浪费。
- 使用 malloc 函数申请内存©
// molloc 函数定义原型
void* malloc(int size)
// size: 指定要申请的内存空间的大小
// 返回值: 申请而来的这块内存的首地址
// 返回值类型:MM内存管理器不关心你拿这块内存来存储何种数据,所以返回void*
// 应用程序在使用 malloc 时,要把返回值转换成目标类型。例如,要申请一块空间存放1000个Contact对象,则
int size = 1000 * sizeof(Contact);
Contact* p = (Contact*) malloc(size); // 强制将返回值类型void* 转换为 Contact*
- 使用 free 函数释放内存©
// free 函数定义原型
void free(void* ptr) // ptr: 先前malloc返回的内存地址
free(p); // 只需知道 malloc 申请的这块内存的首地址即可
- 使用 new 函数申请内存,delete 函数释放内存(C++)
// 申请一个int型内存,系统会自动分配内存大小。
// new 的返回值直接就是**对象指针**,不用像 malloc 那样还要强制转换一下类型
int* p = new int;
// new 的时候可以同时初始化这块内存中存放的值
int* p = new int(123); // 此时*p = 123;
// delete 释放内存
delete p;
---------------------------------------------------------------------------------------------
// 申请多个int型对象大小的内存,例如,申请1024个int型对象大小的内存:
int* p = new int [1024] ;
delete [] p; // 用[]指定对象个数,如果new的时候用了[],则释放的时候必须使用delete [] 操作符
---------------------------------------------------------------------------------------------
// 对于class类型,必须用new/delete来创建、销毁。malloc/free是无法胜任的(因为new/delete会自动调用构造函数/析构函数)
// 使用 new 函数动态创建一个对象:首先申请一块内存,然后在内部调用构造函数
// 使用 delete 函数释放这个对象:首先调用了析构函数,然后释放内存
// new一个对象的时候,可以传参,内部会调用构造函数
Circle* c = new Circle();
Circle* c = new Circle(1,1,4);
delete c;
// new 多个对象(数组)的时候,不能传参数,要求该类必须有**默认构造函数**
Circle* c = new Circle [4];
delete [] c;
- C 动态内存分配举例
struct Contact
{
int id;
char name[16];
}
// 用户自己决定要输入多少条记录
int n = 0;
scanf(“%d”,&n);
// 用户需要多少,就分配多少内存,避免了使用数组时内存空间的不足或浪费
int size = n * sizeof(Contact);
Contact* p = (Contact*) malloc(size);
if (p != NULL) // malloc 的返回值需要检测,因为此时MM可能没有闲置的内存可用,简化if(p)即可
{
p[0].id = 10000;
strcpy(p[0].name, "james");
:
:
p[n-1].id = 10086;
strcpy(p[n-1].name, "bobby");
}
// 释放
free(p);
- 应用程序在malloc之后,应该尽早free释放掉所申请的内存,如果不及时free 内存的话,有可能造成内存泄漏问题。
- 对象的生命期
- 全局对象:生命期是永恒的,只有程序退出时对象才失效。
- 局部对象:生命期是临时的,在超出作用域后对象立即失效。
- 动态对象:生命期是动态的,在 malloc 时生命生效,在free时失效。
4. 结构的应用——链表
-
为什么要引入链表?
- 使用数组时必须事先确定其长度,为防止出现越界的错误,通常将其长度定义得足够大,这样往往会造成内存空间的浪费。
- 使用数组在处理元素的插入、删除操作时要伴随着大量元素的移动,严重影响了处理效率。
- 为了解决数组的这些缺陷引入了链表,这是一种不需要预先分配固定长度的存储空间,而是根据程序需要动态地扩大或者缩小存储空间的数据结构。 在链表中间插入/删除一个对象,不需要数据移动,直接挂在“链条”中即可。
-
链表的概念
- 链表是一种链式存储的数据结构,其元素可以不连续存放 ,而是通过一个指针成员指示其后一个元素的存放地址。
- 链表的每个元素(称为结点),除了包含数据成员(也可称为数据域)之外,还应该至少包含一个存放下一个元素所在地址的指针成员(也可称为指针域)。
-
链表的构造与遍历
首先,用struct语法定义一个类型。下面例子中,以Student来存储一个学生的学号和姓名:
struct Student
{
int id;
char name[16];
Student* next; // 成员变量next, 用于指向下一个对象
};
注意,其中添加一个成员变量next,用于指向下一个对象。
下面构造一个链表,用于演示各个对象“串联”起来的效果。
① 先准备好4个对象
Student ss[4] =
{
{201501, "John", 0 },
{201502, "Jennifer", 0 },
{201503, "AnXi", 0 },
{201504, "Unnamed", 0 }
};
② 把这4个对象“串”起来。
ss[0].next = &ss[1];
ss[1].next = &ss[2];
ss[2].next = &ss[3];
ss[3].next = 0;
①② 两步合起来写如下所示:在初始化的时候把它们串起来
Student ss[4] =
{
{201501, "John", &ss[1] },
{201502, "Jennifer", &ss[2] },
{201503, "AnXi", &ss[3] },
{201504, "Unnamed", 0 }
};
至此,一个“链表”构造完毕!串起来之后,只需要知道“链表头”,就可以访问到链表中的每一个对象。
方法是:从头开始,依次访问,使用next指针来访问下一个对象。
Student* p = ss or &ss[0];
while(p)
{
printf("ID: %d, name: %s\n", p->id, p->name);
p = p->next; // 下一个对象
};
这一过程,称为链表的遍历。需要注意的是,链表中的最后一个对象的next为0/NULL,根据这一特征我们可以判断已经到到链表的结束。
// 查找链表中的对象
Student* find(Student* head, int id)
{
Student* p = head;
while (p)
{
if (p->id == id) // 符合条件
return p;
p = p->next;
}
return NULL; // 没有找到符合条件的对象
}
-
链表的特征
- 链表头:指链表中的第一个对象,通常,我们用链表头来代表这个链表。
- 链表尾:指链表中的最后一个对象。它的next必须设为空指针NULL/0。
-
无头链表和有头链表
- 无头链表:所有的节点都包含了有效数据(不利于表示链表中有0个对象的情况)。
- 有头链表:用一个固定的头节点来指代整个链表,所有的对象挂在这个头节点下面,而头节点本身不包含有效数据。
struct Student
{
int id;
char name[16];
Student* next;
};
// 定义了一个有头节点
Student m_head = { 0 };
// 从**链表头**插入对象
void add(Student* obj)
{
obj->next = m_head.next; // 将链表头指向的下一个对象的地址赋给待插入对象
m_head.next = obj; // 将待插入对象的地址赋给链表头
}
// 把待插入对象插入到**链表末尾**
void add(Student* obj)
{
Student* p = &m_head;
while (p->next)
p = p->next; // 找到最后一个对象
p->next = obj; // 把obj挂在最后一个对象后面
obj->next = NULL; // 现在obj作为最后一个对象
}
// 有头链表的遍历
void show_all()
{
Student* p = m_head.next;
while(p)
{
printf("ID: %d, name: %s\n", p->id, p->name);
p = p->next; // 下一个对象
}
}
// 用户输入数据
// 返回值:0表示用户输入成功;-1表示用户输入有误
int user_input(Student* obj)
{
printf("学号: ");
scanf_s("%d", &obj->id);
printf("姓名: ");
scanf_s("%s", obj->name, 16); //
需指定最多能读取多少位字符,eg: 此例最多读取16 - 1 = 15个字符
return 0;
}
int main()
{
while (1)
{
Student* obj_1 = (Student*)malloc(sizeof(Student));
if (user_input(obj_1) == 0)
{
add(obj_1);
}
else
{
free(obj_1);
}
printf("------------\n");
}
show_all();
return 0;
}
---------------------------------------------------------------------------------------
// 按ID顺序插入结点
// 方法:在插入时,遍历链表,并比较ID的值,找到目标位置。
// 注: 链表插入的核心操作,是找到目标位置,并记录前一个节点pre。新节点直接挂在pre后面就行了。
obj->next = pre->next;
pre->next = obj;
---------------------------------------------------------------------------------------
int insert(Student* obj)
{
Student* cur = &m_head.next; // 当前节点current
Student* pre = &m_head; // 上一个节点previous
while(cur)
{
if(obj->id < cur->id) // 找到这个位置
break;
pre = cur;
cur = cur->next; // 找到最后一个对象
}
// 插入到pre节点的后面
obj->next = pre->next;
pre->next = obj;
return 0;
}
---------------------------------------------------------------------------------------
// 按照ID查找和删除结点
// 方法:遍历链表,并比较ID的值,找到目标位置,记录前一个节点pre,然后执行下面代码删除即可。
pre->next = obj->next;
free(obj);
--------------------------------------------------------------------------------------
void remove(int id)
{
Student* cur = m_head.next; // 当前节点current
Student* pre = &m_head; // 上一个节点previous
while(cur)
{
if(id == cur->id) // 找到这个位置
{
// 删除该节点
pre->next = cur->next;
free(cur);
break;
}
pre = cur;
cur = cur->next; // 找到最后一个对象
}
}