Skip to content

1. 引用

1.1 引用

引用是已存在变量的一个别名,对引用的操作就是对原变量的操作。
引用在定义时必须要初始化,初始化以后绑定的变量不能再次修改。
引用的类型与初始化的绑定的变量类型保持一致。

语法:

cpp
类型 &引用名 = 变量名;

示例:引用与基本类型

cpp
#include<iostream>  
using namespace std;  

int main()  
{  
    int a = 1;  
    int& r_a = a; // r_a引用a,r_a就是a的别名  
    cout << "&a = " << &a << ", a = " << a << endl;  
    cout << "&r_a = " << &r_a << ", r_a = " << r_a << endl;  

    r_a = 2;  
    cout << "a = " << a << endl; // 2  
    cout << "r_a = " << r_a << endl;  
    a = 3;  
    cout << "a = " << a << endl; // 3  
    cout << "r_a = " << r_a << endl;  

    // 引用定义时必须初始化  
    // int &r; // Error  

    int c = 4;  
    r_a = c; // 将c的值赋值给r_a,而不是修改引用目标  
    cout << "a = " << a << endl; // 4  
    cout << "r_a = " << r_a << endl;  

    cout << "&a = " << &a << ", a = " << a << endl;  
    cout << "&r_a = " << &r_a << ", r_a = " << r_a << endl;  
    cout << "&c = " << &c << ", c = " << c << endl;  

    // 引用类型和绑定的目标类型要一致  
    // double &d = c; // Error  
    return 0;  
}

示例:引用与类类型

cpp
#include<iostream>  
using namespace std;  

class A
{
public:
    A(int i) : m_i(i) {}
    void print()
    {
        cout << "m_i = " << m_i << endl;
    }
private:
    int m_i;
};

int main()
{
    A a(10);
    A& r_a = a;
    a.print();
    r_a.print();
    return 0;
}

1.2 常引用

使用引用时,由于可以通过引用去修改变量的值,所以为了防止出现这种情况,可以将引用定义为常引用。

  • 定义引用时加 const 修饰,即为常引用,不能通过常引用修改引用的目标:
cpp
const 类型 &引用名 = 变量名;
类型 const &引用名 = 变量名;
// const在&前即可

示例:对象的常引用和常对象一样,只能调用常函数

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int i) : m_i(i) {}
    void print() const
    {
        cout << "m_i = " << m_i << endl;
    }
    void setValue(int i)
    {
        m_i = i;
    }
private:
    int m_i;
};

int main()
{
    A a(10);
    const A& r_a = a;
    a.setValue(20);
    a.print();
    // r_a.setValue(20); // 常引用和常对象一样,只能调用常函数
    r_a.print();

    int al = 1;
    const int& r_al = al;
    cout << r_al << endl;

    al = 2;
    cout << r_al << endl;

    // r_al = 3; // error 不能通过常引用修改引用的目标
    return 0;
}

普通引用只能引用左值,常引用也叫万能引用,既能引用左值,也能引用右值。

左值和右值:

  • 左值:可以放在赋值运算符(=)左侧,一般普通的变量都是左值,表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。
    • 普通的变量
    • 赋值表达式结果
    • ++--表达式结果
  • 右值:只能放在赋值运算符(=)右侧,一般常量都是右值,在内存中不占据位置
    • 常量
    • 大多数表达式的结果
    • 函数返回非引用变量(将亡右值,即函数返回值)
cpp
#include<iostream>
using namespace std;

int func(void)
{
    int num = 30;
    cout << "&num = " << &num << endl;
    return num; // 临时变量保存num
}

int main()
{
    // res = 临时变量
    const int& res = func();
    cout << "&res = " << &res << endl;
    cout << res << endl; // 30
    int a = 3, b = 5;
    // a + b = 10; // Error
    (a += b) = 10;
    cout << a << endl; // 10
    ++a = 20;
    cout << a << endl; // 20
    // a++ = 30; // Error
    return 0;
}

普通引用只能引用左值,常引用也叫万能引用,既能引用左值,也能引用右值:

cpp
#include<iostream>
using namespace std;

