关于指针,以前提到过:
1,指针就是一个变量,一个存储地址的变量,而地址标识了一块唯一的内存空间
2,指针的类型决定了指针加减整数的时候跳过的字节数,和指针解引用的时候能够访问的字节数
3,指针的大小,在三十二位机下,有三十二根地址线,所以需要三十二位bit来表示地址,因此指针的大小就是4字节,在64位机下就是8字节。
4,指针的运算,指针-指针得到的是指针之间的元素个数。
下面是指针进阶,主要有以下几个重点
1,字符指针
字符指针顾名思义就是指向字符的指针
一般情况下都是用来保存字符变量的地址,如下:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
但是字符指针还有一种用法就是用来指向一个字符串
int main()
{
char* pa = "hello";
printf("%s", pa);
printf("%c", *pa);//打印出来的是h字符
return 0;
}
这里要注意的是,pa内存放的并不是hello这个字符串而是首字符h的地址,这里的”hello“字符串是常量字符串,存放在内存中的常量区(又叫只读数据区),因此是不可以修改的,如果进行修改程序就会挂掉。
所以一般为了安全,可以在char* 前面加上const修饰。
下面这段代码说明了上述字符串是存放在常量区的。
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
这里可以看到字符指针指向的字符串是常量字符串是存放在常量区的,因此str3和str4相等,str1和str2是将常量字符拷贝了一份并在栈区上开辟了一块内存将拷贝字符串存放在栈区上,因此str2和str1是不相同的,他们都是常量字符的拷贝。
2,指针数组
指针数组的概念很简单,就是存放指针的数组
int main()
{
int* pa[5];
int** ppa[5];
int*** pppa[5];
}
这里的pa,ppa,pppa是数组名因为[ ]的优先级高于所以先和[ ]结合就表示pa是数组,int表示数组的每个元素都是int*类型的。
3,数组指针
这里要和指针数组区分,数组指针,是指针,看后两个字就是说明了他是指针,是一个指向数组的指针,就和字符指针是指向字符的指针,整形指针是指向整形的指针是一样的。
int main()
{
int arr[5] = { 1,2,3,4,5 };
int(*pa)[5] = &arr;
for (int i = 0; i < 5; i++)
{
printf("%d\n",(*pa)[i]);
}
return 0;
}
因为的优先级低于[ ]所以要用括号保护起来,使得数组名先和结合就是指针,数组指针的类型是int(*)[5]将数组名去掉剩下的就是类型。要注意这里要用&arr给pa初始化,因为&数组名取出的是整个数组的地址,当然在数值上是和首元素地址相同的但是类型不同,如果不加&也不会影响下面的使用的,但是会报一个类型的警告
在使用的时候要先对pa进行解引用,因为pa内存放的是数组的地址,解引用就拿到了这个数组,也就是数组名,然后就可以使用访问数组的方式来访问元素了。但是数组指针很少这样使用,一般二维数组使用的数组指针更加适合。
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col) {
int j = 0;
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 j = 0;
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 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
// 类型是int(*)[5]
//所以可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
数组名和&数组名的区别
首先含义上,数组名大部分情况都是代表首元素的地址,如果数组名单独放在sizeof内部,那此时数组名就是代表整个数组,&数组名这里的数组名也是代表整个数组取出的是整个数组的地址。
这里可以看出来,数组名和&数组名的地址是一样的,但是他们+1的地址就不一样了,因为他们的类型不同,&数组名+1直接跳过了一个int [5]的字节大小也即是20字节,arr的类型是int* 所以+1跳过了4字节。这里的是十六进制的14,换算成十进制就是20.
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
第一个是普通的数组
第二个是一个指针数组,因为数组名先和后面的[ ]结合。
第三个是数组指针,数组名先和结合构成指针
第四个是存放数组指针的数组,数组名先和[ ]结合构成数组,数组的每个元素都是int()[5],也就是数组指针
相当于int arr[10][5];
4,数组参数、指针参数
一维数组传参
一维数组传参可以使用
1,用数组接受void put(int arr[])
这里的方框内写不写数字都可以,写的和实参的个数不同也不会报错,但是不建议这样写
2,用指针接收void put(int* arr)
如果是指针数组,那么可以用int* arr[]或者int** arr二级指针来接受。
二维数组传参
二维数组的传参,不可以用二级指针接收,因为二级指针是用来存放一级指针的地址的。
1,使用二维数组来接收,int arr[ ][5],这里行可以省略但是列不可以省略。
2,用数组指针来接受,因为传参的时候传过去的是二维数组的数组名,数组名表示首元素的地址,二维数组的首元素是第一行的这个数组,因此传过去的是一个一维数组的地址,所以用数组指针来接受。
二维数组为什么不可以用二级指针接收:
首先在leetcode中我们看到这里面的二维数组都是用二级指针来接受,为什么呢?这是因为,如果你的二维数组是malloc出来的,那么就可以用二级指针来接受,实际二级指针指向的是一个数组指针的首元素的地址,指针的地址当然用二级指针来保存,这个数组指针的每个元素都指向一块空间。代码如下:
int** pa = (int**)malloc(sizeof(int*) * 2);
for (int i = 0; i < 2; i++)
{
pa[i] = (int*)malloc(sizeof(int) * 2);
}
这里实际上是手动模拟了一个二维数组。
但是实际上我们直接定义出来的二维数组(int arr[2][2])不可以用二级指针来接受,因为二维数组的数组名代表首元素的地址,类型是int(*)[2],但是将第一行的这个数组的地址传给二级指针会发生类型不匹配,如果编译通过,就是将这个地址强行转换成了二级指针,但是这里后面访问的时候也会出错,假设arr[0][0]=1,用二级指针的时候因为将数组指针这个地址强行转换成了二级指针,失去了类型限制,这个地址实际上数值就是arr[0][0]的地址,第一次解引用拿到了arr[0][0]也就是1,第二次对1进行解引用,就爆出了访问错误。
指针传参
一级指针
一级指针作为参数可以接收一个变量的地址,可以接受一个一维数组,可以接受一个一级指针变量
二级指针
二级指针做参数,可以接受一个一级指针变量的地址,可以接受一个二级指针,可以接受一个指针数组(首元素地址就是一级指针的地址)
函数指针
函数也有地址吗?答案是有的,函数名就表示函数的地址。
那这样我们就可以用一个指针来保存函数的地址,这就是函数指针 。函数名就是代表函数的地址,加不加取地址符&都可以的。
void print(int (*arr)[2])
{
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 2; j++)
printf("%d ", arr[i][j]);
}
}
int main()
{
void (*pp)(int(*)[2]) = print;
return 0;
}
这就是函数指针的使用,函数指针变量名pp先和*结合表示是个指针,然后前面的void表示返回值是空,后面的括号里面写的是参数类型。
至于函数指针有什么用,后面会提到的。
两段神奇的代码
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
这两段代码出自,《C陷阱和缺陷》,
第一段代码,先将0强转成函数指针(该函数返回值为空,无参)然后对0地址处的函数指针进行解引用,不传参。所以这是一段函数调用。
第二段代码,signal是名,先和后面的括号结合说明这个signal是函数名,函数的参数是(int,void (*)(int)),第一个参数是int整形,第二个参数是函数指针(返回值为空,参数为int),这个signal的返回值也是一个函数指针(该函数返回值是void参数是int)所以这段代码是一段函数声明。
对于第二段代码我们可以进行相应的简化。
typedef void (*pfun_t)(int);//对返回值为空,参数类型为int的函数指针进行冲命名,
//这里重命名为pfun_t只能写在括号里面,否则就会报错。
第二段代码简化如下:
pfun_t signal(int,pfun_t);
函数指针数组
学完了函数指针,下面来看一下函数指针数组。
顾名思义,就是存放函数指针的数组。
形式如下:
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 main()
{
int (*parr[3])(int, int) = { add,sub,mul };
return 0;
}
通过函数指针数组我们可以直接调用对应的函数。
这里函数指针调用函数的方式有两种:
1,(*padd)(2,3);
2,padd(2,3);
此两种可以完成函数调用,如果加*,就需要括起来,防止padd先和后面的括号结合。
函数指针数组可以用在转移表,来简化代码
简单计算器实现:
#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 Print()
{
printf("****1,add 2,sub ****\n");
printf("****3,mul 4,div ****\n");
}
int main()
{
int (*parr[5])(int, int) = { 0,add,sub,mul,div };
int n;
do
{
Print();
scanf("%d", &n);
int x, y;
if (n >= 1 && n <= 4)
{
scanf("%d %d", &x, &y);
printf("%d\n", parr[n](x, y));
}
else if(n!=0)
printf("input is error\n");
} while (n);
return 0;
}
这里使用了函数指针数组就剩去使用switch时候的大量代码冗余
指向函数指针数组的指针
科学的本质在于不断套娃
int (*parr[5])(int, int) = { 0,add,sub,mul,div };
int (*(*pparr)[5])(int, int) = &parr;
这里pparr先和*结合为指针,方框5表示这是个数组指针,除去这些可以看出来该数组指针指向的数组的每个元素都是函数指针,这里给指向函数指针数组的指针初始化用的是&parr用的是数组的地址,函数指针数组的地址。
回调函数
回调函数就是将一个函数指针作为参数传递给另一个函数,在该函数内满足某些条件的时候进行调用,回调函数不是用函数的实现方直接调用而是在特定条件下由另一方调用。
关于回调函数为了加深理解,我们来手动实现一下。
首先说一下void*,空类型指针的作用,空类型指针不可以直接解引用,在使用之前必须进行强制类型转换。空类型的指针可以存放任意类型的变量的地址。
#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* str1, const void* str2)
{
return *(int*)str1 - *(int*)str2;
}
void my_swap(void* str1, void* str2,int byt)
{
for (int i = 0; i < byt; i++)
{
char tmp = *((char*)str1 + i);
*((char*)str1 + i) = *((char*)str2 + i);
*((char*)str2 + i) = tmp;
}
}
void my_bubble_sort(void* arr, int size, int byt, int(*cmp)(void*,void*))
{
int i = 0, j = 0;
for (i = 0; i < size-1; i++)
{
for (j = 0; j < size - 1 - i; j++)
{
if (cmp_int((char*)arr + j * byt, (char*)arr + (j + 1) * byt)>0)
{
my_swap((char*)arr + j * byt, (char*)arr + (j + 1) * byt, byt);
}
}
}
}
int main()
{
int arr[] = { 1,23,3,4,5,3,2,2,2,2,2,23,4,45,5,56,56 };
int size = sizeof(arr) / sizeof(arr[0]);
my_bubble_sort(arr, size, sizeof(int), cmp_int);
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
上述代码,我们模拟实现了一个泛型的排序,可以排序任何类型的数据内部使用的是冒泡排序,只需要给出对应的排序函数即可。这里 就用到了回调函数。和void*类型的指针