引用 C++ 语言标准,“存储持续时间是对象的属性,它定义了包含该对象的存储的最小潜在寿命。”基本上,它告诉你一个变量可以使用多长时间。变量可以是基本类型,如int
,也可以是复杂类型,如class
。不管变量的类型如何,只要编程语言认为它应该存在,它就一定会存在。
C++ 管理内存的方式与 C# 非常不同。首先,不需要垃圾收集器,很少有实现提供垃圾收集器。就 C++ 实现具有自动内存管理而言,它们主要通过智能指针和引用计数来实现。C++ 类不会自动存在于堆中(由 GC 管理或其他方式)。相反,它们的工作方式更像 C# 中的结构。
您可以在需要时将 C++ 类实例推送到堆上,但是如果您在本地声明它,并且不做任何有趣的事情,那么它将有一个自动持续时间,通常使用栈来实现,并且当程序离开类存在的范围时,它将被自动销毁。
C++ 比 C# 更能控制内存管理。这样做的后果是,C++ 语言和运行时环境在防止错误代码方面做得不如 C# 语言和 CLR 多。成为一名优秀的 C++ 程序员的关键之一是理解内存管理是如何工作的,并使用最佳实践来编写高效、正确的代码。
全局变量,包括名称空间内部的变量,以及用 duration 关键字static
标记的变量,都有静态存储持续时间。
全局变量在程序初始化期间初始化(即程序实际开始执行您的main
或wmain
功能之前的时期)。它们按照源代码中定义的顺序进行初始化。依赖初始化顺序通常不是一个好主意,因为重构和其他看似无害的更改很容易在程序中引入潜在的错误。
当程序执行第一次到达包含本地静态的块时,本地静态被零初始化。通常,它们将被初始化为它们的指定值,或者通过在该点调用指定的构造器来初始化。除非在非常罕见的情况下,否则在程序到达并执行语句之前,不需要赋值或构造阶段。一旦本地静态初始化,用其声明指定的初始化将永远不再运行。当然,这正是我们对局部静态的期望。如果每次程序到达它的定义行时它都不断地初始化自己,那么它就和非静态的本地一样。
你可以给全局和局部静态赋值,当然除非你也使它们成为const
。
在一个块中,如果定义一个对象时没有使用new
操作符来实例化它,并且没有存储持续时间关键字,则该对象具有自动持续时间,尽管它可以选择使用register
关键字。这意味着对象是在定义时创建的,当程序退出声明其变量的块时,或者当一个新值被赋给其变量时,对象被销毁。
| | 注意:auto 关键字曾经是显式选择自动存储持续时间的一种方式。在 C++ 11 中,这种用法被删除了。现在它相当于 C# 中的 var 关键字。如果您试图使用 auto 的旧含义编译某个东西,您将会收到一个编译器错误,因为 auto 作为类型说明符必须是唯一的类型说明符。 |
动态持续时间是使用new
操作符或new[]
操作符的结果。new
运算符用于分配单个对象,而new[]
运算符用于分配动态数组。您必须跟踪动态分配的数组的大小。虽然 C++ 实现将适当地释放一个动态分配的数组,但是如果您使用delete[]
运算符,就没有简单或可移植的方法来确定该分配的大小。这可能是不可能的。通过delete
操作释放单个对象。
当使用new
或new[]
分配内存时,返回值是一个指针。指针是保存内存地址的变量。在 C# 中,如果您将对某个对象的所有引用都设置为 null 或其他值,那么在您的程序中就无法再访问内存,因此 GC 可以释放该内存用于其他用途。
在 C++ 中,如果您将指向某个对象的所有指针都设置为nullptr
或其他值,并且无法使用指针算法计算出原始地址,那么您就失去了使用delete
或delete[]
运算符释放内存的能力。因此,您造成了内存泄漏。如果一个程序泄漏了足够的内存,最终它会崩溃,因为系统会用完它的内存地址。然而,即使在此之前,计算机的速度也会慢得可怕,因为它被迫增加分页来适应程序不断增加的内存占用(假设它有虚拟内存,而这是大多数智能手机所没有的)。
| | 注意:常量指针,如语句
const wchar_t* someStr = L"Hello World!";
中的someStr
不是动态持续时间指针。那段记忆只是程序本身的一部分。如果你试图在上面调用delete
或delete[]
,程序将会崩溃。然而,字符串是一个字符数组,所以如果可以删除它,那么delete[]
运算符将是正确的用法。 |
在处理动态内存时,为了消除潜在的泄漏并限制其他相关错误的可能性,您应该始终使用智能指针,如std::unique_ptr
或std::shared_ptr
。我们将在涵盖指针的一章中讨论这些。
线程持续时间是最不常用的存储持续时间。只是最近才标准化。截至本文撰写之时,很少有(如果有的话)C++ 编译器供应商实现了对来自 C++ 11 标准的新thread_local
关键字的支持。
这种情况肯定会改变,但是目前您可以使用特定于供应商的扩展,例如特定于微软的扩展 _declspec(thread)
或者特定于海湾合作委员会的扩展 __thread
,如果您需要这种功能的话。
线程持续时间类似于静态持续时间,只是这些变量不是持续程序的生命,而是每个线程的本地变量;线程的副本在线程期间一直存在。线程持续时间对象的每个线程实例在线程中第一次使用时被初始化,当线程退出时被销毁。线程持续时间对象不会从启动它所在线程的线程继承其值。
自动存储持续时间通常是对象存储持续时间的正确形式,除非您需要它们在创建它们的范围内生存。如果是这种情况,您应该选择最适合您需求的剩余存储持续时间之一。
- 如果对象应该在程序执行的整个过程中都存在,请使用静态存储持续时间。
- 如果对象应该存在于特定线程的整个长度,请使用线程存储持续时间。
- 如果对象只存在于程序或线程的部分持续时间内,请使用动态存储持续时间。
如果这样做有意义的话,你可以偏离这些建议,但是在大多数情况下,这个指导会正确地引导你。
包含以下示例有助于澄清这些概念。样本被大量记录,因此不包括附加注释。我强烈建议您构建并运行这个特定的示例。浏览代码时查看输出将帮助您比简单地阅读代码更容易掌握这些概念。
示例:存储比率示例\SomeClass.h
#pragma once
#include <string>
#include <memory>
class SomeClass
{
public:
explicit SomeClass(int value = 0);
SomeClass(
int value,
const wchar_t* stringId
);
~SomeClass(void);
int GetValue(void) { return m_value; }
void SetValue(int value) { m_value = value; }
static std::unique_ptr<SomeClass> s_someClass;
private:
int m_value;
std::wstring m_stringId;
};
示例:存储比率示例\SomeClass.cpp
#include "SomeClass.h"
#include <string>
#include <ostream>
#include <iostream>
#include <ios>
#include <iomanip>
#include <thread>
#include <memory>
using namespace std;
SomeClass::SomeClass(int value) :
m_value(value),
m_stringId(L"(No string id provided.)")
{
SomeClass* localThis = this;
auto addr = reinterpret_cast<unsigned int>(localThis);
wcout << L"Creating SomeClass instance." << endl <<
L"StringId: " << m_stringId.c_str() << L"." << endl <<
L"Address is: '0x" << setw(8) << setfill(L'0') <<
hex << addr << dec << L"'." << endl <<
L"Value is '" << m_value << L"'." << endl <<
L"Thread id: '" <<
this_thread::get_id() << L"'." << endl << endl;
}
SomeClass::SomeClass(
int value,
const wchar_t* stringId
) : m_value(value),
m_stringId(stringId)
{
SomeClass* localThis = this;
auto addr = reinterpret_cast<int>(localThis);
wcout << L"Creating SomeClass instance." << endl <<
L"StringId: " << m_stringId.c_str() << L"." << endl <<
L"Address is: '0x" << setw(8) << setfill(L'0') <<
hex << addr << dec << L"'." << endl <<
L"Value is '" << m_value << L"'." << endl <<
L"Thread id: '" <<
this_thread::get_id() << L"'." << endl << endl;
}
SomeClass::~SomeClass(void)
{
// This is just here to clarify that we aren't deleting a
// new object when we replace an old object with it, despite
// the order in which the Creating and Destroying output is
// shown.
m_value = 0;
SomeClass* localThis = this;
auto addr = reinterpret_cast<unsigned int>(localThis);
wcout << L"Destroying SomeClass instance." << endl <<
L"StringId: " << m_stringId.c_str() << L"." << endl <<
L"Address is: '0x" << setw(8) << setfill(L'0') <<
hex << addr << dec << L"'." << endl <<
L"Thread id: '" <<
this_thread::get_id() << L"'." << endl << endl;
}
// Note that when creating a static member variable, the definition also
// needs to have the type specified. Here, we start off with
// 'unique_ptr<SomeClass>' before proceeding to the
// 'SomeClass::s_someClass = ...;' value assignment.
unique_ptr<SomeClass> SomeClass::s_someClass =
unique_ptr<SomeClass>(new SomeClass(10, L"s_someClass"));
示例:存储比率示例\存储比率示例. cpp
#include <iostream>
#include <ostream>
#include <sstream>
#include <thread>
#include <memory>
#include <cstddef>
#include "SomeClass.h"
#include "../pchar.h"
using namespace std;
struct SomeStruct
{
int Value;
};
namespace Value
{
// Visual C++ does not support thread_local as of VS 2012 RC. We can
// partially mimic thread_local with _declspec(thread), but we cannot
// have things as classes with functions (including constructors
// and destructors) with _declspec(thread).
_declspec(thread) SomeStruct ThreadLocalSomeStruct = {};
// g_staticSomeClass has static duration. It exists until the program
// ends or until a different value is assigned to it. Even if you left
// off the static keyword, in this case it would still be static since
// it is not a local variable, is not dynamic, and is not a thread-
// local variable.
static SomeClass g_staticSomeClass = SomeClass(20, L"g_staticSomeClass");
}
// This method creates a SomeClass instance, and then changes the
// value.
void ChangeAndPrintValue(int value)
{
// Create an identifier string.
wstringstream wsStr(L"");
wsStr << L"ChangeAndPrintValue thread id: '" << this_thread::get_id()
<< L"'";
// Create a SomeClass instance to demonstrate function-level block scope.
SomeClass sc(value, wsStr.str().c_str());
// Demonstrate _declspec(thread).
wcout << L"Old value is " << Value::ThreadLocalSomeStruct.Value <<
L". Thread id: '" << this_thread::get_id() << L"'." << endl;
Value::ThreadLocalSomeStruct.Value = value;
wcout << L"New value is " << Value::ThreadLocalSomeStruct.Value <<
L". Thread id: '" << this_thread::get_id() << L"'." << endl;
}
void LocalStatic(int value)
{
static SomeClass sc(value, L"LocalStatic sc");
//// If you wanted to reinitialize sc every time, you would have to
//// un-comment the following line. This, however, would defeat the
//// purpose of having a local static. You could do something
//// similar if you wanted to reinitialize it in certain circumstances
//// since that would justify having a local static.
//sc = SomeClass(value, L"LocalStatic reinitialize");
wcout << L"Local Static sc value: '" << sc.GetValue() <<
L"'." << endl << endl;
}
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
// Automatic storage; destroyed when this function ends.
SomeClass sc1(1, L"_pmain sc1");
wcout << L"sc1 value: '" << sc1.GetValue() <<
L"'." << endl << endl;
{
// The braces here create a new block. This means that
// sc2 only survives until the matching closing brace, since
// it also has automatic storage.
SomeClass sc2(2, L"_pmain sc2");
wcout << L"sc2 value: '" << sc2.GetValue() <<
L"'." << endl << endl;
}
LocalStatic(1000);
// Note: The local static in LocalStatic will not be reinitialized
// with 5000\. See the function definition for more info.
LocalStatic(5000);
// To demonstrate _declspec(thread) we change the value of this
// thread's Value::ThreadLocalSomeStruct to 20 from its default 0.
ChangeAndPrintValue(20);
// We now create a new thread that automatically starts and
// changes the value of Value::ThreadLocalSomeStruct to 40\. If it
// were shared between threads, then it would be 20 from the
// previous call to ChangeAndPrintValue. But it's not. Instead, it
// is the default 0 that we would expect as a result of this being
// a new thread.
auto thr = thread(ChangeAndPrintValue, 40);
// Wait for the thread we just created to finish executing. Note that
// calling join from a UI thread is a bad idea since it blocks
// the current thread from running until the thread we are calling
// join on completes. For WinRT programming, you want to make use
// of the PPLTasks API instead.
thr.join();
// Dynamic storage. WARNING: This is a 'naked' pointer, which is a very
// bad practice. It is here to clarify dynamic storage and to serve
// as an example. Normally, you should use either
// std::unique_ptr or std::shared_ptr to wrap any memory allocated with
// the 'new' keyword or the 'new[]' keyword.
SomeClass* p_dsc = new SomeClass(1000, L"_pmain p_dsc");
const std::size_t arrIntSize = 5;
// Dynamic storage array. THE SAME WARNING APPLIES.
int* p_arrInt = new int[arrIntSize];
// Note that there's no way to find how many elements arrInt
// has other than to manually track it. Also note that the values in
// arrInt are not initialized (i.e. it's not arrIntSize zeroes, it's
// arrIntSize arbitrary integer values).
for (int i = 0; i < arrIntSize; i++)
{
wcout << L"i: '" << i << L"'. p_arrInt[i] = '" <<
p_arrInt[i] << L"'." << endl;
// Assign a value of i to this index.
p_arrInt[i] = i;
}
wcout << endl;
//// If you wanted to zero out your dynamic array, you could do this:
//uninitialized_fill_n(p_arrInt, arrIntSize, 0);
for (int i = 0; i < arrIntSize; i++)
{
wcout << L"i: '" << i << L"'. p_arrInt[i] = '" <<
p_arrInt[i] << L"'." << endl;
}
// If you forgot this, you would have a memory leak.
delete p_dsc;
//// If you un-commented this, then you would have a double delete,
//// which would crash your program.
//delete p_dsc;
//// If you did this, you would have a program error, which may or may
//// not crash your program. Since dsc is not an array, it should not
//// use the array delete (i.e. delete[]), but should use the non-array
//// delete shown previously.
//delete[] p_dsc;
// You should always set a pointer to nullptr after deleting it to
// prevent any accidental use of it (since what it points to is unknown
// at this point).
p_dsc = nullptr;
// If you forgot this, you would have a memory leak. If you used
// 'delete' instead of 'delete[]' unknown bad things might happen. Some
// implementations will overlook it while others would crash or do who
// knows what else.
delete[] p_arrInt;
p_arrInt = nullptr;
wcout << L"Ending program." << endl;
return 0;
}
对于不方便运行示例的人来说,以下是我从 Windows 8 Release Preview 上的命令提示符运行该示例时获得的输出,该命令提示符是用 Visual Studio 2012 Ultimate RC 在针对 x86 芯片组的 Debug 配置中编译的。如果在您自己的系统上运行,您可能会为地址和线程标识产生不同的值。
Creating SomeClass instance.
StringId: s_someClass.
Address is: '0x009fade8'.
Value is '10'.
Thread id: '3660'.
Creating SomeClass instance.
StringId: g_staticSomeClass.
Address is: '0x013f8554'.
Value is '20'.
Thread id: '3660'.
Creating SomeClass instance.
StringId: _pmain sc1.
Address is: '0x007bfe98'.
Value is '1'.
Thread id: '3660'.
sc1 value: '1'.
Creating SomeClass instance.
StringId: _pmain sc2.
Address is: '0x007bfe70'.
Value is '2'.
Thread id: '3660'.
sc2 value: '2'.
Destroying SomeClass instance.
StringId: _pmain sc2.
Address is: '0x007bfe70'.
Thread id: '3660'.
Creating SomeClass instance.
StringId: LocalStatic sc.
Address is: '0x013f8578'.
Value is '1000'.
Thread id: '3660'.
Local Static sc value: '1000'.
Local Static sc value: '1000'.
Creating SomeClass instance.
StringId: ChangeAndPrintValue thread id: '3660'.
Address is: '0x007bfbf4'.
Value is '20'.
Thread id: '3660'.
Old value is 0\. Thread id: '3660'.
New value is 20\. Thread id: '3660'.
Destroying SomeClass instance.
StringId: ChangeAndPrintValue thread id: '3660'.
Address is: '0x007bfbf4'.
Thread id: '3660'.
Creating SomeClass instance.
StringId: ChangeAndPrintValue thread id: '5796'.
Address is: '0x0045faa8'.
Value is '40'.
Thread id: '5796'.
Old value is 0\. Thread id: '5796'.
New value is 40\. Thread id: '5796'.
Destroying SomeClass instance.
StringId: ChangeAndPrintValue thread id: '5796'.
Address is: '0x0045faa8'.
Thread id: '5796'.
Creating SomeClass instance.
StringId: _pmain p_dsc.
Address is: '0x009fbcc0'.
Value is '1000'.
Thread id: '3660'.
i: '0'. p_arrInt[i] = '-842150451'.
i: '1'. p_arrInt[i] = '-842150451'.
i: '2'. p_arrInt[i] = '-842150451'.
i: '3'. p_arrInt[i] = '-842150451'.
i: '4'. p_arrInt[i] = '-842150451'.
i: '0'. p_arrInt[i] = '0'.
i: '1'. p_arrInt[i] = '1'.
i: '2'. p_arrInt[i] = '2'.
i: '3'. p_arrInt[i] = '3'.
i: '4'. p_arrInt[i] = '4'.
Destroying SomeClass instance.
StringId: _pmain p_dsc.
Address is: '0x009fbcc0'.
Thread id: '3660'.
Ending program.
Destroying SomeClass instance.
StringId: _pmain sc1.
Address is: '0x007bfe98'.
Thread id: '3660'.
Destroying SomeClass instance.
StringId: LocalStatic sc.
Address is: '0x013f8578'.
Thread id: '3660'.
Destroying SomeClass instance.
StringId: g_staticSomeClass.
Address is: '0x013f8554'.
Thread id: '3660'.
Destroying SomeClass instance.
StringId: s_someClass.
Address is: '0x009fade8'.
Thread id: '3660'.