C++ 秘籍:问题解决方法(四)

原文:C++ recipes a problem-solution approach

协议:CC BY-NC-SA 4.0

九、模板

STL 是使用 C++ 提供的一种叫做模板的语言特性编写的。模板提供了一种方法,您可以用它来编写通用代码,这些代码可以在编译时被专门化,以创建具体的函数和不同类型的类。对模板代码的唯一要求是,可以为程序中用于专门化模板的所有类型生成输出。在这一点上,这可能有点难以理解,但是当你读完这一章的时候,你就会明白了。

9-1.创建模板函数

问题

您希望创建一个函数,可以传递不同类型的参数并返回不同类型的值。

解决办法

可以使用方法重载为您希望支持的每种类型提供不同版本的函数,但这仍然会将您限制在所提供类型的函数中。更好的方法是创建一个模板函数,专门用于任何类型。

它是如何工作的

C++ 包括一个模板编译器,可以用来在编译时将通用函数定义转换成具体函数。

创建模板函数

模板允许您在不指定具体类型的情况下编写代码。代码通常包含您希望使用的类型;清单 9-1 显示了在这些正常情况下编写的函数。

清单 9-1 。 非模板功能

#include <iostream>

using namespace std;

int Add(int a, int b)
{
    return a + b;
}

int main(int argc, char* argv[])
{
    const int number1{ 1 };
    const int number2{ 2 };
    const int result{ Add(number1, number2) };

    cout << "The result of adding" << endl;
    cout << number1 << endl;
    cout << "to" << endl;
    cout << number2 << endl;
    cout << "is" << endl;
    cout << result;

    return 0;
}

清单 9-1 中的Add函数是一个标准的 C++ 函数。它接受两个int参数并返回一个int值。您可以提供这个函数的一个float版本,方法是复制这个函数并修改每个对int的引用,以便它使用一个float来代替。然后,您可以对string和您希望该函数支持的任何其他类型进行同样的操作。这种方法的问题是,即使函数体保持不变,也必须为每种类型复制函数。另一种解决方案是使用模板函数。你可以在清单 9-2 中看到Add的模板版本。

**清单 9-2 。**一个Add的模板版本

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

可以看到,Add的模板版本不再使用具体类型int。相反,该函数是在模板块中定义的。template关键字用来告诉编译器下一个代码块应该被当作一个模板。接下来是尖括号部分(< >),它定义了模板使用的任何类型。这个例子定义了一个模板类型,用字符T. T表示,然后用来指定返回类型和传递给函数的两个参数的类型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意将参数作为const引用传递给模板函数是个好主意。最初的Add实现通过值传递int类型,但是不能保证模板不会被在通过值传递时会造成性能损失的类型使用,比如复制的对象。

现在你已经模板化了Add函数,你可以在清单 9-3 中看到main函数中的调用代码与清单 9-1 中显示的代码没有什么不同。

清单 9-3 。 调用模板Add功能

#include <iostream>

using namespace std;

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

int main(int argc, char* argv[])
{
    const int number1{ 1 };
    const int number2{ 2 };
    const int result{ Add(number1, number2) };

    cout << "The result of adding" << endl;
    cout << number1 << endl;
    cout << "to" << endl;
    cout << number2 << endl;
    cout << "is" << endl;
    cout << result;

    return 0;
}

清单 9-3 包含了一个对Add函数的调用,其位置与清单 9-1 中的代码完全相同。这是可能的,因为编译器可以隐式地计算出与模板一起使用的正确类型。

显式与隐式模板专门化

有时,您希望明确模板可以使用的类型。清单 9-4 显示了一个显式模板专门化的例子。

清单 9-4 。 显性和隐性模板特殊化

#include <iostream>

using namespace std;

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
    cout << "The result of adding" << endl;
    cout << value1 << endl;
    cout << "to" << endl;
    cout << value2 << endl;
    cout << "is" << endl;
    cout << result;

    cout << endl << endl;
}

int main(int argc, char* argv[])
{
    const int number1{ 1 };
    const int number2{ 2 };
    const int intResult{ Add(number1, number2) };
    Print(number1, number2, intResult);

    const float floatResult{ Add(static_cast<float>(number1), static_cast<float>(number2)) };
    Print<float>(number1, number2, floatResult);

    return 0;
}

清单 9-4 添加了一个带三个模板化参数的模板Print函数。该函数在main函数中被调用两次。第一次是隐式推导模板类型。这是可能的,因为传递给函数的三个参数都是类型int;因此,编译器认为您打算调用模板的一个int版本。对Print的第二个调用是显而易见的。这是通过在函数名后面添加包含要使用的类型的尖括号(在本例中是float)来实现的。由于传递给函数的变量类型不同,这是必要的。这里number1number2都是int类型,但是floatResultfloat类型;因此,编译器无法推断出模板使用的正确类型。当我尝试使用隐式专用化编译此代码时,Visual Studio 生成了以下错误:

error C2782: 'void Print(const T &,const T &,const T &)' : template parameter 'T' is ambiguous

9-2.部分专门化模板

问题

你有一个不能用特定类型编译的模板函数。

解决办法

您可以使用部分模板专门化来创建模板重载。

它是如何工作的

模板函数体包含需要隐式属性的代码,这些隐式属性来自用于专门化该模板的类型。考虑清单 9-5 中的代码。

清单 9-5 。 模板功能

#include <iostream>

using namespace std;

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
    cout << "The result of adding" << endl;
    cout << value1 << endl;
    cout << "to" << endl;
    cout << value2 << endl;
    cout << "is" << endl;
    cout << result;

    cout << endl << endl;
}

int main(int argc, char* argv[])
{
    const int number1{ 1 };
    const int number2{ 2 };
    const int intResult{ Add(number1, number2) };
    Print(number1, number2, intResult);

    return 0;
}

这段代码需要来自Add函数和Print函数使用的类型的两个隐式属性。Add功能要求使用的类型也可以与+操作符一起使用。Print函数要求使用的类型可以传递给<<操作符。main函数使用这些带有int变量的函数,因此这两个条件都满足。如果您要对自己创建的类使用AddPrint,那么编译器很可能无法使用带有+<<操作符的类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意这种情况下“合适的”解决方案是添加重载的+<<操作符,这样原始代码就能按预期工作。这个例子展示了如何使用部分专门化来达到同样的结果。

你可以很容易地更新清单 9-5 中的来使用一个简单的类,如清单 9-6 中的所示。

清单 9-6 。 使用带类的模板

#include <iostream>

using namespace std;

class MyClass
{
private:
    int m_Value{ 0 };

public:
    MyClass() = default;

    MyClass(int value)
        : m_Value{ value }
    {

    }

    MyClass(int number1, int number2)
        : m_Value{ number1 + number2 }
    {

    }

    int GetValue() const
    {
        return m_Value;
    }
};

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
    cout << "The result of adding" << endl;
    cout << value1 << endl;
    cout << "to" << endl;
    cout << value2 << endl;
    cout << "is" << endl;
    cout << result;

    cout << endl << endl;
}

int main(int argc, char* argv[])
{
    const MyClass number1{ 1 };
    const MyClass number2{ 2 };
    const MyClass intResult{ Add(number1, number2) };
    Print(number1, number2, intResult);

    return 0;
}

清单 9-6 中的代码无法编译。你的编译器将找不到合适的操作符来为+<<使用MyClass类型。你可以通过使用部分模板专门化来解决这个问题,如清单 9-7 所示。

清单 9-7 。 使用分部分项模板特殊化

#include <iostream>

using namespace std;

class MyClass
{
private:
    int m_Value{ 0 };

public:
    MyClass() = default;

    MyClass(int value)
        : m_Value{ value }
    {

    }

    MyClass(int number1, int number2)
        : m_Value{ number1 + number2 }
    {

    }

    int GetValue() const
    {
        return m_Value;
    }
};

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

template <>
MyClass Add(const MyClass& myClass1, const MyClass& myClass2)
{
    return MyClass(myClass1.GetValue(), myClass2.GetValue());
}

template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
    cout << "The result of adding" << endl;
    cout << value1 << endl;
    cout << "to" << endl;
    cout << value2 << endl;
    cout << "is" << endl;
    cout << result;

    cout << endl << endl;
}

template <>
void Print(const MyClass& value1, const MyClass& value2, const MyClass& result)
{
    cout << "The result of adding" << endl;
    cout << value1.GetValue() << endl;
    cout << "to" << endl;
    cout << value2.GetValue() << endl;
    cout << "is" << endl;
    cout << result.GetValue();

    cout << endl << endl;
}

int main(int argc, char* argv[])
{
    const MyClass number1{ 1 };
    const MyClass number2{ 2 };
    const MyClass intResult{ Add(number1, number2) };
    Print(number1, number2, intResult);

    return 0;
}

清单 9-7 中的代码增加了AddPrint的特殊版本。它通过在函数签名中使用一个空的模板类型说明符和具体的MyClass类型来实现。您可以在Add函数中看到这一点,这里传递的参数属于MyClass类型,返回值属于MyClass类型。部分专门化的Print函数也将const引用传递给MyClass变量。模板函数仍然可以和变量一起使用,比如int s 和float s,但是现在也明确支持MyClass类型。

为了完整起见,清单 9-8 显示了一个优选的实现,它增加了对+<<操作符和MyClass的支持。

清单 9-8 。 增加+<<操作员支持到MyClass

#include <iostream>

using namespace std;

class MyClass
{
    friend ostream& operator <<(ostream& os, const MyClass& myClass);

private:
    int m_Value{ 0 };

public:
    MyClass() = default;

    MyClass(int value)
        : m_Value{ value }
    {

    }

    MyClass(int number1, int number2)
        : m_Value{ number1 + number2 }
    {

    }

    MyClass operator +(const MyClass& other) const
    {
        return m_Value + other.m_Value;
    }
};

ostream& operator <<(ostream& os, const MyClass& myClass)
{
    os << myClass.m_Value;
    return os;
}

template <typename T>
T Add(const T& a, const T& b)
{
    return a + b;
}

