- 浏览: 89100 次
- 性别:
- 来自: 深圳
文章分类
最新评论
-
flyingsir_zw:
导入weibosinaLib的时候遇到的问题,可以解决。方法很 ...
Android library projects cannot be launched -
oldend2012:
哎哟,还真的是这样,谢谢了。
Android library projects cannot be launched
数组是程序设计中是一个非常重要的概念。数组是一个用于收集大量类似数据的容器,
以及其每一个元素能被相同处理过程迭代来处理的一个抽象体。
创建数组一般有三种方式:全局/静态范围的数组,局部变量数组,申请堆空间来创建数组。
其中,全局/静态范围的数组,以及局部变量属于静态数组。
而申请堆空间来创建数组的属于动态数组。
a[7]与p1_a[7]是一样的么?
静态两维数组的排列顺序
动态两维数组的排列顺序
数组与指针
各种数组的声明方式
静态数组和动态数组在内存的组织方式
杂谈,不为人知的数组表达方式
数组的迭代与性能影响
a[7]与p1_a[7]是一样的么?
首先声明几个数组:
上述程序中,5个数组的在赋值的时候除了变量名以外几乎都是一模一样的,是不是他们的实现也一样了呢?
答案是否定的,动态数组和静态数组虽然在使用时看起来没有什么差别,但他们实现是不一样的。
反汇编看一下他们的代码。
数组类型 C/C++代码 汇编实现 简略说明
局部变量 a[7] = 0; MOV DWORD PTR SS:[EBP-C], 0 采用EBP在堆栈定位变量
[EBP - 28] a[0]
...
[EBP - 4] a[9]
静态局部变量 s_a[7] = 0; MOV DWORD PTR DS:[4C5E5C], 0 静态变量会被放到数据.data段中
全局变量 g_a[7] = 0; MOV DWORD PTR DS:[4C5E84], 0 全局变量和静态变量一样,
会被放到数据.data段中
数组指针
(malloc) p1_a[7] = 0; MOV EAX, DWORD PTR SS:[EBP-2C]
MOV DWORD PTR DS:[EAX+1C], 0
对于数组指针,要进行两次寻址
0x1C / 4 = 7
数组指针
(new) p2_a[7] = 0; MOV EAX, DWORD PTR SS:[EBP-30]
MOV DWORD PTR DS:[EAX+1C], 0
同上
根据汇编的结果可见,静态数组都采用了一次寻址,
而动态数组都是两次寻址,这和动态数组本身实现是有关系的。
静态数组的变量本身就是数组第一个元素的地址。
动态数组的变量存放的是一根指向到申请空间的首址指针。
静态两维数组的排列顺序
首先是静态数组,在C/C++中,其实没有正真的两维概念,
对于所有静态数组来说,都是一维的,考虑以下的例子
int a[35]和int b[7][5]
对于前者,就是包含35个int类型值的数组
而对于后者,可以理解为b是一个拥有7个数组类型的数组,
每个数组类型又是一个拥有5个int类型值的数组。
可以得出的结果就是两个数组在内存中的分布其实是一样的。
用简单的程序来验证一下:
如果对两维的概念比较清楚的话,再看两维的排列顺序就不难了
首先是一维的情况(在内存中a[0]和a[1]是连续的)
a[0] a[1] a[2] …… a[33] a[34]
接着是两维(在内存中b[0][0]和b[0][1]是连续的,b[0][4]与b[1][0]是连续的,但b[0][0]和b[1][0]不是连续的)
b[0][0] b[0][1] b[0][2] b[0][3] b[0][4]
b[1][0] b[1][1] b[1][2] b[1][3] b[1][4]
b[2][0] b[2][1] b[2][2] b[2][3] b[2][4]
b[3][0] b[3][1] b[3][2] b[3][3] b[3][4]
b[4][0] b[4][1] b[4][2] b[4][3] b[4][4]
b[5][0] b[5][1] b[5][2] b[5][3] b[5][4]
b[6][0] b[6][1] b[6][2] b[6][3] b[6][4]
以此类推,三维的情况下,首先是第三维的增长,然后再是第二维,最后才是第一维
动态两维数组的排列顺序
首先动态两维数组的创建方式有所不同,以下是经典的创建方法:
牵涉到两个堆地址:[EAX+n]和[ECX+n]
从内存连续性角度来分析的话,
pb[0][0]和pb[0][1]是连续的,
pb[0]和pb[1]也是连续的,
因为其中存放的不再是数组了,而是数组指针
但pb[0][4]和pb[1][0]不再连续
动态数组在内存中的分布大致如下,首先来看看一维:(pa[0]和pa[1]还是连续的)
pa
↓
pa[0] pa[1] pa[2] …… pa[33] pa[34]
两维的话就和静态数组有比较大的区别了:(连续性问题请参考上面的汇编分析)
pb
↓
pb[0] → pb[0][0] pb[0][1] pb[0][2] pb[0][3] pb[0][4]
pb[1] → pb[1][0] pb[1][1] pb[1][2] pb[1][3] pb[1][4]
pb[2] → pb[2][0] pb[2][1] pb[2][2] pb[2][3] pb[2][4]
pb[3] → pb[3][0] pb[3][1] pb[3][2] pb[3][3] pb[3][4]
pb[4] → pb[4][0] pb[4][1] pb[4][2] pb[4][3] pb[4][4]
pb[5] → pb[5][0] pb[5][1] pb[5][2] pb[5][3] pb[5][4]
pb[6] → pb[6][0] pb[6][1] pb[6][2] pb[6][3] pb[6][4]
数组与指针
在C/C++中,数组与指针的关系非常密切,
要清楚理解两者任一个的概念的话,不清楚了解另一个的概念是不可能的。
所以在这里阐述一下他们的关系。
从最简单的开始:
int a[10];
int *pa = a;
声明完这两个变量后,我们可以:
a[0] = 0;
同样我们可以:
*a = 0; // 一次寻址
*pa = 0; // 两次寻址
pa[0] = 0; // 两次寻址
对于*a和*pa来说,其实原型为:
*(a + 0)
*(pa + 0)
因为存在这样一个事实:
在指针上加上一个值后,并不是单纯在地址上加上该值,
而是表示一个偏移量,根据类型不同,偏移单位量也是不同的。
如果以上述例子来说的话,因为int在VC中是4个字节,
那么每+1,在地址上所加的是sizeof(int)也就是4个字节。
这样,就和数组的索引值就陪对了起来。即:
*a 等价于 *(a + 0) 等价于 a[0]
*(a + 5) 等价于 a[5]
从另一个方面来说的话,对于
pa = a;
也可以写为:
pa = &a[0];
同样,并不一定要指向索引为0的元素,
pa = a + 7;
pa = &a[7];
接下来考虑稍微复杂一点的静态两维数组:
int b[7][5];
int (*pb)[5] = b; // 声明和赋值写在同一行可能比较混乱,如果单独写的话,应该是 pb = b;
int (*pb1)[5] = b + 1; // 或者&b[1]
int *pvalue = (*b) + 1; // 或者&(*b)[1]
int *pvalue1 = (*pb) + 1; // 或者&(*pb)[1]
首先解释一下一个常见的问题:
int **pb = b;
如果这么写的话,编译器会不客气地扔出一个错误:
error C2440: '=' : cannot convert from 'int [7][5]' to 'int **'
猜想的原因可能是因为:
静态数组和动态数组的结构不同,如果同样采用int** 指针来引用的话,
在访问具体元素的时候,会产生二义性(指针并不知道引用的数组类型)
而一旦明确了声明后,对指针的操作时采用两次*取值操作就不会受到太大阻碍。
以下几种写法都是等价的:
b[1][1]
pb1[1][1]
*(b[1] + 1)
*(pb1[1] + 1)
*(*(b + 1) + 1)
*(*(pb1 + 1) + 1)
唯一被编译器隐藏的区别就是,与b相关的操作一次寻址,而pb1每次都是两次寻址。
然后看看那个声明就比较混乱的pb
在声明中,指明了它是一个指针,指向一个含有5个元素的数组。
回忆一下第二节.静态两维数组的排列顺序中一句话:
而对于后者,可以理解为b是一个拥有7个数组类型的数组,
每个数组类型又是一个拥有5个int类型值的数组。
两边要表达的意思是一致的,所以,pb的值可以为&b[0] ~ &b[6]中任一个。
接着我们又声明了一个int指针,用来接具体的值,
由于pb固定了b的第一维索引值,所以对其解引用(即*操作),
操作层从第一维转换到了第二维上,所以我们可以用&(*pb)[1]的形式,
或者(*pb) + 1的形式来获得某个元素的指针。
对于动态两维数组,因为其本身声明就是int **pb,所以也不存在什么编译器报错的问题。
而且对其做两次解引用(*求值操作),也确实根据地址求了两次值。
如果算上指针本身是地址,需要间接引用的话,所以一共是做了三次间接引用。
各种数组的声明方式
对于数组的声明,以及在函数参数中的声明,经常会被编译器拦截,
被告知声明不符合要求,所以经常会摸不着头脑最后改变数组的形式。
其实每种声明都可以通过语法实现,只不过因为数组的特殊性,
所以要说明数组的语法通常都比较复杂一点罢了。
下表是对各种情况做了一下整理:(假设对函数的调用为func(a))
数组声明 说明 函数参数声明 备注
int a[10]; 一维静态数组 void func(int [10]);
void func(int []);
void func(int *);
数组长度是否明示对函数参数没有影响,
即使void func(int [100]);这样的声明,
还是能把a作为参数传入,最多某些严肃的编译器抱怨一个警告而已
int *pa 一维数组指针
(静态和动态皆可)
void func(int [10]);
void func(int []);
void func(int *);
数组指针和一维数组之间的区别很小,
唯一区别就是,数组作为参数就是把它自己PUSH进栈,
而指针的话需要间接引用一次,把取得的值PUSH进栈。
int b[7][5] 两维静态数组 void func(int [7][5]);
void func(int [][5]);
void func(int (*)[5]);
两维静态数组的申明方式基本上和一维的差不了多少,
但要注意的是必须指定第二维的大小。
道理很简单,数组在第一维上移动的单位长度必须确定,
比如a[3] -> a[4]移动的距离必定是sizeof(int [5])。
所以,也是为什么第二维必须固定,而不是第一维的原因。
int (*pb)[5] 两维静态数组指针 void func(int [7][5]);
void func(int [][5]);
void func(int (*)[5]);
和一维数组指针基本上一样,区别点也相同。
int **pb 两维动态数组指针 void func(int **);
可怜的两维动态数组指针只有一种调用方式:-(
int c[9][7][5] 三维静态数组 void func(int [9][7][5]);
void func(int [][7][5]);
void func(int (*)[7][5]);
尝试一下声明三维的数组?虽然不常用,呵呵
int (*pc)[7][5] 三维静态数组指针 void func(int [9][7][5]);
void func(int [][7][5]);
void func(int (*)[7][5]);
别眼花了~
int ***pc 三维动态数组指针 void func(int ***);
三维动态数组指针也好可怜=v=
静态数组和动态数组在内存的组织方式
前面也提到过,静态数组在栈内或者在数据段.data内分配空间,
而动态数组在堆内分配空间。下面从内存角度来说明各自的特点。
全局变量数组/静态局部变量数组
这两个数组的生命期都和程序一样长,因为它们的空间是程序运行前就指定完毕的,
在程序执行过程中,也是通过直接寻址,地址一已经硬编码写在了程序代码中。
因为运行前就被分配了空间,所以所有元素的初始值被设为0。
有一种特殊的全局变量数组——字符串
他们是一种末尾必定以'/0'结尾的特殊的char型数组。
虽然也属于全局变量数组,但他们被放在了只读的.rdata段中,
放在只读段中是因为他们是不应该被程序的错误代码而改写的。
看看下面的例子:
char* c1 = "abcde";
char c2[] = "abcde";
c1[0] = 'b'; // 错误
c2[0] = 'b'; // 正确
c1是指针,指向一个全局的字符串数组
c2是一个局部变量数组
当c1想改写全局字符串数组中某个元素的值的时候,
会发生Access Violation的异常,因为在该内存区的数据全是只读的。
局部变量数组
局部变量数组的空间是从栈上被分配出来的。
局部变量数组的生命周期只和函数一样长,当然如果声明在某个循环里的时候,
虽然堆内预留的空间还是存在的,但编译器可能会把这个空间用于其他不和这个变量冲突的变量。
这样做的目的是为了节省堆栈空间。所以,在循环内的数组变量声明周期也就到循环的结束。
在栈上分配空间的时候,由于PUSH的效率很低,特别是遇到数组这种大量数据集合的时候,
所以编译器一般直接通过移动ESP指针来达到分配空间的目的。但编译器做的仅仅是移动指针,
而分配出来的空间中有些什么垃圾数据,编译器是不会去管的,这个C/C++本身实现目标有关系,
C/C++是为了性能而设计出来的一种语言。
它的目标是做最少的事情,用最少的时间完成最多的工作,
所以,初始化内存的责任就转移到了程序员的身上,
另外不仅分配内存如此,从很多方面都能感受到C/C++的这个思想。
栈中的数据不单单有普通变量,数组变量,还有比如返回地址,栈帧,SEH链表,调用参数都在这块空间内。
其中最重要的是返回地址。
为什么这么说,首先介绍一个概念——栈溢出。
栈溢出就是因为不当的操作,把值写到了不应该写的栈地址上面。
而返回地址决定了程序执行的方向,一旦返回地址被修改,导致程序跳到了其他地方,
首先是程序出错这是肯定的,此外由于操作系统最终捕获到错误会利用堆栈的返回地址链,
打印出栈中函数的调用关系,如果返回地址都没有了的话,那就不可能追踪最末层的函数,
也就会给调试带来的一定的困难。
说了那么多,其实反过来想说明的问题是,局部变量数组是一个很容易导致堆栈溢出的变量。
因为数组会有循环迭代,一旦没有控制住,就会越界,越界就有可能发生堆栈溢出。
关于这点,在下一节杂谈,不为人知的数组表达方式中有简略的实例演示。
动态数组(数组指针)
动态数组虽然也有局部的——它的指针,
但一旦new过了之后,它所指向的实际分配空间是在堆里面的。
在堆中申请空间有两种方式:C的malloc和C++的new。
对于基本数据类型的数组来说,这两种申请空间的方式没有什么太大的区别。
int *pa, *pb;
pa = (int *)malloc(sizeof(int) * 10); // 正确
pb = new int[10]; // 正确
但是,如果类数组的话,一定要用new来分配空间,而不是malloc。
MyClass *pa, *pb;
pa = (MyClass *)malloc(sizeof(MyClass) * 10); // 错误
pb = new MyClass[10]; // 正确
采用malloc调用的只是分配了一块sizeof(MyClass) * 10大小的空间,其他什么事情都没做。
采用new调用,不但分配了空间(自动计算),而且还调用了每个MyClass的构造函数。
对于类来说,构造函数是很重要的,如果没有调用构造函数而使用该类变量的话,可能会出现预想不到的结果。
同样,在用new []申请空间后,需要用delete []释放空间。
为什么不是delete,而是delete []?
对于基本数据类型的数组来说,delete只释放了pa[0]的空间,而delete []正确地释放了所有的空间。
对于类的数组来说,delete只调用了pa[0]的析构函数,并是放了空间,
而delete []调用了所有元素的析构函数,并且正确地释放了所有的空间。
如果有疑问的话,这样一段小小的程序会告诉你答案的:-)
代码 输出
杂谈,不为人知的数组表达方式
先看程序=v=
int i = 4;
int a[10];
int j = 5;
int b[7][5];
0[a] = 6;
9[a] = 7;
0[b][0] = 1;
0[1[b]] = 2;
0[b[2]] = 3;
cout << (-1)[a] << endl;
看完之后,有什么想法么?
多数人可能会认为这个东西还是C/C++么?
不过很可惜……
这段程序在M$的编译器中编译成功。
为什么这样?!可能有人要疯了。
慢,其实这个表达方式能够被认可,和第四节数组与指针中的概念有关。
*a 等价于 *(a + 0) 等价于 a[0]
*(a + 5) 等价于 a[5]
有这样一个事实:
*(a + 0) 和 *(0 + a)要表达的东西是一样的。
所以可以推导出:
a[0] 等价于 *(a + 0) 等价于 *(0 + a) 等价于 0[a]
所以不难可以推导出二维的表达方式:
b[0][1] = *(b[0] + 1) = *(1 + b[0]) = 1[b[0]]
b[0][1] = *(b[0] + 1) = *(*(b + 0) + 1) = *(*(0 + b) + 1) = *(0[b] + 1) = 0[b][1]
b[0][1] = *(b[0] + 1) = *(*(b + 0) + 1) = *(*(0 + b) + 1) = *(1 + *(0 + b)) = *(1 + 0[b]) = 1[0[b]]
从这个例子可以看出,数组和指针有着非常亲密的关系^^
好,让我们喘一口气,怪谈还没完呢~
看看最后一个cout输出语句:
cout << (-1)[a] << endl;
(-1)[a]根据刚才的知识我们可以得出: (-1)[a] = a[-1] ……等等,-1?编译器怎么没报错??
在C/C++里当然不会报,因为报了就违背C/C++的设计目标了——多管不用管的
那么再把这个式子转换一下,可以得到*(a - 1)
按照指针的概念,也就是前一个元素的东西,
因为a[0]是数组头了,-1会指向哪里呢?猜猜看~
或许有人会说是i,因为按照书写的顺序,i在数组a上面。
但代码书写并非编译器的实现,根据编译器的实现,还有有没有开启优化,结果是不一样的
按照我目前手头的编译器(见文章的开头),采用DEBUG模式编译出来,输出的结果是5
也就是指向了j。
在80x86体系结构,Windows系列操作系统下,栈在内存中从大地址->小地址方向扩展。
换句话说,栈底在大地址,栈顶在小地址。
这样,压入参数的时候,i被压在了大地址上,也就是下方,
然后数组a压在i的上方,因为数组的顺序还是要沿着地址增长的方向扩展
所以a[0]在上方,a[9]在下方,
然后j又压在了a[0]的上方,所以a[-1]也就是j了。
再换句话说,这也就是所谓的栈溢出!
列出该程序执行该段程序时,栈的情况的话:
0012FEE8 j = 5
0012FEEC a[0] = 6
0012FEF0 a[1] = ?
~
0012FF0C a[8] = ?
0012FF10 a[9] = 7
0012FF14 i = 4
0012FF18 保存的EBP
0012FF1C 返回地址
刚才我们在a[-1],也就是j,现在我们把矛头转一下,直接对准返回地址。
按照栈的情况来看,返回地址用数组a来解释就是a[12],稍微修改一下程序。
在函数结束前增加一行代码:
a[12] = 0;
顺利通过编译,嗯
执行……!调试器跳出,说有Access Violation
看一下当前EIP指针,果然等于0
程序也就这么被栈溢出给抹杀掉了(其实是操作系统抹杀了该程序,栈溢出的行为应该属于借刀杀人=v=)
不过一般非故意的栈溢出不会这么有针对性,都是循环失控导致的,这样会把栈一路上的地址全部破坏掉。
所以一般那些检查栈溢出的代码(DEBUG模式下有些编译器会插入),在栈帧两端设防,
一旦设防标志被破坏就认为发生了栈溢出,所以大多数非故意的栈溢出还是能够通过特定代码监测出来的:-)
数组的迭代与性能影响
从简单的角度出发,我们考虑两维静态数组。
对于数组a[i][j],采用两层循环来迭代,应该把个索引放外面,哪个索引放里面?
如果从理论上来讲,我们知道m固定的情况下,j中的元素是连续的,
又因为高速缓存读入数据时候按块来读,所以尽量应该使最近访问的数据都在连续的内存上。
那么我们得出了答案,也就是i放外面,j放里面,写成程序就是:
#define MAX_I 1024 * 1024
#define MAX_J 1024 * 512
int a[MAX_I][MAX_J];
for (int i = 0; i < MAX_I; i++)
for (int j = 0; j < MAX_J; j++)
a[i][j] = 1;
但理论归理论,还是需要实践才是真理!
编写程序,对a[i][j]进行写入测试,测量工具采用CPU指令RDTSC
RDTSC
卷绕周期为172年
精度±0.001微妙(3.4GHz处理器)(受电源管理和乱序执行的影响)
秒转换:秒数 = 两次测试的差值 / 计算机CPU的频率
测试程序:
测试用机:
Intel(R) Core(TM)2 CPU T7400 2.16GHz 2.16GHz
2.00GB内存
次数 i外j内 j外i内
1 3182607649 29253016390
2 3224917293 27522755221
3 3160380392 27205374131
4 3147323816 27746559464
5 3159460317 27695442775
6 3150892875 27581611161
7 3166696546 27459197090
8 3134793389 27874196311
9 3161698709 27514023030
10 3205793422 27283634963
AVG 3169456440.8 27713581053.6
毫秒 1467ms 12830ms
通过测试可见,两种方式导致的性能差别几乎达到了10倍。
转自http://ikaruga.name/Technology/ccplusplus/staticAndDynamicArray.html
以及其每一个元素能被相同处理过程迭代来处理的一个抽象体。
创建数组一般有三种方式:全局/静态范围的数组,局部变量数组,申请堆空间来创建数组。
其中,全局/静态范围的数组,以及局部变量属于静态数组。
而申请堆空间来创建数组的属于动态数组。
a[7]与p1_a[7]是一样的么?
静态两维数组的排列顺序
动态两维数组的排列顺序
数组与指针
各种数组的声明方式
静态数组和动态数组在内存的组织方式
杂谈,不为人知的数组表达方式
数组的迭代与性能影响
a[7]与p1_a[7]是一样的么?
首先声明几个数组:
int g_a[10]; // 全局变量数组 int main(int argc, char** argv) { int a[10]; // 局部变量数组 static int s_a[10]; // 静态局部变量数组 int *p1_a, *p2_a; // 数组指针 // 为动态数组申请空间 p1_a = (int*)malloc(sizeof(int) * 10); p2_a = new int[10]; // 为数组赋值 a[7] = 0; s_a[7] = 0; g_a[7] = 0; p1_a[7] = 0; p2_a[7] = 0; // 释放空间,并且将指针置0 delete[] p2_a; free(p1_a); p1_a = p2_a = 0; }
上述程序中,5个数组的在赋值的时候除了变量名以外几乎都是一模一样的,是不是他们的实现也一样了呢?
答案是否定的,动态数组和静态数组虽然在使用时看起来没有什么差别,但他们实现是不一样的。
反汇编看一下他们的代码。
数组类型 C/C++代码 汇编实现 简略说明
局部变量 a[7] = 0; MOV DWORD PTR SS:[EBP-C], 0 采用EBP在堆栈定位变量
[EBP - 28] a[0]
...
[EBP - 4] a[9]
静态局部变量 s_a[7] = 0; MOV DWORD PTR DS:[4C5E5C], 0 静态变量会被放到数据.data段中
全局变量 g_a[7] = 0; MOV DWORD PTR DS:[4C5E84], 0 全局变量和静态变量一样,
会被放到数据.data段中
数组指针
(malloc) p1_a[7] = 0; MOV EAX, DWORD PTR SS:[EBP-2C]
MOV DWORD PTR DS:[EAX+1C], 0
对于数组指针,要进行两次寻址
0x1C / 4 = 7
数组指针
(new) p2_a[7] = 0; MOV EAX, DWORD PTR SS:[EBP-30]
MOV DWORD PTR DS:[EAX+1C], 0
同上
根据汇编的结果可见,静态数组都采用了一次寻址,
而动态数组都是两次寻址,这和动态数组本身实现是有关系的。
静态数组的变量本身就是数组第一个元素的地址。
动态数组的变量存放的是一根指向到申请空间的首址指针。
静态两维数组的排列顺序
首先是静态数组,在C/C++中,其实没有正真的两维概念,
对于所有静态数组来说,都是一维的,考虑以下的例子
int a[35]和int b[7][5]
对于前者,就是包含35个int类型值的数组
而对于后者,可以理解为b是一个拥有7个数组类型的数组,
每个数组类型又是一个拥有5个int类型值的数组。
可以得出的结果就是两个数组在内存中的分布其实是一样的。
用简单的程序来验证一下:
int a[35]; int b[7][5]; a[0] = 4; a[1] = 5; a[34] = 6; b[0][0] = 7; b[0][1] = 8; b[1][0] = 9; b[6][4] = 10; a[0] = 4; a[1] = 5; a[34] = 6; MOV DWORD PTR SS:[EBP-8C], 4 MOV DWORD PTR SS:[EBP-88], 5 MOV DWORD PTR SS:[EBP-4], 6 0x8C - 0x04 = 0x88 b[0][0] = 7; b[0][1] = 8; b[1][0] = 9; b[6][4] = 10; MOV DWORD PTR SS:[EBP-118], 7 MOV DWORD PTR SS:[EBP-114], 8 MOV DWORD PTR SS:[EBP-104], 9 MOV DWORD PTR SS:[EBP-90], 0A 0x118 - 0x90 = 0x88以上汇编的结果表明,两维数组的实现就是转化成了一维。
如果对两维的概念比较清楚的话,再看两维的排列顺序就不难了
首先是一维的情况(在内存中a[0]和a[1]是连续的)
a[0] a[1] a[2] …… a[33] a[34]
接着是两维(在内存中b[0][0]和b[0][1]是连续的,b[0][4]与b[1][0]是连续的,但b[0][0]和b[1][0]不是连续的)
b[0][0] b[0][1] b[0][2] b[0][3] b[0][4]
b[1][0] b[1][1] b[1][2] b[1][3] b[1][4]
b[2][0] b[2][1] b[2][2] b[2][3] b[2][4]
b[3][0] b[3][1] b[3][2] b[3][3] b[3][4]
b[4][0] b[4][1] b[4][2] b[4][3] b[4][4]
b[5][0] b[5][1] b[5][2] b[5][3] b[5][4]
b[6][0] b[6][1] b[6][2] b[6][3] b[6][4]
以此类推,三维的情况下,首先是第三维的增长,然后再是第二维,最后才是第一维
动态两维数组的排列顺序
首先动态两维数组的创建方式有所不同,以下是经典的创建方法:
int* pa; int** pb; // 申请空间 pa = new int[35]; pb = new int*[7]; for (int i = 0; i < 7; i++) { pb[i] = new int[5]; } // 赋值操作 pa[0] = 4; pa[1] = 5; pa[34] = 6; pb[0][0] = 7; pb[0][1] = 8; pb[1][0] = 9; pb[6][4] = 10; // 释放空间 delete[] pa; for (int i = 0; i < 7; i++) { delete[] pb[i]; } delete[] pb; 汇编分析 pa[0] = 4; pa[1] = 5; pa[34] = 6; MOV EAX, DWORD PTR SS:[EBP-11C] MOV DWORD PTR DS:[EAX], 4 MOV EAX, DWORD PTR SS:[EBP-11C] MOV DWORD PTR DS:[EAX+4], 5 MOV EAX, DWORD PTR SS:[EBP-11C] MOV DWORD PTR DS:[EAX+88], 6 两次寻址 pb[0][0] = 7; pb[0][1] = 8; pb[1][0] = 9; pb[6][4] = 10; MOV EAX, DWORD PTR SS:[EBP-120] MOV ECX, DWORD PTR DS:[EAX] MOV DWORD PTR DS:[ECX], 7 MOV EAX, DWORD PTR SS:[EBP-120] MOV ECX, DWORD PTR DS:[EAX] MOV DWORD PTR DS:[ECX+4], 8 MOV EAX, DWORD PTR SS:[EBP-120] MOV ECX, DWORD PTR DS:[EAX+4] MOV DWORD PTR DS:[ECX], 9 MOV EAX, DWORD PTR SS:[EBP-120] MOV ECX, DWORD PTR DS:[EAX+18] MOV DWORD PTR DS:[ECX+10], 0A三次寻址!
牵涉到两个堆地址:[EAX+n]和[ECX+n]
从内存连续性角度来分析的话,
pb[0][0]和pb[0][1]是连续的,
pb[0]和pb[1]也是连续的,
因为其中存放的不再是数组了,而是数组指针
但pb[0][4]和pb[1][0]不再连续
动态数组在内存中的分布大致如下,首先来看看一维:(pa[0]和pa[1]还是连续的)
pa
↓
pa[0] pa[1] pa[2] …… pa[33] pa[34]
两维的话就和静态数组有比较大的区别了:(连续性问题请参考上面的汇编分析)
pb
↓
pb[0] → pb[0][0] pb[0][1] pb[0][2] pb[0][3] pb[0][4]
pb[1] → pb[1][0] pb[1][1] pb[1][2] pb[1][3] pb[1][4]
pb[2] → pb[2][0] pb[2][1] pb[2][2] pb[2][3] pb[2][4]
pb[3] → pb[3][0] pb[3][1] pb[3][2] pb[3][3] pb[3][4]
pb[4] → pb[4][0] pb[4][1] pb[4][2] pb[4][3] pb[4][4]
pb[5] → pb[5][0] pb[5][1] pb[5][2] pb[5][3] pb[5][4]
pb[6] → pb[6][0] pb[6][1] pb[6][2] pb[6][3] pb[6][4]
数组与指针
在C/C++中,数组与指针的关系非常密切,
要清楚理解两者任一个的概念的话,不清楚了解另一个的概念是不可能的。
所以在这里阐述一下他们的关系。
从最简单的开始:
int a[10];
int *pa = a;
声明完这两个变量后,我们可以:
a[0] = 0;
同样我们可以:
*a = 0; // 一次寻址
*pa = 0; // 两次寻址
pa[0] = 0; // 两次寻址
对于*a和*pa来说,其实原型为:
*(a + 0)
*(pa + 0)
因为存在这样一个事实:
在指针上加上一个值后,并不是单纯在地址上加上该值,
而是表示一个偏移量,根据类型不同,偏移单位量也是不同的。
如果以上述例子来说的话,因为int在VC中是4个字节,
那么每+1,在地址上所加的是sizeof(int)也就是4个字节。
这样,就和数组的索引值就陪对了起来。即:
*a 等价于 *(a + 0) 等价于 a[0]
*(a + 5) 等价于 a[5]
从另一个方面来说的话,对于
pa = a;
也可以写为:
pa = &a[0];
同样,并不一定要指向索引为0的元素,
pa = a + 7;
pa = &a[7];
接下来考虑稍微复杂一点的静态两维数组:
int b[7][5];
int (*pb)[5] = b; // 声明和赋值写在同一行可能比较混乱,如果单独写的话,应该是 pb = b;
int (*pb1)[5] = b + 1; // 或者&b[1]
int *pvalue = (*b) + 1; // 或者&(*b)[1]
int *pvalue1 = (*pb) + 1; // 或者&(*pb)[1]
首先解释一下一个常见的问题:
int **pb = b;
如果这么写的话,编译器会不客气地扔出一个错误:
error C2440: '=' : cannot convert from 'int [7][5]' to 'int **'
猜想的原因可能是因为:
静态数组和动态数组的结构不同,如果同样采用int** 指针来引用的话,
在访问具体元素的时候,会产生二义性(指针并不知道引用的数组类型)
而一旦明确了声明后,对指针的操作时采用两次*取值操作就不会受到太大阻碍。
以下几种写法都是等价的:
b[1][1]
pb1[1][1]
*(b[1] + 1)
*(pb1[1] + 1)
*(*(b + 1) + 1)
*(*(pb1 + 1) + 1)
唯一被编译器隐藏的区别就是,与b相关的操作一次寻址,而pb1每次都是两次寻址。
然后看看那个声明就比较混乱的pb
在声明中,指明了它是一个指针,指向一个含有5个元素的数组。
回忆一下第二节.静态两维数组的排列顺序中一句话:
而对于后者,可以理解为b是一个拥有7个数组类型的数组,
每个数组类型又是一个拥有5个int类型值的数组。
两边要表达的意思是一致的,所以,pb的值可以为&b[0] ~ &b[6]中任一个。
接着我们又声明了一个int指针,用来接具体的值,
由于pb固定了b的第一维索引值,所以对其解引用(即*操作),
操作层从第一维转换到了第二维上,所以我们可以用&(*pb)[1]的形式,
或者(*pb) + 1的形式来获得某个元素的指针。
对于动态两维数组,因为其本身声明就是int **pb,所以也不存在什么编译器报错的问题。
而且对其做两次解引用(*求值操作),也确实根据地址求了两次值。
如果算上指针本身是地址,需要间接引用的话,所以一共是做了三次间接引用。
各种数组的声明方式
对于数组的声明,以及在函数参数中的声明,经常会被编译器拦截,
被告知声明不符合要求,所以经常会摸不着头脑最后改变数组的形式。
其实每种声明都可以通过语法实现,只不过因为数组的特殊性,
所以要说明数组的语法通常都比较复杂一点罢了。
下表是对各种情况做了一下整理:(假设对函数的调用为func(a))
数组声明 说明 函数参数声明 备注
int a[10]; 一维静态数组 void func(int [10]);
void func(int []);
void func(int *);
数组长度是否明示对函数参数没有影响,
即使void func(int [100]);这样的声明,
还是能把a作为参数传入,最多某些严肃的编译器抱怨一个警告而已
int *pa 一维数组指针
(静态和动态皆可)
void func(int [10]);
void func(int []);
void func(int *);
数组指针和一维数组之间的区别很小,
唯一区别就是,数组作为参数就是把它自己PUSH进栈,
而指针的话需要间接引用一次,把取得的值PUSH进栈。
int b[7][5] 两维静态数组 void func(int [7][5]);
void func(int [][5]);
void func(int (*)[5]);
两维静态数组的申明方式基本上和一维的差不了多少,
但要注意的是必须指定第二维的大小。
道理很简单,数组在第一维上移动的单位长度必须确定,
比如a[3] -> a[4]移动的距离必定是sizeof(int [5])。
所以,也是为什么第二维必须固定,而不是第一维的原因。
int (*pb)[5] 两维静态数组指针 void func(int [7][5]);
void func(int [][5]);
void func(int (*)[5]);
和一维数组指针基本上一样,区别点也相同。
int **pb 两维动态数组指针 void func(int **);
可怜的两维动态数组指针只有一种调用方式:-(
int c[9][7][5] 三维静态数组 void func(int [9][7][5]);
void func(int [][7][5]);
void func(int (*)[7][5]);
尝试一下声明三维的数组?虽然不常用,呵呵
int (*pc)[7][5] 三维静态数组指针 void func(int [9][7][5]);
void func(int [][7][5]);
void func(int (*)[7][5]);
别眼花了~
int ***pc 三维动态数组指针 void func(int ***);
三维动态数组指针也好可怜=v=
静态数组和动态数组在内存的组织方式
前面也提到过,静态数组在栈内或者在数据段.data内分配空间,
而动态数组在堆内分配空间。下面从内存角度来说明各自的特点。
全局变量数组/静态局部变量数组
这两个数组的生命期都和程序一样长,因为它们的空间是程序运行前就指定完毕的,
在程序执行过程中,也是通过直接寻址,地址一已经硬编码写在了程序代码中。
因为运行前就被分配了空间,所以所有元素的初始值被设为0。
有一种特殊的全局变量数组——字符串
他们是一种末尾必定以'/0'结尾的特殊的char型数组。
虽然也属于全局变量数组,但他们被放在了只读的.rdata段中,
放在只读段中是因为他们是不应该被程序的错误代码而改写的。
看看下面的例子:
char* c1 = "abcde";
char c2[] = "abcde";
c1[0] = 'b'; // 错误
c2[0] = 'b'; // 正确
c1是指针,指向一个全局的字符串数组
c2是一个局部变量数组
当c1想改写全局字符串数组中某个元素的值的时候,
会发生Access Violation的异常,因为在该内存区的数据全是只读的。
局部变量数组
局部变量数组的空间是从栈上被分配出来的。
局部变量数组的生命周期只和函数一样长,当然如果声明在某个循环里的时候,
虽然堆内预留的空间还是存在的,但编译器可能会把这个空间用于其他不和这个变量冲突的变量。
这样做的目的是为了节省堆栈空间。所以,在循环内的数组变量声明周期也就到循环的结束。
在栈上分配空间的时候,由于PUSH的效率很低,特别是遇到数组这种大量数据集合的时候,
所以编译器一般直接通过移动ESP指针来达到分配空间的目的。但编译器做的仅仅是移动指针,
而分配出来的空间中有些什么垃圾数据,编译器是不会去管的,这个C/C++本身实现目标有关系,
C/C++是为了性能而设计出来的一种语言。
它的目标是做最少的事情,用最少的时间完成最多的工作,
所以,初始化内存的责任就转移到了程序员的身上,
另外不仅分配内存如此,从很多方面都能感受到C/C++的这个思想。
栈中的数据不单单有普通变量,数组变量,还有比如返回地址,栈帧,SEH链表,调用参数都在这块空间内。
其中最重要的是返回地址。
为什么这么说,首先介绍一个概念——栈溢出。
栈溢出就是因为不当的操作,把值写到了不应该写的栈地址上面。
而返回地址决定了程序执行的方向,一旦返回地址被修改,导致程序跳到了其他地方,
首先是程序出错这是肯定的,此外由于操作系统最终捕获到错误会利用堆栈的返回地址链,
打印出栈中函数的调用关系,如果返回地址都没有了的话,那就不可能追踪最末层的函数,
也就会给调试带来的一定的困难。
说了那么多,其实反过来想说明的问题是,局部变量数组是一个很容易导致堆栈溢出的变量。
因为数组会有循环迭代,一旦没有控制住,就会越界,越界就有可能发生堆栈溢出。
关于这点,在下一节杂谈,不为人知的数组表达方式中有简略的实例演示。
动态数组(数组指针)
动态数组虽然也有局部的——它的指针,
但一旦new过了之后,它所指向的实际分配空间是在堆里面的。
在堆中申请空间有两种方式:C的malloc和C++的new。
对于基本数据类型的数组来说,这两种申请空间的方式没有什么太大的区别。
int *pa, *pb;
pa = (int *)malloc(sizeof(int) * 10); // 正确
pb = new int[10]; // 正确
但是,如果类数组的话,一定要用new来分配空间,而不是malloc。
MyClass *pa, *pb;
pa = (MyClass *)malloc(sizeof(MyClass) * 10); // 错误
pb = new MyClass[10]; // 正确
采用malloc调用的只是分配了一块sizeof(MyClass) * 10大小的空间,其他什么事情都没做。
采用new调用,不但分配了空间(自动计算),而且还调用了每个MyClass的构造函数。
对于类来说,构造函数是很重要的,如果没有调用构造函数而使用该类变量的话,可能会出现预想不到的结果。
同样,在用new []申请空间后,需要用delete []释放空间。
为什么不是delete,而是delete []?
对于基本数据类型的数组来说,delete只释放了pa[0]的空间,而delete []正确地释放了所有的空间。
对于类的数组来说,delete只调用了pa[0]的析构函数,并是放了空间,
而delete []调用了所有元素的析构函数,并且正确地释放了所有的空间。
如果有疑问的话,这样一段小小的程序会告诉你答案的:-)
代码 输出
#include <iostream> using namespace std; class MyClass { public: MyClass() {_i = _s_i++; cout << "MyClass(). " << _i << endl;} ~MyClass() {cout << "~MyClass(). " << _i << endl;} private: int _i; static int _s_i; }; int MyClass:: _s_i = 0; int main(int argc, char** argv) { MyClass *pa, *pb; cout << "[malloc]" << endl; pa = (MyClass *)malloc(sizeof(MyClass) * 10); cout << "[new]" << endl; pb = new MyClass[10]; delete[] pb; pb = 0; free(pa); pa = 0; } [malloc] [new] MyClass(). 0 MyClass(). 1 MyClass(). 2 MyClass(). 3 MyClass(). 4 MyClass(). 5 MyClass(). 6 MyClass(). 7 MyClass(). 8 MyClass(). 9 ~MyClass(). 9 ~MyClass(). 8 ~MyClass(). 7 ~MyClass(). 6 ~MyClass(). 5 ~MyClass(). 4 ~MyClass(). 3 ~MyClass(). 2 ~MyClass(). 1 ~MyClass(). 0
杂谈,不为人知的数组表达方式
先看程序=v=
int i = 4;
int a[10];
int j = 5;
int b[7][5];
0[a] = 6;
9[a] = 7;
0[b][0] = 1;
0[1[b]] = 2;
0[b[2]] = 3;
cout << (-1)[a] << endl;
看完之后,有什么想法么?
多数人可能会认为这个东西还是C/C++么?
不过很可惜……
这段程序在M$的编译器中编译成功。
为什么这样?!可能有人要疯了。
慢,其实这个表达方式能够被认可,和第四节数组与指针中的概念有关。
*a 等价于 *(a + 0) 等价于 a[0]
*(a + 5) 等价于 a[5]
有这样一个事实:
*(a + 0) 和 *(0 + a)要表达的东西是一样的。
所以可以推导出:
a[0] 等价于 *(a + 0) 等价于 *(0 + a) 等价于 0[a]
所以不难可以推导出二维的表达方式:
b[0][1] = *(b[0] + 1) = *(1 + b[0]) = 1[b[0]]
b[0][1] = *(b[0] + 1) = *(*(b + 0) + 1) = *(*(0 + b) + 1) = *(0[b] + 1) = 0[b][1]
b[0][1] = *(b[0] + 1) = *(*(b + 0) + 1) = *(*(0 + b) + 1) = *(1 + *(0 + b)) = *(1 + 0[b]) = 1[0[b]]
从这个例子可以看出,数组和指针有着非常亲密的关系^^
好,让我们喘一口气,怪谈还没完呢~
看看最后一个cout输出语句:
cout << (-1)[a] << endl;
(-1)[a]根据刚才的知识我们可以得出: (-1)[a] = a[-1] ……等等,-1?编译器怎么没报错??
在C/C++里当然不会报,因为报了就违背C/C++的设计目标了——多管不用管的
那么再把这个式子转换一下,可以得到*(a - 1)
按照指针的概念,也就是前一个元素的东西,
因为a[0]是数组头了,-1会指向哪里呢?猜猜看~
或许有人会说是i,因为按照书写的顺序,i在数组a上面。
但代码书写并非编译器的实现,根据编译器的实现,还有有没有开启优化,结果是不一样的
按照我目前手头的编译器(见文章的开头),采用DEBUG模式编译出来,输出的结果是5
也就是指向了j。
在80x86体系结构,Windows系列操作系统下,栈在内存中从大地址->小地址方向扩展。
换句话说,栈底在大地址,栈顶在小地址。
这样,压入参数的时候,i被压在了大地址上,也就是下方,
然后数组a压在i的上方,因为数组的顺序还是要沿着地址增长的方向扩展
所以a[0]在上方,a[9]在下方,
然后j又压在了a[0]的上方,所以a[-1]也就是j了。
再换句话说,这也就是所谓的栈溢出!
列出该程序执行该段程序时,栈的情况的话:
0012FEE8 j = 5
0012FEEC a[0] = 6
0012FEF0 a[1] = ?
~
0012FF0C a[8] = ?
0012FF10 a[9] = 7
0012FF14 i = 4
0012FF18 保存的EBP
0012FF1C 返回地址
刚才我们在a[-1],也就是j,现在我们把矛头转一下,直接对准返回地址。
按照栈的情况来看,返回地址用数组a来解释就是a[12],稍微修改一下程序。
在函数结束前增加一行代码:
a[12] = 0;
顺利通过编译,嗯
执行……!调试器跳出,说有Access Violation
看一下当前EIP指针,果然等于0
程序也就这么被栈溢出给抹杀掉了(其实是操作系统抹杀了该程序,栈溢出的行为应该属于借刀杀人=v=)
不过一般非故意的栈溢出不会这么有针对性,都是循环失控导致的,这样会把栈一路上的地址全部破坏掉。
所以一般那些检查栈溢出的代码(DEBUG模式下有些编译器会插入),在栈帧两端设防,
一旦设防标志被破坏就认为发生了栈溢出,所以大多数非故意的栈溢出还是能够通过特定代码监测出来的:-)
数组的迭代与性能影响
从简单的角度出发,我们考虑两维静态数组。
对于数组a[i][j],采用两层循环来迭代,应该把个索引放外面,哪个索引放里面?
如果从理论上来讲,我们知道m固定的情况下,j中的元素是连续的,
又因为高速缓存读入数据时候按块来读,所以尽量应该使最近访问的数据都在连续的内存上。
那么我们得出了答案,也就是i放外面,j放里面,写成程序就是:
#define MAX_I 1024 * 1024
#define MAX_J 1024 * 512
int a[MAX_I][MAX_J];
for (int i = 0; i < MAX_I; i++)
for (int j = 0; j < MAX_J; j++)
a[i][j] = 1;
但理论归理论,还是需要实践才是真理!
编写程序,对a[i][j]进行写入测试,测量工具采用CPU指令RDTSC
RDTSC
卷绕周期为172年
精度±0.001微妙(3.4GHz处理器)(受电源管理和乱序执行的影响)
秒转换:秒数 = 两次测试的差值 / 计算机CPU的频率
测试程序:
#define MAX_I 32 * 512 #define MAX_J 32 * 512 int g_array[MAX_I][MAX_J]; inline unsigned __int64 GetTick() { __asm RDTSC } int main(int argc, char** argv) { __int64 t1; t1 = GetTick(); for (int i = 0; i < MAX_I; i++) for (int j = 0; j < MAX_J; j++) g_array[i][j] = 1; cout << GetTick() - t1 << endl; t1 = GetTick(); for (int j = 0; j < MAX_J; j++) for (int i = 0; i < MAX_I; i++) g_array[i][j] = 1; cout << GetTick() - t1 << endl; }
测试用机:
Intel(R) Core(TM)2 CPU T7400 2.16GHz 2.16GHz
2.00GB内存
次数 i外j内 j外i内
1 3182607649 29253016390
2 3224917293 27522755221
3 3160380392 27205374131
4 3147323816 27746559464
5 3159460317 27695442775
6 3150892875 27581611161
7 3166696546 27459197090
8 3134793389 27874196311
9 3161698709 27514023030
10 3205793422 27283634963
AVG 3169456440.8 27713581053.6
毫秒 1467ms 12830ms
通过测试可见,两种方式导致的性能差别几乎达到了10倍。
转自http://ikaruga.name/Technology/ccplusplus/staticAndDynamicArray.html
发表评论
-
文件结束符
2012-02-29 15:13 869c++练习中while循环输入需要文件结束符或者错误输入才能退 ... -
source insight快捷键及使用技巧
2010-11-12 11:26 1756退出程序 ... -
关于scanf对输入非法字符的检查和处理
2010-11-11 11:18 4284由于函数scanf(),不做参数类型的匹配检查,因此,用户输入 ... -
makefile missing separator
2010-11-08 16:06 1193objects = HelloWorld.o run:$(o ... -
warning: no newline at end of file
2010-11-08 15:57 932解决的办法: 在最后一行敲一个回车, 然后保存, 重新编译. ... -
return type of 'main' is not `int'
2010-11-08 15:56 1073#include"stdio.h" vo ... -
Linux makefile 教程(六)
2010-10-09 12:23 826五、定义模式规则 你可以使用模式规则来定义一个隐含规则。一个 ... -
Linux makefile 教程(五)
2010-10-09 12:22 966示例一: ifdef ERROR_001 ... -
Linux makefile 教程(四)
2010-10-09 12:18 1327一、示例 下面的例子,判断$(CC)变量是否“gcc”,如果 ... -
Linux makefile 教程(三)
2010-10-09 12:17 1137一、变量的基础 变量在声明时需要给予初值,而在使用时,需要给 ... -
Linux makefile 教程(二)
2010-10-09 12:16 982七、静态模式 静态模 ... -
Linux makefile 教程(一)
2010-10-09 12:10 1075网上的教程,觉得挺不 ... -
Linux下编译C程序
2010-10-09 11:59 1522GCC 支持了许多不同的 ... -
linux vim 使用详解
2010-10-09 11:47 1653在每个用户的主目录下,都有一个 vi 的配置文件". ... -
Linux vi编辑器
2010-09-27 11:29 1723#插入模式 【i】切换进入插入模式,从光标当前位置开始输入文 ...
相关推荐
在VB(Visual Basic)编程语言中,静态数组是一种在程序执行前就已预先定义大小的数组,它的容量在声明时就已经固定,无法在运行时动态调整。了解和掌握静态数组的使用是VB编程基础的重要部分。下面我们将深入探讨VB...
本示例用于演示静态数组变量与动态数组变量的不同。 输出结果是这样的: ______________________________________________________ 256256 4 _______________________________________________...
Delphi实例源码"Delphi实例源码演示静态与动态数组变量的不同.rar"中的代码文件,很可能是为了展示这两种数组类型的创建、访问和修改过程,以及它们在性能上的差异。通过对比实验,开发者可以更好地理解何时选择静态...
本示例代码着重探讨了动态数组、静态数组以及 TBytes 的使用,并展示了它们与 TMemoryStream 的结合应用。理解这些概念对于高效地处理数据至关重要。 1. 动态数组: 动态数组在 Delphi 中是 Variant 类型的一个子...
与静态数组不同,`sizeof(a)`和`sizeof(*a)`都返回4字节,因为`a`在这里只是一个指向动态数组首元素的指针。动态数组在函数中使用时,`sizeof`运算不提供有关数组大小的信息,因为数组大小并不存储在指针本身中。 ...
2. **初始化与销毁**:为了使用静态数组,我们需要提供初始化和销毁功能。初始化函数会分配内存并设置初始值,如所有元素为0;销毁函数则负责释放分配的内存。例如: ```c StaticList* createList(int capacity) { ...
静态数组与动态数组的比较 静态数组在编译时大小已知,无法在运行时改变大小,但它们不需要额外的内存分配开销。动态数组在运行时分配内存,大小可变,但分配和释放内存可能带来性能损失。 ### 6. 动态数组与容器...
在C语言中,静态数组是一种基础且重要的数据结构,它在内存中预先分配固定的大小,用于存储一组相同类型的数据。本资源"**C语言实现使用静态数组.zip**"似乎包含了一个名为"Queue_Array"的项目,可能是一个用C语言...
静态数组和动态数组,初学者可以了解一下,对于动态和静态数组的区别有所描述。
三、静态数组与动态数组 静态数组的大小在声明时固定,适用于已知元素数量的情况。动态数组则通过`ReDim`语句在运行时重新分配大小。例如: ```vb Dim arrSheetName() As String '声明动态数组 ReDim arrSheetName(1...
静态数组实现的栈优点在于它的简单性和效率,不需要动态内存分配,减少了潜在的内存泄漏风险。然而,缺点是缺乏动态扩展能力,如果预设容量不合适,可能导致性能问题。为了解决这个问题,可以考虑使用链表或者动态...
在本主题中,我们将深入探讨如何使用C语言中的静态数组来实现一个循环队列。循环队列是一种线性数据结构,它巧妙地利用了数组的特性,克服了普通队列在满时无法插入元素、空时无法删除元素的问题。 首先,了解数组...
静态数组与动态数组 - **静态数组**:在声明数组时就已经指定了数组的维数和各维度的上界,例如 `Dim arrTemp(5) As Integer`。一旦声明后,就不能改变其大小。 - **动态数组**:声明时并不指定具体的大小,而是...
按照不同的分类标准,可以将数组分为索引数组和关联数组,静态数组和动态数组。下面详细介绍这些概念及其特点,并通过示例加深理解。 首先,根据数组下标的类型,我们可以区分索引数组和关联数组。索引数组的元素...
静态数组链表则是将链表的概念与数组相结合,它在内存中以数组的形式存储链表节点,但每个节点可以动态地改变其大小或指向。这种数据结构结合了数组的高效访问和链表的灵活插入删除的优点。在C语言中实现静态数组...
2. 静态数组与动态数组:静态数组在声明时确定大小,而动态数组可以在运行时改变大小,根据需要选择合适的数组类型。 3. 对象数组:除了基本类型,数组还可以包含自定义对象,排序时可能需要实现IComparable接口。 ...
4. **内存管理**:静态数组在声明时就分配了固定大小的内存,无需动态分配,简化了内存管理。 5. **函数设计**:理解如何设计和实现队列的各个操作函数,例如如何保证入队和出队的正确性,以及如何处理边界情况。 ...
在Delphi中,静态数组是在编译时声明并分配固定大小的内存空间的数据结构。这意味着它们的大小在程序运行过程中不能改变。对于二维数组,我们可以理解为一个矩阵,每个元素可以通过行和列的索引来访问。 下面,我们...
本文将深入探讨如何使用静态数组来实现循环队列,这是一种在C语言中常见且重要的数据结构。 首先,理解循环队列的概念至关重要。循环队列是一种线性数据结构,它具有队列的基本属性——先进先出(First In First ...