二、特殊成员函数#

当定义一个类后,它的对象在未来的操作中,总会不可避免的总会碰到如下的行为: 创建拷贝赋值移动销毁 。 这些操作实际上是通过六种特殊的成员函数来控制的: 构造函数析构函数 拷贝构造函数拷贝赋值函数移动构造函数移动赋值函数 。默认情况下,编译器会为新创建的类添加这些函数,以便它的对象在未来能够执行这些操作。

1. 构造函数#

1. 一般方式构造#

构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。与类名同名,没有返回值,可以被重载,通常用来做初始化工作。 在python中,有类似的____init___ 函数用于初始化工作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<string>
using namespace std;

class student{

    string name;
    int age ;

    public :
        //构造函数
        student(){
            cout << "执行无参构造函数" << endl;
        }

        student(string name ){
            cout << "执行含有一个参数的构造函数" << endl;
        }

        student(string name , int age ){
            cout << "执行含有两个参数的构造函数" << endl;
        }

};


int main(){

    //创建三个对象,会执行三个对应你的构造函数
    student s1 ;
    student s1{"张三"}
    student s1{"张三"28}

    return 0 ;
}

2. 初始化列表方式#

在之前成员的初始化工作,都是在构造函数的函数体里面完成的。如果使用初始化列表,那么成员的初始化赋值是在函数体执行前完成,并且初始化列表有一个优点是: 防止类型收窄,换句话说就是精度丢失

有三种情况需要使用到构造函数初始化列表

  • 情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
  • 情况二、需要初始化const修饰的类成员或初始化引用成员数据;
  • 情况三、子类初始化父类的私有成员;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <string>

using namespace std;

class student{
    string name;
    int age;

    /*
    //早期的方式
    student(string name_val , int age_val){
       name = name_val;
       age = age_val;
    }
    */

    //更好的方式
    student(string name ,int age):name{name},age{age}{
        cout << "执行有参构造函数" <<endl;
    }

};

int main(){
    //编译允许通过,输出 a1 和 a2 为 30 和20 ,小数点省略
    int a (30.22);
    int a = 20.33;

    //编译失败,不允许赋值。防止类型收窄看精度丢失。
    //int a{20.33};

    student s("张三"   18)

    return 0 ;
}

3. 委托构造函数#

一般来说,如果给类提供了多个构造函数,那么代码的重复性会比较高,有些构造函数可能需要包含其他构造函数中已有的代码,为了让编码工作更简单,C++11 允许程序员在一个构造函数的定义中使用另一个构造函数。这种现象被称为委托.

1. 早前的构造函数写法#

早期的做法是,每个构造函数完成变量的赋值工作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <string>

using namespace std;

class Student{

    string name;
    int age ;

    public:
        //无参构造
        Student():name{"张三"},age{19}{

        }; 

        //一个参数构造
        Student(string name_val):name{name_val},age{19}{

        }; 

        //两个参数
        Student(string name_val , int age_val ):name{name_val},age{age_val}{

        }; 
}

int main(){
    Student s1;
    Student s2("李四");
    Student s3("李四" , 20 );
    return 0 ;
}

2. 委托构造函数写法:#

委托构造函数实际上就是已经有一个构造函数定义好了所有初始化工作的逻辑,那么剩下的构造函数就不用做这个活了,全部交给它来做即可。有点类似,A调用C, B调用C ,而C 把所有的初始化代码都写完了。那么A和B只是负责委托C来完成即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <string>

using namespace std;

class Student{

    string name;
    int age ;

    public:

        //无参构造 委托给两个参数的构造函数
        Student():Student{"张三"  19}{

        }; 


         //一个参数构造 委托给两个参数的构造函数
        Student(string name_val):Student{name_val , 19}{

        };

        //由该函数完成最终的成员赋值工作
        Student(string name_val , int age_val):name{name_val},age{age_val}{

        };
}

int main(){
    Student s1;
    Student s2("李四");
    Student s3("李四" , 20 );

    return 0;
}

2. 析构函数#

和python一样,c++也有析构函数。类的析构函数是类的一种特殊的成员函数,与构造函数正好相反,它会在每次删除所创建的对象时执行。

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。不能被重载,一般用于释放资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string>

using namespace std;

class Student{

