c++详细讲解构造函数的拷贝流程

#include <iostream>
#include <string>
using namespace std;
void func(string str){
    cout<<str<<endl;
}
int main(){
    string s1 = "http:www.biancheng.net";
    string s2(s1);
    string s3 = s1;
    string s4 = s1 + " " + s2;
    func(s1);
    cout<<s1<<endl<<s2<<endl<<s3<<endl<<s4<<endl;
    return 0;
}

运行结果:

http:www.biancheng.net

http:www.biancheng.net

http:www.biancheng.net

http:www.biancheng.net

http:www.biancheng.net http:www.biancheng.net

s1、s2、s3、s4 以及 func() 的形参 str,都是使用拷贝的方式来初始化的。

对于 s1、s2、s3、s4,都是将其它对象的数据拷贝给当前对象,以完成当前对象的初始化。

对于 func() 的形参 str,其实在定义时就为它分配了内存,但是此时并没有初始化,只有等到调用 func() 时,才会将其它对象的数据拷贝给 str 以完成初始化。

当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数(Copy Constructor)。

#include <iostream>
#include <string>
using namespace std;
class Student{
public:
    Student(string name = "", int age = 0, float score = 0.0f);  //普通构造函数
    Student(const Student &stu);  //拷贝构造函数(声明)
public:
    void display();
private:
    string m_name;
    int m_age;
    float m_score;
};
Student::Student(string name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//拷贝构造函数(定义)
Student::Student(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
    cout<<"Copy constructor was called."<<endl;
}
void Student::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
int main(){
   const Student stu1("小明", 16, 90.5);
    Student stu2 = stu1;  //调用拷贝构造函数
    Student stu3(stu1);  //调用拷贝构造函数
    stu1.display();
    stu2.display();
    stu3.display();
    return 0;
}

运行结果:

Copy constructor was called.

Copy constructor was called.

小明的年龄是16,成绩是90.5

小明的年龄是16,成绩是90.5

小明的年龄是16,成绩是90.5

第 8 行是拷贝构造函数的声明,第 20 行是拷贝构造函数的定义。拷贝构造函数只有一个参数,它的类型是当前类的引用,而且一般都是 const 引用。

1) 为什么必须是当前类的引用呢?

如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

2) 为什么是 const 引用呢?

拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

当然,你也可以再添加一个参数为非const 引用的拷贝构造函数,这样就不会出错了。换句话说,一个类可以同时存在两个拷贝构造函数,一个函数的参数为 const 引用,另一个函数的参数为非 const 引用。

class Base{
public:
    Base(): m_a(0), m_b(0){ }
    Base(int a, int b): m_a(a), m_b(b){ }
private:
    int m_a;
    int m_b;
};
int main(){
    int a = 10;
    int b = a;  //拷贝
    Base obj1(10, 20);
    Base obj2 = obj1;  //拷贝
    return 0;
}

b 和 obj2 都是以拷贝的方式初始化的,具体来说,就是将 a 和 obj1 所在内存中的数据按照二进制位(Bit)复制到 b 和 obj2 所在的内存,这种默认的拷贝行为就是浅拷贝,这和调用 memcpy() 函数的效果非常类似。

对于简单的类,默认的拷贝构造函数一般就够用了,我们也没有必要再显式地定义一个功能类似的拷贝构造函数。但是当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,默认的拷贝构造函数就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。

下面我们通过一个具体的例子来说明显式定义拷贝构造函数的必要性。

#include <iostream>
#include <cstdlib>
using namespace std;
//变长数组类
class Array{
public:
    Array(int len);
    Array(const Array &arr);  //拷贝构造函数
    ~Array();
public:
    int operator[](int i) const { return m_p[i]; }  //获取元素(读取)
    int &operator[](int i){ return m_p[i]; }  //获取元素(写入)
    int length() const { return m_len; }
private:
    int m_len;
    int *m_p;
};
Array::Array(int len): m_len(len){
    m_p = (int*)calloc( len, sizeof(int) );
}
Array::Array(const Array &arr){  //拷贝构造函数
    this->m_len = arr.m_len;
    this->m_p = (int*)calloc( this->m_len, sizeof(int) );
    memcpy( this->m_p, arr.m_p, m_len * sizeof(int) );
}
Array::~Array(){ free(m_p); }
//打印数组元素
void printArray(const Array &arr){
    int len = arr.length();
    for(int i=0; i<len; i++){
        if(i == len-1){
            cout<<arr[i]<<endl;
        }else{
            cout<<arr[i]<<", ";
        }
    }
}
int main(){
    Array arr1(10);
    for(int i=0; i<10; i++){
        arr1[i] = i;
    }
    Array arr2 = arr1;
    arr2[5] = 100;
    arr2[3] = 29;
    printArray(arr1);
    printArray(arr2);
    return 0;
}

