[-]
数组是程序设计中是一个非常重要的概念。数组是一个用于收集大量类似数据的容器,
以及其每一个元素能被相同处理过程迭代来处理的一个抽象体。
创建数组一般有三种方式:全局/静态范围的数组,局部变量数组,申请堆空间来创建数组。
其中,全局/静态范围的数组,以及局部变量属于静态数组。
而申请堆空间来创建数组的属于动态数组。
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 |
同上 |
用简单的程序来验证一下:
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][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] |
int a[10]; int *pa = a;
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]
而对于后者,可以理解为b是一个拥有 7个数组类型的数组,两边要表达的意思是一致的,所以, pb的值可以为&b[0] ~ &b[6]中任一个 。
每个数组类型又是一个拥有 5个int类型值的数组。
对于动态两维数组,因为其本身声明就是int **pb,所以也不存在什么编译器报错的问题。
而且对其做两次解引用(*求值操作),也确实根据地址求了两次值。
如果算上指针本身是地址,需要间接引用的话,所以一共是做了三次间接引用。
数组声明 | 说明 | 函数参数声明 | 备注 |
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= |
char* c1 = "abcde"; char c2[] = "abcde"; c1[0] = 'b'; // 错误 c2[0] = 'b'; // 正确c1是指针,指向一个全局的字符串数组
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大小的空间,其他什么事情都没做。
代码 | 输出 |
#include |
[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 |
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;看完之后,有什么想法么?
*a 等价于 *(a + 0) 等价于 a[0]有这样一个事实 :
*(a + 5) 等价于 a[5]
好,让我们喘一口气,怪谈还没完呢~
看看最后一个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模式下有些编译器会插入),在栈帧两端设防,
一旦设防标志被破坏就认为发生了栈溢出,所以大多数非故意的栈溢出还是能够通过特定代码监测出来的:-)
#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;但理论归理论,还是需要实践才是真理!
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倍。
虽然现在机器越来越快,但不良的数据结构或者错误的算法导致的延迟还是存在的。
要消除各种耗时操作确实不是一项简单的事情,因为优化需要很多知识和经验。
由于主要还是讲数组的问题,这小节也不能扩展得太大,
至于如何优化程序结构,这还是一个比较大的主题,如果有机会的话就写写看吧^^