对象的复制与赋值操作

C++中动态分配对象的内存有着很微妙的地方,下面就来简单说一下:

结论:如果在类中动态分配了内存,那么就应该编写自己的复制构造函数以及赋值运算符,来提供深层次的内存复制。

动态分配对象内存的好处:有时候在程序运行之前,我们无法知道具体需要多少内存空间,比如编写一个类时,不知道类的某个对象需要占多少内存,这个时候我们就需要动态分配对象内存空间了。动态分配内存使我们能够在想要一块内存的时候就去分配一块我们想要的大小的内存空间。

动态分配对象内存需要注意的地方:

1. 释放内存:我们要在不需要动态分配的内存时释放它们,可以在析构函数中进行释放操作。看下面类的定义:

#include "Square.h"
/**假如是实现一个类似九宫格的样式, 每个格子都是一个Square对象**/
class Collect{
    public:
        Collect(int width, int height);
    protected:
        int mWidth, mHeight;
        Square ** mSquare;
};

Collect类包含了一个Square**变量,原因是Collect对象的样式可能不同(除了9宫格, 还可能是其他样子),因此类的构造函数必须能够根据用户指定的宽度和高度去动态的创建一个二维数组, 构造函数可以如下:

#include "Collect"

Collect::Collect(int width, int height):mWidth(width),  mHeight(height) {
    mSquare = new Square * [width];
    for (int i=0; i<width; ++i) {
        mSquare[i] = new Square[height];
    }
}

析构函数可能如下所示:

Square :: ~Square() {
    for (int i=0; i<mWidth; ++i) {
        delete [] mSquare[i];
    }

    delete [] mSquare;
    mSquare = nullptr;    // C++ 11
 }

2. 对象复制:

#include "Collect"

void printCollect (Collect c) {

    // do some thing
}

int main () {

    Collect c1(4, 5);  // 复制c1以初始化c
    printCollect (c1);

    return 0;
}

Collect 类包含了一个指针变量 mSquare,当执行printCollect (c1) 时,向目标对象c提供了一个mSquare指针的副本,但是没有复制底层的数据,最终导致c和c1都指向了同一块内存空间,如图所示:

collect_1 中的mSquare和 collect_2中的mSquare都指向了同一块堆内存,这将导致的后果:

1. collect_1 修改了mSquare所指的内容,这一改动也会在collect_2中表现出来

2. 当函数printCollect()退出的时候,会调用c的析构函数,从而释放mSquare所指的内存,那么c1就变成了野指针,访问时可能会造成程序崩溃

如果编写了下面代码:

Collect c1(4, 5), c2(4, 2);
c1 = c2;

首先c1, c2 中的mSquare指针变量都指向了一块内存空间,当执行c1=c2时,c1中的mSquare指向了c2中mSquare指向的内存,并且在c1中分配的内存将泄漏。这也是在赋值运算符中首先要释放左边引用的内存,然后再进行深层复制的原因。

至此,我们已经了解到,依赖C++默认的复制构造函数或者赋值运算符未必是个好主意。

Collect 的复制构造函数:

// Collect.h

class Collect {
    public:
        Collect (int width, int height);
        Collect (const Collect & src);
};

// Collect.cpp

Collect :: Collect (const Collect & src) {
    mWidth = src.mWidth;
    mHeight = src.mHeight;
    mSquare = new Square * [mWidth];
    for (int i=0; i<mWidth; ++i) {
        mSquare[i] = new Square[mHeight];
    }

    for (int i=0; i<mWidth; ++i) {
        for (int j=0; j<mHeight; ++j) {
            mSquare[i][j] = src.mSquare[i][j];
        }
    }
}

需要注意的是,复制构造函数复制了所有的数据成员,包括mWidth和mHeight,而不仅仅是指针数据成员。其余的代码对mSquare动态分配的二维数组进行了深层复制。

3. 对象赋值

对象赋值其实就是重载了赋值运算符,看下面代码:

// Collect.h

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

/*
  当对象进行赋值的时候已经被初始化过了,
  所以必须在分配新的内存之前释放动态分配的内存,
  可以将赋值运算看成析构函数以及复制构造函数的结合
*/

// Collect.cpp

Collect & Collect :: operator= (const Collect & rhs) {

    if (this == &rhs)
        return *this;

    for (int i=0; i<mWidth; ++i) {
        delete [] mSquare[i];
    }

    delete [] mSquare;
    mSquare = nullptr;

    mWidth = rhs.mWidth;
    mHeight = rhs.mHeight;
    mSquare = new Square * [mWidth];
    for (int i=0; i<mWidth; ++i) {
        mSquare[i] = new Square[mHeight];
    }
    for (int i=0; i<mWidth; ++i) {
        for (int j=0; j<mHeight; ++j) {
            mSquare[i][j] = rhs.mSquare[i][j];
        }
    }

    return *this;
}

注意:只要类中动态分配了内存空间,就应该自己编写析构函数、复制构造函数以及重载赋值运算符。

