第二章、构造函数语意学

implicit:没在源程序代码中出现的
explicit:在源程序代码中出现的
trivial:没用的
nontrivial:有用的
memberwise:对每一个member施以
bitwise:对每一个bit施以

编译器可能会修改程序发生在“memberwise initialization”成员变量初始化或者返回值优化“return value optimization”(NRV)上

全局变量的内存保证在程序激活的时候被清0,本地变量(在栈中)和堆对象都不会被清为0,它们是上次内存被使用后的痕迹。

1.默认构造函数的构建
只有在4中情况下才会创建一个默认构造函数,并且被合成的构造函数只能满足编译器的需要。它们之所以能够完成任务是借着
“调用成员对象member object或基类base class的默认构造函数” 或者“为每一个object初始化它虚函数机制或者虚基类机制而完成”
被合成后,只有基类的属性和成员类对象才会被初始化,其他的所有非静态数据成员都不会初始化。
其他不在4种情况下是不会合成构造函数的。
所以两种想法是错的:任何class如果没有定义默认构造函数就会被合成;合成的默认构造函数会设定类的每一个数据成员的默认值。

问题:在C++各个不同的编译模块中,编译器如何避免合成出多个默认构造函数,如为A.C模块合成一个,又为B.C模块合成一个。
答:将合成的默认构造函数default constructor、拷贝构造函数copy constructor、析构函数destructor、拷贝赋值运算assignment copy operator用inline方式完成,
inline函数有静态链接,不会被文件以外的人看到。如果函数太复杂,不适合做成inline,就会合成一个explicit non-inline static实体。

情况一:带有默认构造函数的成员类对象  “带有Defailt Construct”的Member class Object
如果一个类没有任何构造函数,但它内含一个成员对象,该成员对象有默认构造函数,那么编译器就会为该class合成一个默认构造函数,但是这个合成操作只有在构造函数真正被调用时才会发生。
例子:
class Foo
{
public:
Foo();
Foo(int);
};
class Bar
{
public:
Foo foo;
char *str;
};
void foo_bar()
{
Bar bar;//Bar::foo必须在此初始化
}
此时编译器会合成构造函数为:
inline
Bar::Bar()
{//C++伪代码
foo.Foo::Foo();//注意没有初始化str
}

如果是程序员定义了一个默认构造函数:
Bar::Bar(){str=0;}
那么编译器会扩张已存在的构造函数,在其中安排一些代码。
Bar::Bar()
{//C++伪代码
foo.Foo::Foo();//注意没有初始化str
str=0;
}

如果有多个成员对象,则按照他们的声明顺序来调用各个构造函数。

情况二:带有默认构造函数的基类
如果没有任何构造函数的类派生自一个带有默认构造函数的基类时,这个派生类会合成一个默认构造函数,调用上一层基类的默认构造函数。

情况三:带有一个虚函数的类
如果没有任何构造函数的类声明(或者继承)一个virtual function,或者派生自一个继承串链。
例子:
class Widget{
public:
virtual void flip=0;
};
class Bell:public Widget
{
}
class Whistle:public Widget
{
}
void flip(const Widget &widget){widget.flip();}
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
}

那么就会有两个扩充:生成一个虚函数表,在类对象中有个额外的指针成员vptr被合成。
函数foo改变为:
(*widget.vptr[0])(&widget)

情况三:带有一个虚基类的类,也就是虚继承的派生类
例子:
class X{public:int i;};
class A:public virtual X{public:int j;};
class B:public virtual X{public:int d;};
class C:public A,public C{public :int k;};
//无法在编译期间决定出pa->X::i的位置
void foo(const A* pa){pa->i=1024;}
main()
{
foo(new A);
foo(new C);
}

函数foo改变为:
void foo(const A* pa){pa->__vbcX->i=1024;}

2.拷贝构造函数(copy constructor)的构建
有三种情况:
情况一:以一个object的内容作为另一个class object的初值。
class X{}
X x;
X xx=x;//明确以一个object内容作为另一个class object的初值
情况二:object被当作参数交给某个函数时
extern void foo(X x);
void bar()
{X xx;
foo(xx);
}
情况三:当函数传回一个class object时:
X foo_bar()
{X xx; return xx;}

默认对每一个成员初始化Default Memberwise Initialization
如果一个类没有提供一个显示的拷贝构造函数时,当类对象以相同类的另外一个对象作为初值时,内部以Default Memberwise Initialization
手法完成的,也就是把每个内建的或派生的数据成员(如一个指针或数组)的值,从某个对象拷贝一份到另外一个对象中。不过它不会拷贝其中
的member class object ,而是以递归的方式施行Memberwise Initialization。
例如:
class String
{
public:
//没有显示拷贝构造函数
private:
char *str;
int len;
}
如果发生了这样的情况
String noun("book");
String verb=noun;
其完成方式就好像个别设定每一个members一样,就符合了Bitwise Copy Semantics位逐次拷贝的表现:
//语意相等
verb.str=noun.str;
verb.len=noun.len;
但如果是:
class String
{
public:
String(const String&);
private:
char *str;
int len;
}
class word{
public:
//没有显示拷贝构造函数
private:
int _occurs;
String _word;
};
此时要合成一个copy constructor,以便调用member class String object的copy constructor:
inline Word::Word(const Word& wd)
{
str.String::String(wd.str);
cnt=wd.cnt;
}

