【c语言】理解指针改值、指针运算和指针强转

目录

1.指针含义和通过指针改值

1.1指针改值

1.2指针变量本身和其值对应的变量

1.3指针改值的本质原理

1.4指针改值时类型不一致会报错

1.5 c语言可用指针间接的修改const常量的值

2指针类型强行转化

2.1类型强转查看每一个地址值

2.2类型强转查看是大端对齐还是小端对齐

2.3类型强转查看结构体在内存的存储方式

3指针运算

3.1指针运算的含义

3.2通过指针运算改变数组元素的值

3.3通过指针运算实现字符串切片

3.4指针类型不同时进行值运算

4.指针强转注意事项

4.1有符号存储和无符号读取带来不同结果

4.2浮点数指针强转整数指针带来的改变

4.3数据类型占用内存长度不一样时的强转

4.4 int内存的本质


1.指针含义和通过指针改值

1.1指针改值

  • 指针也是一种变量
  • 指针的值是一个地址,该地址指向一块内存空间,指针变量的值就是指向的那个内存地址编号
  • 指针是一种数据类型,如果是指向一个int变量,则该指针类型为指向int的指针,即int *,如果是指向一个char变量,则该指针类型为指向char的指针,即char *
  • &可以取的一个变量在内存中的地址。比如a是一个int变量,则a在内存中占用4个BYTE,对应个内存地址,则&a为第一个内存地址的地址编号
  • *符号可以取某个指针所指向的那块内存地址存储的值,修改值时是根据指针类型改值:
  • printf函数中用%p输出指针的值
#include <stdio.h>

int main()
{
    int a;   // a为整数变量,类型为int
    int *p;  // p为指针变量,类型为int *

    a = 1;
    p = &a;

    // 查看内存地址和指针变量的值
    printf("%p, %p\n", &a, p);  
    // 000000000061FE14, 000000000061FE14  
    // int占用了4个BYTE,这里输出的是第1个地址
    
    // 通过指针修改变量的值
    printf("%d\n", *p); // 1
    *p = 10;
    printf("%d\n", *p); // 10

    // p指向另一个变量的地址
    int b = 100;
    p = &b;
    printf("%d\n", *p); // 100

    return 0;
}

1.2指针变量本身和其值对应的变量

#include <stdio.h>

typedef struct student
{
    int age;
} stu;

int main()
{
    stu s1;
    s1.age = 20;

    stu *p = &s1;

    // p的值是一个内存地址
    printf("%p\n", p);   // 0x7ffca64b2014


    // *p表示将该内存地址存储的变量取出来
    printf("%d\n", (*p).age);  // 20

    return 0;
}

1.3指针改值的本质原理

*符号可以取某个指针所指向的那块内存地址存储的值,修改值时是根据指针类型改值:

  • 比如p为int *的指针变量,int占用4个字节,*p = 1表示把指向的那个地址及其后面3个地址,即总共4个连续地址所对应的数值改成1,即把1变成4个BYTE再放到这4个地址。
  • 再比如p为char *的指针变量,char占用1个字节,*p = 1表示把指向的那个地址对应的数值改成1,即把1变成1个BYTE再放到这个地址。
  • 再比如p为stu *的指针变量,其中stu为自定义的一个结构体类型,student1和student2为实例化的结构体,p = &student1,虽然student1占用20个字节,但p的值是第一个地址值,如果进行*p = student2操作,则表示把student2在内存中对应的20个BYTE的值覆盖掉p指向的那个地址及其后面总共20个BYTE的值
#include <stdio.h>

struct stu
{
    char name[8];
    int age;
    int grade;
    char sex;
};

int main()
{
    printf("%d\n", sizeof(stu)); // 20

    stu student1 = {"tom", 15, 80, 'm'};
    stu *p = &student1;
    printf("name=%s, age=%d, grade=%d, sex=%c\n", p->name, p->age, p->grade, p->sex);
    // name=bob, age=15, grade=90, sex=m

    stu student2 = {"bob", 15, 90, 'm'};
    *p = student2;  // 等价于 p = &student2;
    printf("name=%s, age=%d, grade=%d, sex=%c\n", p->name, p->age, p->grade, p->sex);
    // name=bob, age=15, grade=90, sex=m

    return 0;
}

