C语言基础——结构体、内存对齐

本人C语言学习笔记,内容杂乱冗长,也并不专业

结构体定义

typedef struct student{
	XXXX;
}ss;//定义结构体并起别名ss,常用

struct student{
	XXXX;
}ss;//定义结构体并创建全局变量ss

结构体支持直接赋值复制(同类型),stu2=stu1;但普通数组arr1[5],arr2[5]无法直接赋值复制,arr1=arr2;因为本身是常量,要循环arr[i]挨个复制。

定义结构体时{}结束要有;,定义函数时不需要,一般按回车会自动补。

内存对齐

一个成员变量需要多个机器周期去读的现象成为内存不对齐,如一个int型变量的首地址不是4的整数倍,该变量就被分割开了,则要读取它时要先用一个机器周期先读到前半部分,舍去头部非该int的部分,同样再读后半部分,舍掉尾部非该int的部分,再把两次拼接起来,如图。对齐的本质就是牺牲空间换取时间。

在这里插入图片描述

对齐规则:对齐模数可以更改为2的n次方。x86下Linux默认#pragma pack(4),windows默认#pragma pack(8)。Linux最大只支持4字节对齐,即最大n=4。

标准数据类型的首地址只要是其长度的整数倍即可,而对于自定义的结构体类型要遵循以下对齐规则:

  1. 设起始地址为0,取pack(n)的值n和结构体中类型最大值m,两者取小为外对齐大小y,整个结构体所占的内存大小,应当是y的整数倍,否则在尾部补齐。例子中第一种情况的51+1补的1就是为了满足外对齐。
  2. 给每个结构体成员开辟空间时,成员大小与外对齐y再取小为内对齐大小x,每个成员变量开辟的空间首地址应当是x的整数倍,否则补齐。y大于等于x。

再换一种思路:

  1. 先把结构体内对齐定好,即第一个成员从0开始偏移,此后的成员均从(自身类型大小和对齐模数)的最小值的整数倍地址开始,这是内部的对齐偏移,此时可以求出目前结构体的整体大小m。
  2. 然后再计算从(结构体中最大成员类型和对齐模数)的最小值的整数倍,其中最接近m、比m大的值作为结构体的整体大小,在尾部补上,即外对齐。

当出现结构体嵌套的情况,其实本质就是重复第二步外对齐,即子结构体的首地址应当是(子结构体中最大成员类型和对齐模数)的最小值的整数倍,而该子结构体的大小也是在这一基础上再补上外对齐。

其实就是对于自定义结构体,多加了个对齐模数限制对齐最大值,从而对其有限制地提供空间换取时间。

例如:

struct student{
	char name[21];
	short age;
	char sex;
	int scores[3];
	char addr[51];
}stu;

该结构体创建后会占用92个字节,本身应开辟21+2+1+4*3+51=87,但开辟完name[21]后不能立即跟age(个人猜测由于内存到处都在满足内存对齐,char类型的name起始地址一般也是偶数,因为只要有超过1字节就都是偶数,在计算大小时可默认首地址为0),21要空1个对齐到22,(空着不用,sizeof(stu.name)还是21个字节),同理1要空3个对齐到4,51要空1个对齐到52,故87+1+3+1=92。

如果有double则给double开辟空间时候就要对齐到8个字节,如果有short则给short开辟空间时候只需对齐到2。

struct student{
	char name[21];
	short age;
	char sex;
	char addr[51];
	int scores[3];
}stu;

这种情况就只占88个,因为只有name后给short补了1个空,addr前面是char,不需要对齐,这种情况下addr的首地址可以明确是奇数,且这个sex正好和51组成52,scores也不用对齐了。

可见调整结构体成员顺序有可能改变结构体大小,这套对齐规则对联合体(union)同样适用。

结构体偏移量

除了手算一个成员的偏移量,还可以通过标准库stddef.h中的宏函数offsetof(s,m),s为结构体,m为成员,如offsetof(struct Person,age),即可返回成员age在结构体Person中的偏移量。

当然也可以用Person->age的地址减去Person的地址求出偏移量,如(int)&(p->b)-(int)p。

结构体指针变量

#include <stdio.h>

struct student{
	char name[21];
	int age;
	char sex;
	int scores[3];
	char addr[51];
};

