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

news2024/12/29 9:26:50

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});
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/542239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL—系统管理

文章目录 一、系统数据库二、常用管理工具mysqlmysqladminmysqlbinlogmysqlshowmysqldumpmysqlimport/source 一、系统数据库 MySQL安装后&#xff0c;默认会带4个数据库&#xff1a; 数据库含义mysql存储MySQL服务器正常运行所需要的各种信息 &#xff08;时区、主从、用户、…

【CAN卡通信的下位机-STM32cubeIDE-hal库+STMF4xx+数据发送和接收+中断接收方式+基础样例(2)】

【CAN卡通信的下位机-STM32cubeIDE-hal库STMF4xx数据发送和接收中断接收方式基础样例2】 1、概述2、实验环境3、问题描述4、自我尝试与努力(1)在网上查照相关配置文章&#xff0c;进行配置对比。(2)对比st的官方样例例程。(3)请教大佬&#xff0c;帮忙查看代码和调试。(4)总之…

【数据结构】散列表(哈希表)

文章目录 前言一、什么是散列表二、什么是哈希函数三、下面简单介绍几种哈希函数四、冲突处理散列冲突的方法开放定址法再散列函数法公共溢出区法链地址法 五、代码实现1.哈希函数2.链表和哈希表的创建3.哈希表初始化3.从哈希表中根据key查找元素4.哈希表插入元素5.元素删除6.哈…

100天精通Python(可视化篇)——第85天:matplotlib绘制不同种类炫酷气泡图参数说明+代码实战(网格、自定义颜色、钟型、交互、打卡、动态气泡图)

文章目录 专栏导读1. 气泡图介绍1&#xff09;介绍2&#xff09;参数说明 2. 普通气泡图3. 网格气泡图4. 自定义气泡图颜色5. 不同颜色气泡图6. 钟型气泡图7. 交互气泡图8. 打卡气泡图9. 动态气泡图 专栏导读 &#x1f525;&#x1f525;本文已收录于《100天精通Python从入门到…

实战打靶集锦-020-Tre

提示&#xff1a;本文记录了博主一次艰难又失败的提权经历 目录 1. 主机发现2. 端口扫描3. 服务枚举4. 服务探查4.1 Apache探查4.1.1 adminer.php4.1.2 /cms/目录4.1.3 /info.php页面4.1.4 nikto扫描4.1.5 dirb扫描4.1.6 登录Adminer 5. 提权5.1 系统信息枚举5.2 定时任务枚举5…

SocketTools 11在所有HTTP客户端组件支持

SocketTools 11在所有HTTP客户端组件支持 在所有HTTP客户端组件中添加了对HTTP/2.0协议的支持。 更新了TLS 1.2(及更高版本)和SSH 2.0的安全选项&#xff0c;以使用Microsoft Windows 11和Windows Server 2022中提供的密码套件。较旧、安全性较低的密码套件已被弃用&#xff0…

21级计组硬件实验三-八位串行加法器验证

问题1&#xff1a; 小明同学正在做【半加器、全加器与八位串行加法器实验】&#xff0c;他画好了电路图&#xff0c;但还有不少困难&#xff0c;现在他求助于你&#xff0c;请你帮他解决。 答&#xff1a;选A 注释&#xff1a;A是与门&#xff0c;B是或门&#xff0c;C是同…

【cmake】cmake 实现交叉编译

在PC上开发时&#xff0c;我们可以直接在PC端编译、链接、运行&#xff0c;但是到了嵌入式环境&#xff0c;由于嵌入式的设备资源&#xff08;CPU、RAM&#xff09;无法和PC端相比&#xff0c;如果项目比较复杂&#xff0c;很难甚至不可能在设备上编译。因此&#xff0c;我们一…

vue diff算法与虚拟dom知识整理(9) 手写patch递归子节点上树,用自己写的patch实现虚拟节点替换

上文 我们做到让一个文字虚拟节点上树 但子节点显然还 没有完成 那这次我们继续 递归的话 我们需要换个思路 我们将 src下的入口文件 index.js代码改成这样 import h from "./snabbdom/h"; import patch from "./snabbdom/patch";const container docum…

Apache Zeppelin系列教程第七篇——运行paragraph的整个流程分析

