【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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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;

}