int main(){
	struct student stu = { "林冲",30,'M',80,90,100,"汴京" };
	//创建结构体指针变量p,接受结构体变量stu的地址
	struct student* p = &stu;
	
	//结构体变量.成员
	printf("%s\n", (*p).name);
	printf("%d\n", (*p).age);
	printf("%d\n", (*p).scores[0]);
	//结构体指针变量->成员
	printf("%s\n", p->name);
	printf("%d\n", p->age);
	printf("%d\n", p->scores[0]);
	//两种方法操作成员结果相同,一般用->

    return 0;
}

结构体指针开辟堆空间

把struct student stu看做类似int a的形式,其中struct student相当于int,某种要创建的数据类型,其实int写全就是struct int,只是C语言官方自带数据类型省去了,stu则相当于a,该数据类型的具体变量名,故仿写int* p = (int*)malloc(sizeof(int)*3),给结构体变量开辟堆空间则为struct student* p = (struct student*)malloc(sizeof(struct student)*3),即开辟3个结构体student大小的堆区空间,首地址赋值给结构体指针变量p。

可用typedef struct student ss;起别名来简化,用ss代替struct student,即可ss* p = (ss*)malloc(sizeof(ss)*3)。

含指针的结构体变量开辟堆空间

当结构体成员存在指针时,要给该结构体类型开辟空间之后还需要再给里面的指针成员创建堆空间(当然还可以继续套娃)。

#include <stdio.h>
#include <stdlib.h>

typedef struct student ss;
struct student{
	char* name;
	int age;
	char sex;
	int* scores;
	char* addr;
};

int main(){
	ss* p = (ss*)malloc(sizeof(ss) * 3);//开辟3个结构体指针变量的堆空间
	//开辟结构体指针成员name\scores\addr的堆空间,且循环3次
	for (int i = 0; i < 3; i++)	{
		//也可用(p+i)->name
		p[i].name = (char*)malloc(sizeof(char) * 21);
		p[i].scores = (int*)malloc(sizeof(int) * 3);
		p[i].addr = (char*)malloc(sizeof(char) * 51);

	}
	/*****/
	for (int i = 0; i < 3; i++)	{
		//也可用(p+i)->name
		free(p[i].name);
		free(p[i].scores);
		free(p[i].addr);
	}
	free(p);
    return 0;
}

结构体指针值/址传递

#include <stdio.h>
#include <stdlib.h>

typedef struct student ss;
struct student {
	char* name;
	int age;
	int score;
	char addr[51];
};

void function01(ss stu1) {
	//1.char a[21];stu1.name = &a;

	//2.stu1.name = (char*)malloc(sizeof(char)*21);

	strcpy(stu1.name, "卢俊义");
	printf("f1函数:%s\n", stu1.name);
};

int main() {
	ss stu = { NULL,50,99,"水泊梁山" };
	stu.name = (char*)malloc(sizeof(char) * 21);
	strcpy(stu.name, "宋江");
	function01(stu);
	printf("主函数:%s\n", stu.name);
	free(stu.name);
	return 0;
}

给指针赋值char型字符串时直接用指针stu.name=”宋江”会导致后续无法修改,因为p=”XX”的数据创建在常量区,故通常要用复制字符串的方式strcpy(stu.name,”宋江”)。赋值int则可以直接stu.scores[i]=80;故结构体含指针成员初始化也无法用stu = {“宋江”,59,80,“水泊梁山”},因为是指针,初始值可设为NULL,使用时在堆区开辟空间。

与其他变量类型一样,创建新的结构体变量值传递地去接收另一个结构体变量,本质是复制一个副本,如果是变量,则两者此后互相独立,操作的不是同一个变量,无法通过操作新的结构体变量去改变原结构体变量,但如果是结构体指针,则可以通过复制的结构体指针变量去与原结构体变量建立联系。

同样,如果结构体变量里有成员是指针,通过复制的新结构体变量也可以操作该指针上的值,此时是两个指针(一个原指针,一个新创建的指针)共用同一个内存空间,都能改变该指针上的值,如本例输出结果均为卢俊义。效果和地址传递一样,但地址传递是直接操作原结构体。

如果不想改变原结构体指针成员指向的空间,想要复制一个真正独立的副本,则调用时需要重新开辟一个空间再进行操作,可以在栈上开辟,创建一个字符串数组变量 char a[21];stu1.name = &a,或者在堆上开辟 stu1.name = (char*)malloc(sizeof(char)*21),也就是给stu1.name换个地址再赋值,如本例取消任一注释结果就变成
f1函数:卢俊义
主函数:宋江

这段越描述越感觉浅拷贝和深拷贝就是值传递的两种形式。。。

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值