C语言-结构体

结构体

在 C 语言中,char、int、float……等属于系统内置的基本数据类型,往往只能解决简单的问题。当遇到比较复杂的问题时,只使用基本数据类型是难以满足实际开发需求的。因此C 语言允许用户根据实际项目需求,自定义一些数据类型,并且用它们来定义变量。

结构体是一种组合数据类型,由用户自己定义。结构体类型中的元素既可以是基本数据类型,也可以结构体类型。

定义结构体类型的一般格式为:

struct 结构体名
{
成员列表
};

成员列表由多个成员组成,每个成员都必须作类型声明,成员声明格式为:

数据类型 成员名;

struct Employee
{
char name[8];
int age;
int id;
int salary;
};//一定要有分号

定义结构体变量的三种方式

  1. 先定义结构体类型,再定义结构体变量,一般形式为:
struct Employee
{
char name[8];
int age;
int id;
int salary;
};

struct Employee emp;//全局的结构体变量
  1. 在定义结构体类型的同时定义变量,一般形式为:
struct Employee
{
char name[8];
int age;
int id;
int salary;
}emp, emp1;//emp,emp1就是全局变量
  1. 直接定义结构体变量,并且不需指定结构体名,一般形式为:
struct
{
char name[8];
int age;
int id;
int salary;
}emp;

这种方式由于没有指定结构体名,不能再使用该结构体类型去定义其他变量

两个完全相同的匿名结构体,编译器也会把他们当做两个完全不同的类型

结构体的自引用

结构体中是不能直接包含一个该结构体本身的成员变量的,否则sizeof(struct Node)就无法计算其值,会类似递归一直循环计算

struct Node
{
	int value;
	struct Node next;//错误写法
};

此时就需要用到结构体类型的指针

指向结构体变量的指针就是结构体指针,指针变量中保存一个结构体变量的地址, 则这个指针变量指向该结构体变量,需要注意的是指针变量的类型必须和结构体变量的类型相同。

定义结构体指针变量的一般形式为: struct 结构体名* 指针变量名

struct Node
{
	int value;
	struct Node* next;
};

结构体变量初始化

在 C 语言中,结构体变量初始化,本质上是对结构体变量中的成员进行初始化,使用花括号{ }在初始化列表中对结构体变量中各个成员进行初始化。

结构体中的成员可以是标量,数组,指针,甚至是其他结构体。

编译器会将“test”、20、1、10000按照顺序依次赋值给结构体变量emp中的成员name、 age、id、salary。

struct Employee
{
char name[8];
int age;
int id;
int salary;
};

struct Employee emp = {“test”,20,1,10000};

除了采用初始化列表,还可以使用赋值运算符对成员进行初始化

#include
#include
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};      

int main(void)
{
    struct Employee emp;
    strcpy(emp.name,"test");
    emp.age=20;
    emp.id=1;
    emp.salary=10000;
    getchar();
    return 0;
}

使用成员列表的方式初始化时,编译器会自动将字符串“test”复制到字符数组 name 中。而使用成员赋值方式初始化时,需要调用 strcpy 函数,将字符串“test”复制到字符数组 name 中。

在 C 语言中,通过结构体指针 p 也可以引用结构体中的属性(成员变量),有以下两种方式:

(1) (*p).成员变量名; *p表示的是结构体变量,可以通过变量直接引用其属性。注意,“.”运算符优先级是最高的,(*p_emp)两侧的括号不能省略。
(2) p->成员变量名; p表示的是指向结构体变量的指针,“->”称为指向运算符。

#include
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};

int main(void)
{
    struct Employee emp={"test",20,1,10000};
    struct Employee *p_emp = &emp;
    printf("%s\n", p_emp->name);
    printf("%d\n", (*p_emp).age);
    printf("%d\n", emp.id);
    printf("%d\n", emp.salary);
    getchar();
    return 0;
}

typedef 给类型起别名

在 C 语言中,除了使用 C 语言提供的标准类型名:char、int、double……以及自定义的结构体类型。还可以使用 typedef 关键字指定一个新的类型名来代替已有的类型名,相当于给已有类型起别名。

typedef 的一般使用形式为: typedef 原类型名 新类型名

#include

struct Employee
{
char name[8];
int age;
int id;
int salary;
};

typedef int integer;//给基本数据类型起别名
typedef struct Employee test;//给结构体类型起别名

int main(void)
{
    integer a=10;
    test t = {"test",20,1,10000};//如果不起别名,定义结构体类型的变量时,需加struct
    printf("%d\n",a);
    printf("%s",t.name);
    getchar();
    return 0;
}

起别名的另一种方式

typedef struct Stu
{
	int age;
}Stu;

结构体复制

在 C 语言中,允许相同类型的结构体变量之间相互赋值。

#include

struct Employee
{
char name[8];
int age;
int id;
int salary;
};

typedef struct Employee test;

