十二、典型问题分析

问题1:创建异常对象时的空指针问题

创建一个空指针异常对象,意味着这会调用父类的构造函数Exception(0),然后调用init(0, NULL,0),然后调用m_message = strdup(0)

/* Duplicate S, returning an identical malloc‘d string.  */
char * __strdup (const char *s)
{
  size_t len = strlen (s) + 1;
  void *new = malloc (len);

  if (new == NULL)
    return NULL;

  return (char *) memcpy (new, s, len);
}

缺陷:没有处理参数为空指针的情况,默认为参数不能为空。

参数为空指针的情况应该合法,空指针作为字符串的一个特殊值,是有意义的,如果要复制的字符串是一个空指针,只需要返回一个空指针就可以了,

m_message = strdup(message);
// 改为
m_message = (message ? strdup(message) : NULL);
// 在外部对message为空的情况进行了处理

改进之后增强了代码的健壮性

问题2:单链表LinkList中的数据元素删除,异常安全性问题

class Test : public Object
{
    int m_id;
public:
    Test(int id = 0)
    {
        m_id = id;
    }

    ~Test()
    {
        if( m_id == 1 )
        {
            throw m_id;
        }
    }
};

int main()
{
    LinkList<Test> list;
    Test t0(0), t1(1), t2(2);

    try
    {
        list.insert(t0);
        list.insert(t1);    // t1 在析构时抛出异常
        list.insert(t2);

        list.remove(1);
    }
    catch(int e)
    {
        cout << e << endl;
        cout << list.length() << endl;
    }

    return 0;
}

析构函数中抛出是一个不推荐的操作,但是强制这样做之后,要保证单链表对象list的合法性,这叫异常安全性。list.remove(1)删除下表为1的对象的时候,即删除t1对象的时候,肯定会调用t1的析构函数,从而抛出异常,那么期望的结果就是list.length()长度变为2,因为删除了一个元素t1。但是结果是程序直接崩溃,原因是QT使用的编译器所使用的g++编译器实现细节问题,不允许在析构函数中抛出异常,这个异常无法被捕捉。

使用vs之后,发现程序有输出:1 3,之后崩溃,过程如下:

  • vs中允许析构函数抛出异常,可以捕捉,故list.remove(1)之后会产生异常并被捕捉,e的信息就是m_id值为1,故输出1
  • 然后打印list.length(),值为3,意为着单链表的状态和我们期望的不一样,这里就是隐藏的问题,remove()函数没有考虑异常安全性

查看remove()的代码:

bool remove(int i)      // O(n)
{
    bool ret = ((i>=0) && (i<m_length));
    if (ret)
    {
        Node* current = position(i);
        Node* toDel = current->next;
        current->next = toDel->next;

        destroy(toDel);
        m_length--;
    }
    return ret;
}

发现在实现这个函数的时候,是先destroy(toDel)之后,再进行长度的m_length--,这里就不够异常安全,因为在destroy之后,就进入了异常,不会进行长度运算,修改代码,交换两条代码的位置:

bool remove(int i)
{
...
        m_length--;
        destroy(toDel);
...
}

同样的,clear()函数也会有问题,在destroy之后再将m_length清0,同样的问题存在,也会导致单链表的状态混乱

void clear()        // O(n)
{
    // 释放每一个结点
    while(m_header.next)
    {
        Node* toDel = m_header.next;
        m_header.next = toDel->next;
        //delete toDel;
        destroy(toDel);
    }
    m_length = 0;
}

改进之后:

void clear()        // O(n)
{
    // 释放每一个结点
    while(m_header.next)
    {
        Node* toDel = m_header.next;
        m_header.next = toDel->next;
        // 做完指针操作之后,就意味着对应的数据元素已经从单链表中剥离出来的,长度应该--
        m_length--;

        //delete toDel;
        destroy(toDel);
    }
}

问题3:LinkList中遍历操作与删除操作的混合使用

