目录
1.字符指针
以下代码输出的结果是什么?
回答:h hello the hello the
分析:说明字符指针变量可以存放字符串。
第一个h是因为*ps指针指向的就是hello的首字符h,故输出h;后两个因为数组名arr和ps一样都是hello的首字符地址,故均输出hello。
int main()
{
//本质上是把"hello the"这个字符串的首字符地址存储在了ps中
char* ps = "hello the";
char arr[] = "hello the";
printf("%c\n", *ps);//h
printf("%s\n", ps);
printf("%s\n", arr);
return 0;
}
以下代码输出的结果是什么?
回答:应该是1和2不同,3和4相同
分析:数组名代表数组首元素的地址,str1和str2是两个不同的数组,分别需要开辟两个不同的内存空间,str1和str2分别指向两个hello的首地址,因此不同;
"hello bit"是常量字符串,指针变量无法通过解引用操作改变它。对于内存而言既然"hello bit"是常量,那么只需为它开辟一个内存空间即可,地址编号确定,因此3和4指向了同一个地址即常量字符串"hello bit"的首元素地址
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;
}
2.指针数组
定义:指针数组是一个存放指针的数组,本质上是数组,其中存的内容是指针
如下代码中:arr数组中可以存放三个整形指针int*,而a,b,c分别为三个整形数组首元素地址放入其中arr[0],arr[1],arr[2]
int main()
{
//指针数组
//数组——数组中存放的是指针(地址)
//int* arr[3];//存放整型指针的数组
int a[] = { 1,2,3,4,5 };
int b[] = { 2,3,4,5,6 };
int c[] = { 3,4,5,6,7 };
int* arr[3] = { a,b,c };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", *(arr[i]+j));
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
3.数组指针
定义:是一种指针,应该能够指向数组的指针,数组指针中存放的应该是数组的地址
接下来会与之前的指针数组区别:
- int *p1[10];——指针数组
[ ]的优先级高于*号的,所以必须加上()来保证p先和*结合
- int (*p2)[10];
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针
思考一下数组指针的形式
首先得是指针(*)→指针指向的数组有多少元素呢([ ]) →指向的数组又是什么类型
int main() { int arr[10] = { 1,2,3,4,5 }; //arr——是arr[0]的地址 int(*parr)[10] = &arr;//取出的是数组地址 //parr就是一个数组指针——其中存放的是数组的地址 double* p[10]; double* (*pd)[10] = &p; return 0; }
有什么内涵呢
观察以下代码:
int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10 }; int(*pa)[10] = &arr;//数组指针 int i = 0; for (i = 0; i < 10; i++) { printf("%d\n", (*pa + i)); printf("%d\n", *(*pa + i)); } return 0; }
运行后我们看一下:
- *说明pa是指针变量,pa应该存放一个数组的地址,pa+1会跳过一个数组的地址大小;
- *pa是首元素地址,也就是arr数组名,*pa+1 也就是地址前进4位字节(与int指针类型匹配),反映数组中每个元素的地址
- 解引用操作:*(*pa)由此得出arr数组中的元素
实际应用:打印二维数组中的每个元素
p是数组指针,指向一个数组,*p保存首元素地址(arr数组名),[5]反映指向数组中有5个元素,int表示数组元素类型
对于二维数组的首元素是二维数组的第一行,这里传递的arr,其实相当于第一行的地址,是一维数组的地址
- (p+i)得到每一行数组的地址
- *(p+i)得到每一行数组的首元素地址
- (*(p+i)+j)获取每一行数组中每个元素的地址
- *(*(p+i)+j)获得每一行数组中的元素数组内容
void print(int(*p)[5],int r,int c) { int i = 0; int j = 0; for (i = 0; i < r; i++) { for (j = 0; j < c; j++) { printf("%p ", (p + i) ); printf("%p ", *(p + i) ); printf("%p ", *(p + i)+j); printf("%d ", *((*(p+i) + j))); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; print(arr,3,5); return 0; }
验证一下结果:
判断下面代码的意思
int arr[5];——有着5个整型元素的数组
int *parr1[10];——存放10个整型指针的数组
int (*parr2)[10];——该指针能指向一个数组,一个数组中10个元素,每个元素类型是int
int (*parr3[10])[5];——parr3是一个存储数组指针的数组,该数组能存放10个指针,每个数组指针能够指向一个数组,数组5个元素,每个元素是int类型
4. 数组参数、指针参数
一定要分析清楚,实参传过去的是什么。是地址的话是谁的地址,而形参对应又应该用什么形式接收。
一维数组传参
判断哪些可以传参,哪些不能?
#include <stdio.h> void test(int arr[])//ok? {} void test(int arr[10])//ok? {} void test(int *arr)//ok? {} void test2(int *arr[20])//ok? {} void test2(int **arr)//ok? {} int main() { int arr[10] = {0} int *arr2[20] = {0}; test(arr); test2(arr2); }
这里形参的形式都是正确的。
- test()
1,2:当作数组接收 √正确
3:arr为数组名,此时传递的是首元素地址,用int*接收 √正确
- test2()——int* arr2[20]是一个指针数组,存放int*类型的指针
4:传递指针数组,相应类型接收 √正确
5:arr2是指针数组首元素地址,首元素是int*类型的指针,也就成了二级指针 √正确
二维数组传参
判断哪些可以传参,哪些不能?
void test(int arr[3][5])//ok? {} void test(int arr[][])//ok? {} void test(int arr[][5])//ok? void test(int *arr)//ok? {} void test(int* arr[5])//ok? {} void test(int (*arr)[5])//ok? {} void test(int **arr)//ok? {} int main() { int arr[3][5] = {0}; test(arr); }
- arr是一个二维数组,二维数组可以不知道行数,但必须知道列数,也就是一行有多少元素
因此1√正确,2×错误,3√正确
- arr为首元素地址,代表第一行元素的地址,理解为数组指针,应该指向一个数组
1:理解arr为数组指针(能够指向第一行数组,第一行是有5个整型的数组 ),对应的形参格式错误,只是一级指针 ×错误
2:形参接收的应为一个指针数组 ×错误
3:arr为数组指针,指向的数组内有5个int类型元素 √正确
4:传过去的不是二级指针 ×错误
一级指针传参
函数形参形式是接收一级指针时,实参传递的是地址即可
二级指针传参
函数形参形式是接收二级指针时,实参可以传递二级指针,即一级指针的地址,也可以传存放一级指针的数组名
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0;
}
分析:
pc是一级指针,ppc是二级指针,test(ppc)把二级指针ppc进行传参;
test(&pc)传一级指针变量的地址;
test(arr)传存放一级指针的数组,传递了arr数组名,数组名代表首元素的地址,而arr数组每个元素都是char *类型,char *的地址相当于char * *,符合 char(int ** p)中形参的接收要求
5.函数指针
定义:指向函数的指针,存放函数地址的指针
函数指针的形式:
int(*pf)(int,int)
首先应该是指针(例如*pf),指向一个函数,函数的类型如何?
函数包括函数形参的类型,函数的返回类型,因此函数指针也应该指明所指向函数包括的组成部分。
//函数指针——存放函数地址的指针
//&函数名——取到的就是函数的地址
int Add(int x, int y)
{
return x + y;
}
int main()
{
//&函数名等价于函数名
printf("%p\n",&(Add));
printf("%p\n",Add);
//函数指针变量
/*int(*pf)(int, int) = &Add;*/
int(*pf)(int, int) = Add;//意味着Add===pf
printf("%d\n", (*pf)(3, 5));
printf("%d\n", pf(3, 5));
printf("%d\n", Add(3, 5));
return 0;
}
观察上述代码的结果:
- 函数名==&函数名,说明函数名就是函数的地址 !(同时记得数组名 != &数组名)
- 以往我们调用函数的形式Add(3,5)与int (*pf) (3,5)效果相同
pf是函数指针变量,存放函数的地址,由此可以得知pf==Add,函数还可以写成pf(3,5)
(*是对形式的理解,实际并没有起作用)
6.函数指针数组
顾名思义——存放函数指针的数组,把函数的地址存到一个数组,同时是存放同类型的函数指针
int (*pf)(int,int)=Add;
int (*pf1)(int,int)=Sub;
int (*pfArr[2])(int,int);//函数指针数组pfArr
设计一个计算器来理解:
//函数指针数组 //假设实现一个计算器,计算整型变量能够加减乘除 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("************* 1.Add ***************\n"); printf("************* 2.Sub ***************\n"); printf("************* 3.Mul ***************\n"); printf("************* 4.Div ***************\n"); printf("************* 0.退出 ***************\n"); } int main() { int input = 0; do { int(*pfArr[5])(int, int) = {NULL,Add,Sub,Mul,Div}; int x = 0; int y = 0; menu();//计算器界面 printf("请进行你所需要的操作:>"); scanf("%d", &input); if (input >= 1 && input <= 4) { printf("请进行你所需要的计算的两个数值:>"); scanf("%d%d", &x, &y); printf("%d\n", pfArr[input](x, y)); } else if (input == 0) { printf("退出程序\n"); break; } else { printf("请重新选择\n"); } } while(input); return 0; }
7.指向函数指针数组的指针
//整型数组
int arr[5];
int (*p1)[5]=&arr;
//p1是指向(整型数组)的指针
//整型指针的数组
int* arr[5];
int* (*p2) [5]=&arr;
//p2是指向(整型指针数组)的指针
//函数指针
int (*p)(int,int);
//函数指针数组
int (*p2[4])(int,int);
p3=&p2;//取出的是函数指针数组的地址
//p3是一个指向(函数指针数组)的指针
int (*(*p3)[4])(int,int);
8.回调函数
回调函数就是一个通过函数指针调用的函数
如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
快速理解:一个函数本身没有直接被使用,其地址作为另一个函数的形参而被调用
实例1——计算器的实现
//回调函数
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("**** 1. add 2. sub ****\n");
printf("**** 3. mul 4. div ****\n");
printf("**** 0. exit ****\n");
printf("**************************\n");
}
int Calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入2个操作数>:");
scanf("%d %d", &x, &y);
return pf(x, y);
}
int main()
{
int input = 0;
//计算器-计算整型变量的加、减、乘、除
//a&b a^b a|b a>>b a<<b a>b
do {
menu();
int ret = 0;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
ret = Calc(Add);
printf("ret = %d\n", ret);
break;
case 2:
ret = Calc(Sub);
printf("ret = %d\n", ret);
break;
case 3:
ret = Calc(Mul);//
printf("ret = %d\n", ret);
break;
case 4:
ret = Calc(Div);//
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
实例2——qsort函数的学习和模拟实现
qsort函数可以排列任何类型的元素,包括整型变量、字符串变量、结构体等
先来学习下qsort函数的内涵
base中存放的是待排序数组第一个元素的首地址 num是排序待数据数组中元素的个数 size一个数组元素字节的大小 int (*compar)(const void*,const void*)比较待排序数据中两个元素的函数
使用:
int cmp_int(const void* e1, const void* e2) { return *(int*)e1 - *(int*)e2; } void test1() { //整型数据的排序 int arr[] = { 9,8,7,6,5,4,3,2,1 }; int sz = sizeof(arr) / sizeof(arr[0]); //排序 qsort(arr, sz, sizeof(arr[0]), cmp_int); //打印 int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } } struct Stu { char name[20]; int age; }; int sort_by_age(const void* e1, const void* e2) { return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age; } int sort_by_name(const void* e1, const void* e2) { return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name); } void test2() { //使用qsort函数排序结构体数据 struct Stu s[] = { {"zhangsan",30},{"lisi",40},{"wangwu",20}}; int sz = sizeof(s) / sizeof(s[0]); //按照年龄排序 /*qsort(s, sz, sizeof(s[0]), sort_by_age);*/ //按照名字排序 qsort(s, sz, sizeof(s[0]), sort_by_name); } int main() { //test1(); test2(); return 0; }
模拟实现
//模仿qsort函数实现一个冒泡排序的通用算法
//不同类型数据比较方法交给使用者确定
void Swap(char* buf1, char* buf2,int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int num, int size,
int(*cmp)(const void* e1, const void* e2))
{
int i = 0;
//躺数
for (i = 0; i <num-1; i++)
{
//一趟的排序
int j = 0;
for (j = 0;j < num - 1 - i; j++)
{
//两个元素的比较
if (cmp((char*)base+j*size,(char*)base+(j+1)*size) > 0)
{
//交换
Swap((char*)base + j * size, (char*)base + (j + 1) * size,size);
}
}
}
}
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void test3()
{
//整型数据的排序
int arr[] = { 9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
//排序
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
//打印
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
test3();
return 0;
}
需要注意的点:
1.字节大小size的作用
作为qsort函数的设计者,字节大小size十分重要,因为不知道使用者是什么类型的数据进行比较;
2.前后两个数据进行比较时,如何得到地址?为什么要强制转换为char*类型的指针
因为使用者会通过形参size告知前一个元素与后一个元素差几个字节。而char*类型的指针为一个字节,因此跳过一个元素:(char*)地址+1*size,跳过两个元素:(char*)地址+2*size;
3.内部的交换函数如何实现?不知道什么类型的元素,如何将两个元素的内容进行交换呢?
由于我们不知道元素的类型,所以不能将指针转为相对应类型的指针进行交换,但我们知道一个元素的字节大小。因此我们依旧传递为char*类型的指针,通过size一个字节一个字节的交换
4.思考:为什么使用void*空指针
因为void*p空指针可以存放任意类型的指针,qsort函数并不明确知道需要排序的是什么类型的数组,因此先用void*接收,具体什么类型由使用者告知。