template <typename T>
void Print(const T& value1, const T& value2, const T& result)
{
    cout << "The result of adding" << endl;
    cout << value1 << endl;
    cout << "to" << endl;
    cout << value2 << endl;
    cout << "is" << endl;
    cout << result;

    cout << endl << endl;
}

int main(int argc, char* argv[])
{
    const MyClass number1{ 1 };
    const MyClass number2{ 2 };
    const MyClass intResult{ Add(number1, number2) };
    Print(number1, number2, intResult);

    return 0;
}

这段代码直接为MyClass添加了对+操作符的支持。还为与ostream类型一起工作的<<操作符指定了一个功能。这是因为coutostream(代表输出流)兼容。该函数签名作为MyClassfriend添加,以便函数可以从MyClass访问内部数据。您也可以保留GetValue访问器,而不添加操作符作为friend函数。

9-3.创建课程模板

问题

您希望创建一个可以存储不同类型变量的类,而无需复制所有代码。

解决办法

C++ 允许创建支持抽象类型的模板类。

它是如何工作的

您可以使用template说明符将class定义为模板。template说明符将类型和值作为编译器用来构建模板代码专门化的参数。清单 9-9 展示了一个使用抽象类型和值来构建模板类的例子。

清单 9-9 。 创建模板类

#include <iostream>

using namespace std;

template <typename T, int numberOfElements>
class MyArray
{
private:
    T m_Array[numberOfElements];

public:
    MyArray()
        : m_Array{}
    {

    }

    T& operator[](const unsigned int index)
    {
        return m_Array[index];
    }
};

int main(int argc, char* argv[])
{
    const unsigned int ARRAY_SIZE{ 5 };
    MyArray<int, ARRAY_SIZE> myIntArray;
    for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
    {
        myIntArray[i] = i;
    }

    for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
    {
        cout << myIntArray[i] << endl;
    }

    cout << endl;

    MyArray<float, ARRAY_SIZE> myFloatArray;
    for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
    {
        myFloatArray[i] = static_cast<float>(i)+0.5f;
    }

    for (unsigned int i{ 0 }; i < ARRAY_SIZE; ++i)
    {
        cout << myFloatArray[i] << endl;
    }

    return 0;
}

class MyArray创建一个类型为T的 C 风格数组和一些元素。这两者在编写类时是抽象的,在代码中使用它们时是指定的。您现在可以使用MyArray类来创建一个任意大小的数组,其中包含任意数量的元素,这些元素可以用一个int来表示。你可以在main函数中看到这一点,其中MyArray class模板专门创建了一个int的数组和一个float的数组。图 9-1 显示了运行这段代码时生成的输出:这两个数组包含不同类型的变量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1 。运行清单 9-9 中的代码生成的输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意数组模板包装器的创建是一个简单的例子,展示了 STL 提供的std::array模板的基础。STL 版本支持 STL 迭代器和算法,是比自己编写实现更好的选择。

9-4.创建单件

问题

您有一个系统,您想创建一个可以从应用程序的许多地方访问的实例。

解决办法

你可以使用模板创建一个Singleton基类 。

它是如何工作的

singleton 的基础是一个类模板。Singleton类模板包含一个指向抽象类型的static指针,可以用来表示你喜欢的任何类型的类。使用static指针的副产品是可以从程序的任何地方访问类的实例。您应该小心不要滥用它,尽管它可能是一个有用的属性。清单 9-10 展示了如何创建和使用Singleton模板。

清单 9-10 。Singleton模板

#include <cassert>
#include <iostream>

using namespace std;

template <typename T>
class Singleton
{
private:
    static T* m_Instance;

public:
    Singleton()
    {
        assert(m_Instance == nullptr);
        m_Instance = static_cast<T*>(this);
    }

    virtual ~Singleton()
    {
        m_Instance = nullptr;
    }

    static T& GetSingleton()
    {
        return *m_Instance;
    }

    static T* GetSingletonPtr()
    {
        return m_Instance;
    }
};

template <typename T>
T* Singleton<T>::m_Instance = nullptr;

class Manager
    : public Singleton < Manager >
{
public:
    void Print() const
    {
        cout << "Singleton Manager Successfully Printing!";
    }
};

int main(int argc, char* argv[])
{
    new Manager();
    Manager& manager{ Manager::GetSingleton() };
    manager.Print();
    delete Manager::GetSingletonPtr();

    return 0;
}

清单 9-10 中的Singleton类是一个模板类,它包含一个指向抽象类型 t 的私有静态指针。Singleton构造函数将this的造型赋给m_Instance变量。以这种方式使用static_cast是可能的,因为您知道对象的类型将是提供给模板的类型。该类的虚拟析构函数负责将m_Instance设置回nullptr;还有对实例的引用和指针访问器。

清单 9-10 然后使用这个模板创建一个支持SingletonManager类。它通过创建一个继承自Singleton的类并将其自身类型传递给Singleton模板参数来实现这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意将一个类的类型传递到该类派生自的模板中被称为奇怪的递归模板模式

main函数使用new关键字创建一个ManagerManager不是作为类的引用或指针存储的。虽然您可以这样做,但是从这一点来看,最好简单地使用Singleton的访问器。您可以通过使用带有派生类名称的静态函数语法来实现这一点。main函数通过调用Manager::GetSingleton函数创建对Manager实例的引用。

通过对由Manager::GetSingletonPtr返回的值调用delete来删除单例实例。这会导致调用~Singleton,这将清除存储在m_Instance中的地址,并释放用于存储实例的内存。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 这个Singleton类是基于 Scott Bilas 在游戏编程宝石 (Charles River Media,2000)中最初写的实现。

9-5.编译时计算值

问题

您需要计算复杂的值,并且希望避免在运行时计算它们。

解决办法

模板元编程利用 C++ 模板编译器在编译时计算值,并为用户节省运行时性能。

它是如何工作的

模板元编程可能是一个很难理解的话题。这种复杂性来自 C++ 模板编译器的能力范围。除了让您通过从函数和类中抽象类型来执行泛型编程之外,模板编译器还可以计算值。

散列数据是比较两组数据是否相等的常用方法。它的工作原理是在创建时创建数据的散列,并将散列与数据的运行时版本进行比较。您可以使用此方法在程序执行时检测数据文件的可执行文件中的更改。SDBM 散列是一个易于实现的散列函数;清单 9-11 显示了 SDBM 散列算法 的一个普通函数实现。

***清单 9-11 。***SDBM 哈希算法

#include <iostream>
#include <string>

using namespace std;

unsigned int SDBMHash(const std::string& key)
{
    unsigned int result{ 0 };

    for (unsigned int character : key)
    {
        result = character + (result << 6) + (result << 16) - result;
    }

    return result;
}

int main(int argc, char* argv[])
{
    std::string data{ "Bruce Sutherland" };
    unsigned int sdbmHash{ SDBMHash(data) };

    cout << "The hash of " << data << " is " << sdbmHash;

    return 0;
}

清单 9-11 中的SDBMHash函数的工作方式是迭代提供的数据,并通过将数据集中的每个字节处理成一个result变量来计算结果。这个功能版本的SDBMHash对于创建运行时加载的数据的散列是有用的,但是这里提供的数据在编译时是已知的。通过用模板元程序替换这个函数,可以优化程序的执行速度。清单 9-12 就是这么做的。

清单 9-12 。 用模板元程序替换SDBMHash

#include <iostream>

using namespace std;

template <int stringLength>
struct SDBMCalculator
{
    constexpr static unsigned int Calculate(const char* const stringToHash, unsigned int& value)
    {
        unsigned int character{
            SDBMCalculator<stringLength - 1>::Calculate(stringToHash, value)
        };
        value = character + (value << 6) + (value << 16) - value;
        return stringToHash[stringLength - 1];
    }

    constexpr static unsigned int CalculateValue(const char* const stringToHash)
    {
        unsigned int value{};
        unsigned int character{ SDBMCalculator<stringLength>::Calculate(stringToHash, value) };
        value = character + (value << 6) + (value << 16) - value;
        return value;
    }
};

template<>
struct SDBMCalculator < 1 >
{
    constexpr static unsigned int Calculate(const char* const stringToHash, unsigned int& value)
    {
        return stringToHash[0];
    }
};

constexpr unsigned int sdbmHash{ SDBMCalculator<16>::CalculateValue("Bruce Sutherland") };

int main(int argc, char* argv[])
{
    cout << "The hash of Bruce Sutherland is " << sdbmHash << endl;

    return 0;
}

您可以立即看到清单 9-12 中的代码看起来比清单 9-11 中的代码复杂得多。编写模板元程序所需的语法不是最容易读懂的。main函数现在是单行代码。哈希值存储在一个常量中,不调用任何模板函数。您可以通过在模板函数中放置断点并运行程序的发布版本来测试这一点。

清单 9-12 中的模板元程序通过使用递归来工作。要散列的数据的长度被提供给模板参数,并且可以在初始化sdbmHash变量时看到。这里,16传递给模板,就是字符串“Bruce Sutherland ”的长度。模板编译器认识到它已经被提供了可以在编译时评估的数据,因此它自动调用CalculateValue函数中的Calculate元程序函数。这种递归一直发生,直到碰到终止符。终止符是Calculate的部分专门化版本,一旦要散列的数据长度为 1,就会被调用。当到达终止符时,递归调用开始展开,编译器最终将模板元程序的结果存储在sdbmHash变量中。您可以使用调试版本看到模板元程序的运行。编译器不会在调试版本中优化模板元程序,调试版本允许您测试代码并单步执行以查看结果。图 9-2 显示了运行清单 9-12 中代码的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2 。由清单 9-12 中的代码生成的输出,显示了字符串“Bruce Sutherland”的 SDBM 散列

十、内存

在现代计算机中,内存是一种非常重要的资源。你的程序所操作的所有数据都会在某个时候存储到 ram 中,供处理器在以后需要完成你的部分算法时检索。

因此,对于 C++ 程序员来说,理解程序如何以及何时使用不同类型的内存是至关重要的。本章介绍了三种不同的内存空间,如何利用它们,以及每种空间对程序性能的潜在影响。

