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都在测试中发现了。所以,在写代码的过程中,最好先确定验证正确性的手段。