您的位置:首页 > 移动开发 > Objective-C

构造函数语意学(inside the c++ object model)

2015-11-17 19:56 555 查看
1.默认Constructor的构建操作

以下四种情况类会被生成“有用的”默认构造函数(nontrivial default constructor)

a.一个class,带有默认构造函数的成员对象(带有default constructor 的 Member class object)

#include<iostream>
using namespace std;
class Foo
{
public:
int f;
Foo()//如果没有这个无参的构造函数,那么下面 f 的输出将会是0,Bar的构造函数的合成会调用Foo的默认构造函数;但如果没有这个无参的构造函数,且写了一个有参的构造函数,那么下面Bar bar;时,编译器会报错no matching function for call to 'Bar::Bar()'
{
f=6;
}
};
class Bar//含有(<span style="font-family: Arial, Helvetica, sans-serif;">含有构造函数的成员对象</span><span style="font-family: Arial, Helvetica, sans-serif;">)的类</span>
{
public:
Foo foo;//含有构造函数的成员对象
char *str;
};
void foo_bar()
{
Bar bar;//合成Bar的默认constructor的操作,只会在被调用的时候才会发生,也即此处发生该操作
cout<<bar.foo.f<<endl;//6:证明bar.foo已被初始化
//cout<<bar.str<<endl;//error:bar.str未被初始化
}
int main()
{
foo_bar();
}
被合成的Bar default constructor 能够调用class Foo的默认构造函数来处理成员对象Bar.foo,但是它并不产生任何代码来初始化Bar.str。因为Bar.foo初始化是编译器的责任,将Bar.str初始化则是程序员的责任。编译器只做它该做的事情。
另外合成的默认构造函数是以inline的方式完成的,如果函数太复杂则会做成非inline的static实体,上例默认构造函数像这样

inline Bar::Bar()
{
foo.Foo::Foo();
}
如果程序员为Bar.str写了构造函数,但是没有初始化foo那会怎样?答案是编译器会扩张程序员编写的构造函数,将成员对象的构造函数安插到,每一个我们编写的构造函数中,于是上例就变成了这样:
inline Bar::Bar()
{
foo.Foo::Foo();//编译器自动安插的
str=NULL;//程序员编写的
}


b.带有默认构造函数的基类(带有default constructor 的 base class)

和上述情况相似,子类没有构造函数,编译器会默认合成一个调用基类默认(无参)构造函数的子类默认构造函数

c.带有一个虚函数的class(带有一个Virtual Fuction 的 class)

以下两种情况需要合成出default constructor

1)class 声明(或继承)一个virtual fuction

2)class 派生自一个继承串链,其中有一个或多个virtual fuction

d.带有一个virtual base class 的class



有关虚继承的东西暂待

关于默认构造函数的合成两点是需要注意的

1)不是任何没有定义构造函数的class都会被合成出一个默认构造函数

2)编译器合成出来的默认构造函数不会对class内每一个data member都设定默认值

2.copy constructor的构建操作

终于找到两个能把话说明白的,本节参考:http://blog.sina.com.cn/s/blog_756c81be0101cgx0.html;http://blog.csdn.net/zssureqh/article/details/7696231

a.拷贝构造函数被调用的契机

1)按值传递对象(object )

2)按值返回一个对象(object )副本

3)用一个对象(object )对另一个对象(object )初始化

b.概念辨析

1)default memberwise initialization 是与 user defined Initialization 相对应的。是从编译器(计算机)与程序员(用户)的角度出发。

2)bitwise copy (浅拷贝)是与memberwise copy(深拷贝) 相对应的。是两种不同的拷贝方式,编译器通常为了效率会选择bitwise方式拷贝。关于浅拷贝和深拷贝参看复制控制(c++ primer):http://blog.csdn.net/chinajane163/article/details/48344557。

3)那么为什么这两个概念经常会混淆呢?主要原因是二者有部分交集——在类的对象初始化或者赋值(operator=)时,两个概念会同时出现。从对象整体角度出发,默认的对象赋值操作和初始化操作(default assignment and initialization ),编译器会选择memberwise方式(这里不是指memberwise copy,更确切的说应该是:individually assignment
or initialization)操作,即对构成对象中的每一个成员数据分别进行赋值或者初始化。从对象的数据成员角度出发,具体到对象的每一个数据成员的操作,编译器通常采用(可以认为就是)bitwise copy操作,就像memcpy或者memset函数一样,原样将内存中的数据按位复制一份。

c.default memberwise initialization & bitwise copy semantics



如果class没有提供一个显式的拷贝构造函数(explicit copy constructor),当用同类型 object 初始化一个object 时,其内部是按照default memberwise initialization的方式完成的。也就是把每一个内建的或派生的 data member(例如一个数组或指针)的值,从某个object拷贝一份到另一个object上,但不拷贝其具体内容。例如只拷贝指针地址,不拷贝一份新的指针指向的对象,这也就是浅拷贝,不过它并不会拷贝其中member
class object,而是以递归的方式实行memberwise initialization。


