33

我是一名经验丰富的 C 开发人员,刚刚开始接触 C++,我必须承认,我很困惑有多少种方法可以创建、保留和销毁 C++ 对象。在 C 中,生活很简单:在堆栈上分配=副本,和malloc/free管理堆上的数据。C++ 远非如此,至少在我看来。

鉴于此,这是我的问题:

  1. 创建 C++ 对象的所有方法是什么?直接/复制构造函数、赋值等。它们是如何工作的?
  2. 与所有这些类型的对象创建相关的所有不同的初始化语法是什么?T f = x, T f(x);,T f{x};等之间有什么区别?
  3. 最重要的是,什么时候复制/分配/C++ 中的任何内容是正确的,什么=时候要使用指针?在 C 语言中,我非常习惯于到处乱扔指针,因为指针赋值便宜,但结构复制却不那么简单。C++ 的复制语义如何影响这一点?
  4. 最后,所有这些东西是什么,比如shared_ptrweak_ptr等等?

如果这是一个有点宽泛的问题,我很抱歉,但我对何时使用什么感到非常困惑(甚至没有提到我对集合和new运算符中的内存管理的困惑),我觉得我对 C 内存管理的一切了解在 C++ 中分解。这是真的,还是我的心理模型错了?

总结一下:C++ 对象是如何创建、初始化和销毁​​的,我应该什么时候使用每种方法?

4

3 回答 3

26

首先,你的内存管理技能在 C++ 中很有用,只是比 C++ 做事方式低了一个层次,但它们在那里......

关于你的问题,它们有点宽泛,所以我会尽量简短:

1) 创建 C++ 对象的所有方法是什么?

与 C 相同:它们可以是全局变量、局部自动、局部静态或动态。你可能会对构造函数感到困惑,但简单地认为每次创建对象时都会调用构造函数。总是。哪个构造函数只是创建对象时使用什么参数的问题。

赋值不会创建一个新对象,它只是从一个对象复制到另一个对象,(想想memcpy但更聪明)。

2) 与所有这些类型的对象创建相关的所有不同的初始化语法是什么?T f = x、T f(x);、T f{x};等有什么区别?

  • T f(x)是经典的方式,它只是使用作为参数T的构造函数创建一个类型的对象。x
  • T f{x}是新的 C++11 统一语法,因为它可用于初始化聚合类型(数组等),但除此之外它等同于前者。
  • T f = x这取决于是否x是 type T。如果是,则等价于前者,如果是不同类型,则等价于T f = T(x)。这并不重要,因为允许编译器优化多余的副本(复制省略)。
  • T(x). 你忘了这个。一个临时类型的对象T被创建(使用与上面相同的构造函数),它在代码中发生的任何地方都被使用,并且在当前完整表达式的末尾,它被销毁。
  • T f. T如果可用,这将使用默认构造函数创建一个类型的值。那只是一个不带参数的构造函数。
  • T f{}. 默认构造,但使用新的统一语法。请注意,这T f()不是类型的对象T,而是返回T! 的函数。
  • T(). 使用默认构造函数的临时对象。

3) 最重要的是,什么时候在 C++ 中复制/赋值/whatever = 是正确的,什么时候你想使用指针?

您可以使用与 C 中相同的方法。将复制/分配想象为memcpy. 您也可以传递参考,但您也可以等待一段时间,直到您对这些感到满意为止。你应该做的是:不要使用指针作为辅助局部变量,而是使用引用。

4) 最后,shared_ptr、weak_ptr 等这些东西是什么?

它们是您的 C++ 工具带中的工具。你将不得不通过经验和一些错误来学习......

  • shared_ptr在共享对象的所有权时使用。
  • unique_ptr当对象的所有权是唯一且明确的时使用。
  • weak_ptr用于打破树中的循环shared_ptr。它们不会被自动检测到。
  • vector. 别忘了这个!用它来创建任何东西的动态数组。

PS:你忘了问析构函数。IMO,析构函数赋予了 C++ 个性,所以一定要大量使用它们!

于 2013-06-15T08:25:19.287 回答
6

