关闭

【转】数组、指针、引用

1091人阅读 评论(0) 收藏 举报

数组是一种简单的数据结构,用来在一块连续的内存空间中存储多个相同类型的变量。数组名和数组第一个元素的地址都是这块内存空间的首地址,要访问数组中的元素可以使用数组名[索引]”的形式,也可以使用”*(数组名+索引)”的形式。索引从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×412个字节,而不是3个字节。

c = d = ef = 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 = fg = 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指向a3×4=12个字节的地址,因为int一般占4个字节

 

char *pB = b + 3;   //pB指向b3×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++中的概念,初学者容易把引用和指针混淆一起。一下程序中,nm的一个引用(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。由于ki的引用,所以i的值也变成了6

 

    int i = 5;

 

    int j = 6;

 

    int &k = i;

 

    k = j;  // ki的值都变成了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的引用,xn是同一个东西,改变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最小值为4Bytecommit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

 

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

 

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

 

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

 

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

 

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

 

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

 

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)

 

对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?

 

 

动态分配由alloca函数进行分配,calloc分配的是堆内存。

 

 

内存管理还是看林锐的《高质量c++编程》吧。

 

 

            结构、类

 

c的结构中不能有方法,c++的结构和类功能上一样(实现封装,可以继承),只是结构中默认的成员变量或方法是公有的,而类默认的是私有的。很多c的结构体通过宏和函数指针去模拟c++的结构从而实现面对对象,brew就是这么做的。

 

 

            虚函数、多态

 

虚函数的主要作用是为了实现多态,多态的意思是当基类的指针指向派生来时,使用这个指针调用的成员函数是派生类的成员函数。

一个类打算做基类的话,它的析构函数要的虚拟的,这样delete基类指针时才会调用派生类的析构函数。

其他还是看附带的文档《c++中的虚函数》。

 

            CC++程序的链接

 

本节内容完全取至小挺之家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++中,由于存在函数的重载问题,函数的识别方式通函数名,函数的返回类型,函数参数列表三者组合来完成的。因此上面两个相同的函数,经过CC++编绎后会产生完全不同的名字。所以,如果把一个用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"这个编绎指令,而程序员又希望CC++程序能共用这个头文件,因此通常在头文件中使用_cplusplus宏进行区分:

 

#if define _cplusplus
extern "C"{
#endif
int strlen(char* string)
#ifdefine _cplusplus
}
#endif

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:17687次
    • 积分:279
    • 等级:
    • 排名:千里之外
    • 原创:11篇
    • 转载:0篇
    • 译文:0篇
    • 评论:5条
    文章分类
    最新评论