运行结果:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9

0, 1, 2, 29, 4, 100, 6, 7, 8, 9

本例中我们显式地定义了拷贝构造函数,它除了会将原有对象的所有成员变量拷贝给新对象,还会为新对象再分配一块内存,并将原有对象所持有的内存也拷贝过来。这样做的结果是,原有对象和新对象所持有的动态内存是相互独立的,更改一个对象的数据不会影响另外一个对象,本例中我们更改了 arr2 的数据,就没有影响 arr1。

这种将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。深拷贝的例子比比皆是,除了上面的变长数组类,使用的动态数组类也需要深拷贝;此外,标准模板库(STL)中的 string、vector、stack、set、map 等也都必须使用深拷贝。

读者如果希望亲眼目睹不使用深拷贝的后果,可以将上例中的拷贝构造函数删除,那么运行结果将变为:0, 1, 2, 29, 4, 100, 6, 7, 8, 9

0, 1, 2, 29, 4, 100, 6, 7, 8, 9

可以发现,更改 arr2 的数据也影响到了 arr1。这是因为,在创建 arr2 对象时,默认拷贝构造函数将 arr1.m_p 直接赋值给了 arr2.m_p,导致 arr2.m_p 和 arr1.m_p 指向了同一块内存,所以会相互影响。

另外需要注意的是,printArray() 函数的形参为引用类型,这样做能够避免在传参时调用拷贝构造函数;又因为 printArray() 函数不会修改任何数组元素,所以我们添加了 const 限制,以使得语义更加明确。

  • 到底是浅拷贝还是深拷贝

如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。如果类的成员变量没有指针,一般浅拷贝足以。

另外一种需要深拷贝的情况就是在创建对象时进行一些预处理工作,比如统计创建过的对象的数目、记录对象创建的时间等,请看下面的例子:

#include <iostream>
#include <ctime>
#include <windows.h>  //在Linux和Mac下要换成 unistd.h 头文件
using namespace std;
class Base{
public:
    Base(int a = 0, int b = 0);
    Base(const Base &obj);  //拷贝构造函数
public:
    int getCount() const { return m_count; }
    time_t getTime() const { return m_time; }
private:
    int m_a;
    int m_b;
    time_t m_time;  //对象创建时间
    static int m_count;  //创建过的对象的数目
};
int Base::m_count = 0;
Base::Base(int a, int b): m_a(a), m_b(b){
    m_count++;
    m_time = time((time_t*)NULL);
}
Base::Base(const Base &obj){  //拷贝构造函数
    this->m_a = obj.m_a;
    this->m_b = obj.m_b;
    this->m_count++;
    this->m_time = time((time_t*)NULL);
}
int main(){
    Base obj1(10, 20);
    cout<<"obj1: count = "<<obj1.getCount()<<", time = "<<obj1.getTime()<<endl;
    Sleep(3000);  //在Linux和Mac下要写作 sleep(3);
    Base obj2 = obj1;
    cout<<"obj2: count = "<<obj2.getCount()<<", time = "<<obj2.getTime()<<endl;
    return 0;
}

运行结果:

obj1: count = 1, time = 1488344372

obj2: count = 2, time = 1488344375

运行程序,先输出第一行结果,等待 3 秒后再输出第二行结果。Base 类中的 m_time 和 m_count 分别记录了对象的创建时间和创建数目,它们在不同的对象中有不同的值,所以需要在初始化对象的时候提前处理一下,这样浅拷贝就不能胜任了,就必须使用深拷贝了。

