静态数组和动态数组 内存分布

2023-05-16

数组是程序设计中是一个非常重要的概念。数组是一个用于收集大量类似数据的容器,
以及其每一个元素能被相同处理过程迭代来处理的一个抽象体。
创建数组一般有三种方式:全局/静态范围的数组局部变量数组申请堆空间来创建数组
其中,全局/静态范围的数组,以及局部变量属于静态数组
而申请堆空间来创建数组的属于动态数组

  1. a[7]与p1_a[7]是一样的么?
  2. 静态两维数组的排列顺序
  3. 动态两维数组的排列顺序
  4. 数组与指针
  5. 各种数组的声明方式
  6. 静态数组和动态数组在内存的组织方式
  7. 杂谈,不为人知的数组表达方式
  8. 数组的迭代与性能影响

 

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了。

再换句话说,这也就是所谓的栈溢出!
列出该程序执行该段程序时,栈的情况的话:

0012FEE8j = 5
0012FEECa[0] = 6
0012FEF0a[1] = ?
 
0012FF0Ca[8] = ?
0012FF10a[9] = 7
0012FF14i = 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内
1318260764929253016390
2322491729327522755221
3316038039227205374131
4314732381627746559464
5315946031727695442775
6315089287527581611161
7316669654627459197090
8313479338927874196311
9316169870927514023030
10320579342227283634963
AVG3169456440.827713581053.6
毫秒1467ms12830ms

通过测试可见,两种方式导致的性能差别几乎达到了10倍
虽然现在机器越来越快,但不良的数据结构或者错误的算法导致的延迟还是存在的。
要消除各种耗时操作确实不是一项简单的事情,因为优化需要很多知识和经验。
由于主要还是讲数组的问题,这小节也不能扩展得太大,
至于如何优化程序结构,这还是一个比较大的主题,如果有机会的话就写写看吧^^

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