1.4指针改值时类型不一致会报错

#include <stdio.h>

struct stu
{
    char name[8];
    int age;
    int grade;
    char sex;
};

int main()
{
    printf("%d\n", sizeof(stu)); // 20

    {
        //注意*p = student2本质是一种赋值操作,两边类型不一致时会报错
        stu *p1;
        int x = 10;
        //p1 = &x;  // 在c++中会报错,因为p1是 stu *,而&x是int *
        //*p1 = x;    // 在c++中会报错,因为*p1是 stu,而&x是int
    }
    
    return 0;
}

1.5 c语言可用指针间接的修改const常量的值

c语言中的const是有问题的,因为可以通过指针变量间接的修改const常量的值,所以在c语言中用#define常量的时候更多

#include <stdio.h>

int main()
{
    const int x = 10;
    int *p1 = &x;
    // *p1 = 20;  // 会error
    int *p2 = p1;
    *p2 = 20;
    printf("%d\n", x);  // 20

    return 0;
}

2指针类型强行转化

2.1类型强转查看每一个地址值

#include <stdio.h>

int main()
{
    int x = 10;
    int *p1;
    
    // 查看地址编号(第一个BYTE对应的编号值)
    p1 = &x;
    printf("%p,%p\n", p1, &x);   // 000000000061FE0C,000000000061FE0C

    // 类型强转
    char *p2;
    // p2 = p1;  // c++中会报错,因为赋值操作,左边是char * ,右边是int *
    p2 = (char *)p1; // 右边类型强制转化为char *
    printf("%p,%p,%p\n",p1, &x, p2); 
    // 000000000061FE0C,000000000061FE0C,000000000061FE0C

    // 查看这4个BYTE对应的地址编号
    printf("%p\n", p2);     // 000000000061FE0C
    printf("%p\n", p2+1);   // 000000000061FE0D
    printf("%p\n", p2+2);   // 000000000061FE0E
    printf("%p\n", p2+3);   // 000000000061FE0F

    return 0;
}

2.2类型强转查看是大端对齐还是小端对齐

结论:对于数而言是小端对齐,对于字符串而言是大端对齐

(1)windows下数的结果是小端对齐

#include <stdio.h>

int main()
{
    int x = 10;
    int *p1;
    
    // 查看地址编号(第一个BYTE对应的编号值)
    p1 = &x;
    printf("%p,%p\n", p1, &x);   // 000000000061FE0C,000000000061FE0C

    // 类型强转
    char *p2;
    p2 = (char *)p1; // 右边类型强制转化为char *

    // 查看这4个BYTE对应的地址编号
    printf("%p, %x\n", p2, *(p2));       // 000000000061FE0C, a
    printf("%p, %x\n", p2+1, *(p2+1));   // 000000000061FE0D, 0
    printf("%p, %x\n", p2+2, *(p2+2));   // 000000000061FE0E, 0
    printf("%p, %x\n", p2+3, *(p2+3));   // 000000000061FE0F, 0

    // 可以看到低地址存放的是数的低位,为小端对齐

    return 0;
}

(2)linux下数的结果也是小端对齐

[root@localhost test]# cat main.c
#include <stdio.h>

int main()
{
        int x = 10;
        char *p = (char *)&x;

        printf("%p, %x\n", p, *p);
        printf("%p, %x\n", p+1, *(p+1));
        printf("%p, %x\n", p+2, *(p+2));
        printf("%p, %x\n", p+3, *(p+3));

        return 0;
}
[root@localhost test]# gcc main.c -o main
[root@localhost test]# ./main
0x7ffd88297e94, a
0x7ffd88297e95, 0
0x7ffd88297e96, 0
0x7ffd88297e97, 0
[root@localhost test]#

(3)字符串存储是大端对齐

字符串存储是大端对齐,也就是直接将字符串的值放上去

#include <stdio.h>

