zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++中的构造函数与析构函数

C++ 函数 构造函数
2023-09-11 14:19:29 时间

1. 构造函数

每个类都定义了它被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些特殊的函数就叫做构造函数。构造函数的主要作用是初始化对象的非static数据成员。

与其它函数相比,构造函数的特点在于:

1)构造函数的名字与类名相同。

2)类的构造函数也有一个参数列表(可能为空)和一个(可能为空的)函数体。

3)类可以有多个构造函数,和其它函数的重载差不多。

4)构造函数没有返回类型。

5)构造函数不能被声明为const,当创建一个类的const对象时,直到构造函数完成初始化过程,对象才取得const特性,也就是说const对象构造过程生效于构造函数完成初始化之后,构造函数可以向const对象的构造过程中向其写值(因为它还未取得const特性)。

1.1 默认构造函数

如果没有为对象提供初始值,我们就知道它执行了默认初始化过程。类通过一个特殊的构造函数来控制默认初始化过程,这个就是默认构造函数,也就是没有实参的构造函数。

如果没有定义默认构造函数,编译器就会隐式地创建一个默认构造函数,这样由编译器创建的构造函数被称为合成的默认构造函数。

合成的默认构造函数只适用于非常简单的类,一个普通的类必须定义它自己的默认构造函数

1)编译器只有在发现类不存在默认构造函数时才会为类生成一个默认构造函数。

2)一旦定义了其它的构造函数,除非再显式定义默认构造函数,否则将不再有默认构造函数。

3)对于某些类来说,合成的默认构造函数可能执行错误的操作。如果类包含有内置类型或复合类型的成员,只有当这些成员都被赋予了类内的初始值,这个类才能适用于合成的默认构造函数。否则他们的值是未定义的。

4)编译器不能为某些类合成默认的构造函数,如果一个类内包含有其它类类型的成员且这个成员的类型没有默认构造函数,则编译器无法初始化该成员。对于这样的类来说,必须自定义默认构造函数,不能由编译器生成合成的默认构造函数。

如果需要默认的行为,那么可以在参数列表后写上=default来要求编译器生成构造函数。其中=default可以出现在类内部,也可以出现在类的外部,如果出现在内部,则默认构造函数是内联的,如果出现在外部,则默认不是内联的。

class Foo
{
    public:
        Foo()= default; //默认构造函数
        Foo(const std::string &s); //带参数的构造函数
};

1.2 拷贝构造函数

如果一个构造函数的第一个参数是它自身类类型的引用,且任何额外参数都有默认值,则次构造函数是拷贝构造函数。

class Foo
{
    public:
        Foo(); //默认构造函数
        Foo(const Foo&); //拷贝构造函数
};

虽然可以定义一个非const引用的拷贝构造函数,但此参数几乎总数一个const的引用,拷贝构造函数在几种情况下都会被隐式的使用,通常不应该是explicit。

拷贝构造函数被用来初始化非引用类类型参数。如果参数不是引用类型,则调用永远不会成功——为了调用拷贝构造函数,必须拷贝它的实参,而为了拷贝实参,又必须调用拷贝构造函数,永无止境,如果是传入引用的话,就不存在有对实参的拷贝。

1.3 赋值构造函数(拷贝赋值运算符)

一般称呼赋值构造函数较少,实际上指的是重载赋值运算符或者合成拷贝赋值运算符。

1)重载赋值运算符

重载运算符本质上是函数,其名字由operator关键字后接一个要定义的运算符的符号组成,赋值运算符就是一个名为operator=的函数[operator= 看成是一体],也有返回类型和参数列表。

赋值运算符通常应该返回一个指向左侧于运算对象的引用。

class Foo
{
    public:
        Foo & operator=(const Foo&); //赋值运算符
        // ...
};

2)合成拷贝赋值运算符

合成拷贝赋值运算符返回一个指向左侧于运算对象的引用。

//合成拷贝赋值运算符
Sales_data &
Sales_data ::operator=(const Sales_data &rs)
{
    booNo = rs.booNo;   //调用 string::operator=
    units_sold= rs.units_sold; //内置的int赋值
    revenue = rs.revenue; //内置的double赋值
    return *this;
}

2.析构函数

