C++类中特殊的函数

C++类中特殊的函数,第1张

C++类中特殊的函数

最近在学C++,记录自己的所得所悟。

在C++中,当用一个对象去初始化另外一个对象,或者用实参初始化形参,或者从函数返回值的时候,通常都会调用复制构造函数,当把类的一个对象赋值给另外一个对象时,通常会调用类的赋值运算符函数。

1. 初始化与赋值的区别

个人理解:初始化是在构造对象的时候,给对象赋初值,而赋值是将一个对象赋值给已有的另外一个对象,覆盖掉目标对象的内容。

class Person
{
    public:
        Person() = default;
        Person(const string& n, int a):name(n), age(a) {}
    private:
        string name = "";
        int age = 0;
};

Person p1; //定义一个Person的对象p1,使用类内初始值进行初始化
Person p2(p1) //定义Person的对象P2,并使用p1去初始化p2,将调用默认复制构造函数
Person p3 = p1; //p3 为新对象,这是初始化,不是赋值,将调用默认复制构造函数
p3 = p2; //p3为已经存在的对象,所以这是赋值不是初始化!

2. 复制构造函数

如果在设计类的时候,没有显式为类定义复制构造函数,那么编译器会为我们生成一个默认的复制构造函数,默认的构造函数执行的是对应数据成员的复制。比如Person类没有显式定义复制构造函数,编译器会为我们生成一个默认的复制构造函数,比如:

Person p1("Megatron", 1000); //p1.name = "Megatron", p1.age = 1000
Person p2(p1); //相当于p2.name(p1.name), p2.age(p1.age),
//name和age成员为Person类的私有成员,不能使用p1.name去访问p1的name成员,这么写只是为了方便

相当于构造了一个对象p2,p2成员的值等于p1对应成员的值。

与构造函数类似,当我们自己定义了一个复制构造函数的时候,编译器就不再为我们提供默认版本的复制构造函数。

编译器提供的默认复制构造函数在有的情况下能满足我们的需求,但是如果成员变量中有指针成员的时候,就需要特别注意,编译器提供的往往不能满足我们的需求,需要我们自己进行定义。

class Myarray
{
    public:
        Myarray()=default; //默认构造函数
        Myarray(int length) : size(length), ptr(new int[size]) {} //构造函数
        ~Myarray() { delete[] ptr; }
    private:
        int size = 0;
        int* ptr = nullptr;
};

int main()
{
    {
        Myarray arr1(10); //创建一个int类型的动态数组,数组长度为10
        Myarray arr2(arr1); //创建一个动态数组arr2,并用arr1去初始化arr2
                           //此时arr1.size == arr2.size, arr1.ptr == arr2.ptr
    }//离开内层作用域,arr1和arr2将调用析构函数,对同一块内存空间执行两次delete[] *** 作,程序崩溃
    return 0;
}

比如上面的一个简易的动态数组类Myarray,里面有一个int* 成员ptr,执行默认的复制构造函数后,arr1的指针成员ptr与arr2的ptr指向同一块内存空间。当对象离开作用域时,调用析构函数,由于两个对象的指针指向同一块内存空间,调用第一个对象的析构函数时,程序正常运行,把指针指向的内存空间释放掉,但是在执行第二个对象的析构函数时,将会对已经delete掉的内存空间再次执行delete *** 作,将导致程序崩溃,这就是所谓的浅拷贝。

所以类中有指针成员时,往往需要执行深拷贝 *** 作,需要自己定义一个拷贝构造函数,拷贝构造函的声明通常如下:

classname(const classname& obj)

注意,形参列表必须是本类对象的引用,如果是classname(const classname obj),将采用值传递的方式传递参数,那么在用实参去初始化形参的时候,将会调用类的复制构造函数,进入死循环!使用const引用是因为不管是const对象还是非const对象都能传进去。对于上面简易的动态数组类,定义一个如下的复制构造函数

Myarray:Myarray(const Myarray& arr) : size(arr.size)
{
    ptr = new int[size]; //重新申请一块同样大小的内存空间
    //逐个对象复制
    for(int index = 0; index != size; ++index){
        ptr[index] = arr.ptr[index];
    }
}

