C语言指针从入门到进阶:地址、数组、字符串、函数传参、二维数组与二级指针)
一、为什么学习指针指针是 C 语言的重要概念也是 C 语言的重要特色。正确而灵活地使用指针可以表示复杂的数据结构如链表、树、图动态分配内存方便地使用字符串高效地操作数组在函数调用中获得一个以上的结果直接处理内存单元地址。可以说不掌握指针就没有真正掌握 C 语言的精华。指针难在哪里指针本身比较抽象指针可以和前面学过的几乎所有内容结合指针 普通变量指针 数组指针 函数指针 指针。学习指针时始终抓住两点搞清楚指针变量本身搞清楚指针变量指向的目标。二、指针是什么计算机内存按字节编址一个字节对应一个地址编号。例如text内存地址1000 1001 1002 1003 ... 内容 ? ? ? ?地址本身也是一个数值但它不是普通数值而是“内存单元的编号”。所以指针就是地址地址就是内存单元的编号。为了存储和处理“地址”这种特殊数据C 语言设计了指针类型。cint a 10; // a 是 int 类型存放整数 int *p; // p 是 int* 类型用来存放 int 变量的地址区别概念含义指针地址内存单元编号指针值一个地址编号指针类型专门处理地址的数据类型指针变量存放地址的变量注意地址不是单纯的整数而是具有“指向含义”的数值。三、指针变量的定义与使用1. 定义语法c基类型 *指针变量名;例如cint *p; // 指向 int 类型数据的指针变量 char *cp; // 指向 char 类型数据的指针变量 float *fp; // 指向 float 类型数据的指针变量说明基类型指针所指向空间的数据类型*定义时用来说明该变量是指针类型指针变量名符合标识符命名规则。2. 指针变量占多大空间指针变量的大小与基类型无关与系统平台有关32 位系统地址通常是 32 位占 4 字节64 位系统地址通常是 64 位占 8 字节。cprintf(%zu\n, sizeof(int *)); printf(%zu\n, sizeof(char *)); printf(%zu\n, sizeof(void *));3. 取地址与初始化cint a 10; int *p a;a取 a 的起始地址p保存 a 的地址*p通过 p 间接访问 a。cprintf(%d\n, a); // 10 printf(%d\n, *p); // 10 *p 20; printf(%d\n, a); // 204. 多个指针变量定义cint *p, *q, *r; // p、q、r 都是指针 int a, b, c, *p1, *q1; // a、b、c 是 intp1、q1 是指针注意cint *p; p a; // 正确 // *p a; // 错误*p 是 int不是指针四、直接引用与间接引用cint a 10; a 20; // 直接引用 int *p a; *p 30; // 间接引用*p的运算过程拿出 p 中保存的地址值到内存中定位从定位处开始偏移sizeof(基类型)大小的一块空间将这块空间当作一个基类型变量来看。也就是说c*p a关系c*p p *a a和*在一定意义上互为逆运算。五、指针经典用途被调修改主调指针最重要的用途之一让被调函数修改主调函数中的变量。值传递不能修改主调cvoid addOne(int x) { x x 1; } int main() { int n 10; addOne(n); printf(%d\n, n); // 仍然是 10 return 0; }地址传递可以修改主调cvoid addOne(int *p) { *p *p 1; } int main() { int n 10; addOne(n); printf(%d\n, n); // 11 return 0; }记忆要修改谁就传谁的地址在被调函数中一定要有对应的*p运算。示例交换两个数cvoid swap(int *a, int *b) { int t *a; *a *b; *b t; }示例time 函数c#include time.h time_t time(time_t *tloc);两种使用方式c// 方式1通过返回值 time_t t1 time(NULL); // 方式2通过指针形参 time_t t2; time(t2);练习通过指针带出结果求和cvoid add(int a, int b, int *ret) { *ret a b; }同时得到和与差cvoid sum_diff(int a, int b, int *sum, int *diff) { *sum a b; *diff a - b; }同时得到最大值和最小值cvoid max_min(int a, int b, int *max, int *min) { if (a b) { *max a; *min b; } else { *max b; *min a; } }六、指针操作整型一维数组数组特点连续性单一性类型一致有序性按顺序存放。数组名在大多数表达式中代表首元素地址cint a[5] {1, 2, 3, 4, 5}; int *p a; // 等价于 a[0]通过指针访问数组cfor (int i 0; i 5; i) { printf(%d , *(p i)); }核心等价关系ca[i] *(a i) *(p i) p[i]数组作函数参数下面两种写法等价cvoid printArray(int a[], int len); void printArray(int *a, int len);因为数组名作参数时会退化为指针。数组最大值cint findMax(int *a, int len) { int max *a; for (int *p a 1; p a len; p) { if (*p max) { max *p; } } return max; }数组逆序cvoid reverse(int *a, int len) { int *left a; int *right a len - 1; while (left right) { int t *left; *left *right; *right t; left; right--; } }七、指针运算与数组等价关系指针运算cp 1 // 地址增加一个基类型大小 p N // 地址增加 N 个基类型大小 p - 1 // 地址减少一个基类型大小 p - N // 地址减少 N 个基类型大小 p p-- p - q // 同类型指针相减表示相差几个基类型关系运算cp q p q p q p q p q p ! q注意指针相减和关系比较通常要求指向同一块连续空间。数组访问等价关系ca[i] *(a i) *(p i) p[i]指针可以表示位置也可以实现间接访问。八、指针与字符串字符串本质上是特殊的字符型一维数组。cchar s[] hello;可以通过指针访问cchar *p s; // 等价于 s[0]两种字符串表示方式cchar s1[] hello; // 栈上的字符数组可修改 char *s2 hello; // 指向字符串常量区不建议修改 const char *s3 hello; // 更准确明确只读如果尝试修改s2指向的内容属于未定义行为cchar *s hello; // s[0] H; // 错误不要这样做如果要修改应使用数组cchar s[] hello; s[0] H; // 正确puts 函数cint puts(const char *s);设计函数时如果只是读字符串建议写成const char *cvoid printStr(const char *s) { puts(s); }好处避免误修改提高参数适用性可以接收char *、数组名、字符串字面量、const char *。九、const 与指针const表示将所修饰的变量限定为只读。原则就近原则离谁近就限定谁。写法含义const char *p*p只读p 可改char const *p*p只读p 可改char * const pp 只读*p可改const char * const pp 和*p都只读示例cconst char *p1 hello; // *p1 H; // 错误 p1 world; // 正确 char * const p2 hello; // p2 world; // 错误 const char * const p3 hello; // p3 world; // 错误 // *p3 H; // 错误十、字符串操作函数实现思路常见字符串函数cstrcpy strncpy strcat strncat strcmp strncmp1. Strcpycchar *Strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) ; return ret; }要点将 src 中的字符逐个拷贝到 dest最后把\0也拷过去返回 dest 首地址支持链式调用。链式调用cstrcpy(s3, strcpy(s2, strcpy(s1, hello)));2. Strncpycchar *Strncpy(char *dest, const char *src, size_t n) { char *ret dest; while (n *src) { *dest *src; n--; } while (n--) { *dest \0; } return ret; }要点将 src 的前 n 个字符拷贝到 dest如果 src 长度不足 n剩余位置补\0如果 src 长度大于等于 n则最终 dest 中不一定有\0。3. Strcatcchar *Strcat(char *dest, const char *src) { char *ret dest; while (*dest) dest; while ((*dest *src) ! \0) ; return ret; }要点先定位到 dest 的\0位置从\0位置开始拷贝 src返回 dest 首地址。4. Strncatcchar *Strncat(char *dest, const char *src, size_t n) { char *ret dest; while (*dest) dest; while (n *src) { *dest *src; n--; } *dest \0; return ret; }要点最多拼接 n 个字符最后一定要补\0。5. Strcmpcint Strcmp(const char *s1, const char *s2) { while (*s1 *s1 *s2) { s1; s2; } return (unsigned char)*s1 - (unsigned char)*s2; }要点从左到右逐个比较遇到不相同或\0停止返回结束位置两个字符的差值。6. Strncmpcint Strncmp(const char *s1, const char *s2, size_t n) { while (n *s1 *s1 *s2) { s1; s2; n--; } if (n 0) return 0; return (unsigned char)*s1 - (unsigned char)*s2; }要点只比较前 n 个字符遇到不相同或\0停止。十一、memcpy 与 void *c#include string.h void *memcpy(void *dest, const void *src, size_t n);功能将 src 空间上 n 个字节的数据拷贝到 dest 所指向的空间返回值成功返回 dest失败返回 NULL。示例cint a[10] {1, 2, 3, 4, 5}; int b[10]; memcpy(b, a, sizeof(a));void *是万能指针可以接收任意类型的地址一般用于形参设计可以接受任意类型地址而不报警告不能直接解引用如果要访问数据需要强转为具体类型指针。cint a 10; void *vp a; int *p (int *)vp; printf(%d\n, *p);十二、指针操作二维数组整型二维数组cint a[3][4] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};二维数组本质一维数组的数组。cint[4] a[3]; // 理解角度a 是 3 个元素每个元素是 int[4]定义数组指针cint (*p)[4] a[0]; // 正确写法也可以写成cint (*p)[4] a;理解p指向一行一行有 4 个 int*p相当于a[0]即一个一维数组*p也相当于该一维数组的数组名。访问方式ca[i][j] *(*(p i) j)示例cfor (int i 0; i 3; i) { for (int j 0; j 4; j) { printf(%d , *(*(p i) j)); } printf(\n); }练习找出二维数组中最大值cint findMax2D(int (*p)[4], int rows, int cols) { int max p[0][0]; for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (p[i][j] max) { max p[i][j]; } } } return max; }字符型二维数组cchar s[5][10] { hello, world, english, china, america };正确指针写法cchar (*p)[10] s;通过p访问字符串cprintf(%s\n, p[i]);十三、指针数组与二级指针1. 指针数组cchar *s1 hello1; char *s2 hello2; char *s3 hello3; char *s4 hello4; char *s[5]; // 指针数组数组中每个元素都是 char * s[0] s1; s[1] s2;区别cint *arr[3]; // 指针数组3 个 int* int (*p)[3]; // 数组指针指向 int[3]2. 二级指针cint a 10; int *p a; int **pp p; **pp 20; // 等价于 a 20什么时候需要二级指针如果需要保存的是一级指针变量的地址需要修改一级指针本身时。对于指针数组cchar *s[5]; s[0]; // char * s[0]; // char **所以cchar **p s;可以通过二级指针访问字符串数组。十五、补充与易错点1. 野指针指针变量未初始化里面可能是随机值指向不明确cint *p; // 野指针避免方式cint *p NULL;2. NULLNULL表示空地址通常为 0 号地址。cint *p NULL;使用前应判断cif (p ! NULL) { *p 10; }3. 与 * 互为逆运算cint a 10; int *p a; *p p; *a a;4. 指针数组与数组指针cint *arr[3]; // 指针数组3 个 int* int (*p)[3]; // 数组指针指向 int[3]5. 数组名的例外数组名大多数情况下表示首元素地址但有两个例外cint a[5]; sizeof(a); // 整个数组大小20 字节 a; // 类型是 int (*)[5]1 跨整个数组6. 大小端判断小端低地址存低字节。大端低地址存高字节。cint isLittleEndian(void) { int a 1; char *p (char *)a; return *p 1; }小端返回 1大端返回 0。联合体写法cint isLittleEndian(void) { union { int i; char c; } u; u.i 1; return u.c 1; }十七、总结知识点核心结论指针是什么指针就是地址地址就是内存单元编号指针变量存放地址的变量定义语法基类型 *变量名;取地址a解引用*p直接引用通过变量名访问间接引用通过指针访问被调修改主调传地址被调中要有*p运算指针与数组a[i] *(ai) *(pi) p[i]指针运算p1加一个基类型大小p-q表示相差几个基类型字符串char s[]可改char *s指向常量区建议用const char *const就近原则限定谁谁只读字符串函数strcpy、strncpy、strcat、strncat、strcmp、strncmpmemcpy按字节拷贝void *万能指针二维数组int (*p)[4]a[i][j] *(*(pi)j)指针数组char *s[5]元素类型是char *二级指针char **p保存一级指针的地址野指针未初始化或指向不明的指针用NULL避免常见算法选择、冒泡、插入、二分、快排都可以用指针实现