数组是一种简单的数据结构,用来在一块连续的内存空间中存储多个相同类型的变量。数组名和数组第一个元素的地址都是这块内存空间的首地址,要访问数组中的元素可以使用”数组名[索引]”的形式,也可以使用”*(数组名+索引)”的形式。索引从0开始。比如:
int a[10] = {1, 2, 3, 4};
int c = (int)a;
int d = (int)&a[0];
int e = (int)&a;
int f = a[3]; //取第4个数字等于4
int g = *(a + 3); //a在这个时候已经退化为指针了,指向数组分配的内存的首地址,a + 3是将指针向后移动3个数组所存的变量所占的内存字节,本例中为3×4,12个字节,而不是3个字节。
c = d = e,f = g。
关于二维数组或多维数组,c/c++环境下在内存中是以行优先的顺序排列的,即一行一行的排列。比如:
int a[10][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int c = (int)a;
int d = (int)&a[0];
int e = (int)&a[0][0];
int f = (int)&a;
int g = a[2][1];
int h = *(*(a + 2) + 1);
c = d = e = f。g = h。对于二维数组a[i][j],a[i]就是第i行所分配内存的首地址。
指针是一种变量,只是保存的是内存地址(变量所占内存的首地址,函数代码段的入口地址等)。要分清楚的就是指针所指向的地址和指针本身的地址(指针作为变量,要占内存,也就有地址)。一般的指针占4个字节,在c++中由于继承和虚拟的原因也可能出现超过4个字节的指针。
比如我们有如下定义:
int *pI = NULL;
我们说pI是个指针,也许是我们一听到指针就想到地址,而把pI是个变量这一点忽略了,其实pI就是个变量,这个变量的类型是指向int型数据的指针类型,变量的值是一个int型数据在内存中的地址。既然pI是个变量,那它也占内存,它所占内存的地址就是指针pI本身的地址,显然与pI的值所代表的地址是不一样的。
当指针指向数组或字符串或一块内存区域时,通过指针去获取数组中的数据或字符或内存中的某种类型的数据(这种类型也是指针指向的数据的类型)也可以采用二中方式,一种是”*(指针名 + 索引 )”,一种是”指针名[索引]”。索引从0开始。
char *pC = "ABCD";
char c = pC[2]; //取第3个字符'C'
int *pI = (int *)malloc(sizeof(int) * 4); //pI指向一块内存,这块内存用来存放int型数据
pI[0] = 1; //设置第一个int元素为1
*(pI + 1) = 2; //设置第二个int元素为2
free(pI);
需要记住的一点是在对指针做加操作时,地址是以指针指向的数据类型所占的内存字节为单位跳跃的。
int a[10];
char b[10];
int *pA = a + 3; //pA指向a后3×4=12个字节的地址,因为int一般占4个字节
char *pB = b + 3; //pB指向b后3×1=3个字节的地址,因为char一般占一个字节
如果想在函数中将对实参的改变持续到函数的作用域外,那就得传实参的地址,如果实参是指针,那就传指针的地址,也就是指针的指针。在c++下还也可以使用传引用来达到这样的目的,在讲引用的时候会讲。比如:
int a = 10;
int b = 20;
int *c = &a; //指针c指向a
int e = (int)&c;
ChangePointData(&c, &b); //想通过函数改变指针c的指向,需要传c的地址
int f = (int)&c;
void ChangePointData(int **p, int *data) //第一个参数类型为指针的地址,即指针的指针
{
*p = data;
}
我们通过函数改变指针c的指向,让c指向的b的地址,并让c在函数结束后仍然指向b的地址,那么传给函数的就必须是c的地址,形参类型也就是指针的指针类型。我们经常看到这样的代码,先在外部定义一个指针,然后通过一个函数在堆(关于堆和堆栈将在下一节讲)上分配一块内存,并让外部定义的那个指针指向这块内存,这个时候我们传给函数的必是外部这个指针的地址,因为我们要改变指针的指向。如果ChangePointData函数这样写:
int a = 10;
int b = 20;
int *c = &a;
int e = (int)&c;
ChangePointData(c, &b);
int f = (int)&c;
void ChangePointData(int *p, int *data)
{
p = data;
}
虽然在函数体内,看似改变了c的指向,实际上c的指向并没有变。这是因为c/c++的函数在调用过程中,实参是以值的形式传入函数的,也就是说函数操作的数据只是实参的一份拷贝,这个拷贝的值在函数的调用堆栈中,既然数据都不在同一块内存区域,函数的操作就根本不能影响实参了。并且函数结束,调用堆栈清空。为了达到修改的目的,我们只需要在函数体内获得要修改的变量的地址就行了,这样就可以对同一块内存区域进行操作,这也就是传指针的原因了。
写了这么多,没这位兄弟写的好写的全,大家还是去看这个链接吧,顺便把水滴石穿C语言系列全看了(强烈推荐),c就到一个高度了:
http://www.yesky.com/174/1864674.shtml
http://blog.csdn.net/canvashat/category/24547.aspx
现在看引用,以下全部摘至林锐的《高质量c++编程》。
引用是C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,n是m的一个引用(reference),m是被引用物(referent)。
int m;
int &n = m;
n相当于m的别名(绰号),对n的任何操作就是对m的操作。例如有人名叫王小毛,他的绰号是“三毛”。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以n既不是m的拷贝,也不是指向m的指针,其实n就是m它自己。
引用的一些规则如下:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
以下示例程序中,k被初始化为i的引用。语句k = j并不能将k修改成为j的引用,只是把k的值改变成为6。由于k是i的引用,所以i的值也变成了6。
int i = 5;
int j = 6;
int &k = i;
k = j; // k和i的值都变成了6;
上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
以下是“值传递”的示例程序。由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。
void Func1(int x)
{
x = x + 10;
}
…
int n = 0;
Func1(n);
cout << “n = ” << n << endl; // n = 0
以下是“指针传递”的示例程序。由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。
void Func2(int *x)
{
(* x) = (* x) + 10;
}
…
int n = 0;
Func2(&n);
cout << “n = ” << n << endl; // n = 10
以下是“引用传递”的示例程序。由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。
void Func3(int &x)
{
x = x + 10;
}
…
int n = 0;
Func3(n);
cout << “n = ” << n << endl; // n = 10
对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?
答案是“用适当的工具做恰如其分的工作”。
指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?
如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。
先说说实模式和保护模式http://www.china-askpro.com/msg38/qa23.shtml。
8086/8088的微机只有一种工作模式(即实模式)只能处理 1M以下的地址(16位),这种地址被城为实地址。后来Intel为了突破 1M的内存的限制,推出了386等芯片,增加了保护模式,在32位保护模式下,程序可以访问 4G内存空间。但同时为了同以前的程序保持兼容,所以旧程序在实模式下运行,而32位程序可以运行在保护模式下,从而最大地发挥服务器的能力。DOS是运行在实模式的,而Windows 9x/NT都是运行在保护模式的。CPU有专门的保护模式指令。
上面说了,386以后的cpu可以寻址 4G的内存空间,虽然我们的微机现在还没配置这么大的内存,为了编程的方便,在操作系统这级引入了虚拟存储机制,我们编写的程序在编译时使用的是虚拟的 4G地址空间(windows下实际是 2G,还有 2G由操作系统管理使用),程序的首地址是0。程序在执行时操作系统会实现虚拟地址到物理地址的映射,即地址重定位,现在的操作系统多采用段页式存储管理,以方便的进行存储保护,存储共享等,更多信息可以参考《操作系统概念 第六版》。
下面对堆和栈的讲解也不知道是那位兄弟写的了。
五大内存分区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)
明确区分堆与栈
在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
首先,我们举一个例子:
void f() { int* p=new int[5]; }
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到 4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是 1M(好像是,记不清楚了)。当然,我们可以修改:
打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?
动态分配由alloca函数进行分配,calloc分配的是堆内存。
内存管理还是看林锐的《高质量c++编程》吧。
c的结构中不能有方法,c++的结构和类功能上一样(实现封装,可以继承),只是结构中默认的成员变量或方法是公有的,而类默认的是私有的。很多c的结构体通过宏和函数指针去模拟c++的结构从而实现面对对象,brew就是这么做的。
虚函数的主要作用是为了实现多态,多态的意思是当基类的指针指向派生来时,使用这个指针调用的成员函数是派生类的成员函数。
一个类打算做基类的话,它的析构函数要的虚拟的,这样delete基类指针时才会调用派生类的析构函数。
其他还是看附带的文档《c++中的虚函数》。
五 C与C++程序的链接
本节内容完全取至小挺之家blog,他说的很清楚了。
http://blog.codelphi.com/tingxx/archive/2004/10/13/25294.aspx
它们之间的连接问题主要是因为c c++编绎器对函数名译码的方式不同所引起的,考虑下面两个函数:
/* c*/
int strlen(char* string)
{
...
}
//c++
int strlen(char* string)
{
...
}
两个函数完全一样。在c在函数是通过函数名来识别的,而在C++中,由于存在函数的重载问题,函数的识别方式通函数名,函数的返回类型,函数参数列表三者组合来完成的。因此上面两个相同的函数,经过C,C++编绎后会产生完全不同的名字。所以,如果把一个用c编绎器编绎的目标代码和一个用C++编绎器编绎的目标代码进行连接,就会出现连接失败的错误。
解决的方法是使用extern C,避免C++编绎器按照C++的方式去编绎C函数,在头文件中定义:
extern "C" int strlen(char* string);
或
extern "C"
{
int strlen(char* string);
}
当C编绎器遇到extern "C"的时候就用传统的C函数编译方法对该函数进行编译。由于C编绎器不认识extern "C"这个编绎指令,而程序员又希望C,C++程序能共用这个头文件,因此通常在头文件中使用_cplusplus宏进行区分:
#if define _cplusplus
extern "C"{
#endif
int strlen(char* string)
#ifdefine _cplusplus
}
#endif