(一)数组指针、指针数组
-
指针数组 实质是一个数组,这个数组中存储的内容全部是指针变量。 数组指针 实质是一个指针,这个指针指向的是一个数组。 -
int *p[5]; 指针数组 int *p[5]; 核心是p,p是一个数组,数组有5个元素大,数组中的元素都是指针,指针指向的元素类型是int类型的;整个符号是一个指针数组。 int (*p)[5]; 数组指针 核心是p,p是一个指针,指针指向一个数组,数组有5个元素,数组中存的元素是int类型; 总结一下整个符号的意义就是数组指针。 int *(p[5]); 指针数组 解析方法和结论和第一个相同,()在这里是可有可无的。 - 注意:
- 符号的优先级到底有什么用?
- 其实是决定当2个符号一起作用的时候决定哪个符号先运算,哪个符号后运算。
- 遇到优先级问题怎么办?
- 第一,查优先级表;第二,自己记住(只要记住[] . ->这几个优先级比较好即可)。
- 符号的优先级到底有什么用?
- 注意:
(二)函数指针
- 函数指针的实质(还是指针变量)
- (1)函数指针的实质还是指针,还是指针变量。本身占4字节(在32位系统中,所有的指针都是4字节)
- (2)函数指针、数组指针、普通指针之间并没有本质区别,区别在于指针指向的东西是个什么玩意。
- (3)函数的实质是一段代码,这一段代码在内存中是连续分布的(一个函数的大括号括起来的所有语句将来编译出来生成的可执行程序是连续的),所以对于函数来说很关键的就是函数中的第一句代码的地址,这个地址就是所谓的函数地址,在C语言中用函数名这个符号来表示。
- (4)结合函数的实质,函数指针其实就是一个普通变量,这个普通变量的类型是函数指针变量类型,它的值就是某个函数的地址(也就是它的函数名这个符号在编译器中对应的值)
- 函数指针的书写
- 假设我们有个函数是:void func(void); 对应的函数指针:
void (*p)(void);
类型是:void (*)(void);
- 假设我们有个函数是:void func(void); 对应的函数指针:
- 函数名与数组名的区别
- 函数名和数组名最大的区别就是:函数名做右值时加不加&效果和意义都是一样的;但是数组名做右值时加不加&意义就不一样。
-
#include <stdio.h> void func(void); typedef void (*pFunc)(void); int main(void) { int a[3] = {0}; int *p = NULL; int (*p3)[3] = NULL; //数组名做右值时,不加&代表数组首元素的地址(a和&a[0]等同) //而加&时,&a代表整个数组整体的地址,类型为 int (*)[3] p = a; //p = &a; p3 = &a; //函数名做右值时,加不加&都是一样的 pFunc p1 = func; pFunc p2 = &func; return 0; } void func(void) { }
-
- 函数名和数组名最大的区别就是:函数名做右值时加不加&效果和意义都是一样的;但是数组名做右值时加不加&意义就不一样。
- 一个复杂的实例:
- 譬如函数是strcpy函数
char *strcpy(char *dest, const char *src);
,对应的函数指针是:char *(*pFunc)(char *dest, const char *src);
- 譬如函数是strcpy函数
(三)typedef的用法
- typedef是C语言中一个关键字,作用是用来定义(或者叫重命名类型)
- C语言中的类型一共有2种:
-
类型ADT(编译器定义的原生类型) 基础数据类型,如int、double之类的 义类型UDT(用户自定义类型) 不是语言自带的是程序员自己定义的(譬如数组类型、结构体类型、函数类型·····)
-
- 注意:typedef是给类型重命名,也就是说typedef加工出来的都是类型,而不是变量。
- (1)类型是一个数据模板,变量是一个实在的数据。类型是不占内存的,而变量是占内存的。
- (2)面向对象的语言中:类型就是类class,变量就是对象。
- typedef与#define宏的区别
-
typedef char *pChar; #define pChar char *
-
- typedef与结构体
- 结构体在使用时都是先定义结构体类型,再用结构体类型去定义变量。
- C语言语法规定,结构体类型使用时必须是struct 结构体类型名 结构体变量名;这样的方式来定义变量。
- 使用typedef一次定义2个类型,分别是结构体变量类型,和结构体变量指针类型。
-
#include <stdio.h> struct student { char name[20]; int age; }; //使用typedef定义了新的数据类型,此时可以使用struct teacher、teacher_t定义变量 //可以使用pTeacher_t定义结构体指针变量 typedef struct teacher { char name[20]; int age; char course; }teacher_t, *pTeacher_t; int main(void) { struct student stu; struct teacher t; teacher_t t1; t1.age = 20; pTeacher_t p1 = &t1; printf("teacher's age is %d.\n", p1->age); return 0; }
- typedef与const
- typedef int *PINT; const PINT p2; 相当于是int *const p2;
- typedef int *PINT; PINT const p2; 相当于是int *const p2;
- 如果确实想得到const int *p;这种效果,只能typedef const int *CPINT; CPINT p1;
-
#include <stdio.h> typedef int *PINT; typedef const int *PINT_t; int main (void) { int a = 5; int b = 5; const PINT p1 = &b; //无论const 放在哪里,p1的值都是不可改变的 PINT const p2 = &b; // *p1的值是可以改变的 printf("0x%x\n", p2); //p1 = &a; //error: assignment of read-only variable ‘p1’ //p2 = &a; //error: assignment of read-only variable ‘p2’ *p1 = a; *p2 = a; PINT_t p3 = NULL; //同上面相反 *p3 = a; //error: assignment of read-only location ‘*p3’ p3 = &b; printf("0x%x\n", p3); p3 = &a; printf("0x%x\n", p3); return 0; }
- 使用typedef的重要意义(2个:简化类型、创造平台无关类型)
- 简化类型的描述。char ()(char *, char *); typedef char *(*pFunc)(char *, char *);
- 很多编程体系下,人们倾向于不使用int、double等C语言内建类型,因为这些类型本身和平台是相关的(譬如int在16位机器上是16位的,在32位机器上就是32位的)。为了解决这个问题,很多程序使用自定义的中间类型来做缓冲。譬如linux内核中大量使用了这种技术.
内核中先定义:typedef int size_t; 然后在特定的编码需要下用size_t来替代int(譬如可能还有typedef int len_t) - STM32的库中全部使用了自定义类型,譬如typedef volatile unsigned int vu32;
(四)二重指针
- 本质上来说,二重指针和一重指针的本质都是指针变量,指针变量的本质就是变量。一重指针变量和二重指针变量本身都占4字节内存空间。
- 二重指针本质上也是指针变量,和普通指针的差别就是它指向的变量类型必须是个一重指针。
- 二重指针的用法
- 二重指针指向一重指针的地址
- 二重指针指向指针数组的(二重指针就是:指针数组指针)
#include <stdio.h> int main(void) { int *p; int **p1; int a = 5; p = &a; p1 = &p; int *b[5] = {0}; p1 = b; //数组名作右值时,代表数组首元素的地址,指针数组中的元素类型为 int * //所以b的类型就是 int **,和p1二重指针是匹配的。 return 0; }
- 实践编程中二重指针用的比较少,大部分时候就是和指针数组纠结起来用的。
- 实践编程中有时在函数传参时为了通过函数内部改变外部的一个指针变量,会传这个指针变量的地址(也就是二重指针)进去
(五)二维数组
- (1)一维数组在内存中是连续分布的多个内存单元组成的,而二维数组在内存中也是连续分布的多个内存单元组成的。
- (2)二维数组int a[2][4]和一维数组int a[8]其实没有任何本质差别。我们可以把两者的同一单元的对应关系写下来。
a[0] | a[1] | ··· | a[3] | a[4] | ··· | a[8] |
---|---|---|---|---|---|---|
a[0][0] | a[0][1] | ··· | a[0][3] | a[1][0] | ··· | a[1][3] |
- 哪个是第一维哪个是第二维?
- (1)二维数组int a[2][4]中,2是第一维,4是第二维。
- (2)结合内存映像来理解二维数组的第一维和第二维的意义。首先第一维是最外面一层的数组,所以int a[2][4]这个数组有2个元素;其中每一个元素又是一个含有4个元素的一维数组(这个数组就是第二维)。
- (3)总结:二维数组的第一维是最外部的那一层,第一维本身是个数组,这个数组中存储的元素也是个一维数组;二维数组的第二维是里面的那一层,第二维本身是个一维数组,数组中存的元素是普通元素,第二维这个一维数组本身作为元素存储在第一维的二维数组中。
- 二维数组的下标式访问和指针式访问
- (1)回顾:一维数组的两种访问方式。以int b[10]为例, int *p = b;。
b[0] 等同于 *(p+0); b[9] 等同于 *(p+9); b[i] 等同于 *(p+i) - (2)二维数组的两种访问方式:以int a[2][5]为例,(合适类型的)p = a;
a[0][0]等同于*(*(p+0)+0); a[i][j]等同于*(*(p+i)+j)
- (1)回顾:一维数组的两种访问方式。以int b[10]为例, int *p = b;。
- 二维数组的应用和更多维数组
- (1)最简单情况,有10个学生成绩要统计;如果这10个学生没有差别的一组,就用b[10];如果这10个学生天然就分为2组,每组5个,就适合用int a[2][5]来管理。
- (2)最常用情况:一维数组用来表示直线,二维数组用来描述平面。数学上,用平面直角坐标系来比拟二维数组就很好理解了。
- (3)三维数组和三维坐标系来比拟理解。三维数组其实就是立体空间。
- (4)四维数组也是可以存在的,但是数学上有意义,现在空间中没有对应(因为人类生存的宇宙是三维的)。
总结:一般常用最多就到二维数组,三维数组除了做一些特殊与数学运算有关的之外基本用不到。(四轴飞行器中运算飞行器角度、姿态时就要用到三维数组)
(六)二维数组与指针的关系
- 指针指向二维数组的数组名
- (1)二维数组的数组名表示二维数组的第一维数组中首元素(也就是第二维的数组)的首地址
- (2)二维数组的数组名a等同于&a[0],这个和一维数组的符号含义是相符的。
- (3)用数组指针来指向二维数组的数组名是类型匹配的。
-
#include <stdio.h> int main(void) { int a[2][4] = {{1, 2, 3, 4},{5, 6, 7, 8}}; printf("a[1][2] = %d\n", a[1][2]); printf("a[1][2] = %d\n", *(*(a + 1) + 2)); int (*p)[4]; //数组指针,指向一个数组,数组有4个int 类型元素; p = a; //二维数组名作右值,代表二维数组的第一维的数组的首元素地址 //因此,a等同于&a[0] p = &a[0]; printf("a[1][3] = %d\n", *(*(p+1)+3)); return 0; }
- 指针指向二维数组的第一维
- (1)用int *p来指向二维数组的第一维a[i]
-
#include <stdio.h> int main(void) { int a[2][4] = {{1, 2, 3, 4},{5, 6, 7, 8}}; int *p1; p1 = a[0]; p1 = &a[0][0]; //a[0]表示二维数组的第一维的第一个元素, //相当于第二维数组的数组名, //又因为数组名就是首元素地址,所以,a[0]等同于&a[0][0] printf("a[0][1] = %d\n", *(p1 + 1)); int *p2; p2 = a[1]; printf("a[1][1] = %d\n", *(p2 + 1)); return 0; }
- 指针指向二维数组的第二维
- (1)二维数组的第二维元素其实就是普通变量了(a[1][1]其实就是int类型的7),已经不能用指针类型和它相互赋值了。
- (2)除非int *p = &a[i][j];,类似于指针指向二维数组的第一维