int main()
{
    // 普通引用不能引用常量(右值)
    // int &r = 100; // Error

    // 常引用既可以引用左值也可以引用右值
    const int& r = 100; // ok
    cout << r << endl;

    int a = 1;
    const int& r_a = a; // ok
    cout << r_a << endl;
    return 0;
}

1.3 引用型函数参数

  • 将引用用于函数的参数,这时形参就是实参的别名,可以通过形参直接修改实参的值,同时避免参数值传递过程,减小函数调用开销。
cpp
#include<iostream>
using namespace std;

void swap1(int* a, int* b)
{
    int tmp = 1;
    int* p_tmp = &tmp;
    *p_tmp = *a;
    *a = *b;
    *b = *p_tmp;
}

void swap2(int& a, int& b)
{
    int tmp = 1;
    tmp = a;
    a = b;
    b = tmp;
}

void swap3(int a, int b)
{
    int tmp = 1;
    tmp = a;
    a = b;
    b = tmp;
}

int main()
{
    int a = 3, b = 5;
    cout << "a = " << a << ", b = " << b << endl;
    // swap1(&a,&b);
    // swap2(a, b);
    swap3(a, b);
    cout << "a = " << a << ", b = " << b << endl;
    return 0;
}
  • 引用型参数有可能意外修改实参的值,如果不希望修改实参本身,可以将形参定义为常引用,提高传参效率的同时还可以接收常量型的实参:
cpp
#include<iostream>
using namespace std;

class Student
{
public:
    Student(const string& name, int age) : m_name(name), m_age(age)
    {
    }
public:
    string m_name;
    int m_age;
};

void print(const Student& s) // 常引用
{
    cout << s.m_name << ',' << s.m_age << endl;
    // s.age++; // 不允许通过常引用修改变量的值
}

int main()
{
    /*const*/ Student student("蔡徐坤", 18);
    print(student);
    return 0;
}

1.4 引用型函数返回值

  • 可以将函数的返回值声明为引用,避免返回值所带来的内存开销。
  • 不要从函数中返回局部变量的引用,因为所引用的变量内存在函数返回以后被释放,但是可以返回成员变量、静态变量以及全局变量的引用。
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0) : m_data(data)
    {
    }
    int& getValue(void)
    {
        return m_data;
    }
    // 不能返回局部变量的引用
    int& fun(void)
    {
        int a = 123;
        return a;
    }
private:
    int m_data;
};

int main()
{
    A a(100);
    // cout << a.data << endl; // 100
    int b = a.getValue();
    cout << b << endl; // 100

    int& c = a.fun();
    cout << c << endl; // VS不会报错 g++报错

    return 0;
}
  • 如果一个函数返回值类型被声明为普通引用,那么该函数返回值是一个左值(函数返回值本来是右值):
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0) : m_data(data)
    {
    }
    int& getValue(void)
    {
        return m_data;
    }
private:
    int m_data;
};

int main()
{
    A a(100);
    // cout << a.data << endl; // 100
    cout << (a.getValue())++ << endl; // 100
    // cout << (a.getValue1())++ << endl; // 100
    return 0;
}
  • 如果不希望函数直接返回左值,可以返回常引用:
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0) : m_data(data)
    {
    }
    int& getValue(void)
    {
        return m_data;
    }
    const int getValue1(void)
    {
        return m_data;
    }
private:
    int m_data;
};

int main()
{
    A a(100);
    // cout << a.data << endl; // 100
    cout << (a.getValue())++ << endl; // 100
    // cout << (a.getValue1())++ << endl; // error 右值不能++
    cout << a.getValue1() << endl; // 101
    return 0;
}
  • 使用引用的时机(函数的参数和返回值):
    • 如果是基本类型,使用值传递或者指针
    • 如果是数组,只能以指针方式传递
    • 如果是结构体类型,指针、引用都可
    • 如果是类/对象,引用

