c++对象中动态内存分配

c++对象中动态内存分配

假设我们有两个类,一个是电子表格单元格类SpreadsheetCell,另外一个是电子表格类Spreadsheet。我们都使用过电子表格,一个电子表格往往都有行与列组成。所以我们的Spreadsheet类是SpreadsheetCell类的二维数组。

下面我们要用动态的分配内存方式来创建这个电子表格。

class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(int value) : m_val{value} {};
private:
    int m_val;
};

class Spreadsheet
{
public:
    Spreadsheet(size_t width, size_t height);
    Spreadsheet(const Spreadsheet& src);
    void setCellAt(size_t x, size_t y, const SpreadsheetCell &cell);
    SpreadsheetCell &getCellAt(size_t x, size_t y);
    void verifyCoordinate(size_t x, size_t y) const;
    ~Spreadsheet();

private:
    size_t m_width{0};
    size_t m_height{0};
    SpreadsheetCell **m_cells{nullptr};
};

Spreadsheet::Spreadsheet(size_t width, size_t height)
    : m_width{width}, m_height{height}
{
    m_cells = new SpreadsheetCell *[m_width]; // 申请分别指向每一列的指针数组
    for (size_t i{0}; i < m_width; ++i) // 分别为每一列的元素都申请空间,共有m_width列
    {
        m_cells[i] = new SpreadsheetCell[m_height];
    }
}

注意:上面的代码中Spreadsheet类并没有包含一个SpreadsheetCell类型的标准二维数组,而是包含一个SpreadsheetCell**。主要因为不同用户需要的对象维度可能不同,因此类的构造函数需要根据不同用户指定的宽度高度动态的分配二维数组。在c++中与java不同,不能仅编写new SpreadsheetCell[m_width][m_eight]

名称为s1的Spreadsheet对象分配的内存图示如下,宽为4,高为3。在这里插入图片描述

我们继续实现其他成员函数:

void Spreadsheet::verifyCoordinate(size_t x, size_t y) const
{
    if (x >= m_width)
    {
        // 输入范围错误,抛出异常
        throw out_of_range{format("{} must be less than {}!", x, m_width)};
    }
    if (y >= m_height)
    {
        throw out_of_range{format("{} must be less than {}!", y, m_height)};
    }
}

void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell &cell)
{
    verifyCoordinate(x, y);
    m_cells[x][y] = cell;
}

SpreadsheetCell &Spreadsheet::getCellAt(size_t x, size_t y)
{
    verifyCoordinate(x, y);
    return m_cells[x][y];
}

当我们不需要动态分配的内存,就必须释放。析构函数没有参数,并且只有一个。注意,析构函数永远不应该抛出异常!

Spreadsheet::~Spreadsheet()
{
    // m_cells是二级指针,所以一定要先释放一级指针,否则就会内存泄漏
    for (size_t i{0}; i < m_width; ++i)
    {
        delete[] m_cells[i];
    }
    delete[] m_cells;
    m_cells = nullptr;
}

处理赋值和复制

如果没有自行编写拷贝构造函数或者赋值运算符,c++会自动生成。编译器生成的方法递归的调用对象数据成员的拷贝构造函数或者赋值运算符。然而对于基本类型,如int、double、指针等,只是提供表层(或按位)复制或赋值;只是将数据成员从源对象直接复制或者赋值到目标对象。当在对象内动态分配内存时,就会引发问题。例如,当s1被传递给函数printSpreadsheet()时:

void printSpreadsheet(Spreadsheet s) {/* code */};
int main()
{
    Spreadsheet s1 {4, 3};
    printSpreadsheet(s1);
}

Spreadsheet包含一个指针变量:m_cells。Spreadsheet的浅复制向目标对象提供了一个m_cells指针的副本,但没有复制底层数据。最终结果是s和s1都指向同一数据的指针。如图:在这里插入图片描述

如果s修改了m_cells所指向的内容,这一改动也会在s1中表现出来。还有更糟糕的是,当函数printSpreadsheet()退出时,会调用s的析构函数,释放m_cells所指向的内存。使得s1指针所指向的内存不再有效,变成了悬空指针。

还有当使用赋值时,情况同样糟糕:

Spreadsheet s1 {2, 2}, s2 {4, 3};
s1 = s2;

