0%

一、C++类的静态成员

学过C++的都知道,静态成员是属于整个类而不是某个对象的,静态成员变量在内存中只存储一份共所有对象共用,在所有对象中都可以共享它。

静态成员的定义或声明需要加个关键字 static。因为属于整个类,所以可以通过 <类名>::<静态成员名>来使用。

比较容易混淆和出错的地方在于类的静态成员变量和静态成员函数的使用,这两者都属于静态成员。

需要注意的是:静态成员属于整个类,在类对象实例化前,静态成员就已经分配空间了,而非静态成员无需初始化,必须在类实例化对象后才有内存空间,所以就有了个先后顺序:静态成员先于非静态成员存在。

下面我们来看看如下几个情况:

1、通过类名调用静态成员函数和非静态成员函数

阅读全文 »

前面探索了C++对象的内存模型,其中简单的涉及到了虚函数,作为C++实现其多态的一个重要机制,这里进一步探索下虚函数机制,以前也看过网络上关于虚函数机制的一些精彩的文章,但现在决定自己再分析这个虚函数机制以加深理解,看与自己动手探索还是有区别的。

\一、异质链表**

1、“is-a” 原理

在公有派生方式下,对派生类的对象里的基类子对象的水平访问与基类单独生成的对象的访问是一样的。也就是说,我们完全可以把public 继承方式的派生类的对象当做基类的一个对象来使用。反过来则不行。这就是“is-a”原理

在公有派生方式下,基于“is-a”原理,我们可以得出以下几点:

  1. 一个派生类的对象可以赋给基类对象;
  2. 派生类对象可以初始化基类对象;
  3. 派生类的对象可以初始化基类的引用;
  4. 派生类的对象的地址可以赋给指向基类对象的指针。

前面三条都是一个性质,派生类对象赋给基类对象的时候,都是调用基类的赋值构造函数,这样赋值的时候,只会将派生类对象中继承的基类成员赋值给基类对象成员,不会发生内存越界的情况。下面是一段截取的反汇编代码:

1
2
3
4
5
    60: 	base_obj = derived_obj;
00DD442C lea eax,[derived_obj]
00DD442F push eax
00DD4430 lea ecx,[base_obj]
00DD4433 call Base::operator= (0DD16AEh)

最重要的是第四条,对于这个基类的不同派生类的对象,我们可以使用指向基类的指针把它们(派生类对象)穿成一个链表,这个链表对于我们实现C++的多态性有很大的价值,被称之为异质链表。

img

通过基类指针将这个基类的不同子对象串联起来。一个充分必要条件就是,这几个子类都有一个公共的基类,且都与这个基类有“is-a”关系。

对于这我们可以这么理解:派生类的对象所占的存储空间要比基类的对象大,因为派生类继承了基类非私有成员函数和数据,再加上本身的成员数据,实例化的对象自然会比基类对象大,自然,派生类指针的寻址空间要比基类指针的寻址空间大。但由于对象的头部分是一样的,所以即使有超出基类指针所寻址的部分也能根据偏移量正确寻址,相反,如果派生类指针指向基类对象,则会把一部分不属于该基类对象的内存也包括进来,那么当派生类指针指向基类对象来使用派生类的函数的时候可能会发生严重的错误。

\二、虚函数机制前奏*

阅读全文 »

前面简单的论述过C++对象模型,总觉得不够深入,现近闲来进一步挖掘C++对象内存布局情况。主要讨论:单一继承,多重继承,钻石继承的有无虚函数以及虚拟继承的情况。贴出测试程序,并给出测试结论以及对应的类对象的大小计算。(PS:类对象的内存布局取决于编译器,这里的测试都是基于Visual Studio)

\单一的类对象**

单一的类对象主要考虑有虚函数的情况,前面提及的博文已有介绍,类中定义了虚函数,就会产生一个虚函数表(实质就是一个函数指针数组,虚函数表不在类中,VS编译环境下,虚函数表位于常量段,虚表指针在类对象中),类每定义一个对象,便会在对象的最前面安插一个虚表指针,指向虚函数表,这样该类定义的对象会多出4 Byte(32位)。

可以在Visual Studio C++ 编译输出中直接看C++内存布局:工程项目——右键“属性”——配置属性——C/C++——命令行——其他选项里添加“/d1reportAllClassLayout ”,即可在编译输出中查看定义的类的内存布局,上面是输出所有定义的类对象,你可以搜索你自己定义的对象。(最好不要把名字定为base,不然一大堆)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;

class parent
{
//public:
int b;
static int a;
virtual void fun1()
{
cout << "parent: fun1" << endl;

}

/*virtual void fun2()
{
cout << "parent: fun2" << endl;
}*/
};


int main()
{
parent obj_b;
return 0;
}

下面便是定义的类对象内存布局情况:vfptr 即表示虚表指针,static 成员不在类对象中

1
2
3
4
5
1>  class parent	size(8):
1> +---
1> 0 | {vfptr}
1> 4 | b
1> +---

如果类中不定义为虚函数,类对象的大小是4,如下:

1
2
3
4
1>  class parent	size(4):
1> +---
1> 0 | b
1> +---

下面我们来考虑类继承的情况:

\单一继承**

阅读全文 »

在C++对象模型中,Nonstatic data members 被配置于每一个 class object 之内,static data members 则被存放在个别 class object 之外。Static 和nonstatic function members 也被放在个别的class object 之外。存在 virtual function 时,每个class 会产生出一堆指向 virtual function 的指针,这些指针存放在一个表中,称之为虚函数表 virtual table。其实质就是一个函数指针数组,里面存放的是虚函数指针。每一个 class object 被安插一个虚表指针 vptr,指向相关的 virtual table,虚表指针通常置于 class object 的第一位置。另外,当子类虚继承父类时,子类对象中还会安插虚基类指针。注意此处 class 和 class object 的区别。如此说来一个类实例当中存放的就是 nonstatic data member,虚表指针(存在虚函数时或父类中含有虚函数),虚基类指针(虚拟继承时)。类继承时体积会变大,但在虚拟继承的情况下,基类永远只存在一个实例。

阅读全文 »

指针是一个特殊的变量,它里面存储的数值被解释成为内存(用户空间的虚拟内存)里的一个地址。

一、指针的属性

一个指针包含四个方面的内容:指针的类型、指针所指向的类型、指针所指向的内存区、指针本身所占据的内存区。

接下来就下面几个例子作说明:

1
2
3
4
5
1int *p;
2、 char *p;
3int **p;
4int (*p)[3];
5int *(*p)[4];12345

1.1、指针的类型

从语法的角度,只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型,即指针本身所具有的类型。

1
2
3
4
5
1int *p;        //指针的类型是 int*
2char *p; //指针的类型是 char*
3int **p; //指针的类型是 int**
4int (*p)[3]; //指针的类型是 int(*)[3]
5int *(*p)[4]; //指针的类型是 int*(*)[3]12345

然后有的人还是不知道怎么读…(后面再说)
插播:指针类型说明原则:从变量名处起,根据运算符优先级结合,一步一步分析。

阅读全文 »