对应的视屏链接:鲍松山的个人空间_哔哩哔哩_bilibili
目录
指针除了能指向基本数据类型,还能指向数组、函数、结构体……,对于 C 语言来说,指针几乎无所不能,我往往把指针称为任督二脉之中的一脉,因为打通了指针的理解,毫无夸张的说,C 语言才算是真正的入门,在以后的编程中才几乎不受语言语法的限制。
本节重点讲解指针与数组和函数的结合,它们之间的结合会擦出火花,这些火花将是我们理解复杂程序的基础。
1、指针与数组
指针与数组的结合主要会形成一对左右颠倒的概念,看似一样,实则距离遥远,它们是数组指针和指针数组
-
数组指针
数组指针,首先它是一个指针,它指向一个数组,它是“指向数组的指针”简称,对于数组指针,强调的是指针的概念,只不过,指针的能力是用来指向数组类型的,并且方括号中的数字是固定的,例如:int (*p)[10],p 就是指向数组的指针,其中 p 指针规定了只能指向整形的数组,且数组大小只能是 10 个整形元素,不能多也不能少,多之少之都会认为其指针的能力与指向的实体不符。
数组指针的意义何在呢?我们考虑如下的程序:
#include<stdio.h>
int main()
{
int array[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = array;
//int **pp = &array; //应该定义为 int(*pp)[10] = &array;
return 0;
}
对于一个数组,我们可以定义一个相应类型的一级指针,接收其数组名的内容,但是,当我们需要对一个数组进行取地址赋值时,将如何定义指针的类型了?
很多初学者会认为定义一个相应类型的二级指针即可,这是不正确的,(当然,在 C 语言的工程中,即.c 文件中,这个问题是可以编译通过,只不过 C 语言是一种非强类型语言,在 C++中,对类型的检查更加严格,因此编译不过),因为他会认为一级指针接收数组名,那么数组名的地址且不就是二级指针就能接收了?非也,其实,个人认为,指针的操作,往往很多莫名其妙的出错都集中在类型的不匹配,所以,一旦出错,就得认真分析地址与指针的类型是否相应匹配,也只有这样的锻炼,才能把指针真正的融会贯通。那么,我们将如何定义指针的类型,才能接受数组名取地址了?
其实问题不难,只要分析数组足矣,数组名代表整个数组空间,那么,对数组名取地址,即就是在对整个数组取地址,则数组的地址自然要用指向数组的指针才能接收,所以,必须定义指向数组的指针类型,即为数组指针。
-
指针数组
指针数组,首先它是一个数组,数组的元素都是指针,它是“储存指针的数组”的简称。
对于指针数组,强调的是数组的概念,只不过,数组所保存的类型是指针罢了,其地位跟普通的数组没有什么区别,都是数组,只不过是大家保存的类型不同而已,因此,我们美名其曰:保存指针的数组就称其为指针数组, 例如:int *p1[10]
指针数组的意义何在?我们考虑如下程序:
#include<stdio.h>
void main()
{
int a = 10;
int b = 20;
int c = 30;
int *array[3] = {&a, &b, &c}; //初值为地址
}
有时需要使用数组来保存变量地址的,此时就会用到指针数组类型。
-
数组指针 VS 指针数组
数组指针int (*p)[3]与指针数组int *p[3]从定义的形式上看,它们之间的差距仅在于是否有小括号,这也告诉我们,其二者差距的本质在于*与[]的结合律的问题,如果没有小括号,变量 p 先与[]结合,形成数组的概念,若有小括号,则变量 p 就与*号先结合成指针的概念,因此,[]的优先级比*高,能够认清这一点是关键。
对于指针数组,强烈建议,在起初定义时可以有意识的让*向类型方向靠近,例如:int* ar[3], 不要写成 int *ar[3],因为指针在 C 语言中太深入人心了,很多人一旦看到*就会既激动又恐惧,激动的是有机会大施拳脚,恐惧的是害怕操作指针,所以在遇到*时失去思考能力,都认为是指针了,其实不然,所以在定义时如果有意识的让*靠近类型,可以提醒自己定义的是一个数组,只不过存储类型为指针而已,当然,如果已经很熟悉了,那么这一切都是可以无视的,甚至是任性的......
2、指针与函数
指针与函数的结合也会形成一对左右颠倒的概念,它们是函数指针和指针函数
-
函数指针
函数指针,首先它是一个指针,只不过,指针所指向的类型是函数,它是“指向函数的指针”的简称
#include<stdio.h>
int Max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
//情形 1
Max(1,2);
//情形 2
int(*pfun)(int,int);
pfun = &Max;
(*pfun)(1,2);
//情形 3
pfun = Max;
pfun(1,2);
return 0;
}
一般来说,我们调动函数往往是通过函数名来进行调动,例如情形 1,
由于指针的强悍且几乎无所不能,只要你能表示出具体的类型,一定可以想办法定义出相应的指针类型,因此,通过指针来调动函数就显得很自然了,我们把能够指向函数的指针称为函数指针,例如情形2
把 Max 函数的地址赋给了 pfun 函数指针,在调动时先取值,然后再调动函数,这是一种标准的做法
事实上,由于函数名就是函数的入口地址,本身也充当了地址,因此,我们可以简化程序,例如情形 3,由于指针所指之物为函数,因此它的调动就行如直接运行函数,但是,心里要清楚,情形 3 的做法实际是情形 2 的简写过程
注意,一般指针都有其加 1 的能力,但是,函数指针不允许做这样的运算。即pfun+1 是一个非法的操作
-
指针函数
指针函数,首先它是一个函数,只不过函数所返回的类型是指针类型,它是“返回指针类型的函数”的简称
我们把返回指针类型的函数称其为指针函数,那就意味着只要返回值为指针,无论是什么类型的指针,都有资格称为指针函数,形如这样的定义就是指针函数 int* fun();
重点在于其返回值一定要是指针类型。
思考:
如何定义一个函数的返回值为指向函数的指针?
-
函数指针 VS 指针函数
函数指针int(*pfun)(int,int)与指针函数int* fun(int,int)它们本质区别也仅仅在于有无小括号(),这也体现出()结合律高于*,有括号则名字先跟*结合形成指针的概念,只不过指针所指类型为函数,若没有括号则名字先与小括号结合,形成函数的概念,只是函数的返回值为指针类型。
注意,在定义函数指针时,一定要思考所指函数的原型,必须是返回值类型、参数的个数及类型完全一样,这样的指针才能接收其相应的函数地址,否者无法接收
3、指针操作结构体
-
普通操作
typedef struct Student
{
char name[10];
int age;
float height;
}Student;
int main()
{
Student s = {"鲍鱼", 30, 170.8};
printf("name = %s, age = %d, height = %f\n", s.name, s.age, s.height);
//指针的两种操作方式
Student *ps = &s;
printf("name = %s, age = %d, height = %f\n", (*ps).name, (*ps).age, (*ps).height);
printf("name = %s, age = %d, height = %f\n", ps->name, ps->age, ps->height);
return 0;
}
-
结构体传参
typedef struct Student
{
char name[10];
int age;
float height;
}Student;
//结构体传值
void PrintStudent1(struct Student s)
{
printf("name = %s, age = %d, height = %f\n", s.name, s.age, s.height);
}
//结构体传址
void PrintStudent2(struct Student *ps)
{
printf("name = %s, age = %d, height = %f\n", ps->name, ps->age, ps->height);
}
int main()
{
Student s = {"鲍鱼", 30, 170.8};
PrintStudent1(s);
PrintStudent2(&s);
return 0;
}
指针指向结构体时,可以通过指向符->操作其内部成员,简洁方便
当结构体传参时,首选地址传递,效率更高
-
链表
#define ElemType int
typedef struct ListNode
{
ElemType data;
struct ListNode *next;
}ListNode;
typedef ListNode* List;
void ListInit(List *phead)
{
*phead = NULL;
}
List ListCreate()
{
ListNode *phead = (ListNode*)malloc(sizeof(ListNode));
assert(phead != NULL);
phead->data = 1;
phead->next = NULL;
ListNode *p = phead;
for(int i=2; i<=10; ++i)
{
ListNode *s = (ListNode*)malloc(sizeof(ListNode));
assert(s != NULL);
s->data = i;
s->next = NULL;
p->next = s;
p = s;
}
return phead;
}
void ListShow(List phead)
{
ListNode *p = phead;
while(p != NULL)
{
printf("%d-->", p->data);
p = p->next;
}
printf("Over.\n");
}
int main()
{
List mylist;
ListInit(&mylist);
mylist = ListCreate();
ListShow(mylist);
return 0;
}
4、总结
无论是数组指针、指针数组、函数指针、指针函数,其实我们只需关心后面两个字,因为后面两个字是类型的关键,也是类型的大前提
例如,数组指针,则前提一定是指针类型,再者,他们之间的差距很微妙,只在于括号的有无,其关键在于优先级,咱们心里要有数
针对指针(*p)来说,如果出了小括号,遇到的是[],形如(*p)[],则这是指向数组的指针,若出了小括号,遇到的是(),形如(*p)(),则这是指向函数的指针
能否正确区分这些概念,是我们能否正确解读复杂指针的关键。