1.5 指针和引用的区别

  • 相同点

    • 引用和指针都允许对其他变量进行间接访问,通过引用或指针可以修改或获取其他变量的值。
    • 两者都可以用于函数参数传递,允许在函数内修改调用者传递的变量。
    • 在C++中建议使用引用,而不是指针:
      cpp
      int a = 100;
      int &ra = a;
      int * const pa = &a;
      *pa <=等价=> ra
  • 区别

    • 指针可以不做初始化,其目标可以随便改变(指针常量除外),而引用必须初始化,而且其引用目标不能改变:
      cpp
      #include<iostream>
      using namespace std;
      
      int main()
      {
          // int& r; // Error
          int a = 1;
          int& r = a; // 引用定义必须初始化
          int b = 2;
          r = b; // ok,但这是赋值操作,不是改变引用的目标
          cout << &a << endl;
          cout << &b << endl;
          cout << &r << endl;
          return 0;
      }

1.6 使用引用注意事项

  • 避免返回局部变量的引用:不要返回函数内部局部变量的引用,因为局部变量的生命周期在函数结束时结束,返回对应的引用会导致悬空引用。
  • 确保引用指向有效的内存:确保引用在其生命周期内始终指向有效的内存,避免使用悬空引用。
  • 引用作为函数返回值的生命周期:当函数返回引用时,确保返回的引用指向的对象在函数调用后仍然有效。
  • 避免引用和指针混淆:引用和指针是不同的概念,虽然它们都可以用于访问内存,但它们有着不同的语法和语义。避免混淆引用和指针的用法。
  • 避免滥用引用:引用是一种强大的工具,但滥用它可能导致代码的可读性和维护性下降。在确实需要引用的情况下使用它,而不是为了避免传值而过度使用引用。

2. 拷贝构造和拷贝赋值

2.1 拷贝构造

拷贝构造函数(copy-constructor)依然是构造函数(是一个特殊的构造函数),名字依然和类名相同,用于利用一个已有对象初始化一个和已有对象“一模一样”的新对象。

用一个已存在的对象构造同类型的副本对象,会调用该类的拷贝构造函数。拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。

语法:

cpp
class 类名
{
    类名(const 类名& that) {...}
};

示例:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0)
    {
        cout << "A::A(int)" << endl;
        m_data = data;
    }

    A(const A& that)
    {
        cout << "A::A(const A&)" << endl;
        m_data = that.m_data;
    }
    int m_data;
};

int main()
{
    A a1(123); // 调用A类的构造函数
    A a2(a1);  // 调用A类的拷贝构造函数
    A a3 = a1; // 调用A类的拷贝构造函数
    cout << a1.m_data << endl; // 123
    cout << a2.m_data << endl; // 123
    cout << a3.m_data << endl; // 123
    return 0;
}
  • 如果一个类没有定义拷贝构造函数,那么编译器会为该类提供一个缺省的拷贝构造函数:
    • 对于基本类型的成员变量,直接复制
    • 对于类类型成员变量(成员子对象),会调用相应的拷贝构造函数来初始化

一般情况下需自己定义拷贝构造函数,因为编译器所提供的缺省拷贝构造函数已经很好用了:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0)
    {
        cout << "A::A(int)" << endl;
        m_data = data;
    }

    A(const A& that)
    {
        cout << "A::A(const A&)" << endl;
        m_data = that.m_data;
    }
    int m_data;
};

class B
{
public:
    int m_i; // 基本类型的成员变量,对于基本类型的成员变量,直接复制
    A m_a;   // 类类型的成员变量(成员子对象),对于类类型成员变量,会调用相应的拷贝构造函数来初始化
};

int main()
{
    B b1;
    b1.m_i = 123;
    b1.m_a.m_data = 1234;
    B b2(b1); // 调用B的拷贝构造
    cout << b1.m_i << "," << b1.m_a.m_data << endl; // 123,1234
    cout << b2.m_i << "," << b2.m_a.m_data << endl; // 123,1234
    return 0;
}
  • 拷贝构造函数调用时机
    • 用已存在的对象作为同类型对象的构造参数(如上述案例)
    • 以对象形式向函数传递参数
    • 从函数中返回对象(有可能被编译器优化掉)
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(void)
    {
        cout << "A::A(void)" << endl;
    }
    A(const A& that)
    {
        cout << "A::A(const A&)" << endl;
    }
};

void func1(A a) // 此处不加&会多调用一次拷贝构造
{
    cout << "func1" << endl;
}

