【问题标题】:How could creating a string change the value pointed to by a const char*?创建字符串如何更改 const char* 指向的值?
【发布时间】:2015-11-28 03:52:41
【问题描述】:

我编写了一个函数,它接受一个字符串并返回一个 const char *,其中包含该字符串的编码版本。我调用这个函数,然后创建一个新字符串。这样做时,我不知何故无意中更改了指向我的 const char * 的值,我认为这是不可能的。

但是,当我不使用自己的函数,而只是将一个值硬编码到我的 const char 数组中时,创建字符串时该值不会改变。为什么这里有区别,为什么我仍然可以更改 const char 数组的值?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

using namespace std;

// returns "@username@FIN"
const char* encodeUsername(string username)
{
    username = "@" + username + "@FIN";
    return username.c_str();
}

int main(void)
{
    string jack("jack");
    const char* encodedUsername = "@jack@FIN";
    string dummy("hi");
    printf("%s\n", encodedUsername); //outputs "@jack@FIN", as expected.

    string tim("tim");
    const char* encodedUsername2 = encodeUsername(tim);
    string dummy2("hi");
    printf("%s\n", encodedUsername2); //outputs "hi". Why?
}

【问题讨论】:

    标签: c++ string memory-management


    【解决方案1】:

    要了解为什么会发生这种情况,您需要了解 C++ 的几个内在属性。

    1. 在 C++ 中,指针可以指向已释放的内存区域。这是许多其他语言无法做到的,它可以隐藏一些严重的错误。例如,考虑以下代码:
    char* moo()
    {
        char* a = new char[20];
        strcpy(a, "hello");
        delete[] a;
        return a;
    }
    

    请注意,即使我刚刚删除了a,我也可以返回指向它的指针。调用方将收到该指针,并且不知道它指向已释放的内存。此外,如果您立即打印返回值的值,您很可能会看到“hello”,因为delete 通常不会将它释放的内存清零。

    1. std::string 粗略地说是char* 的一个包装器,它将所有分配和释放隐藏在一个非常好的接口后面,因此您无需关心内存管理。 std::string 的构造函数及其上的所有操作分配或重新分配数组,析构函数将其释放。

    2. 1234563功能一结束。所以在这种情况下,只要encodeUsername 返回,username 就会被销毁,因为它是按值传递的,并且包含在函数的范围内。由于对象被销毁,它的析构函数被调用,此时字符串被释放。指向您通过调用c_str() 检索的原始数据的指针现在指向不再存在的东西。
    3. 当您在释放后立即分配对象时,您很可能会重用刚刚释放的对象的内存。在您的情况下,当您创建一个新字符串 tim 时,它会在 encodeUsername 返回时刚刚释放的相同地址分配内存。

    现在,你该如何解决它?

    首先,如果您不关心输入字符串(因为,如果您可以覆盖它),您可以通过引用传递它:

    const char* encodeUsername(string& username)
    

    这会修复它,因为username不是一个副本,所以它不会在函数结束时被销毁。然而,现在的问题是,这个函数会改变你传入的字符串的值,这是非常不可取的,并且会创建一个不直观的界面。

    其次,你可以在返回之前分配一个新的char数组,然后在调用函数结束时释放它:

    const char* encodeUsername(string username)
    {
        username = "@" + username + "@FIN";
        return strdup(username.c_str());
    }
    

    然后在main的末尾:

    free(encodedUsername);
    free(encodedUsername2);
    

    (请注意,您必须使用free 而不是delete[],因为数组是使用strdup 分配的)

    这会起作用,因为我们返回的 char 数组在我们返回之前分配在堆上并且没有被释放。它的代价是现在调用函数需要释放它,这又是一个不直观的界面。

    最后,正确的解决方案是返回 std::string 而不是 char 指针,在这种情况下,std::string 将为您处理所有分配和释放:

    string encodeUsername(string username)
    {
        username = "@" + username + "@FIN";
        return username;
    }
    

    然后在main函数中:

    string encodedUsername2 = encodeUsername(tim);
    printf("%s\n", encodedUsername2.c_str());
    

    【讨论】:

    • 我在哪里可以找到信息来了解std::string 是如何传递一个临时变量(如username)并在函数退出后保持有效的?
    • 任何关于 c++ 的介绍性书籍都将包含足够的信息。它背后的秘密是复制构造函数。每当您将一个字符串分配给另一个字符串或将其返回时,它都会调用一个复制构造函数,该构造函数允许 std::string 在必要时重新分配内存。在 c++11 中,他们引入了移动构造函数的新概念,这使事情变得更加复杂,但现在我建议只探索复制构造函数的工作原理。
    【解决方案2】:

    username 的生命周期在encodeUsername 返回时终止,使该函数返回的指针悬空。换句话说,它是未定义的行为,在这种情况下,它表现为对新创建的字符串的encodeUsername 的返回值所指向的内存的重用。

    如果您返回 std::string 本身,则不会发生这种情况。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-10-22
      • 1970-01-01
      • 2015-09-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多