什么时候会用到拷贝构造函数?

本文探讨了拷贝构造函数的应用场景及深拷贝的重要性,特别是在类中包含指针成员时,通过实例展示了浅拷贝的问题,并介绍了深拷贝的实现方式,包括引用计数方法。

拷贝构造函数顾名思义就是有两个特点:1是构造函数,2该函数有一个同类型的参数,函数的功能就是将该参数的内容拷贝到本身。
通常在一个类(结构)中有指针成员的时候需要定义拷贝构造函数,可以不需要定义。

 

1-什么时候会用到拷贝构造函数?
2-什么时候有必要手动写拷贝构造函数?

1-什么时候会用到拷贝构造函数?
           当任何你想复印东西的时候,而不管东西被复印成什么样子。即任何你想利用一个已有的类实例给另一个类实例赋值时,这种赋值可能是显式的,也可能是隐式的
显式:classa_1=class_2;
隐式:函数的形参有用到类对象却没有用引用或传址技术时
               函数的返回值是一个对象也没有应用传址技术时
2-什么时候有必要用拷贝构造函数?
上述3种情况,如果没有涉及到深拷贝问题,就没有必要自己来编写拷贝构造函数,编译器有默认的可以很完美的完成任务
还一种情况就是变态的拷贝:在拷内过程中进行缩放,放大等处理,不过没什么用

//关于深拷贝以下是引用:traceback:http://www.netology.cn/blog/user1/bachelor/archives/2006/375.html

关于深拷贝
如果一个类中含有指针成员变量,则在利用一个已存在对象构造新的对象时,就会面临两种选择:深拷贝和浅拷贝。
       浅拷贝只是将对象间对应的指针成员变量进行简单的拷贝,即拷贝结束后新旧对象的指针指向相同的资源(指针的值是相同的);这种拷贝会导致对象的成员不可用,如下例:
        class Person
        {
           public :
           //....
           char * home;//the person's home
           void SetHome(char * str)
           {home = str;}
           ~Person()
            {
              //...
              delete [] home;
            }
        }
       //....
       char * place = new char [20];
       strcpy(place,"China");
       Person *A = new Person();
       A->SetHome(place);
   
       Person * B= Person(A);
       delete A;
      //....
      此时对象A 和对象B的成员home值相同,如果A对象 destroy,则对象B的成员home指向的地址变为不可用(对象A撤销时将home指向的资源释放了)。
       深拷贝是相对于浅拷贝而言的,为了避免上述情况的发生,将上例中的代码改造:对象拷贝时将指针指向的内容拷贝,代码如下:
      class Person
        {
           public :
           //....
           char * home;//the person's home
           void SetHome(char * str)
           {home = str;}
       
           Person & Person(const Person & per)
           {
                //...
                if(* this == per)//copy itself
        return *this;
                home = new char[strlen(per.home) +1];//alloc new memory
       strcpy(home,per.home);
       return * this;
           }
           ~Person()
            {
              //...
              delete [] home;
            }
        }
     深拷贝之后,新旧对象的home成员指向的内容的值相同,而其自身的值不同。这样就可避免出现其中之一
     destroy 之后,另一对象的home成员不可用。
     但是,此种开辟新的内存区域然后赋值拷贝的方法在面临指针成员指向的数据量很大时出现效率低下的问题,因此又有下面的处理方法:为指针保留引用计数(reference counting)。
     当类每次拷贝这个指针时,就增加它的计数;对象destroy时检察指针的引用计数,如果为0就删除其指向的资源,否则计数减小。如果指针所指数据相对恒定是,指针引用计数尤为有效。而且在数据量庞大的时候,可以有较高的执行效率。
      类示例代码如下:

//一个引用计数器很容易作为一个类来完成
//用于带一个引用计数值得void指针的类
class VoidRefPtr
{
public:
     //constructor allocates memory for data
     VoidRefPtr(void * pData,int nSize)
     {
      ptr = malloc(nSize);
      memcpy(ptr,pData,nSize);
      nCount = 1;
     }