静态数组和动态数组 内存分布 的相关文章

  • c++中vector的用法详解

    vector 向量 C 43 43 中的一种数据结构 确切的说是一个类 它相当于一个动态的数组 当程序员无法知道自己需要的数组的规模多大时 用其来解决问题可以达到最大节约空间的目的 用法 1 文件包含 首先在程序开头处加上 include
  • C++中extern关键字的作用

    1 基本解释 xff1a extern可以置于变量或者函数 前 xff0c 以标示变量或者函数的定义在别的文件中 xff0c 提示编译器遇到此变量和函数时在其他模块中寻找其定义 此外extern也可用来进行链接指定 也就是说extern有两
  • 双缓冲技术绘图原理及简单的VC实现

    为了增加自己对双缓冲绘图技术的理解 xff0c 简要做个笔记 xff08 以Windows为例 xff09 xff1a 1 Windows 绘图原理 我们在 Windows 环境下看到各种元素 xff0c 如菜单 按钮 窗口 图像 xff0
  • C#中Tuple的使用

    定义 xff1a 元组是具有 特定数量和序列 的元素 的数据结构 xff08 注意断句哈 xff01 xff09 元组通常有四种使用方式 一 表示一组数据 例如 xff0c 一个元组可以表示一条数据库记录 xff0c 并且每一个分量对应表示
  • 调用C++dll

    1 dll 的优点 代码复用是提高软件开发效率的重要途径 一般而言 xff0c 只要某部分代码具有通用性 xff0c 就可将它构造成相对独立的功能模块并在之后的项目中重复使用 比较常见的例子是各种应用程序框架 xff0c ATL MFC 等
  • DLL程序入口DllMain详解

    DLL程序入口点函数 xff1a DllMain xff0c 注意 xff1a 大小写是区别的 仅导出资源的DLL可以没有DllMain函数 函数原型 cpp view plain copy print BOOL APIENTRY DllM
  • C++编写DLL的方法

    在写C 43 43 程序时 xff0c 时常需要将一个class写成DLL xff0c 供客户端程序调用 这样的DLL可以导出整个class 也可以导出这个class的某个方法 一 导出整个class 方法很简单 xff0c 只需要在类的头
  • dll文件的c++制作

    1 首先用vs2005建立一个 c 43 43 的dll 动态 链接 库 文件 xff0c 这时 xff0c DllTest cpp 定义 DLL 应用程序的入口点 include 34 stdafx h 34 include 34 Dll
  • vs2008 C++创建和调用标准DLL

    为了共享代码 xff0c 需要生成标准的dll xff0c 本文将介绍在vs2008 C 43 43 生成及调用dll 一 生成DLL 生成一个名为FunDll的dll文件 xff0c 对外函数为addl step1 vs2008 环境下
  • VSCode使用SSH免密登录服务器

    VSCode使用SSH免密登录服务器 前言一 SSH连接服务器1 1 安装ssh插件1 2 配置连接 二 免密登录服务器2 1 生成公钥2 2 服务器新建授权文件2 3 本地复制公钥到服务器 总结 前言 vscode使用remote ssh
  • 使用directX 7结合C#进行2D游戏编程

    使用directX 7结合C xff03 进行2D游戏编程 前言 对于C 的开发人员来讲 xff0c GDI 43 是一个拥有丰富的绘图API指令 传统 高效的程序集 但不幸的是 xff0c 你要想用她来开发一个复杂而又平滑的动画的时候 x
  • DirectX学习笔记_关于Sprite.Draw2D的说明

    在DirectX的Sprite中提供一个Draw2D的方法 xff0c 该方法绘制一个 Sprite 对象用于二维空间中显示 xff0c 在DirectX 9 0C中 xff0c 该方法有6个重载 xff0c 分别是 1 public vo
  • 关于Direct2D

    关于Direct2D Direct2D是一个硬件加速的 xff0c 提供立即模式的二维图形API 它提供了二维的几何体 xff0c 位图 xff0c 文本的高性能 xff0c 高质量的渲染 十分方便的是 xff0c Direct2D与GDI
  • C# GDI+ 绘图

    1 坐标系统 1 坐标原点 xff1a 在窗体或控件的左上角 xff0c 坐标为 0 0 2 正方向 xff1a X轴正方向为水平向右 xff0c Y轴正方向为竖直向下 3 单位 xff1a 在设置时 xff0c 一般以像素为单位 xff0
  • lock锁和monitor.enter锁

    210 08 05 14 50 28 转载 Lock object 锁的使用 using System using System Threading namespace program class wangjun public static
  • windows重绘机制原理

    一 Windows程序中的绘制和更新 与DOS环境比较 xff0c Windows中的应用程序在处理文字和图形绘制时有以下区别 xff1a 1 只能在窗口的客户区域绘制文字和图形 2 在窗口上绘制的内容不一定能够保留到程序下一次有意地改写时
  • C# Winform 出现异常:无法将顶级控件添加到控件,解决方案如下:

    Form1Test frm 61 new Form1Test frm TopLevel 61 false 重要的一个步骤 frm Parent 61 splitContainerPanel Panel2 frm Show
  • c#中的Form.Show和Form.ShowDialog的区别

    出处 xff1a http hi baidu com cysteine blog item 01e32224702ff5398744f9bf html 区别1 xff1a ShowDialog是模态的 xff08 独占用户输入 xff09
  • 浅析C#中foreach引用变量

    昨天做老师的网站作业 要对一些对象做添加修改删除处理 别的倒没什么 xff0c 删除时出现了点问题似的 因为是从一个类的集合中删除掉一个元素 这样就要遍历整个集合 xff0c 而foreach正是为遍历准备的新玩意 自然而然用上了 于是代码
  • 用汇编的眼光看C++(之拷贝、赋值函数)

    拷贝构造函数和复制函数是类里面比较重要的两个函数 两者有什么区别呢 xff1f 其实也很简单 xff0c 我们可以举个例子 xff0c 加入有这样一个类的定义 xff1a cpp view plain copy class apple pu

