> 文章列表 > C语言深入知识——(2)指针的深入理解

C语言深入知识——(2)指针的深入理解

C语言深入知识——(2)指针的深入理解

1、字符指针

(1)字符指针的普通用法

char a = 'A';
char* pa = &a;

但是一般来说字符指针很少这么用……更多是拿来存储一个字符串

(2)字符串的两种存储以及区别

  • 现在有了两种存储数组的方法
    • ①一个是使用char类型数组存储
    • ②另外一个是使用指向char类型的指针存储数组的首元素地址,将其当成字符串的“标记”
#include <stdio.h>
int main()
{char str1[] = "hello word.";char str2[] = "hello word.";const char *str3 = "hello word.";//将字符串的首字母h的地址存储在str3里面const char *str4 = "hello word.";if(str1 == str2)printf("str1 and str2 are same\\n");elseprintf("str1 and str2 are not same\\n");if(str3 == str4)printf("str3 and str4 are same\\n");elseprintf("str3 and str4 are not same\\n");return 0;
}
  • 两个数组存储不同的字符串,尽管他们的内容是相同的
  • 两个字符指针都指向内容相同的字符串

这意味着str3和str4指向的是同一个常量字符串,C/C++会把常量字符串存储到一个单独的内存区域,当几个指针指向同一串字符串时,他们实际是指向同一块内存。但是用相同的常量字符串去初始化不同数组时,数组会对常量字符串进行拷贝,开辟出不同的内存块,所以str1!=str2,但是str3==str4

(3)存储多个字符串----字符指针数组

int main()
{char* arr[3] = { "abcd", "cdf", "jiuh" };int i = 0;for(i = 0; i < 3; i++){printf("%s\\n", arr[i]);}return 0;
}

2、指针数组

比较简单,直接写下code就可以

char* arr[4];//存储了4个char*指针
char** arr[10];//存储了10个char**指针

3、数组指针

(1)数组的基础概念

  • 数组名字有两种情况如下code
//复习数组的名字含义
#include <stdio.h>
int main()
{int arr[10] = { 0 };printf("%p\\n", arr);//数组名字就是首元素地址printf("%p\\n", &arr[0]);//取出了首元素地址printf("%p\\n, &arr);//1、取出来整个数组的地址,尽管&arr和&arr[0]的值一样,但是意义不一样(类型不一样),前者是数组类型int[10],后者是整型类型intprintf("%zd\\n", sizeof(arr));//2、这里的arr依旧是整个数组return 0;
}

①sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
②&数组名,这里取出来的是整个数组地址,尽管它的值和数组的首元素地址相同,两者是有区别的

最大的区别就在于int arr[10]声明后,arr是首元素地址,指针类型是int*,这是个数组指针。而能存放&arr这个地址的指针类型是int(*)[10]。这点在对两种指针进行+/-整数的时候会更加明显,因为指针会根据指向的类型对地址值进行增加 (数组指针后面会讲)

(2)数组指针的定义

数组指针也是指针

int (*p)[10];//p是一个指向一维整型数组的指针,该数组内含10个int类型

注意[]和()具有相同的优先级,结合性是从左向右结合。并且优先级都比*高。

(3)数组指针的使用

//第一种使用方法
int main()
{int arr[10] = {1, 2, 3, 4, 5};int (*p)[10] = &arr;return 0;
}语法逻辑没毛病,可以,但是一般不会这么使用
//第二种使用方法
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{int i = 0;for(i=0; i<row; i++){for(j=0; j<col; j++){printf("%d ", arr[i][j]);}printf("\\n");}
}void print_arr2(int (*arr)[5], int row, int col)
{int i = 0;for(i=0; i<row; i++){for(j=0; j<col; j++){printf("%d ", arr[i][j]);}printf("\\n");}
}int main()
{int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};//数组名arr,表示首元素的地址//但是二维数组的首元素是二维数组的第一行,指向这一行的指针类型可以写成int [][5]或者int (*)[5]//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址print_arr1(arr, 3, 5);print_arr2(arr, 3, 5);return 0;
}

(4)更多数组指针的解读

int arr[5];//单纯是个整型数组int* parr1[10];//单纯是一个指针数组,每一个指针都指向一个intint (*parr2)[10];//数组指针,该指针指向一个包含10个int元素的数组int (*parr3[10])[5];//指针数组,可以思考成“int(*)[5] parr3[10]”(与int arr[5]是类似的)很明显,这是一个包含10个元素的数组,每个元素都int(*)[5]这种类型的指针,这种指针指向一包含5个元素的数组

4、数组传参和指针传参

(1)一维数组传参

