指针和参考问题

Posted

技术标签:

【中文标题】指针和参考问题【英文标题】:pointers and references question 【发布时间】:2009-10-09 13:30:20 【问题描述】:
#ifndef DELETE
    #define DELETE(var) delete var, var = NULL
#endif

using namespace std;

class Teste 
    private:
        Teste *_Z;

    public:
    Teste()
        AnyNum = 5;
        _Z = NULL;
    
    ~Teste()
        if (_Z != NULL)
            DELETE(_Z);
    

    Teste *Z()
        _Z = new Teste;
        return _Z;
    
    void Z(Teste *value)
        value->AnyNum = 100;
        *_Z = *value;
    

    int AnyNum;
;

int main(int argc, char *argv[])
    Teste *b = new Teste, *a;

    a = b->Z();

    cout << "a->AnyNum: " << a->AnyNum << "\n";

    b->Z(new Teste);

    cout << "a->AnyNum: " << a->AnyNum << "\n";

    //wdDELETE(a);
    DELETE(b);
    return 0;

我想知道这段代码是否存在内存泄漏 它工作正常,*a 设置了两次,AnyNum 在每个cout &lt;&lt; 上打印不同的数字 但我想知道在setter(new Teste)之后_Z发生了什么,我对指针/引用还没有太多了解,但是对于逻辑我猜它正在被交换为新变量 如果它泄漏了,有没有办法在不必再次将 a 设置为 _Z 的情况下做到这一点? 因为地址没有改变,只是分配的直接内存 我打算使用*&amp; 而不仅仅是指针,但它会有所不同吗?

【问题讨论】:

我认为确保不泄漏内存的最佳方法是不要尝试使用疯狂的技巧。这是什么:一个类,其目的是保存一个整数尝试管理另一个动态分配的自身实例的内存? 删除宏有什么用? :\ 你应该使用像 auto_ptr 这样的智能指针。如果有的话,为什么在删除后将变量设置为零?你已经完成了它,你所做的只是引入了一个奇怪的宏。更喜欢堆栈分配。 答案:是的,您正在泄漏“a”。但是代码写得非常糟糕,以至于它有很多机会到处泄漏。每次 Z() 或 Z(Teste*) 都有潜在的泄漏。您没有充分指定所有权语义,并且您似乎使用普通方法作为构造机制的一部分。说白了就是糟糕的 OO 代码,甚至更糟糕的 C++ 代码。 另外,删除的时候不需要检查null,删除null是完全可以接受的,这样会导致无操作。 @sbi - 因为它除了测试和学习指针使用之外没有其他目的? 【参考方案1】:

此行存在内存泄漏:

b->Z(new Teste);

因为函数的定义:

void Z(Teste *value)
    value->AnyNum = 100;
    *_Z = *value;

看起来像没有参数的 Z 应该是一个 getter,而有参数是一个 setter。我怀疑你打算这样做:

void Z(Teste *value)
    value->AnyNum = 100;
    _Z = value;

(注意第三行)也就是说,将指针“值”分配给指针“_Z”,而不是将指向的值复制到 Z 指向的值上。这样,第一次内存泄漏将得到解决,但代码仍然会有一个,因为 _Z 可能已经持有一个指针。所以你必须这样做:

void Z(Teste *value)
    value->AnyNum = 100;
    delete _Z; // you don't have to check for null
    _Z = value;

正如另一条评论中提到的,真正的解决方案是使用智能指针。这是相同代码的更现代的方法:

using namespace std;

class Teste 
    private:
        boost::shared_ptr<Teste> Z_;

    public:
    Teste() : AnyNum(5), Z_(NULL)
     

    boost::shared_ptr<Teste> Z()
    
        Z_.reset(new Teste);
        return Z_;
    

    void Z(boost::shared_ptr<Teste> value)
    
        value->AnyNum = 100;
        Z_ = value;
    

    int AnyNum;
;

int main(int argc, char *argv[])
    boost::shared_ptr<Teste> b = new Teste, a;

    a = b->Z();

    cout << "a->AnyNum: " << a->AnyNum << "\n";

    b->Z(boost::shared_ptr<Teste>(new Teste));

    cout << "a->AnyNum: " << a->AnyNum << "\n";

    return 0;

【讨论】:

谢谢,这澄清了我的问题。 shared_ptr 的目的是不必删除析构函数中的内容? shared_ptr 和 auto_ptr 以及其他智能指针的目的是不必显式删除内容,而是将其自动删除。智能指针并不完美,但它们消除了所有棘手的问题。 shared_ptr 使用 RAII 模式 (en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) 保护资源。一个很好的副作用是您不必编写任何显式删除,并且它澄清了所有权语义,即 Teste 是否“拥有”(负责删除)它传递的指针?原文不清楚,但在重写的代码中,很明显 Teste 不是“拥有”而是“共享”它们。【参考方案2】:

