代码之家  ›  专栏  ›  技术社区  ›  xan

C++字符串操作

  •  6
  • xan  · 技术社区  · 16 年前

    为了清楚起见,使用std::string或equivents不是一个选项-这是char*的全部方法。

    一个类以基本文件名的形式包含“类型”信息。

    在标题中:

    char* mBaseName;
    

    稍后,在.cpp中,它会加载从其他地方传入的信息。

    mBaseName = attributes->BaseName;
    

    static const char* const suffixes[] = {"Version1", "Version", "Version3"}; //etc.
    
    static char* GetSuffix()
    {
        int i = 0;
        //perform checks on some data structures
        i = somevalue;
       return suffixes[i];
    }
    

    然后,基类在运行时创建所需的文件名:

    void LoadStuff()
    {
        char* suffix = GetSuffix();
        char* nameToUse = new char[50];
        sprintf(nameToUse, "%s%s",mBaseName,suffix);
    
        LoadAndSetupData(nameToUse);
    }
    

    你可以立刻看到问题所在。名字永远不会被删除,内存泄漏。

    我可能担心的太多,或者太傻了,但是类似LoadStuff()的代码也会在其他地方出现,所以需要解决。这是令人沮丧的,因为我不太了解事情的运作方式,看不到一个安全和“非黑客”的解决方案。我只会写:

    LoadAndSetupData(mBaseName + GetSuffix());
    

    不用担心。

    更新

    我调用LoadAndSetupData()的代码的问题是,在某些时候,它可能复制了文件名并将其保存在本地,但实际的实例化是异步的,LoadAndSetupData实际上将东西放入队列中,并且至少在那一点上,它希望传入的字符串仍然存在。

    我无法控制此代码,因此无法更新其函数。

    13 回复  |  直到 14 年前
        1
  •  3
  •   Lou Franco    16 年前

    现在看到的问题是如何清理创建并传递给LoadAndSetUpData()的字符串

    1. LoadAndSetUpData()不创建自己的副本
    2. 不能更改LoadAndSetUpData()来执行此操作
    3. 在LoadAndSetupData()返回后,需要字符串在一段时间内仍然存在

    以下是建议:

    1. 您能自己创建要调用的队列对象吗?它们肯定会在使用你的字符串之后被调用吗。如果是,则使用调用delete[]的相同字符串创建清理队列事件

    2. 你能指望的最大数目吗。如果您创建了一个大型字符串数组,是否可以在一个周期内使用它们,并确保当您回到开始时,可以重用该字符串

    3. 你能指望有多少时间吗?如果是的话,在某处注册删除,过一段时间再检查。

    最好的方法是使用char*来取得所有权或复制的函数。没有引用计数或垃圾收集,共享所有权是最难做到的。

        2
  •  3
  •   Community CDub    8 年前

    编辑:这个答案并不能完全解决他的问题——我在这里提出了其他建议: C++ string manipulation

    他的问题是,他需要将创建的char*的作用域扩展到函数外部,直到异步作业完成。

    原始答案:

    在C++中,如果我不能使用标准库或Boost,我仍然有这样的类:

    template<class T>
    class ArrayGuard {
      public:
        ArrayGuard(T* ptr) { _ptr = ptr; }
        ~ArrayGuard() { delete[] _ptr; }
      private:
        T* _ptr;
        ArrayGuard(const ArrayGuard&);
        ArrayGuard& operator=(const ArrayGuard&);
    }
    

    你使用它就像:

    char* buffer = new char[50];
    ArrayGuard<char *> bufferGuard(buffer);
    

    缓冲区将在作用域的末尾被删除(在返回或抛出时)。

    对于动态大小数组的简单数组删除,我希望将其视为在作用域末尾释放的静态大小数组。

    保持简单——如果你需要更漂亮的智能指针,使用Boost。

    如果示例中的50是可变的,则这很有用。

        3
  •  3
  •   Salman A    16 年前

    使用C++内存管理要记住的是所有权。如果LoadAndSetupData数据不会拥有字符串,那么这仍然是您的责任。由于您不能立即删除它(因为异步性问题),您将不得不保留这些指针,直到您知道可以删除它们为止。

    维护已创建的字符串池:

    • 如果您知道在某个时间点之后创建的所有字符串都已被处理,那么请跟踪字符串的创建时间,然后可以删除该子集。-如果你能找出一个单独的字符串何时被处理过,那么就删除这个字符串。

    class StringPool
    {
        struct StringReference {
            char *buffer;
            time_t created;
        } *Pool;
    
        size_t PoolSize;
        size_t Allocated;
    
        static const size_t INITIAL_SIZE = 100;
    
        void GrowBuffer()
        {
            StringReference *newPool = new StringReference[PoolSize * 2];
            for (size_t i = 0; i < Allocated; ++i)
                newPool[i] = Pool[i];
            StringReference *oldPool = Pool;
            Pool = newPool;
            delete[] oldPool;
        }
    
    public:
    
        StringPool() : Pool(new StringReference[INITIAL_SIZE]), PoolSize(INITIAL_SIZE)
        {
        }
    
        ~StringPool()
        {
            ClearPool();
            delete[] Pool;
        }
    
        char *GetBuffer(size_t size)
        {
            if (Allocated == PoolSize)
                GrowBuffer();
            Pool[Allocated].buffer = new char[size];
            Pool[Allocated].buffer = time(NULL);
            ++Allocated;
        }
    
        void ClearPool()
        {
            for (size_t i = 0; i < Allocated; ++i)
                delete[] Pool[i].buffer;
            Allocated = 0;
        }
    
        void ClearBefore(time_t knownCleared)
        {
            size_t newAllocated = 0;
            for (size_t i = 0; i < Allocated; ++i)
            {
                if (Pool[i].created < knownCleared)
                {
                    delete[] Pool[i].buffer;
                }
                else
                {
                    Pool[newAllocated] = Pool[i];
                    ++newAllocated;
                }
            }
            Allocated = newAllocated;
        }
    
        // This compares pointers, not strings!
        void ReleaseBuffer(char *knownCleared)
        {
            size_t newAllocated = 0;
            for (size_t i = 0; i < Allocated; ++i)
            {
                if (Pool[i].buffer == knownCleared)
                {
                    delete[] Pool[i].buffer;
                }
                else
                {
                    Pool[newAllocated] = Pool[i];
                    ++newAllocated;
                }
            }
            Allocated = newAllocated;
        }
    
    };
    
        4
  •  2
  •   Christian Henning Christian Henning    16 年前

    boost

    但我只能鼓励您使用std::string。

        5
  •  2
  •   Todd Gamblin    16 年前

    如果必须使用char*,则LoadAndSetupData()应在调用后显式记录谁拥有char*的内存。你可以做两件事之一:

    1. 复制字符串。 这可能是最简单的事情。LoadAndSetupData将字符串复制到某个内部缓冲区中,调用方始终负责内存。

    2. LoadAndSetupData()文档,它将负责最终释放char*的内存。调用者不必担心释放内存。

    我通常更喜欢像#1中那样的安全复制,因为字符串的分配器也负责释放它。如果你使用#2,分配器必须记住不要释放东西,内存管理发生在两个地方,我发现很难维护。不管是哪种情况,都是 这样呼叫者就知道会发生什么。

        6
  •  2
  •   Evan Teran    16 年前

    由于需要nameToUse才能在函数之后仍然存在,因此您必须使用new,我要做的是返回指向它的指针,以便调用方可以在以后不再需要时“删除”它。

    char * LoadStuff()
    {
        char* suffix = GetSuffix();
        char* nameToUse = new char[50];
        sprintf("%s%s",mBaseName,suffix);
    
        LoadAndSetupData(nameToUse);
        return nameToUse;
    }
    

    然后:

    char *name = LoadStuff();
    // do whatever you need to do:
    delete [] name;
    
        7
  •  1
  •   ididak    16 年前

    在这种情况下,不需要在堆上分配。并始终使用snprintf:

    char nameToUse[50];
    snprintf(nameToUse, sizeof(nameToUse), "%s%s",mBaseName,suffix);
    
        8
  •  0
  •   Dani    16 年前

    在LoadStuff的范围之外,nameToUse到底在哪里使用?如果有人在加载后需要它,它需要传递它,以及内存释放的责任

    如果你能像你建议的那样用c

    LoadAndSetupData(mBaseName + GetSuffix()); 
    

    那么没有任何东西会引用LoadAndSetupData的参数,因此您可以安全地将其更改为

    char nameToUse[50];
    

    正如马丁建议的那样。

        9
  •  0
  •   Martin    16 年前

    你必须管理你为nameouse分配的内存的生命周期。在std::string之类的类中包装它会使您的生活更简单一些。

    我想这是一个小小的愤怒,但既然我想不出更好的办法来解决你的问题,我会指出另一个潜在的问题。在复制或连接字符串时,需要非常小心地检查要写入的缓冲区的大小。strcat、strcpy和sprintf等函数很容易覆盖其目标缓冲区的末尾,从而导致虚假的运行时错误和安全漏洞。

    抱歉,我个人的经验主要是在Windows平台上,他们引入了这些函数的“安全”版本,称为strcat、strcpy和sprintf,它们的许多相关函数也是如此。

        10
  •  0
  •   JSBÕ±Õ¸Õ£Õ¹    16 年前

    第一:为什么需要将分配的字符串持久化到LoadStuff()结束之后?有没有一种方法可以重构以删除该需求。

    由于C++没有提供一种直接的方式来完成这种事情,所以大多数编程环境使用一组指针来防止删除/免费问题。由于东西只能分配/释放一次,所以需要非常清楚谁“拥有”指针。一些示例指南:

    1) 通常,分配字符串的人是所有者,也负责释放字符串。

    2) 如果需要在不同的函数/类中释放,则必须将所有权明确移交给另一个类/函数。

    在您的特定情况下,这归结为LoadStuff()应该删除[]nameToUse,它调用的函数应该生成一个本地副本。

    static char *nameToUse = new char[50];
    
        11
  •  0
  •   xan    16 年前

    谢谢大家的回答。我没有选择一个作为“答案”,因为这个问题没有一个具体的解决方案,关于这个问题最好的讨论都是由我和其他人投票决定的。

    你的建议都很好,而且你对我的问题很有耐心。我相信你可以看到,这是对一个更复杂问题的简化,而且还有很多事情与我给出的例子有关,因此,其中的一些可能并不完全有意义。

    现在,我将把“静态”解决方案扩展到后缀,并构建一个包含可能名称的表。这是非常“黑客”,但将工作,而且避免重构大量复杂的代码,我不能。

        12
  •  0
  •   orcmid    16 年前

    你可以在这里结合一些想法。

    根据应用程序模块化的方式,可能有一个方法(main?)其执行决定了nameToUse可定义为固定大小局部变量的范围。您可以将指针(&nameToUse[0]或只是nameToUse)传递给需要填充指针的其他方法(也可以传递大小)或使用指针,因为您知道当具有局部变量的函数退出或程序以任何其他方式终止时,存储将消失。

    这与使用动态分配和删除之间没有什么区别(因为保存位置的指针必须以大致相同的方式进行管理)。在许多情况下,本地分配更直接,并且在将所需的最大生存期与特定函数的执行持续时间关联起来没有问题时非常便宜。

        13
  •  -1
  •   Jonathan Adelson    16 年前

    我不完全清楚LoadAndSetupData的定义在哪里,但看起来它保留了它自己的字符串副本。因此,您应该在调用LoadAndSetupData之后删除本地分配的副本,并让它管理自己的副本。

    或者,确保LoadAndSetupData清除您分配给它的char[]。

    我的偏好是让另一个函数保留自己的副本并管理它,这样就不会为另一个类分配对象。

    编辑:因为您使用固定大小的new[50],所以您最好将其设置为本地的,让LoadAndSetupData创建自己的副本。