类中的函数
类中的函数不占用内存空间 每一个非内联函数只会诞生一个函数实例 而内联函数会在其每一个使用者模块身上产生一个函数实例
指针
指针都是占据的四字节,那么是如何区分指针指向的对象类型呢? 关键在于如何解释地址中的内容及其大小 其中cast其实是编译器指令,并不改变地址和内容,而是改变,被指出的内存的大小和其内容 的解释方式
static_cast和dynamic_cast 的区别
class animal
{
.....
};
class bear : public animal
{
int cell;
...
};
bear a;
animal *p1 = &a;
animal p2;
//此时,即使动态绑定,也是无法通过p1访问cell的,因为动态绑定是虚函数的
//那么如何通过p1访问cell呢?
(static_cast<bear *> (p1) ) -> cell;
(dymanic_cast<bear *> (p1) ) -> cell;
//此时,算是强制转换,将p1变成了bear对象类型;
//这里我们看不出区别,都是正确的,因为p1是指向a的指针,两者指向同一内存,只是解释的方式不同,内存中都有cell变量
//但是,如果这样
(static_cast<bear *> (p2) ) -> cell;
(dymanic_cast<bear *> (p2) ) -> cell;
//p2本身就是个基类,不存在cell变量
//此时静态转换不会报错, 但是我们知道,一旦访问,就会出错
//动态转换会进行检查,此时就会报错,避免发生这样的错误
默认构造函数
如果我们没有写类A的构造函数 当编译器认为需要的时候,就会生成默认构造函数(是编译器需要,而不是程序需要) 如果写了,那么就不会再生成 1 如果类的成员,具有默认构造函数,那么就会为类生成默认构造函数 比如 (1)类A内,含有成员变量类B,如果类B有默认构造函数,而我们没有为A写构造函数 那么编译器就会自动生成一个A的默认构造函数 (2)如果我们有类A的构造函数,但是没有在构造函数内初始化类B 那么编译器会自动将我们写的构造函数进行扩张 添加B的默认构造函数 而不是我们上面说的 ,如果写了,那么就不会再生成 2如果类派生自一个有默认函数的基类 那么这个继承类的默认函数也会被自动合成出来 而,如果基类有构造函数,那么在编译器看来,其和基类的默认构造函数没有差别 3.带有虚函数的类 4.带有一个虚基类的类
编译器合成出来的默认构造函数,均不会对类中每个成员赋值
带有默认构造函数的类,是其他类的成员
class Foo {
public:
Foo();
Foo(int);
...
};
class Bar
{
public:
Foo foo;//内涵,不是继承!
char * str;
};
void func()//一个函数,用到了Bar
{
Bar bar;
/*
bulabulabulabula...
*/
if(bar.str){...}
}
//被合成的Bar默认构造函数,能够调用class Foo的默认构造函数来处理Bar::foo
//但是,它并不会产生任何代码来初始化Bar::str。
//将Bar::foo初始化式编译器的责任,但是初始化Bar::str是程序员的责任
//于是我们又创建了Bar的默认构造函数
Bar::Bar() { str = 0; }
//现在程序的需求被满足了
//但是编译器还是要初始化成员对象foo
//但是由于Bar的默认构造函数我们已经显式的定义出来了
//编译器不能再合成第二个,该怎么办?
//于是,编译器就扩张了已存在的构造函数
//使得在程序员的代码被执行之前,先调用必要的默认构造函数foo
//如下
Bar::Bar()
{
foo.Foo(); //编译器构造函数
str = 0; //程序员代码
}
//但是,如果Bar类,有很多构造函数
//唯独没有默认构造函数,那该如何?
//答案就是,对每一个构造函数,都进行如上的扩张
//将程序员代码加进去
拷贝构造函数
默认逐个成员初始化 当一个类A是另一个类B的成员时 在进行拷贝初始化时 先逐个初始化B中已有的成员 然后再对A进行逐个成员初始化 如果A的成员中,还有类,那就如上循环 谓之递归
拷贝时虚函数表的指针
class zooanimal
{
public:
zooanimal();//默认构造函数
virtual ~zooanimal();//析构函数
virtual void draw();
private:
//draw所需的数据
};
class bear : public zooanimal
{
bear();
//virtual ~bear(); //不知道为何,有析构的话,下面赋值会报错???
void draw();
private:
//draw 所需的数据
};
bear yogi;
bear winnie = yogi;//会显示不能访问析构函数???什么意思
//yogi会被默认构造函数初始化
//其vpyr设定指向bear class的虚函数表
//将yogi的vptr直接拷贝至winnie的,是安全的
//但是,下面这种呢?
zooanimal franny = yogi; //派生类向基类,这里会发生切割
draw(yogi);//调用bear::draw()
draw(franny);//调用zooanimal::draw()
//如果franny的vptr还和上面一样被设定成指向bear的虚函数表就会出错
//因为都将指向bear::draw(),而实际上他本应该指向zoanimal::draw()
//因此,在此时的拷贝构造时,franny的vptr呗重设了,指向了zooanimal的虚函数表,而非bear的了
Data Member
空类占据几个字节?那么空类的继承类呢?
一个空类占据一个字节 空类也可能需要生成实例对象 但如果是空的,就没有任何标记用于区分不同实例了 因此,分配给空类一个字节的空间 当实例化成对象时,这一个字节的内存就用于区分不同的空类实例
class empty
{
};
empty test;
cout<< sizeof(test); //1byte
那么继承之后的空类,占据几个字节呢 VS中的编译器是有优化的 通常在类和结构体中,都会进行字节对齐(边界调整alignment) 如下,如果进行字节对齐,那输出就是8
class test
{
char a;
int b;
}T;
cout<<sizeof(T);//8
那么继续讲 有两种编译器 一种编译器,在继承之后,会将空基类中的那个字节优化掉 上面讲到,这一字节是作用为了区分空类的不同的实例对象 下面B,已经有了一个int 可以区分不同实例了 因此空基类添加的那一个字节也就失去了意义,可以优化删除掉 因此只占位一个int,输出4 另一种编译器,则不会进行这样的优化 则根据字节对齐,就是8
虚继承为什么也是4字节呢? 因为虚继承会有一个虚函数表 他是一个指针,4字节 因此C,D也是4 若不优化,则就是8
而在E双继承C,D之后 因为相同的基类不会同时出现在继承类中 因此,只有一个A的空类一字节 CD的两个指针4+4字节 此处也不需要这一个字节来区分实例,因此E就是8字节 如果没有优化,加上字节对齐,就是4+4+1+(3)=12
class A
{
}a;
class B :public A
{
int a;
}b;
class C :public virtual A
{
}c;
class D :public virtual A
{
}d;
class E :public C , public D
{
}e;
int main()
{
cout << "a:" << sizeof(a) << endl;
cout << "b:" << sizeof(b) << endl;
cout << "c:" << sizeof(c) << endl;
cout << "d:" << sizeof(d) << endl;
cout << "e:" << sizeof(e) << endl;
system("pause");
return 0;
}
Data Member 的绑定
需要注意的点!!!
一定要将nested type声明,放在类的起始处!!
所以可以直接养成习惯,先数据,后函数?
我们直觉认知,肯定是用的类内的length 但是结果却出乎意料
typedef int length;
class point3d
{
public:
void mumble(length val) { _val = val; }
length mumble() { return _val; }
void out(length _val) { cout << "_vla:" << sizeof(_val) << endl; }
length _len;
private:
typedef double length;
length _val;
};
int main()
{
point3d point;
point.out(1);
cout << "len:"<<sizeof(point._len)<<endl;
system("pause");
return 0;
}
typedef int length;
class point3d
{
private:
typedef double length;
length _val;
public:
void mumble(length val) { _val = val; }
length mumble() { return _val; }
void out(length _val) { cout << "_vla:" << sizeof(_val) << endl; }
length _len;
};
int main()
{
point3d point;
point.out(1);
cout << "_len:"<<sizeof(point._len)<<endl;
system("pause");
return 0;
}
Data Member 的布局
标准规定,在同一个区段中(public,private,protect) 成员的排列只需符合,较晚出现的成员在类对象中有较高的地址即可 而不必非得连续。 为什么不连续呢?还有什么能介于类成员变量之间呢?前面讲的字节对齐 在含有虚函数/继承虚类的类中,还需要一个指针,指向虚函数表,这个是编译器自己合成的 一般虚函数表存在于前面那些成员的总体的最前或最后
Data Member 的存取
static member
静态变量,是属于类的,而不是属于对象的 不论实例化多少个对象,静态变量只有一个,存储在静态存储区,而不是每个对象中 因此,不论经过了多么复杂的继承,或者什么 对于静态变量的访问都是不变的,静态存储区访问
nonstatic member
每个nonstatic member在对象中的偏移位置,在编译期间就可获知 甚至如果member属于一个派生类或者多重继承串也是一样的 因此存取一个nonstatic member其效率和存取一个非继承类的member是一样的
继承与Data Member
class point2d
{
private:
float x;
float y;
};
class point3d
{
private:
float x;
float y;
float z;
};
//他们的布局结构,和struct一样
---
float x
float y
---
---
float x
float y
float z
---
只继承不多态
class concrete
{
private:
int val;
char c1;
char c2;
char c3;
};
//实例化对象后的分布
---
1 int val
2
3
4
---
1 char c1
---
1 char c2
---
1 char c3
---
1 padding
---
//但是如果这样写呢?
class concrete1
{
private:
int val;
char c1;
};
class concrete2 : public concrete1
{
private:
char c2;
};
class concrete3 : public concrete2
{
private:
char c3;
};
//分布
//因为需要字节对齐和边界对齐
---
1 int val
2
3
4
---
1 char c1
2 padding
3 padding
4 padding
---
1 char c2
2 padding
3 padding
4 padding
---
1 char c3
2 padding
3 padding
4 padding
---
加上多态
单一继承
多态就是多了虚函数 因此只在成员对象的后面添加了一个指向虚函数表的指针
内存分布
class point2d
{
public:
//...虚函数
protected:
float _x,_y;
};
class point3d : public point2d
{
public:
//...虚函数
protected:
float _z;
};
//分布,一般虚函数表指针放在数据成员后
---
float _x
float _y
vptr_point2d
float _z
----
//前三行是2d的范围
//全体的,是3d的范围
//我们可以发现,继承类和基类的开始地址是相同的
//其差别只是继承类比继承大一些,因为多了成员对象
多重继承
//代码接上
class vertex
{
public:
//..虚
protected:
vertex *next;
};
class vertex3d: public pointed3d, public vertex
{
public:
//..无虚
protected:
float mumble;
};
对于多重派生对象,将其地址指定最左端的基类指针,此处就是point3d 这时的情况将和前面的单一继承相同 不过vertex的地址,就需要单独修改,需要便宜介于中间的类对象的大小
vertex3d v3d;
vertex *pv;
point2d *p2d;
point3d *p3d;
pv = &v3d;
//这个指定的内部转化
//pv = (vertex*)(((char* )&v3d) + sizeof(point3d));
p2d = &v3d;
p3d = &v3d;//这些就属于单一继承的情况,无需转化
//如果有指针如下;
vertex3d *pv3d;
vertex *pv;
//那么如下操作
pv = pv3d;
//不能简单的转换
/pv = (vertex*)(((char* )&v3d) + sizeof(point3d));
//这里为了防止v3d为0的情况,所以需要修改为:
/pv = pv3d
? (vertex*)(((char* )&v3d) + sizeof(point3d))
: 0;
//分布
---
1 float _x
2 float _y
3 vptr_point2d
4 float _z
5 vertex* next
6 vptr_vertex
7 float mumble
---
1-3为point2d
7-4为point3d
5-6为vertex
1-7为vertex3d
Function 语义学
构造,析构,拷贝语义学