5.指针
(1)基本概念
首先我们要了解指针,就要清楚声明是内存地址值,我们都知道变量由四个部分组成,变量名、数据类型、内存空间地址和变量值。
变量名和数据类型在我们编写程序时就需要确定
变量值当然也可以同时确定,也可以在之后输入确定
而内存空间地址我们之前讲的很少,我们这次详细讲解
内存空间地址:变量所占用的内存空间地址在程序编译时由编译器确定。内存空间一般以字节为单位,每个字节可以存储8位信息,每个字节都有唯一的地址。程序中的每个变量都会占用一个或多个字节的内存空间,把变量所占用的内存空间的第一个字节地址成为变量地址。
例如:
int n = 10;
这一步操作,编译器接收到了一个新的变量,变量名是x,变量中存放的是一个int型整数,并为它分配对应的内存空间,将初始值10放在该内存空间。但是一般情况下,我们一般不关心内存所分配的内存地址值,只能通过变量名n访问其所占用的内存空间。
内存地址以字节为单位,通常用十六进制数来表示。
虽然内存地址是整数,但是内存地址的取值范围可能不同于整数的取值范围,所以不能用整型变量来存储内存地址,必须使用特殊的指针变量存储内存地址。
所以C语言就引用了一种特殊的数据类型——指针,用来表示内存地址,即指针中存放的是一个的内存地址,而指针变量是存放内存地址的变量。
指针包含两个信息:内存地址值和所指向的变量的类型。
(2)声明指针变量
数据类型* 指针变量名
其实,指针也有它的地址值即 &指针变量名 此时这时要用%p格式符来输出,表示输出一个内存地址,之后会介绍。
例如:
int* p;//我们通常用p变量来表示指针变量 /* 同时我们也有另外两种方式来声明指针变量 int *p; int * p; 我们推荐用第一种,因为之后要通过指针进行一系列操作,这样会比较清楚,不会混淆,当然喜欢用哪种都可以 */ //=========================================分割线 int* p = NULL;//表示p为指针,暂不指向任何整型变量,NULL可以赋值给任何类型的指针 //=========================================分割线 //分别定义了 int、float、char 类型的指针变量 int* x; float* f; char* ch; //如上面的定义,指针变量名为 x、f、ch。并不是*x、*f、*ch
(3)取地址运算符和解引用运算符
为了使用指针,C语言专门提供了一对运算符:取地址运算符”&“和解引用运算符"",如果x是变量,则&x就是变量x的内存地址。如果p是指针,则 p就是p当前所指向的变量的值。
//1.空指针NULL int* p = NULL;//表示p为指针,暂不指向任何整型变量,NULL可以赋值给任何类型的指针 //=========================================分割线 //2.同类型对象的指针 int* p,n; p = &n; //也可以这样写 int n; int* p = &n;//将n的内存地址赋值给指针变量p //=========================================分割线 //3.同类型的指针 int* p1,*p2,n; p1 = &n; p2 = p1;//这时候p2指向一个内存地址值,就是n的内存地址值,因为p1指向n,说明p1存储的是n的地址值,也就是说p1的值是n的地址值。 //此时p1的值被赋值给了p2,所以p2中也存储着n的地址值,所以也指向了n
下面我们通过示例来看看指针变量和变量之间的操作
例1:看清变量的地址值 和 指针变量所指向的变量的地址值 和 指针变量地址值
#include<stdio.h> int main() { int* p1, * p2, n; p1 = &n; //%p是取地址占位符,说明要传入一个地址值,所以如果是变量要&n,代表取出n的内存地址值,如果是指针,只要直接输出指针名即可,因为指针所保存的值就是内存地址值 printf("%p\n", &n);//输出n的地址值 001BFE7C printf("%p\n", p1);//输出p1的值 001BFE7C,值就是n的地址值,我们口头说就是p1指向n,就代表了赋值行为 printf("%p\n", &p1);//输出p1的地址值 00BEF840 }
例2:熟悉解引用运算符的使用
这时,这也是我为什么推荐为什么声明指针用第一种方式,因为声明指针如果是int p;会和解引用 p混淆,为了避免混淆,最好使用第一种.
#include<stdio.h> int main() { int* p1, n = 100; p1 = &n; printf("n的值:%d\n",n);//100 printf("n的地址值:%p\n", &n);//输出n的地址值 001BFE7C printf("p1的值:%p\n", p1);//输出p1的值 001BFE7C,值就是n的地址值,我们口头说就是p1指向n,就代表了赋值行为 printf("p1的地址值是:%p\n", &p1);//输出p1的地址值 00BEF840 printf("p1所指向的内存空间中所存放的值:%d\n",*p1);//100, 此时*p1就等价于n,步骤就是通过内存地址值来寻找所指向的变量的值 printf("变量n的值%d",*&n);//无特别意思,只是说明先取它的地址,编译器再通过内存地址来寻找变量的值,就等于它本身了 }
(4)通用指针
相同类型之间的指针变量可以相互赋值,不同类型的指针变量不可以直接赋值,也需要强制类型转换。
int* p; double* p2; p = p2;//错误写法,不能直接赋值 p = (int*)p2;//正确,强制类型转换后进行赋值,当然p2赋值给p之前也要指向一个double类型变量,不然如果直接输出在vs中会报错,
(1)void指针
我们前面提到,一个指针应该包含以下两个信息:变量的内存地址值和所指向的变量的类型。但也是由特例存在,即指针只包含内存地址值而不包含所指向的变量的类型,这种指针就是指向void类型的指针,即void类型,也成为通用指针。
void指针:它可以指向任何类型的变量,也就是void指针可以取任何类型的变量的内存地址值。
例如:
#include <stdio.h> int main() { int x = 100; int* p = &x; void* v;//声明空指针 v v = p; //直接将整型指针赋值给空指针,也就是p所指向变量的地址值赋值给了空指针,也就是这时候空指针有了x的地址值 printf("x的地址值:%p\n", &x); printf("整型指针p所保存的地址值:%p\n", p); printf("void空指针所保存的地址值:%p\n", v); v = &x; printf("void空指针所保存的地址值:%p\n", v);///和上面一样,指向指针和指向变量它的内存地址值不会改变,因为指向的都是x变量 /* 结果: x的地址值:012FFE34 整型指针p所保存的地址值:012FFE34 void空指针所保存的地址值:012FFE34 void空指针所保存的地址值:012FFE34 解释:我们这是可以发现,void是可以存放内存地址值的,注意每次运行的地址值都可能不同,因为内存是实时分配的,运行一次, 编译器就给你分配一个内存地址值 */ }
我们通过上一个例子知道了空指针它是可以保存内存地址值的,也可以被其他指针赋值,但是void指针可以赋值给其他指针吗?
答案是不行的,因为我们都只知道void是空类型,两个变量完整赋值,肯定要类型一样,所以我们要将空指针赋值给其他指针,也要强制类型转换.
#include <stdio.h> int main() { int x = 100; int* p;//声明一个整型指针,暂不指向什么变量,因为我们想用void空指针来赋值 void* v;//声明空指针v v = &x; //p = v;//错误:不能直接赋值,类型不同,无法从“void *”转换为“int *” p = (int*)v;//正确:通过强制类型转换赋值 printf("x的值:%d,x的地址值: %p\n",x,&x); printf("p解引用后:%d,p的地址值:%p\n", *p, p); printf("空类型指针的解引用值: %d,空类型指针的地址值: %p\n", *(int* )v,(int* )v);//前一个解释:先强转为整型指针,再解引用输出 /* 结果: x的值:100,x的地址值: 00CFF988 p解引用后:100,p的地址值:00CFF988 空类型指针的解引用值: 100,空类型指针的地址值: 00CFF988 解析:成功通过强制类型转换来进行赋值 */ }
(2)const修饰指针
简介:我们知道,const对变量来说是指定为一个常量,即const double PI = 3.141592,程序只能读取PI的值,不能修改,同时声明常量的时候必须赋一个初始值,不然就是错误的。回归整体,那么const对于指针来说,意味着什么呢,其实也一样,但是有三种情况
1.指针所指向的变量的值为常量(即指针所指向的变量的值不能被改变-----**const在指针运算符()的左边
2.指针本身的值不能被改变(即指针不能改变所指向的变量-----**const在指针运算符()的右边
3.指针所指向的变量的值为常量并且指针本身的值不能被改变-----**const在指针运算符()的两边
(1)const在指针运算符(*)的左边 即指针所指向的变量的值不能被改变
#include <stdio.h> int main() { int x = 100, y = 200; const int* p = &x; /* const在*号的左边,表示const此时修饰的是指针所指向的变量,所以指针所指向的值不能修改, 而指针本身的值可以修改也就是说指针可以指向其他变量如y */ //*p = 88; //错误 尝试修改x的值(*p就等价于x),因为此时*p即x是一个常量 p = &y;//正确 可以让p指向新的变量y printf("%d", *p);//结果:200 }
(2)const在指针运算符(*)的右边 即即指针不能改变所指向的变量
#include <stdio.h> int main() { int x = 100, y = 200; //const int* p = &x;//const在*的左边 int* const p = &x;//const在*的右边 /* const在*号的右边,表示const此时修饰的是指针本身,所以指针本身不能修改, 而指针所指向的的值可以修改也就是说指针所指向的变量的值能被改变 */ *p = 88; //正确 尝试修改x的值(*p就等价于x) //p = &y;//错误 可以让p指向新的变量y,这时不能再指向任何变量,只能指向它本身即x printf("%d", *p);//结果:88 }
3.const在指针运算符(*)的两边 即指针所指向的变量的值为常量并且指针本身的值不能被改变
#include <stdio.h> int main() { int x = 100, y = 200; //const int* p = &x;//const在*的左边 //int* const p = &x;//const在*的右边 const int* const p = &x; //const在指针运算符(*)的两边 /* const在*号的两边,表示const此时修饰的是指针本身还有指针所指向的变量 所以指针本身不能修改,指针所指向的变量的值也不能修改 而指针所指向的的值可以修改也就是说指针所指向的变量的值能被改变 */ //*p = 88; //错误 //p = &y;//错误 printf("%d", *p);//结果:100 }
(5)指针和函数
大体可以将此分为四部分--值传递、引用传递、函数指针、指针函数
1值传递
值传递即函数参数列表中的形参是局部变量
下面通过交换变量来详细说明
#include <stdio.h> void swap(int x, int y);//函数声明 //目的:我们想调换num1和num2之间的值,但是要通过另一个函数(swap)来完成 //注释:虽然更麻烦,但是这只是为了更好的让我们理解什么是值传递和引用传递的区别 int main() { int num1, num2; scanf("%d%d", &num1, &num2); printf("交换前:%d %d\n", num1, num2); printf("进行交换\n"); swap(num1, num2); printf("交换完毕\n"); printf("交换后:%d %d\n", num1, num2); /* 结果: 3 5 交换前:3 5 进行交换 交换完毕 交换后:3 5 */ /* 解析:为什么没有交换成功呢? 我们知道,函数中的形参是一个局部变量吧,一个局部变量它的作用域是不是只有在本函数内,这也就解释通了,当在swap函数内,他们的值确实是交换了, 但是出了swap函数之后,这个交换后的值就消失了,就是被内存释放了内存空间。所以交换后的值当然还是他们本身了 总结:实参变量的值传递给形参,无论形参的值如何改变,实参都是不会收到影响,因为一出了函数,形参的值就消失了,被内存释放了 */ } void swap(int x, int y) { int t; t = x; x = y; y = t; }
2引用传递
引用传递即函数参数列表中的形参是存放局部变量的地址值的同类型指针
#include <stdio.h> void swap(int* x, int* y);//函数声明,形参是存放局部变量的地址值的同类型指针 //目的:我们想调换num1和num2之间的值,但是要通过另一个函数(swap)来完成 //注释:虽然更麻烦,但是这只是为了更好的让我们理解什么是值传递和引用传递的区别 int main() { int num1, num2; scanf("%d%d", &num1, &num2); printf("交换前:%d %d\n", num1, num2); printf("进行交换\n"); swap(&num1, &num2);//传入两个变量的地址值 printf("交换完毕\n"); printf("交换后:%d %d\n", num1, num2); /* 结果: 3 5 交换前:3 5 进行交换 交换完毕 交换后:5 3 */ /* 解释:有可能会问,为什么这次局部变量结束了,指针的值不会消失呢? 其实,指针确实消失了,但是在消失前完成了值的转换,在上个值传递中,swap里的形参是一个局部变量,是个新的值,只是被赋值了而已 而他们的内存地址值是不同的。而指针中存储的就是num1和num2的地址值,所以*x和*y代表的是真正的num1和num2,他们内存地址值相同 所以*x和*y改变,实参也会跟着改变。 */ /* 总结:调用一个带指针参数的函数时,实参变量的地址值传递给指针形参,所以形参和实参共享相同的变量。 */ } void swap(int* x, int* y) {//接收的是两个变量的地址值, int t; t = *x; *x = *y; *y = t; }
提前说明:指针函数和函数指针的概念经常会混淆,前一个是返回指针的函数,后一个是指向函数的指针
3指针函数
格式:函数类型 *函数名(形式参数表);
函数的返回值是指针时,称之为指针型函数,通常用来获取指针所指向的对象的值
#include <stdio.h> int *max(int* x, int* y);//函数声明,形参是存放局部变量的地址值的同类型指针 //目的:我们想要获得一个最大值,通过max指针函数来完成 int main() { int num1, num2; scanf("%d%d", &num1, &num2); int* result = max(&num1, &num2);//传入两个变量的地址值,返回的也是一个指针,返回后由一个整型指针接收一个最大值变量的地址值 printf("最大值:%d\n", *result); /* 结果: 3 5 最大值:5 */ } int* max(int* x, int* y) { if (*x > *y) { return x;//返回x的值即所指向的变量的地址值 } else{ return y; } }
4函数指针
格式:函数类型 (*函数名)(形式参数表);
注意 和函数名要用括号括起来,否则因为运算符的优先级原因就变成指针函数了,运算符的优先级比 指针运算符高
函数名外层的括号让函数名先与 * 结合,表示是一个指针,再与后面的()结合,表明指针指向的的是一个函数.
#include<stdio.h> int sum(int x, int y); int (*fun) (int, int); //声明函数指针时,形式参数表中的形参名可以省略 int main(){ fun = sum; //fun函数指针指向add函数,fun这个函数指针中就有了sum函数的地址值 printf("%d\n", fun(3, 5)); //这上下两种都可以,推荐使用此种,fum(3,5)此时等价于sum(3,5) printf("%d", (*fun)(4, 2)); /* 结果: 8 6 */ } int sum(int x, int y){ return x + y; }
(6)指针与数组
在C语言里,数组与指针的关系时十分密切的,我们之前讲过,数组名代表的就是一个当前数组的首元素的地址值,也就是说这就是一个常量指针(即const在*的右边的指针,不能改变所指向的变量),所以数组名不能直接与令一个数组(相当于令一个常量指针)进行复制操作,所以其数组首元素地址就是一个常量。
那么例如
#include<stdio.h> int main(){ int a[] = { 1,2,3,5,4 }; printf("数组名代表数组首元素的地址:%p\n", a); for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++){ printf("地址:%p,值:%d\n", a+i, *(a + i)); //第二个元素*(a + i),代表每次向后遍历1位,a+1就代表数组第二位元素的地址值,而加上解引用符就代表数组第二位元素的值, } /* 结果: 数组名代表数组首元素的地址:00F3FD4C 地址:00F3FD4C,值:1 地址:00F3FD50,值:2 地址:00F3FD54,值:3 地址:00F3FD58,值:5 地址:00F3FD5C,值:4 */ /* 解析:这是一个整型数组,所以4个字节代表一个元素,4C+4十六进制是满16进1,即50.50再加4即54,以此类推 */ }
1.指针指向数组
#include<stdio.h> int main(){ int a[] = { 1,2,3,5,4 }; int* p = a;//此时p指向a数组中首元素的地址 printf("数组名代表数组首元素的地址:%p\n", a); for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++){ printf("地址:%p,值:%d\n", a+i, *(a + i)); printf("p指针:地址:%p,值:%d\n", p + i, *(p + i)); printf("甚至可以p[i]= %d\n", p[i]); //第二个元素*(a + i),代表每次向后遍历1位,a+1就代表数组第二位元素的地址值,而加上解引用符就代表数组第二位元素的值 } /* 结果: 数组名代表数组首元素的地址:005CF98C 地址:005CF98C,值:1 p指针:地址:005CF98C,值:1 甚至可以p[i]= 1 地址:005CF990,值:2 p指针:地址:005CF990,值:2 甚至可以p[i]= 2 地址:005CF994,值:3 p指针:地址:005CF994,值:3 甚至可以p[i]= 3 地址:005CF998,值:5 p指针:地址:005CF998,值:5 甚至可以p[i]= 5 地址:005CF99C,值:4 p指针:地址:005CF99C,值:4 甚至可以p[i]= 4 */ /* 解析:p指针指向了数组的时候,就说明你可以把p也当作另一个数组名来看待了 */ }
2.指针数组
1指针与一维数组
简介:就是指针数组中的每一位元素都是存放内存空间的,也就是一个指针,每一位元素都是指针
格式:数据类型 *数组名[常量表达式];
解释:数组名首先与后面的[]结合(下标运算符[]的优先级为1,之前的优先级表中有),表明是数组,再与前面的 * 指针运算符结合,说明数据元素类型是指针类 型
#include<stdio.h> int main(){ int a = 1, b = 2, c = 3; int* p[3]; p[0] = &a; p[1] = &b; p[2] = &c; //数组p是一个有3个元素的指针数组,每个数组元素都指向一个整型变量 }
2指针与二维数组
在二维数组中,直接用数组名,代表的是行的首元素同时也是列的首元素,就是第一行第一列的第一个元素的地址值,我们用平面展开方式来看看数组名+n代表什么含义
/* int a[3][2];//三行两列 a----->a[0]----->a[0][0] a[0][1] a+1--->a[1]----->a[1][0] a[1][1] a+2--->a[2]----->a[2][0] a[2][1] 也就是说数组名+n代表的是第n行的首元素的地址值,即a+n 等价 a[n] 等价 &a[n][0] */
那么向让指针p访问数组a中第n行的元素,可以让p指向数组a中第i的首元素
p = &a[n][0]; //由上可得等价于 p = a[n];
想通过p指针来使数组a中第n行的元素全部为0
#include<stdio.h> int main(){ int* p, n; scanf("%d", &n); int a[3][2] = { 1,3,4,5,2,6 }; for ( p = a[n]; p <= a[n]+1; p++){ *p = 0;//p = &a[n][0],p++后,p = &a[n][1] } }
(7)动态存储分配
静态存储方式:是指在程序运行期间由系统分配固定的存储空间的方式。
动态存储分配:是指在程序运行期间根据需要进行动态的分配存储空间的方式,动态存储分配的内存空间通常称为堆。
我们常用的动态分配内存空间,C语言中主要有malloc()、colloc()函数、free()和realloce()函数,当内存满时是由可能分配失败的
其中free函数就是释放分配的内存空间用的
1.malloc()函数
格式:void* malloc(size)
作用:malloc函数分配指定大小(size个字节)的内存空间,但是不会对分配的内存空间进行初始化,并返回指向该内存空间的通用指针。
int* p; p = (int *)malloc(100*sizeof(int));//分配了可以存放100个整数的内存空间,有可能有人会问sizeof(int)不就是4吗?你要分配整数,写4不就好了吗 //其实在我们编程习惯中,我们一般都习惯这样写,因为这样更明了你要分配一段整数空间 //注意:在执行赋值操作的时候,会把malloc()函数返回的通用指针 void* 自动转换为 int* ,但是在编程习惯中,也最好明确下,用强制类型转换,来明了你要分配的是什么类型的内存空间。
2.free()函数
格式:void free(void* p)
作用:释放之前由malloc或calloc函数分配的内存空间,指针p指向要释放的内存空间,即你要释放哪个指针所分配的内存空间,你就填这个指针进去即可
//如上例malloc所分配的p指针 free(p);//即释放完成
3.colloc()函数
格式:void* colloc(int 所分配的个数,sizeof(数据类型或数组名));
int* p; p = (int *)colloc(10,sizeof(int));//分配10个整型变量的空间
那么这些内存空间分配出来有什么用呢? 我们分配动态内存地址空间主要是应对数组中的内存地址可能有些用不到,存在浪费,所以只要输入你想要的大小,然后malloc或calloc一下,这样那个指针就可以当做一个数组来做,我们之前说过指针和数组的关系很密切。