结构体&指针&链表

一.结构体

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]

在计算机内部:

image-20221217165331302

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.进阶链表操作及问题

暂略,太难了——大厂面试考题(●'◡'●)

posted @ 2022-12-17 17:20  jasony_sam  阅读(304)  评论(0编辑  收藏  举报