没有member class object 的 default memberwise initialization示例如下:

#include<iostream>
using namespace std;

class String
{
public:
char *str;
int len;
String(char *s)
{
strcpy(str,s);
len=strlen(str);
}
};

int main()
{
String noun("book");
String verb=noun;
cout<<verb.str<<"  "<<verb.len<<endl;//book  4
return 0;
}
其内部操作,就是对每一个类对象成员进行复制

verb.str=noun.str;
verb.len=noun.len;


那么memberwise initialization是如何实现的呢?或者说memberwise initialization有哪几种实现方式呢?

答案就是bitwise copy semantics 和 default copy constructor。如果class展现出了bitwise copy semantics ,就会通过bitwise copy (Bitwise copy semantics: raw memory copy. For example, given object a of Class A occupies 10 bytes memory from address 0x0 to 0x9, b = a will copy data
from 0x0 to 0x9 to b's memory space if Bitwise copy semantics applies)实现对象成员(member class object)之间的初始化工作,否则就会通过default copy constructor。

对于某些情况,我们不希望编译器用bitwise copy 把member class object给初始化。那么在哪些情况下,我们不希望class展现Bitwise Copy Semantics,或者说,在哪些情况下,一个class不会展现Bitwise Copy Semantics呢?有以下四种情况。

d.不要Bitwise Copy Semantics!(四种情况)

1)当class 内含有一个member class object,而这个member class object 内有一个copy constructor 时[ 不论是class设计者明确声明,或者被编译器合成 ]

2)当class 继承自 一个base class,而base class 有 copy constructor 时[ 不论是class设计者明确声明,或者被编译器合成 ]

3)当一个类声明了一个或多个virtual 函数时

4)当class派生自一个继承串链,其中一个或者多个virtual base class

下面我们来理解这四种情况为什么不能使用bitwise copy,以及编译器生成的copy constructor都干了些什么。

在前两种情况中,编译器必须将 member 或base class 的“ copy constructors 的调用操作” 安插到被合成的copy constructor中。(就像构造函数中的操作那样)。情况3和4,参考e.重新设定 virtual table 的指针和f.处理virtual base class subobject

e.重新设定 virtual table 的指针

对于情况3,根源我们都知道的,是类对象的之间的赋值造成的,那么你是否只是简单的认为同类对象的直接的复制,如果是父类对象和子类对象之间的初始化会怎么样呢?尤其是当存在虚函数的情况。回忆下,虚函数出现的时候,类的行为变化:

1.是产生虚表(虚函数的地址表,virtual function table ,也称vtbl)

2.是对象里面都出现了一个指向虚表的指针(vptr)

那么当我们用子类的成员给父类成员进行初始化时会发生什么有趣的事情呢?给出下面一个例子

#include<iostream>
using namespace std;

class ZooAnimal
{
public:
virtual ~ZooAnimal(){};
virtual void animate()
{
cout<<"ZooAnimal's animate!"<<endl;
}
virtual void draw()
{
cout<<"ZooAnimal's draw!"<<endl;
}
};

class Bear:public ZooAnimal
{
public:
Bear(){};
void animate()
{
cout<<"Bear's animate!"<<endl;
}
void draw()
{
cout<<"Bear's draw!"<<endl;
}
virtual void dance();
};
当一个Bear class object 以另一个Bear class object(同类型)作为初值时是没有问题的,都可以靠bitwise copy semantics完成(在此排除有pointer的情况)。
int main()
{
Bear yogi;
Bear winnie=yogi;
return 0;
}
yogi 会被 default Bear constructor 初始化,而在 construct 过程中,yogi 的 vptr 指向的是 Class Bear 的 vritual table 。因此把 yogi 的 vptr 值拷给 winnie
的 vptr 是安全的。



但当 ZooAnimal 由其子类初始化时就出问题了



父类子类是两张虚表

f.处理virtual base class subobject

有关虚继承的东西暂待

3.程序转化语意学(program transformation semantics)

在此之前再次复习调用拷贝构造函数的契机:1.用一个object 初始化另一个object,2.按值传递object,3.按值返回一个object副本

a.明确的初始化操作(用一个object初始化另一个object)

已知有这样的定义:

X x0;
有如下例子:
/***显式的以object x0来初始化其他class object***/
void foo_bar()
{
X x1(x0);
X x2=x0;
X x3=X(x0);
}
/***********************************************
*****实际上必要的程序转化有两个阶段
1.重写每一个定义,其中的初始化操作会被剥除
2.class 的 copy constructor调用操作会被安插进去
***********************************************/
void foo_bar()
{
X x1;//定义被改写,初始化操作被剥除
X x2;
X x3;
//编译器安插X copy construction的调用操作
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);//表现出对以下的copy constructor的调用:X::X(const X& xx);
}

b.参数初始化(按值传递object)

已知函数