随机推荐

  • IDEA配置一个入门的ssh事例,解决Artifact war exploded:Error during artifact deployment. See server log for detail

    首先 xff0c 我的版本ideaIU 2017 3 4 xff0c 一 新建一个项目project 1 1 create new project 2 file new project 2 这里选择spring xff0c struts2
  • WinForm 之Control.Invoke 和Control.BeginInvoke 方法的使用 Control 不能在创建它的 Thread 之外被调用。但可以通过 invoke 来保证 C

    WinForm 之Control Invoke 和Control BeginInvoke 方法的使用 Control 不能在创建它的 Thread 之外被调用 但可以通过 invoke 来保证 Control 线程安全 在跨线程更新的时候
  • C#中跨线程访问控件问题解决方案

    net 原则上禁止跨线程访问控件 xff0c 因为这样可能造成错误的发生 xff0c 推荐的解决方法是采用代理 用代理方法来间接操作不是同一线程创建的控件 第二种方法是禁止编译器对跨线程访问作检查 xff0c 可以实现访问 xff0c 但是
  • C#中Invoke的用法(转)

    转载 转自 xff1a http blog 3snews net html 30 34530 27563 html 在多线程编程中 xff0c 我们经常要在工作线程中去更新界面显示 xff0c 而在多线程中直接调用界面控件的方法是错误的做法
  • 【分析】浅谈C#中Control的Invoke与BeginInvoke在主副线程中的执行顺序和区别(SamWang)

    今天无意中看到有关Invoke和BeginInvoke的一些资料 xff0c 不太清楚它们之间的区别 所以花了点时间研究了下 据msdn中介绍 xff0c 它们最大的区别就是BeginInvoke属于异步执行的 Control Invoke
  • C#中Invoke 和 BeginInvoke的涵义和区别

    BeginInvoke 方法真的是新开一个线程进行异步调用吗 xff1f 参考以下代码 xff1a public delegate void treeinvoke private void UpdateTreeView MessageBox
  • C# 理解lock

    一 为什么要lock xff0c lock了什么 xff1f 当我们使用 线程 的时候 xff0c 效率最高的方式当然是 异步 xff0c 即各个线程同时运行 xff0c 其间不相互依赖和等待 但当不同的线程都需要访问某个资源的时候 xff
  • excel操作的几种方法

    using System using System Collections Generic using System Text using System Data using System Windows Forms using Syste
  • c#中如何实现拷贝对象

    大家都知道 xff0c 在C 中变量的存储分为值类型和引用类型两种 xff0c 而值类型和引用类型在数值变化是产生的后果是不一样的 xff0c 值类型我们可以轻松实现数值的拷贝 xff0c 那么引用类型呢 xff0c 在对象拷贝上存在着一定
  • 深入了解Windows句柄到底是什么

    总是有新入门的Windows程序员问我Windows的句柄到底是什么 xff0c 我说你把它看做一种类似指针的标识就行了 xff0c 但是显然这一答案不能让他们满意 xff0c 然后我说去问问度娘吧 xff0c 他们说不行网上的说法太多还难
  • 句柄概念

    句柄 xff08 handle xff09 xff0c 有多种意义 xff0c 其中第一种是指程序设计 xff0c 第二种是指Windows编程 现在大部分都是指程序设计 程序开发这类 第一种解释 xff1a 句柄是一种特殊的 智能指针 当
  • 腾讯云大数据发布最新产品矩阵,助力企业整合打通海量数据

    9月11日 xff0c 主题为 释放数字经济发展的新动能 的腾讯全球数字生态大会大数据专场在线上拉开帷幕 腾讯大数据领域的多位顶级专家 xff0c 与包括 Hadoop 创始人 Doug Cutting 在内的业内顶级大咖 xff0c 以及
  • C/C++中函数参数传递

    看了内存管理的有关内容 xff0c 有一点了解 xff0c 但不是很深入 xff0c 发现之前写代码时有很多细节问题没有注意到 xff0c 只知道这样做可以实现功能 xff0c 却不知道为什么可以这样 xff0c 对于采用自己的方法造成的隐
  • Windows窗口刷新机制详解

    1 Windows的窗口刷新管理 窗口句柄 xff08 HWND xff09 都是由操作系统内核管理的 xff0c 系统内部有一个z order序列 xff0c 记录着当前窗口从屏幕底部 xff08 假象的从屏幕到眼睛的方向 xff09 x
  • C语言中内存分布及程序运行中(BSS段、数据段、代码段、堆栈)

    BSS段 xff08 bss segment xff09 通常是指用来存放程序中 未初始化 的 全局变量 的一块内存区域 BSS是英文Block Started by Symbol的简称 BSS段属于静态内存分配 数据段 xff1a 数据段
  • C# 窗体Show和ShowDialog 方法的区别详解

    CenterParent 窗体在其父窗体中居中 CenterScreen 窗体在当前显示窗口中居中 xff0c 其尺寸在窗体大小中指定 Manual 窗体的位置由 Location 属性确定 WindowsDefaultBounds 窗体定
  • c# 窗口句柄问题 。

    1 如何获得一个窗口的句柄 xff1f 例如获取窗口PictureBox控件 xff08 其他控件也可以 xff09 的句柄 xff0c csharp view plain copy IntPtr handle 61 pictureBox
  • c#中已知一个外部窗口的句柄,怎么关闭

    已知一个外部窗口的句柄 xff0c 怎么关闭它 怎么给这个窗口的一个文本框设置内容 public void Test Handle windowhandle string TextBoxName System windws froms fr
  • 数组内存分配概念

    在这里解答一下 xff1a int arr 4 amp arr 1 61 arr 0 43 sizeof int 静态分配 xff0c 即普通数组 xff0c 由于在栈中分配 xff0c 而栈的生成方向是自高地址向低地址生成 所以有 xff
  • 静态数组和动态数组 内存分布

    数组是程序设计中是一个非常重要的概念 数组是一个用于收集大量类似数据的容器 xff0c 以及其每一个元素能被相同处理过程迭代来处理的一个抽象体 创建数组一般有三种方式 xff1a 全局 静态范围的数组 xff0c 局部变量数组 xff0c