【C语言】指针

一篇带你把指针融入血液里的爽文!

引入

  1. 存储程序数据

    指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

  2. 共享内存数据

    有些数据通过复制方法被共享时效率不太好,诸如结构体等大型数据,占用的字节数太多,复制很消耗性能。但使用指针可以使得不同区域(C语言内存区)的代码可以轻易的共享内存数据,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或其他可能)。

  3. 构建数据结构

    指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表,链式二叉树。

  4. 必要性使用

    如操作申请的堆内存,如C语言中的一切函数调用中的引用调用,区别于传值调用,可以实际改变参数的值。

概念

指针是数据类型中派生类型中的一种。任何程序数据载入内存后,在内部都有他们的地址,这就是指针。

int p;//p是一个普通的整型变量
int* p;//p是一个整型变量的地址
int* p=arr/&arr[0];//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是一个参数为一个整型数据且返回一个指向由整型指针变量组成的数组的指针变量的函数

类型系统

  1. 放哪里
  2. 占多大空间

内存

简单画法

图解——“往格子3放个数字29”

变量

  • 贴标签,放数据(定义,赋值)
  • 通过标签找数据(取值)

图解——“往两个格子(格子8和格子9)放个名称为c的数字999”

上图可表示为如下指令:

char a=29;
char b=38;
short c=999;
char age=18;
int salary=2147483647;

取地址

存放一个特殊变量p(指针变量)

先找位置,再找位置上的值

图解——“将格子6(包含格子6里存放的数据)放入格子1中”

通过特殊变量p所在的地址(即格子1)找到其里面存的值(即a的内存地址6),再通过a的内存地址找到里面存的值(即1234).

short a=1234;
int p=&a;//p=6

默认的p!

在编码阶段无法确定一个变量的内存地址是多少,即无法确定用什么类型的变量来存放它。 固定指针变量所占用的内存大小:4字节,32位系统。(能容纳所有内存地址范围的变量类型)。注意:在64位系统里,是8字节即sizeof(指针)=8

类型

指针的类型

​ 指针的类型一般和指针所指向的类型对应。

指针所指向的类型

放了什么玩意(存放的 内存地址处的变量 的大小,即变量a的类型)

变量p是个short*类型的指针

short a=1234;
short* p=&a;
//p前面的*表示变量p是一个指针类型
//再前面的short表示该指针指向的内存地址处的变量是个shot类型的变量

更准确的说法是:指针p会按照short类型的变量来读取它指向的内存(地址6处的数值)

两者相匹配就是正确的编程代码,我们不妨更改成其他类型的:

char类型:假如,我们认为内存地址6处的变量是个char类型,也就是将数字1234放入1字节中,会造成数据溢出

int类型:如果认为6处的变量是个int类型,即将数字1234放入4字节中,数值上没有问题(能放得下),但是某种程度上不符合预测:如果多余的格子8和格子9里有其他内容,那就更不符合预期了。(在计算机内存中,整数一律采用补码的形式来存储)

这一切要从数据在内存中的存储说起。

所以,这里的“正确”是说给程序员听的,不是说给CPU听的哦~

初始化

&a获取a的地址

short* p;定义一个short*类型的指针变量

p=&a;给指针变量进行初始化

short* p=&a;定义一个 short*类型的指针变量,指向地址a

所以,连起来一个完整的程序就是:

short a=1234;
short* p;//指针的定义
p=&a;//指针的初始化,也即指针变量本身的值
*p=999;//指针变量所指向的内存地址的值

*就表示“指向”的意思

运算

指针的值

有一种错误说法:指针变量中只能存放地址,不要将一个整数或是任何其他非地址类型的数据赋给一个指针变量,我们来研究一下指针的本质~

实际上,指针变量的本质和普通变量是一样的:

编译器只负责听程序员的话一个格子一个格子的放数字

普通变量short a:当我a=1时,你给我找到一块2字节的内存,把1放进去

指针变量short* p:1.当我p=xxx时,你给我找到一块4字节的内存(上面提到p默认为int型),把xxx填充进去(这就和普通变量一样)2.当我*p=yyy时,你给我找到xxx的内存地址,并且按照short类型(也就是2字节大小),把yyy填充到这里。

说简单点就是,我把一个整型变量xxx赋值给指针,然后用它的时候*p把xxx看作是一个内存地址,就去找内存xxx的地方。

用代码表示:

int* p=6;//我强行把一个整型数值6赋值给指针变量p
*p=999//*p去访问内存地址6并修改那个地方的值为999