是的,有:

void Z(Teste *value)

   value->AnyNum = 100;
   *_Z = *value; // you need assignment operator

编译器生成的赋值运算符不会进行深拷贝,而是进行浅拷贝。您需要做的是为Teste 编写一个合适的赋值运算符(可能还有一个复制构造函数)。此外,您不必在删除指针之前检查指针是否为 NULL:

~Teste()

   // no need for checking. Nothing will happen if you delete a NULL pointer
   if (_Z != NULL)
     DELETE(_Z);

【讨论】:

复制构造函数必须手动构建吗?例如: deepcopy(a, b) a->m = b->m; 等等... 如果你的类有一个指向它自身之外的任何东西的指针,而且它不仅仅是提供给它使用的东西(比如文件写入类的文件指针),它需要确保它被复制正确并正确删除。它通常需要手动析构函数、复制构造函数和赋值运算符。 Z 函数不检查空 _z 指针。您有一个可以将 _z 保留为空的构造函数,因此(使用不同的 main)可能会崩溃。 “b->Z(新睾丸);”应该是“b->Z (Teste ());” (使用临时的,确保它破坏)。虽然 AraK 关于析构函数 NULL 检查是正确的,但您仍然可以保留它——它警告读者指针可能为空。哦,你应该同时学习智能指针哑指针,所以尽管智能指针cmets,IMO你还是会因为学习这些东西而获得学分。【参考方案3】:

您还有另一个问题:_Z 不是您应该使用的标识符。一般来说,最好避免前导下划线,特别是双下划线或下划线后跟一个大写字母是为实现保留的。

【讨论】:

参见。 ***.com/questions/228783/… 是的,这只是一个例子 我不明白这个规则...例如,如果我有一个名为_Hmm的成员,它不会受到操作系统或类似的影响,对吧?指令是 STUFF 等...如果它们不影响它,是否使用下划线,它取决于开发人员的喜好?就一个问题! xD 阅读我链接的那篇文章;它解释了关于下划线的所有规则。如果您想 100% 安全而不担心规则,请不要在标识符中使用下划线。 如果您有一个名为 func 的标识符,然后移至 C99,您的代码将失败,因为他们将该标识符添加到需要实现提供的那些标识符中。这同样适用于以 _ 开头的任何其他标识符。如果您的变量是 _THREAD 并且您移动到操作系统决定将 _THREAD 设为宏,那么您的代码将会中断。【参考方案4】:

真是一团糟! 由于选择的标识符名称以开头,整个程序很难阅读:

#ifndef DELETE
    #define DELETE(var) delete var, var = NULL
#endif

我觉得这很丑。 使用类时,它似乎非常不实用。您可以在变量超出范围的情况下使用它,但在析构函数中浪费时间。我认为将代码包装在一些智能指针中会更容易:


class Teste

    private:
        Teste *_Z;

    public:
        Teste()
        ~Teste()    // Delete the _Z pointer.
        Teste *Z();
        void Z(Teste *value);
;

好的。您有一个在析构函数中删除的指针成员。 这意味着您正在获得指针的所有权。这意味着适用四的规则(类似于三规则但适用于所有权规则)。这意味着您基本上需要编写 4 个方法,否则编译器生成的版本会弄乱您的代码。你应该写的方法是:

A Normal (or default constructor)
A Copy constructor
An Assignment operator
A destructor.

您的代码只有其中两个。你需要写另外两个。 或者您的对象不应该拥有 RAW 指针的所有权。 IE。使用智能指针。


Teste *_Z;

这是不允许的。 保留以下划线和大写字母开头的标识符。 您冒着 OS 宏弄乱您的代码的风险。停止使用下划线作为标识符的第一个字符。


~Teste()
    if (_Z != NULL)
            DELETE(_Z);

这不是必需的。简单的删除 _Z 就可以了。 _Z 超出范围,因为它在析构函数中,因此无需将其设置为 NULL。 删除操作符可以很好地处理 NULL 指针。

~Test()
    delete _Z;


Teste *Z()
    _Z = new Teste;
    return _Z;

如果您多次调用 Z() 会发生什么(PS 将 * 放在 Z 旁边而不是 Teste 旁边会使它难以阅读)。 每次调用 Z() 时,都会为成员变量 _Z 赋予一个新值。但是旧值会发生什么?基本上你正在泄漏它。也可以通过返回一个指向拥有的对象的指针 在 Teste 内部,您正在给其他人滥用该对象的机会(删除它等)。不是很好。此方法没有明确的所有权。

Teste& Z()

    delete _Z;       // Destroy the old value
    _Z = new Teste;  // Allocate a new value.
    return *_Z;      // Return a reference. This indicates you are retaining ownership.
                     // Thus any user is not allowed to delete it.
                     // Also you should note in the docs that it is only valid
                     // until the next not const call on the object