当执行完第一行之后,会创建两个对象,内存布局如下:在这里插入图片描述

当执行完第二行赋值语句之后,内存布局如下:在这里插入图片描述

现在,不仅s1和s2中的m_cells指针指向同一内存,而且s1前面所指的内存被遗弃,这称为内存泄漏。所以拷贝构造函数和赋值运算符必须进行深层复制,不能只复制指针数据成员,必须复制指针所指向的实际数据。通过上面例子可以看出,依赖c++默认的拷贝构造函数或者赋值运算符并不总是正确的。

Spreadsheet类的拷贝构造函数

Spreadsheet::Spreadsheet(const Spreadsheet &src)
    : Spreadsheet{src.m_width, src.m_height} // 委托有参构造函数
{
    for (size_t i{0}; i < m_width; ++i)
    {
        for (size_t j{0}; j < m_height; ++j)
        {
            m_cells[i][j] = src.m_cells[i][j];
        }
    }
}

Spreadsheet类的赋值运算符

下面是包含赋值运算符的Spreadsheet类定义:

class Spreadsheet
{
public:
    Spreadsheet &operator=(const Spreadsheet &rhs);
};

下面是一个不太成熟的实现方式:

Spreadsheet &Spreadsheet::operator=(const Spreadsheet &rhs)
{
    // 自赋值检查
    if (this == &rhs)
    {
        return *this;
    }
    // 释放旧内存
    for (size_t i{0}; i < m_width; ++i)
    {
        delete[] m_cells[i];
    }
    delete[] m_cells;
    m_cells = nullptr;

    // 申请新的内存
    m_width = rhs.m_width;
    m_height = rhs.m_height;
    m_cells = new SpreadsheetCell *[m_width];
    for (size_t i{0}; i < m_width; ++i)
    {
        m_cells[i] = new SpreadsheetCell[m_height];
    }

    // 复制数据
    for (size_t i{0}; i < m_width; ++i)
    {
        for (size_t j{0}; j < m_height; ++j)
        {
            m_cells[i][j] = rhs.m_cells[i][j];
        }
    }
    // 返回对象
    return *this;
}

上面代码首先检查自我赋值,然后释放this对象的当前内存,此后分配新内存,最后复制各个元素。这个方法存在不少问题,有很多地方可能出错,this对象可能进入无效的状态。

例如,假设成功的释放了对象,合理的设置了m_width,和m_height,但分配内存的循环抛出异常。如果发生了这样的情况,那么将不会在执行之后的代码,而是从该方法退出。此时,Spreadsheet实例受损,它的m_width,m_height数据成员声明了指定大小,但m_cells数据成员不指向正确数量的内存,根本上说,该代码不能安全地处理异常!

所以我们需要一个全有或者全无的机制,要么全部成功,要么该对象保持不变。为实现这样一个能安全处理异常的赋值运算符,要使用“复制和交换”的惯用法。可以给Spreadsheet类添加mySwap()方法。建议提供一个非成员函数的swap()的版本, 这样一来,各种标准库算法都可以使用它。下面是代码展示:

class Spreadsheet
{
public:
    Spreadsheet &operator=(const Spreadsheet &rhs);
    void mySwap(Spreadsheet &other) noexcept; // noexcept要求函数永不抛出异常
};
void Spreadsheet::mySwap(Spreadsheet &othre) noexcept
{
    swap(m_width, othre.m_width);
    swap(m_height, othre.m_height);
    swap(m_cells, othre.m_cells);
}

// 非成员的myswap函数只是简单地调用myswap方法
void mySwap(Spreadsheet &firsht, Spreadsheet &second) noexcept
{
    firsht.mySwap(second);
}

Spreadsheet &Spreadsheet::operator=(const Spreadsheet &rhs)
{
    Spreadsheet temp{rhs}; // 使用临时变量,以免交换后破环源数据
    mySwap(temp);
    return *this;
}

该实现使用“复制和交换”惯用方法。首先,先创建一份右边的副本,名为temp。然后用当前对象与这个副本交换,这个模式是实现赋值运算符的推荐方法,因为它保证强大的异常安全性。这意味着如果发生任何异常,当前的Spreadsheet对象保持不变。通过三个阶段来实现:

  1. 第一阶段创建一个临时副本。这不修改当前Spreadsheet对象的状态,因此这个阶段不会发生异常,不会出现问题。
  2. 第二个阶段使用mySwap()函数,将创建的临时副本与当前对象交换。mySwap()永远不会抛出异常。
  3. 第三阶段销毁临时对象(由于发生了交换,现在包含了原始对象)清理内存。