当不要Bitwise Copy Semantics时,类就需要合成一个拷贝构造函数:
1.当class内含一个member object,而member object声明有一个copy constructor时(无论是被显示声明如后面的String,还是合成的如Word)
2.当class继承一个base class而后者存在有一个copy constructor时(无论是被显示声明如后面的String,还是合成的如Word)
3.当class声明了一个或多个virtual functions时,此时合成的拷贝构造函数会明确object的vptr是指向本身对象的虚表,而不是直接从右边拷贝过去。
4.当class派生自一个继承串链,其中有一个或多个virtual base classes时。问题并不在于一个类对象以同类的对象作为初值,而是以派生类的某个对象作为初值。

3. 程序转化语意学
考虑下面程序:
#include "X.h"
X foo()
{
X xx;
//..
return xx;
}
对于一个高品质的C++编译器中,下面两种假设都是错的。
假设一:每次foo()调用,就传回xx的值。
这个假设视class X如何定义而定。不过一般都是
void foo(X &_result)
{
X xx;
xx.X::X();
_result.X::X();
return;
}
假设二:如果class X定义了一个copy constructor,那么当foo()被调用时,保证该copy constructor也会被调用。
虽然上面看起来好像如此,但是如果优化掉,就可能不保证copy constructor也会被调用。

明确的初始化操作Explicit Initialization:
已知有这样的定义:X x0;
如果有函数
void foo_bar(){
X x1(x0);
X x2=x0;
X x3=X(x0);
}
必要的程序转换有两个阶段:
阶段一:重写定义,初始化会删除
阶段二:class的构造函数调用操作会被插进去
如:
void foo_bar(){
//被重写
X x1;
X x2
X x3;
//安插构造函数调用操作
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
}

参数的初始化(Argument Initialization)
对于这样的调用:
X xx;
foo(xx);
一种方式:此时会要求局部实体x0以memberwise的方式将xx当作初值。如
X __temp0;
__temp0.X::X(xx);
foo(__temp0);
另外方式:以”拷贝构建的方式把实际参数直接构建在应该的位置上“,然后在退出时再destructor

返回值的初始化(Return Value Initialization)
对于下面函数
X bar()
{
X xx;
//处理xx
return xx;
}
首先添加一个额外参数,用来放置被拷贝构建而得的返回值,然后在return之前插一个拷贝构造函数调用操作。
void bar(X &_result)
{
X xx;
xx.X::X();
//处理
__result.X::X(xx);
return;
}
这样子,如果调用为X xx=bar();
则变成
X xx;
bar(xx);
bar().memfunc();//memfunc为X的函数
可能被转换为
X __temp0;
(bar(__temp0),__temp0).memfunc();
同理
X (*pf)();
pf=bar;
则转化为
void (*pf)(X &);
pf=bar;

在使用者层优化(Optimization at the User Level)
例如:X bar(const T &y,const T &z)
{X xx;
//使用y和z处理x
return xx}
那么优化可以为
void bar(X &__result,const Y &y,const T &z)
{
__result.X::X(y,z);
return;
}

在编译器层面优化(optimization at the Compile Level)
相对于返回值的初始化的结构,省去函数中局部变量的初始化
void bar(X &_result)
{
__result.X::X();
//直接处理__result而不用局部变量
return;
}
上面的优化也叫做Named Return Value (NRV)优化。例如
class test{
    friend test foo(double);
public:
    test()
    {memset(array,0,100*sizeof(double));}
    //添加拷贝构造函数是为了NRV
    test(const test &t)
    {memcpy(array,&t,sizeof(test));}
private:
    double array[100];
};
test foo(double val)
{
    test local;
    local.array[0]=val;
    local.array[99]=val;
    return local;
}
void printlocaltime(void)
{
    struct tm *timeptr=new tm();
    time_t secsnow;
    time(&secsnow);
    localtime_s(timeptr,&secsnow);
    printf("the date is %d-%d-20%02d\n",(timeptr->tm_mon)+1,timeptr->tm_mday,timeptr->tm_year);
    printf("the time is %02d:%02d:%02d\n",timeptr->tm_hour,timeptr->tm_min,timeptr->tm_sec);
    delete timeptr;
}

int _tmain(int argc, _TCHAR* argv[])
{
    printlocaltime();
    for(int cnt=0;cnt<10000000;cnt++)
        {test t=foo(double(cnt));}
    printlocaltime();

    return 0;
}
但是测试后感觉没什么变化。。
但是NVR还是不是很赞同,因为可能没有执行,并且不喜欢在程序中给编译器改变

4.成员初始化列表(Member Initialization List)
下面四种情况必须使用成员初始化列表:
当初始化一个引用成员时
当初始化一个常成员变量时
当调用一个基类的构造函数,而他拥有一组参数时
当调用一个成员类的构造函数,而它拥有一组参数时

其中第四种情况,可以在函数体里面赋值,但是效率不高
class Word{
String _name;
int _cnt;
public:
//没有错误,只是太天真。。
Word(){
_name=0;
_cnt=0;
}
};
它会程序扩展为:
Word::Word{
//调用String的默认构造函数
_name.String::String();
//产生暂时对象
String temp=String(0);
//调用拷贝操作符
_name.String::operator=(temp);
//释放暂时对象
temp.String::~String();
_cnt=0;
}
其实较好的作法就是
Word::word:_name(0)
{
_cnt=0;
}
它会扩展为:
Word::Word{
//调用String的默认构造函数
_name.String::String(0);
_cnt=0;
}

成员初始化列表的初始化顺序是按照声明顺序来初始化,并且在函数体执行之前操作完成。

posted @ 2014-05-14 13:05  Tempal  阅读(223)  评论(0编辑  收藏  举报