wyqmx

导航

C++类的构造函数和析构函数

1.构造函数和析构函数是什么

1.1构造函数

通常一个类,其内部包含有变量和函数,当我们想要使用类的时候,总是会不得不面临这样一个问题,需要对类进行初始化,否则内部这些变量就会是随机值,导致程序出现异常。

为此,我们需要在使用类之前对它进行初始化,C++就提供了这样一类特殊的函数——构造函数,它在创建类的时候会被自动调用,对类进行初始化。

1.2析构函数

析构函数和构造函数类似,它会在类对象被销毁时自动调用,主要负责一些清理工作。通常在函数结束后,在当前函数内生成的那些类就会被调用。

如果构造函数没有使用new来创建堆内存对象的话,一般是不需要析构函数做任何处理的,否则需要在析构函数内使用delete来释放这些堆内存,以避免出现内存泄漏的风险。

2.构造函数

2.1.基本形式

举个例子,测试类当中有三个变量a、b、c需要初始化,可以这样写。

class test {
public:
    test(int a, int b, int c = 0)
        : m_a(a),m_b(b),m_c(c)
    {
        m_a = 100;
        //return;    错误,构造函数不允许return
    }
    void display() {
        cout << m_a << " " << m_b << " " << m_c << endl;
    }
private:
    int m_a;    // m_a = 1,再次赋值,m_a = 100
    int m_b;    // m_b = 2
    int m_c;    // m_c没有输入,默认 = 0
};

int main(){
    test t(1,2):
    t.display();
    return 0;
}

基本格式是 classname() : {} 或 classname() {}

由以下几个特征:

1.没有返回类型,同时也不能有返回值;

2.括号()中间是函数的输入变量,可以在后面赋值,这样输入就会在没有输入的情况下赋默认值(这一条适用于所有函数,但是必须保证默认赋值变量的顺序是从后往前);

3.冒号:后面的内容是初始化列表,使用A(B)的方式,将B赋值给A,用逗号,隔开,但最后一个变量不能有逗号,这段内容写在()和{}中间。需要注意的是,初始化列表不是必须的,可以完全不使用初始化列表,全部都在{}内赋值也可以。

4.花括号{}中间的内容,就像正常的函数实现一样,在初始化时,会执行一次内部的程序。

因此,最后输出的结果如注释写的那样:m_a = 100, m_b = 2, m_c = 0。

2.2.特殊构造函数

构造函数是运行重载的,下面这些构造函数本质和其它构造函数没有任何区别,只是这些用法比较多,从而有了一些特殊的名称而已。

默认构造函数:

当我们没有写任何构造函数的时候,系统也会隐含存在一个构造函数,只不过它的输入变量、初始化列表,以及函数内容都是空的,不进行任何初始化操作。如果我们写一个构造函数不带任何输入,那么就会覆盖掉默认的构造函数,使用人工编写的构造函数。

注意:默认构造函数是公有的

class test {
public:
    //默认构造函数,就算不写出来,程序也会默认附带这样一个构造函数
    test() {}
private:
    int m_a;
};

int main(){
    test t;
    return 0;
}

拷贝构造函数:

以相同的类来作为当前类的唯一输入的构造函数。这里有两个关键点,相同的类唯一输入,说大白话就是,将同样的类A拷贝到B。至于如何进行初始化,那就要针对不同的类来考虑了,通常都需要手动对类的内部变量进行一一拷贝赋值。

class test {
public:
    test(const test& other) 
        //将other中的所有值赋值给当前类
        : m_a(other.m_a)
           m_b(other.m_b)
    {}
private:
    int m_a; int m_b;
};

int main(){
    test t1;
    test t2(t1);
    return 0;
}

转换构造函数:

和拷贝构造函数类似,只不过是将另外一个类,转化为当前类,那么情况就更为复杂,需要依据使用场景和类的具体内容,进行初始化赋值。

例如这个例子,需要用test_a给test_b赋值,但是又无法访问a的私有成员,就需要使用get_a函数,且我们不希望更改test_a的内容加上了const,但是赋值时又需要去掉const,从而使用const_cast修饰。

class test_a {
public:
    test_a(int a) 
        : m_a(a)
    {}
    const int get_a() { return m_a; }
private:
    int m_a;
};

class test_b {
    test_b(const test_a& t)
        : m_b(const_cast<test_a&>(t).get_a())
    {}
private:
    int m_b;
};

int main(){
    test_a ta(1);
    test_b tb(ta);
    return 0;
}

3.析构函数

class test {
public:
    test() { }
    ~test() { }
};

基本格式是 ~classname() {} 

由以下几个特征:

1.没有返回类型,同时也不能有返回值;

2.类名前面需要加一个~,代表析构函数名;

3.花括号{}中间的内容,就像正常的函数实现一样,在类被销毁时,会执行一次内部的程序。

注意:通常情况下,我们不会主动调用析构函数,都是让系统自动去调用。

 

析构函数使用场景一般像下面这样,用于释放构造时占用的堆内存:

class test{
public:
    test() : m_p(new int) {}
    ~test() { delete m_p; }
private:
    int* m_p;
}

4.explicit显式构造

