C++对象模型:constructor

构造函数constructor

explicit的引入,是为了能够制止“单一参数的constructor”被当作一个conversion运算符

带有默认构造函数的对象成员

若一个类中包含对象成员,且该对象有默认构造函数,此时:

  • 若该类没有构造函数
    则编译器会合成一个默认构造函数,且发生在真正调用时
  • 若该类有构造函数,但没有初始化对象成员
    则在已有构造函数中按声明顺序生成构造对象成员的代码,且发生在构造最开始的时候

被合成的默认构造函数,只满足编译器的需要,而不是程序的需要
如初始化类对象成员是编译器的需要,初始化其他类成员变量则是程序的需要

编译器是如何避免合成多个默认构造函数的呢?如在不同文件中构造同一个类
编译器会将合成的默认构造函数,拷贝构造函数,析构函数,赋值运算符重载以inline的方式完成,inline函数有静态链接static linkage,不会被文件以外者看到,若函数太复杂,则会合成出一个explicit non-inline static实例

带有默认构造函数的父类

  • 若子类没有构造函数,则会合成默认构造函数调用父类构造函数
  • 若子类已有构造函数,则会生成调用父类构造函数的代码,且发生在初始化对象成员之前

带有虚函数的类

还有两种情况也需要合成默认构造函数

  • 类中声明了虚函数
  • 类的继承链中,包含虚继承关系
class Widget{
public:
  virtual void flip()=0;
};

void flip(const Widget& w){ w.flip(); }

会生成虚函数表vtbl和其指针vptr

w.flip()的虚调用操作会被改写,以满足多态

(*w.vptr[1])(&w); // &w 为当前对象的this指针

在初始化时,编译器会给每个对象的vptr设定初值,放置适当的vtbl地址
此时会合成,或在已有构造函数中生成相关初始化代码

带有虚基类的类

class X{ public: int i; };
class A:public virtual X{ public: int j; };
class B:public virtual X{ public: int n; };
class C:public A,public B{ public: int m; };

void foo(const A* pa){ pa->i=1024; }

编译器无法知道X::i的实际偏移量(经由pa存取),因为pa的真正类型是可改变的

编译器必须改变“执行存取操作”的代码,使X::i可以延迟至执行期决定
cfront的做法是在子类对象中安插虚基类指针
pa->_vbcX->i=1024;,其中_vbcX是编译器产生的指针,指向虚基类X

此时会合成,或在已有构造函数中生成相关代码,来初始化该指针

为什么虚函数和虚基类只有在运行时才能确定
因为编译器不解析赋值操作

拷贝构造函数copy constructor

如下场景,会调用类的拷贝构造函数

对象的显式初始化操作

X xx=x;

对象作为参数传递给函数
extern void foo(X x);
foo(xx);

对象作为函数返回值
X foo_bar(){
X xx;
return xx;
}

default memberwise initialization

若一个类没有提供显式的拷贝构造函数,则在拷贝构造时,内部以default memberwise initialization方式完成
即拷贝原生类型的成员变量,但对于成员对象,会采用递归的方式施行memberwise initialization
(在同类对象间拷贝构造时)

决定一个类是否生成默认的拷贝构造函数,在于该类是否展现出bitwise copy semantics
(不展现才会生成)

位逐次拷贝bitwise copy semantics

以下声明展现了bitwise copy semantics

class Word{
public:
  Word(const char*);
  ~Word(){ delete[] str; }
private:
  int cnt;
  char* str;
};

这种情况下,不需要合成一个default copy constructor,因为上述声明展现了default copy semantics
而对象的拷贝初始化操作也就不需要以一个函数调用收场

什么时候一个类不展现bitwise copy semantics呢?
1.成员变量中包含成员对象,且对象的类型有拷贝构造函数(包括编译器生成)
2.继承自父类,且父类有拷贝构造函数(包括编译器生成)
3.类中声明了虚函数
4.类的继承链中,包含虚继承关系

前两种情况,会在编译器默认生成的拷贝构造函数中,调用相关成员对象或父类的拷贝构造函数
第三种情况,以子类对象初始化父类对象时,需要保证vptr的操作安全,此时生成的父类的拷贝构造函数会设定vptr的值,而不是直接从子类中拷贝,这也解释了上一章代码

ZooAnimal za= b;
za.rotate();

调用的是ZooAnimal::rotate()

第四种情况

class Raccoon:public virtual ZooAnimal{
public:
  Raccoon(){}
  Raccoon(int val){}
private:
};

class RedPanda:public Raccoon{
public:
  RedPanda(){}
  RedPanda(int val){}
private:
};