LinkList<int> list;

for (int i = 0; i<5; i++)
{
    list.insert(i);
}

for (list.move(0); !list.end(); list.next())
{
    if (list.current() == 3)
    {
        list.remove(list.find(list.current()));
        // 删除成功后,list.current()的返回值是什么
        cout << list.current() << endl;
    }
}

for (int i = 0; i<list.length(); i++)
{
    cout << list.get(i) << endl;
}

分析:

bool remove(int i)      // O(n)
{
    // 注意i的范围
    bool ret = ((i>=0) && (i<m_length));
    if (ret)
    {
        Node* current = position(i);
        Node* toDel = current->next;
        current->next = toDel->next;

        //delete toDel;
        m_length--;
        destroy(toDel);
    }
    return ret;
}

遍历之后current()指向3,删除该元素之后,current()的指向不明,故出现了随机数,改进:再remove中对m_current进行重新定位

bool remove(int i)      // O(n)
{
    // 注意i的范围
    bool ret = ((i>=0) && (i<m_length));
    if (ret)
    {
        Node* current = position(i);
        Node* toDel = current->next;
        // 对m_current进行处理,移动到下一个位置
        if (m_current == toDel)
        {
            m_current = toDel->next;
        }
        current->next = toDel->next;
        //delete toDel;
        m_length--;
        destroy(toDel);
    }
    return ret;
}

问题4:StaticLinkList中数据元素删除时的效率问题

void destroy(Node* pn)
{
    SNode* space = reinterpret_cast<SNode*>(m_space);
    SNode* spn = dynamic_cast<SNode*>(pn);
    for(int i = 0; i < N; i++)
    {
        if (spn == space + i)
        {
            m_used[i] = 0;
            spn->~SNode();
            // 空间归还,对象析构,即可跳出循环,没必要再继续循环下去,加上break
            break;
        }
    }
}

问题5:StaticLinkList是否需要提供析构函数

一个类是否需要提供析构函数,由资源来决定,如果在类的构造函数中申请了系统资源,就需要提供析构函数,在析构函数中对应地释放系统资源。这个判断依据的前提条件是:

所实现的类是一个独立的类,没有任何继承关系

StaticLinkList()
{
    for(int i = 0; i < N; i++)
    {
        m_used[i] = 0;
    }
}
// 从资源的角度看,构造函数只是进行了成员函数的赋值操作,没有申请系统资源,那么是不是可以不提供析构函数

但是这里的StaticLinkList是有继承关系的

template <typename T>
class LinkList : public List<T>
{
...
    void clear()        // O(n)
    {
        // 释放每一个结点
        while(m_header.next)
        {
            Node* toDel = m_header.next;
            m_header.next = toDel->next;
            //delete toDel;
            destroy(toDel);
        }
        m_length = 0;
    }
...
    ~LinkList()
    {
        clear();
    }
...
};

在继承的类中有析构函数,并且在析构函数中调用了一个虚函数,但是构造函数和析构函数中是不会发生多态的,这个clear()函数就是类中实现的函数。所以对于StaticLinkList来说,父类中提供了clear()函数,但是子类中并没有提供该函数,所以不管在子类还是父类中调用这个函数,始终调用的都是LinkList中的clear();继续分析clear()函数,在里面又调用另外一个虚函数destroy(),父类LinkList中有一个destroy()函数版本,子类StaticLinkList中也有一个destroy()函数版本,这意味着:父类的析构函数被调用的时候,始终调用到的都是父类中的destroy()函数,子类中的destroy()是没有办法在析构的时候被调用到的。

int main()
{
    StaticLinkList<int, 10> list;

    for (int i = 0; i<5; i++)
    {
        list.insert(i);
    }

    for (int i = 0; i<list.length(); i++)
    {
        cout << list.get(i) << endl;
    }

    return 0;
}

