5

我有多年的 Java 开发经验,现在我想切换到 C++,我很难理解内存管理系统。

让我用一个小例子来解释一下情况:

据我了解,您可以在堆栈或堆上分配空间。第一个是通过像这样声明一个变量来完成的:

 int a[5]

或者

int size = 10;
int a[size]

相反,如果你想在堆上分配内存,那么你可以使用“new”命令来完成。例如像:

int *a = new int[10]; (notice that I haven't tried all the code, so the syntax might be wrong)

两者之间的一个区别是,如果它在函数完成时分配到堆栈上,那么空间会自动释放,而在另一种情况下,我们必须使用 delete() 显式释放它。

现在,假设我有这样的课程:

class A {
  const int *elements[10];

  public void method(const int** elements) {
    int subarray[10];
    //do something
    elements[0] = subarray;
  }
}

现在,我有几个问题:

  1. 在这种情况下,子数组被分配在堆栈上。为什么函数方法完成后,如果我查看 elements[0] 仍然可以看到子数组的数据?编译器是否翻译了堆分配中的第一个分配(在这种情况下,这是一个好习惯)?
  2. 如果我将子数组声明为“const”,那么编译器不会让我将它分配给元素。为什么不?我认为 const 只涉及无法更改指针,而没有别的。
  3. (这可能很愚蠢)假设我想分配的“元素”不是固定的 10 个元素,而是来自构造函数的参数。是否仍然可以在堆栈中分配它,或者构造函数将它始终分配在堆中?

抱歉这些问题(对于专业的 C 程序员来说可能看起来很傻),但是 C++ 的内存管理系统与 Java 非常不同,我想避免泄漏或代码缓慢。提前谢谢了!

4

6 回答 6

2

A)不,编译器没有翻译它,你也没有冒险进入未定义的行为。要尝试找到与 Java 开发人员相似的地方,请考虑您的函数参数。当你这样做时:

int a = 4;
obj.foo(a);

a当它传递给方法时会发生什么foo?制作了一个副本,将其添加到堆栈帧中,然后当函数返回时,该帧现在用于其他目的。您可以将局部堆栈变量视为参数的延续,因为它们通常被类似地对待,除非调用约定。我认为阅读更多关于堆栈(与语言无关的堆栈)如何工作的信息可以进一步阐明这个问题。

B)你可以标记指针const,或者你可以标记它指向的东西const

int b = 3
const int * const ptr = &b;
^            ^
|            |- this const marks the ptr itself const
| - this const marks the stuff ptr points to const

C)在某些 C++ 标准中可以在堆栈上分配它,但在其他标准中则不行。

于 2013-10-01T17:00:07.953 回答
2

a) 在这种情况下,子数组分配在堆栈上。为什么函数方法完成后,如果我查看 elements[0] 仍然可以看到子数组的数据?编译器是否翻译了堆分配中的第一个分配(在这种情况下,这是一个好习惯)?

它被称为“未定义的行为”,任何事情都可能发生。在这种情况下,subarray保存的值仍然存在,顺便说一句,可能是因为您在函数返回后立即访问了该内存。但是您的编译器也可以在返回之前将这些值清零。您的编译器还可以将喷火龙送到您的家中。任何事情都可能发生在“未定义的行为”中。

b)如果我将子数组声明为“const”,那么编译器不会让我将它分配给元素。为什么不?我认为 const 只涉及无法更改指针,而没有别的。

这是该语言的一个相当不幸的怪癖。考虑

const int * p1; // 1
int const * p2; // 2
int * const p3; // 3
int * p4;       // 4
int const * const p5; // 5

这是所有有效的 C++ 语法。1 表示我们有一个指向const int的可变指针。2 与 1 相同(这是怪癖)。3 表示我们有一个指向可变 int的const 指针。4 表示我们有一个普通的旧可变指针,指向一个可变 int。5 表示我们有一个指向const int的const 指针。规则大致是这样的:从右到左读取 const ,除了最后一个 const,它可以在右边或左边。

c)假设我想分配的“元素”不是固定的 10 个元素,而是来自构造函数的参数。是否仍然可以在堆栈中分配它,或者构造函数将它始终分配在堆中?

