字符的类型是char;
字符指针的类型是char*
表达方式如下:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
这是最简单也是最普遍的表达方式。
但是还有另外一种:
char* pstr = "hello bit.";
在这行代码中,貌似将hello bit.这个字符串放进了字符指针变量pstr中,但事实真的如此吗?
实际上,它的本质是把这个字符串的首字符h的地址放进了pstr中
我们会发现这个概念貌似似曾相识,没错,它跟数组指针的存放方式相差无几。
可以把字符串想象成一个字符数组,因为数组就是地址,取字符串的地址也就可以相当于取数组
当常量字符串出现在表达式中时候,它的值也就是第一个字符的地址。
但是需要注意的是:内容相同的常量字符串只会保存一份地址:
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
这两个不同的指针变量实际上指向的是同一个常量字符串hello bit.
所以这里的str3==str4。
事实上,
各种简单类型例如整型int,字符型char等的指针变量事实上意义和用法都大同小异:
整型指针变量:用来存放整型变量的地址,
int a = 100;
int *p_a = &a;
字符指针变量:用来存放字符型变量的地址,
char b="hello world.";
cahr *p_b = &b;
浮点指针变量:用来存放浮点型变量的地址,
float c = 3,14;
float *p_c = &c;
指针数组是数组元素是指针的数组;
那么数组指针是什么呢?
int *p1[10];
int (*p2)[10];
上方的p1和p2分别代表什么呢?
对它们进行分析:
我们注意到p1的类型实际上是int*,而后方的[10]告诉我们数组的本质,所以可以得出p1是指针数组;
我们注意到p2的类型实际上是int,而括号将*p2同时扩了起来,这说明p2实际上是一个指针变量,接着后面的[10]告诉我们它指向的是一个大小为10个整数的数组,所以p2是一个指针,指向一个数组,这个指针也就称为数组指针。
注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
那么,
数组指针变量也就应该是:存放的应该是数组的地址,能够指向数组的指针变量。
数值指针变量用来存放地址,如果我们需要获取数组的地址也就需要用到:
&数组名
int arr[10] = {0};
&arr;//得到的就是数组的地址
我们存放数组的地址也就存放在指针数组变量中:
int(*p)[10] = &arr;
在未学习数组指针变量之前,二维数组传参我们使用的是数组;
但是在学习了数组指针变量之后,我们应该了解其实写成指针也是可以的。
我们知道:二维数组的元素都是一维数组,那么它的首元素就是第一行数组的地址
所以二维数组的数组名的表示就是第一行一维数组的地址。
那么第一行地址的类型也就是数组指针类型。
也就意味着二维数组传参本质上也就是传递了地址,传递也就是这个一维数组的地址。
//void Print(int (*arr)[5], int r, int c)
//{
// int i = 0;
// for (i = 0; i < r; i++)//行
// {
// int j = 0;
// for (j = 0; j < c; j++)
// {
// printf("%d ", *(*(arr+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);//打印arr数组的内容
//
// return 0;
//}
所以,二维数组传参,形参部分既可以理解为数组也可以理解为是指针。
在此再次强调这二者的区别,
指针数组是元素是指针的一类数组,它的本质是数组。
数组指针是指向某个数组的指针,它的本质是指针。
数组指针变量是用来存放数组的地址;
函数指针变量是用来存放函数的地址。
那么函数的地址是什么?
可以看到add与&add的地址是同一个,这也就说明函数的地址就是函数名的地址。
所以如果要存放函数的地址,也就可以创建函数指针变量。
int (*p)(int x, int y);//(*p)的括号不能省略,如果省略了那么类型就会由int变为int*
需要注意的是,这里的两个参数名通常是可以去掉的,因为它们实际上作用不大,我们使用函数指针变量的主要目的是将这个函数存放起来。
int (*p)(int,int);
而对应的函数指针类型:
int (*) (int a, int b);
我们看到,int是pf3指向函数的返回类型,也就是说这里是指原函数的类型;
char* test(int a, char c)
{
//...
return NULL;
}
int main()
{
char* (*pt)(int, char) = test;
return 0;
}
在上述代码中,通常在主函数中调用这个函数时我们会忽略这个函数的类型其实是char*,所以我们在书写函数指针变量的时候前面也应该是char*而不是char。
注意:我们在书写函数指针变量的时候,其实是无需用*来解引用的。
因为函数的地址就是函数名的地址,无需再使用解引用操作来调出函数的地址,它本身就已经得知了。
int (*pf)(int,char)=&Add;//这样可行
int (pf)(int,char)=&Add;//这样也可行
在了解函数指针变量的基本知识之后,我们就可以试着使用它。
我们知道,指针的重要作用之一就是通过&与*来访问该指针的源头从而返回值。
那么我们也可以使用函数指针来实现函数的调用。
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}//结果也就是5和8(2+3;3+5)
函数指针的表达式很冗长,有时候它会隐藏于某行代码之中很难辨识。反过来看如果我们不认识函数指针,可能碰到一行复杂难懂的代码我们更会无从下手。
举两个例子:
(*(void (*)())0)();
这这行代码的括号刨析一下:
我们知道,像(int)3.14这种形式的书写代表着:强制类型转换。也就是说把原本属于浮点型的3.14强制转换成整型。
而上述代码中的(void(*)()),这是一个函数指针类型的书写格式。
所以也就是说上述类型将原本是整型的0强制转换成函数指针类型。也就是转换成了一个函数的地址。这个函数没有参数,返回类型是void。
而在整个括号的初始还有一个*号,它代表着对函数指针的解引用。
所以,这实际上是一次函数调用。
调用0地址处的函数。
void (*signal(int , void(*)(int)))(int);
同样对每个括号包括的内容进行刨析我们可以得知:
上述代码是函数声明;
signal是一个函数;
函数的参数类型一个是int整型,另一个是函数指针类型,返回类型是void;
而在signa之外又是一个函数指针变量的格式,所以也就是最外边是一个函数指针变量,这个函数的返回类型是void,函数参数类型是int。
总结:这些看似很复杂的代码,如果我们不了解函数指针的知识的话,是很难理解的。
那么既然函数指针变量这一类的代码如此冗杂,我们该如何化繁为简呢?
将函数的地址存放在一个数组中,那么这个元素是函数指针的数组也就被称为函数指针数组
函数指针数组的定义
int (*parr1[10])();
parr1与[]在一起说明它是个数组;
在外围我们看出数组的元素是int (*)() 类型的函数指针。
应用
int A(int x, int y)
{
return x + y;
}
int B(int x, int y)
{
return x - y;
}
int C(int x, int y)
{
return x * y;
}
int D(int x, int y)
{
return x/y;
}
int main()
{
int(*p[4])(int, int) = {A,B,C,D};
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d\n",p[i](8, 4));
}
return 0;
}
有一种关键字可以重命名类型,可以将复杂的类型简单化。
它就是typedf关键字
typedef unsigned int uint;
//将unsigned int 重命名为uint
typedef int* ptr_t;//指针类型重命名
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
在使用的过程中,我们就可以使用重命名后的名字来代替原本冗长的名字,同时需要注意在不同的类型中使用时用法会有所不同。
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);