目录
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;
}