C语言(四)

9.结构体

9.1引入

假如要保存一些人的信息,需要

  • 姓名 -- char name[100];
  • 性别 -- char sex;
  • 民族 -- char nation
  • 身份证号 -- char id[20];
  • 身份证有效期 -- ...
  • 年龄 -- int age;
  • ......

这些数据单独拿出来是没有意义的,只有合在一起才能描述一个人,能不能将上面的数据类型整体的封装起来,用来描述一个复杂的对象。

-- 结构体 struct

对于基本类型、指针类型都是固定大小的;对于数组/结构体数据类型的大小是不固定的,成为构造类型。

9.2语法

构建

struct 结构体名
{
    成员类型1 成员变量1;
    成员类型2 成员变量2;
    成员类型3 成员变量3;
    ...
    成员类型n 成员变量n;
};

struct person
{
    char name[100];
    char sex;
    char nation;
    char id[20];
    int age;
};
  • struct person是构建了一个类型
  • 结构体名,成员变量名 要符合C语言标识符命名规则

通过构建了一个结构体类型后,就可以通过该类型来定义结构体变量了。

定义

struct person human;
//或者在构建结构体时也可以定义变量
struct person
{
    char name[100];
    char sex;
    char id[20];
    int age;
} human;

访问结构体变量成员

//使用结构体变量访问
humen.sex = 'f';
humen.age = 18;
strcpy(human.name, "hong");
//使用结构体指针访问
(&human)->sex = 'f';
(&human)->age = 18;
strcpy((&human)->name, "hong");
//定义一个指针指向humen
struct person *p = &human;
p->sex = 'f';
p->age = 18;
strcpy(p->name, "hong");

结构体数组

struct person human[] = {
    {"hong", 'f', "316546543", 18},
    {"ming", 'm', "654616541", 18}
};

结构体类型的大小

struct t
{
    int a;
    char b;
    short c;
};

struct t tt;	// sizeof(tt) == 8;
  • 结构体中规定成员变量的地址要能被它所占的内存大小整除
  • 整个结构体数据的字节大小要被它的每个成员变量中最大的基本类型整除(如果不能将进行字节填充)
  • 如果结构体中有构造类型,把组合类型拆开看

字节对齐有利于CPU寻址/取数据效率更高(一些内存较为紧张,且对速度要求不太高的情况,可以采用不对齐的方式)

9.3位域与柔性数组

9.3.1位域

如果定义int/char来表示少数几种状态,其实是一种浪费,在结构体中,位域就是用来合理利用内存,实现用位数表示状态。

9.3.2柔性数组

int a[10];

a的空间已经被定义好了,不能扩容

如果我们想在结构体中保存一串字符,定义如下

struct t1
{
    int n;			//保存字符串长度
    char c[100];	//保存字符
};

数组的大小不方便改变

解决方法 -- malloc

struct t1
{
    int n;		//保存字符串长度
    char *p;	//指向保存字符的空间
};

struct t1 *t = malloc(sizeof(*t));
//保存一个字符串:"xiaoming"
t->n = strlen("xiaoming")+1;
t->p = malloc(t->n);
strcpy(t->p, "xiaoming");
//不需要了就free
free(t->p);
free(t);
  • 这种方法繁琐且不安全,free时容易漏掉t->p
struct t1
{
    int n;		//保存字符串长度
    char p[0];	//柔性数组
};
//柔性数组元素个数为0,但是数组名任然表示一个地址,不占空间
//保存一个字符串:"xiaoming"
struct t1 *t = malloc(sizeof(*t) + strlen("xiaoming")+1);
t->n = strlen("xiaoming")+1;
strcpy(t->p, "xiaoming");
free(t);
  • 柔性数组要位于结构体其他成员末尾
  • 柔性数组前一定要有其他非柔性数组成员
  • 柔性数组的好处:减少内存碎片

10.联合体

联合体也是构造类型,关键字 union

语法

定义

union 联合体名
{
    成员类型1 成员变量1;
    成员类型2 成员变量2;
    成员类型3 成员变量3;
    ......
    成员类型n 成员变量n;
};
//示例
union test
{
    int a;
    char b;
};
  • 联合体定义语法与结构体类似

引用

union test t;
t.a = 10;
  • 联合体访问与结构体类似

联合体多个成员变量共同使用同一块内存,各个成员的地址是一样的

联合体适用于各个成员变量不同时使用的情况,且保存的数据类型各不相同,比结构体更省空间

联合体可以用来判断大小端

  • 大端存储方式:低字节存放在高地址
  • 小端存储方式:低字节存放在低地址

那么如何判断大小端呢

union test
{
    int a;
    char b;
};

int main()
{
    union test t;
    t.a = 0x12345678;
}
//如果t.b中的值为0x78,则为小端模式,否则为大端模式

共用体所占字节数

  • 最大成员变量决定至少要多少个字节
  • 对齐方式 成员变量中最大的类型
  • 组合类型拆开看

11.枚举

把该类型的数据的所有可能的值都列出来,该类型数据的值只能是列举出来的值(整数)

