代码走读意见

1 简介

作为一个开发人员,觉得自己没有创造力,一旦涉及到比较弯弯绕的情况,脑子就不够用了。来海康威视就要三个月了,第一次代码走读,发现代码的问题很多,梳理成一个文档,以后再进行C/C++代码的开发,需要注意如下的问题。

2 内存泄漏问题

2.1 SAFENEW之后立即判断,并且确保内存释放

有问题的代码片段如下:

SizeType iItemCount = configNode.Size();
//new多个对象,使用SAFENEW,非常容易导致内存泄漏
pStatus = SAFENEW DWORD[iItemCount];
pStruChannelGroup = SAFENEW NET_DVR_CHANNEL_GROUP[iItemCount];
pStruDetectFaceCfg = SAFENEW NET_DVR_DETECT_FACE[iItemCount];
memset(pStatus, 0, sizeof(DWORD)*iItemCount);
memset(pStruChannelGroup, 0, sizeof(NET_DVR_CHANNEL_GROUP)*iItemCount);
memset(pStruDetectFaceCfg, 0, sizeof(NET_DVR_DETECT_FACE)*iItemCount);

if(pStatus==NULL || pStruChannelGroup==NULL || pStruDetectFaceCfg==NULL)
{
    Msg = "memory allocation failed in NET_DVR_GetDeviceConfig";
    NETSDKPLUGIN_ERROR("Memory allocate fails before NETGetDeviceConfig");
    return DEV_ERR_NOT_ENOUGH_MEMORY;
}

问题在蓝色部分,蓝色部分。第一块蓝色部分的问题在于SAFENEW了内存代码后,没有直接对生成的内存区域首地址进行判空,而是直接使用,如分配失败,使用了NULL直接。会引起程序崩溃。
第二块蓝色,就是程序的if中的表达式,如果在条件判断语句中使用了逻辑运算符,需要判断多个条件经过逻辑判断之后的结果此时应该使用括号扩起每一个条件,增强代码可读性。
第三块蓝色部分则直接造成了内存泄漏,倘若pStatus和pStruChannelGroup分配内存成功,而pStruDetectFaceCfg分配内存空间失败,而程序中打印日志之后,直接返回,并没有对new出来的地址进行delete,造成了内存泄漏。综上修改过后的代码片段如下:

DWORD *pStatus = NULL;
NET_DVR_CHANNEL_GROUP *pStruChannelGroup = NULL;
NET_DVR_DETECT_FACE *pStruDetectFaceCfg = NULL;
    DEV_ERR_RET ret = DEV_ERR_SUCCESS;
do 
    {
        if (true != configNode.IsArray())
        {
            Msg = "the configed data array is not an Array";
            NETSDKPLUGIN_ERROR("- The configed data array is not an Array");
            ret = DEV_ERR_INVALID_PARAM;
            break;
        }
        SizeType iItemCount = configNode.Size();
        if (iItemCount <= 0)
        {
            Msg = "iItemCount is zero";
            NETSDKPLUGIN_ERROR("- iItemCount is zero");
            ret =  DEV_ERR_INVALID_PARAM;
            break;
        }

        pStatus = SAFENEW DWORD[iItemCount];
        pStruChannelGroup = SAFENEW NET_DVR_CHANNEL_GROUP[iItemCount];
        pStruDetectFaceCfg = SAFENEW NET_DVR_DETECT_FACE[iItemCount];
        //SAFENEW 之后要立即判断是否为空
        if((pStatus == NULL) || (pStruChannelGroup == NULL) || (pStruDetectFaceCfg == NULL))
        {
            Msg = "memory allocation failed in NET_DVR_GetDeviceConfig";
            NETSDKPLUGIN_ERROR("Memory allocate fails before NET_DVR_GetDeviceConfig");
            ret = DEV_ERR_NOT_ENOUGH_MEMORY;
            break;
        }

        memset(pStatus, 0, sizeof(DWORD)*iItemCount);
        memset(pStruChannelGroup, 0, sizeof(NET_DVR_CHANNEL_GROUP)*iItemCount);
        memset(pStruDetectFaceCfg, 0, sizeof(NET_DVR_DETECT_FACE)*iItemCount);
}while(0);
SAFEARRAYDELETE(pStruDetectFaceCfg);
SAFEARRAYDELETE(pStruChannelGroup);
SAFEARRAYDELETE(pStatus);
NETSDKPLUGIN_TRACE("- CHikNetDevice::SetFaceDetectionConfig Ends");
return ret;

根据上面的分析,通过使用do-while(0)的结构,并且在new了内存空间之后直接进行判断,一旦判断有内存分配失败情况,直接直接break,在while(0)之后进行内存的释放。

2.2 do-while(0)

在一个函数中,如果要通过if判断写多个return语句,单位认为这类代码不佳,需要转换成do{}while(0)结构,在上述的代码片段中已经简单使用了这种方式来保证内存的释放。可以参考下面的代码片段:

