「地表最强」C语言(十七)阅读程序

环境:CLion2021.3;64位macOS Big Sur

文章目录


地表最强C语言系列传送门:
「地表最强」C语言(一)基本数据类型
「地表最强」C语言(二)变量和常量
「地表最强」C语言(三)字符串+转义字符+注释
「地表最强」C语言(四)分支语句
「地表最强」C语言(五)循环语句
「地表最强」C语言(六)函数
「地表最强」C语言(七)数组
「地表最强」C语言(八)操作符
「地表最强」C语言(九)关键字
「地表最强」C语言(十)#define定义常量和宏
「地表最强」C语言(十一)指针
「地表最强」C语言(十二)结构体、枚举和联合体
「地表最强」C语言(十三)动态内存管理,含柔性数组
「地表最强」C语言(十四)文件
「地表最强」C语言(十五)程序的环境和预处理
「地表最强」C语言(十六)一些自定义函数
「地表最强」C语言(十七)阅读程序

十七、阅读程序

int i;//不初始化,默认为0
int main()
{
	i--;
	if (i > sizeof(i))//sizeof的计算结果为unsigned int,int与其运算时需要转换为unsigned int
		printf(">\n");
	else
		printf("<\n");
int a = 0;
int n = 0;
scanf("%d %d", &a, &n);
printf("%d", Sn(a, n));
	1.
	(*(void(*)())0)();//调用0地址处的函数,该函数无参,返回类型是void
	//void(*)()			 - 函数指针类型
	//(void(*)())0		 - 将0强制类型转换为函数地址
	//*(void(*)())0		 - 对0地址进行解引用操作
	//(*(void(*)())0)()  - 调用0地址处的函数
	2.
	void (*signal(int, void(*)(int)))(int);
	//signal先和()结合,说明signal是函数名
	//signal函数的第一个参数的类型是int型,第二个参数的类型是函数指针,该指针指向一个参数为int,返回类型为void的函数
	//signal函数的返回类型是一个函数指针,该指针指向一个参数为int,返回类型为void的函数
	//signal是一个函数的声明
	//分析时:去掉函数名和参数即为返回类型		数组去掉名字和[]剩下的即为类型
	//void(*)(int) signal(int, void(*)(int));	//这种写法不允许,若函数返回值为函数指针,则*必须和函数名写在一起。
	//typedef 对类型进行重定义
	typedef void (*pfun_t)(int);//对函数指针类型重命名为pfun_t,注意pfun_t必须和*写在一起而不能typedef void (*)(int) pfun_t
	pfun_t signal(int, pfun_t);//清晰地看出这是一个函数的声明

	int a[5] = { 1,2,3,4,5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));	//	2	5
	//*(a + 1),a为首元素地址,首元素为int型,所以a其实是一个int型的指针,因此+1即向后移动了一个int型的字节,指向了2。
	//而&a则取出了整个数组的地址,虽然在数值上和首元素的地址一样,但是其类型为int(*)[5],因此+1后向后移动了一个数组的长度,即5的后边;
	//这时解引用本能访问一个数组,但是他被强制转换成了int型的指针,赋值给了ptr,所以此时ptr-1只能向前移动一个int型的宽度,指向了5.
//结构体Test的大小是20个字节
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;//p为结构体指针,指向Test
//假设p的值为0x100000

	//指针类型决定了指针的运算
	printf("%p\n", p + 0x1);					//跳过一个结构体大小:0x100000 + 0x000014 = 0x100014
	printf("%p\n", (unsigned long)p + 0x1);		//就是整型 + 整型:0x100000 + 0x1 = 0x100001
	printf("%p\n", (unsigned int*)p + 0x1);		//跳过一个整型大小:0x100000 + 0x000004 = 0x100004
	//加16进制数也只是普通加法而已,并不是加地址,地址+地址是没有意义的,因此指针+1后的偏移量还是取决于指针的类型
	int a[4] = { 1,2,3,4 };
	int* ptr1 = (int*)(&a + 1);	
	int* ptr2 = (int*)((int)a + 1);
	printf("%x,%x", ptr1[-1], *ptr2);// 4,200000

