结构体&指针&链表
一.结构体
0.前言
我们所学过的类型如:char
,int
,float
,double
等,都只能描述单一变量。但是结构体,顾名思义,是多个变量的集合,其中包含多个单一变量。所以C语言就发明了结构体用于用来描述复杂对象,如:书,人等具有多个特征的变量。
1.定义
结构(体)是一些值的集合,这些值被称为成员变量。结构的每个成员变量可以具有不同类型。
1.1 语法结构
struct node//结构体类型名,你不止可以有node,还可以有tree等等不同的结构体
{
int a;
char b=1; //成员列表,可以直接初始化
double c;
}a;//变量列表(全局变量)
注:1.因为结构体的声明是在主函数外,所以直接在结构体后面定义的变量是一个全局变量,想要局部变量就要在主函数里再创建2.不要把定义变量和声明结构体搞混了,上面a是变量,node是结构体名字
1.2 结构体的创建
结构体用于描述复杂对象的多个属性,必然具有多个成员变量。结构体成员的类型可以是常量,数组,指针,也可以是其他结构体。
struct book//书
{
char name[20];//书名
char author[15];//作者
float price;//价格
}b1,b2;//全局变量
struct point//坐标
{
int x;
int y;
};
int main()
{
struct book b;//局部变量
return 0;
}
其实在主函数内部声明也可以,但是习惯上来讲在全局声明结构体比较多,而且一些函数也可能用到结构体。
int main() {
struct book {
char name[20];
char author[15];
float price;
}b1, b2;
struct book b;
return 0;
}
其实还有一种方法是对typedef
对类型重定义,如:
typedef struct human
{
char name[20];
int age;
char id[20];
}hu;//这里的hu就不再是定义变量
//因为typedef就是=的意思,所以hu=struct human
struct human
{
char name[20];
int age;
char id[20];
};
typedef struct human hu;
int main() {
//1.
struct human man;
//2.
hu man;//这两种等价
return 0;
}
2.结构体变量的定义和初始化
可以直接在结构体声明后面定义和初始化。
struct book
{
char name[20];
char author[15];
float price;
}b1, b2;
struct book b3 = { 0 };
struct point {
int x;
int y;
};
struct point p1 = { x, y };
也可以在main
函数中定义和初始化。
typedef struct human{
char name[20];
int age;
char id[20];
}hu;
int main() {
struct human man1 = { "sam",18,"8208220628" };
point p1 = { 1,2 };
return 0;
}
如果有需要,甚至可以嵌套定义和初始化。
struct S {
int a;
char c;
double d;
};
struct T {
struct S s;//定义一个S的s套进了T
char name[20];
int num;
};
int main() {
struct T t = { {10, 'x', 1.00}, "yourfriendyo", 21 };//结构体里的结构体初始化也要加{}
return 0;
}
3.结构体成员的访问
3.1 .
操作符
结构体成员是通过操作符.
进行访问的,.
操作符具有两个操作数。左边是结构体变量名,右边是结构体成员名。
//直接用上面的结构体
printf("%d\n",t.num);
printf("%d\n",t.s.a);//嵌套的访问也是一样的,s是t的成员变量,a是s的成员变量
3.2 ->
&*
操作符
当然有的时候有可能我们需要的是一个指向该结构体的指针。这时候我们就需要操作符->
,同样也是两个操作数,如:
//1.
struct T *pt=&t;
printf("%d %c %lf %s %d\n", (*pt).s.a, (*pt).s.c, (*pt).s.d, (*pt).name, (*pt).num);
//2.通常采用这个写法,这样更加直观
printf("%d %c %lf %s %d\n", pt->s.a, pt->s.c, pt->s.d, pt->name, pt->num);
//3.
struct S* ps = &(t.s);
printf("%d %c %lf %s %d\n", ps->a, ps->c, ps->d, pt->name, pt->num);c
1.第一种方法是我们不知道->
,直接引用。
2.第二种方法->
是专门在访问结构体时使用指针的方法。
3.第三种方法更加简单粗暴,我们越过了结构体变量t
,直接创建指针指向(t.s)
.
二.指针
0.为什么要用指针
可能很多人觉得“啊,这东西长得又丑又难用,我为什么要学它” 确实 不,我们应该知道指针的好处:
1.C语言中有一些复杂的数据结构往往需要用指针来构建,如链表和二叉树
2.C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。(人话就是函数里的改变是函数里的变量,对主函数不起作用,用指针就会对主函数起作用)
3.指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效
1.什么是指针
1.1 概念
简单的来说,指针就是地址。我们口头上说的指针其实指的是指针变量。指针变量就是一个存放地址的变量。
1.2 指针的大小
指针在32位机器下是4个字节,在64位机器下是8个字节。(有兴趣可以自己拿sizeof函数测一下)
注:(指针的大小与类型无关)
2.如何声明一个指针
2.1 声明并初始化一个指针
声明
int *p; // 声明一个 int 类型的指针 p
char *p // 声明一个 char 类型的指针 p
int *arr[10] // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向 int 类型对象的指针
int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组
指针的声明比普通变量的声明多了一个一元运算符 *
。运算符 *
是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针所指向的对象。在上述的声明中: p 是一个指针,保存着一个地址,该地址指向内存中的一个变量; *p 则会访问这个地址所指向的变量。
int *p,a;
i=3;//直接访问:按变量地址存取变量值
*p=20;//间接访问:通过存放变量地址的变量去访问变量
//效果一样
初始化
声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:或是使他指向现有的内存,或者给他动态分配内存,否则我们并不知道指针指向哪儿,这将是一个很严重的问题,稍后会讨论这个问题。初始化操作如下:
//1:使指针指向现有的内存
int x=1;
int* p=&x;// 指针p被初始化,指向变量x,其中取地址符&用于产生操作数内存地址
//&x的运算结果是一个指针,p的类型是x的类型加个*,p所指向的类型是a的类型,p所指向的地址嘛,那就是x的地址
//2:动态分配内存给指针
int *p;
p = (int *)malloc(sizeof(int)*10);// malloc 函数用于动态分配内存
free(p);
// free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用,要使用这两个函数需要头文件 stdlib.h
指针的初始化实际上就是给指针一个合法的地址,让程序能够清楚地知道指针指向哪儿。
注:1.指针变量数据类型必须与所赋值的变量类型一致 float y; int *p; p=&y;
2.不允许把一个数(常量)赋予指针变量 int *p; p=1000;
3.被赋值的指针变量前不能再加*
说明符,如写为*p=&a 也是错误的。但对于在定义指针的同时赋值是允许的,如:int *p=&a;其实质可分解为两句,即:int *p;p=&a;
2.2 非法情况
1.指针未被初始化
如果一个指针没有被初始化,那么程序就不知道它指向哪里。它可能指向一个非法地址,也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改,而你并无意去修改它。
2.指针越界访问
int a[5];
int *p=a[0];
p+=5;//此时p就越界了
3.指针指向的空间释放
int* test( )
{
int a=5;
return &a;
}
int main()
{
int* p=test();
*p=10;
return 0;
}//变量a的地址只在test()函数内有效,当把a的地址传给指针p时,因为出了test函数,变量a的空间地址释放,导致p非法
2.3 如何规避非法
对于初学者,和学有所成者,非法情况都是必不可少的。解决方法如下:
1.小心越界 2.避免返回局部变量的地址 3.及时把指针赋成空指针(NULL 指针)
注:NULL 指针是一个特殊的指针变量,表示不指向任何东西。
int *p = NULL;
指针的地址为空值,也可以理解为0。内存地址 0 有一个特别重要的意义,它表明该指针不指向一个可访问的内存位置。
3.指针的运算
C指针的算术运算只限于两种形式:
3.1 指针 +/- 整数
可以对指针变量 p 进行 p++、p--、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数。这为我们提供了另外一种可以取代下标法的方法来处理数组。
如果指针p指向元素a[i],那么p+j指向a[i+j]
在计算机内部:
p 是一个 int 类型的指针,指向内存地址 0x10000008 处。则 p-- 将指向与 p 相邻的下一个内存地址,由于 int 型数据占 4 个字节,因此 p-- 所指的内存地址为 10000004。其余类推。
不过要注意的是,这种运算并不会改变指针变量 p 自身的地址,只是改变了它所指向的地址。
3.2 指针-指针
一个指针减去另外一个指针,其结果是两个指针所指元素之间的距离,如果p 指向a[i]且q指向a[j],那么 p - q等于 i - j。
注意:只有当两个指针都指向相同数组时,指针相减才有意义。
3.3 指针的比较(实际为3.2的推广)
指针可以用关系运算符 \((<, <= , >, >=)\) 和 等号运算符 (== and !=)进行比较.并且仅当两个相互比较的指针指向同一数组元素,比较才有意义。
3.4 *
运算符和 ++
的组合
p=&a[0];
a[i++]=j;
*p++=j;//两个等价,有时能节省时间
--
同理
指针运算是数组与指针关联的一种方式,其余方式将在4.2中讲述。
4.其他指针
4.1 二维指针及多维指针
略,作者偷懒中
4.2 指针与数组
4.3 指针与函数
4.4 结构体指针
赋值就是把结构变量首地址赋予该指针变量,不能把结构名赋予该指针变量。
pt=&t;//是正确的
pt=&T;//是错误的
结构名只能表示一个结构形式,编译系统并不对他们分配内存空间,没有地址。只有定义了这种类型的结构的变量时,才对该变量存储内存空间。
4.5 字符指针
5.例题
例一:找出数组中的最大元素和最小元素
Enter 10 numbers: 34 82 49 102 7 94 23 11 50 31
Largest: 102 Smallest: 7
void max_min(int a[], int n, int *max, int *min)
{
int i;
*max = *min = a[0];
for (i = 1; i < n; i++)
{
if (a[i] > *max)
*max = a[i];
else if (a[i] < *min)
*min = a[i];
}
}
例二:swap函数
void swap(int *a,int *b){
int tmp=*a;
*a=*b;
*b=*a;
}
三.链表
1.链表是什么
1.1 定义
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
在c语言中,链表是一种常见的基础数据结构,可以细分为一小块一小块的结构体变量(节点),这一小块一小块的结构体变量在链表中是首尾相连的。而这每一个节点又可以分成两个部分, 其中一个部分就是涵盖着该结构体变量里的所有信息,另一个部分就是链接每块结构体变量的部分——指针。
1.2 链表的特点
用时申请,不用时释放,插入和删除只需少量操作,能大大提高空间利用率和时间效率。
在链表中,有一个头指针变量,只保存一个地址,头指针指向一个变量,称为节点。在链表中,每一个节点包含两部分:数据部分和指针部分。数据部分用来存放元素所包含的数据,指针部分用来指向下一个节点。最后一个节点的指针指向NULL,表示指向的地址为空(链尾)。
2.链表的相关操作
作为有强大功能的链表,对他的操作当然有许多,比如:链表的创建,修改,删除,插入,输出,排序,反序,清空链表的元素,求链表的长度等等。
因为咱们初讲链表,就以最基本的单链表为例。(接下来的链表均指单链表)
创建链表节点结构体
typedef struct Node
{
int data;
struct Node *next;
}Link;//Link就代指这是链表的结构体
一般创建链表我们都用 typedef struct
,因为这样定义结构体变量时,我们就可以直接可以用 Link;
初始化链表
//尾插法
Link *creatlist(int n)//n为节点个数
{
Link *head,*node,*tail;//定义头节点,普通节点,尾部节点
head=NULL;
for(int i=1;i<=n;i++)
{
node=(Link *)malloc(sizeof(Link));//动态分配内存
scanf("%d",&node->data);
if(head==NULL) head=node;
else tail->next=node;
tail=node;
}
tail->next=NULL;
return head;
}
//头插法
Link *creatlist(int n)
{
Link *head,*node,*tail;//一样定义头节点,普通节点,尾部节点
tail=NULL;
for(int i=1;i<=n;i++)
{
node=(Link *)malloc(sizeof(Link));
scanf("%d",&node->data);
if(tail==NULL) tail=node;
else node->next=head;
head=node;
}
tail->next=NULL;
return head;
}
按值查找
Link *search_data(Link *head,int point)
{
Link *node=head->next;
while(node!=NULL&&node->data!=point)
{
node=node->next;
}
return node;
}
按位置查找
LNode *search_pos(Link *head,int pos)
{
if(pos==0)
return head;
Link *node=head;
int j=0;
while(node!=NULL&&j<pos)
{
node=node->next;
j++;
}
return node;
}
修改链表节点值
就是按位置查找后修改即可,不再展示代码
删除链表节点
void delete(Link *head,int pos)
{
Link *node,*del_node;//del_node即为我们要删除的节点
node=search_pos(head,pos-1);//找到待删除结点的前驱结点
del_node=node->next;
node->next=del_node->next;//前驱结点指向待删除结点的后继结点
free(del_node);//释放待删除结点所占的内存空间(至此,在逻辑与物理上都删除了该节点)
}
//一定要记得free掉待删除结点,不free的话,好的情况只是占用了内存,坏情况成为非法指针造成内存泄漏
插入链表节点
void insert(Link *head,int pos,int point){
Link *node;
Link *new_node=(Link *)malloc(sizeof(Link));
new_node->data=point;
node=search_pos(head,pos-1);
new_node->next=node->next;
node->next=new->node;//不能写反,不然逻辑混乱,导致new_node自己指向自己
}
输出链表
void printlist(Link *head){
Link *node=head->next;
while(node!=NULL)
{
printf("%d ",node->data);
node=node->next;
}
printf("\n");
}
3.进阶链表
3.1 循环链表
3.2 双向链表
4.进阶链表操作及问题
暂略,太难了——大厂面试考题(●'◡'●)