在初始化赋值时,我们通常还习惯这样一种用法:

int a = 2.5;

这里实际上包含有两个步骤:

1.将(2.5)由类型float隐式转化为类型int
2.再将转化后的int类型值,赋值给a

4.1.隐式构造

例如下面这个例子,类的隐式构造当中,就会先调用Implicit(int num = 0)函数,将1转化为Implicit类,然后赋值给Implicit a。

class Implicit {
public:
    Implicit(int num = 0)
        :m_num(num)
    {}
    ~Implicit() {};

    Implicit& operator=(Implicit& other) {
        m_num = other.getNum();
    }
    const int getNum() { return m_num; }
private:
    int m_num;
};

int main(){
    Implicit ip = 1; //允许操作
    return 0;
}

4.2.显式构造

而在构造函数前加上explicit关键字,代表只允许进行显式转换,因此自动调用Implicit(int num = 0)将1转化为Implicit类的操作就会变成非法的,所以我们得想想其他办法。

例如,显式将1转化为Implicit,或者创建中间变量等。

class Explicit {
public:
    explicit Explicit(int num = 0)
        :m_num(num)
    {}
    ~Explicit() {} ;

    Explicit& operator=(Explicit& other) {
        m_num = other.getNum();
    }
    const int getNum() { return m_num; }
private:
    int m_num;
};

int main(){
    //Explicit ep = 1;    //不允许操作
    Explicit ep = Explicit(1);    //允许操作
    Explicit ep = Explicit::Explicit(1);    //允许操作
    Explicit temp(1);  Explicit ep = temp;    //允许操作
    return 0;
}

5.私有构造函数

私有构造函数有以下几个特性:

5.1.私有构造函数不能在外部直接调用

通常利用这种特性,可以屏蔽某些不想要开放的构造函数。例如,禁止拷贝类的时候,可以私有化拷贝构造函数。

5.2.重载时,始终可以调用那些公有的析构函数

如果重载了多个构造函数,只要存在公有的构造函数,就可以直接调用那些公有的构造函数来构造这个类,而不会出现无法构造的情况。

5.3.可以在类的内部调用构造函数的方式来进行私有化构造

例如单例模式当中,内部含有一个指向自身类型的静态指针,通过公有的函数来调用构造函数,并将类赋值给这个指针并返回,来实现类的构造和获取。

class PrivateConstruct {
public:
    ~PrivateConstruct() {
        cout << "destroy PrivateConstruct" << endl;  
        m_pInstance = nullptr;
    };
    static PrivateConstruct* GetInstance() {
        if(m_pInstance == nullptr)
            m_pInstance = new PrivateConstruct();
        return m_pInstance;
    }
    PrivateConstruct(int num) { };    //对应2
private:
    PrivateConstruct() { cout << "creat PrivateConstruct" << endl; };
    PrivateConstruct(const PrivateConstruct& other) { };    //对应1
    static PrivateConstruct* m_pInstance;
};
PrivateConstruct* PrivateConstruct::m_pInstance = nullptr;    //对应3

int main(){
    //PrivateConstruct pc1;    //不允许外部构造,PrivateConstruct()为私有
    PrivateConstruct pc2(1);    //允许外部构造,PrivateConstruct(int)为公有
    //PrivateConstruct pc3(pc2);    //不允许外部构造,PrivateConstruct(PrivateConstruct&)为私有
    PrivateConstruct* p_pc = PrivateConstruct::GetInstance();
    return 0;
}

原则上,只有析构函数是公有的,两种方式都可以析构,并释放内存。

p_pc->~PrivateConstruct();
delete p_pc;

6.私有析构函数

私有析构函数有以下几个特性:

6.1.私有析构函数会导致类无法直接被构造

6.2.在构造函数为公有的情况下,可以通过new创建堆对象的方式构造

可以通过这种方式,来创建类的指针对象。

6.3.成功创建类指针对象之后,也无法通过调用析构函数或delete的方式释放

因为此时的析构函数为私有,外部的delete方法也相当于从外部直接调用析构函数。

6.4.析构函数本身无法在类的内部被直接调用

该特性适用于所有的析构函数。

6.5.可以通过在类内部delete this的方式,间接调用析构函数

class PrivateDestruct {
public:
    PrivateDestruct() { cout << "creat PrivateDestruct" << endl; }
    void Destroy() {
        //~PrivateDestruct();    //error,对应4
        delete this;    //ok,对应5
    }
private:
    ~PrivateDestruct() { cout << "destroy PrivateDestruct" << endl; }
};

int main(){
    //PrivateDestruct pd1;    //error,对应1
    PrivateDestruct* p_pd2 = new PrivateDestruct;    //ok,对应2
    //p_pd2->~PrivateConstruct();    //error,对应3
    //delete p_pd2;    //error,对应3
    p_pd2->Destroy();    //ok,对应5
    return 0;
}

7.总结

不会用不建议瞎用,那些花里胡哨的用法,平时99%的时间都用不到,把常规玩法玩明白,需要的时候翻翻这页资料,找几个demo就行了。

posted on 2023-08-02 23:08  无夜千幕雪  阅读(435)  评论(0编辑  收藏  举报