析构函数执行与构造函数相反的操作,析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数是类的一个成员,它不接受任何参数,因此不能重载,对于一个类只有唯一的析构函数。

class Foo
{
    public:
        
        ~Foo() {  std::cout << "Foo Destructor" <<std::endl;}
        
};

在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序执行初始化。与之相比,析构函数的特点如下:

1.析构函数释放对象在生存期分配的所有资源

2.在析构函数中,首先执行函数体,然后销毁成员,成员按照初始化的逆序销毁

3.析构部分是隐式的,销毁类类型的成员需要调用成员自己的析构函数,

4.内置类型没有析构函数,不存在调用析构的说法

5.与普通指针不同,智能指针是类类型,具有析构函数,与普通指针不同,智能指针在析构阶段被自动销毁

6.隐式销毁一个内置指针类型的成员不会delete它所指的对象:

只有这个指针是通过new构造的,才需要delete,而实际上

  1. 它可以指向一个栈上的地址
  2. 它可以指向别的类的成员
  3. 它可以与别的new构造的指针有alias
  4. 它可以是由new []或者malloc构造的

何时使用析构函数:无论何时一个对象被销毁,就会自动调用析构函数。

1.变量在离开作用域时被销毁

2.当一个对象被销毁时,其成员被销毁

3.容器(无论是标准容器还是数组)被销毁时,其元素被销毁

4.对于动态分配的对象(new ),当对指向它的指针应用delete运算符时,被销毁

5.对于临时对象,当创建它的完整表达式结束时被销毁。

3. 虚函数

所谓虚函数就是指在相应函数名称前面加上virtual,即标记为虚函数。

3.1 一般虚函数

Base类中printRes函数添加virtual 后就是一般虚函数(非虚析构函数),在派生类中有同名的函数printRes会override基类的函数。调用派生类对象derived 的printRes函数表明,基类相应的函数被override。

#include <iostream>
#include <string>

class Base
{
    public:
        Base() { std::cout << "Base Constructor" <<std::endl;}
        virtual ~Base() {  std::cout << "Base Destructor" <<std::endl;}
        virtual void printRes() 
        { std::cout << "print Base " <<std::endl;}

};

class Derived : public Base
{
    public:
        Derived() {  m_array = new int[5]; std::cout << "Derived Constructor" <<std::endl;}
        ~Derived() { delete [] m_array; std::cout << "Derived Destructor" <<std::endl;}
    
        void printRes() 
        { std::cout << "print derived" <<std::endl;}

    private:
        int* m_array;
    
};

int main() 
{

  Base* base  = new Base();
  base ->printRes(); // 调用基类的printRes函数
  delete base;
  std::cout << "--------------------------------------- \n";

  Derived* derived  = new Derived();
  derived ->printRes();  // 调用派生类的printRes函数,override基类的函数
  delete derived;
  std::cout << "--------------------------------------- \n";
  
  Base* poly = new Derived(); //定义一个多态类型
  delete poly;
  return 0;
}

Base Constructor
print Base [调用基类对应函数打印结果]
Base Destructor
---------------------------------------
Base Constructor
Derived Constructor
print derived  [调用派生类对应函数打印结果]
Derived Destructor
Base Destructor
---------------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor

3.1 虚析构函数

对于一般的虚函数,添加virtual 后,在派生类中会override基类的函数,但是对于虚析构函数则是添加一个析构函数,而不是一般的override。

如下代码所示,使用派生类对象构造一个多态类型,如果不对原基类的析构函数标记为virtual,那么C++的编译器不知道它的派生类还有一个析构函数,delete操作时,只会调用Base的析构函数。

在Derived类中stack上创建的指针变量m_array就不会被析构,因为派生类的析构函数没有被调用。

#include <iostream>
#include <string>

class Base
{
    public:
        Base() { std::cout << "Base Constructor" <<std::endl;}
         ~Base() {  std::cout << "Base Destructor" <<std::endl;}

};

class Derived : public Base
{
    public:
        Derived() {  m_array = new int[5]; std::cout << "Derived Constructor" <<std::endl;}
        ~Derived() { delete [] m_array; std::cout << "Derived Destructor" <<std::endl;}

    private:
        int* m_array;
    
};