10-1.使用静态内存

问题

您有一个希望能够在代码中的任何地方访问的对象。

解决办法

静态内存可以被认为是全局变量。程序的任何部分都可以随时访问这些变量及其值。

它是如何工作的

您使用的编译器会自动为您创建的任何全局变量在静态内存空间中添加内存。静态变量的地址通常可以在可执行文件的地址空间中找到,因此可以被程序的任何部分随时访问。清单 10-1 显示了一个无符号整数全局变量的例子。

清单 10-1 。一个全局变量

#include <iostream>
using namespace std;

unsigned int counter{ 0 };

void IncreaseCounter()
{
    counter += 10;
    cout << "counter is " << counter << endl;
}

int main(int argc, char* argv[])
{
    counter += 5;
    cout << "counter is " << counter << endl;

    IncreaseCounter();

    return 0;
}

清单 10-1 中的变量counter是用全局范围声明的。结果是可以在程序中全局访问该变量。您可以在main函数和IncreaseCounter函数中看到这一点。这两个函数都增加了同一个全局counter变量的值。图 10-1 所示的结果证实了这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-1 。显示更改全局变量的结果的输出

全局变量在某些情况下可能是有用的,但在其他情况下可能会导致许多问题。配方 9-4 展示了使用静态类成员变量来创建一个Singleton对象。静态成员也是一种全局变量,因此可以从程序中的任何地方访问。静态变量的一个普遍问题是它们的创建顺序。C++ 标准不保证静态变量会以给定的顺序初始化。这可能导致使用许多依赖全局变量的程序遇到问题,并由于意外的初始化顺序而崩溃。全局变量还会导致多线程编程中的许多问题,因为多个线程可以同时访问静态地址空间,并产生意想不到的结果。通常建议您将全局变量的使用保持在最低限度。

10-2.使用堆栈内存

问题

您需要内存来存储临时变量,以便在函数中工作。

解决办法

C++ 程序可以使用一个增长和收缩的堆栈来为局部变量提供临时空间。

它是如何工作的

因为 C++ 程序中的所有变量都需要内存支持,所以会为函数中定义的变量动态创建临时空间。这是使用堆栈实现的。当调用一个函数时,编译器会添加机器码,分配足够的堆栈空间来存储函数所需的所有变量。

使用两个名为esp的寄存器(在基于 x86 的 CPU 上)来操作堆栈,ebp. esp是堆栈指针,ebp是基址指针。基址指针用于存储前一个堆栈帧的地址。这允许当前函数在执行结束时返回到正确的堆栈。esp寄存器用于存储堆栈的当前顶部;这允许在当前函数调用另一个函数时更新ebp

在程序栈上为局部变量创建足够空间的过程如清单 10-2 所示。

清单 10-2 。显示创建 20 字节堆栈帧的 x86 程序集

push ebp
mov ebp, esp
sub esp 20

清单 10-2 中的三行 x86 汇编语言展示了在 x86 中创建堆栈框架的基础。首先,push指令用于将当前基址指针移动到堆栈上。push指令将esp向下移动足够远,以存储ebp的值,然后将该值移动到堆栈上。然后将esp的当前值移入ebp,将基址指针向上移动到当前堆栈帧的开头。最后一条指令从esp中减去堆栈帧的大小。由此可以清楚地看出,基于 x86 的计算机中的堆栈向下增长到 0。

然后,程序使用从基指针的偏移量来访问堆栈中的每个变量。在图 10-2 所示的 Visual Studio 反汇编中可以看到这三行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-2 。从 x86 程序反汇编,显示堆栈框架的创建

清单 10-3 显示了图 10-2 中的拆卸的代码。

清单 10-3 。用于查看反汇编的简单程序

#include <iostream>

using namespace std;

void Function()
{
    int a{ 0 };

    cout << a;
}

int main(int argc, char* argv[])
{
    Function();

    return 0;
}

您创建的所有局部变量都分配在堆栈上。类变量的构造函数在它们被创建时被调用,它们的析构函数在栈被销毁时被调用。清单 10-4 展示了一个简单的程序,它由一个class 和一个构造函数和一个析构函数组成。

**清单 10-4 。**堆栈上的类变量

#include <iostream>

using namespace std;

class MyClass
{
public:
    MyClass()
    {
        cout << "Constructor called!" << endl;
    }

    ~MyClass()
    {
        cout << "Destructor called!" << endl;
    }
};

int main(int argc, char* argv[])
{
    MyClass myClass;

    cout << "Function body!" << endl;

    return 0;
}

清单 10-4 中变量myClass的构造函数在初始化时被调用。执行函数体的其余部分,当变量超出范围时,调用class析构函数。myClass变量在return语句后超出范围。发生这种情况是因为可能需要函数中的局部变量来计算函数返回值。你可以在图 10-3 中看到清单 10-4 的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-3 。运行清单 10-4 中代码的输出

清单 10-4 中的代码展示了函数中class变量的创建和销毁。在 C++ 中也可以控制堆栈框架的创建。您可以使用花括号在现有范围内创建一个新的范围。清单 10-5 创建了几个不同的作用域,每个作用域都有自己的局部变量。

清单 10-5 。创建多个范围

#include <iostream>

using namespace std;

class MyClass
{
private:
    static int m_Count;
    int m_Instance{ -1 };

public:
    MyClass()
        : m_Instance{m_Count++}
    {
        cout << "Constructor called on " << m_Instance << endl;
    }

    ~MyClass()
    {
        cout << "Destructor called on " << m_Instance << endl;
    }
};

int MyClass::m_Count{ 0 };

int main(int argc, char* argv[])
{
    MyClass myClass1;

    {
        MyClass myClass2;

        {
            MyClass myClass3;
        }
    }

    return 0;
}

清单 10-5 中的代码展示了在一个函数中使用花括号创建多个堆栈框架。类MyClass包含一个static变量m_Count,用于跟踪不同的实例。每次创建一个新实例时,这个变量都会后递增,前递增的值存储在m_Instance中。每次关闭作用域时,都会对局部变量调用析构函数。结果如图 10-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-4 。显示具有多个范围的对象的销毁顺序的输出

10-3.使用堆内存

问题

您需要创建一个比单个本地作用域更长的大型内存池。

解决办法

C++ 提供了newdelete操作符,允许您管理大型动态分配内存池。

它是如何工作的

动态分配内存对于许多长时间运行的程序来说非常重要。对于允许用户生成自己的内容或从文件中加载资源的程序来说,这是必不可少的。如果不使用动态分配的内存,通常很难(如果不是不可能的话)为用于流式视频或社交媒体内容的程序(如 web 浏览器)提供足够的内存,因为您无法在创建程序时确定内存需求。

您可以使用 C++ newdelete操作符在一个通常称为的地址空间中分配动态内存。new操作符返回一个指针,指向动态分配的内存,该内存足够大,可以存储正在创建的变量类型。清单 10-6 展示了如何使用newdelete操作符。

清单 10-6 。使用newdelete

#include <iostream>

using namespace std;

int main(int argc, char* argv[])
{
    int* pInt{ new int };
    *pInt = 100;

    cout << hex << "The address at pInt is " << pInt << endl;
    cout << dec << "The value at pInt is " << *pInt << endl;

    delete pInt;
    pInt = nullptr;

    return 0;
}

这段代码使用new操作符分配足够的内存来存储单个int变量。指针从new返回并存储在变量pInt中。返回的内存是未初始化的,通常在创建时初始化内存是个好主意。你可以在main中看到这一点,这里使用指针解引用操作符将pInt指向的内存初始化为 100。

一旦从堆中分配了内存,您就有责任确保它被正确地返回给操作系统。否则会导致内存泄漏。内存泄漏会给用户带来问题,通常会导致计算机性能下降、内存碎片,在严重的情况下,还会导致计算机因内存不足而崩溃。

使用delete操作符将堆内存返回给操作系统。这个操作符告诉系统,您不再需要从最初调用new返回的所有内存。在调用了delete之后,你的程序不应该再试图使用new返回的内存。这样做会导致未定义的行为,这通常会导致程序崩溃。由于访问被释放的内存而导致的崩溃通常很难发现,因为它们出现在你无法以任何方式链接到违规代码的地方。通过将任何指向内存的指针设置为nullptr,可以确保你的程序不会访问被删除的内存。

清单 10-6 的输出如图图 10-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-5 。来自清单 10-6 的输出显示了动态分配内存中存储的地址和值

清单 10-6 中的newdelete操作符用于分配单个对象。还有newdelete数组操作符,用于分配同一个对象的倍数。清单 10-7 显示了数组newdelete操作符的作用。

清单 10-7 。数组newdelete运算符

#include <iostream>

using namespace std;

class MyClass
{
private:
    int m_Number{ 0 };

public:
    MyClass() = default;
    ~MyClass()
    {
        cout << "Destroying " << m_Number << endl;
    }

    void operator=(const int value)
    {
        m_Number = value;
    }
};

int main(int argc, char* argv[])
{
    const unsigned int NUM_ELEMENTS{ 5 };
    MyClass* pObjects{ new MyClass[NUM_ELEMENTS] };
    pObjects[0] = 100;
    pObjects[1] = 45;
    pObjects[2] = 31;
    pObjects[3] = 90;
    pObjects[4] = 58;

    delete[] pObjects;
    pObjects = nullptr;

    return 0;
}

清单 10-7 中的代码创建了一个对象数组。MyClass类由一个重载的赋值操作符和一个析构函数组成,前者初始化创建的对象,后者显示数组中元素的销毁顺序。在对象数组上使用标准的delete操作符会给你的程序带来各种问题,因为标准的delete操作符只在数组的第一个元素上调用类析构函数。如果您的类分配了自己的内存,那么数组中的每个后续对象都会泄漏它们的内存。使用delete数组操作符可以确保数组中的每个析构函数都被调用。你可以看到数组中元素的每个析构函数都在图 10-6 中被调用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-6 。使用数组运算符delete时,输出显示每个析构函数都已被调用

10-4.使用自动共享内存

问题

您有一个可以由多个具有不同生命周期的系统共享的对象。