这是一个相当广泛的问题,但我会给你一个起点。

在 C 中称为“堆栈变量”的东西也称为具有“自动存储”功能的对象。具有自动存储功能的对象的生命周期相当容易理解:它在控制到达其定义的点时创建,然后在超出范围时销毁:

int main() {
  int foo = 5; // creation of automatic storage
  do_stuff();
  foo = 1;

  // end of function; foo is destroyed.
}

现在,需要注意的是,它= 5被认为是初始化语法的一部分,而= 1被认为是赋值操作。我不想让你=因为在语言语法中被用于两种不同的东西而感到困惑。

无论如何,C++ 将自动存储更进一步,并允许在该对象的创建和销毁期间运行任意代码:构造函数和析构函数。这产生了称为RAII的美妙成语,您应该尽可能使用它。使用 RAII,资源管理变得自动化。

什么是 shared_ptr、weak_ptr 等这些东西?

RAII 的好例子。它们允许您将动态资源(malloc/free 调用)视为自动存储对象!

最重要的是,什么时候在 C++ 中复制/分配/whatever = 是正确的,你什么时候想使用指针?在 C 语言中,我非常习惯于到处乱扔指针,因为指针赋值很便宜,但结构复制就不那么简单了。C++ 的复制语义如何影响这一点?

const到处都是引用,尤其是函数参数。constrefs 避免复制并防止修改对象。如果您不能使用constref,则很可能正常的参考是合适的。如果出于某种原因您想要重置引用或将其设置为 null,请使用指针。

创建 C++ 对象的所有方法是什么?直接/复制构造函数、赋值等。它们是如何工作的?

简而言之,所有构造函数都创建对象。赋值不行。为此读一本书。

于 2013-06-15T08:15:31.127 回答
2
  1. 除了显式之外,在 C++ 中创建隐式对象的方法有很多。几乎所有这些都使用对象类的复制构造函数。请记住:隐式复制可能需要T在范围内声明类型的复制构造函数和/或赋值运算符,public具体取决于复制发生的位置。
    所以当然:

    a)在堆栈中显式创建一个全新的对象:

    T object(arg);

b) 显式复制现有对象:

T original(arg);
...
T copy(original);

如果T类没有定义复制构造函数,则编译器创建默认实现。它尝试创建传递对象的精确副本。这并不总是程序员想要的,因此自定义实现有时可能很有用。
c) 在堆中显式创建一个全新的对象:

T *ptr = new T(arg);

d) 隐式创建一个全新的对象,它的构造函数只接受一个参数并且没有explicit修饰符,例如:

class T
{
public:
    T(int x) : i(x) {}
private:
    int i;
}
...
T object = 5; // actually implicit invocation of constructor occurs here

e) 按值传递给函数的对象的隐式复制:

void func(T input)
{
    // here `input` is a copy of an object actually passed
}
...

int main()
{
    T object(arg);
    func(object); // copy constructor of T class is invoked before the `func` is called
}

f) 按值处理的异常对象的隐式复制:

void function()
{
    ...
    throw T(arg); // suppose that exception is always raised in the `function`
    ...
}
...
int main()
{
    ...
    try {
        function();
    } catch (T exception) { // copy constructor of T class is invoked here
        // handling `exception`
    }
    ...
}

g) 使用赋值运算符创建新对象。我没有使用“复制”这个词,因为在这种情况下,特定类型的赋值运算符实现很重要。如果未实现此运算符,则默认实现由编译器创建,顺便说一句,它与默认复制构造函数具有相同的行为。

class T
{
    T(int x) : i(x) {}
    T operator=() const
    {
        return T(*this); // in this implementation we explicitly call default copy constructor
    }
}
...
int main()
{
   ...
   T first(5);
   T second = first; // assingment operator is invoked
   ...
}

嗯,这就是我不用看 Stroustrup 的书就能记住的。可能是错过了什么。
在我写这篇文章的时候,一些答案被接受了,所以我在这一点上停下来。愿我列出的细节有用。

于 2013-06-15T08:44:01.373 回答