Zeppelin运行paragraph的整个流程分析 前文分别讲述了&#xff0c;JdbcInterpreter、Interpreter、Zengine的流程&#xff0c;本文来主要串联起来paragraph的整个流程 前端 首先前端部分点运行的时候是通过websocket向后端发送请求的zeppelin-web/src/components/websocket/…

swp协议-1

Swp接口是UICC和CLF&#xff08;非接前端&#xff09;之间的面向比特流&#xff0c;点到点通信的协议。CLF是主设备&#xff08;master&#xff09;&#xff0c;UICC是从设备&#xff08;slave&#xff09;。图SWP数据传输虽然是单线协议&#xff0c;但是是全双工数字传输。 1 …

电商项目之海量操作日志的实现

文章目录 1 问题背景2 前言3 思考4 解决思路5 交互6 工作原理7 伪代码实现7.1 安装并配置Canal Server7.2 Canal客户端拉取MQ消息7.3 Canal数据的转换7.4 定制自己的业务逻辑 1 问题背景 有时候客户做了某些操作却不认账&#xff0c;咱们又拿不出证据&#xff1b;有时候客户将账…

入参校验1

文章目录 一、简介1、快速失败(Fail Fast) 二、单字段类入参校验三、JSON实体类校验1、注解解析2、案例1、简单校验2、分组校验3、嵌套校验4、集合校验5、自定义校验 四、相关1、源码文件2、参考地址 一、简介 1、快速失败(Fail Fast) Spring Validation 默认会校验完所有字段…

GPT-4的免费使用方法分享(续)

GPT-4的免费使用方法分享_我爱OJ的博客-CSDN博客 在这篇博客里&#xff0c;我介绍了一些ChatGPT的一些使用方法&#xff0c;但可能有一定的缺陷&#xff0c;有的需要魔法&#xff0c;所以&#xff0c;今天我就来亲测一下&#xff0c;关于ChatGPT的一些免费使用技巧 目录 镜像…

代码随想录算法训练营第九天|KMP算法

记录一下KMP算法&#xff0c;本文摘录自《代码随想录》和部分b站视频帮你把KMP算法学个通透&#xff01;&#xff08;理论篇&#xff09;_哔哩哔哩_bilibili最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibiliKMP字符串匹配算法2_哔哩哔哩_bilibili KMP算法 主要应用&#xff1a;字…

牛客小白月赛65

题目链接 牛客小白月赛65 A-牛牛去购物&#xff08;枚举&#xff09;B-牛牛写情书&#xff08;字符串&#xff09;C-牛牛排队伍&#xff08;模拟&#xff09;D-牛牛取石子&#xff08;博弈&#xff09;E-牛牛的构造&#xff08;构造&#xff0c;思维&#xff09; A-牛牛去购物…

怎么免费使用 ChatGpt,实用!

最近发现了一个可以免费、轻松使用 ChatGpt 的方法&#xff0c;随即做个记录&#xff0c;留着备忘&#xff0c;以后想用也能随时找到方法。 但是不保证该方法永远有效&#xff0c;仅当下有限&#xff0c;做个记录罢了。 因为我使用的是 windows 自带的浏览器 Microsoft Edge &a…

Android--刷机与adb

目录 一、Android设备启动流程 二、刷机模式介绍 三、Windows命令行 四、adb介绍与配置 五、常用的adb命令 一、Android设备启动流程 Android就是Linux内核(Kernel)Java虚拟机(JVM) Android设备启动就分为两个阶段&#xff1a; Linux启动 1.启动电源以及系统启动&#…

详解c++STL—容器list

目录 1、list基本概念 1.1、概念描述 1.2、结点的组成 1.3、list的优点 1.4、list的缺点 1.5、总结 2、list构造函数 2.1、功能描述 2.2、函数原型 2.3、示例 3、list赋值和交换 3.1、功能描述 3.2、函数原型 3.3、示例 4、list大小操作 4.1、功能描述 4.2、函…

ChatGPT的前世今生——混沌初开

目录 ChatGPT的前世今生——混沌初开ChatCPT简介ChatCPT是什么&#xff1f;ChatCPT的火爆程度ChatCPT火爆的原因1、功能强大&#xff0c;应用范围广泛2、训练数据量大&#xff0c;模型效果好3、优秀的商业模式 OpenAI公司公司创始团队 总结公众号文章链接参考链接&#xff1a; …