解决办法

C++ 提供了shared_ptr模板,可以在不再需要内存时自动删除它。

它是如何工作的

C++ 中动态分配的内存必须由程序员删除。这意味着你有责任确保你的程序在任何时候都像用户期望的那样运行。C++ 提供了shared_ptr模板,它跟踪你的程序中有多少地方共享对同一个内存的访问,并且可以在不再需要这个内存时删除它。清单 10-8 展示了如何创建一个共享指针。

清单 10-8 。创建共享指针

#include <iostream>
#include <memory>

using namespace std;

class MyClass
{
private:
    int m_Number{ 0 };

public:
    MyClass(int value)
        : m_Number{ value }
    {

    }

    ~MyClass()
    {
        cout << "Destroying " << m_Number << endl;
    }

    void operator=(const int value)
    {
        m_Number = value;
    }

    int GetNumber() const
    {
        return m_Number;
    }
};

using SharedMyClass = shared_ptr< MyClass >;

int main(int argc, char* argv[])
{
    SharedMyClass sharedMyClass{ new MyClass(10) };

    return 0;
}

这段代码包含一个类MyClass,它有一个私有的整数成员变量。还有一个类型别名用来表示一个shared_ptr到一个MyClass对象。从长远来看,这种类型别名用于使编写代码变得更容易和更易维护。shared_ptr模板本身接受一个对象类型的参数,您希望在您的程序中共享这个对象。在这种情况下,您想要共享类型为MyClass的动态对象。

main函数的第一行创建了一个SharedMyClass的实例。这个实例用一个动态分配的MyClass对象初始化。MyClass对象本身用值 10 初始化。在main的主体中唯一的其他代码是return语句。尽管如此,图 10-7 显示MyClass的析构函数已经在sharedMyClass中存储的对象上被调用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-7 。输出显示MyClass析构函数已经在清单 10-8 中被调用

一旦shared_ptr的最后一个实例超出范围,shared_ptr模板会自动调用它所包装的内存中的delete。在这种情况下,main函数中只有一个shared_ptr;因此MyClass对象被删除,它的析构函数在函数返回语句执行后被调用。

清单 10-9 展示了如何使用shared_ptr将共享内存的所有权从一个函数转移到另一个函数,并且仍然保持这个自动清理代码。

清单 10-9 。在函数间转移动态内存

#include <iostream>
#include <memory>

using namespace std;

class MyClass
{
private:
    int m_Number{ 0 };

public:
    MyClass(int value)
        : m_Number{ value }
    {

    }

    ~MyClass()
    {
        cout << "Destroying " << m_Number << endl;
    }

    void operator=(const int value)
    {
        m_Number = value;
    }

    int GetNumber() const
    {
        return m_Number;
    }
};

using SharedMyClass = shared_ptr< MyClass >;

void ChangeSharedValue(SharedMyClass sharedMyClass)
{
    if (sharedMyClass != nullptr)
    {
        *sharedMyClass = 100;
    }
}

int main(int argc, char* argv[])
{
    SharedMyClass sharedMyClass{ new MyClass(10) };

    ChangeSharedValue(sharedMyClass);

    return 0;
}

清单 10-9 创建一个SharedMyClass实例,指向一个用值 10 初始化的MyClass对象。然后,sharedMyClass实例通过值传递给ChangeSharedValue函数。通过值传递一个shared_ptr来复制指针。现在您有了两个SharedMyClass模板的实例,它们都指向同一个MyClass实例。直到的两个shared_ptr实例都超出范围,才会调用MyClass的析构函数。图 10-8 显示了MyClass实例的初始值被改变了,并且析构函数只被调用了一次。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-8 。显示共享对象的存储值被更改和销毁一次的输出

10-5.创建单实例动态对象

问题

您有一个想要传递的对象,但是您只希望有该对象的一个实例。

解决办法

C++ 提供了unique_ptr模板,允许一个指针实例被转移但不能共享。

它是如何工作的

unique_ptr是一个模板,可以用来存储动态分配内存的指针。它与shared_ptr的不同之处在于,一次只能有一个对动态内存的引用。清单 10-10 展示了如何创建一个unique_ptr

清单 10-10 。创建一个unique_ptr

#include <iostream>
#include <memory>

using namespace std;

class MyClass
{
private:
    int m_Number{ 0 };

public:
    MyClass(int value)
        : m_Number{ value }
    {

    }

    ~MyClass()
    {
        cout << "Destroying " << m_Number << endl;
    }

    void operator=(const int value)
    {
        m_Number = value;
    }

    int GetNumber() const
    {
        return m_Number;
    }
};

using UniqueMyClass = unique_ptr< MyClass >;

void CreateUniqueObject()
{
    UniqueMyClass uniqueMyClass{ make_unique<MyClass>(10) };
}

int main(int argc, char* argv[])
{
    cout << "Begin Main!" << endl;

    CreateUniqueObject();

    cout << "Back in Main!" << endl;

    return 0;
}

清单 10-10 中的unique_ptr是在一个函数中创建的,用来演示当unique_ptr超出作用域时,动态创建的对象的实例被销毁。你可以在图 10-9 的输出中看到这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-9 。显示存储在unique_ptr中的动态分配对象的销毁的输出

清单 10-10 展示了unique_ptr可以用来在不再需要时自动删除动态分配的内存。它没有显示出unique_ptr可以用来在不同的作用域之间转移单个对象的所有权。这显示在清单 10-11 中。

清单 10-11 。在unique_ptr实例之间转移动态分配的内存

#include <iostream>
#include <memory>

using namespace std;

class MyClass
{
private:
    int m_Number{ 0 };

public:
    MyClass(int value)
        : m_Number{ value }
    {

    }

    ~MyClass()
    {
        cout << "Destroying " << m_Number << endl;
    }

    void operator=(const int value)
    {
        m_Number = value;
    }

    int GetNumber() const
    {
        return m_Number;
    }
};

using UniqueMyClass = unique_ptr< MyClass >;

void CreateUniqueObject(UniqueMyClass& referenceToUniquePtr)
{
    UniqueMyClass uniqueMyClass{ make_unique<MyClass>(10) };

    cout << hex << showbase;
    cout << "Address in uniqueMyClass " << uniqueMyClass.get() << endl;

    referenceToUniquePtr.swap(uniqueMyClass);

    cout << "Address in uniqueMyClass " << uniqueMyClass.get() << endl;
}

int main(int argc, char* argv[])
{
    cout << "Begin Main!" << endl;

    UniqueMyClass uniqueMyClass;
    CreateUniqueObject(uniqueMyClass);

    cout << "Address in main's uniqueMyClass " << uniqueMyClass.get() << endl;

    cout << dec << noshowbase << "Back in Main!" << endl;

    return 0;
}

清单 10-11 中的代码在CreateUniqueObject函数中创建了一个MyClass的实例。这个函数还引用了另一个 un qiue_ptr<MyClass>,这个 un【】用于将动态分配的对象转移到函数之外。使用由联合国ique_ptr模板提供的swap函数 来实现传输。当所有的UniqueMyClass实例都超出范围时,在main函数的末尾调用MyClass析构函数。你可以在图 10-10 中看到MyClass实例的内存转移和销毁顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-10 。输出显示了一个unique_ptr的传输和它的动态分配内存的销毁

10-6.创建智能指针

问题

您希望在不支持shared_ptrunique_ptr的系统上使用自动化指针管理。

解决办法

您可以在class中使用成员变量来跟踪当前有多少对数据的引用在使用中。

它是如何工作的

在 C++11 中,unique_ptrshared_ptr模板被添加到 STL 中。有些程序是在无法访问 C++11 或者无法访问 STL 的情况下编写的。在这种情况下,您可以编写自己的智能指针实现。首先,您需要创建一个可用于引用计数的对象。引用计数的工作原理是,每当您复制一个想要计数的对象时,就增加一个整数。清单 10-12 显示了一个引用计数类的代码。

清单 10-12 。引用计数类的代码

class ReferenceCount
{
private:
    int m_Count{ 0 };

public:
    void Increment()
    {
        ++m_Count;
    }

    int Decrement()
    {
        return --m_Count;
    }

    int GetCount() const
    {
        return m_Count;
    }
};

这个类非常基础。它只包含一个跟踪计数的成员变量以及增加和减少计数的方法。GetCount方法提供对计数的访问,允许在调试期间打印。

然后在名为SmartPointer的模板类中使用ReferenceCount类。该类提供了一个模板参数,您可以使用该参数将模板专门化为您希望自动跟踪的对象类型。该类有一个成员变量,一个指向被跟踪对象的指针,另一个指向ReferenceCount对象。通过一个指针来访问ReferenceCount对象,这样它就可以在访问同一个动态分配对象的多个SmartPointer对象之间共享。你可以在清单 10-13 中看到SmartPointer和的代码。

清单 10-13SmartPointer

template <typename T>
class SmartPointer
{
private:
    T* m_Object{ nullptr };
    ReferenceCount* m_ReferenceCount{ nullptr };

public:
    SmartPointer()
    {

    }

    SmartPointer(T* object)
        : m_Object{ object }
        , m_ReferenceCount{ new ReferenceCount }
    {
        m_ReferenceCount->Increment();

        cout << "Created smart pointer! Reference count is "
            << m_ReferenceCount->GetCount() << endl;
    }

    virtual ~SmartPointer()
    {
        if (m_ReferenceCount)
        {
            int decrementedCount = m_ReferenceCount->Decrement();
            cout << "Destroyed smart pointer! Reference count is "
                << decrementedCount << endl;
            if (decrementedCount == 0)
            {
                delete m_ReferenceCount;
                delete m_Object;
            }
            m_ReferenceCount = nullptr;
            m_Object = nullptr;
        }
    }

    SmartPointer(const SmartPointer<T>& other)
        : m_Object{ other.m_Object }
        , m_ReferenceCount{ other.m_ReferenceCount }
    {
        m_ReferenceCount->Increment();

        cout << "Copied smart pointer! Reference count is "
            << m_ReferenceCount->GetCount() << endl;
    }

