我想创建一个函数,以删除c字符串中ch的所有字符。 但是我一直收到访问冲突错误。
Unhandled exception at 0x000f17ba in testassignments.exe: 0xC0000005: Access violation writing location 0x000f787e.
void removeAll(char* &s, const char ch)
{
int len=strlen(s);
int i,j;
for(i = 0; i < len; i++)
{
if(s[i] == ch)
{
for(j = i; j < len; j++)
{
s[j] = s[j + 1];
}
len--;
i--;
}
}
return;
}
我希望C字符串不包含字符“ ch”,但是却出现访问冲突错误。 在调试中,我得到了以下错误:
s[j] = s[j + 1];
我试图修改该功能,但一直出现此错误。
编辑- 输入样本:
s="abmas$sachus#settes";
ch='e' Output->abmas$sachus#settes, becomes abmas$sachus#stts
ch='t' Output-> abmas$sachus#stts, becomes abmas$sachus#ss.
我得到的不是访问输出错误,而是访问冲突错误。 编辑2: 如果有帮助,我正在使用Microsoft Visual C ++ 2010 Express。
答案 0 :(得分:4)
除了您的函数效率低下之外,每当遇到要删除的单个字符时,整个字符串的其余部分都会移位,实际上并没有什么大不了。
在评论中,人们认为您正在用s[j+1]
读取字符串的末尾,但这是不正确的。他们忘记了s[len]
是完全有效的,因为那是字符串的null终止符。
所以我现在正在使用我的水晶球,我相信该错误是因为您实际上是在字符串文字上运行它的。
// This is NOT okay!
char* str = "abmas$sachus#settes";
removeAll(str, 'e');
上面的代码不合法。字符串文字"abmas$sachus#settes"
应该不存储为非常量char*
。但是为了允许与C的向后兼容(前提是您不尝试修改字符串),通常将其作为编译器警告而不是错误来发出。
但是,实际上不允许修改字符串。一旦尝试,您的程序就会崩溃。
如果要对char
数组使用正确的方法(可以可以修改),那么就会遇到另一个问题:
// This will result in a compiler error
char str[] = "abmas$sachus#settes";
removeAll(str, 'e');
结果
错误:从类型为'char *'的右值对类型为'char *&'的非常量引用进行了无效的初始化
那为什么呢?好吧,您的函数采用char*&
类型,它强制调用者使用指针。它正在制定一项合同,规定“即使我从不修改,我也可以修改您的指针”。
有两种方法可以纠正该错误:
请不要这样做:
// This compiles and works but it's not cool!
char str[] = "abmas$sachus#settes";
char *pstr = str;
removeAll(pstr, 'e');
我之所以说这很不好,是因为它树立了危险的先例。如果函数实际上在将来的“优化”中 did 修改了指针,那么您可能会破坏一些代码而没有意识到。
想象一下,您想输出稍后删除了字符的字符串,但是删除了第一个字符,而您的函数决定将指针修改为从第二个字符开始。现在,如果您输出str
,您将获得与使用pstr
不同的结果。
此示例仅假设您将字符串存储在数组中。想象一下,如果您实际上分配了这样的指针:
char *str = new char[strlen("abmas$sachus#settes") + 1];
strcpy(str, "abmas$sachus#settes");
removeAll(str, 'e');
然后,如果removeAll
更改了指针,那么以后使用以下命令清理此内存时,将有一个 Bad 时间:
delete[] str; //<-- BOOM!!!
我确认我的功能定义已损坏方式:
简单地说,您的函数定义应该使用一个指针,而不是指针引用:
void removeAll(char* s, const char ch)
这意味着您可以在任何可修改的内存块(包括数组)上调用它。呼叫者的指针永远不会被修改,这让您感到欣慰。
现在,以下方法将起作用:
// This is now 100% legit!
char str[] = "abmas$sachus#settes";
removeAll(str, 'e');
现在我的免费水晶球阅读已经完成,您的问题已经消失,让我们解决房间里的大象:
您的代码不必要地效率低下!
您无需对字符串进行第一次遍历(使用strlen
)即可计算其长度
内部循环有效地为您的算法提供了最坏情况下的时间复杂度 O(N ^ 2)。
修改len
的小窍门,更糟糕的是,循环变量i
使代码的读取更加复杂。
如果您可以避免所有这些不良事件!?好吧,你可以!
考虑删除字符时的操作。本质上,从删除一个字符的那一刻起,您就需要开始将将来的字符改组到左侧。但是您不必一次洗牌。如果在再遇到一些字符后遇到另一个要删除的字符,则只需将将来的字符向左进一步分流。
我要说的是每个字符最多只需要移动一次。
已经有一个使用指针演示这个问题的答案,但是它没有任何解释,您也是一个初学者,所以让我们使用索引是因为您了解它们。
要做的第一件事是摆脱strlen
。请记住,您的字符串以null结尾。 strlen
所做的全部工作就是搜索字符,直到找到空字节(否则称为0
或'\0'
)...
[请注意,strlen
的实际实现是 super 智能( ie 比一次搜索单个字符要高效得多)...但是当然,对strlen
的呼叫不会更快]
您需要做的就是循环查找NULL终止符,如下所示:
for(i = 0; s[i] != '\0'; i++)
好的,现在要抛弃内循环,您只需要知道将每个新角色贴在哪里即可。仅仅保留一个变量new_size
怎么样,您将在其中计算最终字符串的长度。
void removeAll(char* s, char ch)
{
int new_size = 0;
for(int i = 0; s[i] != '\0'; i++)
{
if(s[i] != ch)
{
s[new_size] = s[i];
new_size++;
}
}
// You must also null-terminate the string
s[new_size] = '\0';
}
一段时间后,您可能会注意到它可能没有意义的“副本”。也就是说,如果i == new_size
复制字符没有意义。因此,您可以根据需要添加该测试。我会说,这可能不会造成太大的性能差异,并且可能会由于增加分支而降低性能。
但是我将其保留为练习。而且,如果您想梦想真正快速的代码以及它们变得多么疯狂,那就去看看 glibc 中strlen
的源代码。准备振作起来。
答案 1 :(得分:2)
通过编写如下函数,可以使逻辑更简单,更高效:
void removeAll(char * s, const char charToRemove)
{
const char * readPtr = s;
char * writePtr = s;
while (*readPtr) {
if (*readPtr != charToRemove) {
*writePtr++ = *readPtr;
}
readPtr++;
}
*writePtr = '\0';
}