☰
Linux嵌入式学习———指针
2026/10/23 9:20:37 网站建设 项目流程

前言

本篇学习指针的相关内容,包括内存和地址的相关概念,指针基本概念,定义,调用,数组与指针,函数与指针,二级指针,野指针与悬空指针,万能指针等等相关内容。以及C语言的右左法则来辅助理解。

一、基本概念

1.1 内存与地址

内存可以看作一列大小相等的格子,他们连成一条“内存条”,每个格子内能够存放两个16进制大小的数据,即8bit数据,每个格子又有一个地址编号,记录他们存在的位置。就像一个单元楼,每层占用的地方一样大,内部如何构造(存储不同数据)它的大小是固定的,每层都有一个地址编号,方便我们直接找到要去的地方。指针就是一种记录地址,并指向地址所在位置的类型。

1.2 指针类型

指针是一种专门用来处理“地址”的特殊数据类型,本质就是地址,地址就是内存单元的编号。

1.3 定义格式

基类型 *指针变量名

基类型:基类型表示的是指针所指向的地址所在空间的数据的类型。

*:在指针定义时,是类型修饰符,用来表示这个变量是指针类型。

在运算时,是解引用运算。

指针变量名:命名规则与标识符一致。

// 举例: int? p = null; /* 表示定义了一个指针变量 p,指向一个地址,这个地址上的空间存放的是 int 型数据。*/

1.4 指针的大小

需要注意,指针是一种“地址”类型,它的大小与基类型无关,它的大小与操作系统的地址总线相关。一个32位操作系统的地址总线位32位,即4字节(1字节=8bit,32位-->32/8=4字节),所以它的指针大小为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 指针的基础运算

单个指针可以进行加减操作,两个指针之间只能在特定情况下做减法运算。其余的都是未定义行为。

指针p+i:表示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";//正确

(2)const关键字修饰

在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 表示数组元素数据

p+i 第i行的整行地址

*(p+i)第i行的首元素地址

*p+i 第一行首元素的下i个元素地址

*(p+i)+j i行j列的元素地址

*(*(p+i)+j ) i行j列的元素值

二维数组理解为一维数组的数组

a[2][3]表示两行三列,可以理解成{a1[3],a2[3]}两个长度为3的数组在一个数组里。而一个a[3]里又有三个元素。类似于这样:

a0[3]---->a00 a01 a02

a1[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] 这一行 // *(*(p+i)+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; // *(p+i) 是第 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 函数回调

把函数的地址传递到另一个函数。

  • 应用场景

    • 通用算法

      • qsort

      • bsearch

    • 事件驱动编程(底层开发,GUI编程)

      • 按钮点击事件

      • 中断处理

    • 状态机与命令模式

经典示例:qsort:void 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 *p(int,int); 表示一个函数,返回值为整型指针
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 期望的是指针的地址。
  • 二级指针步长

    二级指针pp+1移动的字节数却决于操作系统对应的指针的大小。跨过的是一个一级指针的大小(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、不返回局部变量地址这些坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询