Myarray arr1(10);
Myarray arr2(arr1);//调用复制构造函数,arr2和arr1的ptr成员指向不同的内存空间
//对象离开作用域后,不会对同一块内存空间析构两次

2. 析构函数

在对象离开其作用域时,析构函数将会自动调用,用来执行清理工作,比如释放内存,关闭文件,断开数据库连接等等。对在heap(堆)上开辟了动态内存的类(通常含有指针成员),需要在析构函数中手动释放掉开辟的内存空间,将其返还给 *** 作系统,否则申请的这块内存无法得到重复利用,导致内存泄漏。

需要注意的是,如果构造函数中使用new动态分配内存,那么在析构函数中则使用delete释放内存;如果使用new[]分配内存,则使用delete[]释放内存。

3. 拷贝赋值运算符

如果没有为类显式重载赋值运算符,那么编译器也会为我们提供默认的赋值运算符,执行的是把右侧对象的成员赋值给左侧对象的对应成员,如果类内没有指针成员时,默认的赋值运算符能满足我们的需求。但是如果内中有指针成员时,默认的赋值运算符会带来两个问题,比如:

Myarray arr1(10);
Myarray arr2(10);
arr1 = arr2;//赋值 *** 作,其实就是arr1.size = arr2.size, arr1.ptr = arr2.ptr

(1)与默认的复制构造函数相似,默认的赋值运算符将执行指针成员的拷贝,是浅拷贝,在析构的时候也会出现对同一块内存释放两次而导致程序崩溃;

(2)内存泄漏:在赋值之前,arr1对象的ptr成员指向了一块动态内存,是属于arr1自己持有的资源,默认的赋值 *** 作后,左侧对象arr1的指针成员ptr被赋予了新地址,arr1的ptr指针将指向新的内存空间,导致原来指向的内存空间现在没有指针指向了,这一块内存将无法归还给 *** 作系统,造成内存泄漏!

所以,在类含有指针成员时,必须为类定义一个拷贝赋值运算符,就是重载= *** 作符,通常的声明如下:

classname& operator= (const classname& obj);

比如对简易的动态数组类Myarray,可以定义如下的拷贝赋值运算符:

Myarray::Myarray& operator= (const Myarray& arr)
{
    if(this == &arr) //检测赋值是否为自我赋值,如果为自我赋值,直接返回
        return *this;
    delete[] ptr; //将=左侧对象的ptr成员指向的内存空间先释放掉
    ptr = new int[arr.size];//重新为=左侧对象申请内存空间
    //执行深拷贝
    for(int i = 0; i != size; ++i){
        ptr[i] = arr.ptr[i];
    }
}

4. 移动构造函数

C++11增加了移动语义,从而有了所谓的移动构造函数,移动构造函数的参数是右值引用。顾名思义,C++的左值指的是能放在赋值运算符“=”左侧,能用取地址运算符&获取其地址的对象,比如一个变量,引用都是左值;而右值指的是只能出现在赋值运算符的右侧,不能用取地址运算符&获取其地址的对象,比如临时对象,字面量,表达式,函数的返回值等都是右值。比如声明一个如下的函数:

Myarray func(int size)
{
    ...
    return Myarray(size);
}

函数的返回类型为Myarray类型,在程序从函数中将对象返回给调用func的函数时,将会调用复制构造函数,执行一次深拷贝,由于Myarray(size)是一个没有名字的临时对象,是一个即将消亡的右值,可以定义如下的移动构造函数来避免深拷贝

Myarray::Myarray (Myarray&& arr) : size(arr.size), ptr(arr.ptr)
{
    // && 表示右值引用
    //必须将arr的ptr指针置为空指针,否则一个构造出来的对象的ptr与临时对象的ptr指向同一块内存
    arr.ptr = nullptr; //这一步必不可少
}

上面的移动构造函数将临时对象arr所持有的资源转移出去了,避免了深拷贝。同时,将一个即将消亡的临时对象arr的ptr成员设置为nullptr,析构的时候,delete[] 一个nullptr不会有任何问题。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5691200.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存