由一道面试题引发的思考
created by jsjwql
在网上碰到这样的一个题目,据说是微软面试的题目:
将一个字符串的句子翻转(有空格)
如" you welcome, GTSC Microsoft "
->" Microsoft GTSC, welcome you "
一直习惯用标准 库里面的容器和算法, 也尝试了下一下这方面的代码,发现中间还出现不少问题,这里并不是为要要展示算法有多么好,而是从中总结一些平时容易犯的一些错误。
inline bool isLetter(char* p)
{
if (!*p)
return false;
if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') )
return true;
else
return false;
}
//divide a string into words and space-punctuation sectors
char* GetWord(char** p) //problem 1
{
char* tempStr = new char[20]; //problem 2
//char* tempStr = (char *)malloc(sizeof(char) * 20);
char* q= tempStr;
//handle the word and put it into tempStr
while ((**p != '/0')&&isLetter(*p))
{
*q = **p;
q++;
(*p)++;
}
//check if it is a world
if (isLetter(tempStr))
{
*q = '/0'; //add an end for using strlen() later //problem 3
return tempStr;
}
//if it is space-punctuation sector
while ((**p != '/0')&&!isLetter(*p))
{
*q = **p;
q++;
(*p)++;
}
*q = '/0';
return tempStr;
}
char* StringReverse(char* str)
{
if (!str)
return NULL;
char *p = str;
int size = strlen(str) + 1; //contains '/0' //problem 3
char *container = new char[size]; //problem 2
//char *container = (char *)(malloc(sizeof(char) * size));
char *tail = container + size -1;
*tail = '/0'; //add an end
//tail--; //problem 4
while (*p != '/0')
{
//get a sector
char *q = GetWord(&p);
if (!q)
break;
int len = strlen(q);
tail -= len;
char *r = tail;
char *t = q;
//put it into container by the tail
for (int i = 0; i < len; i ++)
{
*r = *t;
r++;
t++;
}
delete q; //problem 2
//free(q);
}
return container;
}
int _tmain(int argc, _TCHAR* argv[])
{
char* originalStr = " you welcome, GTSC Microsoft ";
char* reverseStr = StringReverse(originalStr);
if (reverseStr)
delete reverseStr; //problem 2
//free(reverseStr);
return 0;
}
处理步骤是:
1. 提供一个化词的方法,把单词和非单词区(空格和标点看成一个整体)分开来
2. 申请一片空间,把这些划分出来的对象从改空间的尾部安插下去。
问题1:
char* GetWord(char** p) 不能被定义成char* GetWord(char*p)
我的想法是随着GetWord()函数的处理,p的指针会随之变化,当第一次调用GetWord处理完,p由" you welcome, GTSC Microsoft "变成了
"you welcome, GTSC Microsoft "前面的空格都没有了,但是如果定义成char* GetWord(char*p) ,则p仍然是" you welcome, GTSC Microsoft ",问题原因就像我们要在函数里面分配内存时一样,传入指针的指针。
如果直接传入指针的话,如下图所示,在调用函数的时候回创建一个临时指针,虽然我们可以通过这个指针来操作实际对象object的作用,但是它不会影响原始指针p。
如果传入指针的指针的话,相当于我们传入的是指针p的指针&p,然后生成了&p的一个临时指针,这样的话,临时指针就能影响指针p了。
问题2:
之前在调试代码的时候出现了一个很奇怪的错误,在释放空间的时候出现了问题,我第一反应是不是因为我new了数组,没有用delete[]的方式来释放空间,结果并不是这里出先问题,而是由下面的问题4引起来的。
这就引起了我的兴趣,学c++的都知道,用new创建一个数组的时候,记得用delete[]来删除,否则的话会造成内存泄露。
我就做了一个测试发现的结果确不是这样的,我用的开发工具是VS2005
int *p = new int[10];
int *head = p; //record the address of this array
for (int i = 0; i<5; i++)
{
*p = i;
p++;
}
delete head; //replace delete[] head;
我用vs自带的工具memory观察了内存分配过程,以及释放过程,发现居然不是我想象的只释放第一个数据对象*head,而是整个数组空间都被释放了。为了确认,我还用DoundsChecker 检查了一遍,没内存泄露。在网上和网友讨论了一翻,原来在effective c++ item5中Scott就告诉过我们,new/delete new[]/delete[] 是必须要配套使用的,否则后果是无法预期的,所以说上面的代码虽然没有内存泄露,但是不稳定。
这样的话我们如果在一个函数中返回new出来的数组的话,
int* test()
{
int *p = new int[10];
int *head = p; //record the address of this array
for (int i = 0; i<5; i++)
{
*p = i;
p++;
}
return head;
}
在client端:
int* array = test();
调用这应该用delete[] array来删除new出来的空间,如果使用者不知道函数内部是怎么分配内存的话,很容易直接用delete array,这样可能造成无法预期的问题,也就是可能内存泄露。 这样的函数设计是不是不合理? 个人认为还可以用malloc/free的方式替代。
问题3:
strlen也sizeof的区别,借用上面的例子
char *p = new char[10];
char *head = p; //record the address of this array
for (int i = 0; i<5; i++)
{
*p = ‘a’;
p++;
}
*p = ‘/0’;
int len = strlen(head);
int size = sizeof(head);
delete head; //replace delete[] head;
a | a | a | a | a | /0 |
|
|
|
|
Strlen表示字符串的长度,不包括/0字符,因为它不属于字符串的一部分,只是起了表示字符结束了的作用。但是你要安排空间来存放这个字符/0。所以一个有5个字符的字符串至少要6个字节的空间来存放。如果没有/0的话,strlen就从首地址一直往后找知道找到有/0存在才结束。所以这样的话strlen的长度就无法预期了。
Sizeof 的指的是分配空间的长度,但是sizeof(head)只有4,因为head是一个指针,拥有存放地址的。如果把代码改成
char p[10];
char *q = p;
for (int i = 0; i<5; i++)
{
*q= 'a';
q++;
}
*q = '/0';
int len = strlen(p);
int size = sizeof(p);
则size为10,有一点容易混淆的是以为size为6就是字符串的长度加上字符/0。这只是针对数组的空间刚好容纳字符串和/0的时候。
问题4:
就是容易犯的一个低级数学错误。
假设一个int数组的首地址为p, 数组的长度为10,最后一个元素的地址是
p+10-1 而不是p+10. 相反如果末尾元素的地址是p,首地址就是p-10+1了。
否则的话就越位,容易出现不可预期的错误。