    SmartPointer<T>& operator=(const SmartPointer<T>& other)
    {
        if (this != &other)
        {
            if (m_ReferenceCount && m_ReferenceCount->Decrement() == 0)
            {
                delete m_ReferenceCount;
                delete m_Object;
            }

            m_Object = other.m_Object;
            m_ReferenceCount = other.m_ReferenceCount;
            m_ReferenceCount->Increment();
        }

        cout << "Assigning smart pointer! Reference count is "
            << m_ReferenceCount->GetCount() << endl;

        return *this;
    }

    SmartPointer(SmartPointer<T>&& other)
        : m_Object{ other.m_Object }
        , m_ReferenceCount{ other.m_ReferenceCount }
    {
        other.m_Object = nullptr;
        other.m_ReferenceCount = nullptr;
    }

    SmartPointer<T>& operator=(SmartPointer<T>&& other)
    {
        if (this != &other)
        {
            m_Object = other.m_Object;
            m_ReferenceCount = other.m_ReferenceCount;

            other.m_Object = nullptr;
            other.m_ReferenceCount = nullptr;
        }
    }

    T& operator*()
    {
        return *m_Object;
    }
};

您可以在清单 10-13 的中看到用于存储动态分配对象和SmartPointer类中的ReferenceCount对象的成员变量。对象指针是一个指向抽象模板类型的指针;这允许任何类型的使用被SmartPointer模板跟踪。

SmartPointer中的第一个公共方法是构造函数。可以创建一个新的SmartPointer作为空指针或者指向一个已经存在的对象。一个空的SmartPointerm_Objectm_ReferenceCount都设置为nullptr。另一个构造函数取一个指向T的指针,这个指针导致一个SmartPointer被初始化。在这种情况下,创建一个新的ReferenceCount对象来跟踪传递给构造函数的对象的使用。这样做的副作用是,新的SmartPointer只能在用对象指针初始化时创建;空值SmartPointer只能从另一个SmartPointer对象分配。

SmartPointer析构函数检查一个ReferenceCount对象是否被类持有(记住它可能是空SmartPointer中的nullptr)。如果一个指向ReferenceCount对象的指针被保持,它的计数就会减少。如果计数已经达到 0,那么你知道这个SmartPointer是最后一个引用这个动态分配的对象。在这种情况下,您可以自由地删除ReferenceCount对象和由SmartPointer持有的对象。

SmartPointer中的下一个方法是copy构造器。该方法只是将传递给该方法的参数中的m_Objectm_ReferenceCount指针复制到被复制构造的对象中。然后,它确保引用计数递增。对Increment的调用是必不可少的,因为现在有两个SmartPointer对象引用同一个动态分配的对象。在这里错过对Increment的调用会导致delete在第一个SmartPointer的析构函数中被调用而超出范围。

赋值操作符的工作与copy构造函数略有不同。在copy构造函数中,您可以自由地假设现有对象是新的,因此没有指向现有对象或ReferenceCount实例。这在赋值操作符中是不正确的;因此,有必要解释这种情况的发生。您可以看到赋值操作符首先检查以确保操作符没有将对象赋值给它自己;在这种情况下,就没有工作可做了。如果正在分配一个新对象,则检查ReferenceCount指针是否有效。如果是,那么就叫Decrement;并且在返回 0 的情况下,删除现有的m_ReferenceCountm_Object指针。m_Objectm_ReferenceCount指针总是从赋值操作符方法的参数复制到这个的变量中,并且在新的ReferenceCount对象上调用Increment

接下来是一个move构造函数和move赋值操作符。这些都符合 C++ 的五原则。这是一个编程指南,建议在重载copy构造函数或赋值操作符的任何情况下,都应该重载所有五个析构函数、copy构造函数、赋值操作符、move构造函数和move赋值操作符。移动操作本质上是破坏性的,因此不会调用IncrementDecrement。这些都是不必要的,因为在这两种情况下,参数上的m_Objectm_ReferenceCount指针都被设置为nullptr,这意味着delete永远不会在它们的析构函数中被调用。支持move构造函数和move赋值操作符提供了一种更有效的方法将SmartPointer对象传入和传出函数。

最后一个方法提供了对由SmartPointer对象存储的数据的访问。如果在空的SmartPointer对象上调用这个方法,这可能会导致崩溃。您应该注意只尝试解引用有效的SmartPointer实例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意 清单 10-14 包含调试代码,允许打印对象状态,以便于说明。该代码可以从工作解决方案中删除。

清单 10-14 展示了一个正在使用的SmartPointer类的完整工作示例。

清单 10-14 。使用SmartPointer

#include <iostream>

using namespace std;

class ReferenceCount
{
private:
    int m_Count{ 0 };

public:
    void Increment()
    {
        ++m_Count;
    }

    int Decrement()
    {
        return --m_Count;
    }

    int GetCount() const
    {
        return m_Count;
    }
};

template <typename T>
class SmartPointer
{
private:
    T* m_Object{ nullptr };
    ReferenceCount* m_ReferenceCount{ nullptr };

public:
    SmartPointer()
    {

    }

    SmartPointer(T* object)
        : m_Object{ object }
        , m_ReferenceCount{ new ReferenceCount }
    {
        m_ReferenceCount->Increment();

        cout << "Created smart pointer! Reference count is " << m_ReferenceCount->GetCount() << endl;
    }

    virtual ~SmartPointer()
    {
        if (m_ReferenceCount)
        {
            int decrementedCount = m_ReferenceCount->Decrement();
            cout << "Destroyed smart pointer! Reference count is " << decrementedCount << endl;
            if (decrementedCount <= 0)
            {
                delete m_ReferenceCount;
                delete m_Object;
            }
            m_ReferenceCount = nullptr;
            m_Object = nullptr;
        }
    }

    SmartPointer(const SmartPointer<T>& other)
        : m_Object{ other.m_Object }
        , m_ReferenceCount{ other.m_ReferenceCount }
    {
        m_ReferenceCount->Increment();

        cout << "Copied smart pointer! Reference count is " << m_ReferenceCount->GetCount() << endl;
    }

    SmartPointer<T>& operator=(const SmartPointer<T>& other)
    {
        if (this != &other)
        {
            if (m_ReferenceCount && m_ReferenceCount->Decrement() == 0)
            {
                delete m_ReferenceCount;
                delete m_Object;
            }

            m_Object = other.m_Object;
            m_ReferenceCount = other.m_ReferenceCount;
            m_ReferenceCount->Increment();
        }

        cout << "Assigning smart pointer! Reference count is " << m_ReferenceCount->GetCount() << endl;

        return *this;
    }

    SmartPointer(SmartPointer<T>&& other)
        : m_Object{ other.m_Object }
        , m_ReferenceCount{ other.m_ReferenceCount }
    {
        other.m_Object = nullptr;
        other.m_ReferenceCount = nullptr;
    }

    SmartPointer<T>& operator=(SmartPointer<T>&& other)
    {
        if (this != &other)
        {
            m_Object = other.m_Object;
            m_ReferenceCount = other.m_ReferenceCount;

            other.m_Object = nullptr;
            other.m_ReferenceCount = nullptr;
        }
    }

    T& operator*()
    {
        return *m_Object;
    }
};

struct MyStruct
{
public:
    int m_Value{ 0 };

    ~MyStruct()
    {
        cout << "Destroying MyStruct object!" << endl;
    }
};

using SmartMyStructPointer = SmartPointer< MyStruct >;

SmartMyStructPointer PassValue(SmartMyStructPointer smartPointer)
{
    SmartMyStructPointer returnValue;
    returnValue = smartPointer;
    return returnValue;
}

int main(int argc, char* argv[])
{
    SmartMyStructPointer smartPointer{ new MyStruct };
    (*smartPointer).m_Value = 10;

    SmartMyStructPointer secondSmartPointer = PassValue(smartPointer);

    return 0;
}

清单 10-14 显示了一个使用SmartPointer模板在mainPassValue函数之间传递的MyStruct实例。创建类型别名是为了确保MyStructSmartPointer的类型是有效的,并且始终易于维护。代码使用来自SmartPointer模板的构造函数、copy构造函数和赋值操作符。只有当最后一个SmartPointer实例在main函数结束时超出范围,才会自动删除MyStruct对象。

图 10-11 显示了运行清单 10-14 中的代码时生成的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-11 。SmartPointer行动中的一个工作实例

10-7.通过重载 new 和 delete 调试内存问题

问题

你的程序中有一些内存问题,你想在程序的分配和释放中添加诊断代码。

解决办法

C++ 允许用定制的版本替换newdelete操作符。

它是如何工作的

C++ newdelete操作符归结为函数调用。全局new函数的签名是

void* operator new(size_t size);

全局delete函数的签名是

void delete(void* ptr);

new函数将待分配的字节数作为参数,delete函数将从new返回的内存地址作为指针。这些函数可以被替换以向程序提供额外的调试信息。清单 10-15 展示了一个给内存分配添加一个头的例子,以帮助程序调试。

清单 10-15 。向内存分配添加标头

#include <cstdlib>
#include <iostream>

using namespace std;

struct MemoryHeader
{
    const char* m_Filename{ nullptr };
    int m_Line{ -1 };
};

void* operator new(size_t size, const char* filename, int line) noexcept
{
    void* pMemory{ malloc(size + sizeof(MemoryHeader)) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemory) };
    pMemoryHeader->m_Filename = filename;
    pMemoryHeader->m_Line = line;

    return static_cast<void*>(static_cast<char*>(pMemory)+sizeof(MemoryHeader));
}

void operator delete(void* pMemory) noexcept
{
    char* pMemoryHeaderStart{ reinterpret_cast<char*>(pMemory)-sizeof(MemoryHeader) };
    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemoryHeaderStart) };

    cout << "Deleting memory allocated from: "
        << pMemoryHeader->m_Filename << ":" << pMemoryHeader->m_Line << endl;

    free(pMemoryHeader);
}

#define new new(__FILE__, __LINE__)

class MyClass
{
private:
    int m_Value{ 1 };
};