A func2(void) // 此处不加&会调用2次拷贝构造,加了会调用1次 vs始终是1次,因为做了优化
{
    cout << "func2" << endl;
    A a; // 无参
    // cout << "&a = " << &a << endl;
    return a; // 拷贝
}

int main()
{
    A a1; // 无参
    A a2 = a1; // 拷贝/用已存在的对象作为同类型对象的构造实参
    cout << "1111" << endl;

    func1(a1); // 拷贝 /以对象形式向函数传递参数
    cout << "2222" << endl;

    A a3 = func2(); // 拷贝 //从函数中返回对象
    // cout << "&a3 = " << &a3 << endl;
    return 0;
}

2.2 拷贝赋值

用于将一个对象的状态复制到另一个同类型的对象。

当编译器看到两个对象的赋值操作时,比如 "obj1 = obj2",会将其翻译成函数调用的形式 "obj1.operator=(obj2)",该函数称为拷贝赋值操作符函数,由该函数完成的两个对象的赋值过程。

语法:

cpp
class 类名
{
    类名& operator=(const 类名& that) {...}
};

如果一个类没有定义拷贝赋值函数,那么编译器会为该类提供一个缺省的拷贝赋值函数:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0)
    {
        cout << "构造" << endl;
        m_data = data;
    }

    A(const A& that)
    {
        cout << "拷贝构造" << endl;
        m_data = that.m_data;
    }

    A& operator=(const A& that)
    {
        cout << "拷贝赋值" << endl;
        this->m_data = that.m_data;
        return *this;
    }

    int m_data;
};

int main()
{
    A a1(1); // 构造
    A a2 = A(2); // 构造

    // a1 = a2;
    a1.operator=(a2); // 拷贝赋值
    cout << a1.m_data << endl;
    cout << a2.m_data << endl;

    A a3 = a2; // 拷贝构造
    cout << a3.m_data << endl;

    return 0;
}

2.3 拷贝构造和拷贝赋值的联系和区别

  • 联系:都是用一个已存在的对象去为另一个对象赋值。
  • 区别
    • 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值构造函数对于一个已经被初始化的对象来进行赋值操作。
      cpp
      A a1(1); // 构造
      A a2 = A(2); // 构造
      A a3 = a2; // 拷贝构造 在执行该句代码之前,a3尚未初始化,在这句代码中初始化
      a1 = a2;   // 拷贝赋值 在执行该句代码之前,a1已经被初始化
    • 实现不一样,拷贝构造函数首先是一个构造函数,它调用时便是通过参数的对象初始化产生一个新对象。赋值构造函数是把一个新的对象赋值给一个原有的对象。
      cpp
      A a1(1); // 构造
      A a2 = A(2); // 构造
      A a3 = a2; // 拷贝构造 在执行该句代码之前,a3对象尚未构建出来
      a1 = a2;   // 拷贝赋值 在执行该句代码之前,a1对象已经存在了

3. 浅拷贝和深拷贝

3.1 浅拷贝

拷贝构造函数是一种特殊的构造函数,它在创建对象的时候,是利用同类中,之前实例化的对象来初始化新的对象的。拷贝赋值是一个赋值函数,是使用一个已存在的对象对另一个已存在的对象赋值。

不管是拷贝构造实例化出来的新对象还是拷贝赋值用一个已存在的对象为另一个已存在的对象赋值,参与该过程的两个对象都是一模一样。

如果一个类中没有显性的声明拷贝构造函数,编译器会自动生成一个拷贝构造函数;如果一个类中没有显性的声明拷贝赋值函数,编译器会自动生成一个拷贝赋值函数。

编译器会自动为类提供的函数:

  • 构造函数(无参构造函数)
  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值函数

一旦某个函数用户自己定义了,那么编译器就不会再提供了。

自动生成的(默认的、缺省的)拷贝构造函数和拷贝赋值函数,函数体不为空,执行对象的逐成员赋值。

  • 逐成员赋值:对于类中的每一个数据成员,默认拷贝构造函数会执行相应的拷贝操作。对于基本数据类型(如int、float等)类型的成员,逐成员赋值就是直接复制其值。对于类类型的成员,会调用其拷贝构造函数进行拷贝。对于指针类型的成员,则是直接复制指针值,而不是复制指针所指向的内容。