使用“复制和交换”方法的情况下,不再需要自我赋值的检查来提升效率。

禁止赋值和按值传递

在类中动态分配内存的时候,如果只向禁止其他人复制对象或者为对象赋值,只需要显示地将operator=和拷贝构造函数标记为delete。通过这种方法,当其它任何人按值传递对象时、从函数或方法返回对象时,或者为对象赋值时,编译器就会报错。

Spreadsheet(const Spreadsheet &src) = delete;
Spreadsheet &operator=(const Spreadsheet &rhs) = delete;

不需要提供=delete方法的实现,链接器永远不会查看它们,因为编译器不允许代码调用它们。

使用移动语义处理移动

对象的移动语义需要实现移动构造函数、和移动赋值运算符。如果源对象是操作结束后会被销毁的临时对象,或者是显示使用std::move()时,编译器就会使用这两个方法。移动将内存和其他资源的所有权从一个对象移动到另外一个对象。这两个方法基本上只对成员变量进行浅复制(shallow copy),然后转换已分配内存和其他资源的所有权,从而阻止空指针与内存泄露。

移动构造函数和移动赋值运算符将数据成员从源对象移动到新对象。然后使用源对象处于有效但不确定的状态。通常,源对象的数据成员被重置为空值,但这也不是必须的。为了安全起见,不要使用任何已被移走的对象,因为这样会导致未定义的行为。std::unique_ptr和std::shared_ptr是例外的情况。标准库明确规定,这些智能指针在移动时必须将其内部指针重置为nullptr,使得从智能指针移动后可以安全的重用这些智能指针。

在学习移动语义前需要了解一下什么是右值、什么是右值引用?

左值与右值(点击学习)

右值引用

右值引用是一个对右值的引用。特别地,这是一个当右值是临时对象或使用std::move()显示移动时才使适用的概念。右值引用的目的是在涉及右值时提供可选用的特定重载函数。通过右值引用,某些涉及复制大量值的操作可以通过简单地复制指向这些值的指针来实现。

函数可将&&作为参数说明的一部分(例如type&& name),以指定右值引用参数。通常,临时对象被当做const type &,但当函数重载使用了右值引用时,可以解析临时对象,用于函数重载。如下示例定义了两个handleMessage()函数,一个接收左值引用,另一个接收右值引用。

// 接收左值引用
void handleMessage(string &message)
{
    cout << format("handleMessage with lvalue reference: {}", message) << endl;
}

// 接收右值引用
void handleMessage(string &&message)
{
    cout << format("handleMessage with rvalue reference: {}", message) << endl;
}

int main(int argc, char **argv)
{
    string a{"hello"};
    string b{"world"};
    // 传递临时变量
    handleMessage(a + b);
    // 传递字面量
    handleMessage("nihao");
    return 0;
}

输出结果:

handleMessage with rvalue reference: helloworld
handleMessage with rvalue reference: nihao

如果删除接收左值引用的handleMessage()函数,使用有名称的变量调用handleMessage()函数(例如handleMessage(a);),会导致编译错误, 因为右值引用参数(string&&)永远不会与左值(a)绑定。可以使用std::move()强迫编译器调用handleMessage()函数的右值引用版本。move()函数做的唯一的事就是将左值转换为右值,也就是说它不做任何实际的行动。但是,通过返回右值引用,它可以使编译器找到接受右值引用的handleMessage()重载,然后进行移动。示例:

handleMessage(move(a));  // calls handleMessage(string &&message)

需要强调的是,有名称的变量是左值。因此,在handleMessage()函数中,右值引用参数message本身是一个左值,原因是它具有名称!如果希望将这个右值引用参数作为右值传递给另一个函数,则需要使用std::move(),将左值转换为右值。例如,假设要添加以下函数,使用右值引用参数:

void helper(string &&message)
{
    cout << "helper" << endl;
}

// 接收右值引用
void handleMessage(string &&message)
{
    helper(message);
    // cout << format("handleMessage with rvalue reference: {}", message) << endl;
}

