数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。

10月24日,这日子真好,程序员节....我一个问题困扰了我半天...就是在codeblock(一个IDE)的纯C语言环境下实现链表结构,我最后想打印输出链表中数据的时候总是出现问题(其实是我一直在疑惑:传进去的L_Header本身不就是一个指针吗?为什么要担心没有修改链表的问题?是因为值传递未能初始化L_Header的指向从而产生野指针的原因。)一种用纯C的解决方案是用“指向指针的指针”作传入参数,另一种是用C++中的“&引用”作传入参数下面我首先来论述纯C语言实现的细节:

给出typedef的节点和链表数据类型的定义:

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第1张图片

 此时插入函数有问题的地方在于传入参数LinkList L是通过值传递传入的,根本没有改变实际的L

错误:

 如果在List_HeadInsert(LinkList L)形参不加*则在main中无法打印:

    //测试代码
//    if(L_Header->next==NULL)printf("空!");
//    else
//      printf("%d\n",L_Header->next->data);

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第2张图片

 数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第3张图片

现象:运行慢,无输出

根本原因在于,局部函数中虽然也对L的指向进行了malloc的初始化,但是L_Header这个地址是值传递进来的,也就是拷贝的存放在内存中的L_Header地址值的蓝色圈出来的这一份数据,对原来放在内存中的L_Header的值没有影响,即相当于没有给L_Header初始化!L_Header还是一个野指针!所以会出现运行慢,无输出的情况。

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第4张图片

 数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第5张图片

这是定义的一个结构体指针类型变量L_Header没有问题:

关键在于你要在函数那里要注意,在纯C语言的环境之下你应该在功能函数List_HeadInsert()的形参和main中传入的实参写成"指向指针的指针变量":

正确:

