1. 虚函数
1.1 触发动态绑定(调用虚函数)的条件
<C++ Primer>中说触发虚函数有两个条件:一是所调用的成员函数为虚函数,二是必须通过基类类型的引用或指针进行函数调用。
但是,在vc2005中,如下的代码并不能触发动态绑定:
base_t objBase;
drived_t objDrived;
base_t &pObj = objBase;
pObj.vfcn();
pObj = objDrived;
pObj.vfcn();
实际上,两次对vfcn()的调用都是调用的base_t中的版本,编译器在编译期间已经将调用的函数确定为基类base_t中的版本,并没有生成动态绑定的版本。
如果不是对基类引用pObj赋值、而是将其用作函数的形参的话,那么将触发动态绑定,如下:
void fcn(base_t & pObj){
pObj->vfcn();
}
void main(){
base_t objBase;
drived_t objDrived;
fcn(objBase); // 调用fcn()的基类版本
fcn(objDrived); // 调用fcn()的派生类版本
}
2. 类的作用域
2.1 对象的成员函数可以访问当前对象(this指针指向的对象)的protected和private成员,而且可以访问同类型的其他对象的protected和private成员。如下:
class cls_t {
public:
void ChgOtherVal(cls_t &obj){obj.v = ... }
pirvate:
int v;
}
在ChgOtherVal()中,可以通过 obj访问 obj的protected和private成员。
可以这样理解,无论是当前对象(this指针指向的对象)还是同类型的其他对象(比如上例中的obj),都属于同一个类类型,理所当然在类的作用域下可以访问他们的所有成员。只是,当前对象通过隐含的this指针访问,而其他同类型的对象是通过显式的函数参数(指针或者引用)进行访问,本质上都是该类类型的对象,所以类的成员函数对他们的访问权限是一样的。
3. 复制初始化
<C++ Primer>中说复制初始化使用“=”,“复制初始化首先使用指定的构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象”。但实际上,仅仅当“=”右操作数为同类型对象时,才会调用复制构造函数,否则,调用相应的构造函数直接初始化左操作数(正在创建的对象)。
class Cls_t {
public:
Cls_t(int v=0): val(v) {cout << "I'm Cls_t(int=0)" << endl;}
Cls_t(Cls_t &obj): val(obj.val) {cout << "I'm Cls_t(Cls_t &)" << endl;}
void Print() const { cout << "val=" << val << endl; }
private:
int val;
};
int main() {
Cls_t obj = 100; // 仅仅调用了Cls_t(int=0) ,直接对 obj进行初始化
obj.Print();
Cls_t obj2 = obj; // 调用 Cls_t(Cls_t &) 对 obj2 进行初始化
obj2.Print();
}
同样,<C++ Primer>中说“如果使用常规的花括号括住的数组初始化列表来提供类类型元素的显示的初始化,则使用复制初始化来初始化每个元素。”但是实际上并没有用复制初始化,而是根据初始化列表的类型调用相应的构造函数直接对类类型元素进行初始化。代码如下:
int main() {
... ...
Cls_t objAry [] = {1, 2, 3};
}
定义 objAry的语句产生的输出为:
I'm Cls_t(int=0)
I'm Cls_t(int=0)
I'm ls_t(int=0)
根本没有调用Cls_t的复制构造函数。
4. 初始化与赋值的区别
class Cls_t {
public:
Cls_t(int v=0): val(v) {}
Cls_t(Cls_t &obj): val(obj.val) {cout << "I'm Cls_t(Cls_t &)" << endl;}
void Print() const { cout << "val=" << val << endl; }
Cls_t &operator=(const Cls_t & obj) {
cout << "I'm operator=(const Cls_t &)" << endl;
val=obj.val; return *this;
}
private:
int val;
};
int main() {
Cls_t obj1 = 100;
Cls_t obj2 = 200;
Cls_t obj3 = obj1; // 初始化,所以调用“复制构造函数”
obj3 = obj2; // 赋值,将调用“赋值操作符”
}
5. 友元与继承
“友元关系不能继承,基类的友元对派生类的成员没有特殊的访问权限”,但是,基类的友元对派生类中继承的基类成员有特殊的访问权限。
class Cls10_t{
friend class Cls20_t;
public:
Cls10_t(int v=100): val(v) {}
private:
int val;
};
class Cls11_t: public Cls10_t{
public:
Cls11_t(int v=200): myVal(v) {}
private:
int myVal;
};
class Cls20_t {
public:
void PrintVal(Cls10_t &obj) const {
cout << "I am in Cls20_t. Cls10_t::val=" << obj.val << endl;
}
void PrintVal(Cls11_t &obj) const {
cout << "I am in Cls20_t. Cls11_t::val=" << obj.val
//<< ", Cls11_t::myVal=" << obj.myVal // 不能访问!
<< endl;
}
};
在上面的例子中,Cls20_t可以访问Cls10_t类型对象的私有成员和Cls11_t类型对象中继承自Cls10_t的成员,但是对在Cls11_t中定义的非公有成员没有特殊的访问权限。
“如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。” 与上面同样道理,派生类继承自基类的函数仍然像基类一样访问授予友元关系的类,但是在派生类中自己定义的接口对授予友元关系的类没有特殊的访问权限。如下:
class Cls10_t{
friend class Cls20_t;
public:
Cls10_t(int v=100): val(v) {}
private:
int val;
};
class Cls20_t {
public:
void PrintVal(Cls10_t &obj) const { cout << "I am in Cls20_t. Cls10_t::val=" << obj.val << endl;}
};
class Cls21_t: public Cls20_t{
public:
// void MyPrintVal(Cls10_t &obj) const {cout << "I am in Cls21_t. Cls10_t::val=" << obj.val << endl;}
};
int main() {
Cls10_t obj10;
Cls20_t obj20;
Cls21_t obj21;
obj20.PrintVal(obj10);
obj21.PrintVal(obj10);
//obj21.MyPrintVal(obj10);
}
在上面的例子中,Cls21_t继承自Cls20_t的接口可以访问Cls10_t中的私有成员,但是,Cls21_t自己定义的接口对Cls10_t没有特殊访问权限。
6. 派生类中的复制控制
如果派生类中定义了赋值操作符,那么该赋值操作符应该显式地调用基类部分的赋值操作符(此时,如果基类没有自己定义赋值操作符,将使用合成的赋值操作符),否则将导致不一致的情况:派生类部分被赋值,基类部分没有被赋值。如下:
class Cls1_t {
public:
Cls1_t(int v=0): val(v){}
protected:
int val;
};
class Cls2_t: public Cls1_t {
public:
Cls2_t(int dv=0, int v=0): dVal(dv), Cls1_t(v){}
Cls2_t &operator=(Cls2_t & obj){
//Cls1_t::operator=(obj);
dVal = obj.dVal;
return *this;
}
void Print() const { cout << "val=" << val << ", dVal=" << dVal << endl;}
private:
int dVal;
};
int main(){
Cls2_t obj1;
obj1.Print();
Cls2_t obj2(200, 100);
obj2.Print();
obj1 = obj2;
obj1.Print();
}
Cls2_t的赋值操作符并没有显示调用基类部分的赋值操作符,则程序输出为:
val=0, dVal=0
val=100, dVal=200
val=0, dVal=200
如果Cls2_t的赋值操作符的控制体上加上:Cls1_t::operator=(obj),那么程序输出为:
val=0, dVal=0
val=100, dVal=200
val=0, dVal=200
与赋值操作符类似,如果派生类中定义了复制构造函数,那么该复制构造函数应该显示地调用基类部分的复制构造函数,否则,编译器将调用基类的默认构造函数,从而导致不一致的情况。
但是,派生类的析构函数不应该显式调用基类部分的析构函数,因为,在调用派生类的析构函数之后,编译器将自动调用基类部分的析构函数。在派生类的析构函数中显式调用基类部分的析构函数导致基类部分的析构函数被重复调用,可能引起错误。如下:
class Cls1_t {
public:
~Cls1_t() { cout << "I'm Cls1_t's destructor" << endl;}
};
class Cls2_t: public Cls1_t {
public:
~Cls2_t() {
cout << "I'm Cls2_t's destructor" << endl;
Cls1_t::~Cls1_t(); // 显式调用基类部分的析构函数
}
};
int main(){
Cls2_t obj;
}
程序输出为:
I'm Cls2_t's destructor
I'm Cls1_t's destructor
I'm Cls1_t's destructor
基类部分的析构函数被重复调用。