工作中遇到的问题解决办法

时间:2021-07-23 15:30:03

1.如何一边遍历一个容器,一边erase某些元素?

今天检视胡滔滔的代码,出现了如下形式的代码:

    map<int, int> m;

    m[1] = 1;

    m[2] = 2;

    m[3] = 3;

    m[4] = 4;

    map<int, int>::iterator iter2 = m.begin();

    while(iter2 != m.end())

    {

        m.erase(iter2++);

    }

记得以前对于vector,erase一个元素后,iter会指向被erase元素的下一个元素,所以,我认为上面的代码有问题,因为iter2移动了两次。但实际上,上面的代码执行结果是对的。相当于是先把iter2保存到一个临时迭代器,然后iter2++,然后再将临时迭代器传递给erase进行删除。

但是对于vector,我这样做,出现断言,也就是说,m.erase(iter2++);可能不是一种正规的遍历并erase方法。

出现断言的原因可能是因为在erase vector的一个元素之后,它的内存可能需要发生移动,导致以前保存的迭代器失效,也就是说,在erase之前保存的迭代器都是不能用的。

而对于list,map之类的,erase一个节点之后不会造成其他节点内存的移动,之前保存的迭代器仍然是有效的,故map那样做不会出现问题。

以前以为

    vector<int>::iterator iter = v.begin();

    while(iter != v.end())

    {

        v.erase(iter);

    }

完全是对的,其实是错的,照样会出现断言。在release下,虽然它能完成工作,但实际上它是一种巧合。参考erase的函数声明,参数都是iterator _Where形式的,即参数是传值的,根本不是传引用的,也就是说,根本不存在erase之后iter会移动到下一个元素的可能,在vector中看起来是那样,是因为erase一个元素之后,后面的所有元素向前移动了一个元素位置,iter指向的地址没变,仅仅是元素移动了,让它看起来好像是iter移动了一样,造成了假象。

 

正规的erase方法,显然也不是上面的m.erase(iter2++);原因有主要有2:

1.最好不要在参数中++

2.虽然可以通过

        map<int, int>::iterator tmp = iter2;

 

        iter2++;

        m.erase(tmp);

解决1问题,但是使用erase前保存的迭代器还是不太好,因为对于某些容器erase是会造成迭代器失效的,比如vector,deque等容器,换句话说,上面的方法不通用。

 

通过查看erase函数的说明,可以明确的是erase函数的返回值是指向被erase元素的下一个元素的,这点是可以得到保障的。利用这一点,写出如下代码:

    list<int>::iterator iter3 = l.begin();

    while(iter3 != l.end())

    {

        list<int>::iterator tmp = l.erase(iter3);

        iter3 = tmp;

    }

这样就对所有容器都通用了。

 

通过查看电驴的代码:

    for(ListItems::iterator it = rangeIt.first; it != rangeIt.second; ){

        CtrlItem_Struct* delItem  = it->second;

        if(owner == NULL || owner == delItem->owner){

            // Remove it from the m_ListItems          

            it = m_ListItems.erase(it);

 

            // Remove it from the CListCtrl

            LVFINDINFOfind;

            find.flags = LVFI_PARAM;

            find.lParam = (LPARAM)delItem;

            int result = FindItem(&find);

            if (result != -1)

                DeleteItem(result);

 

            // finally it could be delete

            delete delItem;

        }

        else{

            it++;

        }

    }

可以确定要实现按条件遍历erase确实该这样做,比如我们经常都想实现如上的功能,可惜一般人都写不对。

 

2.下面这种情况是我们经常碰到的,我觉得这是最好的解决办法

       char buffer[128];

#ifdef __STDC_SECURE_LIB__ // Usesecure version with visual studio 2005 to avoid warning.

       sprintf_s(buffer, sizeof(buffer),"%.*g", precision,x);

#else  

       sprintf(buffer, "%.*g", precision,x);

#endif