     //increment the reference count
     VoidRefPtr * Ref()
     {
      nCount ++;
      return this;
     }

     //decrement the count and delete if zero
     void Derf()
     {
      if(!--nCount)
       delete this;
     }

     ~VoidRefPtr()
     {
      delete ptr
     }

     void * ptr()
     {
      return ptr
     }
private:
     void * ptr;//the data
     unsigned int nCount;//the reference count

};

//这个类包含了一个实际的指针和用于对引用计数器自加自减的成员函数
//当引用计数器减到零时,对象撤销自身。
class RefcountCharPtr
{
public:
RefcountCharPtr(const char * pData ="")
{
     //constructor creates a ref pointer
     pRef = new VoidRefPtr((void *)pData,strlen(pData)+1);
}

RefcountCharPtr(const RefcountCharPtr &a)
{
     //copy constructor increments ref count
     pRef = a.pRef->Ref();
}

~RefcountCharPtr()
{
     //destructor decrements ref count
     pRef ->Derf();
}

const RefcountCharPtr & operator = (const RefcountCharPtr & a)
{
     //asignment
     //decrement ref count of existing pointer
     pRef->Derf();
     //increment that of assigned pointer
     pRef = a.pRef->Ref();
     return * this;
}

RefcountCharPtr & operator =(const char * pData)
{
     //char string assignment
     //decrement ref count of existing pointer
     pRef->Derf();
     //make new pointer
     pRef = new VoidRefPtr((void *)pData,strlen(pData)+1);
     return *this;
}

//type conversion to a char string
operator const char * ()
{
     return (char * )pRef->ptr();
}

protected:
private:
VoidRefPtr * pRef;
};
     一个同学给出了一个类似的实现:
      class RefData
{
public:
     RefData(char *s)
     {
      data = new String(s);
     }
int refCount;
String *data;
};

class MyString
{
public:
     MyString(const char *s = "");
//...
//...
private:
     RefData * value; // 私有指针变量
};

MyString::MyString(const char *s = ""):value(new RefData(s))
{
}

MyString & MyString::operator=(const MyString & str)
{
if (value == str.value) //如果是自己复制给自己
     return *this;
if (--(value->refCount) == 0)
     delete value;
value = s.value; // 只拷贝指针
++(value->refCount);
return *this;
}

char & MyString::operator[](int dex) //改变值的时候
{
if (value->refCount >1)
{
     --value->refCount;
     value = new RefData(value->data)
}
return value->data[index];
}

//测试示例
int main()
{
MyString s1 ="hello";
char *p = &s1[1];
MyString s2 = s1;
*p = 'x';
}

     虽然引用计数的处理方法貌似不错,但是其亦存在其弊端:在多线程的条件下,其执行效率尤为低下。具体参见(Herb Shutter的在C++ Report杂志中的Guru专栏以及整理后出版的《More Exceptional C++》中专门的讨论)。
    
     最近在看设计模式的一些东东,偶然又发现了下面的一段:
     下面是一些可以使用Proxy模式的常见情况:
     ...
     4)智能指引(smart reference)--取代了简单的指针,它在访问对象时执行一些附加操作。它的典型用途包括:
      对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为 smartpointers[Ede92]).
      当第一次引用一个持久对象时,将它装入内存。
      在访问一个实际对象前,检察是否已经锁定了它,以确保其他对象不能改变它。
      以上的叙述的用途好像有些像引用计数,但是具体如何,现在偶还不是很清楚,准备将两者放在一起,看其联系和区别究竟如何。
      以上只是最近在看c++时遇到的一点问题,将其放在一起讨论,希望可以明白其原理和实现。

