如何在C++中处理字符串时使用memset?

29

我来自Python背景,最近正在学习C++。我正在学习一个叫做memset的C/C++函数,并遵循来自网站https://www.geeksforgeeks.org/memset-in-cpp/的在线示例,在这里我遇到了一些编译错误:

/**
 * @author      : Bhishan Poudel
 * @file        : a02_memset_geeks.cpp
 * @created     : Wednesday Jun 05, 2019 11:07:03 EDT
 * 
 * Ref: 
 */

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

int main(int argc, char *argv[]){
    char str[] = "geeksforgeeks";

    //memset(str, "t", sizeof(str));
    memset(str, 't', sizeof(str));

    cout << str << endl;

    return 0;
}

使用单引号 't' 时出错
这会打印额外的字符。

tttttttttttttt!R@`

使用双引号时,使用"t"出错

$ g++ -std=c++11 a02_memset_geeks.cpp 
a02_memset_geeks.cpp:17:5: error: no matching function for call to 'memset'
    memset(str, "t", sizeof(str));
    ^~~~~~
/usr/include/string.h:74:7: note: candidate function not viable: no known
      conversion from 'const char [2]' to 'int' for 2nd argument
void    *memset(void *, int, size_t);
         ^
1 error generated.

如何在C++中使用memset?

进一步学习
这里提供了关于memset缺点的优秀教程: https://web.archive.org/web/20170702122030/https:/augias.org/paercebal/tech_doc/doc.en/cp.memset_is_evil.html


16
“t”和‘t’不是同一个东西。 - SergeyA
16
大多数用于C++的在线学习资源都很差,据我所知这个网站也不例外,相反,可以试试这个链接:https://dev59.com/_3RC5IYBdhLWcg3wK9yV - 463035818_is_not_a_number
8
为什么在C++中要使用memset函数?旧的C函数之所以存在是为了向后兼容。 - klutt
13
这是一把已装弹的枪,你把它对准了你的左脚扣动了扳机。你需要瞄准正确的目标。 - Hans Passant
8
在回答问题的人下面,你不应该更改问题。如果你接受了一条评论或回答,但仍然无法解决问题,你可以问另一个问题,但是用另一个问题代替原来的问题这种编辑方式是有破坏性的。 - SergeyA
显示剩余15条评论
4个回答

69

这个声明

char str[] = "geeksforgeeks";

声明一个字符数组,其中包含一个字符串,该字符串是由字符序列组成的,包括终止零符号'\0'

你可以将声明想象为以下等效方式

char str[] = 
{ 
    'g', 'e', 'e', 'k', 's', 'f', 'o', 'r', 'g', 'e', 'e', 'k', 's', '\0'
};

这个函数调用 memset

memset(str, 't', sizeof(str));

覆盖数组中的所有字符,包括终止零。

因此,下一个语句为

cout << str << endl;

由于它会输出字符,直到遇到终止零,因此可能导致未定义的行为。

你可以改为写成

#include <iostream>
#include <cstring>

int main()
{
    char str[] = "geeksforgeeks";

    std::memset( str, 't', sizeof( str ) - 1 );
    
    std::cout << str << '\n';
}

或者采用以下方式
#include <iostream>
#include <cstring>

int main()
{
    char str[] = "geeksforgeeks";

    std::memset( str, 't', std::strlen( str ) );
    
    std::cout << str << '\n';
}

这意味着在数组中保持终止零的不变。

如果你想覆盖包括终止零在内的数组中的所有字符,那么你应该替换这个声明。

std::cout << str << '\n';

对于这个语句
std::cout.write( str, sizeof( str ) ) << '\n';

正如下面的程序所示,因为数组现在不包含字符串,所以会出现这种情况。
#include <iostream>
#include <cstring>

int main()
{
    char str[] = "geeksforgeeks";

    std::memset( str, 't', sizeof( str ) );
    
    std::cout.write( str, sizeof( str ) ) << '\n';
}

关于这个调用
memset(str, "t", sizeof(str));

如果第二个参数的类型(即const char *类型)与第二个函数参数的类型(即int类型)不对应,则会出现问题。请查看函数的声明。

void * memset ( void * ptr, int value, size_t num );

因此,编译器会发出错误消息。

除了字符数组(即使在C++中也经常使用),您还可以使用标准类std::string(或std::basic_string)来模拟字符串。

在这种情况下,无需使用标准C函数memset将字符串填充为单个字符。最简单的方法是:

#include <iostream>
#include <string>

int main()
{
    std::string s( "geeksforgeeks" );
    
    s.assign( s.length(), 't' );
    
    std::cout << s << '\n';
}

另一种方法是使用标准算法std::fillstd::fill_n,它们在头文件<algorithm>中声明。例如:
#include <iostream>
#include <string>
#include <iterator>
#include <algorithm>

int main()
{
    std::string s( "geeksforgeeks" );
    
    std::fill( std::begin( s ), std::end( s ), 't' );
    
    std::cout << s << '\n';
}

或者

#include <iostream>
#include <string>
#include <iterator>
#include <algorithm>

int main()
{
    std::string s( "geeksforgeeks" );
    
    std::fill_n( std::begin( s ), s.length(), 't' );
    
    std::cout << s << '\n';
}

您甚至可以使用 std::string 类的方法 replace 来实现以下任一方式:

#include <iostream>
#include <string>

int main()
{
    std::string s( "geeksforgeeks" );
    
    s.replace( 0, s.length(), s.length(), 't' );
    
    std::cout << s << '\n';
}

或者

#include <iostream>
#include <string>

int main()
{
    std::string s( "geeksforgeeks" );
    
    s.replace( std::begin( s ), std::end( s ), s.length(), 't' );
    
    std::cout << s << '\n';
}

19
原始帖明确表明用户试图学习C++。请至少提及,如果使用std::string,则这些内容都不相关,而应在此处使用它,而不是使用这种复杂的C方法。(虽然这可能与课程的开头无关) - JVApen
8
原帖明确表示用户想知道如何在字符数组中使用memset函数。 :) - Vlad from Moscow
1
好的回答。如果你想让它对OP更好:请注意类型系统的差异。C++具有静态类型系统,其中变量具有固定的静态类型。Python具有完全动态类型系统,其中值具有类型而变量没有。这可能是他在't'"t"方面感到困惑的根源。 - Yakk - Adam Nevraumont
@VladfromMoscow 当然,你是对的。我没有仔细关注strlen调用后具体执行了什么。我修改我的建议为:您可能还需要解释一下,如果将字符串声明为const char *str = "geeksforgeeks";,sizeof将不再报告字符串的长度,而是指针的大小。(即使在这个特定的例子中将其声明为指向字符串文字会导致进一步的问题,但我已经看到足够多的人犯了sizeof指向字符串的指针的错误,我认为值得解释为什么这样做不起作用。) - Ray
@Ray 谢谢。但这会是对一个简单问题过于笼统的回答。:) - Vlad from Moscow
显示剩余2条评论

31

使用单引号 't' 时出错,会打印额外的字符。

这是因为你覆盖了空终止符。

终止符是数组大小的一部分(数组不是魔术),尽管它不是 逻辑字符串大小的一部分

所以,我认为你的意思是:

memset(str, 't', strlen(str));
//               ^^^^^^

使用双引号时使用“t”出现错误

完全不同的事情。您告诉计算机将字符串中的每个字符设置为字符串。这是没有意义的,无法编译。


如何在C++中使用memset?

不要使用。

可以使用类型安全的std::fillstd::beginstd::end组合使用:

std::fill(std::begin(str), std::end(str)-1, 't');
(如果您担心性能问题,不用担心:这将通过模板特化仅委派给memset,无需牺牲类型安全性的情况下进行优化,例如在libstdc++中的示例。)

或者一开始就使用std::string


我从https://www.geeksforgeeks.org/memset-in-cpp/ 中学习 C++ 函数 memset,其中给出的示例如下:

不要从任意的网站上学习 C++,最好自己找本好书来学习。


4
很遗憾,在原始示例中确实使用了 sizeof。可惜这样的代码被用来“教授”C++ :( - 463035818_is_not_a_number
3
从好书上学习是另一个原因。在C ++中有不同类型的文字常量,这与Python完全不同。 - Lightness Races in Orbit
6
此网站(geeksforgeeks)应永久禁止。 - SergeyA
6
@astro123: 从geeksforgeeks.org/memset-in-cpp在线学习 这是你的第一个问题。该教程在其小例子中有一个严重的bug。这在geeksforgeeks.org上并不罕见。虽然其中有一些好东西,但它们经常与坏东西混杂在一起,并且在您成为专家之前,您不会知道如何区分它们。与Stack Overflow不同,geeksforgeeks没有投票机制供人们审查帖子并指示其质量,因此您无法知道哪些值得信任。 - Peter Cordes
1
@PeterCordes 很遗憾,SO文档走了它的路...显然有人需要投票、策划教程。我相信总会有人找到正确的设计方案。 - mbrig
显示剩余8条评论

5

这是 memset 的正确语法...

void* memset( void* dest, int ch, std::size_t count );

将值ch转换为无符号字符,并将其复制到dest指向的对象的前count个字符中。如果该对象是潜在重叠的子对象或不是TriviallyCopyable(例如,标量、与C兼容的结构体或平凡可复制类型的数组),则行为未定义。如果count大于dest指向的对象的大小,则行为未定义。 (source
对于第一种语法memset(str, 't', sizeof(str));。编译器因多余的大小而发出警告。它打印了18次tttttttttttttt!R@。我建议尝试使用char数组的sizeof(str) -1
对于第二种语法memset(str, "t", sizeof(str));,您提供的第二个参数是一个字符串。这就是编译器抱怨错误的原因:从“const char*”到“int”的无效转换。

1
@PeterCordes - 它在谈论类似于这样的东西。在这里,base是平凡可复制的,但它不安全用于memset(或memmove),因为它是一个潜在重叠的子对象。请注意,sizeof(base)==8,然而当它被用作derived的基础(它本身有一个char成员)时,sizeof(derived)==8!因此,派生类的成员存储在base的填充中。因此,在任意base&上使用memset进行覆盖是不安全的,因为在这种情况下,您也会破坏派生成员。 - BeeOnRope
@PeterCordes - 对的,设计决策必须在平台ABI的上下文中进行,而不仅仅是在编译器级别上,因为每个人都必须同意这一点,对吧?无论如何,我发现的唯一一个与实践不矛盾的属性,关于派生类是否可以使用填充,就是“聚合体”。请参见此处base是POD、平凡和标准布局,但仍然不安全。它不是聚合体。当然,这并不是证明 :)。 - BeeOnRope
@BeeOnRope:啊,我不知道“聚合”这个术语有一个特定的技术含义,其中包括没有私有/受保护成员。什么是聚合体和POD,它们为什么很特别?。我还没有检查过,但我认为从C++ ABI关于“POD”的注释中可以看出,(某个草案的)ISO C++必须说您可以踩在聚合体的填充上,但不一定适用于任何POD /可平凡复制类型。因此,当基类不是聚合体时,您可以将派生成员放入该填充中。这就是这个C++ ABI选择达成一致的内容。 - Peter Cordes
@PeterCordes - 是的,我刚刚读完(浏览)了那个FAQ :). 我在你提供的Itanium ABI中没有找到聚合这个词。ABI是很久以前写的,在C++标准中提到的许多变化之前,甚至有些术语都不存在。特别是,在后来的标准中引入了更细微的区别,而ABI文档不会知道这些区别。1/x - BeeOnRope
我没有读过ABI,但根据搜索结果,我找不到涵盖此情况的语言。在你链接的那个部分中,它提到“基类子对象”是“潜在重叠子对象”的一种类型(另一种类型是具有no_unique_address的数据成员),但所有进一步引用“基类子对象”的内容似乎都与此无关(它们都是关于虚表的),而对“p-o子对象”的进一步引用似乎都涉及数据成员的情况,而不是基类的情况。我正在针对这个主题提出一个具体问题,将在这里链接。 - BeeOnRope
显示剩余8条评论

5
弗拉德已经很有帮助地回答了你问题的第一部分,但我觉得第二部分可以更加直观地解释:
正如其他人所提到的,'t'是一个字符,而"t"是一个字符串,而字符串在末尾有一个空结束符。这使得"t"不仅是一个字符,而是一个由两个字符组成的数组-['t', '\0']! 这让memset函数的错误更加直观 - 它可以轻松地将单个char强制转换为int,但当它被给予一个由char组成的数组时,它会出错。 就像在Python中一样,int(['t', '\0'])(或ord(['t','\0']))无法计算。

2
更准确地说,当传递“t”时,实际上是传递“t”中“t”的地址。因此,如果将其转换为memset中的int参数,则会将指向“t”的指针转换为int,而不是将字符串的值转换为int - grovkin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接