06. C语言指针

news/2024/10/10 0:28:56


【指针】

C语言使用数据名调用数据,数据名相当于C语言的直接寻址,直接寻址只能调用固定数据,而指针是间接寻址,指针存储了另一个数据的地址,使用指针调用数据时首先取指针存储的内存地址,之后使用此地址调用数据,使用间接寻址有如下几点优势:
1.统一数据的调用方式,因为指针是调用数据的中间层,修改指针即可调用不同的数据,当代码需要在不同情况下调用不同数据时,可以使用指针统一调用,只需要在不同情况下修改指针为不同的内存地址即可,无需编写多组代码分别调用这些数据,当数据需要在多处代码中同时调用、并在不同情况下同时修改时,使用指针的优势更明显,修改了指针就等于修改了所有使用指针调用数据的代码。
2.突破编译器限制,使用指针调用数据时编译器不会进行限制,只要操作系统不禁止即可,可以通过此特性绕过编译器的某些限制,比如在一个函数内调用另一个函数的局部数据。
3.接收未知的内存地址,比如向操作系统申请内存,比如调用动态链接库成员。

指针的长度在不同计算机中是不同的,在32位处理器程序中指针是无符号4字节整数,在64位处理器程序中指针是无符号8字节整数。
依据指向数据的类型可以将指针分为多种,指针用于存储哪种类型数据的地址就属于哪种类型的指针,编译器通过指针类型确定要操作多少个内存单元,int类型指针操作4个内存单元。

#include <stdio.h>
int main()
{
    int a = 9;
    int * p1 = &a;    //int指定指针类型,*符号表示定义指针,p1为指针名,使用&符号提取一个数据的地址进行赋值,注意这里的&符号并非表示与运算
    
    printf("变量a的值为:%d\n", *p1);     //使用 *p1 调用指针指向的数据
    printf("变量a的地址为:%p\n", p1);    //使用 p1 调用指针本身
    
    p1 = 0;         //指针不再使用后修改为0,避免错误调用
    
    return 0;
}

使用指针可以随意调用数据,数据调用方式更灵活,灵活的代价是容易出错,使用指针时应该做到代码严谨,同时定义指针暂时不使用时应该将其赋值为0,防止直接使用未赋值的指针,若指针占用的内存原有数据可以当做合规的内存地址使用,将会使用一个未知的内存地址,另外指针不再使用后也应该修改为0。

多重指针

指针也可以存储另一个指针的地址,相当于多层间接寻址。

#include <stdio.h>
int main()
{
    int a = 5;
    int * p1 = &a;      //指针
    int ** p2 = &p1;    //指针的指针
    int *** p3 = &p2;   //三重指针,很少使用
    
    printf("p2存储的数据为:%p\n"
        "p2指向的数据为:%p\n"
        "p2最终指向的数据为:%d\n",
        p2, *p2, **p2);
    
    return 0;
}

指针变量、指针常量

1.指针变量,指针本身是变量,赋值后可以修改,可以存储变量、常量的地址。
2.指针常量,指针本身是常量,赋值后不能修改,可以存储变量、常量的地址。
3.变量指针,指向变量的指针,存储变量的地址,可以通过指针修改指向的数据,不能存储常量的地址,但编译器默认只是给一个警告,不会禁止。
4.常量指针,指向常量的指针,存储常量的地址,不能通过指针修改指向的数据,也可以存储变量的地址,此时指针将变量认为是常量,并且不能通过指针修改它,若一个指针即需要指向变量也需要指向常量、同时不需要修改指向的数据,可以将其定义为常量指针。

#include <stdio.h>
int main()
{
    int a = 0;
    const int b = 9;
    
    int * const p1 = &a;    //const在指针名之前,定义指针常量
    const int * p2 = &b;    //const在类型名之前(或类型名与*符号之间),定义常量指针
    
    const int * const p3 = &b;    //组合使用
    
    return 0;
}

指针作为数组元素

#include <stdio.h>
int main()
{
    int i1=1, i2=2, i3=3, i4=4, i5=5;
    int * p1[5] = {&i1, &i2, &i3, &i4, &i5};    //p1为存储指针的数组,元素为int类型指针
    printf("%d\n", *p1[0]);                     //调用数组第一个元素指向的数据
    
    char * p2[2] = {"阿狸", "喜羊羊"};     //可以使用数组为p2赋值,编译器自动取每个数组的地址作为p2元素的值
    printf("%s\n%s\n", p2[0], p2[1]);    //输出p2两个元素指向的字符串
    
    return 0;
}

 ● restrict关键词