编译会报错,无法将右值引用绑定到左值C/C++(1768)。helper()函数需要右值引用,而handleMessage()函数传递message,message具有名称,因此是左值,导致编译错误,正确方式是使用move();

void handleMessage(string &&message)
{
    helper(move(message));
}

注意:有名称的右值引用,如右值引用参数,本身就是左值,因为它具有名称。

右值引用并不局限于函数的参数。可以声明右值引用类型的变量,并对其赋值。

下面的代码在c++中是不合法的:

int &i{2}; // error:cannot bind non-const lvalue reference 
           // of type ‘int&’ to an rvalue of type ‘int’
int a{2}, b{3};
int &j{a + b}; // error: invalid initialization of non-const 
               // reference of type ‘int&’ from an rvalue of type 

使用右值引用后,下面的代码完全合法。

int &&i{2};
int a{2}, b{3};
int &&j(a + b);

注意:如果将临时值赋值给右值引用,则只要右值引用在作用域内,临时值的生命周期就会延长。

实现移动语义

移动语义是通过右值引用实现的。为了对类增加移动语义,需要实现移动构造函数和移动赋值运算符。移动构造函数和移动赋值运算符应使用noexcept限定符标记,告诉编译器,它们不会抛出异常。这对于标准库兼容非常重要,因为如果有了移动语义,标准库容器会移动存储的对象,且不抛出异常。

下面的Spreadsheet类定义包含一个移动构造函数和一个移动赋值运算符。同时也引入了两个辅助方法cleanup()和moveFrom()。前者在析构函数和移动赋值运算符中调用。后者用于把成员变量从源对象移动到目标对象,接着重置源对象。

class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(int value) : m_val{value} {}
    int getv() const
    {
        return m_val;
    }
    friend class Spreadsheet;

private:
    int m_val;
};

class Spreadsheet
{
public:
    Spreadsheet(Spreadsheet &&src) noexcept;
    Spreadsheet &operator=(Spreadsheet &&rhs) noexcept;

private:
    size_t m_width{0};
    size_t m_height{0};
    SpreadsheetCell **m_cells{nullptr};
    
    void cleanup() noexcept;
    void moveFrom(Spreadsheet &src) noexcept;
};

void Spreadsheet::cleanup() noexcept
{
    for (size_t i{0}; i < m_width; ++i)
    {
        delete[] m_cells[i];
    }
    delete[] m_cells;
    m_cells = nullptr;
    m_width = m_height = 0;
}

void Spreadsheet::moveFrom(Spreadsheet &src) noexcept
{
    // 浅拷贝
    m_width = src.m_width;
    m_height = src.m_height;
    m_cells = src.m_cells;

    // 重置源对象,因为所有权发生了转移
    src.m_width = 0;
    src.m_height = 0;
    src.m_cells = nullptr;
}

Spreadsheet::Spreadsheet(Spreadsheet &&src) noexcept
{
    moveFrom(src);
}

Spreadsheet &Spreadsheet::operator=(Spreadsheet &&rhs) noexcept
{
    // 自赋值检查
    if (this == &rhs)
    {
        return *this;
    }
    cleanup();
    moveFrom(rhs);
    return *this;
}

上述代码,移动构造函数和移动赋值运算符都将m_cells的内存所有权从源对象移动到新对象,它们将源对象的m_cells指针设置位空指针,将源对象的m_width和m_height设置为0,以防源对象的析构函数释放这块内存,因为新的对象现在拥有了这块内存。

很明显,只要你知道源对象不会在被使用时,移动语义才有用。

就像普通的构造函数或者拷贝赋值运算符一样,可显示的将移动构造函数和移动赋值运算符设置为默认或者将其删除。

仅当类没有用户声明的拷贝构造函数、拷贝赋值运算符、移动赋值运算符或者析构函数时,编译器才会为类自动生成默认的移动构造函数。仅当类没有用户声明的拷贝构造函数、移动构造函数、拷贝赋值运算符或者析构函数时,才会为类生成默认的移动赋值运算符。

注意:当你声明了一个或者多个特殊成员函数(拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数)时,通常需要声明所有这些函数,这称为“5规则”(rule of five)。可以显示的为它们提供实现,也可以显示默认(=default)或者删除(=delete)它们。

使用std::exchange

定义在< utility >中的std::exchange(),可以用一个新的值替换原来的值,并返回原来的值。例如:

