精华内容
下载资源
问答
  • 并且是纯电动驱动,因此运动起来相对比较安静,充电一次可以使用约90分钟。 SpotMini本身传感器包括:3D距离传感器、惯性测量单元、以及肢内位置/力量传感器。这些都是感知周围环境、了解自己运动状态,从而...

    波士顿动力Spot mini
    SpotMini是一款小型的四足机器人。重量为约25kg(带上手臂30kg)。并且是纯电动驱动的,因此运动起来相对比较安静,充电一次可以使用约90分钟。
    SpotMini本身的传感器包括:3D距离传感器、惯性测量单元、以及四肢内的位置/力量传感器。这些都是感知周围环境、了解自己运动状态,从而实现自主导航的必备传感器。
    3D距离传感器是用来获得二维图像+距离图像(术语叫深度图) 的设备,有很多种,比如基于结构光编码的RGB-D相机、基于飞行时间法的RGB-D相机、基于立体匹配的双目相机,以及激光雷达等。
    比如波士顿动力的Atlas机器人的3D距离传感器,使用的是Carnegie Robotics公司的MultiSense SL,这是一种多模式3D传感器,包括激光,双目相机传感器。
    而SpotMini使用的3D距离传感器同样来自这家公司,型号为MultiSense S7 ,是一种紧凑的双目立体相机,分辨率高达2K x 2K,通过两相机视差可以计算出物体和相机的距离。
    MIT 猎豹
    MIT一直是机器人科技的先驱,如今它们发明了一种机器猎豹,可以说是世界上最敏捷的机器人。
    猎豹是陆地上奔跑最快的动物,能在短短几秒内加速到60英里每小时。在加速过程中,猎豹会四肢两两并用,跳跃加速。现在,MIT的研究人员开发出了一套跳跃算法,并将它用在了机器豹身上。
    机器豹拥有四肢,配置了齿轮,电池,电动马达,与动物猎豹的重量也差不多。研究团队对机器豹进行了测试,它能稳定地在草地上跳跃前进,最高时速能够达到10英里,能够跳跃超过一英尺(30cm)的障碍物。据预计,机器豹的最高时速能达到30英里。
    MIT Cheetah四个主要设计理念都是为提高该机器人的奔跑速度以及奔跑时间(能量效率)服务的。为实现快速奔跑并提高能量利用效率,MIT Cheetah的设计者主要做了三方面的重要工作:1)机械结构设计;2)执行机构设计);3)控制器设计。就目前的情况来看,机械结构以及执行机构的设计应该说是该机器人能够成功的关键,但是在机械结构与执行机构定型后,控制器的设计将会越来越重要。
    宇树科技AlienGo
    高爆发运动能力(后空翻);
    跌倒后的自动翻身功能;
    通过崎岖路面的能力;
    高速行走的能力(Approx. Max. 1.5m/s);
    集成Slam与人体运动tracking的视觉应用;
    其中最亮眼的无疑是通过后空翻展现的高爆发运动能力;这是世界上目前能看到的最大尺寸和重量的四足机器人进行的后空翻,相应的硬件设计、电机拖动与运动控制技术可以说是世界顶尖水平。
    对于四足机器人每条单腿的自由度设计,通常目前都是遵循串联式的Roll-Pitch-Pitch式的方案,三个驱动器都分布在髋关节,对于膝关节的pitch则通过连杆或者滚珠丝杠传递下去,有益于减小腿部惯量并提升动态运动性能。
    但此设计也存在一些弊端,即串联式的关节设计使得电机分布在旋转关节的两侧,因此两电机间的电气连接线在设计上将有所考究。
    在当今主流的关节机器人的驱动器设计中,我们有中空式走线和非中空式走线两种方案,目前大多数机器人都会采用前者。因为作为后者的非中空式走线的线路会直接暴露在关节外侧,一方面影响机器人设计的整体性和紧凑性,另外一方面将带来因为勾拽而导致电气连接线脱落的安全隐患。而中空式走线将会避免这些问题(推测Spotmini也采用中空式走线的方式,如图一所示,并没有外置的电缆),但同时会在结构设计上带来更大的复杂性,并且需要扩大驱动器的径向和轴向尺寸,致使整体设计的紧凑度降低,躯体惯量增大,给后续动态运动能力带来负面影响。
    同时,无论是非中空式走线还是当今主流的中空式走线,在相应关节需要旋转大量次数的工况下,都会带来相应的线材磨损和疲劳损伤,最终影响作为商品的可靠性能。我们在机器人的科研应用中,几乎已经认为中空式走线的方案是“完美”的,因为科研类机器人几乎不考虑疲劳寿命,但作为商业产品性质的AlienGo,我想追求更加极致的产品可靠性,是其硬件设计优化上永远的目标。从作者在iit和关节腿足式机器人打交道的经验来看(采用中空式走线),80%以上的机器人硬件问题都是电气连接线问题,这是目前制约机器人机电平台可靠性的瓶颈。
    蔚蓝AlphaDog
    是生活伙伴和强大的工作助手。通过集成已经十分成熟的人工智能、物联网、5G、虚拟现实/增强现实、智能驾驶、群体智能等技术,阿尔法机器狗可以帮助你更好的守护家人、帮助守护你的财产、为盲人导航、可以在公园或小区里进行治安巡逻、将快递或外卖直接送到家门口、可以在工厂或危险场景下进行设备检查工作、甚至进行人道主义挽救生命
    阿尔法机器狗采用先进的软硬件技术架构、机械设计、智能算法和运动控制算法。它的运动控制算法是业界领先的。同时,蔚蓝采用了开放的业界标准,使得阿尔法机器狗能够无缝集成和连接各类其他先进技术。阿尔法机器狗既是第一台真正的个人机器人,同时也是一款能够开发各类应用的高性能机器人移动平台。

    展开全文
  • 文章目录二维数组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型的指针,可以强转为其它任意类型
    展开全文
  • 文章目录二维数组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 是不同的。这个变换关系由坐标系间的...

    v2-98b79692e8944d4710fb83731035ba19_1440w.jpg?source=172ae18b

    三维空间刚体运动


    一、旋转矩阵

    a234fbfb9d031ed345596f1e02312571.png
    左右手系的区别与向量间的旋转。a 到 b 的旋转可以由向量 w 来描述

    dbca77a9f1c35d93372e6981598c0cfc.png
    坐标变换。对于同一个向量 p,它在世界坐标系下的坐标 pw 和在相机坐标系下的 pc 是不同的。这个变换关系由坐标系间的变换矩阵 T 来描述
    旋转矩阵有一些特别的性质。事实上,它是一个行列式为 1 的正交矩阵。正交矩阵即逆为自身转置的矩阵。反之,行 列式为 1 的正交矩阵也是一个旋转矩阵。

    旋转矩阵的集合定义如下:

    SO(n) 是特殊正交群(Special Orthogonal Group)的意思。

    特殊欧氏群(Special Euclidean Group):

    116e65c93066ae8acad86b388dfe4ad6.png

    与 SO(3) 一样,求解该矩阵的逆表示一个反向的变换:

    42f33f85d9282899b25a13b4d9d0d843.png

    使用Eigen库:

    #include <iostream>
    using namespace std;
    #include <ctime>
    // Eigen 部分
    #include <Eigen/Core>
    // 稠密矩阵的代数运算(逆,特征值等)
    #include <Eigen/Dense>
    
    #define MATRIX_SIZE 50
    
    /****************************
    * 本程序演示了 Eigen 基本类型的使用
    ****************************/
    
    int main( int argc, char** argv )
    {
        // Eigen 中所有向量和矩阵都是Eigen::Matrix,它是一个模板类。它的前三个参数为:数据类型,行,列
        // 声明一个2*3的float矩阵
        Eigen::Matrix<float, 2, 3> matrix_23;
    
        // 同时,Eigen 通过 typedef 提供了许多内置类型,不过底层仍是Eigen::Matrix
        // 例如 Vector3d 实质上是 Eigen::Matrix<double, 3, 1>,即三维向量
        Eigen::Vector3d v_3d;
    	// 这是一样的
        Eigen::Matrix<float,3,1> vd_3d;
    
        // Matrix3d 实质上是 Eigen::Matrix<double, 3, 3>
        Eigen::Matrix3d matrix_33 = Eigen::Matrix3d::Zero(); //初始化为零
        // 如果不确定矩阵大小,可以使用动态大小的矩阵
        Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic > matrix_dynamic;
        // 更简单的
        Eigen::MatrixXd matrix_x;
        // 这种类型还有很多,我们不一一列举
    
        // 下面是对Eigen阵的操作
        // 输入数据(初始化)
        matrix_23 << 1, 2, 3, 4, 5, 6;
        // 输出
        cout << matrix_23 << endl;
    
        // 用()访问矩阵中的元素
        for (int i=0; i<2; i++) {
            for (int j=0; j<3; j++)
                cout<<matrix_23(i,j)<<"t";
            cout<<endl;
        }
    
        // 矩阵和向量相乘(实际上仍是矩阵和矩阵)
        v_3d << 3, 2, 1;
        vd_3d << 4,5,6;
        // 但是在Eigen里你不能混合两种不同类型的矩阵,像这样是错的
        // Eigen::Matrix<double, 2, 1> result_wrong_type = matrix_23 * v_3d;
        // 应该显式转换
        Eigen::Matrix<double, 2, 1> result = matrix_23.cast<double>() * v_3d;
        cout << result << endl;
    
        Eigen::Matrix<float, 2, 1> result2 = matrix_23 * vd_3d;
        cout << result2 << endl;
    
        // 同样你不能搞错矩阵的维度
        // 试着取消下面的注释,看看Eigen会报什么错
        // Eigen::Matrix<double, 2, 3> result_wrong_dimension = matrix_23.cast<double>() * v_3d;
    
        // 一些矩阵运算
        // 四则运算就不演示了,直接用+-*/即可。
        matrix_33 = Eigen::Matrix3d::Random();      // 随机数矩阵
        cout << matrix_33 << endl << endl;
    
        cout << matrix_33.transpose() << endl;      // 转置
        cout << matrix_33.sum() << endl;            // 各元素和
        cout << matrix_33.trace() << endl;          // 迹
        cout << 10*matrix_33 << endl;               // 数乘
        cout << matrix_33.inverse() << endl;        // 逆
        cout << matrix_33.determinant() << endl;    // 行列式
    
        // 特征值
        // 实对称矩阵可以保证对角化成功
        Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> eigen_solver ( matrix_33.transpose()*matrix_33 );
        cout << "Eigen values = n" << eigen_solver.eigenvalues() << endl;
        cout << "Eigen vectors = n" << eigen_solver.eigenvectors() << endl;
    
        // 解方程
        // 我们求解 matrix_NN * x = v_Nd 这个方程
        // N的大小在前边的宏里定义,它由随机数生成
        // 直接求逆自然是最直接的,但是求逆运算量大
    
        Eigen::Matrix< double, MATRIX_SIZE, MATRIX_SIZE > matrix_NN;
        matrix_NN = Eigen::MatrixXd::Random( MATRIX_SIZE, MATRIX_SIZE );
        Eigen::Matrix< double, MATRIX_SIZE,  1> v_Nd;
        v_Nd = Eigen::MatrixXd::Random( MATRIX_SIZE,1 );
    
        clock_t time_stt = clock(); // 计时
        // 直接求逆
        Eigen::Matrix<double,MATRIX_SIZE,1> x = matrix_NN.inverse()*v_Nd;
        cout <<"time use in normal inverse is " << 1000* (clock() - time_stt)/(double)CLOCKS_PER_SEC << "ms"<< endl;
        
    	// 通常用矩阵分解来求,例如QR分解,速度会快很多
        time_stt = clock();
        x = matrix_NN.colPivHouseholderQr().solve(v_Nd);
        cout <<"time use in Qr decomposition is " <<1000*  (clock() - time_stt)/(double)CLOCKS_PER_SEC <<"ms" << endl;
    
        return 0;
    }
    

    二、旋转向量和欧拉角

    由旋转向量到旋转矩阵的过程由罗德里格斯公式(Rodrigues’s Formula ):

    1bd5813e69d2cb245bab6caf7b5d3fac.png
    符号 ∧ 是向量到反对称的转换符

    从一个旋转矩 阵到旋转向量的转换。对于转角 θ,有:

    64216e60f6f20e003ef5261dcb26cb2a.png

    faef2e5c90ed226f6c16ee4f4db9304f.png

    关于转轴 n,由于旋转轴上的向量在旋转后不发生改变,说明

    bbf5032292284a2d48d985e6bae1c148.png

    ZY X 转角相当于把任意旋转分解成 以下三个轴上的转角:

    1. 绕物体的 Z 轴旋转,得到偏航角 yaw;
    2. 绕旋转之后的 Y 轴旋转,得到俯仰角 pitch;
    3. 绕旋转之后的 X 轴旋转,得到滚转角 roll。

    欧拉角的一个重大缺点是会碰到著名的万向锁问题(Gimbal Lock①):在俯仰角为 ±90◦ 时,第一次旋转与第三次旋转将使用同一个轴,使得系统丢失了一个自由度(由三次 旋转变成了两次旋转)。这被称为奇异性问题,在其他形式的欧拉角中也同样存在。

    067f30dea049f7a5d18cfc07d4fa764d.png
    欧拉角的旋转示意图。上方为 ZYX 角定义。下方为 pitch=90 度时,第三次旋转与第一次滚转角相同,使得系统丢失了一个自由度。
    欧拉旋转v.youku.com
    83c6329d469ee80a783d7976c3da4b52.png

    三、四元数

    一个四元数 q 拥有一个实部和三个虚部。本书把实部写在前面(也有地方把实部写在 后面),像这样:

    7df193957d18aa0dcd9a5376470e4da8.png

    其中 i, j, k 为四元数的三个虚部。这三个虚部满足关系式:

    302f3bf73936ad864d1fb979d0c6ac15.png

    实践Eigen:

    #include <iostream>
    #include <cmath>
    using namespace std;
    
    #include <Eigen/Core>
    // Eigen 几何模块
    #include <Eigen/Geometry>
    
    /****************************
    * 本程序演示了 Eigen 几何模块的使用方法
    ****************************/
    
    int main ( int argc, char** argv )
    {
        // Eigen/Geometry 模块提供了各种旋转和平移的表示
        // 3D 旋转矩阵直接使用 Matrix3d 或 Matrix3f
        Eigen::Matrix3d rotation_matrix = Eigen::Matrix3d::Identity();
        // 旋转向量使用 AngleAxis, 它底层不直接是Matrix,但运算可以当作矩阵(因为重载了运算符)
        Eigen::AngleAxisd rotation_vector ( M_PI/4, Eigen::Vector3d ( 0,0,1 ) );     //沿 Z 轴旋转 45 度
        cout .precision(3);
        cout<<"rotation matrix =n"<<rotation_vector.matrix() <<endl;                //用matrix()转换成矩阵
        // 也可以直接赋值
        rotation_matrix = rotation_vector.toRotationMatrix();
        // 用 AngleAxis 可以进行坐标变换
        Eigen::Vector3d v ( 1,0,0 );
        Eigen::Vector3d v_rotated = rotation_vector * v;
        cout<<"(1,0,0) after rotation = "<<v_rotated.transpose()<<endl;
        // 或者用旋转矩阵
        v_rotated = rotation_matrix * v;
        cout<<"(1,0,0) after rotation = "<<v_rotated.transpose()<<endl;
    
        // 欧拉角: 可以将旋转矩阵直接转换成欧拉角
        Eigen::Vector3d euler_angles = rotation_matrix.eulerAngles ( 2,1,0 ); // ZYX顺序,即roll pitch yaw顺序
        cout<<"yaw pitch roll = "<<euler_angles.transpose()<<endl;
    
        // 欧氏变换矩阵使用 Eigen::Isometry
        Eigen::Isometry3d T=Eigen::Isometry3d::Identity();                // 虽然称为3d,实质上是4*4的矩阵
        T.rotate ( rotation_vector );                                     // 按照rotation_vector进行旋转
        T.pretranslate ( Eigen::Vector3d ( 1,3,4 ) );                     // 把平移向量设成(1,3,4)
        cout << "Transform matrix = n" << T.matrix() <<endl;
    
        // 用变换矩阵进行坐标变换
        Eigen::Vector3d v_transformed = T*v;                              // 相当于R*v+t
        cout<<"v tranformed = "<<v_transformed.transpose()<<endl;
    
        // 对于仿射和射影变换,使用 Eigen::Affine3d 和 Eigen::Projective3d 即可,略
    
        // 四元数
        // 可以直接把AngleAxis赋值给四元数,反之亦然
        Eigen::Quaterniond q = Eigen::Quaterniond ( rotation_vector );
        cout<<"quaternion = n"<<q.coeffs() <<endl;   // 请注意coeffs的顺序是(x,y,z,w),w为实部,前三者为虚部
        // 也可以把旋转矩阵赋给它
        q = Eigen::Quaterniond ( rotation_matrix );
        cout<<"quaternion = n"<<q.coeffs() <<endl;
        // 使用四元数旋转一个向量,使用重载的乘法即可
        v_rotated = q*v; // 注意数学上是qvq^{-1}
        cout<<"(1,0,0) after rotation = "<<v_rotated.transpose()<<endl;
    
        return 0;
    }
    
    展开全文
  • 学习C++是一次探索之旅,因为这种语言容纳了好几种编程模式,其中包括面向对象编程、通用编程 传统过程化编程。随着新特性不断添加,C++一度成为一个活动目标,不过现在有了2003年ISO/ ANSIC++标准第...
  • 学习C++是一次探索之旅,因为这种语言容纳了好几种编程模式,其中包括面向对象编程、通用编程 传统过程化编程。随着新特性不断添加,C++一度成为一个活动目标,不过现在有了2003年ISO/ ANSIC++标准第...
  • 学习C++是一次探索之旅,因为这种语言容纳了好几种编程模式,其中包括面向对象编程、通用编程 传统过程化编程。随着新特性不断添加,C++一度成为一个活动目标,不过现在有了2003年ISO/ ANSIC++标准第...
  • Python【每日问】16

    2019-04-30 17:40:00
    请完成个函数,输入这样的一二维数组和一个整数,判断数组中是否含有该整数 答: 【基础题】TCP/UDP/HTTP协议区别 TCP UDP 是传输层协议,HTTP 是应用层协议。 TCP 是面向连接,可靠,有着三握手...
  • 10.4.2 二次规划问题应用 10.5 有约束最小化 10.5.1 有约束最小化函数fmincon 10.5.2 有约束最小化应用 10.6 目标规划 10.6.1 目标规划函数fgoalattain 10.6.2 目标规划应用 10.7 最大最小化 ...
  • 10.4.2 二次规划问题应用 10.5 有约束最小化 10.5.1 有约束最小化函数fmincon 10.5.2 有约束最小化应用 10.6 目标规划 10.6.1 目标规划函数fgoalattain 10.6.2 目标规划应用 10.7 最大最小化 ...
  • 一直很好奇这些变量怎么回事,各处google搜集整理了这些GPU里用到变量历史细则: ...第OpenGL Programming Guide解释:“纹理坐标可以由一维二维,三或者思维组成。一般用strq来区别于对...
  • GPU component,rgba,xyzw,stpq

    千次阅读 2017-06-21 14:10:45
    关于对gpu数据各个分量理解。 前两个不用多说啊 rgba,red,green,blue,alpha。...“纹理坐标可以由维,维,三维或者四维组成。一般用s,t,r,q来区别于对象坐标xyzw计算用坐标uv。维用s,维用st
  • (13) 设棵完全二叉树共有699个结点,则在该二叉树中叶子结点数为(B) 注:利用公式n=n0+n1+n2、n0=n2+1完全二叉数特点可求出 A. 349 B. 350 C. 255 D. 351 (14) 结构化程序设计主要强调是(B) A.程序规模 ...
  • OpenGL笔试题

    2021-01-02 13:30:35
    简述FrameBuffer,RenderBuffer,Depth Buffer,Framebuffer attachment,Stencil buffer关系 ...和二维纹理有什么区别? 如何加速GPU和CPU之间数据传输速率? 用OpenGL渲染二图像,除去在个平.
  • 5. 测试六条基本法则是什么:功(功能)可(可靠性)三效(效率)易(易用性)五(可维护性)六移(可移植性) 6. 回答不上来:这块我不是很熟悉,确实学过,但是很久以前我做过,但是好长时间没用了,我...
  • ExtAspNet是组专业Asp.net控件库,拥有原生AJAX支持丰富UI效果, 目标是创建没有JavaScript,没有CSS,没有UpdatePanel,没有WebServicesWeb应用程序。 支持浏览器: IE 7.0+, Firefox 3.0+, Chrome ...
  • 5.2.2 V$SQLV$SQLAREA的区别 182 5.2.3 与Cursor有关的参数 182 5.3 执行计划 185 5.3.1 Explain Plan For ... 185 5.3.2 SQL Trace10046事件 186 5.3.3 从Library Cache中获取 190 5.4 如何阅读...
  • excel使用

    2012-11-25 17:06:01
    实际输入时候,通常应用等差数列输入法,先输入前个值,定出自变量中数与数之间步长,然后选中A2A3两个单元格,使这项变成个带黑色边框矩形,再用鼠标指向这黑色矩形右下角小方块“■”,当光标...
  • (1)程序进程的区别 (2)进程的五个基本特征:动态性、并发性、独立性、制约性、结构性 3、进程调度 (1)进程的三个基本状态及转换 三个基本状态是等待、执行就绪,在一定的条件下,进程...
  • 一般把存储器中的一个字节称为个内存单元, 不同数据类型所占用内存单元数不等,如整型量占2个单元,字符量占1个单元等, 在第章中已有详细介绍。为了正确地访问这些内存单元, 必须为每个内存单元编上号...
  • MatCom与MATFOR则是提供了大量矩阵运算函数库用于应用程序的二次开发,与MATLAB提供运算函数库一样,使应用程序脱离其软件母体而独立发布与执行成为了可能。 但是,利用MATLAB、MatCom、MATFOR提供矩阵运算...
  • 另有自编译相关著名GIS软件二次开发手册、编译代码、相关项目开发文档、并提供可与Google earth数据交换网上建筑三建模共享软件、GIS软件在相关专业中文操作使用手册以及相关GIS教学课件资料演示案例等资料...
  • 109_函数6_returnbreak的区别 108_函数5_如何定义函数 107_函数4_什么是函数 106_函数3_为什么需要函数 105_函数2_函数使用简单介绍 104_函数1_函数概述 103_数组_7_是否存在多维数组 102_数组_6_二维数组的使用 ...
  • asp.net知识库

    2015-06-18 08:45:45
    .NET 2.0 泛型在实际开发中的一次小应用 C#2.0 Singleton 实现 .Net Framwork 强类型设计实践 通过反射调用類方法,屬性,字段,索引器(2種方法) ASP.NET: State Server Gems 完整动态加载/卸载程序集解决方案 ...
  • 然而,对齐内存访问仅需要一次访问。 14、 static有什么用途?(请至少说明两种) 答: (1)在函数体,一个被声明为静态变量在这一函数被调用过程中维持其值不变。 (2) 在模块内(但在函数体外),一个被声明为静态...
  • 一次看到“逆向工程”这个词是在2001年《机械工程学报》上一篇文章中,主要是讲用三坐标测量仪测量产品中各个部件尺寸并在计算机中快速建模、进而反推其设计思想基本设计原则。第一次使用逆向工程工具...
  • 2.19 为什么用交叉熵代替二次代价函数 28 2.20 什么是损失函数? 28 2.21 常见损失函数 28 2.22 逻辑回归为什么使用对数损失函数? 30 0.00 对数损失函数是如何度量损失? 31 2.23 机器学习中为什么需要梯度下降...
  • RCP法与RSP法主要区别是前者采用循环渐进开发方式,原型将成为最终产品,而后者将被废弃。(√) 三、简答题 1. 软件产品特性是什么? 答: ● 软件是种逻辑产品,具有无形性;  ● 软件产品生产...
  • 进制是基数为2,每位权是以2 为底进制,遵循逢原则,基本符号为01。采用进制码表示信息,有如下几个优点:1.易于物理实现;2.进制数运算简单;3.机器可靠性高;4.通用性强。其缺点是它表示数...

空空如也

空空如也

1 2 3
收藏数 50
精华内容 20
关键字:

一次四维和二次四维的区别