欢迎光临
我们一直在努力

【C++】类与对象

构造和析构函数

对于类定义的每个对象,都需要初始化和清理。(清理可能不需要,一旦需要,但忘记了清理,就会内存泄露)

构造和析构函数,最大的好处是自动调用。这样我们就不用提心吊胆,检查有没有忘记什么操作。

我们可以自己定义构造和析构函数,也可交给编译器自动帮我们构造。

实际情况是,构造函数大多要自己写出来。但析构大部分可以不用自己写。(内置类型和自定义类型)

若我们不显式定义构造函数,编译器自动生成的,大都不满足我们的需求。(尤其是内置类型,随机的值)

析构函数则相反,只要对象中的成员没有额外申请资源,我们就可以不用自己析构。

顺序

析构对象时:反向,后定义的先释放。

构造对象时:正向,先定义的先构造。

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
cout << "Stack()" << endl;
}

~Stack()
{
cout <<" ~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};

class MyQueue
{
public:
MyQueue()
{
cout << "MyQueue()" << endl;
}
~MyQueue()
{
cout << "~MyQueue()" << endl;
}
private:
Stack pushst;
Stack popst;
};

int main()
{
Stack st;

MyQueue mq;

return 0;
}

拷贝构造(复制)

故事发生在初始化对象时,我们想要复制粘贴一份对象到另一个对象中去~

若是实参传入同类型对象,就需要调用拷贝构造。

特点

  • 是构造函数的重载。
  • 其第一个参数必须是对象的引用,若是传值,会无穷递归。后面的参数必须设置为缺省值。且为了防止意外发生,最好采用const 引用。
  • 解释:

    若是给传值传参函数传入类对象,程序会先去调用类的拷贝构造,把实参对象的值拷贝给形参,才算成功传入了实参。

    即传值传参会形成新的拷贝构造。

    而如果类自身的拷贝构造函数,使用的就是传值传参,那么当参数传进来后,它又形成一个新的拷贝构造,将无穷递归,无法返回。

    使用引用,给传入的参数取别名,而不是创建新的对象,避免了无限调用拷贝构造。

    (传指针也可以,但就不称为拷贝构造了,因为传实参的形式不一样,只能称为普通的构造函数)

    class Date
    {
    public:
    Date(int year=1,int month=1,int day =1)
    {
    _year = year;
    _month = month;
    _day = day;
    }
    //拷贝构造函数,const引用
    Date(const Date& d)
    {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    }
    //传指针构造函数
    Date(Date* d)
    {
    _year = d->_year;
    _month = d->_month;
    _day = d->_day;
    }

    void Print()
    {
    cout << _year << '/' << _month << '/' << _day << endl;
    }
    private:
    int _year;
    int _month;
    int _day;
    };

    Date d1(2026, 6, 12);

    //这里采用传指针,可以完成拷贝,但不是拷贝构造,仅仅是一个普通的构造。
    Date d2(&d1);
    d1.Print();
    d2.Print();

    //这样写才是拷贝构造,通过同类型的对象初始化构造,而不是指针。
    Date d3(d1);
    d3.Print();

    //也可这样写,也是拷贝构造。
    Date d4 = d1;
    d4.Print();

            3.凡是自定义类型对象,进行拷贝行为必须调用拷贝构造,故传值传参和传值返回,都会调用拷贝构造完成。

    void Func1(Date d)
    {
    cout << &d << endl;
    d.Print();
    }

    Date d1(2026, 6, 12);

    //C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝。
    //故这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以减少这里的拷贝行为。
    Func1(d1);

    cout << &d1 << endl;

            可以看到,这里打印出来的&d和&d1不同,说明d1的值成功拷贝进入d地址中。

            4.无显式定义的拷贝构造函数时,由编译器默认生成,对内置类型完成值拷贝/浅拷贝,

    注意:如果内置类型是指针并且申请了资源,直接进行值拷贝,会出问题。

            ①两个类对象的成员指针变量会指向同一个位置,一个修改会影响另一个。

            ②若析构一个对象后,其指向空间被释放,另一个对象会析构同一空间,该空间就被释放两次。崩溃。

    这种情况下,必须自己定义相应的拷贝构造函数——

    深拷贝  ①开辟一个相同大小的空间,让新对象的指针指向 这一块空间。 ②给这个空间存入一样的内容。(开辟新空间、拷贝数值)

    如此一来,两对象将互不影响。 

    typedef int STDataType;

    class Stack
    {
    public:
    Stack(int n = 4)
    {
    _a = (STDataType*)malloc(sizeof(STDataType) * n);
    if (_a == nullptr)
    {
    perror("malloc申请空间失败");
    return;
    }
    _capacity = n;
    _top = 0;
    }

    //自定义实现深拷贝。
    Stack(const Stack& st)
    {
    //先让_a指向一块同样大小的空间,再拷贝值。
    _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
    if (nullptr == _a)
    {
    perror("malloc申请空间失败");
    return;
    }

    //进行拷贝。
    memcpy(_a, st._a, sizeof(STDataType) * st._top);

    _capacity = st._capacity;
    _top = st._top;
    }

    void Push(STDataType x)
    {
    if (_top == _capacity)
    {
    int newcapacity = _capacity * 2;
    STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
    if (tmp == nullptr)
    {
    perror("realloc fail");
    return;
    }
    _capacity = newcapacity;
    _a = tmp;
    }

    _a[_top++] = x;
    }

    ~Stack()
    {
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
    }
    private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
    };

    Stack st1;
    st1.Push(1);
    st1.Push(2);

    //如果用自动生成的拷贝构造完成浅拷贝,
    //会导致st1,st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃。
    //这里使用的是自定义的深拷贝构造。
    Stack st2 = st1;

           而对于自定义类型,编译器默认调用其构造、拷贝构造和析构函数。

    class MyQueue
    {
    public:
    //…
    private:
    Stack pushst;
    Stack popst;
    };

    //生成mq1时,编译器自动调用Stack类的构造函数来构造pushst,popst
    MyQueue mq1;

    //MyQueue自动生成的拷贝构造,会自动调用Stack拷贝构造来构造pushst,popst的拷贝
    //只要Stack的拷贝构造没问题,MyQueue的拷贝就没问题。
    MyQueue mq2 = mq1;

            5.若需要显式写析构函数,就需要显式写拷贝构造。反之,也成立。

            6.只有当前函数结束后还存在的 返回对象,才能用引用返回。否则,野引用。

    Date& Func2()
    {
    Date tmp(2024, 7, 5);
    tmp.Print();

    return tmp;
    }

    Date d1(2026, 6, 12);

    //Func2返回一个局部对象tmp的引用作为返回值
    //Func2函数结束,tmp对象就销毁了,相当于一个野引用
    Date ret = Func2();
    ret.Print();

    若是传值返回,会调用拷贝构造,创建一个临时对象,即使函数结束,局部对象均被销毁,临时对象也还存在,程序可以正常运行。

    调用形式

    • Date d2 = d1 ;
    • Date d2(d1) ;

    补充

    数组不需要释放。

    数组在对象空间内,没有额外申请资源。

    指针也不一定需要释放,主要看它是否有动态开辟资源。(以上提到的指针均有额外资源)

    赞(0)
    未经允许不得转载:171主机测评 » 【C++】类与对象
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址