int a{11};
int b{22};
cout << format("Befor exchange() : a = {}, b = {}", a, b) << endl;
int retVal{exchange(a, b)};
cout << format("After exchange() : a = {}, b = {}", a, b) << endl;
cout << format("exchange() returned {}", retVal) << endl;

输出结果:

Befor exchange() : a = 11, b = 22
After exchange() : a = 22, b = 22
exchange() returned 11

在实现移动赋值运算符时,exchange()十分有效。移动赋值运算符需要将数据从源对象移动到目标对象,之后源对象中的数据通常为空。对于前面实现的moveFrom方法使用exchange()可以更简洁的编写。例如:

void Spreadsheet::moveFrom(Spreadsheet &src) noexcept
{
    m_width = exchange(src.m_width, 0);
    m_height = exchange(src.m_width, 0);
    m_cells = exchange(src.m_cells, nullptr);
}
移动对象数据成员

moveFrom()方法对3个数据成员直接赋值,因为这些成员都是基本类型。如果对象还将其他对象作为数据成员,则应当使用std::move()移动这些对象。假设Spreadsheet类还有一个名为m_name的std::string数据成员。接着采用以下方式实现moveFrom()方法:

void Spreadsheet::moveFrom(Spreadsheet &src) noexcept
{
	// Move object data members
	m_name = move(src.m_name);   // 注意string是类,不是基本类型
	
    m_width = exchange(src.m_width, 0);
    m_height = exchange(src.m_width, 0);
    m_cells = exchange(src.m_cells, nullptr);
}
用交换方式实现移动构造函数和移动赋值运算符

前面的移动构造函数和移动赋值运算符的实现都使用了moveFrom()辅助方法,该方法通过执行浅拷贝复制来移动所有数据成员。在此实现中,如果给Spreadsheet类添加新的数据成员,则必须修改cleanup()和moveFrom()方法。如果忘记更改其中一个,则会引入bug。为了避免此类bug,可使用我们上面自定义的mySwap()函数编写移动构造函数和移动赋值运算符。

首先删除cleanup()、moveFrom()辅助方法,将cleanup()方法中的代码移入析构函数。此后,可按照如下方式实现移动构造函数和移动赋值运算符。

Spreadsheet::Spreadsheet(Spreadsheet &&src) noexcept
{
    mySwap(src);
}

Spreadsheet &Spreadsheet::operator=(Spreadsheet &&rhs) noexcept
{
    mySwap(rhs);
    return *this;
}

移动构造函数只是简单地将默认构造的 *this与给定的源对象进行交换。同样,移动赋值运算符将 *this与给定的rhs对象进行交换。

验证Spreadsheet移动操作
Spreadsheet createObject()
{
    return Spreadsheet{3, 2};
}

int main()
{    
	vector<Spreadsheet> vec;

    for (size_t i{0}; i < 2; ++i)
    {
        cout << "Iteration " << i << endl;
        
        // 将生成的临时对象存到数组中,所以会调用移动构造函数
        vec.push_back(Spreadsheet{10, 10});  
        cout << endl;
    }
    Spreadsheet s{2, 3};
    s = createObject();

    Spreadsheet s2{5, 6};
    s2 = s;
}

输出结果:

Iteration 0
Spreadsheet(size_t width, size_t height)
Spreadsheet(Spreadsheet &&src)
~Spreadsheet()

Iteration 1
Spreadsheet(size_t width, size_t height)
Spreadsheet(Spreadsheet &&src)
Spreadsheet(Spreadsheet &&src)
~Spreadsheet()
~Spreadsheet()

Spreadsheet(size_t width, size_t height)
Spreadsheet(size_t width, size_t height)
operator=(Spreadsheet &&rhs)
~Spreadsheet()
Spreadsheet(size_t width, size_t height)
operator=(const Spreadsheet &rhs)
Spreadsheet(size_t width, size_t height)
Spreadsheet(const Spreadsheet &src)
~Spreadsheet()
~Spreadsheet()
~Spreadsheet()
~Spreadsheet()
~Spreadsheet()

注意: 上述输出结果涉及到vector的扩容机制,如果不清楚点击此处学习!