    string name;
    int age ;

    public :
        //构造函数
        Student(){
            cout << "执行无参构造函数" <<endl;
        }
        Student(string name ){
            cout << "执行含有一个参数的参构造函数" <<endl;
        }
        Student(string name , int age ){
            cout << "执行含有两个参数的构造函数" <<endl;
        }

        //析构函{}
        ~Student(){
            cout << "执行析构函数" <<endl;
        }
};

int main(){

    Student *s1 = new Student();
    Student *s2 = new Student();
    Student *s3 = new Student();


    //释放对象
    delete s1;
    delete s2;
    delete s3;

    return 0 ;
}

3. 拷贝构造函数#

1. 初探拷贝#

C++中经常使用一个常量或变量初始化另一个变量, 比如:

1
2
3
4
5
6
7
int mian(){

    int a = 3;
    int  b = a;

    return 0 ;
}

使用类创建对象时,构造函数被自动调用以完成对象的初始化,那么能否象简单变量的初始化一样,直接用一个对象来初始化另一个对象呢?

不难看出,s2对象中的成员数据和 s1 是一样的。相当于将s1中每个数据成员的值复制到s2中,这是表面现象。实际上,系统调用了一个拷贝构造函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>

using namespace std;

class student{

    public :
       string name;
        int age ;

        student(string name , int age ){
            cout << "执行含有两个参数的构造函数" << endl;
        }

        ~student(){
            cout << "执行析构函数" <<endl;
        }
};

int main(){

    Student s1{"张三" , 19 };
    cout << s1.name << " : " << s1.age <<ednl;

    Student s2 = s1;
    cout << s2.name << " :: " << s2.age <<ednl;

    return 0 ;
}

2. 浅拷贝#

指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。如果数据中有属于动态成员 ( 在堆内存存放 ) ,那么浅拷贝只是做指向而已,不会开辟新的空间。默认情况下,编译器提供的拷贝操作即是浅拷贝。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
include <iostream>
include <string>

using namespace std;

class Student {
public:
    int age ;
    string name;

public :
    //构造函数
    Student(string name , int age ):name(name),age(age){
        cout<< " 调用了 构造函数" << endl;
    }

    //拷贝构造函数
    Student(const Student & s){
        cout << "调用了拷贝构造函数" << endl;
        age = s.age;
        name = s.name;

    }

    //析构函数
    ~Student(){
        cout << "调用了析构函数" << endl;
    }
};

int main(){

    Student s1("张三" , 18);
    cout << s1.name << " : " << s1.age <<endl;


    Student s2 = s1;
    cout << s2.name << " :: " << s2.age <<endl;

    return 0 ;
}

3. 浅拷贝引发的问题#

默认情况下,浅拷贝已经足以应付日常的需求了,但是当类中的成员存在动态成员(指针)时,浅拷贝往往会出现一些奇怪的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <string>

using namespace std;

class Student {
public:
    string name;
    string *address;

    Student(string name , string * address):name(name),address(address){
        cout << "执行构造函数" << endl;
    }


   // 这里还是默认的浅拷贝。 由于address是指针类型,如果是浅拷贝,那么两个指针会指向同一个位置。
 Student(const Student & s){
        cout << "调用了拷贝构造函数" << endl;

        name = s.name;
       address = s.address;

    }

     //析构函数
    ~Student(){
        cout << "调用了析构函数" << endl;

        //这里将会删除两次内存空间
        delete address;
        address = nullptr;
    }

};

int main(){
    string address="深圳";
    Student s1("张三" , &address);

    //此处会执行拷贝。
    Student s2 = s1;
    cout << s2.name << " : " << s2.address << endl;


    //修改第一个学生的地址为:北京
    *s1.address = "北京"

    //第二个学生的地址也会变成北京
    cout << s2.name << " : " << s2.address << endl;


    return  0 ; 
}

4. 深拷贝#

深拷贝 也是执行拷贝,只是在面对对象含有动态成员时,会执行新内存的开辟,而不是作简单的指向。在发生对象拷贝的情况下,如果对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <string>

using namespace std;

class Student {
public:
    string name;
    string *address;

    Student(string name , string * address):name(name),address(address){
        cout << "执行构造函数" << endl;
    }