我还可以把一个地址值,强行赋值给一个普通变量:

int a=1;
int b=&a;

这时普通变量b里面存储着a的地址,我*b页同样可以访问到a并修改他的值:

*b=999;

当然这么写程序会报错,为了让b读取到地址a的值,我们要进行类型转换:

*(int*)b=999;

运算符

  • &取地址
  • *解引用

指针的加减

普通变量的加减

int a=1;//定义一个普通变量
int b=a+1;//普通变量+1

毫无疑问,b的值是2

但是对于一个指针变量+1,会怎么样呢?

int a=1;
int* p=&a;
int* p2=p+1;

我们假设变量a放在了格子1处,即p指向格子1

下图直接表示p所指向的内存地址,即*p

p+1=5,即跨过一个p所指向的内存单元的数据类型的大小,也就是4字节的int.

显然,指针是地址上的加减,不是数值上的加减。那么,我们如何实现和普通变量一样的数值上的加减呢?

第一步:把int*类型的p强转为char*类型的p

第二步:p+1

第三步:再把char*类型的p强转为int*类型

用代码表示就是:

p=(*int)((*char)p+1);
  • 两个指针不能进行加法运算,这是非法操作
  • 指针相减:指针减指针的绝对值指的是两个指针之间元素的个数(前提是两个指针指向同一个数组中的元素)

?试编写程序循环访问int类型数组的每一个单元

指针的关系运算

指针与指针之间比较大小

!只有当两个指针指向同一个数组中的元素时,才能进行关系运算。

eg.指针p和指针q指向同一数组中的元素(bool函数)

  • p>q 当p所指的元素在q所指的元素之前时,为1,反之为0
  • p<q 当p所指的元素在q所指的元素之后时,为1,反之为0
  • p==q 所指元素相同为1,反之为0
  • p!=q所指元素不同为1,反之为0

?试编写程序将一个字符串反向输出

指针关系

结构体和指针

结构体指针变量

struct Student{
    char* s_id;
    char* s_name;
    char* s_sex;
    char* s_age;
}

结构体类型指针访问成员的获取和赋值形式:

  1. (*p).成员名
  2. p->成员名
#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[ ]),指针数组就是指针类型的数组

#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;
}

数组指针

指向数组的指针

定义一个数组指针

int arr[]={1,2,3,4,5};
int* p=arr;//arr被转换成了一个指针,可以直接赋值给指针变量

arr是数组第0个元素的地址,所以int* p=arr也可写成int* p=&arr[0],他们都指向数组第0个元素或者说指向数组开头。

定义一个数组指针并输出(根据指针的运算)

#include<stdio.h>
int main(){
    int arr[]={1,2,3,4,5};
    int* p=&arr[2];//根据指针的运算也可以写作 int* p=arr+2
    printf("%d %d %d %d %d",*(p-2),*(p-1),*p,*(p+1),*(p+2));
    return 0;
}
//运行结果:1 2 3 4 5

访问数组的元素

  1. 下标访问

    a[2]

  2. 指针访问

    在定义数组指针int* p=arr后,上面的a[2]等价于*(p+2)

    假设p是指向数组arr中的第n个元素的指针,那么*p++,*++p,(*p)++分别是什么意思呢?

    参见a++与++a的区别(逻辑运算符)

    *p++等价于*(p++),表示先取得第n个元素的值,再将p指向下一个元素

    *++p等价于*(++p),会先进行++p运算,使得p的值增加,指向下一个元素,整体上相当于*(p+1),所以会获得第n+1个数组元素的值

    (*p)++先取得第n个元素的值,再对该元素的值加1.假设p指向第0个元素,并且第0个元素的值为1,执行完该语句后,第0个元素的值就会变为2

取值指向
a++n
++an+1
*p++nn+1
*++pn+1n+1
(*p)++nn

应用

  • 数组指针可以作为函数参数传递,以对数组进行操作或传递多维数组。
  • 在函数定义中,可以使用数组指针来表示参数的类型
//遍历整个二维数组
#include<stdio.h>
void my_printf(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_printf(arr1,3,5);
    return 0;
}

sizeof和指针