定义指针时添加restrict关键词表示代码可以保证指针指向的数据不会通过其他指针或者数据名进行修改,只会通过此指针修改,让编译器放心进行高效优化。

int * restrict p1;

 


【数组指针】

数组指针表示存储数组地址的指针,数组第一个元素的地址就是数组的地址,可以使用数组首元素的指针表示数组指针。

#include <stdio.h>
int main()
{
    int a[5] = {1,2,3,4,5};
    int * p1 = &a[0];
    
    for(int i = 0; i < 5; i++)
    {
        printf("%d\n", *p1);
        p1++;                   //指针加1,指针记录的地址增加数据类型长度,这里的int类型指针会+4,定位到下一个元素
    }
    
    return 0;
}


数组指针可以使用下标的方式调用数组元素,原理与使用数组名相同,“指针地址+下标*数组元素长度”得出数组元素地址。

#include <stdio.h>
int main()
{
    /* 单层指针 */
    int a[5] = {1,2,3,4,5};
    int * p1 = &a[0];
    
    /* 遍历数组 */
    for(int i = 0; i < 5; i++)
    {
        printf("%d\n", p1[i]);    //将指针当做数组名使用,使用下标的方式调用数组元素
    }
    
    return 0;
}


数组指针还有另一种定义方式,它记录了数组长度信息,用于对指针赋值时进行检查,若赋值为长度不同的数组的地址则会发出警告,但默认不会禁止编译,这个长度信息只在编译器中记录,编译后的程序并没有记录此信息。

#include <stdio.h>
int main()
{
    int a[5] = {1,2,3,4,5};
    int (*p1)[5] = &a;         //p1为数组指针的名称,只能赋值为5个int类型元素数组的地址
    
    printf("数组第一个元素的值:%d\n", (*p1)[0]);    //调用指针指向的数据
    printf("数组第一个元素的地址:%p\n", p1);        //调用指针存储的地址
    
    return 0;
}

 


【结构体指针】

#include <stdio.h>
int main()
{
    struct zoo
    {
        char name[20];
        int age;
    } ali = {"阿狸", 8};
    
    struct zoo * p1 = &ali;    //定义结构体指针,只能赋值为同类型结构体实例的地址
    
    printf("%s:%d岁\n", p1->name, p1->age);        //使用->符号调用结构体成员
    printf("%s:%d岁\n", (*p1).name, (*p1).age);    //另一种使用方式
    
    return 0;
}

共用体指针使用方式与结构体指针相同,不再重复介绍。

结构体成员指针

#include <stdio.h>
int main()
{
    struct zoo
    {
        char name[20];
        int age;
    } ali = {"阿狸", 8};
    
    char * p1 = &ali.name[0];
    int * p2 = &ali.age;
    
    printf("%s:%d岁\n", p1, *p2);
    
    return 0;
}

 


【指针运算】

指针变量可以进行数学运算,不同类型的指针运算结果不同,运算规则如下:
1.单数据指针,指针+1,指针本身增加数据的长度,定位到下一个同类型数据,比如int类型指针+1等于指针值+4,减法同理。
2.数组指针,指针+1,指针本身增加数组元素的长度,定位到数组下一个元素。
3.结构体指针,指针+1,指针本身增加结构体的长度,注意结构体长度需要额外计入成员地址对齐占用的存储空间。
4.函数指针,不支持数学运算。

#include <stdio.h>
int main()
{
    int a[5] = {1,2,3,4,5};
    int * p1 = &a[0];
    
    /* 遍历数组 */
    for(int i = 0; i < 5; i++)
    {
        printf("%d\n", *p1);    //输出
        p1++;                   //每次循环指针+1,定位到下一个元素
    }
    
    return 0;
}

数组元素指针相减

指向同一个数组不同元素的指针之间可以进行减法运算,编译器会转换为计算两个指针指向元素的下标相减。

#include <stdio.h>
int main()
{int a[10] = {0,1,2,3,4,5,6,7,8,9};int * p1 = &a[5];int * p2 = &a[8];printf("%lu\n", p2 - p1);    //p2-p1 = 8-5,输出3return 0;
}

 


【指针类型转换】

指针类型可以使用代码转换,编译器使用转换后的类型操作内存地址、进行指针运算,转换语法如下:(类型名*)指针名。
指针类型转换只存在于转换运算式中,不影响指针原来的类型,若需永久转换类型可以使用另一个指针接收转换后的指针。