int main()
{
    char name[4] = "tom";
    char *p = name;
    printf("%p, %c\n", p, *(p));       // 000000000061FE1C, t
    printf("%p, %c\n", p+1, *(p+1));   // 000000000061FE1D, o
    printf("%p, %c\n", p+2, *(p+2));   // 000000000061FE1E, m
    printf("%p, %c\n", p+3, *(p+3));   // 000000000061FE1F, 

    // 可以看到低地址存放的是字符串的左边,为大端对齐

    return 0;
}

2.3类型强转查看结构体在内存的存储方式

#include <stdio.h>

struct stu
{
    char name[5];
    int age;
    char sex;
};

int main()
{
    // 1.查看结构占用的内存大小
    printf("%d\n", sizeof(stu)); // 12

    stu student1 = {"tom", 15, 'm'};
    stu *p1 = &student1;
    printf("name=%s, age=%d, sex=%c\n", p1->name, p1->age, p1->sex);
    // name=tom, age=15, sex=m

    char x = 10;
    //p1 = &x;  // 赋值操作,在c++会报错,因为p1是 stu *,而&x是char *
    // *p1 = x;    // 赋值操作,在c++会报错,因为*p1是 stu,而&x是char

    // 2.强转指针类型
    char *p2 = (char *)p1;

    // 3.p1指向的结构体占用16个字节,先查看这16个地址的16进制值,每个BYTE对应一个两位数的16进制数
    // 分析stu这个结构体,根据内存对齐原理
    // 以此存放name、age、sex这三个成员
    // name会占用8个字节,age占用4个字节,sex占用4个字节
    // 总共占用16个字节
    for (int i = 0; i < sizeof(stu); i++)
    {
        printf("%x\n", *(p2+i));
    }

    // 74
    // 6f
    // 6d
    // 0
    // 0
    // 0
    // 0
    // 0

    // f
    // 0
    // 0
    // 0

    // 6d
    // 0
    // 0
    // 0

    // 4.验证存储内容
    // tom存储时转化为't','o', 'm'三个字符,对应的三个char整数为
    printf("%d,%d,%d\n", 't', 'o', 'm');  // 116,111,109
    // 对应的16进制为
    printf("%x,%x,%x\n", 't', 'o', 'm');  // 74,6f,6d
    // 字符串大端对齐,验证成功
    
    // 上面输出结果中第9到12的地址的值为age的值
    // "f000",这里是小端对齐,"f000"对应的数是16进制的000f,即10进制的15,验证成功

    // 最后一个字符'm'也是验证成功

    return 0;
}

3指针运算

3.1指针运算的含义

指针变量可以进行加减运算(只能加减运算),运算的单位是指针指向类型的内存大小,比如int *的指针p,int是4个字节,进行p++后,p的值(内存地址)与原来的值(内存地址)相差了4个整数(假设内存地址编号是连续的整数的话)。而如果是char *的指针q,q++后,变化一个整数。

#include <stdio.h>