注意:自动生成的拷贝函数的默认操作是“逐成员赋值”,这种拷贝方式叫做“浅拷贝”,这种拷贝方式存在一些问题。浅拷贝将会导致不同对象间的数据共享,同时会在析构函数中引发“double free”异常。默认的拷贝构造和拷贝赋值会存在问题:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(double height = 0, int age = 0, const string& name = "") 
        : m_height(height), m_p_age(new int(age)), m_name(name)
    {
        cout << "构造" << endl;
    }
    ~A()
    {
        cout << "析构" << endl;
        delete m_p_age;
    }

    void print()
    {
        cout << "m_height = " << m_height << ", *m_p_age = " << *m_p_age << ", m_name = " << m_name << endl;
    }
private:
    double m_height;
    int* m_p_age;
    string m_name;
};

int main()
{
    A a1(180.1, 18, "蔡徐坤");
    a1.print();
    // A a2(a1);
    // A a2 = a1;
    // a2.print();
    A a3(180.1, 18, "鸡哥");
    a3.print();
    a3 = a1;
    return 0;
}

默认的拷贝构造和拷贝赋值过程:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(double height = 0, int age = 0, const string& name = "") 
        : m_height(height), m_p_age(new int(age)), m_name(name)
    {
        cout << "构造" << endl;
    }
    ~A()
    {
        cout << "析构" << endl;
        delete m_p_age;
    }
    A(const A& that) // 浅拷贝
    {
        cout << "拷贝构造" << endl;
        m_height = that.m_height;
        m_p_age = that.m_p_age;
        m_name = that.m_name;
    }
    A& operator = (const A& that) // 浅拷贝
    {
        cout << "拷贝赋值" << endl;
        m_height = that.m_height;
        m_p_age = that.m_p_age;
        m_name = that.m_name;
        return *this;
    }

    void print()
    {
        cout << "m_height = " << m_height << ", *m_p_age = " << *m_p_age << ", m_name = " << m_name << endl;
    }

private:
    double m_height;
    int* m_p_age;
    string m_name;
};

int main()
{
    A a1(180.1, 18, "蔡徐坤");
    a1.print();
    // A a2(a1);
    // A a2 = a1;
    // a2.print();
    A a3(180.1, 18, "鸡哥");
    a3.print();
    a3 = a1;
    return 0;
}

3.2 深拷贝

既然默认的拷贝构造和拷贝赋值函数有问题,就必须自己定义一个支持复制指针所指向内容的拷贝构造函数和拷贝赋值函数。即深拷贝。

深拷贝,它是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。深拷贝会创建一个新的对象,并且将原对象中的元素,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(double height = 0, int age = 0, const string& name = "") 
        : m_height(height), m_p_age(new int(age)), m_name(name)
    {
        cout << "构造" << endl;
    }
    ~A()
    {
        cout << "析构" << endl;
        delete m_p_age;
    }

    // 深拷贝构造函数
    A(const A& that) 
    {
        cout << "拷贝构造" << endl;
        m_height = that.m_height;
        m_p_age = new int(*that.m_p_age); // 深拷贝
        m_name = that.m_name;
    }
    
    // 深拷贝赋值函数
    A& operator = (const A& that)
    {
        cout << "拷贝赋值" << endl;
        if (&that != this) // 防止自赋值
        {
            m_height = that.m_height;
            // 释放旧资源
            delete m_p_age;
            // 分配新资源
            m_p_age = new int(*that.m_p_age);
            m_name = that.m_name;
        }
        return *this;
    }

    void print()
    {
        cout << "m_height = " << m_height << ", *m_p_age = " << *m_p_age << ", m_name = " << m_name << endl;
    }

private:
    double m_height;
    int* m_p_age;
    string m_name;
};

int main()
{
    A a1(180.1, 18, "蔡徐坤");
    a1.print();
    // A a2(a1);
    A a2 = a1; // 深拷贝构造
    a2.print();
    A a3(180.1, 18, "鸡哥");
    a3.print();
    a3 = a1; // 深拷贝赋值
    a3 = a3; // 自赋值安全
    return 0;
}