int main(int argc, char* argv[])
{
    int* pInt{ new int };
    *pInt = 1;
    delete pInt;

    MyClass* pClass{ new MyClass };
    delete pClass;

    return 0;
}

这段代码用自定义版本替换了newdelete函数。new的自定义版本不符合标准签名,所以用宏替换了标准版本。这样做是为了让编译器告诉定制的new函数文件名和调用new的行号。这允许您跟踪单个分配到它们在程序源代码中的确切位置。当您处理内存问题时,这是一个非常有用的调试工具。

自定义new函数将MemoryHeader结构的大小添加到程序请求的字节数中。然后将MemoryHeader struct中的m_Filename指针设置为提供给newfilename参数。类似地,m_Line成员被设置为传入的line参数。从new返回的地址是内存用户区开始的地址,不包括MemoryHeader结构;这允许在内存子系统级别添加和寻址调试信息,并且对程序的其余部分完全透明。

delete功能显示了该调试信息的基本用途。它只是打印出被释放的内存块被分配的那一行。它通过从函数传递的地址中减去头的大小来获得内存头的地址。

new宏用于给出一个简单的方法,将__FILE____LINE__宏传递给重载的new函数。这些宏被称为内置宏,由大多数现代 C++ 编译器提供。这些宏被替换为指向文件名和使用它们的行号的指针。将它们添加到new宏中会导致程序中每次调用new的文件名和行号被传递给定制的new分配器。

newdelete函数中使用的mallocfree函数是 C 风格的内存分配函数。这些用于防止与许多不同类型的 C++ 分配函数发生冲突。清单 10-15 中所示的函数适用于分配单个对象。也可以替换 C++ 数组newdelete函数。当您试图跟踪诸如内存泄漏之类的问题时,替换这些函数是非常必要的。清单 10-16 展示了这些函数的作用。

清单 10-16 。替换数组newdelete运算符

#include <cstdlib>
#include <iostream>

using namespace std;

struct MemoryHeader
{
    const char* m_Filename{ nullptr };
    int m_Line{ -1 };
};

void* operator new(size_t size, const char* filename, int line) noexcept
{
    void* pMemory{ malloc(size + sizeof(MemoryHeader)) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemory) };
    pMemoryHeader->m_Filename = filename;
    pMemoryHeader->m_Line = line;

    return static_cast<void*>(static_cast<char*>(pMemory)+sizeof(MemoryHeader));
}

void* operator new[](size_t size, const char* filename, int line) noexcept
{
    void* pMemory{ malloc(size + sizeof(MemoryHeader)) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemory) };
    pMemoryHeader->m_Filename = filename;
    pMemoryHeader->m_Line = line;

    return static_cast<void*>(static_cast<char*>(pMemory)+sizeof(MemoryHeader));
}

void operator delete(void* pMemory) noexcept
{
    char* pMemoryHeaderStart{ reinterpret_cast<char*>(pMemory)-sizeof(MemoryHeader) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemoryHeaderStart) };
    cout << "Deleting memory allocated from: "
        << pMemoryHeader->m_Filename << ":" << pMemoryHeader->m_Line << endl;

    free(pMemoryHeader);
}

void operator delete[](void* pMemory) noexcept
{
    char* pMemoryHeaderStart{ reinterpret_cast<char*>(pMemory)-sizeof(MemoryHeader) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemoryHeaderStart) };
    cout << "Deleting memory allocated from: "
        << pMemoryHeader->m_Filename << ":" << pMemoryHeader->m_Line << endl;

    free(pMemoryHeader);
}

#define new new(__FILE__, __LINE__)

class MyClass
{
private:
    int m_Value{ 1 };
};

int main(int argc, char* argv[])
{
    int* pInt{ new int };
    *pInt = 1;
    delete pInt;

    MyClass* pClass{ new MyClass };
    delete pClass;

    const unsigned int NUM_ELEMENTS{ 5 };
    int* pArray{ new int[NUM_ELEMENTS] };
    delete[] pArray;

    return 0;
}

数组newdelete操作符的签名与标准的newdelete操作符的区别仅在于它们的签名中出现了[]操作符,如清单 10-16 中的所示。图 10-12 显示了该代码生成的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-12 。显示使用替换的newdelete操作符的输出

到目前为止,你在这个配方中看到的newdelete函数已经成为了newdelete操作符的全局替换。也可以替换特定职业的newdelete。您可以将这些函数直接添加到类定义中,并且这些函数将在创建和销毁该类型对象的动态实例时使用。清单 10-17 显示了替换全局newnew[]deletedelete[]操作符的代码,还将newdelete操作符添加到了MyClass class定义中。

清单 10-17 。将newdelete运算符添加到MyClass

#include <cstdlib>
#include <iostream>

using namespace std;

struct MemoryHeader
{
    const char* m_Filename{ nullptr };
    int m_Line{ -1 };
};

void* operator new(size_t size, const char* filename, int line) noexcept
{
    void* pMemory{ malloc(size + sizeof(MemoryHeader)) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemory) };
    pMemoryHeader->m_Filename = filename;
    pMemoryHeader->m_Line = line;

    return static_cast<void*>(static_cast<char*>(pMemory)+sizeof(MemoryHeader));
}

void* operator new[](size_t size, const char* filename, int line) noexcept
{
    void* pMemory{ malloc(size + sizeof(MemoryHeader)) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemory) };
    pMemoryHeader->m_Filename = filename;
    pMemoryHeader->m_Line = line;

    return static_cast<void*>(static_cast<char*>(pMemory)+sizeof(MemoryHeader));
}

void operator delete(void* pMemory) noexcept
{
    char* pMemoryHeaderStart{ reinterpret_cast<char*>(pMemory)-sizeof(MemoryHeader) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemoryHeaderStart) };

    cout << "Deleting memory allocated from: "
        << pMemoryHeader->m_Filename << ":" << pMemoryHeader->m_Line << endl;

    free(pMemoryHeader);
}

void operator delete[](void* pMemory) noexcept
{
    char* pMemoryHeaderStart{ reinterpret_cast<char*>(pMemory)-sizeof(MemoryHeader) };

    MemoryHeader* pMemoryHeader{ reinterpret_cast<MemoryHeader*>(pMemoryHeaderStart) };

    cout << "Deleting memory allocated from: "
        << pMemoryHeader->m_Filename << ":" << pMemoryHeader->m_Line << endl;

    free(pMemoryHeader);
}

class MyClass
{
private:
    int m_Value{ 1 };

public:
    void* operator new(size_t size, const char* filename, int line) noexcept
    {
        cout << "Allocating memory for MyClass!" << endl;
        return malloc(size);
    }

    void operator delete(void* pMemory) noexcept
    {
        cout << "Freeing memory for MyClass!" << endl;
        free(pMemory);
    }
};

#define new new(__FILE__, __LINE__)

int main(int argc, char* argv[])
{
    int* pInt{ new int };
    *pInt = 1;
    delete pInt;

    MyClass* pClass{ new MyClass };
    delete pClass;

    const unsigned int NUM_ELEMENTS{ 5 };
    MyClass* pArray{ new MyClass[NUM_ELEMENTS] };
    delete[] pArray;

    return 0;
}

创建MyClass的单个实例时,MyClass定义中的newdelete操作符在main函数中被调用。在图 10-13 的输出中可以看到这种情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-13 。显示在MyClass中使用成员newdelete操作符的输出

10-8.计算代码更改的性能影响

问题

您希望确定您对代码所做的更改是比现有代码快还是慢。

解决办法

C++ 提供对计算机系统高性能定时器的访问,以实现高精度计时。

它是如何工作的

C++ 编程语言提供了对高分辨率定时器的访问,该定时器允许您对代码的不同部分进行计时测量。这可以让你记录你的函数或算法所花费的时间,并在不同的版本之间进行比较,从而找出哪一个是最有效率和性能的。

清单 10-18 显示了用于计时一个循环中三个不同迭代次数的代码。

清单 10-18 。使用chrono::high_resolution_timer

#include <chrono>
#include <iostream>

using namespace std;

void RunTest(unsigned int numberIterations)
{
    auto start = chrono::high_resolution_clock::now();

    for (unsigned int i{ 0 }; i < numberIterations; ++i)
    {
        unsigned int squared{ i*i*I };
    }

    auto end = chrono::high_resolution_clock::now();
    auto difference = end - start;

    cout << "Time taken: "
        << chrono::duration_cast<chrono::microseconds>(difference).count()
        << " microseconds!" << endl;
}

int main(int argc, char* argv[])
{
    RunTest(10000000);
    RunTest(100000000);
    RunTest(1000000000);

    return 0;
}

这个清单显示了 STL 中的chrono名称空间提供了一个名为high_resolution_clockstruct和一个名为now的静态函数。这个函数从chrono::system_clock struct 中返回一个类型为 t ime_point的对象。清单 10-18 使用auto关键字为RunTest函数中的startend变量推导出这种类型。使用 hig h_resolution_timer::now功能初始化startend,在for循环之前初始化start,在for循环之后初始化end。从end的值中减去start的值,得到函数执行循环所经过的时间。然后使用chrono::duration_cast 模板将time_point差异变量转换成可以以人类可读形式表达的表示,在本例中为微秒。

RunTest函数被调用了三次,不同于main函数。每个调用都有不同数量的循环迭代要运行,以显示计时代码可用于判断哪一次运行的时间效率最低。图 10-14 显示了在英特尔酷睿 i7-3770 上运行程序时产生的输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-14 。输出显示对清单 10-18 中的RunTest的每个后续调用需要更长时间来执行

duration_cast 可用于将系统时间转换为纳秒、毫秒、秒、分、小时以及微秒。当优化许多计算机编程算法时,微秒精度是你正在寻找的。当比较存储器存储类型对程序效率的影响时,这个方法中使用的计时技术将被证明是有用的。

10-9.了解内存选择对性能的影响

问题

你有一个表现很差的程序,但是你不知道为什么。

解决办法

在现代计算机程序中,没有解决性能问题的灵丹妙药。然而,缺乏对现代计算机内存工作原理的理解会导致程序运行不佳。了解高速缓存未命中对程序性能的影响将有助于您编写性能更好的程序。

