Bootstrap

C++之《剑指offer》学习记录(3):拷贝构造函数

笔者最近在找工作时,无意间读到了一本名为《剑指offer》的书,粗略翻阅了一下,感觉这将会是一本能让我不再苦恼于笔试和面试“手搓代码”的书。故笔者写下该系列博客记录自己的学习历程,希望能和这本书的读者朋友们一起交流学习心得。
介绍:《剑指Offer:名企面试官精讲典型编程题(第2版)》剖析了80个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这5个面试要点。
编程题链接:牛客网在线编程_算法面试_面试必刷TOP101 (nowcoder.com)
本博客关键词:拷贝构造函数深拷贝和浅拷贝

拷贝构造函数

构造函数的概念大家应该都不陌生,但是拷贝构造函数是什么呢?这里直接放一段代码:

class MyClass
{
private:
    int age;
    char *name;

public:
    // 构造函数
    MyClass(const char *n, int s_age)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
        age = s_age;
        cout << "调用构造函数" << endl;
    }

    // 拷贝构造函数,也叫复制构造函数
    MyClass(const MyClass &other)
    {
        // 深拷贝
        // name = new char[strlen(other.name) + 1];
        // strcpy(name, other.name);
        age = other.age;
        // 浅拷贝
        name = other.name;
        cout << "调用复制构造函数" << endl;
    }
    ~MyClass()
    {
        cout << "析构函数被调用" << endl;
    }

    void getValue()
    {
        cout << "age: " << age << endl;
    }

    void setValue(int s_age)
    {
        age = s_age;
    }

    void getName()
    {
        cout << "name: " << name << endl;
    }

    void setName(const char *new_name)
    {
        strcpy(name, new_name);
    }
};

其中MyClass(const MyClass &other)就是拷贝构造函数,大家之后写拷贝构造函数的输入可以默认用const 类名 &other来写

  • const的使用,保证了拷贝构造函数不会修改传入的对象。这是符合逻辑的,因为拷贝构造的目的是创建一个新对象,而不是修改现有对象。
  • &,通过引用传参,这一步是必须的,如果使用按值传参,会导致永无休止的递归调用。如果不使用引用传参,代码在编译阶段就会出错的。
    输入参数使用const修饰应该比较容易理解,可是为什么一定要通过引用传参?为什么按值传参会有无限的递归调用呢?下面简单做个解释:
  1. 拷贝构造函数一般会在以下几个场景被调用:
    • 用一个已存在的对象初始化另一个对象
    • 对象作为值传递给函数时
    • 在容器中存储对象时
    • 函数按值返回对象时(可能不会调用)
  2. 由以上可知,在对象作为值传递给函数时,也会调用拷贝构造函数,如果拷贝构造函数本身的输入参数也是按值传递的(输入参数为按值传递会进行值拷贝),则在形参拷贝实参的过程中又会产生拷贝构造函数,如此就会一直递归调用拷贝构造函数,最终导致栈溢出。
  3. 而通过引用传参,就避免了值拷贝的过程,传入拷贝构造函数的是另一个实例的引用。通过引用传递,可以减少类中构造函数和析构函数调用的次数,提高程序的执行效率

深拷贝和浅拷贝

  1. 深拷贝浅拷贝是两种对象复制的方式,它们在复制对象时处理内存和资源的方式不同,尤其是在涉及动态分配内存或指针的情况下。
  2. 深拷贝和浅拷贝在基本类型变量(如int,double等)上的拷贝没有太多的差别,都是按值拷贝,会开辟新的空间。比如对象A的val值拷贝给对象B之后,B的val值会占用新的空间,修改B的val值不会影响A的val值。
  3. 对于指针类型变量,深浅拷贝区别就很大
    • 浅拷贝:在复制对象时,只复制对象的内存地址和基本属性(指针类型),而不复制它所指向的数据,意味着原对象和新对象将共享相同的内存区域或资源。
    • 深拷贝:在复制对象时,不仅复制对象的内存地址和基本属性(指针类型),还复制所有动态分配的内存或资源。也就是说,它会为新对象分配一个新的内存区域,并将原对象的数据复制到这个新区域中。
  4. 以拷贝构造函数为例:
// 拷贝构造函数,也叫复制构造函数
    MyClass(const MyClass &other)
    {
        // 深拷贝
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
        // 浅拷贝
        // name = other.name;
        
        age = other.age;
        cout << "调用复制构造函数" << endl;
    }

测试用例

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyClass
{
private:
    int age;
    char *name;

public:
    // 构造函数
    MyClass(const char *n, int s_age)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
        age = s_age;
        cout << "调用构造函数" << endl;
    }

    // 拷贝构造函数,也叫复制构造函数
    MyClass(const MyClass &other)
    {
        // 深拷贝
        // name = new char[strlen(other.name) + 1];
        // strcpy(name, other.name);
        age = other.age;
        // 浅拷贝
        name = other.name;
        cout << "调用复制构造函数" << endl;
    }
    ~MyClass()
    {
        cout << "析构函数被调用" << endl;
    }

    void getValue()
    {
        cout << "age: " << age << endl;
    }

    void setValue(int s_age)
    {
        age = s_age;
    }

    void getName()
    {
        cout << "name: " << name << endl;
    }

    void setName(const char *new_name)
    {
        strcpy(name, new_name);
    }
};

void show(MyClass &obj1, MyClass &obj2)
{
    obj1.getName();
    obj1.getValue();
    obj2.getName();
    obj2.getValue();
}

MyClass createClass()
{
    cout << "createClass:" << endl;
    MyClass obj("王五", 22);
    return obj; // 这里在GCC中并不会调用拷贝构造函数
}

int main()
{
    vector<MyClass> vec;

    MyClass obj1("张三", 23);
    MyClass obj2 = obj1; // 这里会调用拷贝构造函数——用一个已存在的对象初始化另一个对象

    cout << "vector:" << endl;
    vec.push_back(obj1); // 这里会调用拷贝构造函数——在容器中存储对象时

    MyClass obj = createClass(); 
    obj.getName();

    show(obj1, obj2);

    obj1.setName("李四");
    obj1.setValue(18);

    show(obj1, obj2);

    return 0;
}
;