if (configedNode.IsArray())
    {
        SizeType iItemCount = configedNode.Size();
        DWORD *pStatus = SAFENEW DWORD[iItemCount];
        NET_DVR_CHANNEL_GROUP *pStruChannelGroup = SAFENEW NET_DVR_CHANNEL_GROUP[iItemCount];
        NET_DVR_DETECT_FACE *pStruDetectFaceCfg = SAFENEW NET_DVR_DETECT_FACE[iItemCount];

        if(pStatus==NULL || pStruChannelGroup==NULL || pStruDetectFaceCfg==NULL)
        {
            Msg = "memory allocation failed in NET_DVR_GetDeviceConfig";
            NETSDKPLUGIN_ERROR("memory allocate fails before NET_DVR_GetDeviceConfig");
            return DEV_ERR_NOT_ENOUGH_MEMORY;
        }
        for (SizeType i=0; i<iItemCount; i++)
        {
            Value& Node = configedNode[i];
            pStruChannelGroup->dwGroup = i;
            bool ret = GetCallParam(Node, pStruChannelGroup[i]);
            if (!ret)
            {
                NETSDKPLUGIN_ERROR("GetCallParam fails");
                return DEV_ERR_INVALID_PARAM;
            }
        }
        BOOL ret = NET_DVR_GetDeviceConfig(m_UserID, NET_DVR_GET_FACE_DETECT, iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
        if (ret)
        {
            NETSDKPLUGIN_INFO("Get Device Configuration success, ret is %d", ret);
        }else
        {
            NETSDKPLUGIN_INFO("Get Device Configuration failed, errCode is %d", NET_DVR_GetLastError());
            Msg = "Get Face DetectionConfig failed";
            return DEV_ERR_FAILED;
        }


        SAFEARRAYDELETE(pStruDetectFaceCfg);
        SAFEARRAYDELETE(pStruChannelGroup);
        SAFEARRAYDELETE(pStatus);
        return DEV_ERR_SUCCESS;
    }else
    {
        Msg = "the configed data array is invalid";
        NETSDKPLUGIN_ERROR("the configed data array is invalid");
        return DEV_ERR_INVALID_PARAM;
    }

在上面的代码片段中,可以看到在一个一个函数中,出现了5个return语句,并且这样的用法还极有可能导致内存的泄漏(分析见前一小节)。因此这样的代码结构不佳,可以通过使用do-while(0)进行代码重构,如下:

   do 
    {
        SizeType iItemCount = configNode.Size();
        pStatus = SAFENEW DWORD[iItemCount];
        pStruChannelGroup = SAFENEW NET_DVR_CHANNEL_GROUP[iItemCount];
        pStruDetectFaceCfg = SAFENEW NET_DVR_DETECT_FACE[iItemCount];
        //SAFENEW 之后要立即判断是否为空
        if((pStatus == NULL) || (pStruChannelGroup == NULL) || (pStruDetectFaceCfg == NULL))
        {
            Msg = "memory allocation failed in NET_DVR_GetDeviceConfig";
            NETSDKPLUGIN_ERROR("Memory allocate fails before NET_DVR_GetDeviceConfig");
            ret = DEV_ERR_NOT_ENOUGH_MEMORY;
break;
        }

        memset(pStatus, 0, sizeof(DWORD)*iItemCount);
        memset(pStruChannelGroup, 0, sizeof(NET_DVR_CHANNEL_GROUP)*iItemCount);
        memset(pStruDetectFaceCfg, 0, sizeof(NET_DVR_DETECT_FACE)*iItemCount);

        for (SizeType i=0; i<iItemCount; i++)
        {
            Value& Node = configNode[i];
            pStruChannelGroup[i].dwGroup = i;
            bool Flag = GetCallParam(Node, pStruChannelGroup[i]);
            if (!Flag)
            {
                NETSDKPLUGIN_ERROR("- GetCallParam fails");
                ret  =  DEV_ERR_INVALID_PARAM;
                break;
            }
        }
        if (ret != DEV_ERR_SUCCESS)
        {
            break;
        }

        ret_config = NET_DVR_GetDeviceConfig(m_UserID, NET_DVR_GET_FACE_DETECT, 
            iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, 
            pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
        if (TRUE != ret_config)
        {
            errCode = NET_DVR_GetLastError();
            sprintf(szLan, "Get the FaceConfig failes, errCode is %d", errCode);
            Msg = szLan;
            NETSDKPLUGIN_ERROR("- NET_DVR_GetDeviceConfig Fails, errCode: %d", errCode);
            ret = DEV_ERR_FAILED;
            break;
        }

        for (SizeType i=0; i<iItemCount; i++)
        {
            Value& node = configNode[i];
            if (node.HasMember("enableDetectFace")==false)
            {
                NETSDKPLUGIN_ERROR("- The param transfered doesnot contain the key enableDetectFace");
                ret = DEV_ERR_INVALID_PARAM;
break;
            }

            Value& enableNode = node["enableDetectFace"];
            pStruDetectFaceCfg[i].byEnableDetectFace = enableNode.GetInt();
            //开启人脸侦测的地方,struAlarmHandleType 这个结构体的dwHandleType 需要配置为0x04 ,不然平台会收不到报警
            pStruDetectFaceCfg[i].struAlarmHandleType.dwHandleType = 0x04;
        }
        if (ret != DEV_ERR_SUCCESS)
        {
            break;
        }
        BOOL rett = NET_DVR_SetDeviceConfig(m_UserID,
            NET_DVR_SET_FACE_DETECT, iItemCount,
            pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),
            pStatus,
            pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
        if (false == rett)
        {
            errCode = NET_DVR_GetLastError();
            sprintf(szLan, "Set the FaceConfig failes, errCode is %d", errCode);
            Msg = szLan;
            NETSDKPLUGIN_ERROR("- NET_DVR_SetDeviceConfig Failes, errCode: %d", errCode);
            ret = DEV_ERR_FAILED;
break;
        }
        NETSDKPLUGIN_INFO("- NET_DVR_SetDeviceConfig Success!");
        ret = DEV_ERR_SUCCESS;
break;
    } while (0);

    SAFEARRAYDELETE(pStruDetectFaceCfg);
    SAFEARRAYDELETE(pStruChannelGroup);
    SAFEARRAYDELETE(pStatus);
    NETSDKPLUGIN_TRACE("- CHikNetDevice::SetFaceDetectionConfig Ends");
    return ret;

上述的代码片段演示了do-while(0)的使用,通过使用do-while(0),可以非常可靠的把内存清理的部分放置在while(0)之后以保证内存得到适当的释放,不会造成内存泄漏。另外,也需要注意的是在红色部分,是for循环,for循环内部虽然插入了break语句,但该break语句仅仅控制内层for循环,无法直接退出至while(0)之后,因此,应该在for循环并列之后插入判断,是否需要直接break。这种用法较为常见,也的确使用。
另外使用do-while(0)这种语法结构时,变量的定义应在do之前进行,并且要确保int类型的变量具有初值,因为string类型的变量会默认初始化为空串。而int类型的变量不初始化就是乱的。

2.3 字符串拷贝memcpy, strcpy_s

在字符串拷贝过程中,为字符数组拷贝成一个指定的string,之前我的写法是这样的:

memcpy(struLoginInfo.sDeviceAddress, m_DeviceIp.c_str(), NET_DVR_DEV_ADDRESS_MAX_LEN);
    memcpy(struLoginInfo.sUserName, m_UserName.c_str(), NET_DVR_LOGIN_USERNAME_MAX_LEN);
    memcpy(struLoginInfo.sPassword, m_Password.c_str(), 
NET_DVR_LOGIN_PASSWD_MAX_LEN);

曦哥说使用memcpy是不安全的一种做法,要使用strcpy_s。因此修改如下:

    strcpy_s(struLoginInfo.sDeviceAddress, NET_DVR_DEV_ADDRESS_MAX_LEN, m_DeviceIp.c_str());
    strcpy_s(struLoginInfo.sUserName, NET_DVR_LOGIN_USERNAME_MAX_LEN, m_UserName.c_str());
    strcpy_s(struLoginInfo.sPassword, NET_DVR_LOGIN_PASSWD_MAX_LEN, m_Password.c_str());

使用字符串拷贝,strcpy_s更加安全。另外sprintf_s也可以完成同样的工作:含义是把一个数组格式化后存入另一个数组内。

sprintf((char *)SIpConfig.stuServerIP.sIpV4, "%s", ServerIp.c_str());
SIpConfig.wServerPort = ServerPort;
sprintf((char *)SIpConfig.byUserName, "%s", UserName.c_str());
sprintf((char *)SIpConfig.byPassWord, "%s", Password.c_str());
//把设备ID和显示名称同时修改
sprintf((char *)SIpConfig.byLocalNo, "%s", localNo.c_str());
sprintf((char *)SIpConfig.byDispalyName, "%s", localNo.c_str());

3 使用现有公共库问题

3.1 字符串转换为宽字符串

之前实现了一个字符串转宽字符串的函数,是问题代码:

//string to wchar*
static WCHAR* str_to_wcharstr(std::string mystring)
{

    int mystringSize = (int)(mystring.length()+1);
    wchar_t* mywstring = new wchar_t[mystringSize];
    MultiByteToWideChar( CP_ACP, 0, mystring.c_str(), -1, mywstring, mystringSize );
    return mywstring;
}

问题1:字符串做参数时,或者字符串做返回值时应该使用引用。
问题2:在函数中使用new动态分配的空间没有释放,很可能会造成内存泄漏。
解决方式:可以使用公共库的字符串转宽字符串wstring的格式来避免自定义的问题函数。
引用是一种复合类型,通过在变量名前添加&符号来定义。复合类型是指用其他类型定义的类型。在引用的情况下,每一种引用被关联到某一其他类型不能定义引用类型的引用。引用必须与与该引用同类型的对象[而非常量]初始化

3.2 utf-8转换GBK

在处理中文时需要进行socket数据报的转换,默认Java层发送给插件使用的编码是UTF-8,因此在转换过程中,需要把该UTF-8转换成支持中文的GB-2312或者GBK,恰好,公共库可以方便的帮助我们实现这样的功能。代码片段如下:

void CClientAgent::ProcessContent(PNetSocket pSocket, const std::string& ContentType, char * pContent, uint32_t ContentLength)
{
    //转码读取json内容
    pContent[ContentLength] = '\0';
    //判断包类型是否正确
    if (!VerifyContentTypeValid(pSocket, ContentType))
    {
        return ;
    }

    /*std::string RequestContent(pContent);*/
    //使用公共库CommonTools把uft-8转换为GBK
    std::string RequestContent = CommonTools::U2G(pContent);
    //pContent = (char*)DecodedContent.c_str();
    Document Doc;
    Doc.Parse(RequestContent.c_str());
在DA完成了socket数据报到本地调用的转换,其中char * pContent的内容为调用数据报的包体,通过U2G进行了转换。

3.3 Int2Str Str2Int, Trim, String2Lower

在开发工程项目的过程中,一般会使用Relase模式,而且勾选使用多字符集,在开发过程中,会频繁使用诸如节标题操作,可以自己实现成一个常用的库,然后使用即可。

3.4 std::string与char*, std::wstring与wchar*

之前未开发过项目,才发现这类操作非常常见,自己写的函数也容易出现问题,最好能够使用稳定的库来进行上述转换。自己在这个地方花了不少的时间,最后还是把自己写的那些函数全部给丢掉了,转而使用了公共库函数。


4 函数定义问题

4.1 std::string做参数

使用std::string做参数的时候,要使用引用,因为这样可以降低程序的消耗,如果函数定义为普通的std::string变量,则实际在函数执行时重新生成新的内存空间并拷贝成实参的内容,使用引用比较高效。另外,若在函数中不改变该字符串的内容,应使用const std::string&。如下所示:
/** @fn     bool UploadFile(const std::string& FileName, const std::string& RelativePath)
 *  @brief  实现当前目录下生成的wav文件上传到FTP服务器的相对路径下
 *  @param  FileName [in] 类型std::string引用, wav语音文件名,格式为GUID.wav
 *  @param  RelativePath  [in],类型std::string引用,内容"/BroadCastFile/"表示从相应的FTP根目录下开始的相对路径。
 *  @return 成功创建文件返回true, 否则返回true
 */
bool ClientFtp::UploadFile(const std::string& FileName, const std::string& RelativePath)

4.2 函数中局部变量初始化问题
这是一个常犯的错误,在函数中定义的局部变量,没有进行直接初始化的意识,这样在对应变量的内存中,是乱的,如下面的代码片段:

DEV_ERR_RET CHikNetDevice::SetSipConfig(Value* ParamNode, std::string &Msg, std::string& OutParam)
{
    NETSDKPLUGIN_TRACE("- CHikNetDevice::SetSipConfig Starts");
    std::string ServerIp;
    int ServerPort;
    std::string UserName;
    std::string Password;
    int enabledAutoLogin;
    std::string localNo;
    int loginCycle;
    DWORD errCode;
    char szLan[128];

修改成如下:

DEV_ERR_RET CHikNetDevice::SetSipConfig(Value* ParamNode, std::string &Msg, std::string& OutParam)
{
    NETSDKPLUGIN_TRACE("- CHikNetDevice::SetSipConfig Starts");
    std::string ServerIp;
    int ServerPort = 0;
    std::string UserName;
    std::string Password;
    int enabledAutoLogin = 0;
    std::string localNo;
    int loginCycle = 0;
    DWORD errCode = 0;
    char szLan[128] = {0};

在上述的代码片段中,我们为int,DWORD的变量进行了初始化,同时也为字符数组szLan进行了每个元素0的初始化。这是因为内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成0,在函数体里的内置类型变量不进行自动初始化。
而在C++ Premier中提到,未初始化的变量容易引起运行问题。使用未初始化的变量是常见的程序错误,通常也是难以发现的错误。问题出在为初始化的变量事实上都有一个值。编译器把该变量放到内存中的某个位置,而把这个位置的无论哪种位模式都当成是变量初始化的状态,当被解释成整型值时,任何位模式都是合法的值,虽然这个值不可能是程序员想要的。
所以,建议每个内置类型的对象都要初始化,这样做会更加安全和容易。类类型变量的初始化可以通过自己的默认构造函数实现。

4.3 虚析构函数

直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。之前实现的代码,自己定义的类实现使用的都是默认的析构函数,之后修改为虚析构函数,函数体为空

4.4 Disconnect函数定义

之前不佳的Disconnect函数定义如下:

void CHikNetDevice::Disconnect()
{
    if (m_IsConnected)
    {
        BOOL ret = NET_DVR_Logout(m_UserID);
        if (ret != TRUE)
        {
            NETSDKPLUGIN_ERROR("- User %s logout fails, errCode is %d", m_UserName, NET_DVR_GetLastError());
            return ;
        } else
        {
            NETSDKPLUGIN_ERROR("- User %s logout success", m_UserName.c_str());
            m_IsConnected = false;
        }
    }
}

这主要是因为NET_DVR_Logout(m_UserID);也没什么大问题,可以使用如下的方式定义DisConnect函数

void CHikNetDevice::Disconnect()
{
    if (m_UserID >= 0)
    {
        NET_DVR_Logout(m_UserID);
        m_UserID = -1;
    }
    m_IsConnected = false;
}

4.5 if-else判断次序问题

在函数中可能会出现许多的条件判断,根据条件执行不同的代码路径,而这时我们就碰到了一个问题,是用if判断合法情况,还是用if判断非法情况,这两种判断便会造成如下的代码问题:

if (configNode.IsArray())
    {
        SizeType iItemCount = configNode.Size();
        DWORD *pStatus = SAFENEW DWORD[iItemCount];
        NET_DVR_CHANNEL_GROUP *pStruChannelGroup = SAFENEW NET_DVR_CHANNEL_GROUP[iItemCount];
        NET_DVR_DETECT_FACE *pStruDetectFaceCfg = SAFENEW NET_DVR_DETECT_FACE[iItemCount];
        memset(pStatus, 0, sizeof(DWORD)*iItemCount);
        memset(pStruChannelGroup, 0, sizeof(NET_DVR_CHANNEL_GROUP)*iItemCount);
        memset(pStruDetectFaceCfg, 0, sizeof(NET_DVR_DETECT_FACE)*iItemCount);
        if(pStatus==NULL || pStruChannelGroup==NULL || pStruDetectFaceCfg==NULL)
        {
            Msg = "memory allocation failed in NET_DVR_GetDeviceConfig";
            NETSDKPLUGIN_ERROR("Memory allocate fails before NET_DVR_GetDeviceConfig");
            return DEV_ERR_NOT_ENOUGH_MEMORY;
        }

        for (SizeType i=0; i<iItemCount; i++)
        {
            Value& Node = configNode[i];
            pStruChannelGroup[i].dwGroup = i;
            bool ret = GetCallParam(Node, pStruChannelGroup[i]);
            if (!ret)
            {
                NETSDKPLUGIN_ERROR("- GetCallParam fails");
                return DEV_ERR_INVALID_PARAM;
            }
        }
        BOOL ret = NET_DVR_GetDeviceConfig(m_UserID, NET_DVR_GET_FACE_DETECT, iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
        if (ret == TRUE)
        {
            for (SizeType i=0; i<iItemCount; i++)
            {
                Value& node = configNode[i];
                if (node.HasMember("enableDetectFace")==false)
                {
                    NETSDKPLUGIN_ERROR("- The param transfered doesnot contain the key enableDetectFace");
                    return DEV_ERR_INVALID_PARAM;
                }
                Value& enableNode = node["enableDetectFace"];
                pStruDetectFaceCfg[i].byEnableDetectFace = enableNode.GetInt();
                //开启人脸侦测的地方,struAlarmHandleType 这个结构体的dwHandleType 需要配置为0x04 ,不然平台会收不到报警
                pStruDetectFaceCfg[i].struAlarmHandleType.dwHandleType = 0x04;
            }
            BOOL rett = NET_DVR_SetDeviceConfig(m_UserID, NET_DVR_SET_FACE_DETECT, iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
            if (rett)
            {
                NETSDKPLUGIN_INFO("- NET_DVR_SetDeviceConfig Success!");
                return DEV_ERR_SUCCESS;
            }else
            {
                errCode = NET_DVR_GetLastError();
                sprintf(szLan, "Set the FaceConfig failes, errCode is %d", errCode);
                Msg = szLan;
                NETSDKPLUGIN_ERROR("- NET_DVR_SetDeviceConfig Failes, errCode: %d", errCode);
                return DEV_ERR_FAILED;
            }
        } else
        {
            errCode = NET_DVR_GetLastError();
            sprintf(szLan, "Get the FaceConfig failes, errCode is %d", errCode);
            Msg = szLan;
            NETSDKPLUGIN_ERROR("- NET_DVR_GetDeviceConfig Fails, errCode: %d", errCode);
            return DEV_ERR_FAILED;
        }
        SAFEARRAYDELETE(pStruDetectFaceCfg);
        SAFEARRAYDELETE(pStruChannelGroup);
        SAFEARRAYDELETE(pStatus);

    }else
    {
        Msg = "the configed data array is not an Array";
        NETSDKPLUGIN_ERROR("- The configed data array is not an Array");
        return DEV_ERR_INVALID_PARAM;
    }

首先if-else的格式问题,规范的if-else如下:

If (condition1)
{
    //code
} else{//在else之前应有一个空格
    //code
}

或者可以使用如下的规范:

If (condition1)
{
       //code
}
else
{
    //code
}

另外,可以看到,在判断时,如果先判断合法情况,对应的非法情况就会再末尾的地方,这会给人代码较长的感觉,因此星辰帮助我修改成了如下的代码:

do 
    {
        if (true != configNode.IsArray())
        {
            Msg = "the configed data array is not an Array";
            NETSDKPLUGIN_ERROR("- The configed data array is not an Array");
            ret = DEV_ERR_INVALID_PARAM;
            break;
        }

        SizeType iItemCount = configNode.Size();
        if (iItemCount <= 0)
        {
            Msg = "iItemCount is zero";
            NETSDKPLUGIN_ERROR("- iItemCount is zero");
            ret =  DEV_ERR_INVALID_PARAM;
            break;
        }

        pStatus = SAFENEW DWORD[iItemCount];
        pStruChannelGroup = SAFENEW NET_DVR_CHANNEL_GROUP[iItemCount];
        pStruDetectFaceCfg = SAFENEW NET_DVR_DETECT_FACE[iItemCount];
        //SAFENEW 之后要立即判断是否为空
        if((pStatus == NULL) || (pStruChannelGroup == NULL) || (pStruDetectFaceCfg == NULL))
        {
            Msg = "memory allocation failed in NET_DVR_GetDeviceConfig";
            NETSDKPLUGIN_ERROR("Memory allocate fails before NET_DVR_GetDeviceConfig");
            ret = DEV_ERR_NOT_ENOUGH_MEMORY;
 break;
        }

        memset(pStatus, 0, sizeof(DWORD)*iItemCount);
        memset(pStruChannelGroup, 0, sizeof(NET_DVR_CHANNEL_GROUP)*iItemCount);
        memset(pStruDetectFaceCfg, 0, sizeof(NET_DVR_DETECT_FACE)*iItemCount);

        for (SizeType i=0; i<iItemCount; i++)
        {
            Value& Node = configNode[i];
            pStruChannelGroup[i].dwGroup = i;
            bool Flag = GetCallParam(Node, pStruChannelGroup[i]);
            if (!Flag)
            {
                NETSDKPLUGIN_ERROR("- GetCallParam fails");
                ret =  DEV_ERR_INVALID_PARAM;
                break;
            }
        }
        if (ret != DEV_ERR_SUCCESS)
        {
            break;
        }

        ret_config = NET_DVR_GetDeviceConfig(m_UserID, NET_DVR_GET_FACE_DETECT, 
            iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, 
            pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
        if (TRUE != ret_config)
{
            errCode = NET_DVR_GetLastError();
            sprintf(szLan, "Get the FaceConfig failes, errCode is %d", errCode);
            Msg = szLan;
            NETSDKPLUGIN_ERROR("- NET_DVR_GetDeviceConfig Fails, errCode: %d", errCode);
            ret = DEV_ERR_FAILED;
            break;
        }

        for (SizeType i=0; i<iItemCount; i++)
        {
            Value& node = configNode[i];
            if (node.HasMember("enableDetectFace")==false)
            {
                NETSDKPLUGIN_ERROR("- The param transfered doesnot contain the key enableDetectFace");
                ret = DEV_ERR_INVALID_PARAM;
                break;
            }

            Value& enableNode = node["enableDetectFace"];
            pStruDetectFaceCfg[i].byEnableDetectFace = enableNode.GetInt();
            //开启人脸侦测的地方,struAlarmHandleType 这个结构体的dwHandleType 需要配置为0x04 ,不然平台会收不到报警
            pStruDetectFaceCfg[i].struAlarmHandleType.dwHandleType = 0x04;
        }
        if (ret != DEV_ERR_SUCCESS)
        {
            break;
        }

        //错误信息列表,和要查询的监控点一一对应,例如lpStatusList[2]就对应lpInBuffer[2],由用户分配内存,
        //每个错误信息为4个字节(1个32位无符号整数值),参数值:0或者1表示成功,其他值为失败对应的错误号
        for (SizeType i=0; i != iItemCount; i++)
        {
            if ((pStatus[i]!=0) && (pStatus[i]!=1))
            {
                Flag = false;
                break;
            }
        }
        if (Flag != true)
        {
            ret = DEV_ERR_FAILED;
            break;
        }

        BOOL rett = NET_DVR_SetDeviceConfig(m_UserID,
            NET_DVR_SET_FACE_DETECT, iItemCount,
            pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),
            pStatus,
            pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
        if (false == rett)
        {
            errCode = NET_DVR_GetLastError();
            sprintf(szLan, "Set the FaceConfig failes, errCode is %d", errCode);
            Msg = szLan;
            NETSDKPLUGIN_ERROR("- NET_DVR_SetDeviceConfig Failes, errCode: %d", errCode);
            ret = DEV_ERR_FAILED;
            break;
        }

        for (SizeType i=0; i != iItemCount; i++)
        {
            if ((pStatus[i]!=0) && (pStatus[i]!=1))
            {
                Flag = false;
                break;
            }
        }
        if (Flag != true)
        {
            ret = DEV_ERR_FAILED;
            break;
        }

        NETSDKPLUGIN_INFO("- NET_DVR_SetDeviceConfig Success!");
        ret = DEV_ERR_SUCCESS;
        break;
    } while (0);

    SAFEARRAYDELETE(pStruDetectFaceCfg);
    SAFEARRAYDELETE(pStruChannelGroup);
    SAFEARRAYDELETE(pStatus);
    NETSDKPLUGIN_TRACE("- CHikNetDevice::SetFaceDetectionConfig Ends");

从上述的代码来看,是用if-else判断时,均使用的策略是先判断非法情况,让正常的情况维持在主干内。可以通过蓝色部分和红色部分两个地方进行理解。

4.6 返回值问题

bool String2Speech::ConvertTTS(string str, char *SzFileName)

但在函数体重返回了TRUE, FALSE, 这样不合理,尽管在函数返回时进行了自动的转换,但是是不合适的代码,若返回值为bool确保返回true或false,若为BOOL,确保返回TRUE或者FALSE。

4.7 格式化字符串

在程序中一种常见的情况是需要格式化字符串,即比如说有两个整形的变量,需要把两个整形变量以一种格式化的方式保存到字符串中打印。在这种时候,可以使用stringstream提供的转换和格式化。该类型可以完成多种数据类型之间实现自动格式化。即有一个数值数据集合,要获取它们的string表示形式,或反之。

int val1 = 512, val2 = 1024;
ostringstream format_message;
//ok: converts values to a string representation
format_message<< "val1: "<<val1<<"\n"
              << "val2: "<<val2<< "\n";
string str = format_message.str();

在C语言中,则是通过字符串格式化工具来实现的,参见下面的代码:

    if (!NET_DVR_GetDVRConfig(m_UserID, NET_DVR_GET_SIP_CFG, 0xFFFFFFFF, &SIpConfig, sizeof(NET_DVR_SIP_CFG), &dwReturned))
    {
        errCode = NET_DVR_GetLastError();
        NETSDKPLUGIN_ERROR("- NET_DVR_GetDVRConfig Failes., errCode: %d", errCode);
        char szLan[128] = {0};
        sprintf(szLan, "NET_DVR_GetDVRConfig Fails, errCode is %d", errCode);
        Msg = szLan;
        return DEV_ERR_FAILED;
    }

下面的代码片段,同样适用sprintf实现了拷贝

    sprintf((char *)SIpConfig.stuServerIP.sIpV4, "%s", ServerIp.c_str());
    SIpConfig.wServerPort = ServerPort;
    sprintf((char *)SIpConfig.byUserName, "%s", UserName.c_str());
    sprintf((char *)SIpConfig.byPassWord, "%s", Password.c_str());
    //把设备ID和显示名称同时修改
    sprintf((char *)SIpConfig.byLocalNo, "%s", localNo.c_str());
    sprintf((char *)SIpConfig.byDispalyName, "%s", localNo.c_str()

sprintf_s在下面的代码中也使用,注意引号在C语言中的标识,转义字符

char Param[1024] = {0};
sprintf_s(Param, 1024, "{\"called\":[{\"deviceId\":\"%s\",\"interfaceType\":\"%s\",\"interfaceParam\":\"%s\"}]}",
                pCalledInfo->GetDeviceId().c_str(), pCalledInfo->GetInterfaceType().c_str(), pCalledInfo->GetInterfaceParam().c_str());

4.8 std::string substr使用

在代码中,有一处“interfaceParam”:“100;0”,其中分号之前的100表示设备ID,分号后面的数字表示面板ID。因此,我们可以使用substr的方式分解两个id

DeviceIdToSearch = p.substr(0, f);
Param += p.substr(0, f);
Param += "\" CalledPanelID=\"";
std::string DevPanelId = p.substr(f+1);
DevicePanelId = (BYTE)(atoi(DevPanelId.c_str()));
Param += p.substr(f + 1);

=========================================
substr操作
s.substr(pos, n)
返回一个 string 类型的字符串,它包含 s 中从下标 pos 开始的 n 个字符
s.substr(pos)
返回一个 string 类型的字符串,它包含从下标 pos 开始到 s 末尾的所有字符
s.substr()
返回 s 的副本
在代码片段中可以看到通过调用size_t f = p.find(‘;’); 然后可以比较清楚的理解此时f的值为3。而3恰恰表明了分号之前设备id的符号个数,因此p.substr(0, 3)比较方便就能够获取到分号之前的内容,而p.substr(f+1),包含了从下表4开始到s末尾的所有字符,即分号后面的内容。
4.9 std::string和std::wstring
在项目开发过程中,涉及到把文字转换成语音文件的操作,在转换时使用了SAPI库,使用的字符是UTF-8的宽字符串。
字符串字面值是一串常量字符。字符串常量用双引号括起来的零个或者多个字符表示。不可打印字符串表示成相应的转义字符。

“Hello World!”    “”    “\nCC\toptions\tfile.[cC]\n”。

正如存在宽字符字面值
L‘a’,也存在宽字符串字面值,

L”a wide string literal”

宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。

4.9.1 标准库支持国际字符

标准IO库stream类读写的是由char类型组成的流。标准库同样定义了一组相关联的类型,支持wchar_tleixing 。每个类都加上w前缀,以此与char类型区分开。
wostream wistream wiostream从控制窗口读写wchar_t类型的数据。
wifstream, wofstream, wfstream类型从文件中读写wchar_t。
wistringstream wostringstream, wstringstream则对应string输入输出流读写宽字符对象。
wchar_t的标准输入是wcin;标准输出是wcout;而标准错误时wcerr。

4.9.2 string和wstring

string/wstring,它们两分别对应着char和wchar_t。而且string类型在C++中非常常用。转换的代码如下:

#include <string>
#include <windows.h>
using namespace std;
//Converting a WChar string to a Ansi string
std::string WChar2Ansi(LPCWSTR pwszSrc)
{
int nLen = WideCharToMultiByte(CP_ACP, 0, pwszSrc, -1, NULL, 0, NULL, NULL);

if (nLen<= 0) return std::string("");

char* pszDst = new char[nLen];
if (NULL == pszDst) return std::string("");

WideCharToMultiByte(CP_ACP, 0, pwszSrc, -1, pszDst, nLen, NULL, NULL);
pszDst[nLen -1] = 0;

std::string strTemp(pszDst);
delete [] pszDst;

return strTemp;
}

string ws2s(wstring& inputws){ return WChar2Ansi(inputws.c_str()); }

//Converting a Ansi string to WChar string

std::wstring Ansi2WChar(LPCSTR pszSrc, int nLen)

{
int nSize = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)pszSrc, nLen, 0, 0);
if(nSize <= 0) return NULL;

WCHAR *pwszDst = new WCHAR[nSize+1];
if( NULL == pwszDst) return NULL;

MultiByteToWideChar(CP_ACP, 0,(LPCSTR)pszSrc, nLen, pwszDst, nSize);
pwszDst[nSize] = 0;

if( pwszDst[0] == 0xFEFF) // skip Oxfeff
for(int i = 0; i < nSize; i ++) 
pwszDst[i] = pwszDst[i+1];

wstring wcharString(pwszDst);
delete pwszDst;

return wcharString;
}

std::wstring s2ws(const string& s){ return Ansi2WChar(s.c_str(),s.size());}

5 未实现但添加了代码给人误解

5.1 循环广播

之前的代码逻辑中,自己其实假定了传给我的SoundIds中的语音片段的个数为1,但是在实现时

for (std::list<std::string>::const_iterator it=SoundIds.begin(); it!=SoundIds.end(); it++) 
{   
    //TODO 1024个字符组成的内容。后期可修改为std::string
    bool Flag;

    Flag = FtpClient->UploadFile(FileName, sizeof(FileName)/sizeof(FileName[0]));
   //由定制版本实现指定次数的文件广播
   ret = ControlBroadcast(CMD_TALK_OPENFILE_BROD, Remotes, Msg, FileName, RepeatCount);
}
if (ret == DEV_ERR_SUCCESS)
{
    TALKCLIENTPLUGIN_TRACE("- Broadcast Success");
    CHikTalkClientInterface::Instance().FireTalkEvent(m_DeviceId, m_PanelId, STATUS_TALK_BROADCASTING, Remotes, true);
    CalledEvent(Remotes, true);
    return DEV_ERR_SUCCESS;
}

修改成如下的代码片段:理论就是如果没有实现,就写没有实现,而不要看似实现了而其实并没有实现,代码中的坑肯定会绊倒后来维护的人的。


   DEV_ERR_RET ret = DEV_ERR_FAILED;    
    if (SoundIds.size() > 1)
    {
        Msg = "Multiple voice fragments are not supported temporarily";
        TALKCLIENTPLUGIN_ERROR("- Multiple voice fragments are not supported temporarily");
        return DEV_ERR_API_NOT_SUPPORT;
    }

    CSoundIds::const_iterator it = SoundIds.begin();
    if (CommonTools::IsExistPath(*it))
    {
        Msg = "No Support Specified Path File BroadCast";
        return DEV_ERR_API_NOT_SUPPORT;
    }
    std::string Content = *it;
    std::string FileName = CommonTools::NewGuid()+".wav";
    if (!String2Speech::ConvertTTS(Content, FileName))
    {
        Msg = "TTS Fails, Not Create wav Voice File";
        return DEV_ERR_FAILED;
    }
    ClientFtp FtpClient(this->m_FtpIp, this->m_FtpPort, this->m_FtpUserName, this->m_FtpPassword);
    bool Flag;
 //由定制版本实现指定次数的文件广播
ret = ControlBroadcast(CMD_TALK_OPENFILE_BROD, Remotes, Msg, FileName, RepeatCount);

if (ret == DEV_ERR_SUCCESS)
{
    TALKCLIENTPLUGIN_TRACE("- Broadcast Success");
    //CHikTalkClientInterface::Instance().FireTalkEvent(m_DeviceId, m_PanelId, STATUS_TALK_BROADCASTING, Remotes, true);
    //TODO测试 根据设备是触发响铃还是Answer
    CalledEvent(Remotes, true);
    return DEV_ERR_SUCCESS;
}

5.2 pStatus多个通道的判断

之前的代码在进行设备人脸侦测设置的时候代码逻辑是先通过NET_DVR_GetDeviceConfig获取人脸侦测配置参数,然后在获取到的人脸侦测配置参数中修改需要修改的参数,在设置人脸侦测配置的过程中,使用pStatus数组保存了每个设备的通道配置结果,可以看出,代码并未判断每个通道执行的结果,因为设置人脸侦测的设备为报警盒,只是单通道,所以在写代码的时候就默认只有一个通道。并未对pStatus的每个元素进行判断。

BOOL rett = NET_DVR_SetDeviceConfig(m_UserID, NET_DVR_SET_FACE_DETECT, iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
if (rett)
{
        NETSDKPLUGIN_INFO("- NET_DVR_SetDeviceConfig Success!");
        return DEV_ERR_SUCCESS;
}
else
{
        errCode = NET_DVR_GetLastError();
        sprintf(szLan, "Set the FaceConfig failes, errCode is %d", errCode);
        Msg = szLan;
NETSDKPLUGIN_ERROR("- NET_DVR_SetDeviceConfig Failes, errCode: %d", errCode);
        return DEV_ERR_FAILED;
}

尽管没有进行判断,但又给人一种已经进行判断的感觉,这种代码在出问题时会非常尴尬。
修改代码逻辑如下:

ret_config = NET_DVR_GetDeviceConfig(m_UserID, NET_DVR_GET_FACE_DETECT, 
            iItemCount, pStruChannelGroup, iItemCount*sizeof(NET_DVR_CHANNEL_GROUP),pStatus, 
            pStruDetectFaceCfg, iItemCount*sizeof(NET_DVR_DETECT_FACE));
if (TRUE != ret_config)
{
    errCode = NET_DVR_GetLastError();
    sprintf(szLan, "Get the FaceConfig failes, errCode is %d", errCode);
    Msg = szLan;
    NETSDKPLUGIN_ERROR("- NET_DVR_GetDeviceConfig Fails, errCode: %d", errCode);
    ret = DEV_ERR_FAILED;
    break;
}

 for (SizeType i=0; i<iItemCount; i++)
 {
     Value& node = configNode[i];
     if (node.HasMember("enableDetectFace") == false)
     {
         NETSDKPLUGIN_ERROR("- The param transfered doesnot contain the key enableDetectFace");
    ret = DEV_ERR_INVALID_PARAM;
    break;
}

Value& enableNode = node["enableDetectFace"];
pStruDetectFaceCfg[i].byEnableDetectFace = enableNode.GetInt();
//开启人脸侦测的地方,struAlarmHandleType 这个结构体的dwHandleType 需要配置为0x04 ,不然平台会收不到报警
    pStruDetectFaceCfg[i].struAlarmHandleType.dwHandleType = 0x04;
}
if (ret != DEV_ERR_SUCCESS)
{
    break;
}

//错误信息列表,和要查询的监控点一一对应,例如lpStatusList[2]就对应lpInBuffer[2],由用户分配内存,
//每个错误信息为4个字节(1个32位无符号整数值),参数值:0或者1表示成功,其他值为失败对应的错误号
for (SizeType i=0; i != iItemCount; i++)
{
    if ((pStatus[i]!=0) && (pStatus[i]!=1))
    {
        Flag = false;
        break;
    }
}
if (Flag != true)
{
    ret = DEV_ERR_FAILED;
    break;
}

添加的代码逻辑在获取人脸侦测配置之后对每个通道进行检测,在设置后也要进行同样的逻辑判断逻辑。


6 文件名问题使用GUID

文件名应该是由调用方指定的,而不是由产生wav文件的部分随机生成。之前代码逻辑如下:
bool String2Speech::ConvertTTS(string str, char *SzFileName)
修改后文字转语音的函数声明如下:
bool String2Speech::ConvertTTS(const std::string& Content, const std::string& FileName)
在撰写代码的过程中,尤其是C++的代码,涉及std::string与char *的转换,在开发过程中,三次崩溃有两次都是因为格式化字符串时出现了
NETSDKPLUGIN_DEBUG(“- enabledAutoLogin: %d, ServerIp: %s, ServerPort: %d,localNo: %s, loginCycle: %d”,enabledAutoLogin, ServerIp.c_str(), ServerPort, localNo.c_str(), loginCycle);
上述的代码片段中,ServerIp.c_str()却传入了ServerIp,这种问题可能在开发机上没有问题,却会在服务器上崩溃。在C++开发中,最好使用std::string,而不要使用char *。


7 程序崩溃问题

7.1 std::string 和char*的打印

NETSDKPLUGIN_DEBUG("- enabledAutoLogin: %d, ServerIp: %s, ServerPort: %d,localNo: %s, loginCycle: %d",enabledAutoLogin, ServerIp, ServerPort, localNo.c_str(), loginCycle);

上述的代码在服务器上崩溃,因为ServerIp是一个std::string,而%s对应的是一个以’\0’结尾的c风格字符串。

7.2 获取指针的非空判断

在代码中,指针的获取在获取之后应该进行非空的判断,以防止出现空指针调用,下述代码即出现了空指针调用而导致程序崩溃。

CComPtr <ISpStream> cpWavStream;  
CComPtr <ISpStreamFormat> cpOldStream;  
CSpStreamFormat originalFmt;
hr = pVoice->GetOutputStream(&cpOldStream);
/*
在此处未对cpOldStream进行判断,在开发机上执行成功,但复制到服务器上,程序在这里崩溃,因为cpOldStream为NULL,而在AssignFormat中使用了cpOldStream指针进行成员函数的调用了。
*/
originalFmt.AssignFormat(cpOldStream);
hr = SPBindToFile(FileName.c_str(), SPFM_CREATE_ALWAYS, &cpWavStream, &originalFmt.FormatId(), originalFmt.WaveFormatExPtr());  

修改代码如下:

CComPtr <ISpStream> cpWavStream;  
CComPtr <ISpStreamFormat> cpOldStream;  
CSpStreamFormat originalFmt;
hr = pVoice->GetOutputStream(&cpOldStream);

//在没有声卡的情况下pVoice->GetOutputStream()中cpOldStream会生成空指针
//之前的代码并有对hr和cpOldStream进行非空判断,导致了程序在此处发生崩溃,因为生成了空指针
if (FAILED(hr) || (NULL == cpOldStream))
{
    TALKCLIENTPLUGIN_ERROR("- GetOutputStream failed, lastError:[%d][%d]", hr, GetLastError());
    ret = false;
    break;
}
originalFmt.AssignFormat(cpOldStream);
hr = SPBindToFile(FileName.c_str(), SPFM_CREATE_ALWAYS, &cpWavStream, &originalFmt.FormatId(), originalFmt.WaveFormatExPtr());
在上述代码获取指针之后,进行了操作成功与否的判和空指针判断,如果空指针直接返回。

7.3 加入队列的元素却未删除便delete,之后使用Access Violation,然后使用了这段内存

这个插件崩溃是定位起来花时间最长的,dump文件指明崩溃的类型为Access Violation。先前调用了_Disconnect函数,而该函数重新对m_Connected进行了判断,判断失败就没有从列表中删除添加的this指针,导致程序崩溃,使用了非法内存。
修改过后的代码如下:

CHikTalkClientInterface::Instance().AddDevice(this);
int DevStatus = 0;
if (GetDeviceStatus(DevStatus) == DEV_ERR_SUCCESS)
{
    if (DevStatus == STATUS_TALK_OFFLINE)
    {
        m_Status = INTERCOM_DEV_STATUS_OFFLINE;
    }
    else
    {
        m_Connected = true;
        return DEV_ERR_SUCCESS;
    }
}
else
    m_Status = INTERCOM_DEV_STATUS_UNKNOWN;
//若调用_Disconnect()会导致插件崩溃,出现Access Violation。
CHikTalkClientInterface::Instance().RemoveDevice(this);

猜你喜欢

转载自blog.csdn.net/lk142500/article/details/80665246