假设要描述一下星期一到星期日

0 -- 星期日

1 -- 星期一

2 -- 星期二

......

int day = 0;	//表达并不具体
int day = sunday;//见名知义,可以用枚举实现

语法:

定义

enum 枚举名
{
    枚举成员1 {=初始值},
    枚举成员2 {=初始值},
    枚举成员3 {=初始值},
    ......
    枚举成员n {=初始值}
};

//示例
enum week
{
    sunday = 0,
    Monday,
    Tuesday,
    .....
};
  • 枚举成员不是变量,但是要符合C语言标识符命名规则
  • 枚举成员代表一个值,是那个值的另外一个名字
  • 枚举成员的初始值可以选的,如果不选择,最开始的默认为0,后面的依次+1

12. typedef关键字

作用:给一个类型取新的名字。

示例

typedef unsigned int uint;	//将unsigned int取一个新名字uint
uint a;//等价于 unsigned int a;

12.1 typedefstruct

typedef struct test
{
    ....
} testDef;	//这时就可以用testDef来代替struct test

typedef struct
{
    ....
} testDef;	//也可以这样,这样就只能用testDef来定义结构体变量了

12.2 typedef和函数指针

typedef void *(*pFunc)(char *, char *);
//此时pFunc可以代替void *(*)(char *, char *)

12.3 typedef#define的区别

#define uint8 unsigned char	//用uint8代替unsigned char,在预处理之后,会将uint8替换为unsigned char
typedef unsigned int uint32;//uint32代替unsigned int
//-------
#define Pint int *
Pint p1,p2;//==>int *p1,p2;p1是一个指针变量,而p2是普通变量
typedef Pchar char *;
Pchar p1,p2;//==> char *p1, *p2;p1 p2都是指针变量
  • #define是简单替换,不做类型检查
  • typedef是给类型起别名
  • 给类型起别名用typedef

13.C程序组成

一个完整的工程,是由多个.c.h文件构成的

  • .c 保存源代码
  • .h 头文件

13.1 .c文件的组成

13.1.1头文件包含

#include <***.h>	//系统头文件
or
#include "***.h"	//自定义都文件

作用:在预处理时,将头文件在该位置展开,可以使用预处理命令gcc -E ***.c -o ***.i

对于 #include <***.h> ,编译器时怎么知道这个文件在哪里的 -- 环境变量,每个系统都有环境变量,它指定了编译器,头文件,库文件等重要文件的路径

<>" " 的区别

  • <> 表示头文件通过环境变量所指示的路径去查找
  • " " 头文件会限制当前工程指定的路径去查找,默认当前文件夹,如果该目录下没有该头文件,就会去环境变量所指示的路径去查找

13.1.2宏定义

关键字 #define

语法

//不带参数
#define 宏名 表达式
/*例如*/ #define pi 3.14
//作用:在预处理时,将宏名全部替换为表达式

//带参数
#define 宏名(参数) (表达式)

13.1.3声明

函数声明:

  • 在本文件中的函数

    void func();
    
  • 在同工程中其他文件中的函数

    extern void func();
    

变量声明:

  • 在本文件中的变量

    int a;
    
  • 在同工程中其他文件中的变量

    extern int a;
    

类型声明:

类型的声明/定义只在只在本文件中有效,一般放在头文件中

13.1.4条件编译

有选择地进行编译

#if 表达式
	功能代码;
#endif
/*
如果表达式为真,则编译这段代码
*************/
#if 表达式
	功能代码1;
#else
	功能代码2;
#endif
/*
如果表达式为真,则编译功能代码1
否则编译功能代码2
*************/
#ifdef 宏名	//理解为 #if define
	功能代码;
#endif
/*
表示如果#define了这个宏,则编译这段代码
*************/
#ifndef 宏名	//理解为 #if no define
	功能代码;
#endif
/*
表示如果没有#define这个宏,则编译这段代码
*/

13.2 .h文件的组成

一般一个项目/工程 实现起来代码量大,逻辑复杂,如果写在一个.c文件中,不易维护,可读性差,不易分工,所以要将不同模块的功能代码放在不同的.c 文件中

为什么需要.h文件呢?

.c.h 一般时一一对应的,一个模块有一个.C 来体现,也要有一个.h文件做辅助

.h文件更像是一个说明书,可以将各种功能的实现和声明进行隔离

13.2.1条件编译

/* stdio.h */
#ifndef _STDIO_H_
#define _STDIO_H_
...
#endif
//防止头文件的重复包含

13.2.2函数声明

作业

1.将各种排序卸载不同的.c文件中,通过main.c去调用这些排序

2.定义一个学生信息结构体类型,并定义该类型变量,输入10个学生的成绩,并输出分数最高的学生成绩结构体数据

3.在图书馆中,有很多种的书籍--漫画、科幻..... 设计相应的结构体,并设计一种结构可以保存所有种类书籍的信息

posted @ 2023-07-12 20:13  乐情在水静气同山  阅读(13)  评论(0编辑  收藏  举报