若以一个Raccoon object作为另一个Raccoon object的初值,则bitwise copy绰绰有余
若以RedPanda object作为Raccoon object的初值,编译器则需要生成拷贝构造函数,并初始化virtual base class pointer/offset

下面这种情况,编译器无法知道bitwise copy semantics是否还保持,因为无法知道Raccoon指针是指向Raccoon object还是derived class object

Raccoon *ptr;
Raccoon little_critter= *ptr;

程序转化program transformation

显式初始化

T t1(t0);
T t2= t0;
T t1= T(t0);

会转化成:先声明,再拷贝构造

T t1;
T t2;
T t3;

t1.T::T(t0);
t2.T::T(t0);
t3.T::T(t0);

参数初始化
将一个class object当作函数实参,或函数返回值
参数传递时,会以memberwise方式进行
在编译器实现计算上,有以下两种转化策略
策略一
引入临时性对象,并用拷贝构造函数初始化,再以bitwise方式传递给形参
函数形参也必须被转化,需要以引用方式声明
策略二
将实参对象实际拷贝构造在函数堆栈中

返回值初始化
cfront中采用双阶段转化

  1. 声明一个class object的引用__result
  2. 在return前,使用返回值来拷贝构造传入的引用

对于函数指针

X (*pf)();
pf= bar;

转化为

void (*pf)(X&);
pf= bar;

在使用者层面优化
定义一个构造函数constructor,可以直接计算返回值,而不是调用拷贝构造函数

在编译器层面优化
将返回值直接使用__result代替,称为Named Return Value(NRV)优化
NRV优化现在被认为是C++编译器义不容辞的优化操作
如下代码

class Test{
  friend Test foo(double);
public:
  Test(){
    memset(arr, 0, 100*sizeof(double));
  }
private:
  double arr[100];
};

此时,编译器不会做NRV优化,因为没有拷贝构造函数,如下加上inline copy constructor

inline Test(const Test& t){
  memcpy(this, &t, sizeof(test));
}

是否需要拷贝构造函数

没有任何理由要提供一个拷贝构造函数,因为编译器自动实施了最好的行为

若一个class要大量memberwise初始化操作,则提供一个copy constructor的explicit inline函数实例是合理的(在编译器提供NRV的前提下)

若使用更有效率的memset()memcpy()作为拷贝构造函数的实现,则需要在class中不含任何由编译器产生的内部成员

成员的初始化

必须使用成员初始化列表的情况
1.初始化引用reference成员
2.初始化const成员
3.调用基类构造函数,且其有一组参数
4.调用成员的构造函数,且其有一组参数

class Word{
  String name_;
  int cnt_;
public:
  Word(){
    name_= 0;
    cnt_= 0;
  }
};

编译器会生成一个临时对象,可能的转化如下

public:
  Word(){
    // 默认构造
    name_.String::String();

    // 临时对象
    String temp= String(0);
    // memberwise拷贝
    name_.String::operator=(temp);
    temp.String::~String();

    cnt_= 0;
  }

若使用列表初始化

Word::Word():name_(0){
  cnt_= 0;
}

编译器可能的转化如下

Word::Word() {
  // 直接调用构造函数
  name_.String::String(0);
  cnt_= 0;
}

成员初始化列表到底做了什么,是不是简单的函数调用
可以回答,当然不是简单的函数调用

编译器会在构造函数中生成代码,将初始化列表中的变量按在类中的声明顺序初始化
如下代码

class X{
  int i;
  int j;
  X(int val):j(val),i(j){}
};

由于声明顺序的缘故,i会比j先执行,会导致问题,这种情况,GNU C++编译器g++会做出告警
建议做出如下调整

class X{
  int i;
  int j;
  X(int val):j(val){
    i= j;
  }
};

能否调用成员函数,以初始化数据成员

X::X(int val):i(xfoo(val)),j(val){}

成员函数的使用是合法的,因为此时this指针已经被构造
编译器可能的生成代码如下

X::X(int val){
  i= this->xfoo(val);
  j= val;
}

能否使用子类成员函数返回值,作为父类构造函数的实参

class FooBar:public X{
  int fval_;
public:
  int fval(){ return fval_; }
  FooBar(int val):fval_(val),X(fval()){}
};

编译器可能的生成代码如下

FooBar::FooBar(int val){
  X::X(this, this->fval());
  fval_= val
}

可知,这不是一个好主意

posted @ 2024-11-01 20:11  sgqmax  阅读(7)  评论(0编辑  收藏  举报