到此这篇关于c++详细讲解构造函数的拷贝流程的文章就介绍到这了,更多相关c++构造函数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++类继承时的构造函数

    前言: 子类需要编写自己的构造函数和析构函数,需要注意的是,子类只负责对新增的成员进行初始化和扫尾编写构造和析构函数,父类成员的初始化和扫尾工作由父类的构造函数和析构函数完成. 无论何种类型的继承方式,子类都无权访问父类的所有成员,所以子类对父类的初始化需要父类的构造函数完成.此时,子类的构造函数必须提供父类构造函数所需的参数. 子类构造函数的语法如下: 子类::子类(全部参数表):父类1(父类1参数表),父类2(父类2参数表)      ...对象成员1(对象成员1参数表),对象成员2(对象成

  • C++中的拷贝构造函数详解

    目录 C++拷贝构造函数(复制构造函数)详解 1) 为什么必须是当前类的引用呢? 2) 为什么是 const 引用呢? 默认拷贝构造函数 总结 C++拷贝构造函数(复制构造函数)详解 拷贝和复制是一个意思,对应的英文单词都是copy.对于计算机来说,拷贝是指用一份原有的.已经存在的数据创建出一份新的数据,最终的结果是多了一份相同的数据.例如,将 Word 文档拷贝到U盘去复印店打印,将 D 盘的图片拷贝到桌面以方便浏览,将重要的文件上传到百度网盘以防止丢失等,都是「创建一份新数据」的意思. 在

  • C++中构造函数详解

    构造函数按参数为为:有参构造函数和无参构造函数 按类型分为:普通构造函数和拷贝构造函数 构造函数的三种调用方法:括号法,显示法,隐式转换法: //括号法 Person p1; //默认构造 无参构造 Person p2(13); //有参构造 Person p3(p2); //拷贝构造 //注意:使用无参构造时不要写括号.不然系统会认为该语句是函数声明. 例:Person p1(); //显示法 Person p1; Person p2 = Person(13);//有参构造 Person p3

  • C++探索构造函数私有化会产生什么结果

    目录 对于单个类 私有化与继承 成员变量与私有化 提问:假设只有一个构造方法,如果将之私有化会有什么后果 对于当前类,它是无法实例化的 对于它的子类,子类也是无法实例化的 构造函数与是否能够实例化有关 对于单个类 正常情况下 #include <iostream> using namespace std; class EventDispatcher { public: void test_printf(){ std::cout << "test_printf --\r\n

  • C++分析构造函数与析造函数的特点梳理

    目录 构造函数的调用 构造函数的分类及调用 拷贝构造的调用时机 深拷贝与浅拷贝 构造函数的调用 默认情况下编译器至少给一个类添加3个函数 1.默认构造函数(无参,函数体实现)--完成对象的初始化 2.默认析构函数(无参,函数体为空)--完成对象的清理 3.默认拷贝构造函数,属性进行值拷贝 规则: 如果用户定义了有参构造,c++不会提供无参构造,但是提供默认拷贝构造 如果用户定义了拷贝构造函数,c++不会在提供其他函数 类名(){} 构造函数的语法 1,没有返回值,也不写void: 2,函数名称与

  • C++构造函数的类型,浅拷贝与深拷贝详解

    目录 一.无参构造函数 二.含参构造函数 三.拷贝构造函数 四.深拷贝和浅拷贝 总结 一.无参构造函数 1.如果没有定义构造函数,则系统自动调用此默认构造函数,且什么都不做. 2.如果用户自定义了带参数的构造函数,若还想调用无参的构造函数,必须显示定义 person() { cout << "this object is being created." << endl; } 二.含参构造函数 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参

  • C++的拷贝构造函数你了解吗

    目录 一般情况下的拷贝构造函数: 默认拷贝构造函数: 浅拷贝和深拷贝: 总结 拷贝构造函数用以将一个类的对象拷贝给同一个类的另一个对象,比如之前学习过的string类: string s1; string s2 = s1; 一般情况下的拷贝构造函数: class A { private: int n; double d; char s; public: A(const A& a); }; A::A(const A& a) { this->n = a.n; this->d = a

  • C++中拷贝构造函数的使用

    目录 拷贝构造函数 1. 手动定义的拷贝构造函数 2. 合成的拷贝构造函数 总结 拷贝构造函数 拷贝构造函数,它只有一个参数,参数类型是本类的引用.复制构造函数的参数可以是 const 引用,也可以是非 const 引用. 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象.一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的. 1. 手动定义的拷贝构造函数 Human.h #pra

  • C++构造函数+复制构造函数+重载等号运算符调用

    目录 前言: 1.赋值和初始化的区别 2.初始化和赋值分别调用哪个函数? 3.编写测试类 前言: 初学C++发现了下面这个问题,其中Duck是一个已知的类,并以多种方式指定对象的值: Duck d1(); Duck d2(d1); Duck d3 = d1; Duck d4; d4 = d1; 问题在于,上述d1.d2.d3.d4是如何创建的呢?分别调用的哪个函数呢? 1.赋值和初始化的区别 C++中,赋值和初始化是两个不同的概念: 初始化是指对象创建之时指定其初值,分为直接初始化和复制初始化两

  • c++详细讲解构造函数的拷贝流程

    #include <iostream> #include <string> using namespace std; void func(string str){ cout<<str<<endl; } int main(){ string s1 = "http:www.biancheng.net"; string s2(s1); string s3 = s1; string s4 = s1 + " " + s2; fu

  • C++超详细讲解构造函数与析构函数的用法及实现

    目录 写在前面 构造函数和析构函数 语法 作用 代码实现 两大分类方式 三种调用方式 括号法 显示法 隐式转换法 正确调用拷贝构造函数 正常调用 值传递的方式给函数参数传值 值传递方式返回局部对象 构造函数的调用规则 总结 写在前面 上一节解决了类与对象封装的问题,这一节就是对象的初始化和清理的构造函数与析构函数的内容了:对象的初始化和清理也是两个非常重要的安全问题:一个对象或者变量没有初始状态,对其使用后果是未知,同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题:c++利用了构

  • C++超详细讲解构造函数

    目录 类的6个默认成员函数 构造函数 特性 编译器生成的默认构造函数 成员变量的命名风格 类的6个默认成员函数 如果我们写了一个类,这个类我们只写了成员变量没有定义成员函数,那么这个类中就没有函数了吗?并不是的,在我们定义类时即使我们没有写任何成员函数,编译器会自动生成下面6个默认成员函数. class S { public: int _a; }; 这里就来详细介绍一下构造函数. 构造函数 使用C语言,我们用结构体创建一个变量时,变量的内容都是随机值,要想要能正确的操作变量中存储的数据,我们还需

  • Spring Cloud详细讲解zuul集成Eureka流程

    目录 zuul集成Eureka Zuul路由配置 1. 指定具体服务路由 2. 路由前缀 Zuul过滤器 过滤器类型 使用过滤器 zuul集成Eureka 通过刚才的示例,我们已经可以简单地使用 Zuul 进行路由的转发了,在实际使用中我们通常是用 Zuul 来代理请求转发到内部的服务上去,统一为外部提供服务.内部服务的数量会很多,而且可以随时扩展,我们不可能每增加一个服务就改一次路由的配置,所以也得通过结合 Eureka 来实现动态的路由转发功能.首先需要添加 Eureka 的依赖,代码如下所

  • C++超详细讲解拷贝构造函数

    目录 构造函数 特征 编译器生成的拷贝构造 拷贝构造的初始化列表 显式定义拷贝构造的误区 结论 构造函数 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用 拷贝构造函数是构造函数的一个重载,因此显式的定义了拷贝构造,那么编译器也不再默认生成构造函数. 特征 拷贝构造也是一个特殊的成员函数 特征如下: 拷贝构造是构造函数的一个重载: 拷贝构造的参数只有一个并且类型必须是该类的引用,而不是使用传值调用,否则会无限递归: 若没有显

  • Kotlin构造函数与成员变量和init代码块执行顺序详细讲解

    目录 在Kotlin中经常看到主构造函数.成员变量.init代码块(也叫初始化器),它们的执行时机和顺序是什么样的呢?看一下官方的示例: class InitOrderDemo(name: String) { val firstProperty = "First property: $name".also(::println) init { println("First initializer block that prints ${name}") } val se

  • SpringBoot详细讲解多个配置文件的配置流程

    目录 配置文件加载顺序 验证 前期准备 验证配置文件加载顺序 验证属性互补 总结 一般情况下,springboot默认会在resource目录下生成一个配置文件(application.properties或application.yaml),但其实springboot允许配置多个配置文件(application.properties或application.yaml),但是这并不意味着这些配置文件一定会替换默认生成的配置文件,它们是互补的存在.如果在某些场景下需要把配置文件单独拿出来并且启动的

  • Spring超详细讲解创建BeanDefinition流程

    目录 一.前期准备 1.1 环境依赖 1.2 实体类 1.3 applicationContext.xml 1.4 测试代码 二.探究过程 2.1 目标 2.2 BeanDefinition的创建过程 2.2.1 回顾bean对象的创建 2.2.2 AbstractApplicationContext 2.2.3 AbstractXmlApplicationContext 2.2.4 AbstractBeanDefinitionReader 2.2.5 XmlBeanDefinitionRead

  • Springboot详细讲解RocketMQ实现顺序消息的发送与消费流程

    目录 一.创建Springboot项目添加rockermq依赖 二.配置rocketmq 三.新建一个controller来做消息发送 四.创建消费端监听消息消费消息 五.启动服务测试顺序消息发送与消费 如何实现顺序消息? 需要程序保证发送和消费的是同一个 Queue rocketmq默认发送的消息是进入多个消息队列,然后消费端多线程并发消费,所以默认情况,不是順序消费消息的:有時候,我们需要顺序消费一批消息,比如电商系统 订单创建.支付.完成操作,需要順序执行: RocketMQTemplat

  • Spring Boot超详细讲解请求处理流程机制

    目录 1. 背景 2. Spring Boot 的请求处理流程设计 3. Servlet服务模式请求流程分析 3.1 ServletWebServerApplicationContext分析 3.2 Servlet服务模式之请求流程具体分析 4. Reactive服务模式请求流程分析 4.1 ReactiveWebServerApplicationContext分析 4.2 webflux服务模式之请求流程具体分析 5. 总结 1. 背景 之前我们对Spring Boot做了研究讲解,我们知道怎

随机推荐