但是,使用带_s的函数一定要小心!以免为了消除告警而犯了更加严重的错误!

在一般情况下,我们确实如下面说的那样,用_s可以虽然不能消除越界的错误,但是真的越界时至少能检测到,哪怕是调用默认的无效参数处理函数(即使是让程序崩溃),都比越界了强得多!

For example, the strcpy functionhas no way of telling if the string that it's copying is too big for itsdestination buffer. However, its secure counterpart, strcpy_s, takes the sizeof the buffer as a parameter, so it can determine if a buffer overrun willoccur. If you use strcpy_s to copy eleven characters into a ten-characterbuffer, that is an error on your part; strcpy_s cannot correct your mistake,but it can detect your error and inform you by invoking the invalid parameterhandler.

但是,今天布君却发现我一个DMR的崩溃问题,代码大致如下:

void FormatOutput(LPCSTRformatstring, ...)

{

    int nSize = 0;

    char buff[10];

    memset(buff, 0, sizeof(buff));

    va_list args;

    va_start(args, formatstring);

    nSize = _vsnprintf_s(buff, sizeof(buff), formatstring,args);

    buff[10-1] = 0;

    printf("nSize: %d,buff: %s\n", nSize, buff);

}

因为写得时候已经考虑到越界和结束符问题了,觉得这样写已经很安全了,就算缓冲区不够,_vsnprintf会自动截断的,我错了!

原来对于vsnprintf才是默认截断的,像上面那样写没问题。

像上面那样的参数调用_vsnprintf_s函数,实际调用的函数原型如下

template <size_t size>

int _vsnprintf_s(

  char (&buffer)[size],

  size_t count,

  const char *format,

  va_list argptr

); // C++ only

注意了,sizeof(buff)对应的参数是count,而不是缓冲区的大小,跟_vsnprintf已经不一样了。

参考MSDN:

count

Maximum number of characters towrite (not including the terminating null), or _TRUNCATE.

 

If count is _TRUNCATE, then thesefunctions write as much of the string as will fit in buffer while leaving roomfor a terminating null.

也就是说,当count传递的不是_TRUNCATE的时候,如果缓冲区不够,默认处理方式和strcpy_s的处理方式是一样的,那就是调用无效参数处理函数(默认程序异常),而我还在希望它像_vsnprintf一样自动截断,呜呜!

 

所以,要想让其截断,我们得传递_TRUNCATE才行,真可恶!

 

还有一个需要注意的地方是如果缓冲区不够,_vsnprintf不会加结束符的。正确的做法就是传缓冲区长度减一进去,如

 nSize = vsnprintf( buff, sizeof(buff) - 1,formatstring, args);

对于带_s应该这样

nSize = _vsnprintf_s( buff,sizeof(buff), _TRUNCATE, formatstring, args);

照MSDN,按理说这样也是可以的,如

nSize = _vsnprintf_s( buff,_TRUNCATE, formatstring,args);

为什么直接就写过头了呢,跟踪进去,缓冲区长度竟然是-1,而不是10,为啥?模板不灵了?

答:看了一下,好像还跟_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES、_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT宏的值有关系,使用起来太容易出错了,如果我们没有把握,最好别用这些模板函数,甚至连_s的函数也别用,我们自己去保证正确性(即不越界)估计还更好些!特别是_vsnprintf_s这儿的模板千万别用,实际情况和我们想象的完全不一样。

 

3.在SIP适配器中,因为调度周期过长,加上CPU速度远远大于网络处理速度,导致UDP总是丢包,所以,我需要高精度定时器(精确到1ms)?

在Windows中,目前只知道timeSetEvent可以达到1ms的精度,其他的如SetTimer,CreateTimerQueueTimer,CreateWaitableTimer,WaitForSingleObject,Sleep等都只能16毫秒的精度(有时为15ms)。

 

注意,停止多媒体定时器时异步的,停止函数立刻会返回,但有可能定时器回调函数还在运行。

 