3.3 深拷贝和浅拷贝的一些注意事项

  • :拷贝构造函数和拷贝赋值函数的参数是本类类型的常引用,不能是值传递为什么?

      • 引用:为了防止临时对象的产生
      • const:从语义上来说,在拷贝构造函数中不应该修改原对象的数据,所以是 const。如果不是 const,不能接收 const 对象(不能从一个 const 对象拷贝生成一个新对象)
        cpp
        const A a1(180.1, 18, "蔡徐坤");
        // 一方面不希望参数值被意外改变,另一方面是为了参数可以接受常对象
  • :什么时候应该显示的声明拷贝构造函数?为什么?

    • :当构造函数中开辟了空间或者打开了资源的时候,一定要显示的声明拷贝构造函数(因为开辟空间就必须释放空间,打开资源就必须关闭资源,如果是浅拷贝就会造成资源共享从而意外修改资源或者 double free 或者重复关闭资源)。
      • 当必须写构造函数的时候,一般就必须写拷贝构造函数,默认的拷贝构造函数执行逐成员赋值,是浅拷贝!造成多个对象共用“资源”。
  • :什么是深拷贝,什么是浅拷贝,浅拷贝有什么危害?

      • 浅拷贝:逐成员的简单的赋值
      • 深拷贝:在复制指针/资源的时候,会为被赋值的对象额外开辟新空间/资源
      • 危害:多个对象共用同一个资源
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int data = 0) : m_data(data)
    {
        cout << "构造函数" << endl;
    }
    ~A()
    {
        cout << "析构函数" << endl;
    }
public:
    int m_data;
};

int main()
{
    A* a1 = (A*)malloc(sizeof(A)); // malloc和free不会调用构造函数和析构函数
    cout << "a1->m_data = " << a1->m_data << endl;

    A* a2 = new A(); // new/delete会自动的调用构造函数和析构函数 new会自动计算类型的大小,返回对应类型的指针
    cout << "a2->m_data = " << a2->m_data << endl;

    delete a2; // new/delete会自动的调用构造函数和析构函数
    free(a1);  // malloc和free不会调用构造函数和析构函数
    return 0;
}

4. 临时对象

临时对象(Temporary Object)是在程序执行过程中,由于某种需要(如表达式求值、函数返回等)而临时创建的对象。这些对象通常不会在程序中显式地声明,而是由编译器或运行时库自动创建和销毁。临时对象在C++等面向对象的编程语言中特别常见。

临时对象有几个重要的特点:

  • 自动创建和销毁:临时对象通常在需要时自动创建,并在使用完毕后自动销毁。程序员通常不需要(也不应该)显式地管理这些对象的生命周期。
  • 匿名性:临时对象通常没有名字,因此程序员不能直接在代码中引用它们。它们主要在表达式求值过程中使用,并且一旦表达式计算完成,这些对象就不再可用。
  • 作用域限制:临时对象的作用域通常局限于创建它们的表达式或语句。一旦离开这个作用域,对象就会被销毁。
  • 性能考虑:虽然临时对象对于简化代码和提高抽象层次很有帮助,但它们也可能带来性能上的开销,因为每次创建和销毁对象都需要时间。因此,在性能敏感的代码中,可能需要避免不必要的临时对象创建。

4.1 临时对象的产生方式和解决方案

4.1.1 以值传递的方式给函数传参

这种是最常见的产生临时对象的方式了。

以值传递的方式给函数传参这种方式会直接调用对象的拷贝构造函数,生成一个临时对象传参给函数。当临时对象销毁时,也是函数形参销毁,也是函数执行完后,就会调用该临时对象的析构函数。此时,无论是调用拷贝构造函数和析构函数,都是额外的开销。

cpp
#include<iostream>
using namespace std;

class Person
{
public:
    Person()
    {
        cout << "无参数构造函数!" << endl;
    }
    Person(int a)
    {
        m_age = a;
        cout << "有参数构造函数!" << endl;
    }
    Person(const Person& p)
    {
        m_age = p.m_age;
        cout << "拷贝构造函数!" << endl;
    }
    ~Person()
    {
        cout << "析构函数!" << endl;
    }
    int fun(Person p1) // 普通的成员函数,注意参数是以值的方式调用的
    {
        p1.m_age = 20; // 这里修改对外界没有影响
        return p1.m_age;
    }
    int m_age;
};

