侯捷把书中第三章翻译为 “Data语意学”,另外书中有些语句得读几遍才明白他什么意思,也许是不习惯台湾的一些语言习惯。本文做一些简单的梳理。
对member functions 的函数体的分析,会直到整个class的声明都出现了才开始,而对于 member functions 的参数列表 却是在第一次遭遇时适当地决议(resolved)。
1.
extern int x;
class Point{
public:
float getX(){return x;}
private:
float x;
}
这个例子的getX()返回的是Point的x成员,而不是extern的x。
2.
typedef int length;
class Point{
public:
length getX(){return x;}
private:
typedef float length;
length x;
}
这个例子,我在VS2013的编译结果,会报warning:'“return”: 从“Point::length”转换到“length”,可能丢失数据'
。(实际就是float转换到int)
也就是说对函数参数列表,再第一次遇到length就已经适当决议为int了。
了解对象的内存布局,先捋捋有哪些会影响对象的内存布局,如果是C的结构体的话,比如说这样一个结构:
struct Point{
int x;
int y;
};
可以很简单的理解为一个表格:
C++的类与C的结构体相比有哪些不同呢?
1.无继承,简单的class(与C的结构体一模一样)
2.单继承,没有virtual function 也没有 virtual base class
3.单继承,有virtual function ,没有 virtual base class
4.多继承,没有virtual base class
5.虚拟继承
下面就分别看看这5种情况,内存布局如何。
class Point4d{
private:
type1 x;//这里的type1指代某种数据类型
type2 y;//这里的type2指代某种数据类型
type3 z;//这里的type3指代某种数据类型
type4 t;//这里的type4指代某种数据类型
}
布局原则:较晚出现的member在对象中有较高的地址。注意,这里并没要求member中间的地址是连续的,原因很简单:内存对齐。
所以这种单个access section的情况,布局可能为这样:
对于有多个access section 的情况,例如:
class Point4d{
private:
type1 x;//这里的type1指代某种数据类型
private:
type2 y;//这里的type2指代某种数据类型
private:
type3 z;//这里的type3指代某种数据类型
private:
type4 t;//这里的type4指代某种数据类型
}
c++ standard允许编译器对这几个section自由排列,而不必在乎声明的顺序,不过目前没有谁家的编译器会无聊地随意摆放顺序,因为这并不能带来什么好处。
class Point2d{
protected:
float x;
float y;
}
class Point3d:public Point2d{
protected:
float z;
}
书中提到一个需要注意的点,把一个class分解为两层或者多层的继承体系,可能会造成“空间膨胀”。例如:
class Con{
public:
int val;
char c1;
char c2;
char c3;
}
//分解为三个:
class Con1{
public:
int val;
char c1;
}
class Con2 : public Con1{
public:
char c2;
}
class Con3 : public Con2{
public:
char c3;
}
class Animal{
public:
char *name;
virtual void say();
};
class Dog : public Animal{
public:
void say(){ cout<< "wang wang~" << endl;}
};
class Cat : public Animal{
public:
void say(){ cout<< "miao miao~" << endl;}
};
如果我们自己来实现virtual function,该怎么实现呢?
第一冒出来的想法,就是class每声明一个virtual function,就在对象的结构中增加一个指针,而class的子类实现n个virtual function后,当实例化class的子类时,就需要正确设置n个virtual function的指针指向正确的地址。
把上面的想法进一步优化一下,只使用一个指针,即vptr,指向一个virtual function的表格。
上图中,把vptr放在对象结构的末尾,好处是,可以保留base class C struct的对象布局,因而在C程序代码中也能使用。
不过也有的编译器把vptr放在了对象布局的开头,显然这么做损失了C的兼容性,但是在实现 “成员函数的指针”的时候,能带来一些好处(少一个字段),具体原因为何,下篇讲对象的成员函数再讨论。
这个与单继承相比,在对象的布局方面,实在没什么创新的。
class A{
public:
int a;
//... 其它成员
};
class B{
public:
int b;
//... 其它成员
};
class C:public A,public B{
public:
int c;
//... 其它成员
};
多重继承给C++编译器带来的一些额外的工作:
C cobj;
A *pa;
B *pb;
C *pc;
pa = &cobj; //简单的拷贝地址即可
pb = &cobj; //需要内部转换 pb = (B*)(((char*)&cobj) + sizeof(A));
pb = pc; //需要内部转换 pb = pc?(B*)(((char*)pc)+sizeof(A)):0;
注意比较上面三种指针的赋值方式:
第2种方式与第1种比较,pa能直接拷贝&cobj的原因,在于A是C的继承体系中的第一个base class,而第2种方式需要加上一些偏移量,因为B在C中不是第一个base class。
第3种方式与第2中方式比较,区别在于,一个是引用,一个是指针,对于引用,不存在引用“空”的情况,而指针则可能为“空”,所以第3种情况需要加个条件判断。
ps:
书中在描述第3中情况的时候,给出的表达式少了个括号,如果对照成我上面重新写的例子,书中对于对于pb = pc
的解读就类似:
pc?(B*)((char*)pc)+sizeof(A):0
,少了个括号,是错误的!侯捷也有粗心的时候!读者可以自行验证。
class Share{
public:
int share;
//...other member
};
class A:public virtual Share{
public:
int a;
//...other member
};
class B:public virtual Share{
public:
int b;
//other member
}
class C:public A,public B{
public:
int c;
//other member;
}
继承体系,为典型的“菱形”结构:
怎么实现virtual继承呢?使上面的继承体系中,Share对象在C的对象中只有一份拷贝。
如果让我来实现,我可能会为对象加一个跟vptr类似的指针,指向virtual base class的对象。那如果有多个virtual base class呢?这个问题跟virtual function如果有多个怎么办类似,就直接只用一个指针,指向一个表格,这个表格存的就是一些指向virtual base class object的指针啦。
ok,我上面提到的思路,其实微软的编译器就是这么干的,画个图看看这种策略:
(ps:书中只画了只有一个virtual base class 的情况,我这里把多个的情况也给画了出来,如果有误,请指出,谢谢!)
从上图可以看到,每个通过virtual继承的class object都多了一个指针,指向一个virtual base class table。书中还提到另一种策略,直接去掉virtual base class ptr,就直接利用现有的vptr和vtable就行了!看下图:
虚拟继承这部分就差不多完了。书中还提到一个问题,“由于虚拟继承链的加长,导致间接存取层次的增加”。也就是有n层虚拟派生,就需要经过n个virtual base class 指针进行间接存取,而我们理想的情况是希望有固定的存取时间,不随虚拟派生的深度而改变。
这个问题…我就不画图了,书中提到目前的大部分编译器采用的做法是:把nesed virtual base class的指针放到 derived class object中。编译器提供选项供程序员选择是否产生双重指针。
C++的一个小特性
指向数据成员的指针。
还是接着上面的虚拟继承的例子(与虚拟继承无关,只用它的继承结构)
void test(int C::*dp, C *p){
printf("offset:%d, value:%d\n", dp,p->*dp);
}
//...
C cobj;
cobj.a = 1;
cobj.b = 2;
C *p = &cobj;
int A::*dpa = &A::a; //dpa的值为 a在A中的offset
int B::*dpb = &B::b; //dpb的值为 b在B中的offset
test(dpa, p); //打印offset 0, value 1(即a的值)
test(dpb, p); //打印offset 4, value 2(即b的值)
上面怎么做到的呢?还是编译器干了活,在函数调用的时候,
test(dpa, p); //打印出cobj 中 a 的值 1
test(dpb, p); //打印出cobj 中 b 的值 2
//实际上编译器得内部转换:
//test(dpa?dpa+0:0,p) //只是思路,这么写实际上编译通不过。
//test(dpb?dpb+sizeof(A):0,p) //只是思路,这么写实际上编译通不过。