博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

二维数组

Posted on 2020-03-30 18:37  bw_0927  阅读(0)  评论(0)    收藏  举报

 

 

 

二维数组在概念上是二维的,但在内存中是连续存放的;换句话说,二维数组的各个元素是相互挨着的,彼此之间没有缝隙

在C语言中,二维数组是按行排列的。

将二维数组当作参数的时候,必须指明所有维数大小或者省略第一维的,但是不能省略第二维或者更高维的大小,这是由编译器原理限制的。

事实上,编译器是这样处理数组的:
      设有数组int a[m][n],如果要访问a[i][j]的值,编译器的寻址方式为:
      &a[i][j]=&a[0][0]+i*sizeof(int)*n+j*sizeof(int); //注意n为第二维的维数,

因此,可以省略第一维的维数,不能省略其他维的维数

 

    在定义二维数组的时候对其进行初始化,可以省略第一维,编译器会根据你的初始化语句自动决定第一维度。 

int bin_arr[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //至少需要4行, 4 * 3 * 4 = 48
//int bin_arr[][3] = {1, 2, 3, 4 }; //两行, 2 * 3 * 4 = 24; 不满,有空间浪费
cout << sizeof(bin_arr) << endl;

二维、多维数组,除了第一维大小可以省略,其余的维度大小都不能省略

 

=====

实际上,C语言并没有什么“二维数组”的概念,C语言中只有数组!

我们先明确下数组的知识: 

声明数组的语法是:在名字后跟一个[],这表明这个名字将是个数组,括号中有数字则表示数组的大小。 剩下的那个类型则代表数组中元素的类型。  

所以int a[];  这一句的意思是:a是个数组,其储存的元素是int类型。

同时,在定义数组时必须要能够确定数组大小,这分两种情况:

1
2
3
4
5
6
// 声明数组时可以指明数组大小
int a[10];  // a是一个可含10个int的数组
 
// 也可以不写出大小,但给出初始化列表
// 编译器会根据初始化列表来自动确定数组的大小
int a[] = {1, 2, 3, 4};  // 编译器能确定数组a的大小是4

可以看出,定义数组时是可以省略大小的,只要编译器能推断出它的size就可以。  编译器怎样推断? 首先编译器得知道单个元素的大小,再根据初始化列表的大小,就自然能算出a能存几个元素了(做除法而已嘛)。

 

下面看看“二维数组”到底是什么: 所谓二维数组就是一个存储了数组的数组!

比如 int a[2][3];  这代表 a 是个含有2个元素的数组。

// 数组的名字是a,数组的长度是2,数组的元素是int [3]

 

看上面的讲解“剩下的类型代表数组中元素的类型”,那么剩下的就只有 int [3],这是一个含有3个int的数组。 所以a就是一个含有2个“含有3个int数组”的数组。

==== 上面这些可以理解吗?====

那么“定义数组时可省略大小”的规则对于这样复杂些的数组也是成立的。

如 int a[][3] = { ... };

紧挨着 a 后面的括号中的数才表示a数组的大小(也就是所谓的二维数组的行数),它当然是可以省略的,只要编译器能推断出它应有的大小。

a的元素(就是int [3])是个有3个int的数组,这个元素的大小是可以确定的(就是3个int嘛),而初始化列表的大小也是确定的,所以编译器就可以确定a数组的大小(所以你才可以省略不写)。

那么 int a[][] = { ... }; 这样写是错误的,原因也就知道了吧?  a数组大小不确定,a的元素也是个大小不确定的数组,编译器就没法算出到底a能有多少个元素了

 

所以“只能省略行数”这一说法的真正原因是:只有那个紧挨数组名的括号代表该数组的大小后面那些括号代表的是这个数组的元素。 

元素的大小必须是确定的,所以后面那些括号中的数字才不能省略

 

==========

https://blog.csdn.net/weixin_40539125/article/details/83025410

https://blog.csdn.net/wokaowokaowokao12345/article/details/52999502

 

直接将返回值定义为二级指针,将二维数组与二级指针转化,想了想发现根本不可行。二级指针和二维数组并不等价
查找了下资料,发现了论坛https://bbs.csdn.net/topics/360158391大家的讨论。

我们先看下二维数组与二级指针问题:cannot convert from 'double [10][10]' to 'double ** '
例如:

double arry[5][10] = {0};       
double** pp;
pp=arry;
           直接的赋值会提示错误:cannot convert from 'double [10][10]' to 'double ** '

解决方法:

    constexpr int kRow = 3;
    constexpr int kColumn = 10;
    double* d1 = new double(1);  // 单个元素
    double* d2 = new double[10];  // 数组
    
    double** d3 = new double*[kRow];  // 动态分配二维数组
    for (int i = 0; i < kRow; ++i) {
        d3[i] = new double[kColumn];
    }
    cout << sizeof(d3) << endl; // 8   是个指针
    
    double d4[kRow][kColumn];  // 静态分配的二维数组
    cout << sizeof(d4) << endl; // 240    
    // double (*d41)[kColumn] = d4[0];  // error: cannot convert 'double*' to 'double (*)[10]'
    double (*d42)[kColumn] = &d4[0];    // OK
    for (int i = 0; i < kRow; ++i) {
        double (*d43)[kColumn] = &d4[i];
        for (int j = 0; j < kColumn; ++j) {
            // d43[j] = (double)i * 10 + j;  // error: incompatible types in assignment of 'double' to 'double [10]'
            *d43[j] = (double)i * 10 + j;  // d43是个指向数组的指针;*d43才是数组,才可以用下标操作符。
        }
    }

 

 

一个函数形如:

void f(float **p){
/* 想要在函数体中按二维数组的方式访问*/
p[1][1] = 0;  //c++用vc编译ok,运行出错(非法访问)
}

float **p; //其实这里的p并不是一个二维数组的指针,只不过是一个指向指针的指针

像你这样访问肯定是会出问题的。

    float a[2][2]={0, 1, 2, 3};
    float **p=(float**)a;
    
    cout << "a, &a, p: " << endl;
    cout << a << endl;
    cout << &a << endl;
    cout << p << endl << endl;
    cout << "p+1, p+2, p+3: " << endl;
    cout << (p+1) << endl;
    cout << (p+2) << endl;
    cout << (p+3) << endl << endl;
    
    cout << "&a[0][0], 01, 10, 11:" << endl;
    cout << &a[0][0] << endl << &a[0][1] << endl << &a[1][0] << endl << &a[1][1] << endl << endl;

输出:
a, &a, p: 
0x7ffd387fda00
0x7ffd387fda00
0x7ffd387fda00

p+1, p+2, p+3: 
0x7ffd387fda08
0x7ffd387fda10
0x7ffd387fda18

&a[0][0], 01, 10, 11:
0x7ffd387fda00
0x7ffd387fda04
0x7ffd387fda08
0x7ffd387fda0c


  cout << *p << endl;    // 输出的是一个无意义的值 0x3f80000000000000

  cout << **p << endl;    // segment falutl,对地址0x3f80000000000000进行解引用,必挂无疑
  cout << *((float*)p) << endl;   // 0

  cout << *((float*)p+1) << endl;   // 1

 

 

 

 

只能 cout << *((float*)p) << endl;  这样使用,强制把float** p 转成一维指针float*,不好,这得程序员牢牢记住p的真实类型。

或者更好的办法是使用指向数组的指针:

float (*px)[2] = a;
cout << px[1][0] << endl;

cout << *(*(px + 1) + 1);  // 这样也是OK的

 

 

所以说,二位数组并不能简单的转换成指向指针的指针。

正确的指向二维数组的指针应该是:

float a[5][10];

float (*p)[10];//只需要定义为指向第二维的指针,忽略第一维
p=a;

p[0][1]=a[0][1];
 

二级指针和二维数组并不等价

二级指针是一个指向指针的指针 
而二维数组其实就是一个指针,char a[3][4]; a是指向整个二维数组的首地址。它相当于(char *)[4],并不是char **;

所以不能直接:t=a; 
要这样:t = (char **)a;

 

我们知道char array[]=”abcdef”; array是数组的首地址, 
那么在二维数组中array当然也是数组的首地址, 
看看这个定义char Array[][3] ={“ab“,“cd“,“ef“}; 
怎么知道的呢?定义这样一个数组,在vc调试窗口中 
我们看到: 

Array ---------0x64324234 

|------Array[0]---0x64324234 “ab“ 

|------Array[1]---0x64324337 “cd“ 

|------Array[2]---0x6432433A “ef” 

已经很明白了,实际编译器是这样实现二维数组的,实际上Array是“一维指针数组“的首地址,其中每一个元素指针都对应一个字符串,那么好我们来看看是否可以这样来使用Array二维数组。
char **pArray = Array;编译器提示出错,怎么办呢?加个(char **)试试,仍然出错。

设断看一下pArray的值和Array的值是相等的【值相等,但含义不同】,但我们是否可以象使用Array那样来同样输出字符串呢?

很明显是不行的,编译器不会把pArray+i处理成pArray+i*3寻找到第i个指针的地址,而只是简单的加了一个i.

这说明编译器只做了很简单的将地址值赋给了pArray,而它实际没有任何意义. 我们不能用它来访问任何数据. 很奇怪吗? 


再来看看这样定义char *p[] = {“ab“, “cd“, “ef“};定义了一个指针数组.

char **sp = p;这样的用法经常看到,为什么这样就可以使用sp来访问字符串了呢,

的确编译器在编译的时候识别出了sp是一个指向一维数组的指针的指针, 那么我们就可以把它做为数组名来操纵整个数组了.

解决一:将二维数组定义为全局变量。

              全局变量尽量少用,做项目时,可能会导致其他问题。

解决二:由于数组是不可修改的左值,所以在C/C++中函数的返回值不能是数组,但可以返回一个指向二维数组首元素的指针。例如:

 

————————————————

二维数组存放方式

二维数组中元素排列的顺序是按行存放的,即在内存中先顺序存放第一行的元素,再存放第二行的元素…

二维数组作为函数参数

二维数组作为函数参数,实参可以直接使用二维数组名,在被调用函数中对形参数组定义可以指定所有维数的大小,也可以省略第一维的大小说明,如:

void f(int a[3][4]);  
void f(int a[][4]); 

它们是合法且等价,也可以使用如下形式:

void f(int (*a)[4]);          //a是一个指向数组的指针,数组长4

但不能省略第二维的大小,如下面的定义是不合法的,编译时会出错:

void f(int a[][]);  
void f(int a[3][]);  

因为从实参传递来的是数组的起始地址,如果在形参中不说明列数,编译器将无法定位元素的的位置

各个维数不固定的二维数组

如果二维数组的各个维数不固定,我们将不能使用以上定义方法,可以通过以下2种简单的方法实现。

  1. 将二维数组当一维数组操作

被调用函数定义:

void f(int *a,int n); 

实参传递:

int a[3][4];  
f(*a,12);  
  1. 手工转变寻址方式

被调用函数定义:

void f(int **a,int m,int n);  

实参传递:

int a[3][4];  
f((int **)a,3,4);  

这样在被调用数组中对对元素a[i][j]的访问可以使用如下形式:

*((int *)a+n*i+j);  

注意不能使用a[i][j]来直接访问,因为编译器无法为其定位

代码

#include <stdio.h>
/********************************* 
* 方法1: 第一维的长度可以不指定 * 
*                但必须指定第二维的长度 *                             
*********************************/ 
void print_a(int a[][5], int n, int m)     
{ 
          int i, j; 

          for(i = 0; i < n; i++) 
          { 
                      for(j = 0; j < m; j++) 
                            printf("%d ", a[i][j]); 

                      printf("\n"); 
          } 
} 
/***************************************** 
*方法2: 指向一个有5个元素一维数组的指针 * 
*****************************************/ 
void print_b(int (*a)[5], int n, int m)         //数组元素的类型
{ 
          int i, j; 

          for(i = 0; i < n; i++) 
          { 
                      for(j = 0; j < m; j++) 
                            printf("%d ", a[i][j]);
                      printf("\n"); 
          } 
}
/*********************************** 
*方法3: 利用数组是顺序存储的特性, * 
*              通过降维来访问原数组!          * 
***********************************/ 
void print_c(int *a, int n, int m)                           
{ 
          int i, j; 

          for(i = 0; i < n; i++) 
          { 
                      for(j = 0; j < m; j++) 
                            printf("%d ", *(a + i*m + j)); 

                      printf("\n"); 
          } 
}
int main(void) 
{ 
        int a[5][5] = {{1, 2}, {3, 4, 5}, {6}, {7}, {0, 8}}; 

        printf("\n方法1:\n");   
        print_a(a, 5, 5); 

        printf("\n方法2:\n");   
        print_b(a, 5, 5);   

        printf("\n方法3:\n");   
        print_c(&a[0][0], 5, 5); 

        getch(); 
        return 0; 
} 

 

参考

http://guoyiqi.iteye.com/blog/1626922


 

C++:多维数组的动态分配(new)和释放(delete)

分配一个元素,和分配一个数组,都是用int* 来保存。

int *a = new int(5);
int *arr = new int[5];

对于简单的一维数组动态内存分配和释放,相信大家都是知道的,不过还是举个例子吧:

1 int *array1D;
2 //假定数组长度为m
3 //动态分配空间
4 array1D = new int [m];
5 //释放
6 delete [] array1D;

但是,对于多维数组动态分配,大家可能不太熟悉。下面以常见的二维和三维数组为例来说明:
1. 二维数组的动态分配和释放

int **array2D;
//假定数组第一维长度为m, 第二维长度为n
//动态分配空间
array2D = new int * [m];
for( int i=0; i<m; i++ )
{
  array2D[i] = new int [n] ;
}
//释放
for( int i=0; i<m; i++ )
{
  delete [] arrar2D[i];
}
delete [] array2D;

三维数组的动态分配和释放
int ***array3D;
//假定数组第一维为m, 第二维为n, 第三维为h
//动态分配空间
array3D = new int ** [m];
for( int i=0; i<m; i++ )
{
  array3D[i] = new int *[n];
for( int j=0; j<n; j++ )
{
  array3D[i][j] = new int [h];
}
}
//释放
for( int i=0; i<m; i++ )
{
for( int j=0; j<n; j++ )
{
delete []array3D[i][j];
}
delete []array3D[i];
}
delete []array3D;
————————————————
 

1.
A (*ga)[n] = new A[m][n];
...
delete []ga;
缺点:n必须是已知
优点:调用直观,连续储存,程序简洁(经过测试,析构函数能正确调用)

2. A** ga = new A*[m];
for(int i = 0; i < m; i++)
ga[i] = new A[n];
...
for(int i = 0; i < m; i++)
delete []ga[i];
delete []ga;
缺点:非连续储存,程序烦琐,ga为A**类型
优点:调用直观,n可以不是已知

 


 

指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。

2.1指针数组

char *arr[4] = {"hello", "world", "shannxi", "xian"};

//arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。

2.2数组指针

首先来定义一个数组指针,既然是指针,名字就叫pa

char (*pa)[4];
pa是一个指针, 指向一个char [4]的数组,每个数组元素是一个char类型的变量

既然pa是一个指针,存放一个数组的地址,那么在我们定义一个数组时,数组名称就是这个数组的首地址,那么这二者有什么区别和联系呢?

char a[4];,
a是一个长度为4的字符数组,a是这个数组的首元素首地址。

既然a是地址,pa是指向数组的指针,那么能将a赋值给pa吗?答案是不行的!

因为a是数组首元素首地址pa存放的却是数组首地址a是char 类型,a+1,a的值会实实在在的加1;而pa是char[4]类型的,pa+1,pa则会加4

虽然数组的首地址和首元素首地址的值相同,但是两者操作不同,所以类型不匹配不能直接赋值,

但是可以这样:pa = &a,pa相当与二维数组的行指针,现在它指向a[4]的地址。


 

http://c.biancheng.net/view/2022.html

 

二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

从概念上理解,a 的分布像一个矩阵:

0   1   2   3
4   5   6   7
8   9  10  11

但在内存中,二维数组a 的分布是一维线性的,整个数组占用一块连续的内存

 

 

C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示: 

 

 

 

为了更好的理解指针和二维数组的关系,我们先来定义一个指向 a 的指针变量 p:

int (*p)[4] = a;

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。

[ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针,这在《C语言指针数组》中已经讲到。

对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。

数组名 a 在表达式中也会被转换为和 p 等价的指针!

下面我们就来探索一下如何使用指针 p 来访问二维数组中的每个元素。按照上面的定义:
1) p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行

2) *(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素,下面的运行结果有力地证明了这一点:

#include <stdio.h>

int main(){

  int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

  int (*p)[4] = a;

  printf("%d\n", sizeof(*(p+1)));

  return 0;

}

运行结果:

16

3) *(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢?

*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;

就像一维数组的名字,在定义时或者和 sizeof、&一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针

4) *(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据

根据上面的结论,可以很容易推出以下的等价关系:

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

【实例】使用指针遍历二维数组。

#include <stdio.h>

int main(){

  int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};

int(*p)[4];    // 没有使用int **p2这样的方式,这种方式下无法用p2进行有效的数组变量,会越界,见上面图

int i,j;

p=a;

for(i=0; i<3; i++){

  for(j=0; j<4; j++) printf("%2d ",*(*(p+i)+j));

  printf("\n");

  }

  return 0;

}

运行结果:

 0   1   2   3
 4   5   6   7
 8   9  10  11

指针数组和二维数组指针的区别

指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:

  1. int *(p1[5]);  //指针数组,可以去掉括号直接写作 int *p1[5];
  2. int (*p2)[5]; //二维数组指针,不能去掉括号

指针数组和二维数组指针有着本质上的区别:

指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。

二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。