    //深拷贝
    Student(const Student & s){
            cout << "调用了拷贝构造函数" << endl;
            age = s.age;
            name = s.name;

         if(address == nullptr){
             //开辟新的空间
             address = new string();
             *address = s.address;
         }
    }

      //析构函数
     ~Student(){
         cout << "调用了析构函数" << endl;

         if(address != nullptr){
             delete address;
             address = nullptr;
         }
     }

};

int main(){
    string address="深圳";
    Student s1("张三" , &address);

    //此处会执行拷贝。
    Student s2 = s1;
    cout << s2.name << " : " << s2.address << endl;


    //修改第一个学生的地址为:北京
    *s1.address = "北京"

    //第二个学生的地址也会变成北京
    cout << s2.name << " : " << s2.address << endl;


    return  0 ; 
}

5. 触发拷贝的场景#

如果是生成临时性对象或者是使用原有对象来初始化现在的对象,那么都会执行拷贝构造。一般调用拷贝构造函数的场景有以下几个:

  • 对象的创建依赖于其他对象。
  • 函数参数(传递对象)
  • 函数返回值 (返回对象)

  • Student.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <string>

using namespace std;

class Student {
public:
    int age ;
    string name;

public :
    //构造函数
    Student(){
        age = 19 ;
        name = "张三";
        cout<< " 调用了 构造函数" << endl;
    }

    //拷贝构造函数
    Student(const Student & s){
        cout << "调用了拷贝构造函数" << endl;
        age = s.age;
        name = s.name;

    }

    //析构函数
    ~Student(){
        cout << "调用了析构函数" << endl;
    }
};

1. 对象创建依赖于其他对象#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include "Student.cpp"

using namespace std;



int main(){

    Student stu1("张三",18); //执行构造函数
    Student stu2 = stu1; //执行拷贝构造函数

    return 0 ;
}

2. 函数参数#

编译器不会优化这个方向,因为一旦优化掉了,那么就不会执行拷贝的工作,那就代表传递进来的对象实际上就是外部的原对象,有可能在函数内部对对象进行了修改,会导致外部对象跟着修改。所以默认情况下,只要是函数参数传递,都会发生拷贝。 如果不想发生拷贝,请使用 引用 或者 指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
#include "Student.cpp"

using namespace std;


void printStun(Student s){

    cout << s.name << " : "<< s.age << endl;
}

int main(){
    Student stu1("张三",18); //执行构造函数
    printStudent(stu1); //执行拷贝构造函数

    return 0;
}

3. 函数返回值#

为了避免过多的临时性对象创建,消耗内存,编译器内部做了优化,函数的返回值不会再产生临时对象,直接把生成的对象赋值给外面接收的变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
#include "Student.cpp"

using namespace std;


//构造函数 由于函数执行完毕会有个临时对象来接收值,所以这里还会执行拷贝构造
Student createStu(){
    return Student("张三",18); 
}

int main(){
    Student stu = createStu(); //这里还会执行构造函数
    return 0;
}

4. 编译器自动优化#

编译器有时候为了避免拷贝生成临时对象而消耗内存空间,所以默认会有优化、避免发生过多的拷贝动作所以打印的日志可能不是我们所期望的,这时候,如果手动编译的话,可以添加参数,

1
2
3
4
5
#如果手动编译 可以添加以下参数  -fno-elide-constructors 
g++ -std=c++11  main.cpp -fno-elide-constructors

# 如果使用cmake编译,可以添加配置 
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-elide-constructors")`

6. 再说函数返回值#

c++ 严禁函数返回内部的存放于栈内存的局部变量引用或者指针,因为函数一旦执行完毕,那么内部的所有空间都将会被释放,这时如果在外面操作返回值,将出现错误。所以有时候临时拷贝的工作就变得不可避免。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int* getNum(){
    int a = 9;
    return &a ;
}

int* getNum2(){
    int a = new int(9);
    return &a ;
}

int main(){
    int *p = getNum(); //错误! 一旦getNum()执行完毕,内部栈空间都将释放掉。

    int *p = getNum2(); // 正确,因为函数返回的指针指向的地址位于堆内存中。
    return 0 ;
}

7. 移动构造函数#

