赞
踩
class Point
{
public:
Point( float xval );
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print( ostream &os ) const;
float _x;
static int _point_count;
};
struct mumble {
/* stuff */
char pc[ 1 ];
};
// grab a string from file or standard input
// allocate memory both for struct & string
struct mumble *pmumb1 = ( struct mumble* )
malloc(sizeof(struct mumble)+strlen(string)+1);
strcpy( &mumble.pc, string );
程序模型:数据和函数分开。
抽象数据类型模型:数据和函数一起封装以来提供。
面向对象模型:可通过一个抽象的base class封装起来,用以提供共同接口,需要付出的就是额外的间接性。
纯粹使用一种典范编程,有莫大的好处,如果混杂多种典范编程有可能带来意想不到的后果,例如将继承类的对象赋值给基类对象,而妄想实现多态,便是一种ADT模型和面向对象模型混合编程带来严重后果的例子。
class ZooAnimal {
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void rotate();
protected:
int loc;
String name;
};
shape *ps=new circle();
ps->rotate();
if(circle *pc=dynamic_cast<circle *>(ps))...
- 通常很多C++程序员存在两种误解:
- 没有定义默认构造函数的类都会被编译器生成一个默认构造函数。
- 编译器生成的默认构造函数会明确初始化类中每一个数据成员。
- 通常C++初级程序员会认为当一个类为没有定义拷贝构造函数的时候,编译器会为其合成一个,答案是否定的。
- 编译器只有在必要的时候在合成拷贝构造函数。
- 这是的重点是探索: 那么编译器什么时候合成,什么时候不合成,合成的拷贝构造函数在不同情况下分别如何工作呢?
X::X( const X& x);
Y::Y( const Y& y, int =0 ); //可以是多参数形式,但其第二个即后继参数都有一个默认值
当一个类对象以另一个同类实体作为初值时,大部分情况下会调用拷贝构造函数。
编译器何时合成拷贝构造函数
- 并不是所有未定义有拷贝构造函数的类编译器都会为其合成拷贝构造函数,编译器只有在必要的时候才会为其合成拷贝构造函数。
- 必要的时刻是指编译器在普通手段无法完成解决“当一个类对象以另一个同类实体作为初值”时,才会合成拷贝构造函数。也就是说,当常规武器能解决问题的时候,就没必要动用非常规武器。
X foo()
{
X xx;
if(...)
return xx;
else
return xx;
}
void foo(X &result)
{
result.X::X();
if(...)
{//直接处理result
return;
}
else
{//直接处理result
return;
}
}
- 对于初始化队列,厘清一个概念是非常重要的:(大概可以如下定义)
- 把初始化队列直接看做是对成员的定义,
- 构造函数体中进行的则是赋值操作。
有四种情况必须用到初始化列表:
前两者因为要求定义时初始化,所以必须明确的在初始化队列中给它们提供初值。
后两者因为不提供默认构造函数,所有必须显示的调用它们的带参构造函数来定义即初始化它们。
显而易见的是当类中含有对象成员或者继承自基类的时候,在初始化队列中初始化成员对象和基类子对象会在效率上得到提升——省去了一些赋值操作嘛。
最后,一个关于初始化队列众所周知的陷阱,初始化队列的顺序
class X{};
class Y:virtual public X{};
class Z:virtual public X{};
class A:public Y, public Z{};
// Lippman的一个法国读者的结果是
sizeof X yielded 1
sizeof Y yielded 8
sizeof Z yielded 8
sizeof A yielded 12
// vs2010上的结果是
sizeof X yielded 1
sizeof Y yielded 4
sizeof Z yielded 4
sizeof Z yielded 8
- 本文所有内容在建立在一个前提下:使用VC编译器。
- 着重点在于:
- VC的内存对齐准则;
- 同样的数据,不同的排列有不同的大小;
- 在有虚函数或虚拟继承情况下又有如何影响?
- 内存对齐?!What?Why?
- 对于一台32位的机器来说如何才能发挥它的最佳存取效率呢?当然是每次都读4字节(32bit),这样才可以让它的bus处于最高效率。实际上它也是这么做的,即使你只需要一个字节,它也是读一个机器字长(这儿是32bit)。更重要的是,有的机器在存取或存储数据的时候它要求数据必须是对齐的,何谓对齐?它要求数据的地址从4的倍数开始,如若不然,它就报错。还有的机器它虽然不报错,但对于一个类似int变量,假如它横跨一个边界的两端,那么它将要进行两次读取才能获得这个int值。比方它存储在地址为2-5的四个字节中,那么要读取这个int,将要进行两次读取,第一次读取0-3四个字节,第二次读取4~7四个字节。但是如果我们把这个整形的起始地址调整到0,4,8…呢?一次存取就够了!这种调整就是内存对齐了。我们也可以依次类推到16位或64位的机器上。
数据成员的布局
对于一个类来说它的对象中只存放非静态的数据成员,但是除此之外,编译器为了实现virtual功能还会合成一些其它成员插入到对象中。我们来看看这些成员的布局。
C++ 标准的规定:
一般的编译器怎么做?
编译器合成的成员放在哪?
- 为了实现虚函数和虚拟继承两个功能,编译器一般会合成Vptr和Vbptr两个指针。那么这两个指针应该放在什么位置?C++标准肯定是不曾规定的,因为它甚至并没有规定如何来实现这两个功能,因此就语言层面来看是不存在这两个指针的。
class X{
public:
int a;
virtual void vfc(){};
};
int main()
{
using namespace std;
X x;
cout<<&x.a<<" "<<&x<<endl;
system("pause");
}
对象成员或基类对象成员后面的填充空白不能为其它成员所用
class X{
public:
int x;
char c;
};
class X2:public X
{
public:char c2;
};
Vptr与Vbptr
class X{
virtual void vf(){};
};
class X2:virtual public X
{
virtual void vf(){};
};
class X3:virtual public X2
{
virtual void vf(){};
}
- c++支持三种类型的成员函数,每一种调用方式都不尽相同
- static-Function
- nostatic-Function
- virtual-Function
保证nostatic member function至少必须和一般的nonmember function有相同的效率是C++的设计准则之一。 事实上在c++中非静态成员函数(nostatic member function)与普通函数的调用也确实具有相同的效率,因为本质上非静态成员函数就如同一个普通函数.
float Point::X();
//成员函数X被插入额外参数this
float Point:: X(Point* this );
将一个成员函数改写成一个外部函数的关键在于两点,
//p->X();被转化为
?X@Point@@QAEMXZ(p);
//obj.X();被转化为
?X@Point@@QAEMXZ(&obj);
// p->function()
// 将转化为
(*p->vptr[1])(p);
(a+=b).static_fuc();
- 深度探索C++对象模型》是这样来说多态的:
- 在C++中,多态表示“以一个public base class的指针(或引用),寻址出一个derived class object”的意思。
Point ptr=new Point3d; //Point3d继承自Point
//例1,虚函数的调用
ptr->z();
//例2,RTTI 的应用
if(Point3d *p=dynamic_cast<Point3d*>(ptr) )
return p->z();
ptr->z();
//被编译器转化为:
(*ptr->vptr[4])(ptr);
class Base1 { public: Base1(); virtual ~Base1(); virtual void speakClearly(); virtual Base1* clone() const; protected: float data_Base1; }; class Base2 { public: Base2(); virtual ~Base2(); virtual void mumble(); virtual Base2* clone() const; protected: float data_Base2; }; class Derived: public Base1,public Base2 { public: Derived(); virtual ~Derived(); virtual Derived* clone() const; protected: float data_Derived; };
Base2* pbase2 = new Derived;
//编译器转化,新的Derived对象必须调整
//以指向Base2 subobject
Derived* temp = new Derived;
Base2* pbase2 = temp?temp + sizeof(Base1):0;
//调整后,调用虚函数正确
pbase2->data_Base2; // 指向base2
//删除pbase2时候,必须正确调用virtual destructor实例
//然后delete
delete pbase2; // 指向derived
vtbl_Derived; //主要表格
vtbl_Base2_Derived; //次要表格
- 取一个nonstatic data member的地址,得到的结果是该member在 class 布局中的byte位置(再加1),它是一个不完整的值,须要被绑定于某个 class object的地址上,才可以被存取.
- 取一个nonstatic member function的地址,假设该函数是nonvirtual,则得到的结果是它在内存中真正的地址.然而这个值也是不全然的,它也须要被绑定与某个 class object的地址上,才可以通过它调用该函数,全部的nonstatic member functions都须要对象的地址(以參数 this 指出).
double // return type
(Point::* // class the function is member
pmf) // name of the pointer to member
(); // argument list
double (Point::*coord)() = &Point::x;
coord = &Point::y;
(origin.*coord)();
或 (ptr->*coord)();
(coord)(&origin);
和(coord)(ptr);
Point::*
的作用是是作为 this 指针的空间保留者.这这也就是为什么 static member function(没有 this 指针)的类型是"函数指针",而不是"指向member function的指针"的原因.注意以下的程序片段:
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;
pmf,一个指向member function的指针,被设值为Point:?()(一个 virtual function)的地址,ptr则被指定以一个Point3d对象,
ptr->z();
则被调用的是point3d:: z(),(ptr->pmf)();
仍然是Point3d:: z()被调用也就是说,虚拟机制仍然可以在使用"指向member function的指针"的情况下运行 !
对一个nonstatic member function取其地址,将获得该函数在内存中的地址,然而面对一个 virtual function,其地址在编译时期是未知的,所能直到的仅是 virtual function在其相关的 virtual table中的索引值.也就是说,对一个 virtual member function取其地址,所能获得的仅仅是一个索引值.
具体实现过程如下:
例子:
class Point {
public:
virtual ~Point();
float x();
float y();
virtual float z();
};
对nonstatic函数取地址:
&Point::x();
或 &Point::y();
得到的则是函数在内存中的地址,由于它们不是 virtual对virtual函数取地址:
&Point::~Point;
得到的结果是1(索引值.)&Point:: z();
得到的结果是2(索引值.)对指向虚函数的函数指针调用:
(*ptr->vptr[(int)pmf])(ptr);
float (Point::*pmf)();
// 二者都能够被指定给pmf
float Point::x() { return _x; }
float Point::z() { return 0; }
即使是一个抽象基类,如果它有非静态数据成员,也应该给它提供一个带参数的构造函数,来初始化它的数据成员。 或许你可以通过其派生类来初始化它的数据成员(假如nostatic data member为publish或protected),但这样做的后果则是破坏了数据的封装性,使类的维护和修改更加困难。由此引申,类的data member应当被初始化,且只在其构造函数或其member function中初始化。
不要将析构函数设计为纯虚的,这不是一个好的设计。 将析构函数设计为纯虚函数意味着,即使纯虚函数在语法上允许我们只声明而不定义纯虚函数,但还是必须实现该纯虚析构函数,否则它所有的继承类都将遇到链接错误。
真的必要的时候才使用虚函数,不要滥用虚函数。 虚函数意味着不小的成本,编译很可能给你的类带来膨胀效应:
不能决定一个虚函数是否需要 const ,那么就不要它。
决不在构造函数或析构函数中使用虚函数机制(并不是说不要把构造函数和析构函数设置为虚函数)。
struct Point {
float x,y,z;
};
实际上struct还要复杂一点,它有时表现的会和C struct完全一样,有时则会成为class的胞兄弟。 ↩︎
Ref: C++虚函数表,虚表指针,内存分布 ↩︎
Ref: 深度探索c++对象模型(一) ↩︎
详情请参考: C++中的类所占内存空间总结 ↩︎
参考一下实现: 二重调度问题:解决方案之虚函数+RTTI ↩︎
Bitwise copy semantics 是Default Memberwise Intializiation的具体实现方式。[别人的解释] ↩︎
关于更多的memory alignment(内存对齐)的知识见VC内存对齐准则(Memory alignment), VC对齐 ↩︎ ↩︎
Sun公司实现的编译器 - 虚函数表取负值,表示取回虚基类对象的偏移量,rhs 表示一个存在虚基类的对象. ↩︎
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。