一:关于变量的存储分配问题
变量是内存空间的一种抽象,程序中定义的每个变量在程序运行时都应该有与之对应的内存空间。但是,何时给变量分配空间以及相应空间分配在哪里,这要视变量的性质而定。通常把程序运行时一个变量占有内存空间的时间段称为该变量的生存期。
C++把变量的生存期分为静态、动态和自动三种。对于具有静态生存期的变量,他们的内存空间从程序开始执行时就进行分配,直到程序结束才收回他们的空间,全局变量具有静态生存期。对于具有自动生存期的变量,他们的内存空间在程序执行到定义它们的复合语句(包括函数体)时才分配,当定义它们的复合语句执行结束时,它们的空间将被收回,局部变量和函数的参数一般具有自动生存期。对于具有动态生存期的变量,其内存空间用new操作分配、用delete操作回收,动态变量具有动态生存期。
另外,在定义局部变量时,可以为它们加上存储类修饰符auto、static或register来指出它们的生存期。局部变量的默认存储类为auto,即定义局部变量时,如果未指定存储类,则其存储类为auto。定义为auto存储类的局部变量具有自动生存期;定义为static存储类的局部变量具有静态生存期;定义为register存储类的局部变量也具有自动生存期,它与auto存储类的局部变量的区别是:register存储类是建议编译程序把相应的局部变量的空间分配在CPU的寄存器中,其目的是为了提高对局部变量的访问效率。值得注意的是:具有register存储类的局部变量的存储空间可能在CPU的寄存器中,也有可能在内存中,这要有编译器根据CPU寄存器的使用情况来决定。
static存储类的局部变量的作用是:它能在函数调用时获得上一次调用结束时该局部变量的值,即static存储类使得某些局部变量的值能在函数多次调用之间得以保留。例如:
#include <iostream>
using namespace std;
int z = 0;
void f()
{
int x = 0;
static int y = 0;
x++;
y++;
z++;
cout << "x=" << x << "y=" << y << "z=" << z << endl;
}
int main()
{
f();
z++;
f();
return 0;
}
输出结果:x=1,y=1,z=1
x=1,y=2,z=3
值得注意的是:如果在一个static存储类的局部变量定义中给出了初始化,则该初始化只在函数的第一次调用时进行,以后的调用中不再进行初始化,它的值为上一次函数调用结束时的值。虽然全局变量也能起到static存储类局部变量的作用,但static存储类的局部变量能受到函数封装的保护。例如,在上述程序的函数main中可以使用全局变量z,但不能使用函数中定义的局部变量y。
二:程序运行时在内存中存放位置:
当一个程序准备运行时,操作系统为其分配一块内存空间,其中包括四个部分:静态数据区(static data)、代码区(code)、栈区(stack)和堆区(heap,或称自由存储区,free store)。
在程序的内存空间中,静态数据区用于全局变量、static存储类的局部变量以及常量的内存分配;代码区用于存放程序的指令,对c++程序而言,代码区存放的是所有函数代码;栈区用于auto存储类的局部变量、函数的形式参数以及函数调用时有关信息(如函数返回地址等)的内存分配;堆区用于动态变量的内存分配。
在c++中,如果定义一个变量时没有进行初始化,则对于全局变量和static存储类的局部变量,编译程序将隐式地自动把他们按位模式初始化为0;对于其他变量,编译程序不会对它们进行初始化。对变量进行显示初始化是一种良好的习惯。
静态数据区和代码区的大小是固定的,而栈区和堆区的大小将会随着程序的运行不断的变化,不过,操作系统通常会对程序的栈区和堆区空间的最大值有一定的限制。
三:为什么要重载new与delete
当用操作符new和delete来创建和撤销动态对象时,操作符new和delete将调用系统的通用堆存储管理来进行动态内存的分配与归还。对于频繁地在堆空间中创建和撤销某一类对象的程序,系统的堆内存管理的效率往往不高,这是因为系统的堆存储管理要考虑各种大小的堆的内存的分配与归还。特别地,系统的堆存储管理会面临“碎片”问题,即经过多次分配与归还操作之后,在系统的堆空间中可能会出现很多很小的自由空间,夹插在已分配的空间之间。对于一个新的分配空间请求,有时这些自由空间中每一个的大小都不能满足要求,但它们的总和能满足。这时,为了能够进行存储分配,系统的堆存储管理往往要对已分配的空间进行移动以实现把较小的自由空间合并为较大的自由空间,而这种存储空间的移动操作需要花费大量的时间,从而影响存储分配的效率。默认的分配器失败时会抛出异常, 或许你想改变这种行为。
四:Demeter法则
对于过程式程序设计泛型,结构化程序设计技术提供了一种良好的程序设计风格。那么,良好的面型对象程序设计风格是什么呢?
大多数面向对象语言都提供了支持面向对象程序设计的机制,使用这些机制能够方便的编写出面向对象的程序。但这并不表明只要使用这些面向对象语言所提供的机制就能编写出良好的、符合棉线对象思想的程序,如果不加约束,编写出的程序仍然不会是面向对象的。
封装和继承是面向对象程序设计的两个主要特征,它们在两个层次上(实例和类)强调了模块的可复用性和易维护性,而模块的可复用性和易维护性的关键在于降低模块之间的耦合度。良好的面向对象程序设计风格的目标应是降低模块间的耦合度、增强模块的可复用性与易维护性。
在面向对象程序设计中,模块间的耦合反映在对象类之间成员函数的相互调用上。要降低模块间的耦合度,应该对成员函数中能访问的对象或对象类的集合做一定的限制,并尽量使该集合最小,从而降低成员函数对环境的依赖,这个要求被称为Demeter法则。Demeter法则的基本思想是:一个类的成员函数除了能访问自身类结构的直接子结构外(表层子结构),不能以任何方式依赖于任何其他类的结构;并且每个成员函数只应对某个有限类集合中的对象发送消息。其中,“自身类结构的直接子结构”是指本类的数据成员。如果本类的数据成员是成员对象,则成员对象类的数据成员不包含在内,因为,对本类而言,成员对象类的数据成员属于“深层子结构”。Demeter法则可以形象的描述成:“仅与你的直接朋友交谈”。
Demeter法则存在两种表达形式:类表达形式和对象表达形式。
1.Demeter法则的类表达形式(L1)
对于类C中的任何成员函数M,M中能直接访问或引用的对象必须属于下述类之一:
a.类C本身。
b.成员函数M的参数。
c.M或M所调用的成员函数所创建的对象的类。
d.全局对象所属的类。
e.类C的成员对象所属的类。
2.Demeter法则的对象表达式形式(L2)对于类C中的任何成员函数M,M中能直接访问或引用的对象必须属于下述对象之一:
a.this指向的对象。
b.成员函数M的参数对象。
c.M或M所调用的成员函数所创建的对象。
d.全局变量中包含的对象。
e.C类的成员对象。
其中L1法则适合静态类型的面向对象语言(如C++),它在编译是检查,而L2法则适合动态类型的面向对象语言(如Smaltalk),它需要在运行时检查。
五:用指针参数来实现泛型函数
比如说要编写一个排序函数能对任意的数组进行排序,我们可以把要排序的数组的首地址作为void *类型传递给排序函数,并且,还要把数组元素的个数和元素的尺寸(占类存字节数)以及如何比较两个数组元素的大小告诉该排序函数:
void sort(void *base, //需排序的数据首地址
unsigned int count, //数据元素的个数
unsigned int element_size, //数据元素的尺寸
int (*cmp)(const void *,const void *) //比较两个数据元素大小的指针)
{
不论采用何种排序算法,一般都需要对数组进行以下操作:
1. 取第i个元素
(char *)base + i*element_size
2. 比较第i个和第j个元素的大小。
(*cmp)( (char *)base + i*element_size, (char *)base + j*element_size )
3. 交换第i个和第j个元素
char *p1=(char *)base+i*element_size,
*p2=(char *)base+j*element_size;
for(int k=0; k<element_size; k++)
{
char temp=p1[k];
p1[k] = p2[k];
p2[k] = temp;
}
}
int int_compare(const void *p1, const void *p2)
{
if(*(int *)p1 < *(int *)p2)
return -1;
else if(*(int *)p1 > *(int *)p2)
return 1;
else
return 0;
}
int double_compare(const void *p1, const void *p2)
{
if(*(double *)p1 < *(double *)p2)
return -1;
else if(*(double *)p1 > *(double *)p2)
return 1;
else
return 0;
}
int A_compare(const void *p1, const void *p2)
{
if(*(A *)p1 < *(A *)p2)
return -1;
else if(*(A *)p1 > *(A *)p2)
return 1;
else
return 0;
}
int a[100];
sort(a, 100, sizeof(int), int_compare);
double b[200];
sort(b, 200, sizeof(double), double_compare);
A c[300];
sort(c, 300, sizeof(A), A_compare);
用指针实现泛型函数的不足之处在于:需要定义额外的参数并且要进行大量的指针运算,这不仅使得实现起来比较麻烦,而且使得程序易读性差和容易出错。另外,用指针实现泛型函数也不便于编译程序进行类型检查。