LinkList List_HeadInsert(LinkList *L)//也叫逆向建立单链表
{//在纯C中请写成指向
    LNode *s;//指向新节点的指针
    int x;
    (*L)=(LinkList)malloc(sizeof(LNode));//创建的是“头指针”,不管有没有头节点,头指针都指向链表中的第一个元素
                                      //这个头指针是在main中声明的,在这里定义的。
    (*L)->next=NULL;//初始化链表为空链表
    printf("请输入进行头插法建立单链表的元素x:");
    scanf("%d",&x);
    while(x!=9999)//输入9999表示结束
    {
        s=(LNode*)malloc(sizeof(LNode));//创建新节点
        s->data=x;
        s->next=(*L)->next;//把头节点的next部分给到新的头节点s的next部分
        (*L)->next=s;//头节点的next部分指向

         printf("%d\n",(*L)->next->data);

        printf("请输入下一个插入的元素x:");
        scanf("%d",&x);
    }

 加上*后正常打印输出:

    //测试代码
//    if(L_Header->next==NULL)printf("空!");
//    else
//      printf("%d\n",L_Header->next->data);

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第6张图片

原理的简单理解如下:

值传递:将一份数据复制一份传入功能函数,复制的那一份怎么改变都不影响原来的那一份

地址传递:将一份数据,我先在main函数中调用的地方输入参数的地方用“取地址符号&”取出那个数据的地址,意思是我对这个地址的单元内的数据到功能函数的地方再用“解引用*”对这个数据作出的仍和改变都会印象数据本身的值

在操作上就是,如果我想要改变某个玩意儿的值,你首先要确定的就是你需要修改的元数据是什么,确定这个需要修改元数据之后,我说你别管它是什么“结构体指针类型”还是什么乱七八糟的指针类型,你在main函数调用功能函数的实参中就对它做一个取地址,而后紧接着在功能调用的形参列表中对它进行一个解引用即可。这是一定的。而后你在功能函数中再考虑你到底要修改的是什么,比如在main中List_HeadInsert(&L_Header);到功能函数中

LinkList List_HeadInsert(LinkList *L)到功能函数的具体实现中我要十分明确我需要修改的是(*L)//本来传进来是L,它代表的是放“头指针地址数据”的地址,而我要的是链表的头指针的地址,即要对L解引用即可得到头指针的地址,用->即可访问指针指向的结构体中的成员,也就是说想要修改L_Header表头指针的next部分就必须写为这样:(*L)->next;想修改L_Header的地址(指向头节点的指针,就是头节点的地址)就必须写成这样(*L)才能得到头节点在内存中地址。

我们来有注释的,有重点标注的来研究一下main中功能调用和功能函数实现的对比,这里我为了重点突出就不直接用代码段了,我先给出图片,然后在注释中讲解:

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第7张图片

int main()
{
    LinkList L_Header;

    List_HeadInsert(&L_Header);

    return 0;

}

LinkList List_HeadInsert(LinkList *L)
{
    LNode *s;//指向新节点的指针
    int x;
    (*L)=(LinkList)malloc(sizeof(LNode));//创建的是“头指针”,不管有没有头节点,头指针都指向链表中的第一个元素
                                      //这个头指针是在main中声明的,在这里定义的。
    (*L)->next=NULL;//初始化链表为空链表
    printf("请输入进行头插法建立单链表的元素x:");
    scanf("%d",&x);
    while(x!=9999)//输入9999表示结束
    {
        s=(LNode*)malloc(sizeof(LNode));//创建新节点
        s->data=x;
        s->next=(*L)->next;//把头节点的next部分给到新的头节点s的next部分
        (*L)->next=s;//头节点的next部分指向

         printf("%d\n",(*L)->next->data);

        printf("请输入下一个插入的元素x:");
        scanf("%d",&x);
    }
    return *L;
}

正确:

下面给出纯C语言实现的头文件typedef_linklist_in_c.h:

#ifndef TYPEDEF_LINKLIST_H_INCLUDED
#define TYPEDEF_LINKLIST_H_INCLUDED
#include
#include 
#include 


//线性表链式实现的定义
typedef struct LNode//带标记LNode的结构体定义,意思是可以在定义新结构体的时候用struct LNode newlnode;定义一个新的节点
{
    int data;//节点的data部分
    struct LNode *next;//指向一个链表节点结构的节点的指针,是该节点的next节点

}LNode,*LinkList;//两个数据类型的侧重点不一样,第一个侧重与节点,第二个侧重指向链表的指针。
//struct LNode newlnode和LNode newlnode;功能相同

/*不需要,这个作为在头插法建立中的片段
//初始化不带头节点的链表
bool InitList_without_header_ptr(LinkList *L)
{
    (*L)=NULL;
    return true;
}
*/

//初始化带头结点的链表
bool InitList_involve_header_ptr(LinkList *L)
{
    (*L)=(LNode *)malloc(sizeof(LNode));
    if((*L)==NULL)return false;
    (*L)->next=NULL;//L->next就是头指针
                 //L的data部分不存储数据
    return true;
}

//判断带头结点的链表是否为空
bool is_Empty(LinkList L)
{
        if(L->next==NULL)return true;
        else return false;
}

#endif // TYPEDEF_LINKLIST_H_INCLUDED

下面给出纯C语言实现的头文件function_of_linklist_in_c.h:

#ifndef FUNCTION_OF_LINKLIST_H_INCLUDED
#define FUNCTION_OF_LINKLIST_H_INCLUDED
//头插法建立单链表(带头节点)
//每次将s所指的节点插在最前端,头节点的下一个节点居然是最后插入的一个元素
LinkList List_HeadInsert(LinkList *L)//也叫逆向建立单链表
{
    LNode *s;//指向新节点的指针
    int x;
    (*L)=(LinkList)malloc(sizeof(LNode));//创建的是“头指针”,不管有没有头节点,头指针都指向链表中的第一个元素
                                      //这个头指针是在main中声明的,在这里定义的。
    (*L)->next=NULL;//初始化链表为空链表
    printf("请输入进行头插法建立单链表的元素x:");
    scanf("%d",&x);
    while(x!=9999)//输入9999表示结束
    {
        s=(LNode*)malloc(sizeof(LNode));//创建新节点
        s->data=x;
        s->next=(*L)->next;//把头节点的next部分给到新的头节点s的next部分
        (*L)->next=s;//头节点的next部分指向

         printf("%d\n",(*L)->next->data);

        printf("请输入下一个插入的元素x:");
        scanf("%d",&x);
    }
    return *L;
}

//尾插法建立单链表(解决头插法结点次序和输入的顺序不一致的问题)
//每次将s所指的节点插在最末端,保证从头到尾的顺序
LinkList List_TailInsert(LinkList *L)//也叫正向建立单链表
{
    int x;
    (*L)=(LinkList)malloc(sizeof(LNode));
    LNode *s,*tail=(*L);//是不能比如int s=a=b=c=3;用逗号分隔定义时初始化还是可以的
                     //tail指向表的最后一个节点
    printf("请输入进行尾插法建立单链表的元素x:");
    scanf("%d",&x);

    while(x!=9999)
    {
        s=(LNode*)malloc(sizeof(LNode));
        s->data=x;//先初始化要插入的s元素
        if((*L)->next==NULL) (*L)->next=s;

        /*
        能不能s->next=NULL;之后先让tail=s;?
        当然可以,只不过s->next=NULL;可以省略不写
        因为你反正要改变tail的指向,最终我们关心的是表尾元素tail需要指向NULL
        原来s->next指向哪其实我并不关心。
        */
        tail->next=s;//链表中原来最后一个节点的next部分指向s所指的节点
        tail=s;//是改变tail的指向为s所指的地址,而不是改变原来tail中的元素为s,不用担心覆盖的问题

        printf("请输入下一个插入的元素x:");
        scanf("%d",&x);
        //printf("%d",tail->data);//测试代码
        printf("\n");
    }
    tail->next=NULL;//将表尾节点最终设置指向NULL
                    //其实是将s->next=NULL;而后改变原来表尾tail->next=s;而后改变表尾指针的指向新的表尾节点tail=s;
                    //其实我不关心原来s->next,故s->next=NULL;可以省略不写,只需在最后将tail->next=NULL;


    return *L;
}


//按序号查找节点
LNode *Getelem(LinkList L,int i)
{

    LNode *p=L->next;
    if(i==0) return L;
    if(i<1)return NULL;
    for(int j=1;jnext;
        j++;
    }
    return p;
}

//按值查找节点
LNode *locate_by_val(LinkList L,int val)
{
    LNode *p=L->next;
    while(p!=NULL&&p->data!=val) p=p->next;
    return p;
}


void seqprint_allelem_in_linklist(LinkList L)
{
     LNode *p=L->next;

    while(p!=NULL)
    {
        printf("%d",p->data);
        p=p->next;
    }

}//顺序输出头插法建立的链表的所有元素


#endif // FUNCTION_OF_LINKLIST_H_INCLUDED

下面是纯C实现的main函数:

#include
#include 
#include 
#include "../typedef_linklist_in_c.h"
#include "../function_of_linklist_in_c.h"

int main()
{
    LinkList L_Header;
    //注意,L_Header是一个struct LNode类型的指针
    //LinkList在typedef对它定义的时候已经给它附加了这个数据类型是指针数据类型的说明!


//    LinkList L_Tail;
//    InitList_involve_header_ptr(&L_Tail);

    List_HeadInsert(&L_Header);
//    List_TailInsert(&L_Tail);

    //打印头插法的链表
    seqprint_allelem_in_linklist(L_Header);
    
    //测试代码
//    if(L_Header->next==NULL)printf("空!");
//    else
//      printf("%d\n",L_Header->next->data);

    return 0;

}

纯C运行结果:

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第8张图片

下面我就来介绍一下,从不同的IDE试验中发现这个问题的过程,首先我先来提一下关于VC中使用scanf函数报错“unsafed”建议使用“scanf_s"的问题的解决方案,进过这样设置之后我们的代码中的scanf就不必写成scanf_s,也就更具有移植性,而后再给大家讲解一下C++中的”&引用“(这里有C++语法基础的同学或者看完我的超链接之后不吃力的同学可以继续往下看。),即你在功能函数形参中使用“&引用”可以达到一种效果,就是说你不用在原本纯C中该写(*L)的地方考虑用指针,你在这个地方完全可以放行的用L,简化指针。原理请查看超链接。实际上“&引用”,就是提供了一种对指针不那么拿捏的时候还想修改传入数据本身的一种解决方案(我为什么会想到在VC中查看是什么原因...就是因为数据结构书上采用的是C++语法和C语法的联合表达形式...不用完全的C++语法的cin>>和cout<<输入输出,还用的printf和scanf,却用到C++中的应用对参数进行传递,这是让我很恼火的地方)下面我来给出我在VC中对功能函数形参不加任何指针时后的输出:

错误:

此时编译器没有报出任何错误,编译也是通过的,但我告诉你,L_Header传到这个功能函数里并没有修改L_Header的值,原理上面已经说了。数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第9张图片

 正确:

那么我们现在调整一下程序,下面是完整的分文件编写的代码,和运行结果:

“typedef_linklist_in_cpp.h”:

#ifndef TYPEDEF_LINKLIST_H_INCLUDED
#define TYPEDEF_LINKLIST_H_INCLUDED
#include
#include 
#include 


//线性表链式实现的定义
typedef struct LNode//带标记LNode的结构体定义,意思是可以在定义新结构体的时候用struct LNode newlnode;定义一个新的节点
{
    int data;//节点的data部分
    struct LNode *next;//指向一个链表节点结构的节点的指针,是该节点的next节点

}LNode,*LinkList;//两个数据类型的侧重点不一样,第一个侧重与节点,第二个侧重指向链表的指针。
//struct LNode newlnode和LNode newlnode;功能相同

//初始化不带头节点的链表
bool InitList_without_header_ptr(LinkList &L)
{
    L=NULL;
    return true;
}

/*带头结点的不需要在main中先初始化,在头插法建立链表的时候把代码片段放入到它里面实现的
//初始化带头结点的链表
bool InitList_involve_header_ptr(LinkList &L)
{
    L=(LNode *)malloc(sizeof(LNode));
    if(L==NULL)return false;
    L->next=NULL;//L->next就是头指针
                 //L的data部分不存储数据
    return true;
}
*/

//判断带头结点的链表是否为空
bool is_Empty(LinkList L)
{
        if(L->next==NULL)return true;
        else return false;
}

#endif // TYPEDEF_LINKLIST_H_INCLUDED

"function_of_linklist_in_cpp.h":

#ifndef FUNCTION_OF_LINKLIST_H_INCLUDED
#define FUNCTION_OF_LINKLIST_H_INCLUDED
//头插法建立单链表(带头节点)
//每次将s所指的节点插在最前端,头节点的下一个节点居然是最后插入的一个元素
LinkList List_HeadInsert(LinkList &L)//也叫逆向建立单链表
{
    LNode *s;//指向新节点的指针
    int x;
    L=(LinkList)malloc(sizeof(LNode));//创建的是“头指针”,不管有没有头节点,头指针都指向链表中的第一个元素
                                      //这个头指针是在main中声明的,在这里定义的。
    L->next=NULL;//初始化链表为空链表
    printf("请输入进行头插法建立单链表的元素x:");
    scanf("%d",&x);
    while(x!=9999)//输入9999表示结束
    {
        s=(LNode*)malloc(sizeof(LNode));//创建新节点
        s->data=x;
        s->next=L->next;//把头节点的next部分给到新的头节点s的next部分
        L->next=s;//头节点的next部分指向

         printf("%d\n",L->next->data);

        printf("请输入下一个插入的元素x:");
        scanf("%d",&x);
    }
    return L;
}

//尾插法建立单链表(解决头插法结点次序和输入的顺序不一致的问题)
//每次将s所指的节点插在最末端,保证从头到尾的顺序
LinkList List_TailInsert(LinkList &L)//也叫正向建立单链表
{
    int x;
    L=(LinkList)malloc(sizeof(LNode));
    LNode *s,*tail=L;//是不能比如int s=a=b=c=3;用逗号分隔定义时初始化还是可以的
                     //tail指向表的最后一个节点
    printf("请输入进行尾插法建立单链表的元素x:");
    scanf("%d",&x);

    while(x!=9999)
    {
        s=(LNode*)malloc(sizeof(LNode));
        s->data=x;//先初始化要插入的s元素
        if(L->next==NULL) L->next=s;

        /*
        能不能s->next=NULL;之后先让tail=s;?
        当然可以,只不过s->next=NULL;可以省略不写
        因为你反正要改变tail的指向,最终我们关心的是表尾元素tail需要指向NULL
        原来s->next指向哪其实我并不关心。
        */
        tail->next=s;//链表中原来最后一个节点的next部分指向s所指的节点
        tail=s;//是改变tail的指向为s所指的地址,而不是改变原来tail中的元素为s,不用担心覆盖的问题

        printf("请输入下一个插入的元素x:");
        scanf("%d",&x);
        //printf("%d",tail->data);//测试代码
        printf("\n");
    }
    tail->next=NULL;//将表尾节点最终设置指向NULL
                    //其实是将s->next=NULL;而后改变原来表尾tail->next=s;而后改变表尾指针的指向新的表尾节点tail=s;
                    //其实我不关心原来s->next,故s->next=NULL;可以省略不写,只需在最后将tail->next=NULL;


    return L;
}


//按序号查找节点
LNode *Getelem(LinkList L,int i)
{

    LNode *p=L->next;
    if(i==0) return L;
    if(i<1)return NULL;
    for(int j=1;jnext;
        j++;
    }
    return p;
}

//按值查找节点
LNode *locate_by_val(LinkList L,int val)
{
    LNode *p=L->next;
    while(p!=NULL&&p->data!=val) p=p->next;
    return p;
}


void seqprint_allelem_in_linklist(LinkList L)
{
     LNode *p=L->next;

    while(p!=NULL)
    {
        printf("%d",p->data);
        p=p->next;
    }

}//顺序输出头插法建立的链表的所有元素


#endif // FUNCTION_OF_LINKLIST_H_INCLUDED

main文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "../../typedef_linklist_in_cpp.h"
#include "../../function_of_linklist_in_cpp.h"

int main()
{
    LinkList L_Header;//定义的是结构体指针

  
 

    //    LinkList L_Tail;
    //    InitList_involve_header_ptr(L_Tail);

    List_HeadInsert(L_Header);
    //    List_TailInsert(L_Tail);

    //打印头插法的链表
    seqprint_allelem_in_linklist(L_Header);
     
    
    return 0;
}

最终输出结果:

数据结构和算法学习笔记:关于用纯C语言实现线性表链式实现时打印链表中元素无法打印的问题的原理以及用“指向指针的指针”解决,和用C++语言“引用&”作函数参数更加简洁好理解的解决方案。_第10张图片

如有问题欢迎随时与我沟通。

你可能感兴趣的:(C++编程,算法,1024程序员节,数据结构,c++,c语言)