void foo(X x0);
有下面的调用方式
X xx;
foo(xx);
实际上编译器会先创建一个临时对象,再调用 copy constructor 将xx拷贝到临时对象temp上(像这样 temp.X::X( xx ) )。再将临时对象的地址以bitwise的方式拷贝到x0,像这样(void foo(X &x0) )(我觉得引用会是一个const Type *a)。其中class X声明了一个析构函数在foo()调用完后,对付那个暂时性的object。
c.返回值的初始化(按值返回一个object副本)

按b.参数初始化来看,是产生了一个临时object result,然后把object result的引用作为操作物的。但实际上在函数体内还会再产生一个临时object temp2,整个过程就是对temp2进行操作,然后在return之前将temp2 copy constructor给result。

void bar(X &_result)
{
X temp2;
temp2.X::X();//default constructor

//do someting on object temp2

_result.X::TEMP2(temp2);//copy constructor
return ;
}

d.在使用者层面做优化

一般来说是这么写的:

X bar(const X& p1, const X& p2)
{
X xx;
xx.X::X(p1,p2);//default constructor? OR member fuction?
return xx;
}
//编译器内部的伪码:
void bar(X& _result, const X& p1, const X& p2)
{
X xx;
xx.X::X(p1,p2);//default constructor? OR member fuction?
_result.X::XX(xx);//copy constructor
return;  //什么也不返回
}
如果这么写:
X bar(const X& p1, const X& p2)
{
return X(p1,p2);
}
//编译器内部的伪码:
void bar(X& _result, const X& p1, const X& p2)
{
_result.X::X(p1,p2);//default constructor? OR member fuction?
return;//什么也不返回
}

相比较而言,少了个copy constructor。换句话说,少用局部变量,能直接计算返回的,就直接计算返回,这样可以少产生一个拷贝构造函数。

e.在编译器层面做优化

d.在使用者层面做优化的例子,原先是使用者注意少用局部变量,能直接计算返回的,就直接计算返回,这样可以少产生一个拷贝构造函数。现在这件事情编译器代你做了,换句话说,即使我们仍用了xx做局部变量,保存计算结果,return xx,编译器也会自动把xx用_result替换掉,替你精简掉一个copy constructor。

然后呢,lippman把这种行为叫做“NRV优化”, NRV优化的本质是优化掉拷贝构造函数,当然去掉它的前提是作为使用者的我们用了xx做局部变量、return xx;如果我们没有这么做,而是直接return X(p1,p2) 那么这种行为也就不会发生了。

f.copy constructor 要不要?

写的已经很精简了,自己也没办法再精简了,又懒得把整段整段抄下来了,还是直接截图吧









当然memcpy的使用前提是:在class不含任何编译器产生的内部members。想想如果在class中有一个虚函数会怎么?

4.成员们的初始化队列

a.必须使用初始化列表的情况

1)当初始化一个reference member 时

2)当初始化一个const member 时

3)当调用一个base class 的 constructor,而它拥有一组参数时(没例子,不明了)

4)当调用一个member class 的 constructor,而它拥有一组参数时(没例子,不明了)

class Word
{
private:
String _name;
int _cnt;
public :
Word()
{
_name=0;
_cnt=0;
}
};
/********************constructor的内部扩张*******************
***Word constructor会先产生一个暂时性的string object,
***然后将它初始化,再用assignment将暂时性object指定给_name
***最后在摧毁object
*******************constructor的内部扩张********************/
Word::Word()
{
_name.String::String();//String default constructor
String temp=String(0);//compiler create temporal object
_name.String::operator=(temp);//copy _name by memberwise
temp.String::~String();//destroy temporal object
_cnt=0;
}
/*********constructor改进******较佳的方式*******************/
Word::Word:_name(0)
{
_cnt=0;
}
/*******************constructor的内部扩张*******************/
Word::Word()
{
_name.String::String(0);
_cnt=0;
}
/************template code*************陷阱*****************/
template<class Type>
foo<Type>::foo(Type t)
{
_t=t;//是否是个好的写法,视t的具体类型而定
}
Word::Word():_cnt(0),_name(0){}//这会引导某些程序员积极地坚持所有member 都在初始化列表中初始化,甚至是个int的member
对于初始化列表有些地方需要注意:list中的项目次序是由class中member的声明次序决定的,而不是initialization list的排列次序决定的。

这个问题需要注意,不然会产生以下错误:

/****************************************************
*****出错原因
*****由于声明次序的缘故,实际上先执行的是_name(_cnt)
*****然后才执行的_cnt(val) 导致_name的值无法预知
****************************************************/
class Word
{
private:
int _name;
int _cnt;
public :
Word(int val):_cnt(val),_name(_cnt){}//会出错
};
/*****************建议这么做***********************/
class Word
{
private:
int _name;
int _cnt;
public :
Word(int val):_cnt(val)
{
__name=_cnt;
}
};
初始化列表的代码放在explicit user code 之前,所以上述建议方法不会导致错误。

还有初始化列表虽然支持用本class的member fuction初始化member data,但不建议这么做
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: