指针的基本概念:
1.用来存放地址的变量,地址唯一标识一块地址;
2.指针固定4/8个字节;
3.指针的类型决定指针加减整数的步长和解引用的权限;
4.指针相减返回元素个数。
一、字符指针
先看一串代码
#include<stdio.h>
int main()
{
const char* p = "abcdefg";
printf("%s\n", p);
return 0;
}
通过p打印出结果是常量字符串abcdefg,难道一个字符指针可以接收一串字符,显然是不行的。我们通过打印p的地址可以发现,p其实是存放了字符串首元素的地址,通过首元素的地址把字符串打印出来。
理解了这个,我们再看一段代码
#include <stdio.h>
int main()
{
char str1[] = "abcdefg";
char str2[] = "abcdefg";
const char* str3 = "abcdefg";
const char* str4 = "abcdefg";
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;
}
在c语言中,常量字符串会被存放到一个单独的内存区域,当不同的指针指向它时,实际上指向的区域是相同的,即地址相同。而相同的常量字符串初始化不同的数组,实际上会开辟出不同的内存块。下面看代码运行。
二、数组指针
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr+1= %p\n", &arr + 1);
return 0;
}
对于一个数组,int arr[10],我们知道arr就是数组首元素的地址,而当&arr时,它的打印也是首元素的地址。那意思是它俩相同?(x86环境)
通过后面两个打印,我们可以发现arr+1跳过的是4个字节,而&arr+1跳过的是40个字节,即10个元素。前面我们说到,指针的类型决定指针加减整数的步长,所以arr是int类型的指针,而&arr就是数组指针,即int(*)[10]类型的指针类型。
数组指针在日常的一个运用就是可以在传参时不使用数组而是使用指针接收。
void test(int arr[3][5])
{
}
void test(int (*p)[5])
{
}
int main()
{
int arr[3][5] = { 0 };
test(arr);
return 0;
}
三、参数
一维数组传参举例
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int* arr)
{}
void test2(int* arr[20])
{}
void test2(int** arr)
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
二维数组传参举例
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
void test(int(*arr)[5])
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
指针传参比较简单,但要注意下面这个二级指针传参例子
void test(char** p)
{}
int main()
{
char* arr[10];
test(arr);
return 0;
}
arr首元素的地址是一个char*的地址显然是可以用char**接收的。
四、函数指针
#include <stdio.h>
void test()
{}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
void (*p)() = test;
printf("%p\n", p);
return 0;
}
这段代码打印结果:
代码中p就是函数指针,指向一个函数,它的参数是空,返回类型是void,即p的类型是void (*)()。
举个简单的使用函数指针的例子
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*p)(int, int) = add;
int ret0 = add(3, 5);
int ret1 = (*p)(3, 5);
int ret2 = p(3, 5);
printf("%d\n", ret0);
printf("%d\n", ret1);
printf("%d\n", ret2);
return 0;
}
下面来看看来自《C陷阱和缺陷》的两句代码
#include <stdio.h>
int main()
{
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);
return 0;
}
#include <stdio.h>
int main()
{
//代码1
(*(void (*)())0)();
//( * ( void (*)() ) 0 )()
//void (*)() 中间这一块就是我们上面讲到的函数指针
// (void (*)()) 加上括号就是强制类型转换
// (* )() 两边的这个就是调用某地址返回类型为void,没有参数的函数
// 这个代码是一次函数调用,它的意思就是先将地址0强制转换为void (*)()类型,然后调用这个函数。
//代码2
void (*signal(int, void(*)(int)))(int);
//void (*signal(int, void(*)(int) ) ) (int);
//void(*)(int) 是一个函数指针类型作为一个参数,这个指针指向的函数,参数为int,返回值为void
//将整句代码分成下面两个部分
//signal(int, void(*)(int) ) signal函数有两个参数,一个是int,一个是void(*)(int)
//void (*) (int) 这是一个函数指针类型
// 这个代码是一次函数声明,声明的是参数为int的signal(int, void(*)(int))函数,其返回值是void(*)(int)
return 0;
}
五、函数指针数组
举个运算器的例子
#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 com(int a, int b)
{
return a % b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int (*p[6])(int x, int y) = { 0, add, sub, mul, div ,com };
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 5:com\n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 5 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else
printf("输入有误\n");
}
return 0;
}
六、指向函数指针数组的指针
举例说明
void test()
{
printf("abc\n");
}
int main()
{
void (* p )() = test;//定义一个函数指针p
void (* parr[5] )();
parr[0] = *p; //定义一个函数指针数组parr,将p指针指向的函数地址赋给parr[0]
void (*(*pparr)[5])() = &parr;//定义一个指针pparr,将parr整个数组地址赋给pparr
test();
(*p)();
(*parr[0])();
(*(*pparr)[0])();
return 0;
}
七、回调函数
回调函数就是通过函数指针调用的函数。一个函数指针作为参数传给另外一个函数,当这个指针被调用其所指向的函数时称这个为回调函数。
一个回调函数的代表qsort
在cplusplus中查询qsort,它有四个参数,第一个参数是首元素地址,类型是void*,目的是为了接收各种类型的地址,第二个参数是数组长度,类型是size_t,第三个参数是单个元素长度,类型是size_t,目的是为了区分各个类型,因为这个函数是要接收各个类型的元素,第四个参数是函数指针,用来给出排序顺序,两个元素相减,小于0为升序,大于0为降序。
int cmp_int(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
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), cmp_int);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
模拟实现回调函数qsort
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
struct student
{
char name[20];
int age;
};
int cmp_int(const void* a, const void* b)
{
return *(int*)b - *(int*)a;
}
int cmp_stu_age(const void* a, const void* b)
{
return ((struct student*)a)->age - ((struct student*)b)->age;
}
int cmp_stu_name(const void* a, const void* b)
{
return strcmp(((struct student*)a)->name, ((struct student*)b)->name);
}
void swap(char* a, char* b,size_t size)
{
for (int i = 0; i < size; i++)
{
char tmp = * a;
* a = * b;
* b = tmp;
a++;
b++;
}
}
void print_int(int arr[],size_t a)
{
for (int i = 0; i < a; i++)
{
printf("%-2d", arr[i]);
}
printf("\n");
}
void print_stu(struct student stu[], size_t s)
{
for (int i = 0; i < s; i++)
{
printf("%s %d", stu[i].name, stu[i].age);
printf("\n");
}
}
void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const void* a, const void* b))
{
for (int i = 0; i < num - 1; i++)
{
for (int j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + size * j, (char*)base + size * (j + 1)) > 0)
{
swap((char*)base + size * j, (char*)base + size * (j + 1), size);
}
}
}
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
struct student stu[3] = { {"zhangsan",20},{"kele",15},{"afei",30} };
size_t a = sizeof(arr) / sizeof(arr[0]);
size_t s = sizeof(stu) / sizeof(stu[0]);
bubble_sort(arr, a, sizeof(arr[0]), cmp_int);
print_int(arr, a);
bubble_sort(stu, s, sizeof(stu[0]), cmp_stu_age);
print_stu(stu, s);
bubble_sort(stu, s, sizeof(stu[0]), cmp_stu_name);
print_stu(stu, s);
return 0;
}
在bubble_sort函数中,只有将地址强制转换为char*,将每个元素都对它的每个字节进行比较和交换才能完成全类型排序。