int main(void)
{
    test t1 = {"test",20,1,10000};
    test t2 = t1;//将结构体变量t1的各个成员的值原样复制一份到变量t2的各个成员中,t1变量中数据的修改不会影响到t2变量
    getchar();
    return 0;
}

如果不想复制一份数据,可以使用指针,这样p_t2就也指向t1变量对应的数据

test *p_t2 = &t1;

结构体传参

#include

struct S 
{
	int i;
	char c;
};

//如果直接把结构体传入test函数,test函数中使用的是传入的结构体的一份拷贝
//改变其内容,并不会影响传入的结构体
void test(struct S s)
{
	s.i = 5;
	s.c = 'a';
}

int main(){
	struct S s = {0};
	test(s);
	printf("%d\n", s.i);//还是输出0
	printf("%d\n", s.c);//还是输出0
    getchar();
    return 0;
}

如果想要实现改变传入的结构体的数据,则可以传其地址或者说指针

#include

struct S 
{
	int i;
	char c;
};

void test(struct S* s)
{
	s->i = 5;//结构体指针就可以使用这种->的方式,获取结构体内部的属性
	s->c = 'a';
}

int main(){
	struct S s = {0};
	test(&s);
	printf("%d\n", s.i);//输出5
	printf("%c\n", s.c);//输出a
    getchar();
    return 0;
}

结构体内存对齐

结构体的对齐规则

1.第一个成员在与结构体变量偏移量为0的地址处

2.从结构体变量偏移量为0的地址处开始算,其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

对齐数:编译器默认的一个对齐数与该成员数据的大小相比的较小值

VS中默认的对齐数为8(gcc编译器没有默认对齐数)

3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数,取其中的最大值)的整数倍

4.如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

#include
 
//假设s1的内存地址为0x00000010
struct S1
{
	char c1;//c1在结构体变量偏移量为0的位置,所以c1的内存地址为0x00000010,char类型的数据占了一个字节
	char c2;//c2的对齐数为1,所以c2的内存地址为0x00000011
	//value占4个字节,4与8比较,4较小,所以value的对齐数为4,所以要从s1偏移量为0的地址开始算对齐数整数倍的地址,给value
	//所以value的地址为0x00000014,0x00000012和0x00000013地址都被跳过,最后value再占4个字节,s1的所占内存总大小为8个字节
	int value;
};

//假设s2的内存地址为0x00000010
struct S2
{
	char c1;//c1在结构体变量偏移量为0的位置,所以c1的内存地址为0x00000010,char类型的数据占了一个字节
	//value占4个字节,4与8比较,4较小,所以value的对齐数为4,所以要从s2偏移量为0的地址开始算对齐数整数倍的地址,给value
	//所以value的地址为0x00000014,0x00000011、0x00000012、0x00000013地址都被跳过
	int value;
	//c2的对齐数为1,c2的内存地址为0x00000018
	char c2;
	//但是在计算结构体所占内存的总大小时,要为最大对齐数的整数倍数,此时结构体中所有数据占了9个字节,最大对齐数为4
	//所以s2的总大小为12,0x00000019、0x00000020、0x00000021的空间都被浪费
};

int main(void)
{
	struct S1 s1 = {0};
	struct S2 s2 = {0};
	printf("%d\n", sizeof(s1));//输出8
	printf("%d\n", sizeof(s2));//输出12
	getchar();
	return 0;
}

结构体嵌套的情况

#include
//计算s111的大小为16,s111的最大对齐数为8
struct S1
{
	double d;
	char c;
	int i;
};

//假设s222的内存地址为0x00000010
struct S2
{
    //c的内存地址为0x00000010
	char c;
    //嵌套的结构体对齐到自己的最大对齐数的整数倍处,s1的最大对齐数为8,所以s1的内存地址为0x00000018,0x00000011~0x00000017都被浪费了,又因为s1的大小为16,所以s1占用16个字节的空间
	struct S1 s1;
    //d的内存地址为0x00000034,d占用8个字节,所以最后s222占用的内存空间为32
	double d;
};

int main(void)
{
	struct S1 s111 = {0};
	struct S2 s222 = {0};
	printf("%d\n", sizeof(s1));//输出16
	printf("%d\n", sizeof(s2));//输出32
	getchar();
	return 0;
}

可以使用offsetof宏,来计算结构体中某变量相对于首地址的偏移

函数原型:size_t offsetof(structName, memberName);

#include
#include

struct S1
{
	char c;
	int i;
	double d;
};

int main(void)
{
	printf("%d\n", offsetof(struct S1, c));//输出0
	printf("%d\n", offsetof(struct S1, i));//输出4
	printf("%d\n", offsetof(struct S1, d));//输出8
	getchar();
	return 0;
}

为什么存在内存对齐?

1.平台原因。不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常

