5/22/09 c++
类的实做
1.C++中类的构造 (还是为那些细节而折腾。)
我们主要关注的是各个成员函数。
C++中有4个特殊的成员函数:(默认)构造函数,析构函数,(默认)拷贝构造函数,(默认)拷贝赋值函数。
主要的是编译器会为我们合成它们么?
(默认)构造函数
什么是默认的构造函数?
最初的记忆是如果类中没有任何的构造函数,那么编译器就会帮你合成默认构造函数。
按照(inside the c++)的说法,这个属于新手常见的误解。(不断补充,改正认识)
书载,如果类中没有任何用户定义的构造函数,编译器会有个trivial的默认构造函数产生,它什么也不会做。但是不会合成出来。(具体什么意思?myth)接着说只有下面的几种情况下,才有一个non-trivial的默认构造函数被合成,并且会在需要的时候,既是调用的时候合成。(这点貌似体现了C++的设计理念)合成的事是处于编译器的需要,而不是处于程序员的需要,且合成的默认构造函数只会做编译器需要做的初始化,其余的不会做。另外,如果类中有了用户定义的构造函数,那么默认的构造函数也不会被合成(?是吗?如果类中有用户定义的默认构造函数,那么默认的构造函数编译器肯定不会合成的,如果用户定义的不是默认构造函数呢?会合成默认的么?),编译器需要做的初始化将会通过扩展已有的构造函数来达到。在【1】书中说到:"如果class A内含一个或多个以上的对象成员,那么class A的每一个构造函数必须调用每一个对象成员的默认构造函数"。编译器会扩展以存在的构造函数,在其中安插一些代码,使得用户代码在被执行之前,先调用必要的默认构造函数。而在2书中也有类似的论述:默认情况下,在构造函数的函数体被执行前,对象中的所有成员都已经被他们的默认构造函数所初始化了,那些没有构造函数的成员则将拥有一个未定义的初始值。
a)类中含有对象成员,且这个对象成员的类含有默认的构造函数。这时编译器将会合成个non-trivial的默认构造函数,它的主要作用就是调用对象成员类的默认构造函数来初始化类中的对象成员。除此之外,它应该不会做其他的初始化。如果这个类中有多个这样的对象成员,那么编译器将会按照对象成员的声明顺序依次来调用默认的构造函数进行初始化,这么些代码将被安插在用户代码之前。(这句话对后面的有些问题产生了影响,也深刻的反映了c++数据在被使用之前初始化的准则)
b)类继承的基类中含有默认的构造函数。被合成的默认构造函数将会调用基类的默认构造函数。
c)和virtual有关。类中有虚函数或者类继承了虚函数或者是虚继承。因为虚函数,vtable的存在都是编译器设置的,用户全然不知,故而vptr的初始化全是编译器的责任。当类和虚有关的时候,编译器要合成默认的构造函数来正确的初始化vptr。
上面的这一段基本上是来自于书【1】c++对象模型。
补充个细节:初始化和赋值
初始化的意思就是对象第一次出现,给它初始化。赋值的意思是将对象的旧值用新值取代。
对象的初始化是调用构造函数或拷贝构造函数来完成的,赋值是调用赋值运算符来做的。
赋值的旧值如果占有资源的话,可能需要在赋予新值之前释放掉旧的资源。这些东西也决定了拷贝构造函数和拷贝赋值在实现上有些不同。这个主题在efficient c++和c++编程惯用法上都有介绍。
(默认)拷贝构造函数
用的情况有三种:一是直接的将一个对象用另一个同类的对象来做初始化。二是函数参数的传递,三是函数值的返回。如果没有定义拷贝构造函数,那么编译器将会合成一个默认的拷贝构造函数(真伪?)。inside the c++ object model还提到不要位拷贝语义的4种情况,就是和编译器合成默认的构造函数一样的条件。
a)类中含有对象成员,且这个对象成员的类含有拷贝构造函数(无论是通过声明还是合成的),
后面的几个都是一样的,就不重复了。另外,合成的拷贝构造函数和合成的默认构造函数有点不同,前者会复制所有的数据成员,而后者只会初始化编译器需要的。
这个默认的函数(默认合成的也是trivial的)做的拷贝是按成员拷贝(并且可以递归),每个成员是位拷贝。位拷贝我的理解就是直接拷贝内存(位)上的值,所谓的浅拷贝了。如果对于简单的数据类型,这些默认的就够了。也不会出什么问题。如果对于指针呢?如果类中含有指针这种动态内存分配的数据,就得定义自己的拷贝构造函数和拷贝赋值运算符了(ec,item 11)。因为有指针出现,如果按照默认的拷贝构造的语义,就是浅拷贝,直接拷贝的内存,也就是将对象中指针的值赋予了另一个对象的指针数据,这样就导致两个不同对象的指针数据成员指向了同一个地址空间,这个结果未定义。(如果有个对象析构了,释放了内存,那么另一个对象将指向一个被释放的空间)另外如果类中有静态的数据成员,那么也有可能要定义自己的拷贝构造函数用来对静态数据进行处理(c++编程惯用法).自己提个问题什么时候需要定义自己的默认构造函数?都没有提到过。
拷贝构造函数也是构造函数,很简单的,它们都是用来初始化数据成员的。在(拷贝)构造函数中,多用成员初始化列表(Member init list),少用赋值(ec,item 12)。对于类中的数据都是普通的类型,比如整型之类用初始化和赋值都是等价的,怎么方便怎么用。如果类中有对象成员,并且这个对象成员的类还有默认的构造函数,你对对象成员使用赋值你想会有什么情况发生?一个类有对象成员,并且还有默认的构造函数这个条件达到了编译器合成一个non-trivial的默认构造函数的要求。很明显,这里已经有了构造函数,故而不会合成默认构造函数(不确定),只会在现有的构造函数的用户代码前面安插上一些扩展代码,用来调用对象成员的默认构造函数来初始化对象成员。这个能很好的解释在构造函数的用户代码执行之前,有对象成员就被默认的构造函数给初始化了。(以前我对于这点并不知道原因,今天算了解透了。仔细想想这个理由也不是很充分,因为下面的那个例子中的构造函数并不是默认的构造函数,编译器将会合成默认的构造函数,用来初始化name,ptr没有初始化,这和例子中的构造函数没有什么关系,看样子前面写了那么多是白费了!充分的还是那句有颜色的话从1书上摘录的)所以对象成员已被默认的构造函数初始化了。再加上后面使用的是赋值,那么对象成员的类的赋值运算符又被调用了。这里对对象成员的初始化一共有2个函数调用,一个是默认的构造,一个是赋值运算符。而如果我们使用mil的话,那么将会直接掉用对象成员的类的拷贝构造函数来初始化,一步到位,就一个函数调用,故而都偏向于使用mil,而不是赋值来初始化。
还有关于mil的一点说明,就是成员的初始化顺序只和成员的声明顺序有关,和在mil中的顺序无关。【1】说也说到了4中情况必须用mil,我只理解前面简单的2种,就是reference和const成员必须用mil(why?)。下面有个最常用的例子用来诠释上面的一大段话:
class NamedPtr NamedPtr::NamedPtr(const string& initname,int* initptr)
{ {
string name; name=initname;
int * ptr; ptr=initptr;
}; }
对于左上的这个类,它含有对象成员name,并且string有默认的构造函数,满足了上面的条件(这句话扯淡了)。如果使用右上的赋值形式对name数据成员初始化的话,在name=initname执行之前,name就已经被string类默认的构造函数初始化了,然后遇到语句name=initname将调用赋值运算符。如果使用mil,将只调用string的拷贝构造函数。
在讲解到这部分的时候,【1】书还讲了很多关于copy constructor在程序转换(优化)方面的问题,说是和效率很有关系,自己看过了,但没有怎么消化过。
拷贝赋值运算符
编译器会合成默认的。效果和合成的拷贝构造函数类似。在同样的条件下,我们自己要定义自己的拷贝赋值运算符。在这里写一个原型的拷贝赋值运算符对我们理解有很大的帮助。赋值要注意的问题全在里面了。const X& operator=(const X& a)是写成成员还是非成员我们先不考虑。首先有个返回值,还是个常引用。有返回值是为了保持自然地语义,因为=运算符本身就有返回值,返回的是左值,这也是为了支持连锁赋值(a=b=c)。一般是返回引用,因为引用不需要对象的拷贝,效率高点,但是有必须返回对象的情况则要返回对象(在ec上有讲解)。是个常量的原因是:避免作为左值,例如:(a=b)=c,这个式子是错误的。关于const的用法,会单独的讲解的。在函数里面首先要做的就是避免自我赋值,自我赋值是很危险地:因为如前面所述,赋值的时候会先释放掉老的资源,对于自我赋值a=a,a的资源会释放掉,在用a来赋值是没有意义的。if(&a!=this)这句就很重要了,这里用this也说明了是成员。然后释放老的资源,分配新的空间等等,最后要返回:return *this。赋值运算符会给所有的数据赋值。这点也和拷贝构造函数一样。
但是有一点要注意的是:返回引用和指针是一样的,引用和指针所指的对象在函数调用结束后继续存在,否则就必须返回值。
析构函数
1.C++中类的构造 (还是为那些细节而折腾。)
我们主要关注的是各个成员函数。
C++中有4个特殊的成员函数:(默认)构造函数,析构函数,(默认)拷贝构造函数,(默认)拷贝赋值函数。
主要的是编译器会为我们合成它们么?
(默认)构造函数
什么是默认的构造函数?
最初的记忆是如果类中没有任何的构造函数,那么编译器就会帮你合成默认构造函数。
按照(inside the c++)的说法,这个属于新手常见的误解。(不断补充,改正认识)
书载,如果类中没有任何用户定义的构造函数,编译器会有个trivial的默认构造函数产生,它什么也不会做。但是不会合成出来。(具体什么意思?myth)接着说只有下面的几种情况下,才有一个non-trivial的默认构造函数被合成,并且会在需要的时候,既是调用的时候合成。(这点貌似体现了C++的设计理念)合成的事是处于编译器的需要,而不是处于程序员的需要,且合成的默认构造函数只会做编译器需要做的初始化,其余的不会做。另外,如果类中有了用户定义的构造函数,那么默认的构造函数也不会被合成(?是吗?如果类中有用户定义的默认构造函数,那么默认的构造函数编译器肯定不会合成的,如果用户定义的不是默认构造函数呢?会合成默认的么?),编译器需要做的初始化将会通过扩展已有的构造函数来达到。在【1】书中说到:"如果class A内含一个或多个以上的对象成员,那么class A的每一个构造函数必须调用每一个对象成员的默认构造函数"。编译器会扩展以存在的构造函数,在其中安插一些代码,使得用户代码在被执行之前,先调用必要的默认构造函数。而在2书中也有类似的论述:默认情况下,在构造函数的函数体被执行前,对象中的所有成员都已经被他们的默认构造函数所初始化了,那些没有构造函数的成员则将拥有一个未定义的初始值。
a)类中含有对象成员,且这个对象成员的类含有默认的构造函数。这时编译器将会合成个non-trivial的默认构造函数,它的主要作用就是调用对象成员类的默认构造函数来初始化类中的对象成员。除此之外,它应该不会做其他的初始化。如果这个类中有多个这样的对象成员,那么编译器将会按照对象成员的声明顺序依次来调用默认的构造函数进行初始化,这么些代码将被安插在用户代码之前。(这句话对后面的有些问题产生了影响,也深刻的反映了c++数据在被使用之前初始化的准则)
b)类继承的基类中含有默认的构造函数。被合成的默认构造函数将会调用基类的默认构造函数。
c)和virtual有关。类中有虚函数或者类继承了虚函数或者是虚继承。因为虚函数,vtable的存在都是编译器设置的,用户全然不知,故而vptr的初始化全是编译器的责任。当类和虚有关的时候,编译器要合成默认的构造函数来正确的初始化vptr。
上面的这一段基本上是来自于书【1】c++对象模型。
补充个细节:初始化和赋值
初始化的意思就是对象第一次出现,给它初始化。赋值的意思是将对象的旧值用新值取代。
对象的初始化是调用构造函数或拷贝构造函数来完成的,赋值是调用赋值运算符来做的。
赋值的旧值如果占有资源的话,可能需要在赋予新值之前释放掉旧的资源。这些东西也决定了拷贝构造函数和拷贝赋值在实现上有些不同。这个主题在efficient c++和c++编程惯用法上都有介绍。
(默认)拷贝构造函数
用的情况有三种:一是直接的将一个对象用另一个同类的对象来做初始化。二是函数参数的传递,三是函数值的返回。如果没有定义拷贝构造函数,那么编译器将会合成一个默认的拷贝构造函数(真伪?)。inside the c++ object model还提到不要位拷贝语义的4种情况,就是和编译器合成默认的构造函数一样的条件。
a)类中含有对象成员,且这个对象成员的类含有拷贝构造函数(无论是通过声明还是合成的),
后面的几个都是一样的,就不重复了。另外,合成的拷贝构造函数和合成的默认构造函数有点不同,前者会复制所有的数据成员,而后者只会初始化编译器需要的。
这个默认的函数(默认合成的也是trivial的)做的拷贝是按成员拷贝(并且可以递归),每个成员是位拷贝。位拷贝我的理解就是直接拷贝内存(位)上的值,所谓的浅拷贝了。如果对于简单的数据类型,这些默认的就够了。也不会出什么问题。如果对于指针呢?如果类中含有指针这种动态内存分配的数据,就得定义自己的拷贝构造函数和拷贝赋值运算符了(ec,item 11)。因为有指针出现,如果按照默认的拷贝构造的语义,就是浅拷贝,直接拷贝的内存,也就是将对象中指针的值赋予了另一个对象的指针数据,这样就导致两个不同对象的指针数据成员指向了同一个地址空间,这个结果未定义。(如果有个对象析构了,释放了内存,那么另一个对象将指向一个被释放的空间)另外如果类中有静态的数据成员,那么也有可能要定义自己的拷贝构造函数用来对静态数据进行处理(c++编程惯用法).自己提个问题什么时候需要定义自己的默认构造函数?都没有提到过。
拷贝构造函数也是构造函数,很简单的,它们都是用来初始化数据成员的。在(拷贝)构造函数中,多用成员初始化列表(Member init list),少用赋值(ec,item 12)。对于类中的数据都是普通的类型,比如整型之类用初始化和赋值都是等价的,怎么方便怎么用。如果类中有对象成员,并且这个对象成员的类还有默认的构造函数,你对对象成员使用赋值你想会有什么情况发生?一个类有对象成员,并且还有默认的构造函数这个条件达到了编译器合成一个non-trivial的默认构造函数的要求。很明显,这里已经有了构造函数,故而不会合成默认构造函数(不确定),只会在现有的构造函数的用户代码前面安插上一些扩展代码,用来调用对象成员的默认构造函数来初始化对象成员。这个能很好的解释在构造函数的用户代码执行之前,有对象成员就被默认的构造函数给初始化了。(以前我对于这点并不知道原因,今天算了解透了。仔细想想这个理由也不是很充分,因为下面的那个例子中的构造函数并不是默认的构造函数,编译器将会合成默认的构造函数,用来初始化name,ptr没有初始化,这和例子中的构造函数没有什么关系,看样子前面写了那么多是白费了!充分的还是那句有颜色的话从1书上摘录的)所以对象成员已被默认的构造函数初始化了。再加上后面使用的是赋值,那么对象成员的类的赋值运算符又被调用了。这里对对象成员的初始化一共有2个函数调用,一个是默认的构造,一个是赋值运算符。而如果我们使用mil的话,那么将会直接掉用对象成员的类的拷贝构造函数来初始化,一步到位,就一个函数调用,故而都偏向于使用mil,而不是赋值来初始化。
还有关于mil的一点说明,就是成员的初始化顺序只和成员的声明顺序有关,和在mil中的顺序无关。【1】说也说到了4中情况必须用mil,我只理解前面简单的2种,就是reference和const成员必须用mil(why?)。下面有个最常用的例子用来诠释上面的一大段话:
class NamedPtr NamedPtr::NamedPtr(const string& initname,int* initptr)
{ {
string name; name=initname;
int * ptr; ptr=initptr;
}; }
对于左上的这个类,它含有对象成员name,并且string有默认的构造函数,满足了上面的条件(这句话扯淡了)。如果使用右上的赋值形式对name数据成员初始化的话,在name=initname执行之前,name就已经被string类默认的构造函数初始化了,然后遇到语句name=initname将调用赋值运算符。如果使用mil,将只调用string的拷贝构造函数。
在讲解到这部分的时候,【1】书还讲了很多关于copy constructor在程序转换(优化)方面的问题,说是和效率很有关系,自己看过了,但没有怎么消化过。
拷贝赋值运算符
编译器会合成默认的。效果和合成的拷贝构造函数类似。在同样的条件下,我们自己要定义自己的拷贝赋值运算符。在这里写一个原型的拷贝赋值运算符对我们理解有很大的帮助。赋值要注意的问题全在里面了。const X& operator=(const X& a)是写成成员还是非成员我们先不考虑。首先有个返回值,还是个常引用。有返回值是为了保持自然地语义,因为=运算符本身就有返回值,返回的是左值,这也是为了支持连锁赋值(a=b=c)。一般是返回引用,因为引用不需要对象的拷贝,效率高点,但是有必须返回对象的情况则要返回对象(在ec上有讲解)。是个常量的原因是:避免作为左值,例如:(a=b)=c,这个式子是错误的。关于const的用法,会单独的讲解的。在函数里面首先要做的就是避免自我赋值,自我赋值是很危险地:因为如前面所述,赋值的时候会先释放掉老的资源,对于自我赋值a=a,a的资源会释放掉,在用a来赋值是没有意义的。if(&a!=this)这句就很重要了,这里用this也说明了是成员。然后释放老的资源,分配新的空间等等,最后要返回:return *this。赋值运算符会给所有的数据赋值。这点也和拷贝构造函数一样。
但是有一点要注意的是:返回引用和指针是一样的,引用和指针所指的对象在函数调用结束后继续存在,否则就必须返回值。
析构函数
还没人转发这篇日记