有时候我们需要新创建的对象,拥有旧对象一模一样的数据,当然这可以使用拷贝操作来完成。但是假设旧对象不再使用,那么拷贝操作就显得有点弱,因为旧对象仍然占据着空间。C++11中推出了 移动构造操作,也就是完全的把旧对象的数据 "移动" 到新对象里面去,并且把旧对象被清空了。

注意: 移动构造或者是拷贝构造,针对的都是数据的拷贝或者移动,对象的该创建还是创建,他们两针对的仅仅是数据是以和种方式得来而已。

  • 移动语义

要想完成移动构造,需要配合右值引用来实现。因为把某一份数据,移动到另一个地方去,实际上操作的是右值,而右值引用恰好指向的是右值。这常常出现在对象的搬运上,假设现在要做两份数据的交换,最初的设想是进行值的拷贝、复制,但是这样会产生一份临时拷贝值,不如直接移动过去划算。但在C++里面的移动 实际上并不是真的在移动数据,只是窃取了原来数据的控制权而已。

1. 没有移动构造#

使用getStu函数生成一个学生对象,调用返回后,使用stu来接收。 在没有移动构造函数的情况下,临时对象的产生会调用拷贝构造。此举会造成资源的浪费,并且效率也低。编译器为了避免过多的临时对象的创建工作,内部已经做了优化。

  • 学生类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Student{
public :
    int *age;
    Student():age(new int(18)){
        cout << "执行构造函数~!~"<<endl;
    }

    //深拷贝
    Student(const Student &s):age(new int(*s.age)){
        cout << "执行拷贝构造函数~!~"<<endl;
    }
    //移动赋值
    ~Student(){
        cout <<"执行析构函数~!" << endl;
        delete  age;
    }
};

Student getStu(){
    Student s ;
    cout <<"getTemp =" << __func__ << " : " << hex << s.age << endl;
    return s;
}
  • main函数
1
2
3
4
5
6
int main(){

    Student stu = getStu();

    return 0 ;
}

2. 使用移动构造#

使用移动构造,会使得在返回对象时,不会调用拷贝构造函数,这也就避免产生了对象的拷贝工作。

  • 学生类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Student{
public :
    int *age;
    Student():age(new int(18)){
        cout << "执行构造函数~!~"<<endl;
    }

    //深拷贝
    Student(const Student &s):age(new int(*s.age)){
        cout << "执行拷贝构造函数~!~"<<endl;
    }

    //移动构造
    Student( Student &&s):age(s.age){
         cout << "执行移动!!!构造函数~!~"<<endl;
        //移动之后,一般即可让原有对象的指针变成空指针
        s.age = nullptr;

    }

    //移动赋值
    ~Student(){
        cout <<"执行析构函数~!" << endl;
        delete  age;
    }
};

Student getStu(){
    Student s ;
    cout <<"getTemp =" << __func__ << " : " << hex << s.age << endl;
    return s;
}
  • main
1
2
3
4
5
int main(){

    Student stu = getStu();
    return 0 ;
}

3. std::move函数#

move 函数名字很具有迷惑性,但是它并不能移动任何东西。它唯一的作用就是把一个左值转化成一个对应的右值引用类型,继而可以通过右值引用使用该值。并且在使用move函数可以避免不必要的拷贝工作, move 是将对象的状态或者所有权从一个对象转化到另一个对象,只是转换状态或者控制权,没有内存的搬迁和拷贝 . 使用move函数即表示该对象已经不再使用,要被即将销毁了。

1
2
3
4
5
6
7
8
9
int main(){

    int a = 3;
    int &b = a ; //左值引用指向左值
    int &&c = 3; //右值引用指向右值
    int &&d = move(b); //右值引用,指向右值, move函数强制转化 左值引用b 成右值引用

    return 0 ;
}
  • 使用move出发移动构造函数

默认情况下,如果直接赋值,那么执行的是拷贝构造函数,并且有时候为了避免频繁的执行拷贝工作,可以直接使用move函数转化左值成右值引用,进而变成直接操作数据。

1
2
3
4
5
6
7
8
int main(){
    Student stu1 ;
    Student stu2 = stu1; // 执行拷贝构造函数

    Student stu3 = move(stu1); // 执行移动构造函数
    Student stu3(movestu1) ; // 和上一行代码同效果
    return 0 ;
}