2.性能原因。数据结构(尤其是栈)应该尽可能的在自然边界上对齐。为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存仅需要一次访问

所以内存对齐,是一种空间换时间的做法

可以更改默认对齐数,使用预处理指令#pragma

#include
 
#pragma pack(4)//设置默认对齐数为4
struct S1
{
	char c;
	double d;
};
# pragma pack()//取消设置的默认对齐数,还原为默认值

int main(void)
{
	struct S1 s1 = {0};
	printf("%d\n", sizeof(s1));//输出12
	getchar();
	return 0;
}

位段

位段的声明和结构体是类似的,但是有两个不同

1.位段的成员必须是int、signed int、unsigned int或者short、char(整型类型)

2.位段的成员名后面有一个冒号和一个数字,如果是int类型的成员,数字不能大于32,如果是char类型的成员,数字不能大于8

位段的是按照需要开辟内存使用空间的,如果位段的成员是int类型,则以4个字节为单位开辟空间,如果位段的成员是char类型,则以1个字节为单位开辟空间

#include

//位段,指的是二进制位
struct S 
{
	int a : 2;//a变量需要2个比特位
	int b : 5;//b变量需要5个比特位
	int c : 10;//c变量需要10个比特位
	int d : 30;//d变量需要30个比特位
};

int main(){
	struct S s = {0};
    //在开辟内存时,位段S中数据类型为int,所以先分配4个字节,a占2个比特位,b占5个比特位,c占10个比特位,这个字节中还剩余15个比特位,不够分配给d,所以会再开辟4个字节的空间用于存放d,这15个比特位的未使用空间会被浪费掉。所以最后s占了8个字节的空间
	printf("%d\n", sizeof(s));//输出8
    getchar();
    return 0;
}

位段赋值时

#include

struct S 
{
	char a : 3;//一个字节中,从右往左占3个比特位
	char b : 4;//一个字节中,从右往左占4个比特位
	char c : 5;//一个字节中,从右往左占5个比特位
	char d : 4;//一个字节中,从右往左占4个比特位
	//所以所占内存为 0bbbbaaa 000ccccc 0000dddd
};

int main(){
	struct S s = {0};
	printf("%d\n", sizeof(s));//输出3,这个位段占3个字节
	s.a = 10;//10的二进制是1010,但是a只占3个比特位,所以只有010会放入a的内存空间中
	s.b = 20;//20的二进制是10100,但是b只占4个比特位,所以只有0100会放入b的内存空间中
	s.c = 3;//3的二进制是11,但是c占5个比特位,所以会把00011会放入c的内存空间中
	s.d = 4;//4的二进制是100,但是d占4个比特位,所以只有0100会放入d的内存空间中
	//所以在赋值完之后,位段从0bbbbaaa 000ccccc 0000dddd变为00100010 00000011 00000100,转化为16进制22 03 04
    return 0;
}

位段涉及很多不确定因素,是不跨平台的

枚举

枚举的定义

#include

enum Color
{
	//枚举的可能取值-是常量
	RED,//默认值为0,可以在定义的时候修改,但是只能在定义的时候修改。比如RED = 5;如果RED为5,那么GREEN的值为6,BLUE的值为7。比如RED = 'a';那么GREEN的值为b,BLUE的值为c
	GREEN,
	BLUE
};

int main()
{
	enum Color c = BLUE;
	//输出0 1 2,是这三个枚举常量的默认值
	printf("%d %d %d", RED, GREEN, BLUE);//为什么可以直接取到这三个枚举值?
    return 0;
}

枚举中每个常量是什么类型的数据,是由编译器指定的,一般是整型,可以用sizeof()计算其大小,然后看是什么类型的数据

#define也可以定义常量

#include
#define RED 0;

int main()
{
	int red = RED;
	printf("%d", red);//输出0
    return 0;
}

枚举相比于#define的好处

1.增加代码的可读性和可维护性

2.相比于#define,枚举有类型检查,更加严谨。#define只是声明了一个标识符,但是枚举中的变量,类型为此枚举类型

联合

联合也叫共用体或者联合体

联合是一种特殊的自定义类型,在联合中的成员变量,共用同一块内存空间。

#include

union MyUnion
{
	char c;
	int i;
};
int main()
{
	union MyUnion un = {0};
	printf("%d\n", sizeof(un));//输出4
    //un的地址、un.c的地址、un.i的地址都是一样的
	printf("%p\n", &un);
	printf("%p\n", &(un.c));
	printf("%p\n", &(un.i));
    return 0;
}

联合变量的大小,至少是最大成员的大小

当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍

//最大成员大小为arr的大小5,但是最大对齐数为4,所以MyUnion联合体的大小为8个字节
union MyUnion
{
	int i;//最大对齐数为4
	char arr[5];//最大对齐数为1,是按照数组中元素的数据类型来决定最大对齐数的
};

你可能感兴趣的:(c语言,开发语言,后端)