int main()
{
    Person p(10); // 初始化
    p.fun(p);
    cout << p.m_age << endl;
    return 0;
}

输出示例:

有参数构造函数!
拷贝构造函数!
析构函数!
析构函数!
10

问题分析:

  1. 多了一次析构函数,表明中间产生了一个对象,然后被销毁
  2. 我们并不期望出现一次拷贝构造,因为我们不希望产生多余对象,自始至终我们都只希望出现一个对象

这个中间的对象就是临时对象。产生的原因:由于 fun 成员函数里面的形参是 Person p1,这样会导致在调用这个 fun 函数时候,由于是值传递所以会传递过去的是实参的复制品即临时对象,并不是外面 main 函数的实参。

解决方案:
把值传递的方式修改为引用传递的方式即可:

cpp
int fun(Person &p1) // 引用型函数参数
{
    p1.m_age = 20; // 这里修改,实参也会修改
    return p1.m_age;
}
4.1.2 类型转换成临时对象/隐式类型转换保证函数调用成功

这种方式就是把类型转化前的对象当作了形参传递给构造函数,生成临时对象,临时对象结束后就会调用析构函数。

cpp
#include<iostream>
using namespace std;

class Person
{
public:
    Person()
    {
        cout << "无参构造函数!" << endl;
    }
    Person(int a)
    {
        m_age = a;
        cout << "有参构造函数!" << endl;
    }
    Person(const Person& p)
    {
        m_age = p.m_age;
        cout << "拷贝构造函数!" << endl;
    }
    Person& operator=(const Person& that)
    {
        cout << "拷贝赋值函数!" << endl;
        if (&that != this)
        {
            m_age = that.m_age;
        }
        return *this;
    }
    ~Person()
    {
        cout << "析构函数!" << endl;
    }
    int fun(Person p) // 普通的成员函数,注意参数是以值的方式调用的
    {
        p.m_age = 20; // 这里修改对外界没有印象
        return p.m_age;
    }
    int m_age;
};

int main()
{
    Person p; // 无参构造
    cout << endl;
    p = 1000; // 隐式类型转换
    cout << endl;
    cout << p.m_age << endl;
}

输出示例:

无参构造函数!

有参构造函数!
拷贝赋值函数!
析构函数!

1000
析构函数!

问题分析:
p = 1000 引起类型转换:

  1. 创建一个临时对象(调用有参构造函数,1000作为参数)
  2. 临时对象赋值给 p(调用拷贝赋值函数)
  3. 临时对象销毁(调用析构函数)

解决方案:
使用直接初始化方式:

cpp
Person p = 1000; // 直接初始化
// 或
Person p(1000);  // 直接调用构造函数
4.1.3 函数返回对象

在函数返回对象时候,会创建一个临时对象接收这个对象;从而调用了拷贝构造函数,和析构函数。

cpp
#include<iostream>
using namespace std;

class Person
{
public:
    Person()
    {
        cout << "无参构造函数!" << endl;
    }
    Person(int a)
    {
        m_age = a;
        cout << "有参构造函数!" << endl;
    }
    Person(const Person& p)
    {
        m_age = p.m_age;
        cout << "拷贝构造函数!" << endl;
    }
    ~Person()
    {
        cout << "析构函数!" << endl;
    }
    int fun(Person p) // 普通的成员函数,注意参数是以值的方式调用的
    {
        p.m_age = 20; // 这里修改对外界没有印象
        return p.m_age;
    }
    int m_age;
};

Person test(Person& p)
{
    cout << "test!" << endl;
    Person p1; // 无参构造
    p1.m_age = p.m_age;
    return p1; // 返回临时对象
}

int main()
{
    Person p;
    cout << "main准备调用test" << endl;
    test(p); // 不接收返回值
    cout << "main调用test完毕" << endl;
    return 0;
}

解决方案:

  1. 使用常引用接收返回值(延长临时对象生命周期):
    cpp
    const Person& p2 = test(p);
  2. 返回引用(避免临时对象创建):
    cpp
    Person& test1(Person& p)
    {
        cout << "test1" << endl;
        p.m_age++;
        return p;
    }

知识如风,常伴吾身