void Z(Teste *value)
    value->AnyNum = 100;
    *_Z = *value;

您正在将新构造的对象(包含指针)的内容复制到另一个动态创建的对象中! 如果没有先分配 _Z 会发生什么。构造函数将其设置为 NULL,因此不能保证它具有有效值。 您分配的任何对象也应该删除。但是这里的值是动态分配的,传递给 Z 但从未被释放。你逃脱的原因是因为指针是 c opied 到 _Z 并且 _Z 在其析构函数被销毁时被删除。


Teste *b = new Teste, *a;

这真的很值得读。不要偷懒把它写出来。 这被认为是不好的风格,你将永远无法通过任何代码审查。

Teste* b = new Teste;
Teste* a; // Why not set it to NULL

a = b->Z();

获取 a 的 ab 对象。但是谁在破坏对象 a 或 b?

b->Z(new Teste);

之后就变得太复杂了。

【讨论】:

感谢您的帖子,确实很有帮助。我写的这段代码不是我的项目,我只是想了解一些关于引用和 ptr 的概念。您说当您“拥有”变量时,getter 应该返回引用而不是指针,对吗?生病检查一下!这解决了我的问题。我的问题不太好......我想知道的是,如何确保 *a 指针在使用 setter 方法后仍然可以使用,明白吗?我的英语不及格有时单词就是不来...非常感谢您和大家的帮助。这个网站就是这么酷。 你好,赋值运算符就像复制构造函数?即:应该通过从A删除和复制到B来分配? 基本上是的。但是您可以只复制指针的值。您必须复制内容。【参考方案5】:

(我试图将其添加为注释,但是这搞砸了代码..)

我也强烈建议不要使用

#ifndef DELETE
  #define DELETE(var) delete var, var = NULL
#endif

而是类似

struct Deleter

  template< class tType >
  void operator() ( tType*& p )
  
    delete p;
    p = 0;
  
;

用法:

Deleter()( somePointerToDeleteAndSetToZero );

【讨论】:

这需要什么课程?一个简单的函数有什么问题? template&lt;class T&gt; void do_delete(T*&amp; p) delete p;p=0; 一个简单的宏有什么问题? @sbi 功能确实是一样的,我使用了结构,因为它可以传递给 std::for_each 等。 @Jonathan,对于可以用实际语言表达的任何内容,您都应该避免使用宏。这是一个简单宏问题的完美示例:parashift.com/c++-faq-lite/inline-functions.html#faq-9.5 @Jonathan:你有没有这样尝试过你的宏:DELETE(p+idx) @stijn:你肯定有一个非常有效的观点。不过,我会同时提供两者,因为 Deleter()(p) 语法太尴尬了。【参考方案6】:

(不是真正的答案,但不会做评论)

您定义宏的方式很容易出现细微的错误(而到目前为止没有人发现它的事实只是证明了这一点)。考虑您的代码:

if (_Z != NULL) // yes, this check is not needed, but that's not the point I'm trying to make
                DELETE(_Z);

预处理器通过后会发生什么:

if (_Z != 0)
        delete _Z; _Z = 0;

如果您仍然看不到它,让我正确缩进它:

if (_Z != 0)
        delete _Z;
_Z = 0;

考虑到特定的 if 条件,这没什么大不了的,但它会与其他任何事情一起爆炸,您将花费 年龄 试图弄清楚为什么您的指针突然为 NULL。这就是为什么内联函数比宏更受欢迎的原因——把它们弄乱更难。


编辑:好的,你在宏定义中使用了逗号,所以你很安全......但我仍然会说在这种情况下使用 [inline] 函数更安全。我不是永远不要使用宏的人之一,但我不会使用它们,除非它们是绝对必要的,而且在这种情况下它们不是

【讨论】:

【参考方案7】:

void Z(Teste *value) 值->AnyNum = 100; *_Z = *值;

b->Z(new Teste);

造成内存泄漏

'new Teste' 永远不会被删除,而是您正在做的是分配一个新对象作为参数,然后使用 *_Z = *value 复制其中的任何内容,但调用后该对象不会被删除。

如果你要写作

Test* param - new Teste;
b->Z(param)
delete param;

这样会更好

当然大多数人会使用 boost::shared_ptr 或类似的东西来避免关心删除等

【讨论】:

new Teste 在类的析构函数中被删除,但旧的 _Z 永远不会被删除,对吧?促进?快速谷歌告诉我是一个非常有名的图书馆。也许我稍后会看看,ty

以上是关于指针和参考问题的主要内容,如果未能解决你的问题,请参考以下文章

指针、参考还是智能指针?

C++二级指针和指针引用

数据结构&算法07-链表技巧&参考源码

c语言中的指针类型属于原子类型还是结构类型?

关于C语言free函数的问题

C语言结构体指针定义问题 - C / C++ -