它是如何工作的

现代处理器的速度比内存访问延迟快得多。这导致了程序中糟糕的内存访问模式会严重影响处理性能的情况。了解如何构建 C++ 程序以有效利用处理器缓存,对于编写尽可能高性能的程序至关重要。

在现代计算机系统中,从主存储器中读取和写入数据可能需要数百个周期。处理器实现缓存来帮助缓解这个问题。现代 CPU 高速缓存的工作原理是将大量数据同时从主内存读入速度更快的高速缓存。这些块被称为缓存线 。英特尔酷睿 i7-3770 处理器上的 L1 高速缓存行大小为 32KB。处理器一次性将整个 32KB 数据块读入 L1 缓存。如果您正在读取或写入的数据不在高速缓存中,结果就是高速缓存未命中,处理器必须从 L2 高速缓存、L3 高速缓存或系统 RAM 中检索数据。缓存未命中的代价可能非常高,代码中看似无害的错误或选择可能会对性能产生重大影响。清单 10-19 包含一个初始化一些数组的循环和三个具有不同内存访问模式的不同循环。

清单 10-19 。探索内存访问模式的性能影响

#include <chrono>
#include <iostream>

using namespace std;

const int NUM_ROWS{ 10000 };
const int NUM_COLUMNS{ 1000 };
int elements[NUM_ROWS][NUM_COLUMNS];
int* pElements[NUM_ROWS][NUM_COLUMNS];

int main(int argc, char* argv[])
{
    for (int i{ 0 }; i < NUM_ROWS; ++i)
    {
        for (int j{ 0 }; j < NUM_COLUMNS; ++j)
        {
            elements[i][j] = i*j;
            pElements[i][j] = new int{ elements[i][j] };
        }
    }

    auto start = chrono::high_resolution_clock::now();

    for (int i{ 0 }; i < NUM_ROWS; ++i)
    {
        for (int j{ 0 }; j < NUM_COLUMNS; ++j)
        {
            const int result{ elements[j][i] };
        }
    }

    auto end = chrono::high_resolution_clock::now();
    auto difference = end - start;

    cout << "Time taken for j then i: "
        << chrono::duration_cast<chrono::microseconds>(difference).count()
        << " microseconds!" << endl;

    start = chrono::high_resolution_clock::now();

    for (int i{ 0 }; i < NUM_ROWS; ++i)
    {
        for (int j{ 0 }; j < NUM_COLUMNS; ++j)
        {
            const int result{ elements[i][j] };
        }
    }

    end = chrono::high_resolution_clock::now();
    difference = end - start;

    cout << "Time taken for i then j: "
        << chrono::duration_cast<chrono::microseconds>(difference).count()
        << " microseconds!" << endl;

    start = chrono::high_resolution_clock::now();

    for (int i{ 0 }; i < NUM_ROWS; ++i)
    {
        for (int j{ 0 }; j < NUM_COLUMNS; ++j)
        {
            const int result{ *(pElements[i][j]) };
        }
    }

    end = chrono::high_resolution_clock::now();
    difference = end - start;

    cout << "Time taken for pointers with i then j: "
        << chrono::duration_cast<chrono::microseconds>(difference).count()
        << " microseconds!" << endl;

    return 0;
}

清单 10-19 中的第一个循环用于设置两个数组。第一个数组直接存储整数值,第二个数组存储指向整数的指针。每个数组包含 10,000 × 1,000 个唯一元素。

理解多维数组在内存中的布局,理解为什么这个测试会产生与缓存未命中性能问题相关的结果是很重要的。可以认为一个 3 × 2 的阵列的布局如表 10-1 所示。

表 10-1 。3 × 2 阵列的布局

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是电脑内存并不是二维的。数组的元素按照表 10-1 中显示的数字顺序线性排列在内存中。给定一个 4 字节的整数大小,这意味着第 2 行第 1 列中的值可以在第 1 行第 1 列中的值之后 12 个字节找到。将行大小扩展到 10,000,您可以看到下一行开头的元素不可能与前一行位于同一缓存行。

这个事实允许用一个简单的循环来测试高速缓存未命中的性能含义。你可以在清单 10-18 的第二个循环中看到这一点,其中增加的j值用于遍历列而不是行。第三个循环按照正确的顺序遍历数组。也就是说,它在内存中按线性顺序遍历这些行。第四个循环以线性顺序遍历pElement数组,但是必须取消对指针的引用才能到达数组中存储的值。结果显示了第一个循环中缓存感知编程的影响,第二个循环中的理想情况,以及第三个循环中不必要的内存间接寻址的结果。图 10-15 显示了这些结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-15 。来自清单 10-19 中循环的结果

你可以看到,在无序遍历数组时,我电脑中的处理器完成一个简单循环所需的时间增加了 10 倍。这样的问题会导致程序的口吃和延迟,让用户和客户对你的软件感到沮丧。指针解引用的情况也比直接访问整数的情况慢大约两倍。在大量使用动态内存之前,您应该考虑这一点。

10-10.减少内存碎片

问题

您有一个程序,它要求您在很长一段时间内创建许多小的内存分配,这就引入了内存碎片问题。

解决办法

您可以创建一个小的块分配器,用于将小的分配打包到更大的页面中。

它是如何工作的

将少量分配捆绑在一起的第一步是创建一个包含更大内存页面的类。这个方法向您展示了一种简单的方法,将一个 32KB 的内存页面包装到一个类中,并管理这个池中的内存分配。使用布尔值数组跟踪内存,该数组知道给定的内存块是空闲的还是在使用中。当所有当前页面都已满时,会添加新的内存页面。

这种方法的缺点是所有分配的最小大小为 32 字节。任何小于 32 字节的内存请求都会从当前活动的内存页面中分配一个完整的块。当页面完全清空时,它们也会被释放,以确保程序不会增长到一个高水位,并且永远不会释放不需要的内存。清单 10-20 显示了Page的类定义。

清单 10-20Page类定义

class Page
{
private:
    char m_Memory[1024 * 32];
    bool m_Free[1024];
    Page* m_pNextPage;

public:
    Page();
    ~Page();

    void* Alloc();
    bool Free(void* pMem);

    bool IsEmpty() const;
};

Page类定义包含两个数组。有一个char数组服务于内存分配请求。这个池是一个字节数组,在本例中大小为 32KB。池中有 1,024 个独立的块,每个块的大小为 32 字节。1024 个块被镜像到布尔数组m_Free中。这个数组用于跟踪一个给定的块是已经分配了还是可以分配。m_pNextPage指针存储下一页的地址。如果当前页完全被使用,则下一页用于分配块。

该类由五个方法组成:构造函数、析构函数、Alloc方法、Free方法和IsEmpty方法,用于确定页面是否不再使用。清单 10-21 展示了Page类的构造函数和析构函数的函数体。

清单 10-21Page构造函数和析构函数

Page()
    : m_pNextPage{ nullptr }
{
    memset(m_Free, 1, 1024);
}

~Page()
{
    if (m_pNextPage)
    {
        delete m_pNextPage;
        m_pNextPage = nullptr;
    }
}

Page构造函数负责初始化指向nullptrm_pNextPage指针,并将m_F ree 数组中的所有元素设置为truePage的析构函数负责删除指向m_pNextPage的对象指针,如果它已经被分配。

清单 10-22 显示了Page:: Alloc方法的代码。

清单 10-22Page::Alloc

void* Alloc()
{
    void* pMem{ nullptr };

    for (unsigned int i = 0; i < 1024; ++i)
    {
        if (m_Free[i] == true)
        {
            m_Free[i] = false;
            pMem = &m_Memory[i * 32];
            break;
        }
    }

    if (pMem == nullptr)
    {
        if (m_pNextPage == nullptr)
        {
            m_pNextPage = new Page();
        }

        pMem = m_pNextPage->Alloc();
        }

    return pMem;
}

Alloc方法负责在页面链表中寻找第一个未使用的内存块。第一步是遍历m_Free数组,检查每个块,看它当前是否在使用中。如果找到一个空闲块,pMem返回值被设置为空闲块的地址。该块的布尔值被设置为false以指示该块现在正在使用中。如果找到一个空闲块,循环就被中断。

如果没有找到空闲块,就必须从另一个内存页面分配内存。如果已经创建了另一个页面,指针m_pNextPage已经保存了它的地址。如果没有,则创建一个新页面。然后在m_pNextPage上调用Alloc方法。此时,Alloc方法是递归的。它会被反复调用,直到找到一个包含空闲内存块的内存页,以将堆栈向上返回到调用代码。从一个页面返回的内存也必须在不再需要时返回到那个页面。清单 10-23 中的Free方法负责执行这项任务。

清单 10-23Page::Free

bool Free(void* pMem)
{
    bool freed{ false };

    bool inPage{ pMem >= m_Memory && pMem <= &m_Memory[(NUM_PAGES * BLOCK_SIZE) - 1] };
    if (inPage)
    {
        unsigned int index{
            (reinterpret_cast<unsigned int>(pMem)-reinterpret_cast<unsigned int>(m_Memory))
            / BLOCK_SIZE };

        m_Free[index] = true;
        freed = true;
    }
    else if (m_pNextPage)
    {
        freed = m_pNextPage->Free(pMem);

        if (freed && m_pNextPage->IsEmpty())
        {
            Page* old = m_pNextPage;
            m_pNextPage = old->m_pNextPage;
            old->m_pNextPage = nullptr;
            delete m_pNextPage;
        }
    }

    return freed;
}

Page::Free方法首先检查被释放的内存地址是否包含在当前页面中。它通过将该地址与内存页面开始的地址和页面中最后一个块的地址进行比较来实现这一点。如果被释放的内存大于或等于页面地址,并且小于或等于页面中的最后一个块,则内存是从该页面分配的。在这种情况下,该块的m_Free布尔值可以被设置回true。内存本身不需要清除,因为new不保证它返回的内存中包含的值,这是调用者的责任。

