1. 指针的引入
指针是C语言的灵魂。先来做几道题,再看什么是指针:
int i = 100;
int *p = &i;
- 指针指向的对象地址是什么?&i
- 指针指向的对象类型是什么?int
- 指针指向的对象内容是什么?100
除此之外,关于指针变量也有几个问题:
- 指针变量的地址是什么?&p
- 指针变量的类型是什么?int*
- 指针变量的内容是什么?&i
带着上面的几个问题学习指针,搞懂这几个问题后指针就彻底明白了。
最开始的时候,我一直认为数组和指针是同一种数据类型、是替换的关系,后来才发现是两种不同的类型,在内存表示上只不过有相似之处而已。
指针是一个内存地址,描述了数据在内存中的位置。指针变量用于保存这个“首地址”,注意这里是首地址。下面是一个指针的基本定义与用法的例子:
int i = 10;
int *p = &i;
上面的示例中,i是一个int类型的普通变量,初始化为10;&符号是取址符,用来获取变量i的地址;p是一个指针变量,用来保存变量i的首地址,也就是说指针变量p指向了变量i。
为什么要说是首地址呢?这里只是强调,也可以直接说地址或者是起始地址,上面的示例中指针变量p指向的变量i所在的内存块,即从指向的这个地址开始的后面4个字节,都属于p的空间,注意同一CPU架构下,指针变量都是等长的,但指针指向的数据占用的内存大小却不同。
2. 有效地址
通常在定义一个变量时,就向系统申请了一块内存空间来存放该变量,也就是说在定义变量的时候,变量在内存中有了一个确定的位置,即有了一个有效的内存地址。
int i = 10;
int *p = &i;//对变量i取址
C语言中提供了&运算符来获取变量的内存地址,即返回该变量在内存中的首地址。取到这个内存地址后,就可以存储到指针变量中了,它是一个合法的、有效的内存地址。
注意,在给指针变量初始化时,必须是一个有效的内存地址,不能随便赋值,否则会造成严重的后果。
3. 解引用
既然指针是某项数据的内存地址,那么当然也可以通过这个地址访问到具体的数据,这个操作称为“解引用”。解引用使用操作符*来表示,也称为取值符。解引用通常用来访问或修改某个指针指向的值。
int a = 10;
int *p = &a;
*p = 100;//解引用
上面的实例中,*p解引用获取到a的值为10,后面重新赋值为100,所以a的值就为100。
4. 二级指针
如果一个指针指向的内容还是一个指针(类型),称为二级指针,也叫作指针的指针。具体的可以理解为,二级指针指向了一级指针,即保存了一级指针的地址;一级指针保存了某个变量的地址。
int a = 10;
int *p1 = &a;
int **p2 = &p1;
**p2 = 100;
上面的例子中,a是一个int类型的普通变量,p1是一级指针,保存了变量a的内存地址;p2是二级指针,保存了指针变量p1的地址,所以要在p2前面加上2个*。对二级指针解引用时,也需要加上2个*来访问到变量a的值,也就是说一个*只能解一次引用,几级指针就要解几次引用,加几个*。
二级指针及多级指针通常应用于构建复杂的数据结构,比如链表、二叉树等等。
5. 指针常量与常量指针
指针常量和常量指针是容易混淆的两个概念,完全是两种不同的东西。一般来说,修饰符在前,而概念在后,意思就是说,后面的词语才是核心主体。比如指针常量,指针是修饰符,而主体是常量,常量才是原本的数据类型。这样记忆就好记多了。先来看指针常量。
5.1 指针常量
指针常量,主体是常量,也就是说本质就是一个常量,指针作为修饰符来表示常量的类型,也可以理解为一个指针类型的常量。书写方式是const关键字在*符号的后面。
int a = 10;
int* const p = &a;
- 这里的p是一个常量,那么在定义后就不能再重新被赋值;
- 指针保存的地址可以修改,这里的p可以指向其他的变量地址;
- 指针指向的值不能修改,a的值不能修改。
5.2 常量指针
常量指针,主体是指针,本质是一个指针,常量作为修饰符来说明指针的指向(地址)是一个常量,也可以理解为一个指向常量的指针。书写方式是const关键字在*符号的前面。
int a = 10;
int const *p = &a;
//等同于
const int *p = &a;
- 这里的p是一个指针,只不过使用const修饰为常量了,本质还是指针,注意常量是指指针的值,即a的地址,而不是指针指向的a的值;
- 指针指向的内存地址中的值可以被修改,即a的值;
- 指针变量中保存的内存地址不能被修改,即a的地址不能被修改,因为是常量。
5.3 常量指针常量
常量指针常量,也称为一个指向常量的常指针,在定义初始化后,指针的值(内存地址)和指针指向的内存地址中的值都不能被修改。
int a = 10;
const int const *p = &a;
6. 指针数组与数组指针
指针数组与数组指针也是很容易混淆的两个概念,也可以按照“修饰+主体”来理解。
6.1 指针数组
指针数组,首先它是一个数组,指针作为修饰符来指明了数组的元素类型,可以理解为一个数组中的所有元素都是指针类型的,每一个元素都是一个指针。
char *arr[4] = {"http", "www", "peiqiblog", "com"};
arr是一个指针类型的数组,一共有4个元素,每一个元素都是指向字符串的指针。
6.2 数组指针
数组指针,本质上是一个指针,指向的是一个数组。数组作为修饰符来说明指针是一个数组类型的,可以理解为一个数组类型的指针。
int (*p)[4];//指向数组的指针
p是一个数组指针,指向一个int类型的数组,数组中包含4个int类型的元素。
7. 指针函数与函数指针
指针函数与函数指针也是很容易混淆的两个概念,也可以按照“修饰+主体”来理解。
7.1 指针函数
指针函数,首先是一个函数,指针作为修饰符指明了函数的返回值类型是一个指针,即返回值是一个地址,可以理解为一个指针类型的函数。指针函数的声明形式为:指针类型 函数名(参数列表);
int* func(int arg1,int arg2);
func函数的返回值类型是int*,所以它是一个指针函数。注意函数的返回值类型必须是同类型的指针变量来接收,也就是说返回值类型是int*类型,就不能用char*类型来接收。
7.2 函数指针
函数指针,本质是一个指针,函数作为修饰符来指明指针的类型为函数类型,可以理解为一个函数类型的指针,或者指向一个函数的指针。函数指针指向一个函数的首地址,可以通过它来调用函数。
int sum(int a,int b){
return a+b;
}
int (*func) (int arg1,int arg2);
func = sum;//将function函数的首地址赋值给指针func
int res = func(1,2);
函数sum是一个普通函数,而func是一个接收两个int类型参数且返回值为int的函数指针,函数名称就是函数地址,可以将函数名称直接赋值给函数指针,就可以通过函数指针来调用sum函数了。
注意,这里的函数指针必须用圆括号()括起来,否则就变成了一个返回值为整型指针的函数声明了。
7.3 指针作为函数参数
通常情况下,函数的传参一般都是值传递,就是说无论传递的是什么数据,到了函数里面,都会拷贝一份副本,函数内部操作的就是这份副本,操作副本不会影响函数外部数据本身的值。
如果是传递的指针变量那就不一样了,我认为还是值传递,因为指针的值是地址,所以实际上传递的是某个变量的地址(地址只有一份),这样的话函数就可以通过传递的这个变量地址来访问或修改变量本身的值了。
void func(int *p1,int *p2){
*p2 = *p1;
}
int a = 10;
int b = 20;
func(&a,&b);
函数func是一个接收2个指针变量参数的函数,后面调用该函数传递的就是变量a和b的地址,由于传递的是变量的地址,所以函数内部的操作会影响到外部变量a和b的值。
有需要补充的,或者需要更正的地方,请评论区留言!