C++常见问题总结_构造函数与析构函数

构造函数与析构函数

  1. 构造函数

    每个类都分别定义了他的对象被初始化的方式, 类通过一个或几个特殊的成员函数来控制其对象的初始化过程。 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建, 就会执行构造函数。
    构造函数的名字和类名相同。和其他函数不同的是,构造函数没有返回类型;类可以包含多个构造函数,和其他重载函数差不多,不同构造函数之间必须在参数数量或参数类型上有所差别。
    构造函数的名字和类名相同,和其他重载函数差不多,不同的构造函数必须在参数数量或参数类型上有所区别。
    不同于其他的成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。

    合成的默认构造函数
    我们的类没有显示定义构造函数, 那么编译器就会为我们隐式的定义一个默认构造函数,初始化规则:1、如果存在类内的初始值,用它来初始化成员; 2、否则,默认初始化该成员。

    某些类不能依赖于合成的默认构造函数
    1、只有当类没有声明任何构造函数时,编译器才会自动的生成默认构造函数。
    2、如果类包含有内置类型或者符合类型的成员,则只有当这些成员全部都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
    (如果定义在块中的内置类型或复合类型(数组或指针)的对象被默认初始化,则他们的值将是未定义的。)
    3、有的时候编译器不能为某些类合成默认的构造函数。例如:如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
    在 c++11 新标准中, 如果我们需要默认的行为, 那么可以通过在参数列表后面写上=default 来要求编译器生成构造函数。

    Sales_data()=default;

    默认构造函数的作用
    当对象被默认初始化或值初始化时自动执行默认构造函数
    默认初始化:
    1、当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
    2、当一个类本身含有类类型的成员且使用合成的默认构造函数时
    3、当类类型的成员没有在构造函数初始值列表中显示的初始化时。
    值初始化:
    1、在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
    2、当我们不使用初始值定义一个局部静态变量时。
    3、当我们通过书写形如 T()的表达式显示的请求值初始化时,其中 T 是类型名。
    类必须包含一个默认构造函数以便在上述情况使用。

class nodefault{
public:
    nodefault(string &s);
};
struct A{
    nodefault my_mem;
};
A a;//错误,不能为A和成构造函数
struct B{
    B(){}       //错误,b_member没有初始值
    nodefault b_member;
}
**构造函数初始值列表**
```
Sales_data(const string&s,unsigned n,double p):Bookno(s),units_sold(n),reve(p*n){}
```
构造函数初始值列表负责为新创建的对象的一个或几个数据成员赋初值。当某个数据成员背构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。构造函数不应该轻易覆盖掉类内的初始值, 除非新赋的值与原值不同。如果你不能使用类内初始值, 则所有的构造函数都应该显示的初始化每个内置类型。
当我们定义变量时习惯于立即对其进行初始化,而非先定义、再赋值:
string foo="hello world";//定义并初始化
string bar;//默认初始化成空的string
bar="hello world";//为bar赋一个新值

就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化

Sales_data::Sales_data(const string &s,unsigned cnt,double price)
{
    bookno=s;
    units_sold=cnt;
    revenue=cnt*price;
}

此版本与原来的区别是这个版本对数据成员执行了赋值操作,原来的版本初始化了他的数据成员。
构造函数的初始值有时必不可少:有时候我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。如果成员是 const 或者是引用的话必须将其初始化。类似的,当成员属于某种类类型且该类型没有定义默认构造函数时,也必须将这个成员初始化。

class constref{
    public:
        constref(int ii);
    private:
        int i;
        const int ci;
        int &ri;
};
constref::constref(int ii)
{
    i=ii;
    ci=ii;  //错误:不能给const赋值
    ri=ii;  //错误ri 没有被初始化
}
//随着构造函数体一开始执行,初始化就完成了

在很多类中,初始化和赋值的区别事关底层的效率问题:前者直接初始化数据成员,后者则先初始化再赋值,并且一些数据成员必须被初始化。
成员初始化的顺序
构造函数初始值列表只用于说明初始化成员的值,而不限定初始化的具体执行顺序。
成员初始化顺序与他们在类中定义的出现顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

class X{
    int i;
    int j;
public:
    //错误,i在j之前初始化,未定义的。
    X(int val):j(val),i(j){}
}

默认实参和构造函数
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

委托构造函数
C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程, 或者说是它把它自己的一些(或者全部职责)职责委托给了其他构造函数。
一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外的一个构造函数匹配。

class sales_data{
public:
//非委托构造函数使用对应的实参初始化成员
sales_data(string s,unsigned cnt,double price):bookno(s),units_sold(cnt),reven(cnt*price){}
//其余构造函数全都委托给另一个构造函数
sales_data(): sales_data("",0,0){}//定义默认构造函数
sales_data(string s):sales_data(s,0,0){}
sales_data(istream &is):sales_data(){read(is,*this)}//先委托给默认构造函数,默认构造函数委托给三参数构造函数
};