#include <stdio.h>
void test1(int arr[])//可以这么写
{//code
}
void test1(int arr[10])//10写与不写都行,无所谓,C会将它忽略
{//code
}
void test1(int *arr)//可以的,传过来的arr1的拷贝是一个int元素地址
{//code
}void test2(int *arr[20])//20写与不写都行,C依旧会将它忽略
{//code
}
void test2(int **arr)//也可以,传过来的arr2的拷贝是一个int*元素的地址
{//code
}int main()
{int arr[10] = {0};int *arr2[20] = {0};test(arr1);test2(arr2);
}

(2)二维数组传参

void test(int arr[3][5])//可以使用,不过3会被忽略
{//code
}
void test(int arr[][])//不可以使用,5必须留下来
{//code
}
void test(int arr[][5])//可以使用
{//code
}
//二维数组传参,函数形参的设计只能省略第一个[]的数字。
//对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。void test(int *arr)//不行,类型不匹配,拷贝过来的参数是一个指向一维数组的指针,而这个参数仅仅是一个一维指针
{//code
}
void test(int* arr[5])//不行,这是一个指针数组,根本没有关系 
{//code
}
void test(int (*arr)[5])//可以这么写,指针类型匹配了
{//code
}
void test(int **arr)//不行,指针类型不匹配
{//code
}int main()
{int arr[3][5] = {0};test(arr);
}
//使用二维数组指针(深刻理解)
void print(int(*p)[20], int x, int y)
{//二维数组的名字就是首元素的地址,其地址就是第一行数组的地址,也就是指向一维数组类型的指针,因此不能写形参为int**//又因为一维数组的整体地址&arr和其首元素地址&arr[0]起始位置相同,故从值来看是一样的,但是两者的指针类型完全不同for (int i = 0; i < x; i++){for (int j = 0; j < y; j++){printf("%d", *(* (p + i) + j));//得到数组名,利用i得到每一行的数组名,利用j得到某一行的每一列的元素地址}}
}
int main()
{int arr[10][20] = { {1, 2, 3}, {2, 3, 4} };print(arr, 10, 20);return 0;
}

(3)一级指针传参

#include <stdio.h>
void print(int *p, int sz)
{int i = 0;for(i=0; i<sz; i++){printf("%d\\n", *(p+i));}
}
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9};int *p = arr;int sz = sizeof(arr)/sizeof(arr[0]);//一级指针p,传给函数print(p, sz);return 0;
}

(4)二级指针传参

#include <stdio.h>
void test(int** ptr)
{printf("num = %d\\n", **ptr);
}
int main()
{int n = 10;int*p = &n;int **pp = &p;test(pp);test(&p);return 0;
}

(5)总结

指针的类型一定要匹配好,尽管类型不匹配的时候依旧可以进行传参(因为所有地址在同一个平台都是一样大小的,只是存储一个地址,理论上用什么类型的指针都可以存储),但是在后续使用指针的时候(解引用指针)就会发生错误,指针会根据指针类型访问不同大小的内存

5. 函数指针

  • 整型指针int*
  • 字符指针char*
  • 指向数组int arr[10]的数组指针int (*p)[10]
  • 指向函数function()的函数指针int (*pf)(int, int) = function(或者int (*pf)(int, int) = &function)其中函数function()是一个有两个int参数,返回值为int的函数

(1)函数指针

  • 对于函数function(),其函数指针类型为【返回值 (*指针名) (参数类型的列表)】

  • 若想使用这个函数就要进行解引用,使用【(* pf)(参数列表)】或者【pf(参数列表)】都可以。编译器在处理的时候,没有 * 也行,但是要用 * 就一定要加括号

    • 尽管这样很矛盾,但是调用函数指针时,使用*pf和pf是一样的,即:函数名==函数的地址,而(&函数名)==函数的地址
    • 若是理解pf为指针,则*pf变成函数名字,*pf()就相当于function()
    • 若是理解pf为函数名,则pf本身就是函数的名字,pf()就相当于function()
    • C允许这两种写法,认为两种都合理
//例子
char* test(int c, float* pf)
{//某些代码
}
int main()
{char* (*pt)(int, float*)pf = test;test(参数列表)return 0;
}

(2)有关函数指针的一些有趣的代码

//代码1
(*(  void (*)()  )0)();//从最里面开始理解void(*)()是一个指向“返回值为空,参数列表为空”函数的函数指针
//然后将0的int类型强制转化为函数指针类型,于是0成了一个函数指针类型的地址
//再解引用0这个地址,得到0地址处的函数,然后使用这个函数
//代码2
void (*  signal(int , void(*)(int))  )(int);//等价代码如下
//typedef void(*pfun_t)(int);//注意pfun_t是一个和void(*)(int)同类型名,将pfun_t放在*旁边是为了指明pfun_t是一个指针而已,这只是语法形式要求
//pfun_t signal(int, pfun_t);//因此有一个简化代码的技巧就是使用typedef

这两段代码来自于《C陷阱与缺陷》,是本出名的C语言书籍

6. 函数指针数组

(1)使用例子

