最近在看Effective c++ 感觉这本书不错,学习c++的同学可以看一下,加深自己对c++的认识。
1.什么时候类中需要复制构造函数(copy constructor)和赋值操作符(Assignment)?
不止Effective c++中讲到,我依然还记得c++ primer中也提到过这个问题,可见这个问题的重要性
答案:当class中有动态配置的内存时或者说当class类中有指针成员时需要复制构造函数和赋值运算符并同时需要析构函数(在析构函数中添加一些释放内存等操作)
可是大家想过没有为什么是这样哈?
下面就套用书上的一个例子说明
class String
{
public:
String(const char *value);
~String();
private:
char *data;
};
String::String(const char *value)
{
if(value)
{
data = new char[strlen(value)+1];
strcpy(data,value);
}else{
data = new char[1];
*data='\0';
}
}
String::~String()
{
delete [] data;
}
上面的String类中没有copy constructor 和 assignment
当我们在调用这个类并执行下面的程序时大家就能看出问题了
String a="Hello";
String b="World";
b=a;
对象a中指针data指向内存的一个位置
对象b中的指针data指向内存的另一个位置
当执行b=a; 时因String类中没有赋值操作符,只能调用类中默认的赋值操作符 即:右边对象的数据成员赋值给左边对象
此时细心的读者就会发现问题,左边的对象的指针data指向的内存怎么没有释放--------这就会导致第一个问题------内存泄漏
另外当调用完系统默认赋值操作符后对象a,对象b中的指针data都指向了内存的同一块区域,这就会产生第二个问题---当某个指针释放内存后,另外一个就成了悬浮指针-----用一个名词来说就是野指针(是指向被释放的或者访问受限内存的指针)。
添加赋值操作符的代码如下
String& String::operator =(const String &rhs)
{
if(this == &rhs)
{
return *this;
}
delete [] data;
data = rhs.data;
return *this;
}
当程序中只要有传值操作时就会调用copy constructor复制构造函数(我们应该避免用这种传值的方式,应该用引用)
void doNothing(String localString){}
String str="I Love The World";
doNothing(str);
看上面的代码说明当类中有指针成员时,类中没有复制构造函数也会产生野指针的情况
当str字符串以传值的方式到函数doNothing中,也就是调用系统默认的也就是缺省的复制构造函数,则localString中便有了str中指针成员的副本,但是当函数执行完后,localString的指针成员data所指向的内存已经释放,而str中的指针成员data还指向着这段内存,所以就会造成------------野指针的情况
结论-----当类中具有指针成员时必须在类中编写自己的复制构造函数和赋值运算符以及在析构函数中释放内存。
原因-----如果不这样做的话会出现 -----------内存泄漏和野指针(指针指向一块已经释放的内存)
2.构造函数中初始化列表与在构造函数体内进行赋值初始化的区别?
读过c++ primer和Effective c++的人应该都知道尽量使用初始化列表的方式进行对类的数据成员进行初始化。
为什么是这样呢?有什么特殊情况么?
首先,当类成员中含有const成员和引用成员时必须使用初始化列表的形式进行初始化,因为在函数体内无法用赋值运算符进行初始化。
除了const成员和引用成员外其他数据成员应该怎么选择哈?
当类的数据成员不是内建型的也就是不是内置型的(如int double float都为内置型成员)时,使用初始化列表比在函数体内使用赋值运算符效率高
因为,当在函数体内使用赋值运算符时首先要调用该非内置类型数据成员的默认构造函数 然后再调用赋值运算符进行赋值,而使用初始化列表的方式只需要调用复制构造函数即可(copy constructor),前者需要调用两个函数,而后者则需要调用一个函数。
当类的数据成员是内置型成员时,初始化列表和使用赋值运算符的效果是等同的。
当类中有数组成员时,只能在构造函数体内用赋值运算符进行初始化,不能在初始化列表中进行初始化的。
例外情况:
当类中有大量的内置型成员,则使用赋值运算符的效果比初始化列表的方式要好。看下面的例子
class ManyDataMbrs{
public:
ManyDataMbrs();
ManyDataMbrs(const ManyDataMbrs &x);
void init();
private:
int a,b,c,d,e,f,g,h;
double i,j,k,l,m,n;
};
void ManyDataMbrs::init()
{
a=b=c=d=e=f=g=h=0;
i=j=k=l=m=n=0;
}
ManyDataMbrs::ManyDataMbrs()
{
init();
}
3.类中各数据成员的初始化顺序是由谁来决定的?
可能我们以前并不注意类中成员初始化顺序。大家认为类中数据成员的初始化顺序是跟构造函数中的初始化列表和函数体内的初始化顺序一样么?
如果大家都这么认为的话就打错特错了哈!
类中数据成员的顺序是与在类中定义的顺序来初始化的,与构造函数中初始化列表顺序和函数体内顺序是完全没有关系的哈!
#include <iostream>
class Test{
public:
Test(int value):y(value) , x(y){}
friend std::ostream& operator<<(std::ostream &out , const Test &T);
private:
int x;
int y;
};
std::ostream& operator<<(std::ostream &out , const Test &T)
{
return out << "x= " << T.x << " y= " <<T.y <<std::endl;
}
int main()
{
Test T(3);
std::cout << T;
}
因在类中x先定义而y后定义,则先初始化x,但是x是由y初始化的那么此时x将会是一个随机值,而y再根据value可以正常初始化
打印结果如下:
结论-------请大家在对类的数据成员初始化时尽量保持使类中数据成员的声明顺序和初始化顺序相同--
4.如果该类是基类(有类从此类派生),则尽量让基类的析构函数为虚函数
先向大家说明一下一个类的虚函数是执行的原理。
虚函数是多态机制的基础,就是在程序在运行期根据调用的对象来判断具体调用哪个函数,现在我们来说说它的具体实现原理。
当一个类中含有虚函数,该类实例化也就是创建该类的对象时,会给该对象的数据成员分配空间,同时在该空间中会有一个虚函数指针vptr,这个指针在对象内存的位置是在所有该对象所有数据成员的最前面,该虚函数指针指向虚函数表vtbl,这个虚函数表vtbl中存放着该类中所以虚函数的函数指针,其中包括从基类继承的虚函数,通过虚函数指针来判断执行哪个虚函数。
编译器会循着该对象的vptr所指的vtbl,决定调用哪一个函数,编译器会在vtbl中寻找适当的函数指针。
常常应该将基类的析构函数设置为虚函数,因为当基类指针或引用绑定的是该类的派生类时,在删除该指针时,如果基类的析构函数不为虚函数,则只会调用基类的析构函数(因为非虚函数时,是由该指针的静态类型所决定),而不会调用该指针所指向派生类对象的析构函数,所以应该将基类的析构函数设置为虚函数。当基类的析构函数为虚函数时,由于该指针动态绑定的是派生类对象,所以先调用派生类对象的析构函数,然后再依次调用基类的析构函数。
当我们想封装一个抽象类,只想让别人继承它,并不希望它实例化,此时就应该将该类中的某个成员函数为纯虚函数。如果没有找到合适函数为纯虚函数时,可将基类的析构函数设置为纯虚函数,但需要注意的是,当基类的析构函数为纯虚函数时,该函数必须实例化,因为基类的析构函数一定是会调用的。而其他成员函数如果是纯虚函数时则如果基类不需要操作则不需要该纯虚函数的实体。
5.在赋值操作符operator=重载中必须检查左右对象是否相同,同时传回*this的引用!
刚才的代码再贴过来继续讨论
String& String::operator =(const String &rhs)
{
if(this == &rhs)
{
return this;
}
delete [] data;
data = rhs.data;
return *this;
}
假如我们不检查左右对象是否相同的话会出现什么问题哈?
当我们尝试进行
a=a;
则首先会释放左边对象的内存空间,然后将右边对象的指针data成员赋值给左边对象的指针
别慌----此处就有错误了,左右两对象一样,你释放了左边对象指针成员data的内存,右边的指针成员data指向了已经释放的内存,又是野指针的情况,所以我们必须在赋值运算符中必须检查左右对象是否相同。
检查两个对象相同常用了有两种方式
检查*this==rhs,这样检查判断两个对象的值是否相同,所比较的是两个对象的数据成员值是否相同,并不是检查两个对象的地址
检查this==&rhs,此时是检查两个对象在内存中的地址是否相同,这在c++中是我们常用的,计算速度相对比较快。
最后我们传回的是*this的引用,也就是对象的引用。这样不用传值那样需要复制增加的效率,同时在各种选择下,只有传回*this的引用时最佳的。
6.在赋值运算符operator=中我们只对该类的数据成员赋值对么?
答案是错误的,如果该类不是从其他类派生而来的,那么当调用赋值运算符的时候只将自己的数据成员赋值就好了,但是如果该类是从其他类派生而来的,此时情况就大不同了哈!
我们常常在类的赋值运算符中会忽略这一点,千万要注意哈!
看下面的例子
class Base{
public:
Base(int initialValue = 0) : x(initialValue){}
private:
int x;
};
class Derived : public Base{
public:
Derived(int initialValue = 0) : Base(initialValue) , y(initialValue){}
Derived& operator=(const Derived &rhs);
private:
int y;
};
Derived& Derived::operator=(const Derived &rhs)
{
if(this == &rhs)
{
return *this;
}
y = rhs.y;
return *this;
}
首先,先说明派生类的构造函数,在 派生类的构造函数中应该先对该类的直接基类在初始化列表中进行初始化,然后再初始化该类的数据成员。
然后再说赋值运算符中的数据成员的赋值,在派生类中只针对该派生类的数据成员y进行了赋值,这肯定是错误的,在派生类中应该对基类的数据成员进行赋值,但基类的数据成员是private ,派生类访问不到,所以只能在派生类中显示的调用基类的赋值操作符
Derived& Derived::operator=(const Derived &rhs)
{
if(this == &rhs)
{
return *this;
}
<strong>Base::operator=(rhs);</strong>
y = rhs.y;
return *this;
}
同样在派生类的复制构造函数中也应该对基类的复制构造函数(拷贝构造函数)进行显式调用。
class Base{
public:
Base(int initialValue = 0) : x(initialValue){}
Base(const Base &rhs) : x(rhs.x){}
private:
int x;
};
class Derived : public Base{
public:
Derived(int initialValue = 0) : Base(initialValue) , y(initialValue){}
<strong>Derived(const Derived &rhs) : Base(rhs) , y(rhs.y){}</strong>
Derived& operator=(const Derived &rhs);
private:
int y;
};
Derived& Derived::operator=(const Derived &rhs)
{
if(this == &rhs)
{
return *this;
}
<strong>Base::operator=(rhs);</strong>
y = rhs.y;
return *this;
}
结论-----当类从某个类派生而来时,该派生类的的构造函数(对直接基类进行初始化)、赋值运算符operator=以及拷贝构造函数中都要对基类的数据成员进行处理,这是非常容易犯错的哈!