list对象是一个子类StaticLinkList的对象,于是在主程序结束的时list对象就会被析构,接着就调用到父类的析构函数,从而调用父类中的clear()函数,其中的destroy()函数肯定是父类中的实现,这里就会有问题了

template <typename T>
class LinkList : public List<T>
{
protected:
    virtual void destroy(Node* pn)
        {
            delete pn;
        }
};

父类的destroy直接delete对应的内存空间,这个内存空间来自于子类creat()函数创建的空间toDel,这个空间是子类中的unsigned char m_space[sizeof(SNode) * N]中的空间,所以对于现在父类的destroy的空间就不是堆空间了,这就会造成程序的不稳定了,因为delete关键字只能释放堆空间,程序的崩溃时间无法预测。子类中所希望的destroy函数并没有被调用,这种问题在实际工程中不允许出现。

解决办法:在子类中添加自己的析构函数

~StaticLinkList()
{
    this->clear();
}

调用的还是父类中clear()函数,但是clear调用的destroy函数却是当前类中的实现,原因是:构造函数和析构函数是不会发生多态的,在构造函数或析构函数中调用的虚函数必然是当前类中实现的版本,不管是直接调用还是间接调用,都是这样。所以这里一定会调用到子类中的destroy()函数,断点调试:

发现在父类的clear()函数中调用的确实是子类的destroy()函数,符合预期。

注意:经典问题

构造函数和析构函数中是不会发生多态的,所调用的虚函数都是当前类中实现的版本,不管直接调用还是间接调用

问题6:是否有必要增加多维数组类?

没有必要

多维数组的本质:数组的数组,本质还是一维数组

二维数组类对象

int main()
{
    DynamicArray< DynamicArray<int> > d;

    d.resize(3);

    for(int i=0; i<d.length(); i++)
    {
        // d[i].resize(3);
        d[i].resize(i + 1); // 不规则二维数组
    }

    for(int i=0; i<d.length(); i++)
    {
        for(int j=0; j<d[i].length(); j++)
        {
            d[i][j] = i + j;
        }
    }

    for(int i=0; i<d.length(); i++)
    {
        for(int j=0; j<d[i].length(); j++)
        {
            cout << d[i][j] << " ";
        }

        cout << endl;
    }

    return 0;
}

原文地址:https://www.cnblogs.com/chenke1731/p/9611145.html

时间: 2024-10-10 07:31:45

十二、典型问题分析的相关文章

从零开始学安全(四十二)●利用Wireshark分析ARP协议数据包

wireshark:是一个网络封包分析软件.网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料.Wireshark使用WinPCAP作为接口,直接与网卡进行数据报文交换,是目前全世界最广泛的网络封包分析软件 什么是ARP协议    协议分析篇第一个要研究的就是ARP协议.ARP(Address Resolution Protocol,地址解析协议)用于将IP地址解析为物理地址(MAC地址).这里之所以需要使用MAC地址,是因为网络中用于连接各个设备的交换机使用了内容可寻址

手机自动化测试:appium源码分析之bootstrap十二

手机自动化测试:appium源码分析之bootstrap十二 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq:908821478. ScrollTo package io.appium.android.bootstrap.handler; import com.android.uiautomator.core.UiObject; import com.android.uiautomator.c

Linux常用命令(十二)日志文件分析

? ? ? ? ? ? ? ? ? ? ? ? Linux常用命令(十二)日志文件分析 日志文件是用于记录Linux系统中各种运行消息的文件,相当于Linux主机的"日记".不同的日志文件记载了不同类型的信息,如Linux内核消息.用户登录事件.程序错误等. 一.主要日志文件 ???? ? ? ? 在Linux系统中,日志数据主要包括以下三种类型. ■ 内核及系统日志: 这种日志数据由系统服务rslslog统一管理,根据其主配置文件/etc/rsyslog.conf中的设置决定将内核消

Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高效率编码-第三方SDK详解系列(二)--Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能 这一章很多,但是很有趣,也是这书的最后一章知识点了,我现在还在考虑要不要写这个拼图和2048的案例,在此之前,我们先来玩玩Android5.X的新特性吧!