#include <stdio.h>
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 div(int a, int b)
{return a / b;
}
int main()
{int x, y;int input = 1;int ret = 0;do{printf("*************************\\n");printf(" 1:add             2:sub \\n");printf(" 3:mul             4:div \\n");printf("*************************\\n");printf("请选择:");scanf("%d", &input);switch (input){case 1:printf("输入操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\\n", ret);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\\n", ret);break;case 3:printf("输入操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\\n", ret);break;case 4:printf("输入操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\\n", ret);break;case 0:printf("退出程序\\n");breark;default:printf("选择错误\\n");break;}} while (input);return 0
}

(2)改良后

#include <stdio.h>
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 div(int a, int b)
{return a / b;
}
int main()
{int x, y;int input = 1;int ret = 0;int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表,即函数指针数组,这里的0(或者写NULL)起到占位的作用,理论上放什么都行,只要后续处理好就行…while (input){printf("*************************\\n");printf(" 1:add           2:sub \\n");printf(" 3:mul           4:div \\n");printf("*************************\\n");printf("请选择:");scanf("%d", &input);if ((input <= 4) && (input >= 1)){printf("输入操作数:");scanf("%d %d", &x, &y);ret = (*p[input])(x, y);}else{printf("输入有误\\n");}printf("ret = %d\\n", ret);}return 0;
}
//当计算器后续需要加入更多的运算函数时,那么使用开关语句就会显得冗长,但是使用函数指针数组就不会有这个问题,这将会大大缩减代码。但是使用函数指针也具有有缺点,它只能存放同样函数签名(函数签名定义了函数的输入和输出,即:函数签名==参数+返回值)的函数

(3)转移表的概念

像类似上面使用函数指针数组的方法就叫做转移表,具有一种跳转使用函数的效果。即“函数指针数组”==“转移表”

7. 指向函数指针数组的指针

void test(const char* str)
{printf("%s\\n", str);
}
int main()
{//声明函数指针pfun,并且进行初始化void (*pfun)(const char*) = test;//声明一个函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str) = { pfun };pfunArr[0] = test;//指向函数指针数组pfunArr的指针ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}
//*代表ppfunArr是指针,[]代表这个指针指向一个内含5元素的数组,而每个元素的类型都是void (*)(const char*)
//因此按照运算符的顺序来解读是比较快的    

8. 回调函数

  • 回调函数就是一个通过函数指针调用的函数。
  • 如果你把函数的指针(地址)作为参数传递给另外一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
  • 回调函数不是由该函数的实现方式直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应

(1)例子一:使用qsort

①前要:C库函数qsort能对数组进行排序

void qsort(void *base,//指向了待排序数组的第一个元素 size_t nitems,//排序的元素个数size_t size,//每个元素的大小,单位是字节int (*compar)(const void*, const void*)//指向一个函数,这个函数可以比较两个元素的大小);//其底层是使用快速排序的方法来排序的,依靠compar指向的不同函数内部不同的比较,可以解决不同类型的数据快速排序

②使用qsort:对数组进行排序

#include <stdio.h>
#include <stdlib.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{return ( *(int*)p1 - *(int*)p2 );//注意不能直接解引用void指针,另外如果倒过来就是逆序输出了
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };int i = 0;qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++){printf( "%d ", arr[i]);}printf("\\n");return 0;
}

(2)例子二:模拟实现类似qsort函数的bubble函数(底层采用冒泡函数)

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)//其中一个比较函数,这个是整型比较,是用户决定这个函数应该如何编写
{return (*(int*)p1 - *(int*)p2);//不过注意void不能直接解引用!!!
}void _swap(void *p1, void * p2, int size)//其中一个排序方法,这个是排序是冒泡排序,可以由开发者决定底层排序的方法,在qsort中使用的底层函数是快排
{int i = 0;for (i = 0; i< size; i++)//之所以这么做,是因为没有办法预测有多少个字节,只能通过一个一个字节进行交换,最后所有字节进行交换,即两个数据进行了交换{char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}void bubble(void *base, int count, int size, int(*cmp )(void*, void*))//相当于qsort,但是实现逻辑不仅仅是快速排序,内部的_swap函数也可能是其他的排序算法,cmp函数可能比较不同类型的数据。     注意base是void*类型,写int*会写死的,只能限定于整型
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){int flag = 1;//①优化代码,若是没有交换就说明不需要经过排序就是有序的了for (j = 0; j < count - i - 1; j++){if (cmp ((char*) base + j * size, (char*)base + (j + 1) * size) > 0)//比较函数,这里改成(char*)就可以利用size适应不同的字节{flag = 0;_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);//排序函数,这里改成(char*)就可以利用size适应不同的字节}if(flag == 1){break;}}}
}int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++){printf( "%d ", arr[i]);}printf("\\n");return 0;
}

注意我们不是在模拟qsort函数,而是作一个类似qsort函数的“冒泡排序通用的bubble函数”