创建 C++ 对象

14

我注意到创建C++对象有两种方式:

BTree *btree = new BTree;

并且

BTree btree;

据我所知,唯一的区别在于如何访问类对象(. vs. ->运算符),以及当使用第一种方式时,私有整数将被初始化为0。

哪种方式更好,有什么区别?

你如何知道何时使用其中一种?


3
有关堆和栈的信息,请参见以下链接:https://dev59.com/hHVD5IYBdhLWcg3wHn2d,https://dev59.com/jXRC5IYBdhLWcg3wG9Nb。 - Georg Fritzsche
6个回答

32

两者的不同之处:

  • 它们在内存中的不同部分创建对象(heap vs stack)。

  • 对象的生命周期是不同的: 在第一种情况下,代码显式地管理内存分配,并且必须显式地管理内存释放(使用delete/delete[])。

    在第二种情况下,对象在其封闭范围的末尾自动释放(可以是方法、方法内的嵌套块或类)。

你使用哪个取决于对象的生命周期(是否应该在创建它的方法之外存在)。


1
+1,我想要补充一个简单的经验法则:优先使用自动分配,仅在必要时使用显式分配,例如一个不好的例子:void foo() { BTree* btree=new BTree; btree->DoSomething(); delete btree; } - Frunsi
如果对象在main函数中声明,那么对于两种方法,当main函数退出时,它会自动被释放吗? - neuromancer
使用new分配在堆上的对象,如果在程序结束之前没有被删除,它不会被“释放”。例如,当程序结束时,它的析构函数不会被调用。但是,该对象使用的内存将返回给操作系统。无论内存泄漏有多严重,运行在现代操作系统下的程序都无法在退出后继续消耗内存。 - Tyler McHenry
为什么在非自动情况下,整数变量会被初始化为0,但在自动情况下,整数变量不会被初始化? - neuromancer
1
因为这是标准中定义的一种优化技术,用于堆栈内存。可以参考另一个问题的答案:https://dev59.com/u3M_5IYBdhLWcg3wWRuV#1414286。 然而,我更喜欢假设值不会被初始化为0,并在所有情况下进行赋值(当在同一代码中使用两种类型的分配时,减少出错的风险)。 - ckarras

5
第一种形式在堆上创建对象,而第二种在栈上创建对象。
当函数运行结束时,第二种形式将被销毁。而第一种形式会一直存在,直到被明确删除。
如果您只想在当前作用域中使用对象,则最好使用第二种形式。您不需要担心如何摆脱它,因为这个过程将由系统自动完成。另外请注意,如果类在栈上创建,则某些库可能无法正常工作。
如果对象应该比函数生存更长时间,则使用new形式是更好的选择。

3
这两种形式的区别在于对象存储空间的分配时间不同。使用形式为BTree bTree;的静态分配方式会在编译时完成其分配,也就是说编译器将在运行时为该对象安排内存空间。而使用BTree *pbTree = new BTree的动态分配方式则是在运行时完成其分配,也就是只有当程序执行到这一点时才会分配内存。
在这种情况下,静态和动态分配之间的差异并不明显。考虑以下情况,您需要为整数数组分配内存空间,但是元素数量只能在运行时确定,也就是说只有在程序开始执行后才能知道数组所占用的确切内存空间。
// in this function, we want to return a copy of the parameter array
int *array_cpy( int *arr, int num ){
    int *copy = new int[ num ];

    int i;
    for( i = 0; i < num; i++ ){
        copy[ i ] = arr[ i ];
    }

    return copy;
}

在这里,定义 int copy[num]; 是不合适的,一方面是因为我之前所说的原因,另一方面是因为 copy 的生命周期超出了函数。然而,由于最近的语言规范允许使用VLA,第二个原因成为了解决这个问题的关键。


3
在选择是应该使用堆栈内存还是堆内存分配内存时,您需要深入了解两者之间的区别。
是的,堆栈内存具有自动释放作用域时被清除的优点,尽管可以通过智能指针和堆内存实现相同的功能。更重要的是背后的原因是什么。
性能: 堆栈内存之所以能够自动清除,是因为它涉及在内存超出作用域时简单转移堆栈指针。因此,堆栈内存的分配和释放比堆内存快得多。
内存生命周期: 堆分配的内存可以在分配函数的范围之外使用。而堆栈不行。由于上面关于调整堆栈指针的推理,一旦内存被释放,它就是空闲的,并且很可能会被下一个堆栈分配覆盖。
简而言之,当分配是暂时的,特别是如果需要重复分配并且性能很重要时,请使用堆栈内存。当对象需要存在于分配方法之外时,请使用堆内存。

2
高层次的区别是对象的生命周期。例如,如果你正在编写一个视频游戏,你将通过new在堆上分配与怪物对应的对象。这样,怪物的底层对象的生命就与怪物本身一样长,而在编写程序时是不可知的。当玩家杀死怪物时,你的代码可以使用delete销毁怪物对象。
另一方面,对于总得分计数器,你会使用另一种形式,因为你知道你想让计数器保留多长时间(可能是整个游戏运行期间!)。通过将该形式放在“全局范围”之外的任何函数体中,它将被静态地分配作为程序二进制文件的一部分。
最后,如果你正在计算数组的总和,就像这样:
int mysum(int* arr, int len) {
  int sum = 0;
  for (int i = 0; i < len; ++i) { sum += arr[i] }
  return sum;
}

sum变量是分配在堆栈上的,这基本上是你想要的:一个临时变量,你不需要明确地释放它,并且只有当该函数实际运行时才存在。


0

嗯,它们被存储在内存的相当不同的区域。

这是一篇很好的阅读材料。 堆和栈


我知道一些关于堆和栈的知识。但我为什么要关心它们被放在哪里呢?有什么情况下我会想把它放在堆中,有什么情况下我会想把它放在栈里呢? - neuromancer
堆栈在应用程序中是有限的。堆没有限制(虚拟内存空间)。堆栈也仅限于变量的范围。堆允许您传递结构,而不考虑范围。 - Romain Hippeau
这个语句是错误的。请不要忽略静态内存。例如,在全局(或命名空间范围)中声明一个 BTree btree; 语句会在静态内存中创建对象,这在内存受限的环境中非常有意义,尤其是在所有那些自动内存管理语言(像 Java 咳咳)似乎让我们失明的情况下。 - Frunsi
一个补充:从内存角度来看,例如,“应用程序”对象实例应该在静态内存中分配:在应用程序启动时,操作系统确保静态内存可用并允许应用程序启动或不启动。然后,应用程序对象可以安全地分配更多内存(并处理错误)等等。但不幸的是,人们经常忘记这一点,忽略分配错误或自己处理。我只会责怪Java和其他公司...但这可能太过分了;-) - Frunsi

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