How Javascript works (Javascript工作原理) (十二) 网络层探秘及如何提高其性能和安全性

个人总结:阅读完这篇文章需要20分钟,这篇文章主要讲解了现代浏览器在网络层传输所用到的一些技术. 这是 JavaScript 工作原理的第十二章. 正如在之前关于渲染引擎的文章中所讲的那样,我们相信好的和伟大的 JavaScript 开发者之间的差别在于后者不仅仅只是理解了语言的具体细节还了解其内部构造和运行环境. 网络简史 49 年前,ARPAnet 诞生了.它是早期的报文分组交换网络及第一个实现 TCP/IP 协议套件的网络.该网络连通了加利福亚大堂和斯坦福研究所.20 年后,Tim Ber

关注程序员健康,使用中医十二经略来解释一个神奇的梦境!

神奇的梦境 今天早上在一个QQ群看到一个朋友问有没有人能够解梦,他说: 在半山腰上突然呕吐,感觉要死了,让身边的人赶紧叫医生去. 医生很久才来,回到老家躺在床上两个人(老阿姨)分别按着我的两双手,另一个人大概是大夫(女的), 从我的脚开始按摩,脚按完要按其他部位时,我说先换身衣服,让她们先出去.换好衣服后表哥进来了, 问表哥究竟我得了什么病,表哥说应该是胃气,意思是呼吸时胃里有很多空气,多调理调理就好了, 当时我也不太信,感觉她们都在骗我,然后突然就醒了.. 群里面另外一个朋友说:青年男人梦见自

【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程

原文:[.NET Core项目实战-统一认证平台]第十二章 授权篇-深入理解JWT生成及验证流程 [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4密码授权模式,从使用场景.原理分析.自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本篇就针对第一个思考问题详细的讲解下Ids4是如何生成access_token的,如何验证access_token的有效性,最后我们使用.net webapi来实现一个外部接口(本来想用JAVA来实现的,

《构建之法》第十一、十二章学习总结

第十一章的内容是软件设计与实现. 在第一节中,讲的是关于分析和设计方法,向我们介绍在"需求分析"."设计与实现"阶段."测试""发布"阶段该搞清楚的问题. 在第二节中,讲的是关于图形建模和分析方法.在表达实体和实体之间的关系时,可以用到思维导图(Mind Map).实体关系图(ERD).UCD ;在表达数据的流动时,可以用到DFD工具:在表达控制流的时候可以用到FSM工具:前面提到的这些图形建模方法各有特点,UML却可以有一个

十二年IT职业生涯心得--致我们终将逝去的青春(连载)

章节001最近老婆带儿子回娘家住一段时间,一个人无聊的很,回到家安静的屋子让人很不适应.正好赶上世界杯,到家就把电视开开弄点动静出来,周末赛事熬了几个通宵.也趁着这"自由"的时间给自己写写总结! 话说long long ago专科毕业后雄心勃勃地去了当时业内号称的电脑公司工作,觉得凭自己的能力在电脑城能做出点名堂,而这一做就三年多.做的最多的就是接货送货(各种CRT显示器,各种主板,显卡,机箱,电源),拧螺丝,组装(给网吧装电脑的话一装就百来台,RJ45水晶头熟练到几乎闭着眼都能做),

企业搜索引擎开发之连接器connector(二十二)

下面来分析线程执行类,线程池ThreadPool类 对该类的理解需要对java的线程池比较熟悉 该类引用了一个内部类 /** * The lazily constructed LazyThreadPool instance. */ private LazyThreadPool lazyThreadPool; 该成员实现了单例模式,即该对象只有一个实例,属于懒汉式单例模式,当实例化该成员时,启用了线程同步机制 /** * Shut down the {@link ThreadPool}. Afte