如果Spreadsheet类未实现移动语义,对移动构造函数和移动赋值运算符的所有调用将被替换为对拷贝构造函数和拷贝赋值运算符的调用。在前面的示例中,循环中的Spreadsheet对象拥有10000个(100*100)个元素。Spreadsheet移动构造函数和移动赋值运算符的实现不需要分配任何内存,而拷贝构造函数和拷贝赋值运算符各需要101次分配。因此,某些情况下使用移动语义可以大幅度提高性能。

使用移动语义实现交换函数

考虑交换两个对象的swap()函数模板,这是另一个使用移动语义提高性能的示例。下面swapCopy()实现没有使用移动语义:

template <typename T>
void swapCopy(T &a, T &b)
{
    T temp(a);
    a = b;
    b = temp;
}

上述代码因为都是复制操作,所以如果类型T的复制开销很大,这个交换实现将严重影响性能。使用移动语义,swap()函数可以避免所有复制。下面是标准库的实现方式:

template <typename T>
void swapMove(T &a, T &b)
{
    T temp(move(a));
    a = move(b);
    b = move(temp);
}
在返回语句中使用std::move()

对于return object;形式的语句,如果object是局部变量、函数的参数或者临时值,则它们被视为右值表达式,并触发返回值优化(RVO)。此外,若object是一个局部变量,则会启动命名返回值优化(NRVO)。RVO和NRVO都是复制省略的形式,使得从函数返回对象非常有效。使用复制省略,编译器可以避免复制和移动函数返回的对象。这导致了所谓的零拷贝值传递语义。

现在,使用std::move()返回对象时会发生什么?不管是写return object;还是写return move(object);,在这两种写法下,编译器都将其视为右值表达式。

但是,通过使用move(),编译器无法再应用RVO和NRVO,因为这只适用于形式为return object;的语句。由于RVO和NRVO不在适用,编译器的下一个选择是在对象支持的情况下使用移动语义。如果不支持,使用复制语义,这会对性能产生很大影响!因此,当从函数中返回一个局部变量或参数时,只要写return object;就可以了,不要使用move();

注意:(N)RVO仅适用于局部变量或者函数参数。因此,返回对象的数据成员不会触发(N)RVO,此外还需要注意以下形式:

return condition ? object1 : object2;

这不是return object;的形式,所以编译器不会应用(N)RVO,而是使用拷贝构造函数返回object1或者object2,你可以重写返回语句,使其支持(N)RVO。

if (condition) {
    return object1;
} else {
    return object2;
}

如果确实想使用条件运算符,可以使用move()编写,但注意,这不会触发(N)RVO并强制使用移动语义或复制语义:

return condition ? move(object1) : move(object2);
向函数传递参数的最佳方法

对于非基本类型的函数参数建议使用const引用参数,以避免对传递给函数的实参进行不必要的昂贵复制。但是好,如果混合使用右值,情况就发生了改变。假设有一个函数复制了作为其参数之一传递的实参。例如:

class DateHolder
{
public:
	void setData(const vector<int>& data) { m_data = data; }
private:
	vector<int> m_data;
};

setData()方法生成一份传入数据的副本。为了避免右值情况下的任何复制,需要添加一个重载优化setData()方法。

class DateHolder
{
public:
	void setData(const vector<int>& data) { m_data = data; }
	void setData(vector<int>& data) { m_data = move(data); } 
private:
	vector<int> m_data;
};

当以临时值调用setData()时,不会产生任何复制,数据会被移动。

以下代码中触发对setData()的const引用重载版本的调用,从而生成数据的副本。

DataHolder wrapper;
vector maData { 1, 2, 3 };
wrapper.setData(myData);

另外,下面的代码使用临时变量调用setData(),这会触发对setData()的右值引用重载版本的调用。随后将移动数据,而不是复制数据。

wrapper.setData({1, 2, 3});

但是,这种为左值和右值优化setData()的方法需要实现两个重载。那么有没有更好的方法呢?有的!那就是值传递!到目前为止,建议使用const引用参数来传递对象,以避免任何不必要的复制,但是现在我们使用值传递。需要澄清的是,对于不被复制的参数,通过const引用传递仍然是应使用的方法,值传递建议仅适用于函数无论如何都要复制的参数。在这种情况下,通过使用值传递语义,代码对于左值和右值都是最优的。如果传入一个左值,它只复制一次,就像const引用那样;如果传入一个右值,则不会进行复制,就像右值引用参数一样。例如:

class DateHolder
{
public:
	void setData(vector<int> data) { m_data = move(data); }
private:
	vector<int> m_data;
};

上述代码,如果将左值传递给setData(),则会将其复制到data参数中,然后移动到m_data。如果将右值传递给setData(),则会将其移动到data参数中,然后再次移动到m_data中。

注意:对于函数本身将复制的参数,更倾向于值传递,但仅当该参数属于支持移动语义的类型时。否则,请使用const引用参数。

零规则

零规则指出,在设计类时,应当使其不需要5个特殊成员函数(析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符)。为了要做到这一点,应当避免拥有任何旧式的、动态分配的内存。改用现代的结构,如标准库容器。例如在Spreadsheet类中,用vector<vector< SpreadsheetCell >>替代SpreadsheetCell**数据成员。vector自动处理内存,因此不需要上述5个特殊成员函数。

本文所用到完整代码如下

class SpreadsheetCell
{
public:
    SpreadsheetCell(){};
    SpreadsheetCell(int value) : m_val{value}
    {
        // cout << "mval = " << m_val << endl;
    }
    int getv() const
    {
        return m_val;
    }
    friend class Spreadsheet;

private:
    int m_val;
};

class Spreadsheet
{
public:
    Spreadsheet(size_t width, size_t height);
    Spreadsheet(const Spreadsheet &src);
    Spreadsheet &operator=(const Spreadsheet &rhs);

    Spreadsheet(Spreadsheet &&src) noexcept;
    Spreadsheet &operator=(Spreadsheet &&rhs) noexcept;

    void setCellAt(size_t x, size_t y, const SpreadsheetCell &cell);
    SpreadsheetCell &getCellAt(size_t x, size_t y);

    void verifyCoordinate(size_t x, size_t y) const;
    void mySwap(Spreadsheet &other) noexcept;
    void print() const;
    ~Spreadsheet();

private:
    size_t m_width{0};
    size_t m_height{0};
    SpreadsheetCell **m_cells{nullptr};
    // void cleanup() noexcept;
    // void moveFrom(Spreadsheet &src) noexcept;
};

Spreadsheet::Spreadsheet(size_t width, size_t height)
    : m_width{width}, m_height{height}
{
    cout << "Spreadsheet(size_t width, size_t height)" << endl;
    m_cells = new SpreadsheetCell *[m_width]; // 申请分别指向每一列的指针数组
    for (size_t i{0}; i < m_width; ++i)       // 分别为每一列的元素都申请空间,共有m_width列
    {
        m_cells[i] = new SpreadsheetCell[m_height];
    }
}

void Spreadsheet::verifyCoordinate(size_t x, size_t y) const
{
    if (x >= m_width)
    {
        // 输入范围错误,抛出异常
        throw out_of_range{format("{} must be less than {}!", x, m_width)};
    }
    if (y >= m_height)
    {
        throw out_of_range{format("{} must be less than {}!", y, m_height)};
    }
}

void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell &cell)
{
    verifyCoordinate(x, y);
    m_cells[x][y] = cell;
}

SpreadsheetCell &Spreadsheet::getCellAt(size_t x, size_t y)
{
    verifyCoordinate(x, y);
    return m_cells[x][y];
}

Spreadsheet::~Spreadsheet()
{
    cout << "~Spreadsheet()" << endl;

    // m_cells是二级指针,所以一定要先释放一级指针,否则就会内存泄漏
    for (size_t i{0}; i < m_width; ++i)
    {
        delete[] m_cells[i];
    }
    delete[] m_cells;
    m_cells = nullptr;
    m_height = 0;
    m_width = 0;
}

Spreadsheet::Spreadsheet(const Spreadsheet &src)
    : Spreadsheet{src.m_width, src.m_height} // 委托有参构造函数
{
    cout << "Spreadsheet(const Spreadsheet &src)" << endl;

    for (size_t i{0}; i < m_width; ++i)
    {
        for (size_t j{0}; j < m_height; ++j)
        {
            m_cells[i][j] = src.m_cells[i][j];
        }
    }
}