当一个构造函数委托给另外一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。然后控制权才会交还给委托者的构造函数

隐式的类类型转换(类型转换构造函数)
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数。
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

class Sales_data{
public:
    Sales_data()=default;
    Sales_data(string s,unsigned cnt,double price):bookno(s),units_sold(cnt),reven(cnt*price){}
    Sales_data(string s):bookno(s){}
    Sales_data(istream &is):{read(is,*this)}
};
string null_back="9-999-99";
item.combine(null_book);//在需要Sales_data的地方可以使用string或istream代替
  • 只允许一步类型转换:
    编译器只会自动的执行一步类类型转换。如:
Item.combine("9-999-99”);//错误, 先把 9-999-99 转换成 string 再将这个 string 转换成sales_data。
Item.combine(string("9-999-99”));//正确, 先显示转换成 string, 在隐式转换成 sales_data.
Item.combine(sales_data("9-999-99”));//正确, 隐式转换成 string, 再显示转换成 sales_data。
  • 抑制构造函数定义的隐式转换
    在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit 加以阻止。关键字explicit 只对一个实参的构造函数有效。只能在类内声明构造函数时使用 explicit 关键字,在类外部定义时不能重复。
class Sales_data{
public:
    Sales_data()=default;
    sales_data(string s,unsigned cnt,double price):bookno(s),units_sold(cnt),reven(cnt*price){}
    explicit Sales_data(string s):bookno(s){}
    explicit Sales_data(istream &is):{read(is,*this)}
};
  • explicit构造函数只能用于直接初始化
    发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)。此时我们只能使用直接初始化而不能使用explicit构造函数:
Sales_data itm1(null_book);//正确,直接初始化
Sales_data itm2=null_book;//错误,不能将 explicit 函数用于拷贝形式的初始化过程。
  • 为转换显示地使用构造函数
    尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示地强制进行转换:
Item.combine(sales_data(null_book));//正确:
Item.combine(static_cast<sales_data>(cin));//正确, 执行了显示的转化

析构函数
释放对象使用的资源,并销毁对象的非static数据成员。无参数、无返回值。

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

析构函数无参数所以也不可以被重载,对于一个给定的类,只会有唯一一个析构函数。

  • 析构函数工作
    如同构造函数有一个初始化部分和一个函数体, 析构函数有一个函数体(通常释放在生存期分配的资源)和一个析构部分。在一个构造函数中,成员的初始化是在函数体执行之前完成的,在一个析构函数中,首先执行函数体,然后销毁成员。成员按照初始化顺序的逆序销毁。
    在一个析构函数中,不存在类似构造函数中初始化列表的东西控制成员如何销毁,析构部分是隐士的。销毁类类型的成员需要执行自己的析构函数。内置类型没有析构函数,销毁内置类型什么也不做。
    NOTE:隐式销毁一个内置指针类型的成员,不会delete它所指向的对象,与普通指针不同,智能指针是类类型,所以具有析构函数。因此,与普通指针不同,智能指针成员在析构阶段会被销毁。

  • 析构函数调用时机
    无论何时一个对象被销毁,就会自动调用其析构函数。
    1、变量离开作用域是被销毁
    2、当一个对象被销毁时,其成员被销毁。
    3、容器被销毁时,元素被销毁
    4、对于动态分配的对象,应用 delete 时被销毁
    5、临时对象,创建他的完整表达式结束时被销毁
    (当指向一个对象的引用或指针离开作用域时,析构函数不会执行)

{//新作用域
    p和p2指向动态分配的内存
    Sales_data*p=new Sales_data;//p是一个内置指针
    auto p2=make_shared<Sales_data>();//p2是一个shared_ptr
    Sales_data item(*p);//拷贝构造函数将*p拷贝到item中
    vector<Sales_data> vec;//局部对象
    vec.push_back(*p2;)//拷贝p2指向的对象
    delete p;//对p指向的对象执行析构函数
}
//退出局部作用域;对item、p2和vec调用析构函数
//销毁p2会递减其引用计数;如果计数变为0,对象被释放
//销毁vec会销毁其元素
  • 合成的析构函数
    当一个类未定义自己的析构函数时,编译器会为他定义一个合成析构函数。对于某些类,合成的析构函数被用来阻止该类型对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。
class Sales_data{
public:
    ~sales_data(){}
};
//空的函数体执行完毕之后,成员会被自动销毁。

析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的

猜你喜欢

转载自blog.csdn.net/xc13212777631/article/details/80404208
今日推荐