int main() 
{
  Base* base  = new Base();
  delete base;
  std::cout << "--------------------------------------- \n";
  Derived* derived  = new Derived();
  delete derived;
  std::cout << "--------------------------------------- \n";
  Base* poly = new Derived(); //定义一个多态类型
  delete poly;
  return 0;
}

Base Constructor
Base Destructor
---------------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
---------------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor

如果是将Base类的析构函数标记为virtual,那么相当于提前告知这个类可能被扩展出子类,相应的还有子类的析构函数需要被调用。

#include <iostream>
#include <string>

class Base
{
    public:
        Base() { std::cout << "Base Constructor" <<std::endl;}
        virtual ~Base() {  std::cout << "Base Destructor" <<std::endl;}

};

class Derived : public Base
{
    public:
        Derived() {  m_array = new int[5]; std::cout << "Derived Constructor" <<std::endl;}
        ~Derived() { delete [] m_array; std::cout << "Derived Destructor" <<std::endl;}

    private:
        int* m_array;
    
};

int main() 
{
  Base* base  = new Base();
  delete base;
  std::cout << "--------------------------------------- \n";
  Derived* derived  = new Derived();
  delete derived;
  std::cout << "--------------------------------------- \n";
  Base* poly = new Derived(); //定义一个多态类型
  delete poly;
  return 0;
}

Base Constructor
Base Destructor
---------------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
---------------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor

3.3 纯虚函数

在函数体的声明语句中书写=0将一个函数声明为纯虚函数,它是没有实际意义的,无需定义。

可以为纯虚函数提供定义,但函数体只能在类的外部,不能在类的内部为一个=0的函数(纯虚函数)提供函数体。

含有纯虚函数的类,是抽象基类,抽象基类只负责定义类的接口,而后续其它的类可以覆盖该接口,不能直接创建一个抽象基类的对象,就是只能用于创建派生类对象。

#include <iostream>
#include <string>

class Base
{
    public:
        Base() { std::cout << "Base Constructor" <<std::endl;}
        virtual ~Base() {  std::cout << "Base Destructor" <<std::endl;}
        void printRes() = 0; //=0表示讲一个虚函数声明为纯虚函数,它只有声明,无需定义
};

class Derived : public Base
{
    public:
        Derived() {  m_array = new int[5]; std::cout << "Derived Constructor" <<std::endl;}
        ~Derived() { delete [] m_array; std::cout << "Derived Destructor" <<std::endl;}

    private:
        int* m_array;
    
};

int main() 
{

  Derived* derived  = new Derived();
  delete derived;
  std::cout << "--------------------------------------- \n";
}

4.其它说明

4.1委托构造

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

#include <iostream>
class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() { // 委托 Base() 构造函数
        value2 = value;
    }
};

int main() {
    Base b(2);
    std::cout << b.value1 << std::endl;
    std::cout << b.value2 << std::endl;
}

4.2 继承构造

在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 using 引入了继承构造函数的概念:

#include <iostream>
class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() { // 委托 Base() 构造函数
        value2 = value;
    }
};
class Subclass : public Base {
public:
    using Base::Base; // 继承构造
};
int main() {
    Subclass s(3);
    std::cout << s.value1 << std::endl;
    std::cout << s.value2 << std::endl;
}

4.3 显式虚函数重载

在传统 C++ 中,经常容易发生意外重载虚函数的事情。例如:

struct Base {
    virtual void foo();
};
struct SubClass: Base {
    void foo();
};

SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成为了一个普通的类方法,这将造成灾难性的后果。

C++11 引入了 override 和 final 这两个关键字来防止上述情形的发生。

override

当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译:

struct Base {
    virtual void foo(int);
};
struct SubClass: Base {
    virtual void foo(int) override; // 合法
    virtual void foo(float) override; // 非法, 父类没有此虚函数
};

final

final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。

struct Base {
    virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法

struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final

struct SubClass3: Base {
    void foo(); // 非法, foo 已 final
};

4.4显式禁用默认函数

传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。 另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。 当程序员有需要时,可以重载这部分函数。

这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。

并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。

C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。 例如:

class Magic {
    public:
    Magic() = default; // 显式声明使用编译器生成的构造
    Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
    Magic(int magic_number);
}