参考sizeof的用法总结

  • 当对数组名使用sizeof时,返回的是整个数组占用的内存字节数

  • 把数组名赋值给一个指针后,再对指针使用sizeof时,返回的是指针的大小

    所以将一个数组传递给另一个函数时,需要用另外一个参数传递数组元素的个数。

    #include<stdio.h>
    int main(){
        int a=10;
        int* i=&a;
        double f=95.0629;
        double* q=&f;
        int arr[3]={1,2,3};
        int* p=arr;
        printf("sizeof(a)=%d\n",sizeof(a));
        printf("sizeof(i)=%d\n",sizeof(i));
        printf("sizeof(f)=%d\n",sizeof(f));
        printf("sizeof(q)=%d\n",sizeof(q));
        printf("sizeof(arr)=%d\n",sizeof(arr));
        printf("sizeof(p)=%d\n",sizeof(p));
        return 0;
    }
    //sizeof(a)=4
    //sizeof(i)=8
    //sizeof(f)=8
    //sizeof(q)=8
    //sizeof(arr)=12
    //sizeof(p)=8
    

函数和指针

函数参数:指针还有一个作用是在函数参数中作引用调用

函数指针数组

存放函数指针类型元素的数组

//应用:实现计算器
#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语言中,函数名作为右值时,就是这个函数的指针。

#include<stdio.h>
void echo(const char* msg){
    printf("%d",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后面就是一个数据,则直接修饰这个数据

#include<stdio.h>
int main(){
    int a=1;
    int const*p1=&a;//直接修饰p1:const后面是*p1,实质是数据a,则修饰*p1,通过p1不能修改a的值
    const int*p2=&a;//跳过int: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包装后的类型

typedef是为现有的类型起的一个别名,使使用起来方便,它并没有产生新的类型。

#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}

特殊指针

空指针

指向空,或者说不指向任何东西。在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是((void*)0)(将0地址强制转换成void类型的函数指针),在C++中,NULL的实质是0。

换种说法:任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块

void类型的指针

由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道它指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针,因为

编译器不允许直接对void*类型的指针做解指针操作。

野指针

不正确,指向位置随机的指针

危害

  1. 指向不可访问的地址

    危害:触发段错误。(所谓段错误,就是访问了不能访问的内存。比如内存不存在,或者受保护等)

  2. 指向一个可用的,但是没有明确意义的空间

    危害:程序可以正常运行,但通常在这种情况下,我们就会认为我们的程序是正确的没有问题的,然而事实上就是有问题存在,所以这样就掩盖了程序上的错误

  3. 指向一个可用的,而且正在被使用的空间

    危害:如果我们对这样一个指针进行解引用,对其所指向的空间内容进行了修改,但是实际上这块空间正在被使用,那么这个时候变量的内容突然被改变,当然就会对程序的运行产生影响,因为我们所使用的变量已经不再是我们想要使用的那个值了。通常这样的程序都会崩溃,或者数据被损坏

产生及解决

  1. 原因:指针变量声明时没有被初始化

    解决:指针声明时初始化,可以是具体的地址值,也可以让它指向NULL

  2. 原因:指针p被free或者delete之后,没有设置为NULL

    解决:指针指向的内存空间被释放后,指针应该指向NULL

  3. 原因:指针操作超越了变量的作用范围

    解决:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL

规避

  1. 定义创建一个指针变量时一定要记得初始化
  2. 动态开辟的空间,free()释放内存后,一定要马上将对应的指针置为NULL空指针
  3. 不用在函数中返回栈空间的指针(地址)或局部变量的地址
  4. 注意在动态开辟内存后,对其返回值做合理判断,判断其是否为空指针

多重指针

即多级指针,指针变量也是有其对应地址的,那么既然有地址,就可以用另一个指针变量指向他的地址,也就是指向指针变量地址的指针,简称指向指针的指针(双重指针或二级指针)。而指向指针的指针也是有地址的,那么可以有其指向其地址的指针,这就是多重指针了。

int a=111;//普通变量
int* p=&a;//普通指针(一级指针):指向普通变量的地址
int* p1=p;//同一级指针之间是相互赋值,而不是指向
int** q=&p;//二级指针(双重指针):指向一级指针的地址
int*** r=&q;//三级指针(三重指针):指向二级指针的地址

双重指针作为函数形参

一般来说函数的形参无法改变实参,除非形参是指针类型的(本文引入部分提到指针的必要性使用)。那么进一级说,如果实参是一个指针,想要在一个函数中改变一个指针的指向,形参指针p应该定义成二级指针。

例如:若定义了以下函数fun,如果p是该函数的形参,要求通过p把动态分配存储单元的地址传回主调函数

void fun(int** p){
 *p=(int*)malloc(10*sizeof(int));
}

当然,把指针融入血液里主要靠你自己!快去敲代码吧~

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值