指针类型可以随意转换,编译器不做限制,若一个未知数据需要通过指针逐个操作其中每个字节,可以将指针转换为char类型,之后即可按字节读写其中的数据。

#include <stdio.h>
int main()
{
    int a = 257;
    int *p1 = &a;
    printf("%d\n", *(char*)p1);    //int指针转换为char类型,只调用第一个字节,257转二进制 = 0001 0000 0001,低8位为1,输出1
    
    return 0;
}

结构体指针类型转换

#include <stdio.h>
int main()
{
    struct k1
    {
        char c[4];
        //......
    } ali = {"ali"};
    
    struct k2
    {
        int i;
        //......
    };
    
    struct k1 * p1 = &ali;
    struct k2 * p2 = (struct k2 *)p1;    //p1转换为k2类型
    
    printf("0x%x\n", p2->i);
    
    return 0;
}

空类型指针

指针类型可以定义为void,表示类型为空,空类型指针可以存储任何类型数据的地址。
空类型指针不能直接使用,需要首先转换为具体的类型再使用,否则编译器无法确定要操作多少内存单元。

当你需要接收一个指针但又不确定它的类型时,可以定义一个空类型指针接收,之后再转换为具体类型使用。

#include <stdio.h>
int main()
{
    int a = 9;
    
    int * p1 = &a;
    void * p2 = p1;    //空类型指针
    
    //printf("%d\n", *p2);          //错误,不能使用
    printf("%d\n", *(int *)p2);     //正确,转换为int类型再使用
    
    return 0;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/27843.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

程序员天天 CURD,怎么才能成长,职业发展的思考(2)

接着上一篇:程序员天天 CURD,怎么才能成长,职业发展思考 上一篇写到了用年限来谈程序员的发展,在 4 - 6 年这个时间段需要做的一些事情,接着写这个时间段的。 第 4、5 年时候,你可能会做一些关于基层管理工作。这个时期会遇到一些困难。 这个时期,既要编写代码,又要做基…

git 服务端

1.安装gityum install -y git 2.查看版本git --version 3.创建用户useradd gitpasswd git 4.初始化仓库git init --bare /home/git/dataCollect.git 5.将拥有者改为gitgit init --bare /home/git/dataCollect.git

如何基于surging跨网关跨语言进行缓存降级

概述surging是一款开源的微服务引擎,包含了rpc服务治理,中间件,以及多种外部协议来解决各个行业的业务问题,在日益发展的今天,业务的需求也更加复杂,单一语言也未必能抗下所有,所以在多语言行业解决方案优势情况下,那么就需要多语言的协同研发,而对于协同研发环境下,…

AtCoder Grand Contest 001

D. Arrays and Palindrome 如果两个字符要求相同就给它们连边,对于一个长度为 \(x\) 的回文串,\(x\) 是偶数会连 \(x/2\) 条边,奇数会连 \(x/2 - 0.5\) 条边。 \(a\) 和 \(b\) 两个序列总和为 \(2n\),要让 \(n\) 个字符相同至少连 \(n - 1\) 条边,也就是奇数个数超过 \(2\…

AtCoder Beginner Contest 352题解

AtCoder Beginner Contest 352 Time : 2024-05-04(Sat) 20:00 - 2024-05-04(Sat) 21:40 A AtCoder Line 问题陈述 AtCoder 铁路线有 $N$ 个车站,编号为 $1, 2, \ldots, N$ 。 在这条线路上,有趟进站列车从 $1$ 站出发,依次停靠 $2, 3, \ldots, N$ 站,有趟出站列车从 $N$ 站…

windows安装ffmpeg

官网 https://ffmpeg.org/download.html这个是别人已经编译好的,不染源码还需要重新编译解压到一个目录,添加到环境变量

SpringBoot3.1.5对应新版本SpringCloud开发(2)-Eureka的负载均衡

Eureka的负载均衡 负载均衡原理负载均衡流程老版本流程介绍 当order-servic发起的请求进入Ribbon后会被LoadBalancerInterceptor负载均衡拦截器拦截,拦截器获取到请求中的服务名称,交给RibbonLoadBanlancerCient,然后RibbonLoadBanlancerCient会将服务名称当作服务id交给Dyn…

i-MES生产制造管理系统-设备点检

考虑到设备的分布区域比较分散,为了方便设备管理人员进行作业,设备点检模块通过安卓版的移动 PDA 完成,在此之前我们登录进入 MES 系统,创建点检项目,包括每一个点检项目的标准值以及上下限,如下图所示: 创建完点检项目之后,我们针对不同的设备类型,定义点检方案,在…