目录
前言
要想学好C语言,就一定要把指针学好!!!
虽然指针是C语言中较难的一部分,但是如果你真心愿意花时间慢慢学,一定还是能学好的,首先不要被它吓倒了。
一、指针数组与数组指针
1.arr 与 &arr 有什么区别?
arr -- 通常情况下表示数组首元素的地址,但是有两个特殊情况表示整个数组
1)sizeof(arr) ,计算的是整个数组的大小
2)&arr ,取出的是整个数组的地址
注意:arr、&arr[0]、&arr 数值上相同,但是表示的意义不同
2.指针数组
本质上是数组,每个元素的类型为指针类型
char* ch_arr[4]; int* i_arr[4]; 下面代码是否存在问题? 1 int main() 2 { 3 int* arr[4]; 4 int a = 1, b = 2, c = 3, d = 4; 5 int* pa = &a, pb = &b, pc = &c, pd = &d; 6 7 return 0; 8 } 其实我们建议第5行的代码中,' * ' 靠近 变量名 而不是 类型 这样可以避免我们误认为后面的 pb、pc、pd也是指针类型的变量
3.数组指针
本质是指针,存放的是数组的地址
int arr[5]; int (*p)[5] = &arr; //演示代码 int main() { int arr1[4] = { 1,2,3,4 }; /*int arr2[4] = { 2,3,4,5 }; int arr3[4] = { 3,4,5,6 };*/ //下标访问数组 for (int i = 0; i < 4; i++) { printf("%d ", arr1[i]);//arr[i] <=> *(arr + i) } printf("\n"); //指针访问数组 int* p = arr1; for (int i = 0; i < 4; i++) { printf("%d ", *(p + i));//p[i] <=> *(p + i) } printf("\n"); //数组指针访问数组 int(*pa)[4] = &arr1; //p -- &arr //*p -- *&arr //*p -- arr for (int i = 0; i < 4; i++) { ///把(*pa)[i] 写成 *pa[i] 这样的形式 //编译器可能跑的过去,但这样是存在问题的 printf("%d ", (*pa)[i]); //*(*pa + i) <=> (*pa)[i] } printf("\n"); return 0; } 运行结果如下:
4.识别数组指针与指针数组
如果你觉得你已经认识了它们,可以试着分析下面的代码分别代表什么 int arr1[10]; int *parr2[8];//指针数组 //类型: int * //变量名: parr2[8] int (*parr3)[6];//数组指针 //类型: int ()[6] //变量名: *parr3 int (*parr4[3])[4];//parr4是数组 //数组中存放的是指针,指针指向的又是数组 //类型: int (*)[4] //变量名: parr4[3]
二、数组传参和指针传参
1.一维数组传参
一维数组传参,形参是指针 void print(int arr[], int sz)//int arr[] <=> int *arr { for (int i = 0; i < sz; i++) { //printf("%d ", arr[i]);//*(arr + i) <=> arr[i] printf("%d ", *(arr + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); return 0; }
能不能只传递一个参数呢?把数组的大小sz放到函数里面去计算
int sz = sizeof(arr) / sizeof(arr[0]);因为函数里面的arr表示的不是数组,而是指针了,所以无法正确计算出数组原本的大小了
2.二维数组传参
二维数组的数组名 -- 表示首元素地址 int arr[3][4] = {1,2,3,4, 2,3,4,5, 3,4,5,6}; 我们也可以这样理解: 二维数组其实可以看成是一维数组的数组 arr[0] = {1, 2, 3, 4}; arr[1] = {2, 3, 4, 5}; arr[2] = {3, 4, 5, 6}; arr[3][4] = {arr[0], arr[1], arr[2]}; //打印输出时可以做如下替换 arr[i][j] <=> *(*(arr + i) + j) //函数调用创建形参可以做如下替换 int arr[3][4] <=> int (*arr)[4]
3.数组传参分析
下面哪些一维数组传参方式是正确的呢? void test1(int arr[]) {} void test1(int *arr) {} void test1(int arr[10]) {} void test2(int *arr2[]) {} void test2(int *arr2[10]) {} void test2(int** arr2) {} int main() { int arr1[4] = { 0 }; int* arr2[10] = { 0 }; test1(arr1); test2(arr2); return 0; } //都是正确的
下面哪些二维数组传参方式是正确的呢? void test(int arr[3][4]) {} void test(int arr[3][]) {} void test(int arr[][4]) {} void test(int* arr[4]) {} void test(int(*arr)[4]) {} void test(int** arr) {} int main() { int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 }; test(arr); return 0; } //1、3、5、6 //1、2、3.行可以省略,但是列不行。 //如果我们只知道列数,也能够判断一行有多少个元素 //如果我们只知道行数,就不知道一行有多少个元素了 //4.指针数组类型 //5.数组指针类型 //6.二级指针
4.指针传参
一级指针传参 void print(int* p, int sz) { for (int i = 0; i < sz; i++) { printf("%d ", *(p + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); int *p = arr; //一级指针p传参 print(p, sz); return 0; } 思考:当函数的参数部分为一级指针的时候,这个函数能够接收什么? int a; print(&a, sz);//地址 int b; int p = &b; print(p, sz);//指针变量 int arr[10]; print(arr, sz);//数组名
二级指针传参 void print(int** pstr) { printf("%d ", **pstr); } int main() { int num = 10; int* p = # int** pp = &p; print(&p); print(pp); return 0; } 思考:当函数的参数部分为一级指针的时候,这个函数能够接收什么? print(&p);//一级指针的地址 print(pp);//二级指针变量 int *arr[10]; print(arr);//指针数组
三、函数指针
字符指针 char* 整型指针 int* 数组指针 int (*)[10] 指向数组的指针 函数指针 ??? 指向函数的指针
1.函数有地址吗?
如果你觉得已经认识函数指针了, 请试着补充下面的函数指针吧 char* test(int x, float* y) { } int main() { pt = test; return 0; } //char* (*pt)(int, float*) = test; void** test(int** x, char y, double* z) { } int main() { pt = test; return 0; } //void** (*pt)(int**, char, double*) = test;
2.复杂的函数指针
接下来我们可以试试挑战难度更大的 (*(void(*)())0)(); //上述代码是一次函数调用 //其中 void(*)() 是函数指针类型 //放到括号里面表示强制类型转换 //把 0 强制类型转换为 函数指针类型 //然后解引用,调用 0地址处的这个函数 //';'前的一对括号用于传参,此函数的参数类型为空void void ( *signal(int, void(*)(int)) )(int); //上述代码是一次函数声明 //一个函数,除去它的名字与参数,剩下的就是它的返回类型 //函数与参数 signal(int, void(*)(int)) //返回类型是一个函数指针 void (*)(int); //signal函数有两个参数,一个是 int 类型的,另一个是 void (*)(int) 类型的函数指针 //signal函数的返回值也是一个函数指针
3.运用函数指针数组自定义一个计算器
指针数组 -- 每个元素的类型为 指针类型 函数指针数组 -- 每个元素的类型为 函数指针类型
这次我们不使用switch选择语句,用今天学习的内容 #include <stdio.h> int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; } void menu() { printf("******************\n"); printf("*** 简易计算器 ***\n"); printf("**** 0.exit ****\n"); printf("***** 1.Add *****\n"); printf("***** 2.Sub *****\n"); printf("***** 3.Mul *****\n"); printf("***** 4.Div *****\n"); printf("******************\n"); } //函数指针数组实现计算器 int main() { int input = 0; int x = 0, y = 0; int ret = 0; int (*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div }; do { menu(); printf("请选择需要进行的操作:>"); scanf("%d", &input); if (0 == input) { printf("退出程序\n"); } else if (input >= 1 && input <= 4) { printf("请选择两个操作数:>"); scanf("%d %d", &x, &y); ret = pfArr[input](x, y); printf("%d\n", ret); } else { printf("非法输入!\n"); } } while (input); return 0; }
运行截图
四、指向函数指针数组的指针
指向指向函数指针数组的指针是一个指针,指向一个数组,数组的每个元素都是函数指针
int my_strlen(const char* str) { ... ... return 0; } int main() { //指针数组 char* arr1[4] = { 0 }; //数组指针 char arr2[] = "abcd"; char(*p)[5] = &arr2; //函数指针 int (*pf)(const char*) = my_strlen; //函数指针数组 int (*pfArr[10])(const char*); return 0; }
演示代码:
int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int main() { int (*pf)(int, int) = Add; //函数指针数组 int (*pfArr[4])(int, int) = { Add, Sub }; //指向函数指针数组的指针 int (*(*ppfArr)[4])(int, int) = &pfArr; return 0; }
五、回调函数
1.概念理解
百度百科:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
2.应用举例
我们依然使用上文的计算器代码作为示例
1)如果使用switch语句,我们可以发现存在很多重复的代码
2)在这里我们可以巧妙地运用回调函数解决代码冗余的问题
#include <stdio.h> int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; } void menu() { printf("******************\n"); printf("*** 简易计算器 ***\n"); printf("**** 0.exit ****\n"); printf("***** 1.Add *****\n"); printf("***** 2.Sub *****\n"); printf("***** 3.Mul *****\n"); printf("***** 4.Div *****\n"); printf("******************\n"); } void Calc(int (*pf)(int, int)) { int x = 0, y = 0; int ret = 0; printf("请选择两个操作数:>"); scanf("%d %d", &x, &y); ret = pf(x, y); printf("%d\n", ret); } int main() { int input = 0; do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: Calc(Add); break; case 2: Calc(Sub); break; case 3: Calc(Mul); break; case 4: Calc(Div); break; case 0: printf("退出计算器\n"); break; default: printf("非法输入\n"); break; } } while (input); return 0; }
3.快速排序qsort
qsort是一个库函数,可以排序任意类型的数据,头文件是 <stdlib.h> 这里我们不关心qsort是如何实现排序的,只需要学会如何使用就够了 //void qsort(void* base,//指向待排序数组的第一个元素 // size_t num, //待排序的元素个数 // size_t size, //每个元素的大小(字节) // //指向一个函数,这个函数可以比较两个数据的大小 // int (*compare)(const void*, const void*) //); //注意: void* 类型的指针不能进行解引用操作,也不能进行加一减一操作 //void* -- 无具体类型的指针,可以接收任何类型的地址 #include <stdio.h> #include <stdlib.h> #include <string.h> //qsort使用者需要自己实现该函数 int cmp_int(const void* buf1, const void* buf2) { return *(int*)buf1 - *(int*)buf2; } //测试qsort排序整型数组 void test1() { int arr[10] = { 9,8,7,6,5,4,3,2,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); //用qsort排序一个整型数组,我们需要创建一个函数,能够比较两个整型的大小 qsort(arr, sz, sizeof(arr[0]), cmp_int); for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } //测试qsort排序结构体类型的数据 struct Hum { char name[20]; int age; }; int cmp_H_age(const void* buf1, const void* buf2) { return ((struct Hum*)buf1)->age - ((struct Hum*)buf2)->age; } int cmp_H_name(const void* buf1, const void* buf2) { return strcmp(((struct Hum*)buf1)->name, ((struct Hum*)buf2)->name); } void print_H(struct Hum* h, int sz) { for (int i = 0; i < sz; i++) { printf("%d, %s\n", (h + i)->age, (h+i)->name); } } void test2() { struct Hum h[] = { {"zhangshan", 17},{"lisi", 19},{"wangwu", 38} }; int sz = sizeof(h) / sizeof(h[0]); qsort(h, sz, sizeof(h[0]), cmp_H_age); print_H(h, sz); qsort(h, sz, sizeof(h[0]), cmp_H_name); print_H(h, sz); } int main() { test1(); test2(); return 0; }
运行截图
六、数组与指针的例题分析
1.整型数组和指针
数组 -- 能够存放一组相同类型的元素,数组的大小取决于它的元素个数和元素类型 指针 -- 泛指地址,也可以是指针变量,大小是4/8个字节 数组名(arr) -- 通常情况下表示数组首元素的地址,但是有两个特殊情况表示整个数组 1)sizeof(arr) ,计算的是整个数组的大小 2)&arr ,取出的是整个数组的地址 注意:arr、&arr[0]、&arr 数值上相同,但是表示的意义不同 分析下面代码的运行结果 int main() { int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a)); printf("%d\n", sizeof(a + 0)); printf("%d\n", sizeof(a + 1)); printf("%d\n", sizeof(a[1])); printf("%d\n", sizeof(*a)); printf("%d\n", sizeof(&a)); printf("%d\n", sizeof(*&a)); printf("%d\n", sizeof(&a + 1)); printf("%d\n", sizeof(&a[0])); printf("%d\n", sizeof(&a[0] + 1)); return 0; }
2.字符数组与指针
1)字符数组
strlen() 与 sizeof()
strlen() 是库函数,头文件是 <string.h> ,strlen函数只针对字符串, 求的是字符串的长度,本质上统计的是\0之前出现的字符个数 sizeof() 是操作符,计算的是占用内存空间的大小,单位是字节, 不关注内存中存放的内容
分析下面代码的运行结果
#include <stdio.h> #include <string.h> int main() { char arr[] = { 'a','b','c','d','e','f' }; printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(*arr)); printf("%d\n", sizeof(arr + 0)); printf("%d\n", sizeof(arr[1])); printf("%d\n", sizeof(&arr)); printf("%d\n", sizeof(&arr + 1)); printf("%d\n", sizeof(&arr[0] + 1)); printf("%d\n", strlen(arr)); printf("%d\n", strlen(*arr)); printf("%d\n", strlen(arr + 0)); printf("%d\n", strlen(arr[1])); printf("%d\n", strlen(&arr)); printf("%d\n", strlen(&arr + 1)); printf("%d\n", strlen(&arr[0] + 1)); return 0; } 思考:如果把 arr 改为 "abcdef" 呢?
分析结果 #include <stdio.h> #include <string.h> int main() { char arr[] = { 'a','b','c','d','e','f' }; //arr表示整个数组,1*6个字节 printf("%d\n", sizeof(arr));//6 //arr表示首元素地址,*arr表示第一个元素 printf("%d\n", sizeof(*arr));//1 //arr+0 表示首元素的地址 printf("%d\n", sizeof(arr + 0));// 4/8 //数组下标为1的元素 printf("%d\n", sizeof(arr[1]));//1 //&arr整个数组的地址 printf("%d\n", sizeof(&arr));// 4/8 //跳过整个数组,指向数组后面一个位置 printf("%d\n", sizeof(&arr + 1));// 4/8 //指向第二个元素的地址 printf("%d\n", sizeof(&arr[0] + 1));// 4/8 //strlen统计\0前面字符类型的元素的个数 //自动在内存中寻找\0,我们无法知道什么时候在哪里找到\0,所以是随机值 //arr表示整个数组,strlen遍历整个数组都没有找到\0,自动向后寻找 printf("%d\n", strlen(arr));//随机值 //分析同上 printf("%d\n", strlen(arr + 0));//随机值 //strlen函数里面的数据应该为开始统计的地址 printf("%d\n", strlen(*arr));//非法访问 //'b' - 98 ,访问地址为98处的内容 printf("%d\n", strlen(arr[1]));//非法访问 //arr表示整个数组的地址,这个随机值R与上面代码的随机值不同 printf("%d\n", strlen(&arr));//随机值R //跳过了整个数组(大小为6个字节),虽然还是随机值,但是比随机值R小6个字节 printf("%d\n", strlen(&arr + 1));//随机值R-6 //跳过了一个字节,虽然还是随机值,但是比随机值R小1个字节 printf("%d\n", strlen(&arr[0] + 1));//随机值R-1 return 0; } ---------- ---------- ---------- ---------- int main() { char arr[] = "abcdef"; printf("%d\n", sizeof(arr));// 7 printf("%d\n", sizeof(*arr));// 1 printf("%d\n", sizeof(arr + 0));// 4/8 printf("%d\n", sizeof(arr[1]));// 1 printf("%d\n", sizeof(&arr));// 4/8 printf("%d\n", sizeof(&arr + 1));// 4/8 printf("%d\n", sizeof(&arr[0] + 1));// 4/8 printf("%d\n", strlen(arr));// 6 printf("%d\n", strlen(*arr));// error printf("%d\n", strlen(arr + 0));// 6 printf("%d\n", strlen(arr[1]));// error printf("%d\n", strlen(&arr));// 6 printf("%d\n", strlen(&arr + 1));// 随机值 printf("%d\n", strlen(&arr[0] + 1));// 5 从第二个元素的地址开始访问 return 0; }
2)字符指针
分析下面代码的运行结果
#include <stdio.h> #include <string.h> int main() { const char* p = "abcdef"; printf("%d\n", sizeof(p)); printf("%d\n", sizeof(p + 1)); printf("%d\n", sizeof(*p)); printf("%d\n", sizeof(p[0])); printf("%d\n", sizeof(&p)); printf("%d\n", sizeof(&p + 1)); printf("%d\n", sizeof(&p[0] + 1)); printf("%d\n", strlen(p)); printf("%d\n", strlen(p + 1)); printf("%d\n", strlen(*p)); printf("%d\n", strlen(p[0])); printf("%d\n", strlen(&p)); printf("%d\n", strlen(&p + 1)); printf("%d\n", strlen(&p[0] + 1)); return 0; }
分析结果 int main() { const char* p = "abcdef"; printf("%d\n", sizeof(p));// 4/8 printf("%d\n", sizeof(p + 1));//'b'的地址 4/8 printf("%d\n", sizeof(*p));//1 printf("%d\n", sizeof(p[0]));//'a' 1 printf("%d\n", sizeof(&p));// 4/8 printf("%d\n", sizeof(&p + 1));// 4/8 printf("%d\n", sizeof(&p[0] + 1));//'b'的地址 4/8 printf("%d\n", strlen(p));//6 printf("%d\n", strlen(p + 1));//p+1是'b'的地址 5 printf("%d\n", strlen(*p));//error printf("%d\n", strlen(p[0]));//error printf("%d\n", strlen(&p));//随机值 printf("%d\n", strlen(&p + 1));//随机值 printf("%d\n", strlen(&p[0] + 1));//&p[0]+1是'b'的地址 5 return 0; }
3.二维数组
分析下面代码的运行结果
#include <stdio.h> #include <string.h> int main() { int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 }; printf("%d\n", sizeof(a)); printf("%d\n", sizeof(a[0][0])); printf("%d\n", sizeof(a[0])); printf("%d\n", sizeof(a[0] + 1)); printf("%d\n", sizeof(*(a[0] + 1))); printf("%d\n", sizeof(a + 1)); printf("%d\n", sizeof(*(a + 1))); printf("%d\n", sizeof(&a[0] + 1)); printf("%d\n", sizeof(*(&a[0] + 1))); printf("%d\n", sizeof(*a)); printf("%d\n", sizeof(a[3])); return 0; }
分析结果 #include <stdio.h> #include <string.h> int main() { int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 }; //计算整个二维数组的大小,4*12 printf("%d\n", sizeof(a));//48 //计算第一行第一个元素的大小 printf("%d\n", sizeof(a[0][0]));//4 //我们可以把二维数组看作是一维数组的数组 //计算二维数组中第一个数组的大小,4*4 printf("%d\n", sizeof(a[0]));//16 //计算a[0][1]的地址的大小 printf("%d\n", sizeof(a[0] + 1));//8 //计算a[0][1]的大小 printf("%d\n", sizeof(*(a[0] + 1)));//4 //跳过二维数组中的第一个一维数组,指向第二个一维数组 printf("%d\n", sizeof(a + 1));//8 //计算二维数组中第二个数组的大小,4*4 printf("%d\n", sizeof(*(a + 1)));//16 //&a[0]是第一行的地址,&a[0] + 1是第二行的地址 printf("%d\n", sizeof(&a[0] + 1));//8 //计算二维数组中第二个数组的大小,4*4 printf("%d\n", sizeof(*(&a[0] + 1)));//16 //a就是二维数组中第一个数组的地址 printf("%d\n", sizeof(*a));//16 //注意,这里并没有越界 //a[3]表示维数组中的第四个一维数组的地址 //如果存在第四个一维数组,那么它的大小应当与前面三个一维数组的大小相等 printf("%d\n", sizeof(a[3]));//16 return 0; }
总结
要想学好C语言,就一定要把指针学好!!!
希望本文能够对你有所帮助,也欢迎广大读者批评指正<^w^>