C语言的BOSS来了
为什么需要指针?
- 指针的使用使得不同区域的代码可以轻易的共享内存数据。当然你也可以通过数据的复制达到相同的效果,但是这样往往效率不太好,因为诸如结构体等大型数据,占用的字节数多,复制很消耗性能。但使用指针就可以很好的避免这个问题,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或者其他可能)
- 指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表、链式二叉树等。
- 有些操作必须使用指针。如操作申请的堆内存。还有:C语言中的一切函数调用中,值传递都是"按值传递"的,如果我们要在函数中修改被传递过来的对象,就必须通过这个对象的指针来完成。
指针的概念
指针是什么?
类比C语言中的数组,指针这个概念也泛指一类数据类型。任何程序数据载入内存后,在内部都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
因此:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。
c
int p;//p是一个普通的整型变量
int *p;//p是一个返回整型数据的指针
int p[3];//p是一个由整型数据组成的数组
int *p[3];//p是一个由返回整型数据的指针所组成的数组
int (*p)[3];//p是一个指向由整型数据组成的数组的指针
int **p;//二级指针
int p(int);//返回值为整型的函数(有一个整型变量的参数)
int (*p)(int);//p是一个指向有一个整型参数且返回类型为整型的函数的指针
int *(*p(int))[3];//p是一个参数为一个整型数据且返回一个指向由整型指针变量组成的数组的指针变量的函数
指针的类型:把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型
指针所指向的类型:把指针声明语句里的指针名字和名字左边的指针声明符'*'去掉,剩下的部分就是指针所指向的类型。
指针的值 (或者叫指针所指向的内存区或地址)
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的值都是一个32位整数,因为32位程序里内存地址全都是32位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。
运算符&和*
- &取地址
- *解引用
c
int a=10;
int *p=&a;
//定义整型指针变量p,初始化p的值为&a,p指向变量a
*p=30;//通过指针变量p引用a变量,改变a的值为30
指针的运算
c
char a[20];
int *ptr=(int*)a;//强制类型转换并不会改变a的类型
ptr++;
在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整型变量a.接下里的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4,因为在32位程序中,int占4个字节。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的4个字节,此时指向了数组a中从第4号单元开始的四个字节。
c
int array[20]={0};
int *ptr=array;
for(i=0;i<20;i++){
(*ptr)++;
ptr++;
}
此例将整型数组中各个单元的值加1.由于每次循环都将指针ptr加1个单元,所以每次都能访问数组的下一个单元。
c
char a[20]="You_are_a_girl";
int *ptr=(int*)a;
ptr+=5;
加5后,ptr已经指向数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。
两个指针不能进行加法运算,这是非法操作。
指针相减:指针减指针的绝对值指的是两个指针之间元素的个数。
前提:两个指针必须指向同一空间(指向同一个数组中的元素)
eg: &arr[9]-&arr[0]
指针的关系运算
指针于指针之间比较大小
只有当两个指针指向同一个数组中的元素时,才能进行关系运算
eg: 指针p和q指向同一数组中的元素
- p<q 当p所指的元素在q所指的元素之前时,为1,反之为0
- p>q 当p所指的元素在q所指的元素之后时,为1,反之为0
- p==q 所指元素相同为1,反之为0
- p!=q 所指元素不同为1,反之为0
编写程序将一个字符串反向输出
c
#include<stdio.h>
int main(){
char str[50],*p,*s,c;
printf("Enter string:");
gets(str);
p=s=str;//将指针初始化
while(*p){
p++;//将p指向字符串最后
}
p--;
while(s<p){
c=*s;
*s++=*p;
*p--=c;
}
puts(str);
return 0;
}
空指针
指向空,或者说不指向任何东西。在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是((void*)0),在C++中,NULL实质是0
换种说法:任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。
下面代码摘自stdlib.h
c
#ifdef _cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
void*类型指针
由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针作出正确的类型转换,然后再解指针。因为编译器不允许直接对void*类型的指针做解指针操作。
指针类型转换
强制类型转换:前面的例子涉及到了,略
那可不可以把一个整数当作指针的值直接赋给指针呢?可以
c
unsigned int a;
TYPE *ptr;//TYPE是int,char或结构类型等
a=N;//N必须代表一个合法的地址
ptr=(TYPE*)a;//这里的TYPE*是把无符号整数a的值当作一个地址来看待。
//相反的,把指针指向的地址即指针的值当作一个整数取出来也可以
int a=123,b;
int *ptr=&a;
char* str;
b=(int)ptr;
str=(char*)b;
结构体和指针
结构体指针变量
c
struct Student{
char* s_id;
char* s_name;
char* s_sex;
int* s_age;
};
结构体类型指针访问成员的获取和赋值形式:
- (*p).成员名
- p->成员名
实例:
c
#include <stdio.h>
struct Inventory{//商品
char description[20];//货物名
int quantity;//库存数据
};
int main ()
{
struct Inventory sta={"iphone",20};
struct Inventory* stp=&sta;
printf("%s %d\n",stp->description,stp->quantity);
printf("%s %d\n",(*stp).description,(*stp).quantity);
return 0;
}
数组和指针
指针数组:存放指针的数组(int* arr[]) 指针数组就是指针类型的数组
c
#include<stdio.h>
int main(){
int a=0;
int b=1;
int *p1=&a;
int *p2=&b;
int *arr1[]={p1,p2};//指针数组
int *arr2[]={&a,&b};//指针数组
return 0;
}
数组指针:指向数组的指针
c
//int(*)[]
//应用:遍历整个二维数组
#include<stdio.h>
void my_print(int(*p)[5],int x,int y){
int i=0;
for(i=0;i<x;i++){
int j=0;
for(j=0;j<y;j++){
printf("%d",*(*(p+i)+j));
//printf("%d",p[i][j]);
//p[n]等同于*(p+n)
//p[n][m]等同于(*(p+n)+m)
}
printf("\n");
}
}
int main(){
int arr1[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
my_print(arr1,3,5);
return 0;
}
- 当对数组名使用sizeof时,返回的是整个数组占用的内存字节数。
- 当把数组名赋值给一个指针后,再对指针使用sizeof时,返回的是指针的大小。
c
#include<stdio.h>
int main(){
int arr[3]={1,2,3};
int* p=arr;
printf("sizeof(arr)=%d\n",sizeof(arr));
printf("sizeof(p)=%d\n",sizeof(p));
return 0;
}
这就是为什么我们将一个数组传递给一个函数时,需要用另外一个参数传递数组元素个数的原因了。
函数和指针
c
typedef struct{
char name[31];
int age;
float score;
}Student;
void show(const Student *ps){
printf("name:%s,age:%d,score:%.2f\n",ps->name,ps->age,ps->score);
}
我们只是在show函数中读取Student变量的信息,而不会去修改它,为了防止意外修改,我们使用常量指针去约束。另外我们为什么使用指针而不是直接传递Student变量呢?
从定义的结构可以看出,Student变量的大小至少是39个字节,那么通过函数直接传递变量,实参赋值数据给形参需要拷贝至少39个字节的数据,极不高效。而传递变量的指针却快很多。
因为在同一个平台下,无论什么类型的指针,大小都是固定的:x86指针4字节,x64指针8字节,远远比一个Student结构体变量小。
函数指针数组
定义:存放函数指针类型元素的数组
c
//应用:实现计算器
#include<stdio.h>
int Add(int x,int y){
return x+y;
}
int Sub(int x,int y){
return x-y;
}
int Mul(int x,int y){
return x*y;
}
int Div(int x,int y){
return x/y;
}
void menu(){
printf("1.Add\n");
printf("2.Sub\n");
printf("3.Mul\n");
printf("4.Div\n");
printf("0.exit\n");
}
int main(){
menu();
int input=0;
printf("请选择:");
scanf("%d",&input);
int ret=0;
int(*pfarr[])(int,int)={0,Add,Sub,Mul,Div};
do{
if(input==0){
printf("退出\n");
break;
}
else if(input>=1&&input<=4){
int x=0;
int y=0;
printf("请输入两个操作数:");
scanf("%d%d",&x,&y);
ret=pfarr[input](x,y);
printf("结果是%d\n",ret);
break;
}
else{
printf("选择错误!");
}
}while(input);
return 0;
}
指向函数指针数组的指针
int(*(*parr)[4])(int)(int)=&parr
函数的指针
每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后。函数的机器指令存放在一个特定的逻辑区域:代码区。
既然是存放在内存中,那么函数也是有自己的指针的。
C语言中,函数名作为右值时,就是这个函数的指针。
c
#include<stdio.h>
void echo(const char* msg){
printf("%s",msg);
}
int main(){
void(*p)(const char*)=echo;//函数指针变量指向echo这个函数
p("Hello ");//通过函数的指针p调用函数,等价于echo("Hello ")
echo("World");
return 0;
}
const和指针
如果const后面是一个类型,则跳过最近的原子类型,修饰后面的数据。(原子类型是不可再分割的类型,如int,short,char以及typedef包装后的类型)
如果const后面就是一个数据,则直接修饰这个数据
c
#include<stdio.h>
int main(){
int a=1;
int const*p1=&a;//const后面是*p1,实质是数据a,则修饰*p1,通过p1不能修改a的值
const int*p2=&a;//const后面是int类型,则跳过int,修饰*p2,效果同上
int* const p3=NULL;//const后面是数据p3,也就是指针p3本身是const
const int* const p4=&a;//通过p4不能改变a的值,同时p4本身也是const
int const* const p5=&a;//效果同上
return 0;
}
typedef包装后的类型
c
#include<stdio.h>
typedef int* pint_t;
//将int*类型包装为pint_t,则pint_t现在是一个完整的原子类型
int main(){
int a=1;
const pint_t p1=&a;
//同样,const跳过类型pint_t,修饰p1,指针p1本身是const
pint_t const p2=&a;//const直接修饰p2,同上
return 0;
}
野指针
野指针:不正确,指向位置随机的指针
野指针的危害
- 指向不可访问的地址
危害:触发段错误。(所谓段错误,就是访问了不能访问的内存。 比如内存不存在,或者受保护等等) - 指向一个可用的,但是没有明确意义的空间
危害:程序可以正确运行,但通常这种情况下,我们就会认为我们的程序是正确的没有问题的,然而事实上就是有问题存在,所以这样就掩盖了我们程序上的错误。 - 指向一个可用的,而且正在被使用的空间
危害:如果我们对这样一个指针进行解引用,对其所指向的空间内容进行了修改,但是实际上这块空间正在被使用,那么这个时候变量的内容突然被改变,当然就会对程序的运行产生影响,因为我们所使用的变量已经不是我们所想要使用的那个值了。通常这样的程序都会崩溃,或者数据被损坏。
野指针的产生原因及解决方法
- 原因:指针变量声明时没有被初始化
解决:指针声明时初始化,可以是具体的地址值,也可让它指向NULL - 原因:指针p被free或者delete之后,没有置为NULL
解决:指针指向的内存空间被释放后,指针应该指向NULL - 原因:指针操作超越了变量的作用范围
解决:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL
如何规避野指针?
- 定义创建一个指针变量时一定要记得初始化
- 动态开辟的内存空间,free()释放内存后,一定要马上将对应的指针置为NULL空指针
- 不用在函数中返回栈空间的指针(地址)或局部变量的地址
- 注意在动态开辟内存后,对其返回值做合理判断,判断其是否为空指针
多重指针(多级指针)
指针变量也是有其对应地址的,那么既然有地址,就可以用另一个指针变量指向它的地址,也就是指向指针变量地址的指针,简称指向指针的指针(双重指针或二级指针)。而指向指针的指针也是有地址的,那又可以有指向其地址的指针,这就是多重指针了。
c
int a=111;//普通变量
int *p=&a;//普通指针(一级指针):指向普通变量的地址
int *p1=p;//同一级指针之间是相互赋值,而不是指向
int **q=&p;//二级指针(双重指针):指向一级指针的地址
int ***r=&q;//三级指针(三重指针):指向二级指针的地址
双重指针作为函数形参
一般来说函数的形参无法改变实参,除非形参是指针类型的。那么如果实参是一个指针,想要在一个函数中改变一个指针的指向应该怎么做?
例如:若定义了以下函数fun,如果p是该函数的形参,要求通过p把动态分配存储单元的地址传回主调函数,则形参p应当怎样正确定义?
形参指针p应该定义成二级指针,只有二级指针才能在函数中改变一级指针的指向。
c
void fun(int **p){
*p=(int*)malloc(10*sizeof(int));
}