前言
问周围学习C语言的人哪一块最难掌握,100个差不多也有98个会选择指针这一部分,毕竟还是要有2个比较Nb的大佬排除在外……
为什么指针就这么难?分析一下有下几点:
- 首先,不理解指针,认为知识点本身就难(畏难情绪)。
- 没有语言基础的人学完前面C的基本知识,到学习这一块的时候理解不了C的以内存为核心的机制(前面的基本知识不涉及到内存)。
- 计算机基本的知识不扎实,对计算机存储机制不了解(《计算机科学导论》登台)。
- 指针使用比较严格,不能出现指针悬空的问题,指针悬空之后一定不能进行写操作,不然……
- 指针涉及到的知识点很多,后续数据结构中都离不开指针。
分析这么多问题所在,其实指针难不难?真的不难,得到一个实验室学长的真传之后,发现指针也就那回事,毕竟这个学长是前面2个Nb人物中的一个。
个人学习指针知识点感觉最为重要的一点是:指针类型等同于int、char、数组等数据类型。在C语言—写在前面这篇文章中C知识点框架的图示中,数据类型这块可以看到,指针类型是一个数据类型,等同于其他的数据类型。
下面一点点进行总结,*,&,&(C++),数组与指针、函数与指针、结构体与指针。
知识点
- 指针的基本概念
- 数组与指针
- 函数与指针
- 结构体与指针
指针的基本概念
指针的本质
先贴一张我课堂老师的PPT
指针类型的本质,就是存放内存的地址信息
一个变量有那三个要素?
变量类型变量名:int a;
变量的值:a = 10;
变量在内存中存储的地址:0x6ffe4c
这里定义一个指针类型的变量int *p,p = &a,p的值便是0x6ffe4c,&a可以理解为一个运算操作,取地址运算,返回的结果是一个地址。然后再用这个地址给指针类型的变量进行赋值。
再回头看一个变量包括的三个要素分别是:int a;10(a的值);0x6ffe4c(&a,a的地址)。核心是&a,没有在内存中分配给变量a存储空间,也就是没有int类型的变量a的存在,所有都建立在&a的基础上。
其实int *p; p代表的就是一个地址值,和int a;a代表的是一个整形的常量,一样的道理。
运算符‘*’、‘&’、‘&’(C++)
这一块知识点比较关键、重要,但是很好理解,很简单。
首先,’*’运算符,取值运算符,表示的是取值运算,拿上面的变量进行解释,p=0x6ffe4c,*p 则是这个内存地址上存储的数值,那便是10;也就是*p的值是10;
‘&’运算符,取址运算符,表示的是取地址运算,那上面的进行解释,p=0x6ffe4c,0x6ffe4c这个地址怎么得到的,&a,对int类型的变量a进行取地址运算,便可以得到这个变量的地址这个要素,也就是0x6ffe4c,也就是&a的地址值,也就是赋值给指针类型的变量 p 的地址值。
‘&’(C++)中 & 符这里表示的不是取地址,而是引用,‘引用’不是很好理解,看例子:
int a = 10;int &ra = a;
这里‘&ra’表示引用,引用什么作用?其实很简单就是a的别名,还拿前面的变量来说明,a的类型是int类型,a的值是10,变量在内存的地址是0x6ffe4c,对于ra来说,ra的类型也是int类型,ra的值也是10,变量在内存的地址也是0x6ffe4c。简简单单的一个别名而已,就像玩LOL的都知道给VN贴上标签的选手:简自豪、别名:小狗、uzi。大家都喊他小狗、uzi,实际上小狗就是简自豪。&(C++)引用在一些代码中可以取代指针,可以达到可指针相同的功能。引用与指针的比较(转载):
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给 形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效 率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用”*指针变量名”的 形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。引用的总结(转载):
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
(4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。前面的能完全理解,指针这块部分基本上理解已经有一半了,后面介绍的一些指针与其他方面的知识结合全部都是建立在这个基础之上的。
XXX与指针
这个部分主要是通过程序来进行总结的,毕竟这是一门动手科目。
指针运算符
‘*’、‘&’、‘&’(c++)使用代码:
#include<stdio.h>
//C++的输入输出流头文件
#include<iostream>
using namespace std;
int main(){
int a = 10;
int &ra = a;
int *p = &a;
//C++中的输出流
cout<<"ra:"<<ra<<"\tra内存地址:"<<&ra<<endl;
cout<<"a:"<<a<<"\t a内存地址:"<<&a<<endl;
//0x6ffe4c 0x表示16进制
printf("a的地址:0x%x\na的值:%d",p,*p);
return 0;
}
运行后的截图:
数组与指针
一维数组、字符串与指针代码:
#include<stdio.h>
#include<string.h>
int main(){
//定义指针变量 *p,*pc,*s
int *p;
int a[10];
char *pc,*s;
char str[20];
//读入一维数组a的数据
for(int i=0;i<10;i++)
scanf("%d",&a[i]);
//清楚输入缓存 方便gets输入
rewind(stdin);
gets(str);
//指针初始化 给指针赋值 p表示的是一个数组的首地址 这里数组名 a 是这个数组的首地址
p = a;
//字符串 数组名 str 表示的是字符串的首地址 可以直接赋值给 char类型的指针 pc
pc = str;
//char类型的指针 操作和字符串相同 可以直接赋值对其完成初始化
s = "I love C!";
//对指针的操作 +- 一个常数n,实际上指针地址变化了 n*sizeof(ElemType)
for(int i=0;i<10;i++)
//这里 p 和 a 一样,运行截图 理解一下
printf("%4d%4d%4d%4d\n",p[i],a[i],*(p+i),*(a+i));
//输出字符串
puts(pc);
//char 指针类型的变量 可以做char类型的字符串进行处理
for(int i=0;i<strlen(s);i++)
printf("%c ",s[i]);
return 0;
}
运行截图:
二维数组与指针的代码:
#include<stdio.h>
int main(){
int be[4][3] = {{1,2,3},{4,5,6},{7,8,9},{10,11,12}};
//定义一个行指针 该指针指向一行的元素
int (*p)[3];
//定义一个指针
int *pb;
//初始化指针类型的变量 pb 和行指针 p
pb = &be[0][0];
p = be;
printf("普通指针输出二维数组:\n");
for(int i=0;i<12;i++)
printf("%d ",*(pb+i));
printf("\n\n按照顺序表的规则 用行指针进行输出二维数组:\n");
for(int i =0;i<12;i++){
printf("%d\t",*(*p+i));
if((i+1)%3==0)
printf("\n");
}
printf("\n行指针输出二维数组:\n");
for(int i=0;i<4;i++){
for(int j=0;j<3;j++)
printf("%d\t",*(*(p+i)+j));
printf("\n");
}
printf("\n输出二维数组每个元素在内存中的地址:\n");
for(int i=0;i<4;i++){
for(int j=0;j<3;j++){
printf("0x%x\t",*(p+i)+j);
}
printf("\n");
}
printf("\n按照顺序表的规则 输出二维数组每个元素在内存中的地址:\n");
for(int i =0;i<12;i++){
printf("0x%x\t",*p+i);
if((i+1)%3==0)
printf("\n");
}
printf("\n输出二维数组每个行指针的地址值:\n");
for(int i=0;i<4;i++)
printf("0x%x\t",p+i);
printf("\n\n输出二维数组每个行指针的地址值:\n");
for(int i=0;i<4;i++)
printf("0x%x\t",*(p+i));
printf("\n\n按照顺序表的规则 输出二维数组每个行指针的地址值:\n");
for(int i =0;i<12;i++)
if(i%3==0)
printf("0x%x\t",*p+i);
printf("\n\n输出二维数组每个行指针的地址上存储的数值:\n");
for(int i=0;i<4;i++)
printf("%d\t",*(*(p+i)));
return 0;
}
程序运行截图:
理解二维数组与指针的关系,这里有篇文章理解的比较好,可以借鉴进行理解。核心还是根据数据在内存中的存储方式是顺序存储的方式,一维数组中,一个指针可以代表,一个地址单位,+-n(n=1),移动的是一个地址单位,二维数组中,一个行指针,代表的是一行元素存储在内存中的首地址,+-n(n=1),移动的大小是一行所有元素的地址单位,到下一行的首地址。
这里有一张我理解内存地址与数值图:
重点需要理解(m行数、n列数):
关于内存:p+i、* (p+i)、(* p +n); (* p+i)、* (p+i)+j;
关于值:* * (p+i),* (* p+n),* (* (p+i)+j);
程序和运行截图里面均有涉及,可以根据代码提示和注释进行详细理解。
函数与指针
- 指针作为函数的参数
- 返回值为指针的函数
- 函数指针
指针作为函数的参数代码:
#include<stdio.h>
//传数值 交换数值
void swap(int a,int b){
int t;
t = a;a = b;b = t;
}
//传地址 交换数值
void swapp(int *a,int *b){
int t;
t = *a;*a = *b;*b = t;
}
//传地址 交换地址
void swapP(int *a,int *b){
int *t;
t = a;a = b;b = t;
}
//引用作为参数
void swapY(int &a,int &b){
int t;
t = a;a = b;b = t;
}
int main(){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
printf("原始数据值:a=%d\tb=%d\n",a,b);
printf("原始数据值地址:a=0x%x\tb=0x%x\n\n",&a,&b);
//传数值 交换数值
swap(a,b);
printf("传值值交换:a=%d\tb=%d\n",a,b);
printf("传值值交换地址:a=0x%x\tb=0x%x\n\n",&a,&b);
//传地址 交换数值
swapp(&a,&b);
printf("传址值交换:a=%d\tb=%d\n",a,b);
printf("传址值交换地址:a=0x%x\tb=0x%x\n\n",&a,&b);
//传地址 交换地址
swapP(&c,&d);
printf("传址址交换:c=%d\td=%d\n",c,d);
printf("传址址交换地址:c=0x%x\td=0x%x\n\n",&c,&d);
//引用作为参数
swapY(c,d);
printf("引用为参交换:c=%d\td=%d\n",c,d);
printf("引用为参交换地址:c=0x%x\td=0x%x\n\n",&c,&d);
return 0;
}
运行截图:
这里遗留一个问题?传地址交换地址,是可以实现数值上的交换,这里怎么改写才能完成交换数值的作用
引用的部分,参考前面介绍指针运算符部分,很详细。
返回值为指针的函数代码:
#include<stdio.h>
//求一个数组中最大数 用返回指针来实现
int *Maxvalue(int a[],int n){
int max = a[0];
for(int i=0;i<n;i++)
max = max>a[i]?max:a[i];
//返回的类型是指针类型 地址
return &max;
}
int main(){
int a[10];
//定义一指针 存放数组中最大值 所在的地址
int *pMax;
for(int i=0;i<10;i++)
scanf("%d",&a[i]);
//接受的类型也是指针类型 返回最大值的地址 保存pMax中
pMax = Maxvalue(a,10);
printf("%d\n",*pMax);
return 0;
}
运行截图:
函数指针:
#include<stdio.h>
//函数指针用的较少 编程中很少会用到 比较麻烦
//函数指针调用函数求解两个数的和
int add(int a,int b){
return a+b;
}
int main(){
//定义函数指针p 参数个数和形参类型
//可以结合二维数组的定义 来进行理解这个定义方式
//后面的结构体 理解的方式也是相同
int (*p)(int,int);
//给指针进行初始化
p = add;
int a,b,sum;
scanf("%d%d",&a,&b);
//通过函数指针调用函数 进行运算 返回数值赋值给sum
sum = (*p)(a,b);
printf("sum=%d",sum);
return 0;
}
运行截图:
理解:函数在内存中也有分配给其的地址,没有内存地址的分配,任何变量、函数……都不存在,指向函数的指针便是存放的是某个函数的首地址,只要拿到函数的首地址,将函数的地址赋给函数指针,则可以通过函数指针进行间接的调用函数。
结构体与指针:
这一部分的内容,在后面的数据结构有关的部分,会有详细的总结,这里不做总结。
结语
总感觉这博文怎么这么难写完,撒了狗粮之后才补完……
文章的内容除了标明转载和连接的部分,均是博主学习的总结,给学习C语言指针的同学一交流学习的机会,如果有理解不到或者理解错误的地方请指出,遇到问题可以留言方便交流学习。