<think> 我们正在讨论C++中的拷贝构造函数。根据用户的问题,他想要了解拷贝构造函数的定义以及使用场景。结合提供的引用内容,我们可以总结如下: ### 拷贝构造函数的定义 拷贝构造函数是C++类的一种特殊构造函数,用于使用一个已存在的对象来初始化一个新对象。其定义必须满足以下特征: 1. **函数名与类名相同**(因为是构造函数) 2. **参数必须是同类对象的引用**(通常是常量引用`const ClassName&`) 3. **无返回值类型** 定义格式: ```cpp ClassName(const ClassName& other); // 标准形式 ``` ### 拷贝构造函数的调用场景 根据引用内容,拷贝构造函数主要在以下场景被调用: 1. **显式使用对象初始化新对象** 当使用一个已存在对象初始化另一个新对象时: ```cpp AA obj1; AA obj2(obj1); // 调用拷贝构造函数[^3] AA obj3 = obj1; // 调用拷贝构造函数(注意这不是赋值)[^3] ``` 2. **函数参数传递** 当对象作为函数参数**按值传递**时: ```cpp void func(AA obj); // 按值传参 int main() { AA original; func(original); // 调用拷贝构造函数创建形参obj[^2] } ``` 3. **函数返回对象** 当函数按值返回对象时(可能触发拷贝构造,但编译器通常会优化): ```cpp AA createAA() { AA local; return local; // 可能调用拷贝构造创建临时对象[^2] } ``` 4. **容器操作** 当对象被放入标准容器(如`vector`)时: ```cpp std::vector<AA> vec; AA item; vec.push_back(item); // 调用拷贝构造函数复制对象到容器[^1] ``` ### 默认拷贝构造函数 若未显式定义拷贝构造函数: - 编译器会自动生成默认版本 - 默认行为:**浅拷贝**(逐成员字节复制)[^2] - **风险**:若类管理动态内存,浅拷贝会导致多个对象共享同一资源,引发双重释放错误 ### 需要自定义拷贝构造函数的场景 当类涉及资源管理(如动态内存、文件句柄等)时,必须自定义拷贝构造函数实现**深拷贝**: ```cpp class String { public: String(const char* str = "") { m_data = new char[strlen(str) + 1]; strcpy(m_data, str); } // 自定义深拷贝 String(const String& other) { m_data = new char[strlen(other.m_data) + 1]; // 重新分配内存 strcpy(m_data, other.m_data); // 复制内容 } ~String() { delete[] m_data; } private: char* m_data; }; ``` ### 完整示例 ```cpp #include <iostream> #include <cstring> class Data { public: Data(const char* s) { size = strlen(s); ptr = new char[size + 1]; strcpy(ptr, s); std::cout << "构造函数: " << ptr << "\n"; } // 自定义拷贝构造函数(深拷贝) Data(const Data& other) { size = other.size; ptr = new char[size + 1]; // 关键:独立分配内存 strcpy(ptr, other.ptr); std::cout << "拷贝构造: " << ptr << "\n"; } ~Data() { std::cout << "析构: " << ptr << "\n"; delete[] ptr; } private: char* ptr; size_t size; }; void test(Data d) { // 按值传参触发拷贝构造 // 函数结束时会析构形参d } int main() { Data orig("Original"); Data copy1 = orig; // 场景1:显式初始化 test(orig); // 场景2:函数传参 return 0; } ``` **输出**: ``` 构造函数: Original 拷贝构造: Original ← copy1初始化 拷贝构造: Original ← test函数传参 析构: Original ← test形参析构 析构: Original ← copy1析构 析构: Original ← orig析构 ``` ### 关键总结 | 特性 | 说明 | |--------------------|----------------------------------------------------------------------| | **定义要求** | 参数为`const ClassName&`的构造函数 | | **默认行为** | 浅拷贝(可能引发资源冲突) | | **必须自定义场景** | 类管理动态内存、文件句柄等资源时 | | **禁用拷贝构造** | 可通过`= delete`禁止(如单例模式) | > **最佳实践**: > - 优先使用`const T&`传递对象避免拷贝 > - 资源管理类必须实现深拷贝或禁用拷贝(遵循Rule of Three/Five) §§相关问题§§ 1. 深拷贝和浅拷贝的具体区别是什么?[^4] 2. 如何通过`= delete`禁止拷贝构造函数? 3. 移动构造函数与拷贝构造函数有何性能差异? 4. 为什么拷贝构造函数参数必须是引用?[^2] 5. C++11后的Rule of Five包含哪些特殊成员函数? </think> ### C++拷贝构造函数:定义与使用场景详解 #### 一、拷贝构造函数定义 拷贝构造函数是特殊的构造函数,用于**使用已有对象初始化新对象**。其核心特征: ```cpp class ClassName { public: // 拷贝构造函数定义 ClassName(const ClassName& other); // ✅ 必须为同类型对象的常量引用 }; ``` **关键特性**: 1. **参数要求**:必须是同类对象的**常量引用**(`const ClassName&`) - 原因:避免无限递归(值传递会触发新的拷贝构造)[^2][^4] 2. **默认行为**:若未显式定义,编译器生成**浅拷贝**版本(按字节复制)[^2] 3. **调用时机**:对象初始化时**自动调用**(非赋值操作) #### 二、拷贝构造函数调用场景 1. **显式对象初始化** 使用现有对象直接创建新对象: ```cpp AA obj1; AA obj2(obj1); // ✅ 调用拷贝构造 [^3] AA obj3 = obj1; // ✅ 初始化而非赋值,调用拷贝构造 [^3] ``` 2. **函数参数传递(值传递)** 对象作为函数参数按值传递时: ```cpp void process(AA obj); // 值传递参数 int main() { AA original; process(original); // ✅ 调用拷贝构造创建形参obj [^2] } ``` 3. **函数返回对象(值返回)** 函数返回对象时创建临时副本: ```cpp AA createAA() { AA local; return local; // ✅ 可能调用拷贝构造(编译器可能优化)[^2] } ``` 4. **容器操作** 对象被放入标准容器时: ```cpp std::vector<AA> vec; AA item; vec.push_back(item); // ✅ 调用拷贝构造复制到容器 [^1] ``` #### 三、必须自定义拷贝构造的场景 当类涉及**资源管理**时,需自定义深拷贝: ```cpp class String { public: // 自定义深拷贝 String(const String& other) { size = other.size; data = new char[size + 1]; // 重新分配内存 strcpy(data, other.data); // 复制内容 } private: char* data; int size; }; ``` **必须自定义的情况**: - 类包含**动态内存**(如`new`分配的指针) - 持有**文件句柄**、**网络连接**等资源 - 需要**非浅拷贝行为**(如引用计数) #### 四、深浅拷贝对比 | 类型 | 行为 | 风险 | |--------|--------------------------|--------------------| | 浅拷贝 | 逐字节复制(默认行为) | 多个对象共享资源 → 双重释放 | | 深拷贝 | 创建资源的新副本(需自定义) | 安全独立 | #### 五、完整示例 ```cpp #include <iostream> #include <cstring> class DataBlock { public: DataBlock(const char* d) { size = strlen(d); data = new char[size + 1]; strcpy(data, d); } // 自定义深拷贝构造 DataBlock(const DataBlock& other) : size(other.size) { data = new char[size + 1]; strcpy(data, other.data); std::cout << "深拷贝构造调用\n"; } ~DataBlock() { delete[] data; } private: char* data; size_t size; }; int main() { DataBlock original("Hello"); DataBlock copy(original); // 触发深拷贝构造 return 0; } ``` **输出**: ``` 深拷贝构造调用 ``` > **关键原则**: > 1. 资源管理类必须自定义拷贝构造(遵循**Rule of Three**) > 2. 简单数据类可依赖编译器生成的浅拷贝 > 3. 禁止拷贝时使用`= delete`(如单例模式)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值