ptr1[-1]等价于*(ptr1 - 1),与上边的4类型一样,不做重点解释,重点解释ptr2:
(int)a将数组a的首地址强制转换为int类型,很多人会有疑问,地址怎么转换成整数呢?其实很简单,我们假设数组a[]的首元素地址为0x00ffaa34,我们都知道,地址是用十六进制表示的,若不考虑前边的0x,剩下的就是一个十六进制数,因此将其强制类型转换为int时,就是将这个地址变成了00ffaa34,这是00ffaa34是一个整型的数字,将其+1变成00ffaa35,然后再将其强制类型转换为int*,其实就变成了0x00ffaa35而已;而内存中一个字节占一个存储单元,因此此时ptr2指向了int类型1的第二个字处,又由于已经被转换为int*型,因此又可以访问4个字节的大小,刚好可以访问到int型2的第一个字节(小端存储,不了解什么是小端存储的朋友可以看这篇文章的1.2节,有详细的介绍地表最强C语言汇总(一)基本数据类型(持续更新)
没看懂没关系,配合下边的图在看文字即可秒懂:

在这里插入图片描述

	int a[3][2] = { (0,1),(2,3),(4,5) };//逗号表达式	1	3	5
	int* p;
	p = a[0];
	printf("%d",p[0]);	// 1
	int a[5][5];// 0 0 0 0 0	0 0 0 0 0	0 0 0 0 0	0 0 0 0 0	0 0 0 0 0
	int(*p)[4];	// 0 0 0 0		0 0 0 0		0 0 0 0		0 0 0 0		0 0 0 0
	p = a;		// a:int(*)[5]
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);//	FFFFFFFC,-4

在这里插入图片描述
指针- 指针得到的是两个指针之间的元素个数,由上图可以看出,p[4][2]和a[4][2]的地址之间相差4个元素,而且数组在内存中是按照低地址到高地址分配空间的,因此结果为-4,在内存中的存储方式为补码,其补码为:
原码:10000000 00000000 00000000 00000100
反码:11111111 11111111 11111111 11111011
补码:11111111 11111111 11111111 11111100
存好之后,分别以%d的形式打印出来和%p的形式打印出来,而这两种形式,同一个数据,取出来是不同的结果。
对于%d的形式取出,要以整型的格式打印数据,将其转换为原码后就是-4;
对于%p的形式取出,要以地址的格式打印数据,而且地址并没有正负之分,因此直接将上述二进制序列以16进制打印:FF FF FF FC

	int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));//*(aa+1) <=>aa[1],即第二行首元素的地址,即6的地址;
	//或者这么理解,aa+1是一个数组指针,其类型为int(*)[5],对其解引用得到了一个数组aa[1],这个数组又是其首元素的地址类型为:int*
	//可以看出,其实ptr2不需要强制类型转换
	printf("%d,%d\n", *(ptr1 - 1), *(ptr2 - 1));//	10,5

注意:对(aa + 1)解引用得到的是一个数组而非数字6,因为aa + 1表示的是一个数组的地址。而这个数组,表示的是其说元素的地址即6的地址。

	char* a[] = { "study","at","fuda" };//a是一个指针数组,每个元素都是char*,为字符串首元素的地址
	char** pa = a;//pa指向a首元素,a首元素类型为char*
	pa++;
	printf("%s\n", *pa);//at
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;
	printf("%s\n", **++cpp);		//POINT
	printf("%s\n", *--* ++cpp + 3);	//ER	注意--是操作在谁的身上
	printf("%s\n", *cpp[-2] + 3);	//ST
	printf("%s\n", cpp[-1][-1] + 1);//EW

在这里插入图片描述

