首页 > 编程语言 > 详细

C++对象模型-构造函数语意学

时间:2020-02-11 21:39:30      阅读:68      评论:0      收藏:0      [点我收藏+]

1.默认构造

由于编译器会尽可能的为所有的警告和错误做出解释。但也因此导致了部分情况下的过度解析。
书中给的例子是编译器由于过度解析,使用了类型转换运算符的解析代码,导致隐藏了真正的错误。

cin << intval;
int temp = cin.operator int();
temp << intval;

分析一下:

  • 程序员的目的是实现读取输入,但是误将 >> 写成了 <<, 而istream并没有重载 << 运算符,编译器一看这条路不通啊,于是只好按照 << 左移位来解析;
  • but,要想实现左移位,又必须将cin转成整型。那么编译器就去找istream有没有类型转换函数好将cin转成整型后再进行移位操作。如果找到了,那么,cin << intval; 正常执行了,并不报错。但不符合程序员的本意。
  • 为了避免这种转换发生,istream中使用了operator void*()来替换operator int()。

要注意由于隐式转换可能造成的不良后果。

(1) 隐式类型转换

隐式类型转换虽然会"暗地里"做一些转换操作,但这种机制的好处也是显而易见的。而且C++为了让这个"暗地里"的隐式操作能够被程序员察觉显形,提供了一个修饰符"explicit",类型转换构造函数和类型转换函数声明前加上explicit关键字将阻止编译器隐式类型转换操作。任何尝试隐式转换的操作都会报错。

如下:

#include<iostream>
using namespace std;
class A
{
public:  
    //explicit
    A(int a):m_a(a)
    {
        cout << "construct A from int" << endl;
    }
    //explicit
    operator int()
    {
        cout <<"convert A to int " << end; 
        return m_a;
    }
public:    
    int m_a;
};
int main(void)
{
    A a(5);//显式类型转换构造
    A b = 5;//隐式类型转换构造
    int i = a;//隐式类型转换函数
    return 0;
}
  • A a(5);总是正常的。
  • 第一个explicit注释掉,A a(5);运行正常;打开该注释,编译提示:类型转换失败
  • 第二个explicit注释掉,int i = a;运行正常。 打开该注释 编译提示:非法的存储类,即赋值失败。

(2) 默认构造函数

讨论trivial和notirvial其实就是讨论构造函数存在的必要性。构造本质上要为对象的生成做一些辅助操作。
但如果对象生成的需求仅仅是分配空间就够了,那么构造函数其实也没有什么意义。
构造函数确实不是必须的,甚至于某些情况下,编译器连系统默认构造函数都不会提供
比如有一个类就像C中结构体的一样,struct A* pa = new (sizeof(struct A));就足够,即不需要初始化成员变量,也不需要负责成员变量的构造,更不需要初始化虚函数表,或者虚基类表。那么编译器也没有必要提供一个默认构造多此一举了。

  • 构造时先构造基类,再构造子类的非基类部分,析构时先析构子类的非基类部分,再析构基类。
  • 初始化表用于指导基类子对象或者成员变量如何初始化,也包括类类型的成员变量。
  • 基类子对象按照继承顺序必须在初始化表中指定构造方式,且必须在子类的非基类成员之前被构造出来
  • 对于类类型成员变量,既可以按照初始化表中指示进行构造,也可以之后在函数体中按照声明顺序依次构造对象成员。
  • 子类构造函数执行时会先按照继承顺序插入调用各基类的构造函数的代码。再调用用户定义的子类的构造部分。
  • 不管子类是否默认构造,其基类子对象或者是子类中的类类型成员即使是默认构造,子类至少要提供默认构造来调用基类的构造函数。除非基类连默认构造都不需要。
  • 基类如果是有参构造,则子类必须显式指定构造函数,因为要指示基类如何构造。提供的默认构造无法做到调用基类有参构造。故而需要自定义构造函数来指导基类构造方式。对于类的类类型成员变量也是如此。

综合来看,作者想要说明的是:在基类或者类成员没有自定义构造函数时默认构造函数的作用,在以下几种条件下会体现即:

  • 有类类型成员变量需要被构造时,需要调用类类型成员的默认构造函数来调用该成员对象的默认构造。
  • 有基类子对象需要被构造时,需要调用子类的默认构造函数来调用基类的默认构造
  • 带有虚函数的类,需要其默认构造来初始化每个对象的vptr,注意:虚函数表和函数指针覆盖是在编译期完成的。
  • 虚继承的类,需要其默认构造来初始化每个对象的虚基类表,注意:虚基类初始化表是在编译期完成的。

看作者总结的四种情况(没有自定义构造函数的情况下):

public class B
{
    B(){}
};
class A
{
    class B b;
};
  • 类类型的成员变量需要调用默认构造,即B没有自定义构造时;
public class B
{
    B(){}
};
class A:public class B
{
};
  • 基类需要调用默认构造,即B没有自定义构造时
class A
{
virtual fun(){}
};
  • 存在虚函数时需要通过默认构造函数来,即A::fun需要动态寻址时
class A : virtual public class B
{
};
  • 存在虚基类时需要通过默认构造函数来,即A中的B子对象需要动态寻址时

2.默认拷贝构造

(1) 默认拷贝构造

类需要执行拷贝构造的三种情形

  • 明确使用 = ;
  • 对象作为实参;
  • 作为返回值,不考虑编译器优化(部分编译器会把临时对象直接作为有名对象返回,从而减少一次拷贝构造)

注意:拷贝构造的本质是还是构造,本质操作是初始化操作,而非拷贝操作。
拷贝构造同样被分成了trivial和notirvial,是trivial还是notrivial和默认构造解释差不多。先理解下几个名词。

bitwise copy是编译器默认提供的位拷贝,即memcpy系列,以bit为单位。加个semantics(语意)引申意义后面讲。
memberwise init即基本类型成员的赋值,以成员为单位。

bitwise copy semantics:位拷贝语意,即一个类的拷贝构造过程中的初始化操作应该是固定且连续的memwise init,不能被安插子对象或者类类型的成员变量的拷贝构造(尽管子对象或者类类型的成员变量内部也是递归mmwise init的),另外也不能由于存在虚函数或者虚基类增加拷贝构造操作而在该类的拷贝构造中额外增加虚表指针或者虚基类表指针的重定位操作。

换句话说:bitwise copy semantics上只能有 POD数据类型C++ POD(Plain Old Data)类型
某些情况下对象不能有bitwise copy semantics,否则拷贝构造会出现问题。即上述的几个不能。

以下四种情况不应该表现出bitwise copy semantics,默认构造必须是notrivial的

  • 含有拷贝构造的基类子对象
  • 类类型成员变量有拷贝构造
  • 类声明有虚函数时
  • 类派生自虚基类时

前两种,基类子对象或者类类型成员的拷贝构造必须被当前对象的拷贝构造调用。默认拷贝构造必须是notrivial。
类存在虚函数表vtbl时或者继承自虚基类时,需要重定虚表指针,默认拷贝构造必须是notirvial的。
也就是默认拷贝构造必须要有才行。

最终要回归trivial和notrivial,不要陷在bitwise copy semanstics里。

(2) 程序转化语意学

C++对象模型-构造函数语意学

原文:https://www.cnblogs.com/kuikuitage/p/12296711.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!