如果您需要动态分配,那么这通常会在堆上,但堆栈和堆的概念是依赖于实现的(即无论您的编译器供应商做什么)。

最后,如果您有 Java 背景,那么您需要考虑内存的所有权。例如,在您的方法void A::method(const int**)中,您将指针指向本地创建的内存,而该内存在方法返回后消失。您的指针现在指向没有人拥有的内存。最好将该内存实际复制到一个新区域(例如,该类的数据成员A),然后让您的指针指向内存。此外,虽然 C++ 可以使用指针,但明智的做法是不惜一切代价避免使用它们。例如,在可能和适当的情况下尽量使用引用而不是指针,并使用std::vector任意大小数组的类。这个类也会处理所有权问题,因为将一个向量分配给另一个向量实际上会将所有元素从一个向量复制到另一个向量(现在使用右值引用除外,但暂时忘记这一点)。有些人认为“赤裸裸的”新/删除是不好的编程习惯。

于 2013-10-01T17:22:59.217 回答
1

Java 和 C/C++ 之间的主要区别之一是显式未定义行为 (UB)。UB 的存在是 C/C++ 性能的主要来源。UB 和“不允许”之间的区别在于 UB 是未经检查的,所以任何事情都可能发生。在实践中,当 C/C++ 编译器编译触发 UB 的代码时,编译器将执行任何生成最高性能代码的操作。

大多数时候这意味着“没有代码”,因为你不能比这更快,但有时会有更积极的优化来自 UB 的结论,例如被取消引用的指针不能为 NULL(因为那将是 UB ),因此稍后对 NULL 的检查应始终为 false,因此编译器将正确地决定可以不进行检查。

由于编译器通常也很难识别 UB(并且不是标准要求的),因此“任何事情都可能发生”确实是正确的。

1)根据标准,在您离开范围后取消引用指向自动变量的指针是 UB。为什么这行得通?因为数据仍然存在于您离开它的位置。直到下一个函数调用覆盖它。把它想象成你卖掉了一辆汽车。

2)指针中实际上有两个可能的常量:

int * a;                        // Non const pointer to non const data
int const * b;                  // Non const pointer to const data
int * const c = &someint;       // Const pointer to non const data
int const * const d = &someint; // Const pointer to const data

const之前是*指数据,之后const*指指针本身。

3)不是一个愚蠢的问题。在 C 中,在堆栈上分配具有动态大小的数组是合法的,但在 C++ 中则不是。这是因为在 C 中不需要调用构造函数和析构函数。这是 C++ 中的一个难题,并针对最新的 C++11 标准进行了讨论,但决定保持原样:它不是标准的一部分。

那么为什么它有时会起作用呢?好吧,它适用于 GCC。这是 GCC 的非标准编译器扩展。我怀疑他们只是对 C 和 C++ 使用相同的代码,然后他们“把它留在了那里”。您可以使用 GCC 开关将其关闭,使其以标准方式运行。

于 2013-10-01T18:43:10.550 回答
0

该标准没有讨论堆栈或堆,在这种情况下,您的阵列具有自动存储,在大多数现代系统中将在堆栈上。一旦退出范围然后访问它,保留指向自动对象的指针只是简单的未定义行为。3.7.3 第 1节中的 C++ 标准草案说(强调我的):

显式声明的块范围变量 register 或未显式声明的 static 或 extern 具有自动存储持续时间。这些实体的存储一直持续到创建它们的块退出。

于 2013-10-01T16:58:50.283 回答
0

a) 你看到它是因为它的堆栈空间还没有被回收。随着堆栈的增长和缩小,此内存可能会被覆盖。不要这样做,结果是不确定的!

b) subarray 是一个整数数组,而不是一个指针。如果它是 const,则不能分配给它。

c) 根本不是一个愚蠢的问题。您可以使用新的展示位置来做到这一点。也可以使用变量来对堆栈上的数组进行标注。

于 2013-10-01T17:04:29.603 回答
0

re a):当函数返回时,数据仍然在你放它的地方,在堆栈上。但是在那里访问它是未定义的行为,并且该存储将几乎立即被重用。它肯定会在下一次调用任何函数时被重用。这是使用堆栈的方式所固有的。

于 2013-10-01T17:08:43.783 回答