#pragma pack(4)//将默认对齐数设置为4
struct A {
    int a;
    short b;
    int c;
    char d;
};
struct B {
    int a;
    short b;
    char c;
    int d;
};
#pragma pack()//将默认对齐数恢复为初始值
int main() {
    struct A sa = {0};
    struct B sb = {0};
    printf("结构体A的大小是%dbytes\n",sizeof(sa));// 4+2+2+4+1+3=16
    printf("结构体B的大小是%dbytes\n",sizeof(sb));// 4+2+1+1+4=12
    //结构体内的成员变量类型,个数都相同,但是对齐后所占空间却不一样,因此要合理安排成员变量的位置
    return 0;
}
int main()
{
    struct tagTest1{
        short a;
        char d;
        long b;
        long c;
    };
    struct tagTest2{
        long b;
        short c;
        char d;
        long a;
    };
    struct tagTest3{
        short c;
        long b;
        char d;
        long a;
    };

    struct tagTest1 stT1;// 2+1+5+8+8=24
    struct tagTest2 stT2;// 8+2+1+5+8=24
    struct tagTest3 stT3;// 2+6+8+1+7+8=32
    //注意技巧,以本题的tagTest3为例:成员变量short后为long,默认对齐数大于short,
    //而且long前的所有成员变量所占空间大小的总和没有达到long的大小,因此一定需要补齐到long的大小,即占用8个字节的大小;
    //同理第三个char类型的变量也是如此,因此这个结构体的大小就是8*4=32字节

    printf("%d %d %d\n",sizeof(stT1),sizeof(stT2),sizeof(stT3));
    return 0;
}
#define MAX_SIZE 2+3
struct _Record_Struct {
    unsigned char Env_Alarm_Id: 4;
    unsigned char Paral: 2;
    unsigned char state;//不是位段成员,单独开辟一个字节
    unsigned char avail: 1;
} *Env_Alarm_Record;
//struct _Record_Struct *pointer = (struct _Record_Struct*) malloc(sizeof(struct _Record_Struct) * MAX_SIZE);
int main()
{
    printf("%d",sizeof(struct _Record_Struct) * MAX_SIZE);// 3 * 2 + 3 = 9
    return 0;
}
int main()
{
    unsigned char puc[4];
    struct tagPIM
    {
        unsigned char ucPim1;
        unsigned char ucData0 : 1;
        unsigned char ucData1 : 2;
        unsigned char ucData2 : 3;
    }*pstPimData;
    pstPimData = (struct tagPIM*)puc;
    memset(puc, 0, 4);//库函数,将puc的前四个字节置为0
    pstPimData->ucPim1 = 2;
    pstPimData->ucData0 = 3;
    pstPimData->ucData1 = 4;
    pstPimData->ucData2 = 5;
    printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]);//02 29 00 00
    //00000010 00101001 00000000 00000000 -> 02 29 00 00
    return 0;
}

puc为4个字节,又通过memset函数将其四个字节全部置为0,因此puc就是这样的:
00000000 00000000 00000000 00000000
接着又将其首地址赋值给了pstPimData,其实单单地址本身,并没有什么能够计算的,只有当地址和类型结合起来才能够进行有效的计算,因为计算总需要知道类型;因此对计算起决定性作用的是以什么样的视角去看待这个地址
puc就是以数组的视角去看待这个起始地址,这个数组又是4个字节的,因此他能访问4个字节的空间;而pstPimData看待这个地址是以结构体的视角来看待的,因此他能访问多少字节就需要知道这个结构体的大小,经计算为2字节。也就是说通过pstPimData只能访问2个字节(关于结构体大小的计算在地表最强C语言汇总(十二)结构体、枚举和联合体一文中有详细介绍,这里不做赘述)
其中ucPim1占一个字节,ucData0、ucData1、ucData2共用1个字节,对其赋值就变成了这样:
pstPimData->ucPim1 = 2:00000010
下面三个共用一个字节,就是说讨论的是一个字节内,不同赋值的变化,此处涉及到位段,不明白的话建议参考上边提到的的文章。
pstPimData->ucData0 = 3:00000001
pstPimData->ucData0 = 4:00000001
pstPimData->ucData0 = 5:00101001
赋值结束后,数组的前两个字节就变成了00000010 00101001,而后两个字节始终没有被操作,仍然是00000000 00000000

union Un
{
	short s[7];//14
	int n;//4
};
int main()
{
	printf("%d\n", sizeof(union Un));//14+2=16
	return 0;
}

联合体的大小计算,详细请参考地表最强C语言汇总(十二)结构体、枚举和联合体

int main()
{
    union
    {
        short k;//2
        char i[2];//2
    }*s, a;
    s = &a;
    s->i[0] = 0x39;//00111001
    s->i[1] = 0x38;//00111000
    printf("%x\n", a.k);//0011100100111000 -> 3839(小端存放)
    //数组随着下标的增长,地址由低位到高位变化。
    return 0;
}

关于大小端的问题,可以参考地表最强C语言汇总(一)基本数据类型

enum ENUM_A
{
    X1,//0
    Y1,//1
    Z1 = 255,
    A1,//256
    B1,//257
};
int main()
{
    enum ENUM_A enumA = Y1;
    enum ENUM_A enumB = B1;
    printf("%d %d\n", enumA, enumB);//1 257
    return 0;
}

关于枚举,详细请参考地表最强C语言汇总(十二)结构体、枚举和联合体

哪个变量不是指针
#define INT_PTR int*
typedef int* int_ptr;
INT_PTR a, b;//b不是指针,因为宏是原封不动的替换,此句等价于int* a,b;这样定义的a是指针而b不是。就是说,不能用这种方式连续定义指针
int_ptr c, d;//这种方式定义的都是指针
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值