4.如何采用使用CreateTimerQueueTimer,CreateWaitableTimer等函数?

这些函数对操作系统有限制,所以源代码中是根据宏来判断是否有效的,因为我们的VC6或者VS2005对应的SDK可能都不是最新的,所以导致编译不能通过,提示“找不到标识符”。此时,就需要我们显式定义

#define _WIN32_WINNT 0x0500

对于CreateWaitableTimer,只要定义了它,就可以编译通过了。

对于CreateTimerQueueTimer,虽然定义了它,但是VC6对应的SDK的头文件中确实没有包含这个函数,导致仍然编译不能通过,此时除了定义它,还得换编译器成VS2005.

 

对于默认建立的MFC程序,在stdafx.h中默认定义了#define _WIN32_WINNT 0x0501,故上面的函数可以直接使用。但是对于控制台程序,就需要显式定义了,记住得定义在#include<Windows.h>之前,直接包含Windows.h就可以了,不要去显式包含Winbase.h

 

5.分配内存和拷贝究竟对性能有多大影响?比如在EasySip中,实现文件导入功能后,启动适配器时间明显变长,怎么解决?

答:

void CjhhfhujfhjfDlg::OnBnClickedButton1()

{

    DWORD dwPrev = GetTickCount();

 

    char* pv = NULL;

    for (int i = 0; i <1000000; ++i)

    {

        pv = (char*)malloc(32);

        strcpy(pv, "1234567890123456789012345678901");

    }

 

    pv[0] = '\0';

    SetDlgItemText(IDC_EDIT2,pv);

    SetDlgItemInt(IDC_EDIT1,GetTickCount() - dwPrev,FALSE);

}

 

void CjhhfhujfhjfDlg::OnBnClickedButton2()

{

    // TODO: 在此添加控件通知处理程序代码

    DWORD dwPrev = GetTickCount();

    char* pv = (char*)malloc(1000000* 32);

    for (int i = 0; i <1000000; i++)

    {

        strcpy(pv, "1234567890123456789012345678901");

    }

    SetDlgItemText(IDC_EDIT2,pv);

    SetDlgItemInt(IDC_EDIT1,GetTickCount() - dwPrev,FALSE);

}

 

前者:297 ms      328 ms(带strcpy

后者:0   ms     78  ms(带strcpy

由此可见,多次分配内存时比一次分配同样多内存花的时间多多了,而拷贝实际上很快的,和分配内存比起来,基本上可以忽略不计。这和我在EasySip中,优化了字符串拷贝,但性能没得到一点提升的现象刚好吻合!所以,如果想要优化,只能采取一次多分配的方式。

 

6.注意,Windows操作系统仅仅承诺最大路径为260个字符(包含空字符,可见259个),而不是字节,所以,当包含中文时,超过260字节的路径是完全正常的,但是有些类库在处理时有缺陷,比如CFile,CFileDialog,可能用到了lstrlen之类的函数,导致判断出错,而fopen,CreateFile

是能够正常处理,最好的解决办法是使用Unicode编码,那样就怎么都不出问题了。

 

7.软件中非常重要的事情就是要防止重复,比如在自动化中,我所有的命令处理逻辑都是重复的,要是正确还好,错了那就得全部改,痛苦死了。还比如在自动化Traffic配置的时候,大师的Model提供的函数名和界面上的文字没有很好的一一对应起来,导致我想将控件对应到函数很麻烦,得一一核对。如果你的代码中没有重复的东西(包括代码,逻辑),再加上你能给变量取一个很好的名,写出来的代码就相当不错了。

 

8.不管多么简单的代码,只要写多了,肯定会写错。最近在写自动化的代码,就验证了这个结论。幸好,有很好的验证方法用来测试代码的正确性,所以bug都在测试中发现了。所以,在写代码的过程中,最好先确定验证正确性的手段。