void Spreadsheet::print() const
{
    for (int i = 0; i < m_width; ++i)
    {
        for (int j = 0; j < m_height; ++j)
        {
            cout << m_cells[i][j].m_val << " ";
        }
    }
    cout << endl;
}
#if 0
Spreadsheet &Spreadsheet::operator=(const Spreadsheet &rhs)
{
    // 自赋值检查
    if (this == &rhs)
    {
        return *this;
    }
    // 释放旧内存
    for (size_t i{0}; i < m_width; ++i)
    {
        delete[] m_cells[i];
    }
    delete[] m_cells;
    m_cells = nullptr;

    // 申请新的内存
    m_width = rhs.m_width;
    m_height = rhs.m_height;
    m_cells = new SpreadsheetCell *[m_width];
    for (size_t i{0}; i < m_width; ++i)
    {
        m_cells[i] = new SpreadsheetCell[m_height];
    }

    // 复制数据
    for (size_t i{0}; i < m_width; ++i)
    {
        for (size_t j{0}; j < m_height; ++j)
        {
            m_cells[i][j] = rhs.m_cells[i][j];
        }
    }
    return *this;
}
#endif

void Spreadsheet::mySwap(Spreadsheet &othre) noexcept
{
    swap(m_width, othre.m_width);
    swap(m_height, othre.m_height);
    swap(m_cells, othre.m_cells);
}

// 非成员的myswap函数只是简单地调用myswap方法
void mySwap(Spreadsheet &firsht, Spreadsheet &second) noexcept
{
    firsht.mySwap(second);
}

Spreadsheet &Spreadsheet::operator=(const Spreadsheet &rhs)
{
    cout << "operator=(const Spreadsheet &rhs)" << endl;
    Spreadsheet temp{rhs};
    mySwap(temp);
    return *this;
}

/* void Spreadsheet::cleanup() noexcept
{
    for (size_t i{0}; i < m_width; ++i)
    {
        delete[] m_cells[i];
    }
    delete[] m_cells;
    m_cells = nullptr;
    m_width = m_height = 0;
}
void Spreadsheet::moveFrom(Spreadsheet &src) noexcept
{
    m_width = exchange(src.m_width, 0);
    m_height = exchange(src.m_width, 0);
    m_cells = exchange(src.m_cells, nullptr);
}

Spreadsheet::Spreadsheet(Spreadsheet &&src) noexcept
{
    cout << "Spreadsheet(Spreadsheet &&src)" << endl;
    moveFrom(src);
}

Spreadsheet &Spreadsheet::operator=(Spreadsheet &&rhs) noexcept
{
    cout << "operator=(Spreadsheet &&rhs)" << endl;
    // 自赋值检查
    if (this == &rhs)
    {
        return *this;
    }
    cleanup();
    moveFrom(rhs);
    return *this;
} */

Spreadsheet::Spreadsheet(Spreadsheet &&src) noexcept
{
    cout << "Spreadsheet(Spreadsheet &&src)" << endl;
    mySwap(src);
}

Spreadsheet &Spreadsheet::operator=(Spreadsheet &&rhs) noexcept
{
    cout << "operator=(Spreadsheet &&rhs)" << endl;
    mySwap(rhs);
    return *this;
}

Spreadsheet createObject()
{
    return move(Spreadsheet{3, 2});
}

原创博文,转载请注明出处!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++,可以使用new来动态地在堆内存分配对象。例如,用 new ClassName()语法实现从堆内存分配ClassName对象,并将此对象的地址存储在ClassName *类型指针。\[2\]这种方式可以在程序运行时动态地分配内存,而不是在编译时就确定内存大小。这对于需要根据运行时条件来确定内存大小的情况非常有用。 另外,在C++,std::string类也会动态地分配内存来存储字符串数据。当我们给std::string赋值一个较长的字符串时,如果当前分配的内存空间不足以容纳新的字符串,std::string会动态地分配更多的内存来存储新的字符串,并将原先的内容拷贝到新的内存空间。\[3\]这样可以确保std::string能够容纳任意长度的字符串,但也会带来一定的性能开销。 总结起来,C++动态内存分配可以通过new关键字来实现对象的动态分配,而std::string类则会在需要时动态地分配内存来存储字符串数据。这样可以灵活地管理内存,并确保能够容纳任意长度的字符串。 #### 引用[.reference_title] - *1* *3* [C++ string介绍和坑](https://blog.csdn.net/weixin_43679037/article/details/127536657)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [C++动态内存分配new](https://blog.csdn.net/qq_40965507/article/details/119383348)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值