ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Linux嵌入式学习———指针

Linux嵌入式学习———指针 前言本篇学习指针的相关内容包括内存和地址的相关概念指针基本概念定义调用数组与指针函数与指针二级指针野指针与悬空指针万能指针等等相关内容。以及C语言的右左法则来辅助理解。一、基本概念1.1 内存与地址内存可以看作一列大小相等的格子他们连成一条“内存条”每个格子内能够存放两个16进制大小的数据即8bit数据每个格子又有一个地址编号记录他们存在的位置。就像一个单元楼每层占用的地方一样大内部如何构造存储不同数据它的大小是固定的每层都有一个地址编号方便我们直接找到要去的地方。指针就是一种记录地址并指向地址所在位置的类型。1.2 指针类型指针是一种专门用来处理“地址”的特殊数据类型本质就是地址地址就是内存单元的编号。1.3 定义格式基类型 *指针变量名基类型基类型表示的是指针所指向的地址所在空间的数据的类型。*在指针定义时是类型修饰符用来表示这个变量是指针类型。在运算时是解引用运算。指针变量名命名规则与标识符一致。// 举例 int? p null; /* 表示定义了一个指针变量 p指向一个地址这个地址上的空间存放的是 int 型数据。*/1.4 指针的大小需要注意指针是一种“地址”类型它的大小与基类型无关它的大小与操作系统的地址总线相关。一个32位操作系统的地址总线位32位即4字节1字节8bit32位--32/84字节所以它的指针大小为4字节大小固定不变。同理64位操作系统的指针大小位8字节。1.5 解引用 *解引用分为三步且解引用的运算数类型必须为指针类型。1找通过指针找到地址所在位置2偏移通过基类型的大小圈定内存。如int型4字节就取4个字节的大小。3用将地址内的数据当作对应基类型的变量操作。1.6 实现被调修改主调指针可以实现被调修改主调。C语言中的传递都是值传递如果是一个普通变量通过值传递到函数内后函数内的变量接收这个值而这个变量在函数内的生命周期仅限变量定义到函数结束的过程所以无法做到普通变量的值修改。指针变量却不同指针传递的是地址的值。只要有地址就能找到对应的数据就能直接对数据进行修改。1.7 野指针与悬空指针野指针没有指向过有效内存地址是乱的。常见于没有初始化指针。悬空指针指向过有效内存但内存没了指针还留着旧地址。常见于指针指向的地址释放后堆内存释放未置空。解析举例一个人A和另一个人B见面A把B的样子记住了B走后A可以随意修改他记住的B的样貌对B没有任何影响这就是普通变量的值传递。然后A在名片上知道了B的住址他通过住址指针找到了B并在B的脸上乱涂乱画直接对B造成了影响这就是指针的被调修改主调通过一个住址就能对B进行修改。后来A不小心把名片搞脏了地址发生了改变变成一个并不存在的地址。如果A没有验证就按照这个地址去找会随机找到一个地址这个地址是不确定的。即指针未初始化地址值是随机的属于野指针。后来A找回了B的地址但是B已经搬走了换成了别的人住此时A再按照地址找虽然能找到那个房子但是找不到B了。即指针指向的地址内的数据被释放了给了“别人”但是指针还是指向那个地址此时指针属于悬空指针。1.8 万能指针 void *没有定义指针的类型可以传递所有类型。然后通过强制转换得到需要的类型。不能直接引用必须强制转换。1.9 指针的基础运算单个指针可以进行加减操作两个指针之间只能在特定情况下做减法运算。其余的都是未定义行为。指针pi表示p移动i个基类型的地址p-i同理p1-p2表示两个指针地址之间的差值。解析就类似一列相同的屋子的地址A、B、C他的占地大小我们就可以说是基类型大小。我们可以说A的前面第X个屋子是B后面第X个屋子是C但不能说A*X有什么含义。这是无法定义的。两个屋子AB之间相减我们可以得到他们相距几个屋子但相加也是没有什么意义的。二、数组与指针2.1 指针传递数组的原因当我们需要传递一个数组数据时如果按照以前的方法 将数组数据通过循环一个个传递到函数内这样的方式即麻烦又占用空间类比到其他的数据如果每次调用数据时都要传递他们的值是非常影响效率和空间的所以我们使用指针只需要传递他们的地址知道他们占用了多少空间就可以取得里面的数据。利用指针传递的方式提高了效率降低了空间占用率。2.2 整型一维数组指针在学习函数时我们知道数组的变量名代表了首元素地址而只要知道数组的长度就能得到数组的所有元素。2.2.1 定义所以我们可以直接定义一个指针变量记录数组的首元素地址int[] a new int[10]; int[] p a;2.2.2 取值我们又知道数组的其他元素的下标含义就是距离首元素的距离。那么我们可以得到// 在 C# 中数组下标即元素位置 p a; // 首元素引用 p[1] a[1]; p[2] a[2]; // ……此时我们做到了用指针来表示数组。但这只是找到了地址如果需要使用地址里的值还需要进行解引用运算 *(p 1) a[1]。那么我们只需要传入指针和数组的长度就能在函数里对数组进行修改了。此处需要注意指针变量的大小是固定的和操作系统有关。所以使用sizeof取得的值也是固定的这个值并不是数组的长度。2.2.3 示例// 遍历打印 static void PrintArray(int[] arr) { foreach (int item in arr) { Console.Write(${item,3}); } Console.WriteLine(); }// 整型一维数组求最大值 static int MaxArray(int[] arr) { int max arr[0]; foreach (int item in arr) { if (item max) max item; } return max; }//逆序 void reverseArray(int *begin, int *end) { while (begin end) { swap(begin, end); begin; --end; } }//选择排序 void selectSort(int *begin, int *end) { while (begin end) { int *i begin; while (i end) { if (*i *begin) swap(i, begin); i; } begin; } }//冒泡排序 void bubbleSort(int *begin, int *end) { while (begin end) { int *i begin; while (i end) { if (*i *(i 1)) swap(i, i 1); i; } --end; } }//插入排序 void insertSort(int *begin, int *end) { int *p; for (p begin 1; p end; p) { int t *p; int *q p; while (q begin *(q - 1) t) { *q *(q - 1); --q; } *q t; } }//二分查找 int *binaryFindIter(int *begin, int *end, int f) { while (begin end) { int *mid begin (end - begin) / 2; if (*mid f) end mid - 1; else if (*mid f) begin mid 1; else return mid; } return NULL; }//二分查找递归 int *binaryFindRec(int *begin, int *end, int f) { if (begin end) return NULL; int *mid begin (end - begin) / 2; if (*mid f) return binaryFindRec(begin, mid - 1, f); else if (*mid f) return binaryFindRec(mid 1, end, f); else return mid; }//快速排序 void quickSort(int *begin, int *end) { if (end begin) return; int *p begin; int *q end; int *k begin; while (begin end) { while (begin end *end *k) --end; while (begin end *begin *k) begin; swap(begin, end); } swap(k, begin); quickSort(p, end - 1); quickSort(begin 1, q); }2.3 字符型一维数组指针与整型数组同理但如果是单个字符的字符型我们按整型的传入首地址指针和长度就可以。但是如果是字符串字符串有结束标志\0那么就不用传入数组长度直接判断到结束标志时结束就可以了。2.3.1 const与字符串常量区问题1字符串常量区在进行字符串处理时会出现报错原因是字符串常量区的字符串是不可修改的只读类型的常量。字符串常量区指的是当使用 双引号定义字符串时这个定义出来的字符串存储的位置在字符串常量区如果直接使用指针引用这个字符串的地址指向的是常量区地址是无法修改的所以会报错。而数组在定义时会先开辟一块数组大小的空间然后进行赋值运算将字符串常量区的值赋值到数组中。char *s hello; //错误 char s[10] hello;//正确2const关键字修饰在C语言中存在关键字const可以限定某个变量为只读模式。存在就近原则。const char *p限定 *p 只读内容不可改 char * const p限定 p 只读指向不可改 const char * const p指向和内容都不可改。当存在只读数据时尽量都加上const修饰防止修改不能修改的数据造成错误。2.3.2 示例//遍历 void printStr(char *s) { while (*s ! \0) { putchar(*s); } putchar(\n); }//求字符串长度 int Strlen(const char *s) { const char *p s; while (*p ! \0) p; return (int)(p - s); }//strcpy char *Strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) { dest; src; } return ret; }//strcat char *Strcat(char *dest, const char *src) { char *ret dest; while (*dest ! \0) dest; while (*src ! \0) *dest *src; *dest \0; return ret; }//strcmp int Strcmp(const char *s1, const char *s2) { while (*s1 *s2 *s1 ! \0) { s1; s2; } return *s1 - *s2; }//strncmp int Strncmp(const char *s1, const char *s2, int n) { if (n 0) return 0; while (n 1 *s1 *s2 *s1 ! \0) { s1; s2; --n; } return *s1 - *s2; }//逆序 void reverseStr(char *begin, char *end) { while (begin end) { char t *begin; *begin *end; *end-- t; } }//字符转整数 int strToInt(char *begin, char *end) { if (end begin) return *end - 0; return (*end - 0) strToInt(begin, end - 1) * 10; }//单词逆序 void reverseWords(char *begin, char *end) { reverseStr(begin, end); char *left begin, *right begin; while (right end 1) { if (*right || *right \0) { reverseStr(left, right - 1); left right 1; } right; } }2.4 整型二维数组指针2.4.1 定义格式int *p[n] 本质是int[n] *p 基类型为数组类型 数组指针指向一维数组的指针2.4.2 取值定义时p 表示一个变量元素。*p 表示一个数组内的变量元素。**p 表示一个数组的数组的变量元素就是二维变量元素取值时p 表示二维数组首地址*p 表示行首元素的数据**p 表示数组元素数据pi 第i行的整行地址*pi第i行的首元素地址*pi 第一行首元素的下i个元素地址*pij i行j列的元素地址**pij i行j列的元素值二维数组理解为一维数组的数组a[2][3]表示两行三列可以理解成{a1[3],a2[3]}两个长度为3的数组在一个数组里。而一个a[3]里又有三个元素。类似于这样a0[3]----a00 a01 a02a1[3]----a10 a11 a12取值时对p运算相当于二维数组中的行地址运算和一维数组移动类似,就是移动行。对*p运算相当于对行地址里的数据进行运算就是对列地址运算。对**p运算相当于再去列地址里的数据进行运算就是二维数组的每个元素进行运算了。2.4.3 示例//初始化 int a[3][4] {1,2,3,4,5,6,7,8,9,10,11,12}; int (*p)[4] a; // p 指向 a[0] 这一行 // *(*(pi)j) 等价于 a[i][j]//遍历 void printMatrix(int (*a)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { printf(%3d, *(*(a i) j)); } putchar(\n); } }//指针函数求最大值 int *maxMatrix(int (*a)[4], int rows) { int *ret *a; for (int i 0; i rows; i) for (int j 0; j 4; j) if (*ret *(*(a i) j)) ret *(a i) j; return ret; }2.5 字符型二维数组指针字符型的二维数组在利用指针传递时如果是单个字符元素和整型相似。如果是字符串就相当于基类型为字符串的一维数组。char s[ 5 ][ 10 ]; char *p[ 10 ] s; 指针表示指向一个一维数组类型存放的基类型是长度10以内的字符串数据只需要对*p运算就能得到某一行的字符串数据。2.5.1 示例//定义 char s[5][10] {12,34,56,78,910}; char (*p)[10] s; // *(pi) 是第 i 行首地址类型 char*//遍历 void printStrMatrix(char (*p)[10], int rows) { for (int i 0; i rows; i) { printf(%s\n, *(p i)); } }2.6 指针数组指针数组表示一个数组数组内的基类型是指针类型。2.6.1 示例//遍历 void printPtrArray(char *s[], int len) { for (int i 0; i len; i) printf(%s\n, s[i]); }//逆序交换指针 void reversePtrArray(char *s[], int len) { for (int i 0; i len / 2; i) { char *t s[i]; s[i] s[len - 1 - i]; s[len - 1 - i] t; } }//选择排序。交换指针 void selectSortStr(char *s[], int len) { for (int i 0; i len - 1; i) { int min i; for (int j i 1; j len; j) if (strcmp(s[j], s[min]) 0) min j; char *t s[i]; s[i] s[min]; s[min] t; } }//冒泡排序。交换指针 void bubbleSortStr(char *s[], int len) { for (int i 0; i len - 1; i) for (int j 0; j len - 1 - i; j) if (strcmp(s[j], s[j 1]) 0) { char *t s[j]; s[j] s[j 1]; s[j 1] t; } }//插入排序。交换指针 void insertSortStr(char *s[], int len) { for (int i 1; i len; i) { char *k s[i]; int j i - 1; while (j 0 strcmp(s[j], k) 0) { s[j 1] s[j]; --j; } s[j 1] k; } }//找最大字符串 char *maxStr(char **s, int len) { char *ret *s; for (int i 0; i len; i) if (strcmp(ret, *(s i)) 0) ret *(s i); return ret; }三、函数与指针3.1 函数指针函数指针表示一个指针指向了一个函数。3.1.1 定义int (*p)int int 表示一个指针变量。指向返回值为int类型带有两个int类型形参的函数函数名表示了它的地址类似数组去掉标识符剩下的就是函数的返回值类型。3.1.2 函数回调把函数的地址传递到另一个函数。应用场景通用算法qsortbsearch事件驱动编程底层开发GUI编程按钮点击事件中断处理状态机与命令模式经典示例qsortvoid qsort(void *base, size_t nmemb, size_t size,int (*compar)(const void *, const void *));描述qsort() 函数用于对一个包含 nmemb 个元素、每个元素大小为 size 的数组进行排序。base 参数指向数组的起始位置。数组中的内容将根据比较函数进行升序排序该比较函数由 compar 指向并通过两个指向被比较对象的参数调用。比较函数必须返回一个整数值当第一个参数被认为小于、等于或大于第二个参数时分别返回小于零、等于零或大于零的值。如果两个元素在比较中被视为相等则它们在排序后的数组中的顺序是未定义的。qsort_r() 函数与 qsort() 函数相同唯一的区别在于比较函数 compar 需要传入第三个参数。通过 arg 参数传递给比较函数的指针使得比较函数无需使用全局变量来传递任意参数因此具有可重入性可在多线程环境中安全使用。3.1.3 示例//函数指针 int add(int a, int b) { return a b; } int main(void) { int (*p)(int, int) add; // p 指向 add printf(%d\n, (*p)(1, 2)); // 输出 3 return 0; }//排序回调 void selectSortCb(int *a, int len, int (*key)(int)) { for (int i 0; i len - 1; i) for (int j i 1; j len; j) if (key(a[i]) key(a[j])) { int t a[i]; a[i] a[j]; a[j] t; } } // 调用selectSortCb(a, 10, func2); 按平方值排序//函数指针数组实现四则运算 int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divi(int a, int b) { return a / b; } int (*ops[4])(int, int) { add, sub, mul, divi }; // 调用ops[i](10, 4)3.2 指针函数表示一个函数函数的返回值类型为指针。3.2.1 定义int *pintint 表示一个函数返回值为整型指针3.2.2 示例//返回最大元素地址 int *maxArray(int *begin, int *end) { int *ret begin; while (begin end) { if (*begin *ret) ret begin; begin; } return ret; }//返回最长字符串 char *maxStr(char **s, int len) { char *ret *s; for (int i 0; i len; i) if (strcmp(ret, *(s i)) 0) ret *(s i); return ret; }//Memcpy void *Memcpy(void *dest, const void *src, int n) { char *d (char *)dest; const char *s (const char *)src; while (n--) *d *s; return dest; }//二分查找返回元素地址 int *binaryFindIter(int *begin, int *end, int f) { while (begin end) { int *mid begin (end - begin) / 2; if (*mid f) end mid - 1; else if (*mid f) begin mid 1; else return mid; } return NULL; }四、二级指针4.1 如何理解C语言是值传递当一个指针作为形参传入函数时这个指针的地址值被函数内的指针变量接收是局部变量只能修改它指向的地址内的数据因为函数内外的指针指向的是同一个地址修改同一个地址的值从而做到被调修改主调。 但如果对这个一级指针进行修改它依旧是被作为形参局部变量修改无法带出它的指针值。于是当需要修改一级指针的指向时需要使用二级来修改。就是取一级指针的地址值通过二级指针指向一级指针地址值然后传递修改。一级指针作为函数形参被传递时函数内部修改的是一级指针指向的值。 这里一级指针有一片空间这个空间存放的是一个指向的地址的地址值就像定义一个变量内存里存放的是它的变量值这个变量被传递时只是传递它的值的大小传递到函数后重新开辟一块空间来存放这个数据。所以我们无法对这个变量做出修改。所以我们无法修改这个一级指针因为函数收到的是一个地址值并新开辟一块空间来存放这个地址值。 但是这个数据的地址没”死亡“前是不会变化的它里面的数据就能通过函数修改。4.2 三个应用场景1被调修改主调2指针数组传参3动态创建真正的二维数组在堆区申请一个 3 行 4 列 的二维数组非连续空间常用于字符串数组int **arr (int **)malloc(3 * sizeof(int *));// 申请 3 个行指针 for(int i 0; i 3; i) { arr[i] (int *)malloc(4 * sizeof(int));// 为每一行申请 4 个整型空间 } // 使用完之后必须逐行 free再 free(arr)4.3 易错点不要越级赋值int a 10; int **pp a; // 报错 a 的地址里装的是 int而 pp 期望的是指针的地址。二级指针步长二级指针pp1移动的字节数却决于操作系统对应的指针的大小。跨过的是一个一级指针的大小32位4字节64位8字节*pp类型*pp是int *类型值为地址4.4 示例//二级指针修改外部指针动态分配 void getMemory(char **p, int num) { *p (char *)malloc(num); } // 调用char *str NULL; getMemory(str, 100);//二级指针遍历数组 void printByPtr(char **s, int len) { for (int i 0; i len; i) printf(%s\n, *(s i)); }//二级指针交换字符串指针 void swapPtr(char **s1, char **s2) { char *t *s1; *s1 *s2; *s2 t; }//二级指针选择排序 void selectSortPtr(char **begin, char **end) { while (begin end) { char **min begin; for (char **i begin; i end; i) if (strcmp(*i, *min) 0) min i; swapPtr(min, begin); begin; } }//二级指针二分查找 char *binaryFindStr(char **begin, char **end, char *f) { while (begin end) { char **mid begin (end - begin) / 2; int cmp strcmp(f, *mid); if (cmp 0) begin mid 1; else if (cmp 0) end mid - 1; else return *mid; } return NULL; }五、右左法则在分析指针与其他的数据类型结合时常常分不清变量的含义到底是什么。此时使用右左法则来分析变量的类型是什么是整型指针数组指针函数指针函数指针数组5.1 核心标准5.2 分析步骤5.3 举例1、普通的指针和数组 int *p1; 先向右没有 再向左确认p是指针 再向左int指针指向的类型是int型数据 int arr1[5]; 向右[ ]变量是一个数组 向左int数组内的元素是int型 2、指针数组和数组指针 int *p2[5]; // 指针数组 先向右数组确认变量是一个数组类型 向左指针数组内的元素为指针类型 向左int指针指向的类型为int型 总结定义一个数组变量数组元素的基类型为指向整型的指针 指针数组一个基类型为指针的数组 int (*p3)[5]; // 数组指针 向右括号返回向左 向左指针变量类型为指针类型 再向右数组指针指向的是数组 再向左int数组元素的类型为int 总结定义了一个指针变量。指向一个基类型为整型的数组 数组指针指向数组的指针 3、指针函数和函数指针 int *f1(int); // 指针函数 变量向右int表示一个函数函数内的参数类型为int带参函数 向左*指针表示函数返回值为指针 再向左int表示返回的指针指向整型 总结定义一个函数变量函数的返回值类型为一个指向整型的指针 指针函数一个函数返回值是指针 注意不要返回局部变量地址生命周期有限 int (*f2)(int); // 函数指针 先向右强制向左*指针表示变量为指针 向右int表示这个指针变量指向函数 再向左函数的返回值类型为整型 总结定义一个指针变量指向一个返回值类型为整型的函数 函数指针一个指针指向的是函数 4、进阶函数指针数组 int (*f3[5])(int); // 函数指针数组 变量向右变量类型为数组类型 向左数组元素为指针 向右指针指向函数 向左函数返回值为int 总结定义一个数组数组基类型为指向一个返回值为int型的函数的指针 如果把变量外的去掉会报错 int *f35 假设正确按右左法则分析向右是数组再向右是函数数组的基类型是函数再向左函数返回值是整型指针。 但是有一个问题函数的大小是不确定的所以无法定义函数数组。 5、返回数组指针的函数 int (*f5(int))[5]; // 返回数组指针的函数 向右变量类型为函数 向左函数返回值为指针 向右指针指向数组 向左数组类型为int型 总结定义一个函数变量返回值为指向一个基类型为int型的数组的指针 // 如果用 typedef 就一目了然 typedef int(*pArr)[5]; pArr f6(int); 6、二级指针 int **pp p; // 二级指针 变量向右向左变量类型为指针类型 向左指针指向的是一个指针 向左。指针指向的指针指向的是一个整型int 说明最后的数据类型为整型5.4 总结总结在判断变量类型时先判断它最先和谁结合确认它的变量类型然后第二个是修饰这个已经结合的变量第三个修饰已经修饰过的结合变量……依次类推。就是后结合的修饰上一级结合的。六、总结指针的主要作用是被调修改主调还有减少重复的数据传递优化空间利用率。指针学习以“地址”为总开关先抓定义与解引用找地址、按基类型偏移、按类型使用再沿“一维数组→字符串→二维数组指针/指针数组→函数指针/指针函数→二级指针→右左法则”这条主线展开一维整型指针重点练遍历、反转、选择/冒泡/插入/快速排序和二分查找迭代与递归字符型指针重点练长度、复制、连接、比较、反转、转整数和单词逆序二维数组指针抓int (*p)[n]、char (*p)[n]的行指针本质指针数组抓“元素是指针、排序交换的是指针”函数指针抓回调与函数指针数组指针函数抓“返回指针的函数”二级指针抓“被调修改主调、指针数组传参、动态二维”全程用右左法则辨析复杂声明并始终防野指针/悬空指针、字符串常量区只读、const修饰、\0终止、二分边界begin end、释放后置NULL、不返回局部变量地址这些坑。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表