在这里顺便提一句,如果想在const方法中改变对象的某个数据成员,那么可以在成员变量前添加“mutable”关键字,例如:

// Test.h

class Test {

    double mValue;
    mutable int mNumAccesses = 0;
};

// Test.cpp

double Test :: getValue () const {
    mNumAccesses ++;
    return mValue;
}
时间: 2024-12-22 06:08:08

对象的复制与赋值操作的相关文章

复制构造函数 与 赋值操作函数

1 class Widget{ 2 3 Widget(); //默认构造函数 4 5 Widget(const Widget& rhs); //复制构造函数 6 7 Widget& operator= (const Widget& rhs);//赋值操作函数 8 9 }; 10 Widget w1; //调用默认构造函数 11 Widget w2(w1); //调用复制构造函数 12 w1 = w2 ; //调用赋值操作函数 上面的语句很好理解,但是需要我们注意的是“=”也可以用来

C风格字符串和C++ string 对象赋值操作的性能比较

<<C++ Primer>> 第四版 Exercise Section 4.3.1 部分Exercise 4.2.9 习题如下: 在自己本机执行如下程序,记录程序执行时间: 1 #include "stdafx.h" 2 #include <iostream> 3 #include <string> 4 #include <vector> 5 #include <ctime> 6 7 using namespace

JavaScript对象属性赋值操作的逻辑

对象进行属性赋值操作时,其执行逻辑如下所示: 1. 当前对象中是否有该属性?有,进行赋值操作:没有,进行下一步判断. 2. 对象的原型链中是否有该属性?没有,在当前对象上创建该属性,并赋值:有,进行下一步判断. 3. 原型链中该属性是否允许操作?是,在当前对象上创建同名属性,并赋值:否,属性赋值失败. 无论是属性赋值还是新建属性,都是在当前对象上进行的,不会修改原型链!第三种情况下,新建的属性将会覆盖对象从原型链继承来的同名属性.

对象的复制(clone、序列化)

那就先看是clone方法复制对象  摘自 java.lang.Object.clone()分析 首先,看一下源码:public class Object  {    protected native Object clone() throws CloneNotSupportedException;} 由源代码我们会发现: 第一:Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于Java中的 非native方法.这也解释了为什么要用Object中cl

C++:地址传递以及赋值操作的内存管理规范

# C++:地址传递以及赋值操作的内存管理规范# 为了保证类实例能确定.有效的清除自己创建的内存占用,同时保证低内存占用,应该使类内部的数据尽量使用引用传递,对于外部传入的对象本身可以使用引用传递,对于外部对象拥有的各种数据,例如其他对象或是已经传入到内部的对象的字段.方法返回值,都应该使用复制值操作来获取数据,而不是重复使用引用. 关于引用对象本身而不是对象的数据,这里有一个正确的数据保证这样做的有效性.如果引用的对象先消失,则它可以清除自己的内存,如果引用者先消失,也不会影响引用的对象的内存

c++总结之类型,对象的定义和声明,对象的初始化和赋值

一.对象的类型 对象的类型决定了对象占用内存空间的大小,和内存的布局,内存中可存储值的范围以及对该对象可以进行的操作,由于对象的类型决定可以对其执行的操作,因此const属性也可以看做对象类型的组成部分.类型又分为静态类型和动态类型,对于普通对象,静态类型和动态类型一般是一致的:对于指针和引用类型,静态类型和动态类型可以相同也可以不同,静态类型是指针和引用定义时声明的类型,而动态类型是指程序运行时实际绑定的类型.当静态类型和动态类型不同时,一般来说有两种情况:一是指涉到常量的指针和引用绑定了一个

关于js 浅拷贝 深拷贝 以及赋值操作。

最近同事又碰到关于深浅拷贝以及赋值的问题,今天我也研究一下记录一下,加深一下记忆. 举一个简单的例子: var people = { age:10, name:"小华", arr:[1,2,3] }; 做一个 赋值操作: var people2 = people; 然后做一个浅拷贝操作: var people3 = {}; for (var i in people){ people3[i] = people[i] }; 然后最后再做一个深拷贝操作: function deepClone

对象的复制和引用

1 对象的引用: 对象的引用其实就是对同一个对象进行操作,只是在原对象的基础上进行操作 例如: void fun(box &T){ box.length ++; } 2 对象的复制 而对象的复制就是创建一个新的对象,把一个对象的值赋值给这个新对象,赋值的 过程中首先调用复制构造函数  Box::Box(const Box&b) { height = b.height; width = b.width; length = b.length; }把传入对象的值赋给新创建的对象 凡是对象的拷贝或

对象 与 解构赋值

对象 定义:在javascript中,对象就是拥有属性和方法的无需数据集合 创建对象: 1.object构造函数 var person=new object(); 2.字面量方式 var person = {}; 对象的属性: 对象属性的分类 1.数据属性:一般用于储存数据值 2.访问器属性:不包含数据值,多用于get/set操作 定义对象的数据属性 通过点运算符.来操作一个对象的属性 person.name="zhangsan"; 通过方括号[]操作一个对象的属性 person[&q