精华内容
下载资源
问答
  • 文章目录二维数组1 一维数组中的==指针数组=...解引用是指针说法4.2 为了方便理解,再一次详细描述一下5 理解指针数组的本质==内存空间的分配和使用5.1 二维数组的比较5.2 从个方面对它们进行讨论5.2.1 内容:5.2

    二维数组

    1 一维数组中的指针数组数组的指针区别

    1.1 指针数组

    int *p[N];
    

    读解,此时,[]优先级比*高,先结合进行运算,即首先,它成为了一个数组。

    • 它的大小就是N,即可以存放N个元素

    但是什么元素呢?

    • int *这玩艺儿一看就眼熟了,这不就是整型指针吗?是的,就是整型指针
    • 也就是说,元素是整型指针
    • 故叫指针数组

    说白了,它仍然是数组。就这里的这个定义来看,它和普通的整型数组不一样。因为,它里面只能装==指向整型变量的指针==。

    1.2 数组的指针,即行指针

    前面的叫指针数组,那么这个数组的指针能不能简称为数组指针呢?我个人觉得不可以。

    但确实也有人学着学着,就这么叫了。

    先看具体的一个示例

    int (*p)[N];
    

    大家都知道,()的优先级最高,那么,它里面的表达式优先运算,则计算结果就是:

    • *p是指针,再往前看,就是int,也就是说,是指针的基类型是整型

    • []是数组的标识,前面是指针,然后又做成了数组,这就不好理解了

      • 数组内有N个元素,基类型肯定也是整型
      • 普通情况,()这个位置里,应该是一个变量名,而这时,变成了一个指针
      • 所以,它是一个数组的指针,含义是定义了一个指向N个元素的一维数组的指针

    1.3 指针可以指向什么?

    int a;		// 变量===本质上就是一个单位的整型内存空间
    int *p;		// 变量的指针
    p = &a;		// 让指针指向对应的内存空间
    
    
    int arr[N];		// 数组
    int (*p)[N];	// 数组的指针
    p = &arr;		// 指针指向数组对应的内存空间【的首地址】
    

    可以明确地看到,什么样的指针,指向什么样的内存空间,必须对应,才可以完成指向,否则,编译出错。

    1.4 再看指针数组

    #include<stdio.h>
    #define N 5
    int main(void)
    {
        int *p[N];	// 定义指针数组,也就是说,这是定义了一组指针
        int a = 10;
        int b = 20;
        int c = 30;
    
        // 一个个完成指针数组里的元素的赋值,指针赋值,即完成指向初始化
        p[0] = &a;
        p[1] = &b;
        p[2] = &c;
        
        printf("变量a = %d, 指针pa = 0X%p, 指针解引用*pa = %d\n", a, p[0], *p[0]);
        printf("变量b = %d, 指针pb = 0X%p, 指针解引用*pb = %d\n", b, p[1], *p[1]);
        printf("变量c = %d, 指针pc = 0X%p, 指针解引用*pc = %d\n", c, p[2], *p[2]);
    
        return 0;
    }
    

    运行结果如下:

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point01.c' -o 'arr_point01.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point01' }
    变量a = 10, 指针pa = 0X000000000061FE44, 指针解引用*pa = 10
    变量b = 20, 指针pb = 0X000000000061FE48, 指针解引用*pb = 20
    变量c = 30, 指针pc = 0X000000000061FE4C, 指针解引用*pc = 30
    

    从程序运行可以看出:

    1. 这是一组指针,还有两个未用到
    2. 指针占用4个字节的内存大小,顺序排列
    3. 指针完成指向时,要取普通变量的地址
    4. 指针解引用可以直接取值

    基本知识点:

    • &为取地址
    • *为取值,即指针解引用

    1.5 数组的指针之赋值

    #include<stdio.h>
    
    #define N 5
    
    int main(void)
    {
        int (*p)[N];        // 这是一个数组的指针,只有指向有N个元素的数组【本质上,是一个行指针】
        int a[N] = {1, 2, 3, 4, 5};
        int i;
    
        p = &a;             // a是数组名,本身也是地址,即数组首地址,但这个指针是数组的指针,就要直接取数组的地址
    
        printf("查看内地址:\n");
        printf("\n指针本身的地址:0X%p", &p);
        printf("\n指针指向的地址:0X%p", p);
        printf("\n数组的首地址:0X%p", a);
        printf("\n数组的首元素的地址:0X%p", &a[0]);
        printf("\n数组的地址:0X%p", &a);
        
        printf("\n指针解引用值:%d", *p[0]);
    
        a[0] = 11;
        *p[0] = 111;    // 刚好首行首列,操作有效
        *p[1] = 22;     // 没有这一行,操作无效
        *p[3] = 33;     // 没有这一行,操作无效
        printf("\n打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
    
        // 使用行指针时,先取行,再取行上的列,注意解引用的顺序
        *(*(p)+0) = 112;      // 首行0列  
        *(*(p)+1) = 22;       // 首行1列  
        *(*(p)+2) = 33;       // 首行2列  
    
        printf("\n再打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
        
    
        return 0;
    }
    

    运行结果如下:

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point02.c' -o 'arr_point02.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point02' }
    查看内地址:
    
    指针本身的地址:0X000000000061FE08
    指针指向的地址:0X000000000061FE10
    数组的首地址:0X000000000061FE10
    数组的首元素的地址:0X000000000061FE10
    数组的地址:0X000000000061FE10
    指针解引用值:1
    打印数组里的值:111 2 3 4 5
    再打印数组里的值:112 22 33 4 5
    

    从程序运行可以看出:

    1. 数组的首地址、首个元素的地址、数组的地址,都是同一个地址
    2. 指针本身是要占用内存空间的,它是指针变量
    3. 指针完成指向后,可以存取该地址单元【基类型空间大小】
    4. 数组的指针指向一个数组,这个数组,就是一个单元
    5. 数组的指针,第一次解引用,就是取行,再一次解引用,就是取列
    6. 所以,数组的指针,可以直接和二维数组对应

    1.6 进一步观察行指针,即数组的指针的移动

    #include<stdio.h>
    
    #define N 5
    
    int main(void)
    {
        int (*p)[N];        // 这是一个数组的指针,只有指向有N个元素的数组【本质上,是一个行指针】
        int a[N] = {1, 2, 3, 4, 5};
        int i;
    
        p = &a;             // a是数组名,本身也是地址,即数组首地址,但这个指针是数组的指针,就要直接取数组的地址
    
        printf("查看内地址:\n");
        printf("\n指针本身的地址:0X%p", &p);
        printf("\n指针指向的地址:0X%p", p);
        printf("\n数组的首地址:0X%p", a);
        printf("\n数组的首元素的地址:0X%p", &a[0]);
        printf("\n数组的地址:0X%p", &a);
        
        printf("\n指针解引用值:%d", *p[0]);
    
        a[0] = 11;
        *p[0] = 111;    // 刚好首行首列,操作有效
        *p[1] = 22;     // 没有这一行,操作无效
        *p[3] = 33;     // 没有这一行,操作无效
        printf("\n打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
    
        // 使用行指针时,先取行,再取行上的列,注意解引用的顺序
        *(*(p)+0) = 112;      // 首行0列  
        *(*(p)+1) = 22;       // 首行1列  
        *(*(p)+2) = 33;       // 首行2列  
    
        printf("\n再打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
    
        printf("\n数组的内存大小:%d字节【十六进制】", (int)sizeof(a));
        printf("\n完成指向后的p指针所指的地址:0X%p", p);
        printf("\n完成指向后的p指针移动【行移动】所指的地址:0X%p", p + 1);
        printf("\n完成指向后的p指针移动【行移动】所指的地址:0X%p", p + 2);
        printf("\n完成指向后的p指针移动【列移动】所指的地址:0X%p", *(p));
        printf("\n完成指向后的p指针移动【列移动】所指的地址:0X%p", *(p) + 1);
    
        return 0;
    }
    

    查看最后的运行结果

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point02.c' -o 'arr_point02.exe' -Wall -g
    -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point02' }
    查看内地址:
    
    指针本身的地址:0X000000000061FE08
    指针指向的地址:0X000000000061FE10
    数组的首地址:0X000000000061FE10
    数组的首元素的地址:0X000000000061FE10
    数组的地址:0X000000000061FE10
    指针解引用值:1
    打印数组里的值:111 2 3 4 5
    再打印数组里的值:112 22 33 4 5
    数组的内存大小:20字节【十六进制】
    完成指向后的p指针所指的地址:0X000000000061FE10
    完成指向后的p指针移动【行移动】所指的地址:0X000000000061FE24
    完成指向后的p指针移动【行移动】所指的地址:0X000000000061FE38
    完成指向后的p指针移动【列移动】所指的地址:0X000000000061FE10
    完成指向后的p指针移动【列移动】所指的地址:0X000000000061FE14
    

    最后四行表明

    1. 行移动,一次是20个字节,刚好就是五个int的字节数
    2. 列移动,一次是4个字节,即一个int的字节数

    2 对于数组的地址

    2.1 概念

    • 数组的地址
    • 数组的首地址
    • 数组元素的地址,首元素的地址
    • 二维数组a[m][n]
      • 数组首地址
      • 首行地址
      • 首行首列地址
      • 第一个元素【仍然是数组】的地址,即a[0]的地址
      • 第一个数据元素的地址,即a[0][0]的地址

    一维数组

    int a[5];
    

    a表示的是数组的首地址,a等价于&a[0]

    二维数组

    int a[2][2] = {1, 2, 3, 4};
    

    a表示的整个数组的首地址,a[0]表示的是第一行的首地址,这两者者在数值上是一样的,但含义不同(或者说类型不同),数组名a是对于整个数组,a[0]是对于第一行

    从上面的示例运行结果来看,有些地址就是同一个地址

    在用数组的地址进行赋值的时候,虽然三者值相同,但是三者不可随意混用(以int a[2][2]为例)

    a--------是int (*)[2]型

    a[0]-----是int *型

    对于a[0]&a[0][0],两个类型都是int *型的,所以下述两种赋值方法等价

    第一种:

    int a[2][2] = {1, 2, 3, 4};
    int *p;
    p = a[0];
    

    第二种:

    int a[2][2] = {1, 2, 3, 4};
    int *p;
    p = &a[0][0];
    

    对于int a[2][2]来说,如果将a[0]改为&a[0],那么&a[0]和a的类型相同,都为int (*)[2]类型,下面以int a[5][5]为例,列出了二维数组的元素在不同方式表达下的不同类型。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BW6lOvME-1607311364560)(20201120-C语言-二维数组.assets/image-20201120145832572.png)]

    也可以用一维指针数组来保存二维数组中某个元素的地址

    int a[2][2] = {1, 2, 3, 4};
    int *p[2];
    p[0] = &a[0][0];
    printf("%d", *p[0]);
    

    3 二维数组的解引用

    以二维数组

    a[2][3]={1, 2, 3, 4 ,5, 6};
    

    为例(第一维是行,第二维是列)

    第一种:*(*a+1)--------等价于a[0][1],因为*的优先级比+高,所以先解引用,进入第二维在第二维里面地址+1,再次解引用得到元素值

    第二种:*(*(a+1))------等价于a[1][0],比上面第一种多加了一个括号,括号优先级最高,先+1移动地址(注意是在第一维里面移动即行上的移动),然后解引用进入第二维,再解引用得到元素的值

    第三种:*(&a[0][0]+1)-----等价于a[0][1],这里使用了&取地址符【注意,这里取出来的是变量元素即基元素的地址,地址就是指针,如果指针移动,将以它为基准,一次移动一个基元素内存大小,本质上,也就是列上的移动】,将原本表示第一个元素的a[0][0]返回到第二个维度,然后第二维地址+1,再解引用得到元素的值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbekSuFT-1607311364564)(20201120-C语言-二维数组.assets/image-20201120150728181.png)]

    对于a[2][3]的解引用的过程:

    1. 二维数组,共2行,3列
    2. 一维数组名,本质上是列指针,而二维数组名,本质上是行指针,但这些数组名,都是常指针,即,指向固定不变
    3. 一维数组名,一次解引用,即可获取对应列上的元素
    4. 二维数组名,二次解引用,才可以获取对应行的对应列上的元素
    5. 对于一维,在解引用之前,是可以让指向偏移的,但指针不需要移动,只是指向发生偏移
    6. 对于二维,在解引用一层之前,可以偏移,即【行偏移】,再一次解引用之前,还可以再偏移,即【列偏移】,偏移到指定位置后,再第二层解引用
    7. 直接取基元素的地址,则偏移一定是以基元素为准,一次一个基元素的内存单位大小
    8. 本质上:
    • 行指针,基元素变为一个一维数组
    • 列指针,基元素即为基元素本身
    • 基一旦发生变化,移动或是偏移时,指针跨过的内存单位大小就随之而变化
    1. 行指针,经过一次解引用,就化为列指针,仍然是地址;也就是说,行指针经过两次解引用后,也就是取值,不再是地址;
    2. 列指针,经过一次解此用,就是取值,不再是地址;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V8vTBpkk-1607311364570)(20201120-C语言-二维数组.assets/image-20201120150824653.png)]

    示例分析

    #include <stdio.h>
    
    int main(void)
    {
        int a[2][3] = {1, 2, 3, 4, 5, 6};
    
        printf("%d***%d\n", *(a[1]), (*a)[0]);
        printf("%d***%d\n", *(a[1]+1), (*a+1)[0]);
        printf("%d***%d\n", *(a[1]+1), (*a+1)[1]);
    
        return 0;
    }
    

    运行结果如下:

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point03.c' -o 'arr_point03.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point03' }
    arr_point03.c: In function 'main':
    arr_point03.c:5:19: warning: missing braces around initializer [-Wmissing-braces]
         int a[2][3] = {1, 2, 3, 4, 5, 6};
                       ^
                        {      } {      }
    4***1
    5***2
    5***3
    

    虽然有警告,也说明内存是线性,仍然可以一次贯穿了来完成赋值

    • 二维可以用一维的方式来初始化
    • 先移动,再解引用,还是先解引用,再移动,程序员自己要明白清楚
    • 数组的下标操作,和解引用有相同的效果,但下标是可以直接定位到相应的行或是列上的
      • 行标定位行
      • 列标才定位列
      • 对于二维数组,带行标的,肯定还是列指针,还可以做偏移,还可以再解引用
      • 对于一维数组,只能带列标,带上列标,即取值
      • 对于行指针,解引用一次后,就成了一维数组上的列指针,再带上列标,即取值

    4 解引用和下标

    4.1 下标是数组说法,解引用是指针说法

    *(a[1]+1)--------表示的是a[1][1]的值

    过程解析:

    1. 行标a[1],标号为1,即第二行,转化为列指针
    2. 偏移a[1]+1,偏移量为1,即第二行第二列,仍然是列指针
    3. 解引用*(a[1]+1),即取值,取的就是a[1][1]元素的值

    (*a+1)[1]--------表示的是a[0][2]的值

    过程解析:

    1. 解引用*a,由行指针转为列指针,指在首行首列,即第1列
    2. 偏移(*a+1),指在首行第2列
    3. 取列标(*a+1)[1],仍然是一个数组,如果是列标为[0]即当前所指位置,而这时,列标号为1,即偏移一个基元素,也就是取首行第二列的下一个元素,即首行第三列的元素,即a[0][2]元素

    4.2 为了方便理解,再一次详细描述一下

    先退回一维数组,以

    int a[5];
    

    来说,a表示的数组a的首地址,a[2]表示在a的基础上移动2个地址(注意a的类型是int *型的),再解引用得到元素的值,意思是a[2]

    实际上包含了两步

    • 第一步地址移动
    • 第二步解引用得到元素的值(注意第二步,有点隐式转换的意思,经常被人忽略)

    现在来解释上面的二维数组就容易多了

    • 先来看第一个*(a[1]+1)

      • a[1]代表第二行的首地址,注意这里的维度已经是第二维度了
      • 然后括号优先第二维地址+1
      • 最后解引用得到元素的值
    • 再看第二个(*a+1)[1],这里提一句,因为[]的优先级是比高的所以这里的括号不能去掉

      • 第一步先解引用进入第二维度(*优先级高于+)
      • 然后第二维地址+1
      • 然后再在当前基础上再移动一次地址,只要不是[0],就会发生位置偏移
      • 最后下标取值
      • 得到元素的值,这里可能有点绕,换个说法就是[1]是在当前维度进行移动,然后解引用(“当前维度”有点不太严谨,为了方便理解先将就这么用了)

    a[2][1]来说一共有四步

    • 其中包含了两次地址移动,两次解引用
    • 执行顺序是:
      • 地址移动->解引用->地址移动->解引用
      • (这里提一句,[]的结合性是左结合的,所以在移动的时候先移动行(第一维)再移动列(第二维))

    详细步骤:

    • 第一步:在当前维度地址+2,因为a的维度是第一维,所以是第一维地址+2,即行+2
    • 第二步:解引用进入第二维度
    • 第三步:在当前维度地址+1,因为这时已经进入第二维,所以第二维地址+1,即列+1
    • 第四步:解引用得到元素的值

    5 理解指针数组的本质==内存空间的分配和使用

    概括的说,指针其实就是可变数组的首地址,说是可变数组,是指其包含内容的数量的可变的,并且是可动态申请和释放的,从而充分节约宝贵的内存资源。我一向喜欢一维数组,除非万不得已,我一般是不用二维数组的,多维的则更是很少涉足了。因为一维简单,容易理解,而用指针指向的多维数组就具有相当的复杂性了,也因此更具有讨论的必要。

    本质上,就是为了更好地使用和操纵内存

    5.1 三个二维数组的比较

    int **Ptr;
    int *Ptr[5];
    int (*Ptr)[5];
    

    三例都是整数的二维数组,都可以用形如 Ptr[0][0] 的方式访问其内容;但它们的差别却是很大的。

    5.2 从四个方面对它们进行讨论

    5.2.1 内容:

    ​ 它们本身都是指针,它们的最终内容都是整数。注意这里说的是最终内容,而不是中间内容,比如你写 Ptr[ 0 ],对于三者来说,其内容都是一个整数指针,即 int *Ptr[1][1] 这样的形式才是其最终内容。

    5.2.2 意义:

    ​ (1)、int **Ptr 表示指向"一群"指向整数的指针的指针。【可以认为是指针数组,只能指向指针,这些被指向的指针是整型指针】
    ​ (2)、int *Ptr[5] 表示指向 5 个指向整数的指针的指针。【就是5个指针,成了一组】
    ​ (3)、int (*Ptr)[5] 表示指向"一群"指向 5 个整数数组的指针的指针。【即数组的指针,只能指向数组,不能指向整型元素】

    5.2.3 所占空间:

    ​ (1)、int **Ptr 和 (3)、int (*Ptr)[5] 一样,在32位平台里,都是4字节,即一个指针。但 (2)、int *Ptr[5] 不同,它是 5 个指针,它占5 * 4 = 20个字节的内存空间。

    5.2.4 用法:

    ​ (1)、int **Ptr

    ​ 因为是指针的指针,需要两次内存分配才能使用其最终内容。首先,Ptr = (int **)new int *[5];这样分配好了以后,它和(2)的
    意义相同了;然后要分别对 5 个指针进行内存分配,例如:Ptr[0] = new int[20];它表示为第 0 个指针分配 20 个整数,分配好以后, Ptr[0] 为指向 20 个整数的数组。这时可以使用下标用法 Ptr[0][0]Ptr[0][19]了。
    ​ 如果没有第一次内存分配,该 Ptr 是个"野"指针,是不能使用的,如果没有第二次内存分配,则 Ptr[0] 等也是个"野"指针,也是不能用的。当然,用它指向某个已经定义的地址则是允许的,那是另外的用法(类似于"借鸡生蛋"的做法),这里不作讨论(下同)。
    ​ (2)、int *Ptr[5]
    ​ 这样定义的话,编译器已经为它分配了 5 个指针的空间,这相当于(1)中的第一次内存分配。根据对(1)的讨论可知,显然要对其进行一次内存分配的。否则就是"野"指针。
    ​ (3)、int (*Ptr)[5]
    ​ 这种定义我觉得很费解,不是不懂,而是觉得理解起来特别吃力,也许是我不太习惯这样的定义吧。怎么描述它呢?它的意义是"一群"指针,每个指针都是指向一个 5 个整数的数组。如果想分配 k 个指针,这样写:

    Ptr = (int(*)[5]) new int[sizeof(int)*5*k]

    这是一次性的内存分配。分配好以后,Ptr 指向一片连续的地址空间,其中 Ptr[0] 指向第 0 个 5 个整数数组的首地址,Ptr[1] 指向第1 个 5 个整数数组的首地址。

    综上所述,我觉得可以这样理解它们:
    int ** Ptr <==> int Ptr[ x ][ y ];
    int *Ptr[ 5 ] <==> int Ptr[ 5 ][ x ];
    int ( *Ptr )[ 5 ] <==> int Ptr[ x ][ 5 ];
    这里 x 和 y 是表示若干的意思。

    6 指针数组(数组每个元素都是指针)详解

    如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。

    指针数组的定义形式一般为:

    dataType *arrayName[length];
    

    [ ]的优先级高于*,该定义形式应该理解为:

    dataType *(arrayName[length]);
    

    括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

    除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:

    #include <stdio.h>
    int main()
    {    
        int a = 16, b = 932, c = 100;    //定义一个指针数组    
        int *arr[3] = {&a, &b, &c};	//也可以不指定长度,直接写作 int *arr[]    
        
        //定义一个指向指针数组的指针    
        int **parr = arr;    
        printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);    
        printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));    
        
        return 0;
    }
    

    运行结果:
    16, 932, 100
    16, 932, 100

    arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

    parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。

    第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。

    第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

    指针数组还可以和字符串数组结合使用,请看下面的例子:

    #include <stdio.h>
    int main()
    {    
        char *str[3] = {        
            "www.cuit.edu.cn",        
            "数学学院学习C语言",        
            "C Language"    
        };    
        
        printf("%s\n%s\n%s\n", str[0], str[1], str[2]);    
        
        return 0;
    }
    

    运行结果:
    www.cuit.edu.cn
    数学学院学习C语言
    C Language

    需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

    也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。

    为了便于理解,可以将上面的字符串数组改成下面的形式,它们都是等价的。

    #include <stdio.h>
    int main()
    {
        char *str0 = "www.cuit.edu.cn";
        char *str1 = "数学学院学习C语言";
        char *str2 = "C Language";
        char *str[3] = {str0, str1, str2};
    
        printf("%s\n%s\n%s\n", str[0], str[1], str[2]);    
        
        return 0;
    }
    

    7 二维数组的内存理解

    7.1 基本概念理解

    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 的分布是一维线性的,整个数组占用一块连续的内存:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UeLCBcZf-1607311364577)(20201120-C语言-二维数组.assets/image-20201120160552706.png)]

    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,那么每个一维数组的首地址如下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8V4NRlep-1607311364582)(20201120-C语言-二维数组.assets/image-20201120160640958.png)]

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

    int (*p)[4] = a;		// 典型应用:行指针指向二维数组
    

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

    [ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

    数组名 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

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

    *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

    1. *(*(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 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
    

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

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

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

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

    • 指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。
    • 二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。
    • 所有的指针,占用的内存空间大小是一样的
    • 但指向的内容是由它的基类型决定的
    • 所以void型的指针,可以强转为其它任意类型
    展开全文
  • §4-3非稳态导热问题数值解法由前可知:非稳态导热和稳态导热...一、一维非稳态导热时间——空间区域离散化1 、基本概念如图 4-8 所示, x 为空间坐标, τ 为时间坐标。1 )时间步长:指从一个时间层到下一个时...

    §4-3非稳态导热问题的数值解法

    由前可知:非稳态导热和稳态导热二者微分方程的区别在于控制方程中多了一个非稳态项,其中扩散项的离散方法与稳态导热一样。

    本节重点讨论:( 1 )非稳态项离散的方法;

    ( 2 )扩散项离散时所取时间层的不同对计算带来的影响。

    一、一维非稳态导热时间——空间区域的离散化

    1 、基本概念

    如图 4-8 所示, x 为空间坐标, τ 为时间坐标。

    1 )时间步长

    :指从一个时间层到下一个时间层的间隔

    2 )节点( n, i )——表示空间网格线与时间网格线的交点,即表示了时间——空间区域中一个节点的位置,相应的记为:

    2 、非稳态项的离散

    非稳态项的离散有三种不同的格式:

    1 )向前差分

    2 )向后差分

    3 )中心差分

    1 )向前差分

    将函数 t 在节点( n,i+1 )对点( n,i )作泰勒展开,则有:

    其中 0 (

    )截断误差表示余项中

    的最低阶为一次。

    由上式得:函数 t 在节点( n,i+1 )对点( n,i )处一阶导数的向前差分公式:

    ( 4-10 ) 2 )向后差分

    将函数 t 在节点( n,i-1 )对点 (n,i) 作泰勒展开,可得

    的向后差分公式:

    ( 4-11 )

    3 )中心差分

    的向前差分与向后差分之和,即得

    的中心差分表达式:

    二、一维非稳态导热微分方程的离散方法

    1 、泰勒级数展开法

    1 )一维非稳态导热微分方程中的扩散项离散与稳态导热微分方程中的方法相同,则

    对一维非稳态导热微分方程中

    的扩散项 → 中心差分;

    非稳态项 → 向前差分。

    ( 1 )非稳态项:

    采用向前差分为:

    ( 4-13 )

    ( 2 )稳态项:

    采用中心差分则为:

    ( 4-14 )

    由此可得:

    变形得:

    ( 4-15 )

    由此可见,只要 i 时层上各节点的温度已知,那么 i+1 时层上各节点的温度即可算出,且不需设立方程组求解。此关系式即为显式差分格式。

    2 )显示差分与隐式差分格式

    求解非稳态导热微分方程,是从已知的初始温度分布出发,根据边界条件依次求得以后各个时间层上的温度值。

    ⑴ 显示差分格式

    定义:就是指若已知 i 时层上各节点的温度值,根据该差分格式即可算出( i+1 )

    层上各内点的温度,而不必求解联立方程。即

    是前一时刻( i ) n 节点及相邻两节点温度的显函数。

    优点:计算工作量小;

    缺点:受时间及空间步长的限制。

    ⑵ 隐式差分格式

    对一维非稳态导热微分方程

    中的扩散项在 (i+1) 时层上采用中心差分,非稳态项将 t 在节点( n,i+1 )处对节点( n,i )采用向前差分,得:

    (4-16)

    式中,已知的是 i 时层上的值

    ,而未知量有 3 个,无法求解

    定义:就是指已知 i 时层上各节点的温度值

    ,根据差分格式不能直接算出 (i+1) 时层上各节点的温度,而必须求解( i+1 )时层上的一个联立方程组,才能算出 (i+1) 时层各节点的温度,此种差分格式称隐式差分格式。

    优点:不受时间及空间的步长影响;

    缺点:计算工作量大。

    综上可知: ① 非稳态导热微分方程中,扩散项采用中心差分,非稳态项采用向前差分得到显式差分格式;

    ② 非稳态导热微分方程中,扩散项采用中心差分,非稳态项采用向后差分得到隐式差分格式。

    2 、热平衡法

    1 )优点:( 1 )不受网格是否均匀限制;

    ( 2 )不受物体是否为常数限制。

    2 )求解方法

    如图 4-9 所示,一无限大平板,右侧面受周围流体的冷却,表面传热导数为 h ,对于边界节点 N 代表了宽为

    的元体。

    对于该元体,根据傅立叶定律和能量守恒定律得:

    =

    变形为

    ( 4-17 )

    其中

    ——是以

    为特征长度的傅里叶数,称网络傅里叶数,记为:

    ——是以

    为特征长度的毕渥数,称网络毕渥数,记为:

    一项变形如下:

    所以

    =

    (

    2

    2

    ) +2

    +2

    ( 4-18 )

    补充:4-17 的推导过程

    对于

    的元体:

    ⑴ 根据傅立叶定律,在 i 时层上,从节点 N — 1 传导给节点 N 的热流量,即从 N — 1 传给元体

    单位面积的热流量为:

    ( a )

    ⑵ 根据牛顿冷却公式,平板右侧被冷却时,在 i 时层上其单位面积损失的热流量为:

    ( b )

    ⑶ 在 i 时层上元体

    热力学能的增量:

    其中

    ⑷ 根据能量守恒定律可知:在 I 时层通过导热和对流进入元体

    的能量应等于元体热力学能的变化量,即

    =

    变形为:

    =

    (

    2

    2

    ) +2

    +2

    ( 4-18 )

    说明:对多维非稳态导热问题应用热平衡法来建立离散方程的原则与过程与之类似。

    三、讨论一维导热问题显式差分格式稳定性限制的物理意义

    从离散方程的结构分析:

    对于一维导热显式格式的内节点方程

    , 其中

    由方程式得知,点 n 上 i+1 时刻的温度是在该点 i 时刻温度的基础上计及了左右两邻点温度的影响后得出的。若两邻点的影响保持不变,则合理的情况是:

    越高,则

    越高;

    越低,则

    越低。

    在上式中,满足这种合理性是有条件的,即上式中

    前的系数必大于等于零,即

    (1-2

    )

    0

    亦即:

    否则,将出现不合理情况。若 (1-2

    )<0 ,则表明节点( n,i )在 i 时刻的

    越高,经

    时段后,

    越低,这种节点温度随时间的跳跃式变化是不符合物理规律的,所以称该方程具有不稳定性。

    2 、对于一维导热显示格式的对流边界节点方程:

    得出合理解的条件是:

    2

    2

    0

    即:

    由此可见:( 1 )对流边界节点要得到的合理的解,其限制条件比内节点更为严格,所以,当由边界条件及内节点的稳定性条件得出的

    不同时,应选较小的

    来确定允许采用的

    ( 2 )对于第一、二类边界条件,其限制条件只有内节点的限制条件。

    ( 3 )内边界节点差分方程的稳定性条件不同,但在数值计算时,二节点又必须选择相同的

    x ,

    。因此,在选择了

    x 后,则

    的选择就要受到稳定条件的限制,不能任意选择,而必须按两节点的稳定性条件分别计算

    ,取其中较小

    作为时间步长,方能满足二者稳定性要求。

    四、数值解法的求解步骤

    1 、首先选择空间坐标间隔

    ,即距离步长。对二维问题一般使

    y=

    x ;

    2 、对显式格式差分方程,根据方程的稳定性条件选择允许的最大时间步长

    ; 在稳定性条件允许范围内,

    越大,计算工作量越小,但精度较差;对一维问题,一般取

    ,即可满足工程精度要求;对于隐式差分方程,

    x ,

    可任意选取,不必进行稳定性条件校核;

    3 、按题意给定的初始温度分布,确定各节点上的温度初值

    4 、根椐建立的差分方程组,求

    时刻各节点的温度

    5 、再由

    为初值,换用

    (即 i=2 ),重复 4 计算出

    ,如此反复,最后得到 i 时刻的

    展开全文
  • 高斯消元法解线性方程组,行列式基本运算,矩阵则运算与基础知识,逆矩阵,伴随矩阵,矩阵初等变换,向量基本运算(注意矩阵和向量之间内积外积区别)基本上大学线性代数前4,5章节内容都需要过遍,看快点,...

    1月6日补充
    Eigen补充参考学习的资料
    Eigen官网:http://eigen.tuxfamily.org/dox/group__QuickRefPage.html#QuickRef_Types
    Eigen解方程组:https://blog.csdn.net/u013354805/article/details/48250547
    Eigen快速入门:https://www.cnblogs.com/python27/p/EigenQuickRef.html
    Eigen讲解资料:https://blog.csdn.net/houjixin/article/details/8494582

    补充的C++内容
    C++这个东西博大精深,slam方面要用到的除了C++的基本操作外,要学习包括类和对象、继承、重载。可以通过这个网站了解入门操作
    http://www.runoob.com/cplusplus/cpp-constructor-destructor.html
    模板和模板类:http://www.cnblogs.com/gaojun/archive/2010/09/10/1823354.html
    关键在于写代码和看代码,不是一两天可以学会搞定。

    (1)本节课内容

    1、旋转矩阵 2、旋转向量与欧拉角 3、四元数 4、Eigen库用法

    (2)需要的基础知识

    1、线性代数:高斯消元法解线性方程组,行列式基本运算,矩阵四则运算与基础知识,逆矩阵,伴随矩阵,矩阵初等变换,向量基本运算(注意矩阵和向量之间内积外积区别)基本上大学线性代数前4,5章节的内容都需要过一遍,看的快点,用时2小时内
    2、C++:因人而异了,C++Primer第一部分基础章节过一遍,然后重点看下类、引用,继承,重载、模板类的部分。
    3、有点空间想象力。

    (3)开发环境

    编译平台:ubuntu16.04,
    编译软件:IDE:Clion 编译器:Cmake 语言标准:C++11
    关于Clion安装请移步:https://blog.csdn.net/weixin_38593194/article/details/85122716
    关于Cmake用法请移步:https://blog.csdn.net/weixin_38593194/article/details/85077946

    (4)达成标准

    线性代数:

    1、说明Ax=b在什么情况下有解?有唯一解?无解?
    2、高斯消元法原理,行列式计算
    3、矩阵内外积运算区别,矩阵初等变换与四则运算
    4、如何利用矩阵解线性齐次方程组
    5、QR分解原理
    6、Cholesky分解原理

    旋转的表达:

    1、理解坐标系间的欧氏变换,旋转矩阵的构成,旋转轴和旋转角与罗德里格斯公式。
    2、理解欧拉角
    3、四元数的四则运算
    4、用四元数表示旋转矩阵

    Eigen使用

    1、使用Eigen进行矩阵的四则运算
    2、编程实现 A 为 100 × 100 随机矩阵时,⽤ QR 和 Cholesky 分解求 x 的程序。

    (5)填坑

    三维旋转:旋转矩阵,欧拉角,四元数:http://www.cnblogs.com/yiyezhai/p/3176725.html
    有兴趣可以看看

    0、旋转矩阵

    这块是学了李群和李代数后觉得需要补充出来的内容和性质,跟李群和李代数关系不大,跟本章后面需要理解和明确的定义和性质有关
    首先先来定义旋转的作用对象——刚体
    1、刚体:在外力作用下处于平衡状态的物体,如果物体的变形不影响其平衡位置及作用力大小和方向,则该物体可视为刚体。(这个定义表示作用力带来的速度的方向和旋转等变换也是不受形变影响的)。
    2、点:在几何学上点是 没有大小而只有位置,即点存在于三维空间中的某一个位置
    3、向量: 可以形象化地表示为 带箭头的线段。箭头所指:代表向量的方向;线段长度:代表向量的大小。 (请勿将向量与它的坐标两个概念混淆,向量有方向是矢量,坐标无方向是标量,坐标表示的是向量在某个坐标系下的位置)
    例如,在线性空间的基(e1,e2,e3),向量a(a1,a2,a3)在该基向量下的坐标是
    在这里插入图片描述
    左手坐标系和右手坐标系,常用右手
    在这里插入图片描述
    4、向量运算
    对于三维向量a(a1,a2,a3),b(b1,b2,b3)
    向量内积
    在这里插入图片描述
    向量外积在这里插入图片描述
    三维空间里,外积的方向垂直于a,b,是a,b共同的法向量,大小为两个向量张成的四边形面积。
    即:a×b=|a||b|sin<a,b>
    符号^表示反对称矩阵。扩充反对称矩阵的县官定义和性质
    在这里插入图片描述
    性质1
    两个反对称矩阵A,B,A±B也为反对称矩阵
    性质2
    对于反对称矩阵A,A的转置,xA都为反对称矩阵(x为常数)

    上面向量外积的公式需要记住,后面会用到。

    1、坐标系之间的欧式变换

    两个坐标系之间的旋转关系和平移关系,统称坐标系之间的变换。
    将地面设置为惯性坐标系或者世界坐标系W(认定其为固定不动的),机器人为移动坐标系C(随着运动可变坐标系)。如图Xw、Yw、Zw定义世界坐标系。机器人运动过程中,假设移动坐标系定义为Xc,Yc,Zc。某定点p,在移动机器人上的相机视野中Pc,在世界坐标系下看Pw,对同一个点有两个坐标,他们之间有什么关系?
    这时可以用一个矩阵T表述其中的转换关系,T表述旋转和平移两种操作。 在这里插入图片描述
    先理解旋转
    我们设某个单位正交基(e1; e2; e3) 经过一次旋转,变成了 (e′ 1; e′ 2; e′ 3)。那么,对于同一个向量 a(注意该向量并没有随着坐标系的旋转而发生运动),它在两个坐标系下的坐标为 [a1; a2; a3]T 和 [a′ 1; a′ 2; a′ 3]T。根据坐标的定义,有:
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20181224191928235.png?x-oss-
    在这里插入图片描述
    用旋转矩阵R可以用来描述相机的旋转。
    旋转矩阵特殊的性质:
    它是一个行列式为 1 的正交矩阵‹。反之,行列式为 1 的正交矩阵也是一个旋转矩阵。旋转角度越小,R越接近单位矩阵。旋转矩阵的集合定义如下:
    在这里插入图片描述
    SO(n) 是特殊正交群(Special Orthogonal Group)的意思。解释“群”的内容留到下一讲
    再看平移
    搞定了旋转,任何变换的平移都可看作在世界坐标系W下的三维移动,所以平移是不需要进行任何矩阵变换 的,设世界坐标系中向量a,经过一次旋转R和一次平移t后得到a‘,则把平移和旋转合在一起,有:a’=Ra+t
    这就是本节一开始所说的T(变换)
    ——————————————————————1月7日补充内容————————————————————————

    1.9变换矩阵

    这里引出的是变换矩阵的定义,后面李代数会用到,本节中需要知道变换矩阵怎么来的,表现成什么样子就行。
    上面我们使用a’=Ra+t表现了一次旋转后的关系,如果再进行2次连续的变换呢?a"=R1a’+t1。那n次该如何表示呢?
    在这里插入图片描述
    在这里插入图片描述
    由此,旋转可用旋转矩阵R组成的特殊正交群SO(3)表示,将平移和旋转放在一起可用变换矩阵T组成的特殊欧式群SE(3)表述。

    2、旋转向量

    任意旋转都可以用一个旋转轴和一个旋转角来表示,于是有了旋转向量。使用一个向量,其方向与旋转轴一致,长度等于旋转角。旋转向量就是下节要讲的SO(3)的李代数so(3),而变换矩阵的李代数则是se(3)旋转向量是三维的,变换矩阵的是6维(三维旋转+三维平移)
    接下来具体推一下旋转向量和旋转矩阵之间如何转换的。从向量到旋转矩阵的过程由
    罗德里格斯公式

    假设旋转轴n,角度θ的旋转,则旋转向量为θn****这个公式很重要,需要看结论的移步本节最后分割线。
    由图1得推导过程:
    在这里插入图片描述
    图1
    设k是定义旋转轴的单位矢量,并且令v是任何矢量以k为角度θ旋转(右手规则,图中为逆时针)。
    使用点和叉积,矢量v可以分解为平行和垂直于轴k的分量,
    在这里插入图片描述
    其中与k平行的分量是
    在这里插入图片描述
    ps:k是单位矢量,(v·k)k=|v||k|cos<v,k>k=|v|k(k表示矢量方向)
    称为矢量投影的v上ķ
    在这里插入图片描述
    矢量ķ × v可以被看作是副本v ⊥由90°逆时针旋转ķ,所以它们的大小相等,但方向是垂直的。同样地,矢量ķ ×(ķ × v)的副本v ⊥通过逆时针旋转180°约ķ,使得ķ ×(ķ × v)和v ⊥是在幅度,但方向相反相等(即它们各自的底片其他,因此减号)。(推导过程是旋转向量的概念,用旋转轴和旋转角表示旋转向量)
    则平行于轴的分量在旋转下不会改变幅度或方向,
    在这里插入图片描述
    垂直分量会改变方向但保持其大小
    在这里插入图片描述
    (上面公式描述的是一个旋转的过程,见下图)
    ps:向量的加法表述向量=水平分量+垂直分量

    这种旋转是正确的,因为矢量v ⊥和ķ × v具有相同的长度,并ķ × v是v ⊥通过逆时针旋转90°左右ķ。使用三角函数正弦和余弦对v⊥和k × v进行适当的缩放,得到旋转的垂直分量。旋转分量的形式类似于2D平面极坐标(r,θ)中的径向矢量 在笛卡尔的基础上
    其中e x,e y是指示方向的单位矢量。
    在这里插入图片描述
    现在完全旋转的矢量是
    在这里插入图片描述
    通过替换的定义v ∥rot和v ⊥rot在方程结果
    在这里插入图片描述
    在这里插入图片描述
    推导的罗德里格斯轮换公式
    在这里插入图片描述
    对于旋转矩阵R和旋转向量的关系可以表述为
    在这里插入图片描述在这里插入图片描述
    反之可计算从一个旋转矩阵到旋转向量的转换,对于转角θ
    在这里插入图片描述

    在这里插入图片描述

    ===================================================================分割线顶顶顶顶

    3、欧拉角

    在航空或者航模里经常用到,是一种符合人的思维、十分直观的描述物体在三个轴线上旋转的方式。
    对欧拉角的理解参考:https://blog.csdn.net/sinat_27456831/article/details/50042915
    但是欧拉角由万向节死锁的问题,在计算的过程中有奇异性,所以计算的时候不采用,在人机交互的时候采用欧拉角,会比较直观。

    4、四元数

    四元数是一种紧凑的无奇异的表述三维向量的方式。
    在这里插入图片描述
    i,j,k为三个虚部,他们满足关系式
    在这里插入图片描述
    由于其特殊性,有时候也采用一个标量一个向量的形式来表达四元数
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    5、Eigen

    Eigen是一个矩阵运算库,这部分其实不难,有个学习的例程,照着敲敲就会了,分享一个例子。
    链接:https://pan.baidu.com/s/1Lb7H7YiceNo6lW_UF71ptg
    提取码:k9p2

    6、C++11

    主要难点在于C++11,,,,引用,继承,模板类。等我学习两天再来补充。

    展开全文
  • 文章目录二维数组1 一维数组中的==指针数组=...解引用是指针说法4.2 为了方便理解,再一次详细描述一下5 理解指针数组的本质==内存空间的分配和使用5.1 二维数组的比较5.2 从个方面对它们进行讨论5.2.1 内容:5.2

    二维数组

    1 一维数组中的指针数组数组的指针区别

    1.1 指针数组

    int *p[N];
    

    读解,此时,[]优先级比*高,先结合进行运算,即首先,它成为了一个数组。

    • 它的大小就是N,即可以存放N个元素

    但是什么元素呢?

    • int *这玩艺儿一看就眼熟了,这不就是整型指针吗?是的,就是整型指针
    • 也就是说,元素是整型指针
    • 故叫指针数组

    说白了,它仍然是数组。就这里的这个定义来看,它和普通的整型数组不一样。因为,它里面只能装==指向整型变量的指针==。

    1.2 数组的指针,即行指针

    前面的叫指针数组,那么这个数组的指针能不能简称为数组指针呢?我个人觉得不可以。

    但确实也有人学着学着,就这么叫了。

    先看具体的一个示例

    int (*p)[N];
    

    大家都知道,()的优先级最高,那么,它里面的表达式优先运算,则计算结果就是:

    • *p是指针,再往前看,就是int,也就是说,是指针的基类型是整型

    • []是数组的标识,前面是指针,然后又做成了数组,这就不好理解了

      • 数组内有N个元素,基类型肯定也是整型
      • 普通情况,()这个位置里,应该是一个变量名,而这时,变成了一个指针
      • 所以,它是一个数组的指针,含义是定义了一个指向N个元素的一维数组的指针

    1.3 指针可以指向什么?

    int a;		// 变量===本质上就是一个单位的整型内存空间
    int *p;		// 变量的指针
    p = &a;		// 让指针指向对应的内存空间
    
    
    int arr[N];		// 数组
    int (*p)[N];	// 数组的指针
    p = &arr;		// 指针指向数组对应的内存空间【的首地址】
    

    可以明确地看到,什么样的指针,指向什么样的内存空间,必须对应,才可以完成指向,否则,编译出错。

    1.4 再看指针数组

    #include<stdio.h>
    #define N 5
    int main(void)
    {
        int *p[N];	// 定义指针数组,也就是说,这是定义了一组指针
        int a = 10;
        int b = 20;
        int c = 30;
    
        // 一个个完成指针数组里的元素的赋值,指针赋值,即完成指向初始化
        p[0] = &a;
        p[1] = &b;
        p[2] = &c;
        
        printf("变量a = %d, 指针pa = 0X%p, 指针解引用*pa = %d\n", a, p[0], *p[0]);
        printf("变量b = %d, 指针pb = 0X%p, 指针解引用*pb = %d\n", b, p[1], *p[1]);
        printf("变量c = %d, 指针pc = 0X%p, 指针解引用*pc = %d\n", c, p[2], *p[2]);
    
        return 0;
    }
    

    运行结果如下:

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point01.c' -o 'arr_point01.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point01' }
    变量a = 10, 指针pa = 0X000000000061FE44, 指针解引用*pa = 10
    变量b = 20, 指针pb = 0X000000000061FE48, 指针解引用*pb = 20
    变量c = 30, 指针pc = 0X000000000061FE4C, 指针解引用*pc = 30
    

    从程序运行可以看出:

    1. 这是一组指针,还有两个未用到
    2. 指针占用4个字节的内存大小,顺序排列
    3. 指针完成指向时,要取普通变量的地址
    4. 指针解引用可以直接取值

    基本知识点:

    • &为取地址
    • *为取值,即指针解引用

    1.5 数组的指针之赋值

    #include<stdio.h>
    
    #define N 5
    
    int main(void)
    {
        int (*p)[N];        // 这是一个数组的指针,只有指向有N个元素的数组【本质上,是一个行指针】
        int a[N] = {1, 2, 3, 4, 5};
        int i;
    
        p = &a;             // a是数组名,本身也是地址,即数组首地址,但这个指针是数组的指针,就要直接取数组的地址
    
        printf("查看内地址:\n");
        printf("\n指针本身的地址:0X%p", &p);
        printf("\n指针指向的地址:0X%p", p);
        printf("\n数组的首地址:0X%p", a);
        printf("\n数组的首元素的地址:0X%p", &a[0]);
        printf("\n数组的地址:0X%p", &a);
        
        printf("\n指针解引用值:%d", *p[0]);
    
        a[0] = 11;
        *p[0] = 111;    // 刚好首行首列,操作有效
        *p[1] = 22;     // 没有这一行,操作无效
        *p[3] = 33;     // 没有这一行,操作无效
        printf("\n打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
    
        // 使用行指针时,先取行,再取行上的列,注意解引用的顺序
        *(*(p)+0) = 112;      // 首行0列  
        *(*(p)+1) = 22;       // 首行1列  
        *(*(p)+2) = 33;       // 首行2列  
    
        printf("\n再打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
        
    
        return 0;
    }
    

    运行结果如下:

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point02.c' -o 'arr_point02.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point02' }
    查看内地址:
    
    指针本身的地址:0X000000000061FE08
    指针指向的地址:0X000000000061FE10
    数组的首地址:0X000000000061FE10
    数组的首元素的地址:0X000000000061FE10
    数组的地址:0X000000000061FE10
    指针解引用值:1
    打印数组里的值:111 2 3 4 5
    再打印数组里的值:112 22 33 4 5
    

    从程序运行可以看出:

    1. 数组的首地址、首个元素的地址、数组的地址,都是同一个地址
    2. 指针本身是要占用内存空间的,它是指针变量
    3. 指针完成指向后,可以存取该地址单元【基类型空间大小】
    4. 数组的指针指向一个数组,这个数组,就是一个单元
    5. 数组的指针,第一次解引用,就是取行,再一次解引用,就是取列
    6. 所以,数组的指针,可以直接和二维数组对应

    1.6 进一步观察行指针,即数组的指针的移动

    #include<stdio.h>
    
    #define N 5
    
    int main(void)
    {
        int (*p)[N];        // 这是一个数组的指针,只有指向有N个元素的数组【本质上,是一个行指针】
        int a[N] = {1, 2, 3, 4, 5};
        int i;
    
        p = &a;             // a是数组名,本身也是地址,即数组首地址,但这个指针是数组的指针,就要直接取数组的地址
    
        printf("查看内地址:\n");
        printf("\n指针本身的地址:0X%p", &p);
        printf("\n指针指向的地址:0X%p", p);
        printf("\n数组的首地址:0X%p", a);
        printf("\n数组的首元素的地址:0X%p", &a[0]);
        printf("\n数组的地址:0X%p", &a);
        
        printf("\n指针解引用值:%d", *p[0]);
    
        a[0] = 11;
        *p[0] = 111;    // 刚好首行首列,操作有效
        *p[1] = 22;     // 没有这一行,操作无效
        *p[3] = 33;     // 没有这一行,操作无效
        printf("\n打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
    
        // 使用行指针时,先取行,再取行上的列,注意解引用的顺序
        *(*(p)+0) = 112;      // 首行0列  
        *(*(p)+1) = 22;       // 首行1列  
        *(*(p)+2) = 33;       // 首行2列  
    
        printf("\n再打印数组里的值:");
        for ( i = 0; i < N; i++)
        {
            printf("%d ", a[i]);
        }
    
        printf("\n数组的内存大小:%d字节【十六进制】", (int)sizeof(a));
        printf("\n完成指向后的p指针所指的地址:0X%p", p);
        printf("\n完成指向后的p指针移动【行移动】所指的地址:0X%p", p + 1);
        printf("\n完成指向后的p指针移动【行移动】所指的地址:0X%p", p + 2);
        printf("\n完成指向后的p指针移动【列移动】所指的地址:0X%p", *(p));
        printf("\n完成指向后的p指针移动【列移动】所指的地址:0X%p", *(p) + 1);
    
        return 0;
    }
    

    查看最后的运行结果

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point02.c' -o 'arr_point02.exe' -Wall -g
    -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point02' }
    查看内地址:
    
    指针本身的地址:0X000000000061FE08
    指针指向的地址:0X000000000061FE10
    数组的首地址:0X000000000061FE10
    数组的首元素的地址:0X000000000061FE10
    数组的地址:0X000000000061FE10
    指针解引用值:1
    打印数组里的值:111 2 3 4 5
    再打印数组里的值:112 22 33 4 5
    数组的内存大小:20字节【十六进制】
    完成指向后的p指针所指的地址:0X000000000061FE10
    完成指向后的p指针移动【行移动】所指的地址:0X000000000061FE24
    完成指向后的p指针移动【行移动】所指的地址:0X000000000061FE38
    完成指向后的p指针移动【列移动】所指的地址:0X000000000061FE10
    完成指向后的p指针移动【列移动】所指的地址:0X000000000061FE14
    

    最后四行表明

    1. 行移动,一次是20个字节,刚好就是五个int的字节数
    2. 列移动,一次是4个字节,即一个int的字节数

    2 对于数组的地址

    2.1 概念

    • 数组的地址
    • 数组的首地址
    • 数组元素的地址,首元素的地址
    • 二维数组a[m][n]
      • 数组首地址
      • 首行地址
      • 首行首列地址
      • 第一个元素【仍然是数组】的地址,即a[0]的地址
      • 第一个数据元素的地址,即a[0][0]的地址

    一维数组

    int a[5];
    

    a表示的是数组的首地址,a等价于&a[0]

    二维数组

    int a[2][2] = {1, 2, 3, 4};
    

    a表示的整个数组的首地址,a[0]表示的是第一行的首地址,这两者者在数值上是一样的,但含义不同(或者说类型不同),数组名a是对于整个数组,a[0]是对于第一行

    从上面的示例运行结果来看,有些地址就是同一个地址

    在用数组的地址进行赋值的时候,虽然三者值相同,但是三者不可随意混用(以int a[2][2]为例)

    a--------是int (*)[2]型

    a[0]-----是int *型

    对于a[0]&a[0][0],两个类型都是int *型的,所以下述两种赋值方法等价

    第一种:

    int a[2][2] = {1, 2, 3, 4};
    int *p;
    p = a[0];
    

    第二种:

    int a[2][2] = {1, 2, 3, 4};
    int *p;
    p = &a[0][0];
    

    对于int a[2][2]来说,如果将a[0]改为&a[0],那么&a[0]和a的类型相同,都为int (*)[2]类型,下面以int a[5][5]为例,列出了二维数组的元素在不同方式表达下的不同类型。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5MxJd6rN-1607310361627)(20201120-C语言-二维数组.assets/image-20201120145832572.png)]

    也可以用一维指针数组来保存二维数组中某个元素的地址

    int a[2][2] = {1, 2, 3, 4};
    int *p[2];
    p[0] = &a[0][0];
    printf("%d", *p[0]);
    

    3 二维数组的解引用

    以二维数组

    a[2][3]={1, 2, 3, 4 ,5, 6};
    

    为例(第一维是行,第二维是列)

    第一种:*(*a+1)--------等价于a[0][1],因为*的优先级比+高,所以先解引用,进入第二维在第二维里面地址+1,再次解引用得到元素值

    第二种:*(*(a+1))------等价于a[1][0],比上面第一种多加了一个括号,括号优先级最高,先+1移动地址(注意是在第一维里面移动即行上的移动),然后解引用进入第二维,再解引用得到元素的值

    第三种:*(&a[0][0]+1)-----等价于a[0][1],这里使用了&取地址符【注意,这里取出来的是变量元素即基元素的地址,地址就是指针,如果指针移动,将以它为基准,一次移动一个基元素内存大小,本质上,也就是列上的移动】,将原本表示第一个元素的a[0][0]返回到第二个维度,然后第二维地址+1,再解引用得到元素的值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YfchNWN-1607310361633)(20201120-C语言-二维数组.assets/image-20201120150728181.png)]

    对于a[2][3]的解引用的过程:

    1. 二维数组,共2行,3列
    2. 一维数组名,本质上是列指针,而二维数组名,本质上是行指针,但这些数组名,都是常指针,即,指向固定不变
    3. 一维数组名,一次解引用,即可获取对应列上的元素
    4. 二维数组名,二次解引用,才可以获取对应行的对应列上的元素
    5. 对于一维,在解引用之前,是可以让指向偏移的,但指针不需要移动,只是指向发生偏移
    6. 对于二维,在解引用一层之前,可以偏移,即【行偏移】,再一次解引用之前,还可以再偏移,即【列偏移】,偏移到指定位置后,再第二层解引用
    7. 直接取基元素的地址,则偏移一定是以基元素为准,一次一个基元素的内存单位大小
    8. 本质上:
    • 行指针,基元素变为一个一维数组
    • 列指针,基元素即为基元素本身
    • 基一旦发生变化,移动或是偏移时,指针跨过的内存单位大小就随之而变化
    1. 行指针,经过一次解引用,就化为列指针,仍然是地址;也就是说,行指针经过两次解引用后,也就是取值,不再是地址;
    2. 列指针,经过一次解此用,就是取值,不再是地址;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YvWUTkKu-1607310361638)(20201120-C语言-二维数组.assets/image-20201120150824653.png)]

    示例分析

    #include <stdio.h>
    
    int main(void)
    {
        int a[2][3] = {1, 2, 3, 4, 5, 6};
    
        printf("%d***%d\n", *(a[1]), (*a)[0]);
        printf("%d***%d\n", *(a[1]+1), (*a+1)[0]);
        printf("%d***%d\n", *(a[1]+1), (*a+1)[1]);
    
        return 0;
    }
    

    运行结果如下:

    PS E:\clangstudy\class02> cd "e:\clangstudy\class02\" ; if ($?) { gcc 'arr_point03.c' -o 'arr_point03.exe' -Wall -g -O2 -static-libgcc -std=c11 -fexec-charset=utf-8 } ; if ($?) { &'.\arr_point03' }
    arr_point03.c: In function 'main':
    arr_point03.c:5:19: warning: missing braces around initializer [-Wmissing-braces]
         int a[2][3] = {1, 2, 3, 4, 5, 6};
                       ^
                        {      } {      }
    4***1
    5***2
    5***3
    

    虽然有警告,也说明内存是线性,仍然可以一次贯穿了来完成赋值

    • 二维可以用一维的方式来初始化
    • 先移动,再解引用,还是先解引用,再移动,程序员自己要明白清楚
    • 数组的下标操作,和解引用有相同的效果,但下标是可以直接定位到相应的行或是列上的
      • 行标定位行
      • 列标才定位列
      • 对于二维数组,带行标的,肯定还是列指针,还可以做偏移,还可以再解引用
      • 对于一维数组,只能带列标,带上列标,即取值
      • 对于行指针,解引用一次后,就成了一维数组上的列指针,再带上列标,即取值

    4 解引用和下标

    4.1 下标是数组说法,解引用是指针说法

    *(a[1]+1)--------表示的是a[1][1]的值

    过程解析:

    1. 行标a[1],标号为1,即第二行,转化为列指针
    2. 偏移a[1]+1,偏移量为1,即第二行第二列,仍然是列指针
    3. 解引用*(a[1]+1),即取值,取的就是a[1][1]元素的值

    (*a+1)[1]--------表示的是a[0][2]的值

    过程解析:

    1. 解引用*a,由行指针转为列指针,指在首行首列,即第1列
    2. 偏移(*a+1),指在首行第2列
    3. 取列标(*a+1)[1],仍然是一个数组,如果是列标为[0]即当前所指位置,而这时,列标号为1,即偏移一个基元素,也就是取首行第二列的下一个元素,即首行第三列的元素,即a[0][2]元素

    4.2 为了方便理解,再一次详细描述一下

    先退回一维数组,以

    int a[5];
    

    来说,a表示的数组a的首地址,a[2]表示在a的基础上移动2个地址(注意a的类型是int *型的),再解引用得到元素的值,意思是a[2]

    实际上包含了两步

    • 第一步地址移动
    • 第二步解引用得到元素的值(注意第二步,有点隐式转换的意思,经常被人忽略)

    现在来解释上面的二维数组就容易多了

    • 先来看第一个*(a[1]+1)

      • a[1]代表第二行的首地址,注意这里的维度已经是第二维度了
      • 然后括号优先第二维地址+1
      • 最后解引用得到元素的值
    • 再看第二个(*a+1)[1],这里提一句,因为[]的优先级是比高的所以这里的括号不能去掉

      • 第一步先解引用进入第二维度(*优先级高于+)
      • 然后第二维地址+1
      • 然后再在当前基础上再移动一次地址,只要不是[0],就会发生位置偏移
      • 最后下标取值
      • 得到元素的值,这里可能有点绕,换个说法就是[1]是在当前维度进行移动,然后解引用(“当前维度”有点不太严谨,为了方便理解先将就这么用了)

    a[2][1]来说一共有四步

    • 其中包含了两次地址移动,两次解引用
    • 执行顺序是:
      • 地址移动->解引用->地址移动->解引用
      • (这里提一句,[]的结合性是左结合的,所以在移动的时候先移动行(第一维)再移动列(第二维))

    详细步骤:

    • 第一步:在当前维度地址+2,因为a的维度是第一维,所以是第一维地址+2,即行+2
    • 第二步:解引用进入第二维度
    • 第三步:在当前维度地址+1,因为这时已经进入第二维,所以第二维地址+1,即列+1
    • 第四步:解引用得到元素的值

    5 理解指针数组的本质==内存空间的分配和使用

    概括的说,指针其实就是可变数组的首地址,说是可变数组,是指其包含内容的数量的可变的,并且是可动态申请和释放的,从而充分节约宝贵的内存资源。我一向喜欢一维数组,除非万不得已,我一般是不用二维数组的,多维的则更是很少涉足了。因为一维简单,容易理解,而用指针指向的多维数组就具有相当的复杂性了,也因此更具有讨论的必要。

    本质上,就是为了更好地使用和操纵内存

    5.1 三个二维数组的比较

    int **Ptr;
    int *Ptr[5];
    int (*Ptr)[5];
    

    三例都是整数的二维数组,都可以用形如 Ptr[0][0] 的方式访问其内容;但它们的差别却是很大的。

    5.2 从四个方面对它们进行讨论

    5.2.1 内容:

    ​ 它们本身都是指针,它们的最终内容都是整数。注意这里说的是最终内容,而不是中间内容,比如你写 Ptr[ 0 ],对于三者来说,其内容都是一个整数指针,即 int *Ptr[1][1] 这样的形式才是其最终内容。

    5.2.2 意义:

    ​ (1)、int **Ptr 表示指向"一群"指向整数的指针的指针。【可以认为是指针数组,只能指向指针,这些被指向的指针是整型指针】
    ​ (2)、int *Ptr[5] 表示指向 5 个指向整数的指针的指针。【就是5个指针,成了一组】
    ​ (3)、int (*Ptr)[5] 表示指向"一群"指向 5 个整数数组的指针的指针。【即数组的指针,只能指向数组,不能指向整型元素】

    5.2.3 所占空间:

    ​ (1)、int **Ptr 和 (3)、int (*Ptr)[5] 一样,在32位平台里,都是4字节,即一个指针。但 (2)、int *Ptr[5] 不同,它是 5 个指针,它占5 * 4 = 20个字节的内存空间。

    5.2.4 用法:

    ​ (1)、int **Ptr

    ​ 因为是指针的指针,需要两次内存分配才能使用其最终内容。首先,Ptr = (int **)new int *[5];这样分配好了以后,它和(2)的
    意义相同了;然后要分别对 5 个指针进行内存分配,例如:Ptr[0] = new int[20];它表示为第 0 个指针分配 20 个整数,分配好以后, Ptr[0] 为指向 20 个整数的数组。这时可以使用下标用法 Ptr[0][0]Ptr[0][19]了。
    ​ 如果没有第一次内存分配,该 Ptr 是个"野"指针,是不能使用的,如果没有第二次内存分配,则 Ptr[0] 等也是个"野"指针,也是不能用的。当然,用它指向某个已经定义的地址则是允许的,那是另外的用法(类似于"借鸡生蛋"的做法),这里不作讨论(下同)。
    ​ (2)、int *Ptr[5]
    ​ 这样定义的话,编译器已经为它分配了 5 个指针的空间,这相当于(1)中的第一次内存分配。根据对(1)的讨论可知,显然要对其进行一次内存分配的。否则就是"野"指针。
    ​ (3)、int (*Ptr)[5]
    ​ 这种定义我觉得很费解,不是不懂,而是觉得理解起来特别吃力,也许是我不太习惯这样的定义吧。怎么描述它呢?它的意义是"一群"指针,每个指针都是指向一个 5 个整数的数组。如果想分配 k 个指针,这样写:

    Ptr = (int(*)[5]) new int[sizeof(int)*5*k]

    这是一次性的内存分配。分配好以后,Ptr 指向一片连续的地址空间,其中 Ptr[0] 指向第 0 个 5 个整数数组的首地址,Ptr[1] 指向第1 个 5 个整数数组的首地址。

    综上所述,我觉得可以这样理解它们:
    int ** Ptr <==> int Ptr[ x ][ y ];
    int *Ptr[ 5 ] <==> int Ptr[ 5 ][ x ];
    int ( *Ptr )[ 5 ] <==> int Ptr[ x ][ 5 ];
    这里 x 和 y 是表示若干的意思。

    6 指针数组(数组每个元素都是指针)详解

    如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。

    指针数组的定义形式一般为:

    dataType *arrayName[length];
    

    [ ]的优先级高于*,该定义形式应该理解为:

    dataType *(arrayName[length]);
    

    括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

    除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:

    #include <stdio.h>
    int main()
    {    
        int a = 16, b = 932, c = 100;    //定义一个指针数组    
        int *arr[3] = {&a, &b, &c};	//也可以不指定长度,直接写作 int *arr[]    
        
        //定义一个指向指针数组的指针    
        int **parr = arr;    
        printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);    
        printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));    
        
        return 0;
    }
    

    运行结果:
    16, 932, 100
    16, 932, 100

    arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

    parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。

    第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。

    第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

    指针数组还可以和字符串数组结合使用,请看下面的例子:

    #include <stdio.h>
    int main()
    {    
        char *str[3] = {        
            "www.cuit.edu.cn",        
            "数学学院学习C语言",        
            "C Language"    
        };    
        
        printf("%s\n%s\n%s\n", str[0], str[1], str[2]);    
        
        return 0;
    }
    

    运行结果:
    www.cuit.edu.cn
    数学学院学习C语言
    C Language

    需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

    也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。

    为了便于理解,可以将上面的字符串数组改成下面的形式,它们都是等价的。

    #include <stdio.h>
    int main()
    {
        char *str0 = "www.cuit.edu.cn";
        char *str1 = "数学学院学习C语言";
        char *str2 = "C Language";
        char *str[3] = {str0, str1, str2};
    
        printf("%s\n%s\n%s\n", str[0], str[1], str[2]);    
        
        return 0;
    }
    

    7 二维数组的内存理解

    7.1 基本概念理解

    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 的分布是一维线性的,整个数组占用一块连续的内存:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5ySl2rA-1607310361647)(20201120-C语言-二维数组.assets/image-20201120160552706.png)]

    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,那么每个一维数组的首地址如下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KNTVpKms-1607310361649)(20201120-C语言-二维数组.assets/image-20201120160640958.png)]

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

    int (*p)[4] = a;		// 典型应用:行指针指向二维数组
    

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

    [ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

    数组名 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

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

    *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

    1. *(*(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 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
    

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

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

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

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

    • 指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。
    • 二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。
    • 所有的指针,占用的内存空间大小是一样的
    • 但指向的内容是由它的基类型决定的
    • 所以void型的指针,可以强转为其它任意类型
    展开全文
  • 三维空间刚体运动、旋转矩阵左右手系的区别与向量间的旋转。a 到 b 的旋转可以由向量 w 来描述坐标变换。对于同个向量 p,它在世界坐标系下的坐标 pw 和在相机坐标系下的 pc 是不同的。这个变换关系由坐标系间的...
  • 13.3.2 二维小波变换图像压缩 13.3.3 图像压缩中阈值确定与作用命令 13.3.4 基于小波包变换图像压缩 13.3.5 小波变换用于图像去噪 13.3.6 小波分析用于图像增强 13.3.7 图像钝化与锐化 13.3.8 ...
  • 文章目录、线性分类数学基础与应用1、Fisher基本介绍2、Fisher判别思想3、举例、Fisher判别推导(python)1、代码2、代码结果、Fisher分类器1、定义2、scikit-learn中LDA函数代码测试3、监督降维技术...
  • 13.3.2 二维小波变换图像压缩 13.3.3 图像压缩中阈值确定与作用命令 13.3.4 基于小波包变换图像压缩 13.3.5 小波变换用于图像去噪 13.3.6 小波分析用于图像增强 13.3.7 图像钝化与锐化 13.3.8 ...
  • 、任务简介斯坦福CS231n关于卷积和池化课程2017CS231n 斯坦福李飞飞...三、任务作业 1、卷积与全连接的区别卷积层可以保全空间结构,对于32*32*3的输入图像,不是把它展成个长的向量,而是三维数据输入,可以...
  • 最后,读者将学习如何创建二维数组以及如何使用嵌套循环来处理它们。 第6章:分支语句和逻辑操作符 如果程序可以根据实际情况调整执行,我们就说程序能够智能地行动。在本章,读者将了解到如何使用if 、if else...
  • 最后,读者将学习如何创建二维数组以及如何使用嵌套循环来处理它们。 第6章:分支语句和逻辑操作符 如果程序可以根据实际情况调整执行,我们就说程序能够智能地行动。在本章,读者将了解到如何使用if 、if else...
  • 最后,读者将学习如何创建二维数组以及如何使用嵌套循环来处理它们。 第6章:分支语句和逻辑操作符 如果程序可以根据实际情况调整执行,我们就说程序能够智能地行动。在本章,读者将了解到如何使用if 、if else...
  • 预习数据结构3.4,3.5

    2018-02-01 20:03:48
    .线性表是什么? 是个序列,具有顺序,数量有限。 ,顺序存储相关: ...,数组长度与线性表的区别 数组长度是存储空间的长度是不变的。 线性表会随着元素的增加和删减会变化。 地址计算方法 数组从
  • 二维表 (40) 将E-R图转换到关系模式时,实体与联系都可以表示成(B) A. 属性 B. 关系 C. 键 D. 域 (41) 在下列选项中,哪个不是个算法一般应该具有基本特征(C) A. 确定性 B. 可行性 C. 无穷性 D. 拥有足够情报...
  • (内有最新习题和ppt教程) ... 二维表 (40) 将E-R图转换到关系模式时,实体与联系都可以表示成______。(B) A. 属性 B. 关系 C. 键 D. 域 (41) 在下列选项中,哪个不是个算法一般应该具有基本特征______。(C)...
  • 5.5.1 ODB与RDB概念设计的区别 99 5.5.2 EER模式到ODB模式的映射 100 小结 101 复习题 102 练习题 102 参考文献 102 第6章 对象-关系和扩展-关系系统 104 6.1 SQL及其对象-关系特性概述 104 ...
  • ExtAspNet是组专业Asp.net控件库,拥有原生AJAX支持和丰富UI效果, 目标是创建没有JavaScript,没有CSS,没有UpdatePanel,没有WebServicesWeb应用程序。 支持浏览器: IE 7.0+, Firefox 3.0+, Chrome ...
  • 10.4.2 使用字符串指针变量与字符数组的区别 158 10.5 函数指针变量 159 10.6 指针型函数 160 10.7 指针数组和指向指针的指针 161 10.7.1 指针数组的概念 161 10.7.2 指向指针的指针 164 10.7.3 main 函数的参数 166...
  • 5.2.2 V$SQL和V$SQLAREA的区别 182 5.2.3 与Cursor有关的参数 182 5.3 执行计划 185 5.3.1 Explain Plan For ... 185 5.3.2 SQL Trace和10046事件 186 5.3.3 从Library Cache中获取 190 5.4 如何阅读...
  • ArcGIS地统计分析模块应用、网络路径分析和矢量空间分析(网络追踪分析及市区选房分析为例)、空间校正、明暗等高线(以土壤项目数据为例)、噪声影响分布分析、专题图制作、三维地形分析、ArcGIS影像配准、虚拟...
  • 一般把存储器中的一个字节称为个内存单元, 不同数据类型所占用内存单元数不等,如整型量占2个单元,字符量占1个单元等, 在第章中已有详细介绍。为了正确地访问这些内存单元, 必须为每个内存单元编上号...
  • 本文阐述了利用Visual C++软件实现个类MATLAB功能强大科学计算器内核一般实现原理,包括设计目标相关问题探讨、算法分析、各功能子模块设计、用户实现接口定义等软件工程开发中主要步骤,着重解答内核...
  • •第代用户界面:虚拟现实在三维空间(虚拟现实界面元素)。 3、传统人机接口 •操作命令 联机(键盘操作命令)、脱机(作业控制语言) 用户组合自编(Shell语言):DOS Shell;UNIX ;B...
  • 利用的是双目成像的技术,把左图和右图合起来提取出空间的点,用光流在上面做,就能把场景的流动分析出来。 光流也可以利用深度学习的模型来做,把左右两图用同样的模型来提取特征,经过计算就能得出个深度的...
  • 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇...
  • def是一个级指针,它指向是一个一维数组指针,数组元素都是float. (2)double*(*gh)[10]; gh是一个指针,它指向一个一维数组,数组元素都是double*. (3)double(*f[10])(); f是一个数组,f有10个元素,元素都是...
  • 《数据结构 1800题》

    热门讨论 2012-12-27 16:52:03
    8.对于个数据结构,一般包括哪个方面讨论?【北京科技大学 1999 、1(2分)】 9. 当你为解决某问题而选择数据结构时,应从哪些方面考虑?【西安电子北京科技大学 2000】 10. 若将数据结构定义为个二元...
  • windows 程序设计

    2011-07-24 21:16:30
    主要的区别在于C语言链接库函数的机械码连结到您的程序代码中,而Windows函数的程序代码在您程序执行文件外的DLL中。 当您执行Windows程序时,它通过个称作「动态链接」的过程与Windows相接。个Windows的.EXE...
  • asp.net知识库

    2015-06-18 08:45:45
    深入剖析ASP.NET组件设计]书第章关于ASP.NET运行原理讲述补白 asp.net 运行机制初探(httpModule加载) 利用反射来查看对象中私有变量 关于反射中创建类型实例两种方法 ASP.Net应用程序多进程模型 NET委托...

空空如也

空空如也

1 2 3
收藏数 49
精华内容 19
关键字:

一维二维三维四维空间的区别