int main()
{
    int a = 0;
    int *x = &a;

    // 通过指针运算查看内存地址值的变量
    printf("%p\n", x);   // 000000000061FE14
    printf("%p\n", x+1); // 000000000061FE18
    printf("%p\n", x+2); // 000000000061FE1C
    // 可以看到int *类型的变量加1确实是变化了4个单位的内存地址

    short b = 0;
    short *q = &b;
    printf("%p\n", q);   // 000000000061FE0A
    printf("%p\n", q+1); // 000000000061FE0C
    printf("%p\n", q+2); // 000000000061FE0E
    // 可以看到short * 运算时变化两个单位

   

3.2通过指针运算改变数组元素的值

#include <stdio.h>

int main()
{
    // 1.用指针运算改变整数数组的值
    int a[10] = {0};
    int *p = a;

    p += 5; //这一步使得p指向下标为5的那个元素
    *p = 100;

    p -= 2; //指向下标为3的那个元素
    *p = 66;

    for (int i = 0; i < 10; i++)
    {
        printf("a[%d] = %d ", i, a[i]);
        
    }
    // a[0] = 0 a[1] = 0 a[2] = 0 a[3] = 66 a[4] = 0 a[5] = 100 a[6] = 0 a[7] = 0 a[8] = 0 a[9] = 0
    // 可以看到只有 a[3] = 66 a[5] = 100

    return 0;
}

3.3通过指针运算实现字符串切片

#include <stdio.h>

int main()
{
    char *name = (char *)"hello world";
    name++;
    printf("%s\n", name); // ello world

    return 0;
}

3.4指针类型不同时进行值运算

  • int ab[10];
  • short *p = ab; //一个short类型的指针变量,指向了一个int型的地址,但不影响p本身的类型
  • 注意在c++中【short *p = ab;】会报错,必须要类型相同才可赋值,需【short *p = (short *)ab】
  • p += 2;//p在内存中移动了几个字节? 移动了4个字节,因为p是short *,short是2个字节,所以对应指针移动的单位就是2个字节,结果是移动到了2*2=4个字节,即移动到了ab[1]的地址了
  • 指针运算的时候,不要在意指针具体指向一个什么样类型的地址,在意的是指针本身是什么样的类型,本身int *类型,则移动的单位是4个字节,本身是short *类型,则移动的单位是2个字节
#include <stdio.h>

int main()
{
    // 1.大字节强转小字节类型来修改值
    int a[5] = {1, 2, 3, 4, 5};
    // short *p = a;  // 这里不强转的话,会warning,如果是c语言,系统默认会强转
    short *p = (short *)a;  // int * 转化为 short *
    printf("%d, %d\n", *p, a[0]);  // 1, 1  
    // *p传入的是第一个地址的值,%d表示打印int,会从*p传入的地址这开始取4个地址然后转化为整数打印出来

    p += 2;  // 移动的是2个short,即1个int,p现在指向的是a中的第二个int元素的首地址
    *p = 100;
    for (int i = 0; i < 5; i++)
    {
        printf("a[%d]=%d\t", i, a[i]);
    }
    // a[0]=1   a[1]=100    a[2]=3  a[3]=4  a[4]=5  


    //2.小字节转大字节来修改值
    short b[5] = {1, 2, 3, 4, 5};
    int *p2 = (int *)b;
    p2 += 1;   // 移动了1个int大小,即4个字节,即2个short大小,而b的元素的short类型,因此p现在指向的是第3个元素的首地址

    *p2 = 100; // p2本身是int*类型指针,int占4个字节,因此修改值时会将指向的那个地址及其后面总共4个地址的值用100转化后的4个BYTE形式覆盖

    printf("\n");
    for (int i = 0; i < 5; i++)
    {
        printf("b[%d]=%d\t", i, b[i]);
    }
    // b[0]=1   b[1]=2  b[2]=100    b[3]=0  b[4]=5  

    return 0;
}

4.指针强转注意事项

4.1有符号存储和无符号读取带来不同结果

#include <stdio.h>

int main(int argc, char **args)
{
    char a = -100;
    char *p_a = &a;

    printf("%d\n", a);  // -100
    printf("%d\n", *p_a); // -100

    unsigned char *p2;
    p2 = (unsigned char *)p_a;
    printf("%d\n", *p2); // 156

    return 0;
}

在上面的示例代码中,char类型是带符号的,先定义⼀个初始值为-100,打印出来的值也是正确的-100,然后额外定义⼀个unsigned char指针类型的指针变量,然后把char指针的变量强制转换后赋给unsigned插⼊类型的指针变量,再打印出这块地址的变量的值,和上⾯不⼀样了。

当然会不⼀样,unsigned char都说了是⽆符号,所以肯定不会打印负的数出来。那么为什么明明是同⼀块地址,打印出来的值会不⼀致呢?

是这样的,数据在计算机中存储的形式是⼆进制(补码形式),也就是说是⼀串由1和0组成的数据,char类型,那么就是8位,⽽-100,对应的原码是11100100,对应的补码是

10011100,等于这串数据就是10011100。也就是说上⾯这串数据被存储在计算机中,并且被打上了char的标签,那么我们在调⽤的时候,计算机会⽤char的⽅式去解读10011100这串数据,也就是为什么会打印出-100了。

而这时候,我们将char的指针变量强制转换后赋值给unsigned char的指针变量,说明什么,说明我新建了一个指针变量去指向这块地址并且打上了unsigned char这个标签,那么我下一次去调用这个指针变量c的时候,就会用unsigned char的方法去解读了,所以 10011100会被当做无符号数解读,所以它的原码就是它本身,再转化成10进制就是 156。

结论:指针的强制转换的结果,就是对于同一块地址的数据,在读取时指针是什么类型,就按什么类型读取

4.2浮点数指针强转整数指针带来的改变

#include <stdio.h>

int main(void)
{
    float a = 10.25;
    float *p_a = &a;

    
    printf("%f\n", a);     // 10.250000
    printf("%f\n", *p_a);  // 10.250000

    int *p2;
    p2 = (int *)p_a;

    printf("%d\n", *p2);   // 1092878336

    return 0;
}

定义一个浮点数10.25,以及一个float类型和int类型的指针变量,我是64位操作系统,int占用32位,和float一样。

10.25,存储在计算机中是:0100 0001 0010 0100 0000 0000 0000 0000。(参考:浮点数的十进制和二进制转换:浮点数的十进制和二进制转换(详细例子解答)_浮点数的十进制与二进制转换-CSDN博客。小数部分转2进制的方法:小数转化为二进制数是什么?_百度知道

一共是32位,那么按照上面的思路,我把这块地址给他一个int的标签,那么上面这串数据用int读出是多少呢,答案就是1092878336。

4.3数据类型占用内存长度不一样时的强转

示例代码

#include <stdio.h>

int main(void)
{
    char a[16] = {1,0,1,0,
                  2,0,2,0,
                  3,0,3,0,
                  4,0,4,0,
                 };
    
    char *p_a = a;
    int *p2 = (int *)p_a;

    printf("----------char------------\n");
    printf("first:%d\n", *p_a);
    printf("second:%d\n", *(p_a+1));
    printf("third:%d\n", *(p_a+2));
    printf("fourth:%d\n", *(p_a+3));

    printf("----------int------------\n");
    printf("first:%d\n", *p2);
    printf("second:%d\n", *(p2+1));
    printf("third:%d\n", *(p2+2));
    printf("fourth:%d\n", *(p2+3));

    return 0;
}

运行结果

[root@localhost test]# gcc main.c -o main -lm
[root@localhost test]# ./main
----------char------------
first:1
second:0
third:1
fourth:0
----------int------------
first:65537
second:131074
third:196611
fourth:262148
[root@localhost test]# 

我们定义了一个char类型且size是16的数组,打印出前面几个数据,p_a + 1即把指针指向下一个地址,因为数组的地址是连续的,所以指向下一个单元的数据,也就是数组的下一个数据被打印出来了,这个是没问题的,此时我把这个地址打上int的标签赋给一个int类型的指针,然后也对它进行加1打印的操作,显示的却是4个看似很乱的值,这可不是乱码,那么这四个值是怎么来的呢?可以把地址列一下

我们结合int类型是占用了4个字节的知识,把前面四个字节给他当作一个变量,发现把第四个数

据到第一个数据从高位到低位串起来(因为小端对齐),0 1 0 1(十进制)转换后就是 00000000 00000001 00000000 00000001,那么这个数据就是65537,那么第二个数131074呢,没错就是把第八个数据到第五个数据给他串起来,以此类推。

由上面的例子我们看出,指针+1是对于指针类型对应的数据单元大小而言的,强转后数据的单元大小也就随之改变了,所以每次+1后地址跳转的也不一样。

4.4 int内存的本质

本质是连续的4个char的内存空间

#include <stdio.h>

 int main()
{
    int a = 123;
    char *p = (char *)&a;

    printf("%08x\n", a); 
    //0000007b

    printf("%02x,%02x,%02x,%02x\n", p[3], p[2], p[1], p[0]);
    //00,00,00,7b

    return 0;
}

  • 22
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值