如果在当前的Page中没有找到内存,那么Free方法检查Page是否有指向另一个Page对象的指针。如果是,那么在那个Page上调用Free方法。与Alloc方法一样,Free方法本质上是递归的。如果对m_pN extPage 上的Free的调用返回了一个true值,则检查Page以查看它现在是否为空。如果是,则可以释放Page。因为Page使用一个简单的链接列表来跟踪页面,你必须确保你没有孤立列表的尾部。你需要确保当前页面的m_pNextPage指针被设置为指向被释放的Pagem_pNextPage指针。在Free方法中调用IsEmpty方法;这个方法的主体如清单 10-24 所示。

清单 10-24Page::IsEmpty

bool IsEmpty() const
{
    bool isEmpty{ true };

    for (unsigned int i = 0; i < NUM_PAGES; ++i)
    {
        if (m_Free[i] == false)
        {
            isEmpty = false;
            break;
        }
    }

    return isEmpty;
}

IsEmpty方法检查空闲列表以确定页面当前是否被使用。如果Page中的任何一个块不空闲,那么Page就不是空的。页面链表是通过另一个名为SmallBlockAllocator的类来访问的。这简化了调用代码的页面管理。清单 10-25 显示了SmallBlockAllocator类。

清单 10-25 。SmallBlockAllocator

class SmallBlockAllocator
{
public:
    static const unsigned int BLOCK_SIZE{ 32 };

private:
    static const unsigned int NUM_ BLOCKS { 1024 };
    static const unsigned int PAGE_SIZE{ NUM_ BLOCKS * BLOCK_SIZE };

    class Page
    {
    private:
        char m_Memory[PAGE_SIZE];
        bool m_Free[NUM_ BLOCKS];
        Page* m_pNextPage;

    public:
        Page()
            : m_pNextPage{ nullptr }
        {
            memset(m_Free, 1, NUM_ BLOCKS);
        }

        ~Page()
        {
            if (m_pNextPage)
            {
                delete m_pNextPage;
                m_pNextPage = nullptr;
            }
        }

        void* Alloc()
        {
            void* pMem{ nullptr };

            for (unsigned int i = 0; i < NUM_ BLOCKS; ++i)
            {
                if (m_Free[i] == true)
                {
                    m_Free[i] = false;
                    pMem = &m_Memory[i * BLOCK_SIZE];
                    break;
                }
            }

            if (pMem == nullptr)
            {
                if (m_pNextPage == nullptr)
                {
                    m_pNextPage = new Page();
                }

                pMem = m_pNextPage->Alloc();
            }

            return pMem;
        }

        bool Free(void* pMem)
        {
            bool freed{ false };

            bool inPage{ pMem >= m_Memory &&
                pMem <= &m_Memory[(NUM_ BLOCKS * BLOCK_SIZE) - 1] };
            if (inPage)
            {
                unsigned int index{
                    (reinterpret_cast<unsigned int>(pMem)-
                     reinterpret_cast<unsigned int>(m_Memory)) / BLOCK_SIZE };
                m_Free[index] = true;
                freed = true;
            }
            else if (m_pNextPage)
            {
                freed = m_pNextPage->Free(pMem);

                if (freed && m_pNextPage->IsEmpty())
                {
                    Page* old = m_pNextPage;
                    m_pNextPage = old->m_pNextPage;
                    old->m_pNextPage = nullptr;
                    delete m_pNextPage;
                }
            }

            return freed;
        }

        bool IsEmpty() const
        {
            bool isEmpty{ true };

            for (unsigned int i = 0; i < NUM_BLOCKS; ++i)
            {
                if (m_Free[i] == false)
                {
                    isEmpty = false;
                    break;
                }
            }

            return isEmpty;
        }
    };

    Page m_FirstPage;

public:
    SmallBlockAllocator() = default;

    void* Alloc()
    {
        return m_FirstPage.Alloc();
    }

    bool Free(void* pMem)
    {
        return m_FirstPage.Free(pMem);
    }
};

在清单 10-25 的中,Page类可以被视为SmallBlockAllocator的内部类。这有助于确保只有SmallBlockAllocator本身可以用作Page对象的接口。SmallBlockAllocator首先创建静态常量来控制块的大小和每个Page包含的块数。从SmallBlockAllocator公开的公共方法只有一个Alloc方法和一个Free方法。这些简单地包装对Page::AllocPage::Free的调用,并在成员m_FirstPage上调用。这意味着SmallBlockAllocator类总是至少有一页内存分配给小的分配,并且只要SmallBlockAllocator处于活动状态,这一页就会驻留在你的程序中。

清单 10-26 显示了重载的newdelete操作符,它们需要将小额分配路由到SmallBlockAllocator

清单 10-26 。将小额分配路由至SmallBlockAllocator

static SmallBlockAllocator sba;

void* operator new(unsigned int numBytes)
{
    void* pMem{ nullptr };

    if (numBytes <= SmallBlockAllocator::BLOCK_SIZE)
    {
        pMem = sba.Alloc();
    }
    else
    {
        pMem = malloc(numBytes);
    }

    return pMem;
}

void* operator new[](unsigned int numBytes)
{
    void* pMem{ nullptr };

    if (numBytes <= SmallBlockAllocator::BLOCK_SIZE)
    {
        pMem = sba.Alloc();
    }
    else
    {
        pMem = malloc(numBytes);
    }

    return pMem;
}

void operator delete(void* pMemory)
{
    if (!sba.Free(pMemory))
    {
        free(pMemory);
    }
}

void operator delete[](void* pMemory)
{
    if (!sba.Free(pMemory))
    {
        free(pMemory);
    }
}

清单 10-26 中的newnew[]操作符根据SmallBlockAllocator类支持的块大小检查分配的字节数。如果被请求的内存大小小于或等于 SBA 的块大小,则在static sba对象上调用Alloc方法。如果它较大,则使用malloc。两个delete函数都在sba上调用Free。如果Free返回false,那么被释放的内存不存在于任何小块页面中,并且使用free函数被释放。

这涵盖了实现一个简单的小块分配器所需的所有代码。清单 10-27 展示了使用这个类的一个工作示例程序的完整清单。

清单 10-27 。 一个工作的小块分配器例子

#include <cstdlib>
#include <iostream>

using namespace std;

class SmallBlockAllocator
{
public:
    static const unsigned int BLOCK_SIZE{ 32 };

private:
    static const unsigned int NUM_BLOCKS{ 1024 };
    static const unsigned int PAGE_SIZE{ NUM_BLOCKS * BLOCK_SIZE };

    class Page
    {
    private:
        char m_Memory[PAGE_SIZE];
        bool m_Free[NUM_BLOCKS];
        Page* m_pNextPage;

    public:
        Page()
            : m_pNextPage{ nullptr }
        {
            memset(m_Free, 1, NUM_BLOCKS);
        }

        ~Page()
        {
            if (m_pNextPage)
            {
                delete m_pNextPage;
                m_pNextPage = nullptr;
            }
        }

        void* Alloc()
        {
            void* pMem{ nullptr };

            for (unsigned int i{ 0 }; i < NUM_BLOCKS; ++i)
            {
                if (m_Free[i] == true)
                {
                    m_Free[i] = false;
                    pMem = &m_Memory[i * BLOCK_SIZE];
                    break;
                }
            }

            if (pMem == nullptr)
            {
                if (m_pNextPage == nullptr)
                {
                    m_pNextPage = new Page();
                }

                pMem = m_pNextPage->Alloc();
            }

            return pMem;
        }

        bool Free(void* pMem)
        {
            bool freed{ false };

            bool inPage{ pMem >= m_Memory &&
                pMem <= &m_Memory[(NUM_BLOCKS * BLOCK_SIZE) - 1] };
            if (inPage)
            {
                unsigned int index{
                    (reinterpret_cast<unsigned int>(pMem)-
                     reinterpret_cast<unsigned int>(m_Memory)) / BLOCK_SIZE };
                m_Free[index] = true;
                freed = true;
            }
            else if (m_pNextPage)
            {
                freed = m_pNextPage->Free(pMem);

                if (freed && m_pNextPage->IsEmpty())
                {
                    Page* old = m_pNextPage;
                    m_pNextPage = old->m_pNextPage;
                    old->m_pNextPage = nullptr;
                    delete m_pNextPage;
                }
            }

            return freed;
        }

        bool IsEmpty() const
        {
            bool isEmpty{ true };

            for (unsigned int i{ 0 }; i < NUM_BLOCKS; ++i)
            {
                if (m_Free[i] == false)
                {
                    isEmpty = false;
                    break;
                }
            }

            return isEmpty;
        }
    };

    Page m_FirstPage;

public:
    SmallBlockAllocator() = default;

    void* Alloc()
    {
        return m_FirstPage.Alloc();
    }

    bool Free(void* pMem)
    {
        return m_FirstPage.Free(pMem);
    }
};

static SmallBlockAllocator sba;

void* operator new(size_t numBytes, const std::nothrow_t& tag) noexcept
{
    void* pMem{ nullptr };

    if (numBytes <= SmallBlockAllocator::BLOCK_SIZE)
    {
        pMem = sba.Alloc();
    }
    else
    {
        pMem = malloc(numBytes);
    }

    return pMem;
}

void* operator new[](size_t numBytes, const std::nothrow_t& tag) noexcept
{
    void* pMem{ nullptr };

    if (numBytes <= SmallBlockAllocator::BLOCK_SIZE)
    {
        pMem = sba.Alloc();
    }
    else
    {
        pMem = malloc(numBytes);
    }

    return pMem;
}

void operator delete(void* pMemory)
{
    if (!sba.Free(pMemory))
    {
        free(pMemory);
    }
}

void operator delete[](void* pMemory)
{
    if (!sba.Free(pMemory))
    {
        free(pMemory);
    }
}

int main(int argc, char* argv[])
{
    const unsigned int NUM_ALLOCS{ 2148 };
    int* pInts[NUM_ALLOCS];

    for (unsigned int i{ 0 }; i < NUM_ALLOCS; ++i)
    {
        pInts[i] = new int;
        *pInts[i] = i;
    }

    for (unsigned int i{ 0 }; i < NUM_ALLOCS; ++i)
    {
        delete pInts[i];
        pInts[i] = nullptr;
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值