深刻理解指针与链表

来源:互联网 发布:telnet 1521端口不通 编辑:程序博客网 时间:2024/06/05 15:01

本文关键字:内容地址

指针概念是构成C/C++的重要元素之一,在直入主题前,我们来了解一下什么是指针。

指针是一个变量的地址,通过这个地址,可以访问变量的内存单元,指针只是一个概念,而指针变量是对指针的具体实现。指针是特定的一个地址,是不变的,而指针变量可以存放不同的地址,是可变的。类比整型变量,整型变量里存放的是整数,指针变量里存放的是地址也就是指针

通常,我们也可以把指针变量说成指针,严格意义上当然不能这样讲。

我们举一个函数调用的例子:

void change(int x)

{

x=1;

}

int main()

{int a=0;

change(a);

}

此时的a在经过函数调用后的值是多少?当然还是0,因为函数入口处的形参x只是接收传进来的变量a的值0,然后形参x的值0被改变为1,改变的只是形参的值,原来的a的值当然是不变的。这里我们可以得出形参的一个重要性质:形参只是被动地接收传进来的值

既然形参只是被动地接收传进来的值,为什么传进指针能改变实参的值呢?

这还得提到指针的定义,指针的定义是类型说明符*变量名,比如说你想要定义一个int类型的指针a,那么你应该这样写:int *a。其中类型说明符int表示指针a所指向的变量的数据类型是整数类型,*其实也是一个说明符,它说明了变量a是一个指针变量。这里尤其要关注的一点是*只有在定义时作为一个指针说明符,如果不用在定义中,如a是一个指针变量,那么*a就表示取指针变量a所指向的内容,清楚这点非常重要!!!

比如int *a,b=1; a=&b;定义了一个指针a,整型变量b,且b初始化为1,用&符号取b的地址给a,于是变量a就存放了b的地址,而语句*a表示取指针变量a存放的地址所对应的内容也就是1

在进一步理解指针前,先来理解下地址:

定义一个整型变量bb内存分配给它的一块地址比如001b系统随机分配给它的一个值,这个值是一直在变动的,而地址是固定的,可尝试如下代码:

#include<stdio.h>

int main()

{

int b;

printf("%d\n%d",b,&b);

}

也就是说,在内存中,你可以看到地址为001处存放的内容是一个不确定的值,直到你赋值它一个值比如1b的值就确定了,那么变量b的地址是001,变量b存放的内容是1

那么指针变量呢?

我们作如下定义:

int *a;

则内存分配给指针变量a一个地址比如002,变量a存放的内容也是一个不确定的值,但是这个不确定的值不可以随意赋值,指针变量a只存放某一个变量的地址,所以只能赋值变量a地址,那么现在假设a=&b,则a的地址是002a存放的内容是001,再通过语句*a就可以取到a存放的地址001所对应的内容1了。

在函数调用中,如果传进一个实参的指针,那么取该指针所对应的内容,再赋值,就可以改变实参,如之前的函数调用的例子,我们作如下修改:

void change(int *x)

{

*x=1;

}

int main()

{int a=0;

change(&a);

}

注意:函数入口处的*是和定义中的*一样,是指针说明符!

在这个函数调用时,形参是一个整数类型的指针变量,它接收指针a传进来的内容,指针a的内容是什么,当然是地址,我们假设为001,则指针x的内容就变为001了,在函数体中,我们用*x=1,表示取指针x所指向的内容也就是指针x存放的地址所对应的内容。指针x存放的地址是001,所对应的内容是0,然后这个内容01赋值,于是地址001所对应的内容变成1了,a的地址是001,当然对应的内容由于函数调用中的改变变成1了。

好,有了上述知识储备,现在开始真正进入主题

我们都知道结构体是一个数据类型,通过定义一个结构体变量可以访问它里面的成员,但在函数体中,若想要改变结构体里的成员,必须有指向这个结构体的指针,同样,想改变指向结构体的指针,必须有指向它的指针即指针的指针。

我们先定义一个单链表中的结点:

typedef struct node{

int data;

struct node *next;

}snode,*linklist;

这里的操作是定义了一个名为node的结构体,用typedef来把node的名字起了一个别名snode。不过这里要说明一点,typedef定义的结构体别名是在第二个括号之后的,结构体的原名如果你要写的话是写在struct和第一个括号之间的,如果略去结构体中的内容,就可以这样表示:

typedef struct node{}snode,node这个结构体类型名起一个别名snode,当然结构体的原名可以不写,系统会给你一个乱的名字,你可以用别名来操作。但是在这里,这个名称是必写的,因为我们要定义指针域,也就是在结构体中定义指向此结构体的指针,定义时就必须用到之前你已经写好的结构体原名,这里是node

typedef struct node{}*linklist,node*这个结构体指针类型名起一个别名linklist,以后linklist这个名字就是node*,它能定义指向结构体的指针。而linklist*node**就是指向结构体指针的指针。

我们先说明指向结构体的指针有关内容:

结点{数据data;指针域*next}这是一个结点,它的地址假设为001

则用linklist s,我们就定义了指向这个结点的指针ss的内容也就是存放的地址是001

它本身的地址是002,在函数调用中如果传进这样的s就可以用->符号去访问或者改变这个指针变量所指向的结点中的内容,如s->data=1;就将s所指向的结点中的数据改为1了。

那么如何用无返回值类型的函数来初始化链表呢?先看如下代码:

typedef struct node{

int data;

struct node *next;

}snode,*linklist;

void create(linklist *x)

{

*x=(linklist)malloc(sizeof(node));

(*x)->next=NULL;

}

int main()

{

linklist s;

create(&s);

printf("%d",s->next);

}

我们在主函数中定义了一个指向结构体的指针变量s,在函数中传进s的地址,形参x是一个指向结构体的指针变量的指针,它被动地接收了指针s的地址,现在我们假设结点的地址是001,指针s本身的地址是002,则指针s存放了结点的地址,也就是001,形参x接收了s的地址002,所以x(本身地址未知,存放002)指向s,关系可表示为

x(存放002->002(对应s,它的内容是001)

s(本身地址是002,存放001)->001(对应结点,内容自定义)

在函数体中通过*我们可以得到x所指向的内容也就是x存放的地址002对应的内容001

这个001被分配内存语句赋值。这里说明一下动态内存分配:

由于数组长度必须事先固定,如果要进行频繁地插入删除,不仅降低了时间与空间效率,还容易导致溢出,而链式结构中不需要事先定好长度,要想插入,申请一个空间,删除则可以释放一个空间,相对数组更加灵活高效。语句(linklist)malloc(sizeof(node))是向内存中申请一个空间,空间大小为结构体node的大小,而这个申请到的空间类型是不定的,必须用(linklist)来强制转换成指向结构体的指针类型,分配好后,我们就得到了和定义时模板一样的结构体空间和指向它的指针,然后赋值给*x,所以001被改变为新申请到的指针,我们暂且称为new,这个new就是所谓的指向头结点的指针,再将头结点的指针域赋值空指针NULL

说到空指针,这里有一个有趣的地方,空指针的值是0,它也是一个地址,也就是随便给一个指针变量a赋值,a=0是允许的,但a赋值为其他整数是不允许的,哪怕你知道地址比如是001,你赋值给a一个001,也会出现错误,原因是你赋值的类型还是整数,不是地址,而空指针就是这么任性。。

函数调用好后,s的内容就被改变为new了,s存放着你新开辟的结点的地址。所以用s来访问头结点的指针域就得到0

总结:

在函数调用中,想改变实参的内容,必传进实参的指针,这个形参得到了实参的地址,通过对形参使用*我们就通过这个地址得到了对应的内容然后作出修改,于是实参的内容改了。如果传进的只是实参的内容,那么形参接收实参的内容实质上就是复制了实参的内容,修改时改的还是形参的内容,无论这个内容是指针还是指针的指针。

                                                                

                                                          20171013