结构体与枚举

定义结构体类型

在C语言中,结构体(struct)是一种自定义的数据类型,它允许你将多个不同类型的变量组合在一起。

这些变量被称为“成员”或“字段”

结构体类型一般定义在函数外部。

比如我需要在程序中使用一个学生student对象,同时处理一个学生的所有数据信息,就可以定义一个student结构体类型:

// 学生的属性有: 学号,姓名,性别,语文成绩,数学成绩,英语成绩...
struct student {
    int stu_id; // 整型值学号,用于唯一标识一个学生
    char name[25];  // 表示学生姓名的字符串数组,长度为25
    char gender; //f: female, m: male
    int chinese;    // 整型语文成绩
    int math;       // 整型数学成绩
    int english;        // 整型英语成绩
};  // 注意不要忘记末尾的分号

解释:

  1. 结构体定义使用关键字struct,在这里,struct student 表示定义了一个新的结构体类型。
  2. student是一个标识符,是此结构体类型的名字,此结构体类型包含六个成员。
  3. 结构体类型的名字建议采用"单词小写,下划线分隔"的方式。某些C程序在给结构体命名时,会添加后缀"_s"以表明它是一个结构体类型,这是一个值得学习的命名风格。

 声明结构体变量(对象)

在一般情况下,声明结构体变量需要使用关键字struct:

struct student s1; // 声明一个student结构体类型的变量s1

 初始化结构体变量(对象)

声明好一个结构体类型变量后,就需要初始化这个变量。

结构体对象的初始化和数组的初始化非常类似,也就是逐一对应的给对应成员赋值:

注意:

  1. 声明中的struct关键字不能省略。
  2. 和数组初始化一样,若一个成员未被手动初始化,它会被默认初始化为0值。

如果你觉得每次声明一个结构体变量都需要使用关键字struct很烦,那么你可以考虑给结构体类型起别名,仍然要使用关键字typedef,但语法稍特别,如下:

typedef struct student{     // student是此结构体的名字,起了别名的结构体可以省略名字。后续代码只使用别名操作结构体
    int stu_id; 
    char name[25];  
    char gender; 
    int chinese;    
    int math;       
    int english;        
} Student;  // 注意末尾的Student是该结构体类型的别名

这样起别名后,你就可以在声明结构体变量时省略struct关键字:

Student s1,s2;

结构体变量的内存布局(重要)

Student s1 = { 1, "Faker",'m' ,100,100,100 };

我们可以计算出要连续存储这些数据项,至少需要的内存空间:

int + 25char + char + int * 3

也就是

4 + 25 + 1 + 12 = 42

但实际上,我们可以在VS中使用监视窗口计算'sizeof(s1)',发现结构体变量s1占用44个字节的数据。

多出来的两个字节,被称之为"对齐填充(padding)"。可以连带复习下计算机基础边界对齐。

对齐填充

在32位平台环境下,处理器可以一次性高效的处理最多4个字节数据。(因为地址总线的宽度和寄存器的大小是32位)

所以内存中的某个数据项(尤其是4个字节和大于4个字节的),内存空间的起始地址最好总是4的倍数。这样可以有效减少内存访问次数,提高效率。此时,该数据项就实现了"内存对齐"结构体与枚举_第1张图片

以往我们分析过数组的内存布局,由于数组连续且存储单元大小一致,所以数组中的元素总是天然"内存对齐"的。

但结构体变量就不同了,一个结构体变量可能包含不同大小的数据项,此时连续存储就要考虑对齐问题了。

不同编译器,不同平台都会对数据项实现"内存对齐",但可能会有一些差异,只需要明确对齐的概念就可以了。

结构体变量(对象)的基本操作

声明初始化一个结构体变量后,就可以使用该变量进行一系列操作了。结构体变量主要可以进行的操作有:

  1. 访问/修改结构体成员
  2. 结构体变量给结构体变量赋值
  3. 结构体变量作为参数传递
  4. 结构体变量作为函数返回值

下面我们以上面定义的结构体Student,来演示一下结构体对象的这些基本操作。

访问/修改结构体成员

访问/修改结构体成员是结构体变量最基础的操作。

我们可以直接用"结构体变量名.成员名"的形式来访问/修改结构体成员。其中的"."是结构体成员运算符,它是一个一元运算符,它的优先级比大多运算符都要高。

Student s1 = { 1, "王小明",'m' ,100,100,100 };

// 数学成绩改为80
s1.math = 80;
// 此时打印数学的成绩就是80,而不是100
printf("math score: %d", s1.math);
// 可以结合自增自减符号使用
s1.chinese--;

注:--后缀运算符和.运算符优先级是一样的,但它们的结合性是左结合性的,所以要从左往右计算,s1.chinese--是等价于(s1.chinese)--的。

这一点在使用->运算符时,也是一样的。

结构体变量给结构体变量赋值

在C语言中,允许结构体变量直接给另一个结构体变量赋值,如下:

Student s1 = { 1, "王小明",'m' ,100,100,100 };
Student s2 = { 2, "李华",'f' ,90,90,90 };

// 这样进行赋值后,s1中的成员数据会完全覆盖掉s2本身的成员数据
// 也就是将s1内存空间中的数据完全拷贝覆盖到s2中
s2 = s1;

注意:

  1. 不要将结构体对象的名字看成和数组名一样的指针,结构体名字就是一个普通的变量名,和int a = 10;中的a没有什么区别。
  2. s2 = s1;不是让s2指向s1的内存区域,因为它们都不是指针。在这里,=的目的是将s1中各数据项取值,全部复制拷贝、覆盖原本s2的各数据项。此语句执行后,s1保持不变,s2各数据项取值变成和s1一致。
  3. 你可以把结构体对象之间用"="的赋值,当成两个int变量用=赋值,本质是一样的。

结构体变量直接作为参数传递

你可以选择直接将一个结构体变量作为参数传递:

// 定义一个函数直接传递操作结构体类型
void operate_struct(Student s) {
    // 打印结构体成员
    printf("Student { stu_id = %d, name = %s, gender =  %c, chinese = %d, math = %d, english = %d }\n",
           s.stu_id,
           s.name,
           s.gender,
           s.chinese,
           s.math,
           s.english);
    // 尝试修改结构体成员,能成功吗?
    s.chinese = 200;
}

// main函数:
Student s1 = { 1, "Faker",'m' ,100,100,100 };
operate_struct(s1);

最终输出的结果是:

Student { stu_id = 1, name = Faker, gender = m, chinese = 100, math = 100, english = 100 }

但在函数体内部修改成员取值显然是无法影响实参本身的,因为函数得到的只是该结构体对象的拷贝。

注意:

  1. Student是结构体的别名,如果结构体没有起别名,仍然需要使用"struct 结构体类型名 变量名"来声明结构体类型形参。
  2. 由于C语言的值传递,一个结构体变量直接作为参数传递时,会复制一个它的副本传递给函数。所以上述代码中,main函数中的结构体变量s和operate_struct函数中的s,不是同一个结构体。

    也就是说,结构体变量直接作为参数传递时,完全不可能通过函数修改这个结构体变量。

  3. 结构体变量直接作为参数传递时,由于存在复制整个结构体的操作,往往会存在效率问题。一般情况下,还是更建议将结构体变量的指针作为参数进行传递。

 结构体指针作为参数传递(重要)

将结构体变量的指针作为参数进行传递,演示代码如下:

// 定义一个函数传递结构体指针操作结构体类型
void ptr_operate_struct(Student* p) {       // 若函数体内部不需要修改结构体对象,应显式声明const
    printf("Student { stu_id = %d, name = %s, gender =  %c, chinese = %d, math = %d, english = %d }\n",
           (*p).stu_id,
           (*p).name,
           (*p).gender,
           (*p).chinese,
           (*p).math,
           (*p).english);
    // 尝试修改结构体成员,能成功吗?
    (*p).chinese = 200;
}

// main函数:
Student s1 = { 1, "Faker",'m' ,100,100,100 };
ptr_operate_struct(&s1);    // 取地址

调用函数,第一行打印的结果是完全一样的,但修改成员是可以成功的。

注意事项:

  1. Student是结构体的别名,如果结构体没有起别名,仍然需要使用"struct 结构体类型名 *变量名 "来声明结构体指针类型形参。
  2. "(*s)"中的小括号是不能省略的,因为成员运算符"."的优先级高于解引用运算符"*"的。为了保证解引用运算符先计算,所以需要加括号改变优先级。
  3. ptr_operate_struct函数得到的是结构体指针变量的拷贝副本,但副本指针仍指向原本的结构体变量。所以,将结构体变量的指针作为参数进行传递时,函数是可以修改原本结构体变量的。若此函数没有修改结构体的需求,应明确将形参声明为const,这是一个良好的编程习惯。
  4. 在C语言开发中,推荐使用指针传递结构体变量。这种方法提高了程序的灵活性和效率,尤其适用于大型结构体,因为它避免了不必要的数据复制。

 箭头运算符->

箭头运算符->(由一个减号和大于号组成)是C语言当中一个比较特殊的运算符,它实际上是结构体指针变量解引用和成员访问运算的结合,和索引运算符[]一样是属于编译器优化的语法糖。

它使得程序员不用再写丑陋的(*p).解引用成员访问语法,提供了一种更直观简洁的语法形式。

比如上面的ptr_operate_struct函数可以写成如下代码:

void ptr_operate_struct(Student* p) {
    printf("Student { stu_id = %d, name = %s, gender =  %c, chinese = %d, math = %d, english = %d }\n",
           p->stu_id,   // 完全等价于(*p).std_id
           p->name,
           p->gender,
           p->chinese,
           p->math,
           p->english);
}

利用这个运算符可以实现对结构体成员的访问,也包括赋值:

// 宏函数定义
#define SIZE_ARR(arr) (sizeof(arr) / sizeof(arr[0]))

Student s1 = { 1, "Faker",'m' ,100,100,100 };
Student* p = &s1;

// 将math修改为90,math是int类型可以直接用=赋值
p->math = 90;
printf("%d\n", p->math);

// name是一个字符数组名, 不能直接用=赋值
// p->name = "Showmaker";

// 若想要改变name字符串的内容,需要使用字符串复制函数,n取字符数组长度-1
strncpy(p->name, "Showmaker", SIZE_ARR(p->name) - 1);
// 不要忘记将数组尾元素设置为空字符,保证数组表示一个字符串, 这是使用strncpy函数的惯用法
p->name[SIZE_ARR(p->name) - 1] = 0;

// 此时name已设置为Showmaker
printf("%s\n", p->name);

 

结构体变量/指针作为函数返回值

结构体变量/指针作为函数返回值,和作为参数传递时没有本质上的区别,但我们现在创建的结构体对象,都是局部结构体对象,所以:

  1. 返回一个指向当前栈区结构体对象的指针就是返回一个悬空指针,使用这样的指针会产生未定义行为。
  2. 而如果直接返回当前结构体对象本身,那函数调用得到的是这个结构体对象的副本/拷贝(这一点和数组不同,注意区分)。

枚举

关键字 enum

在编程中,我们可能会碰到一个场景,需要对一系列的状态/离散的数值进行逻辑处理。

比如在一个电商购物应用程序中,需要根据订单的不同状态执行相应的逻辑。这些状态可能包括:

新建订单(NEW)、已支付(PAID)、已发货(SHIPPED)、已送达(DELIVERED)、已完成(COMPLETED)、已取消(CANCELLED)等。

一个直观的方法是使用整数或宏定义来表示这些状态,并在函数调用中传递相应的状态码:

// 定义订单状态的宏
#define NEW 0
#define PAID 1
#define SHIPPED 2
#define DELIVERED 3
#define COMPLETED 4
#define CANCELLED 5

void handle_order(int order_status) {
    switch (order_status) {
        case NEW:
            // 新建订单处理逻辑
            break;
            // 其他状态处理逻辑...
    }
}

int main() {
    int order_status = NEW; // 设置初始订单状态为新建
    handle_order(order_status); // 根据订单状态进行处理

    // ...
    return 0;
}

这种方法确实可以实现功能,但它存在几个明显的问题:

  1. 没有明确地表明 NEW、PAID、SHIPPED 等状态码是属于同一种类型,这会降低代码的可读性和可维护性。
  2. 宏定义本质上是文本替换,这意味着在调试程序时,状态信息的名称(如 NEW 或 PAID)将不会保留,仅保留数值(如 0、1)。
  3. 当状态数量较多时,为每个状态创建宏定义赋整数值会变得很繁琐。此外,我们更关心的是状态名称而非其数值。
  4. handle_order函数实际上可以传入任何整数值作为参数,这使得代码具有安全隐患。

鉴于这些问题,C语言提供了一种特殊的数据类型——枚举(enum)。

在C语言中,枚举类型是一种自定义的复合数据类型,它由一组可命名的整数常量组成。每个枚举成员都对应一个整数值,你可以通过成员的名称来直接引用这些整数值。

使用枚举可以让代码更加清晰、类型安全,并且在调试时能够看到状态的实际名称,而不仅仅是一个数字。

定义枚举类型

// 定义一个名为 order_status 的枚举类型,表示订单的不同状态
enum order_status {
    NEW,        // 新订单
    PAID,       // 已支付
    SHIPPED,    // 已发货
    DELIVERED,  // 已送达
    COMPLETED,   // 已完成
    CANCELLED   // 已取消  
};
int main(void) {
    // 声明一个枚举类型变量
    enum order_status status;
    return 0;
}

当然,和定义结构体类型一样,你也可以给枚举类型起别名,这样就可以在声明时去掉enum关键字:

// 定义一个别名为 OrderStatus 的枚举类型,表示订单的不同状态
typedef enum {
    NEW,        // 新订单
    PAID,       // 已支付
    SHIPPED,    // 已发货
    DELIVERED,  // 已送达
    COMPLETED,   // 已完成
    CANCELLED   // 已取消  
} OrderStatus;

int main(void) {
    // 声明一个枚举类型变量,此时可以省略enum关键字
    OrderStatus status;
    return 0;
}

 

初始化枚举类型变量

定义枚举类型后,你可以声明该枚举类型的变量,并将预定义的枚举值赋给它:

OrderStatus status = NEW;

枚举类型的成员本质上就是一个整数,所以它们在内存中都是对应整数值。

在定义枚举类型时,若不主动给枚举成员赋值,那么这些成员的取值将从0开始,向后逐一累加。比如上面的枚举类型OrderStatus:

NEW = 0

PAID = 1

SHIPPED = 2

...

当然你也可以在定义枚举类型时,给成员手动赋值(但没必要这么做)

typedef enum {
    NEW = 888,    
    PAID,   // 自动累加 889    
    SHIPPED,   // 890
    DELIVERED,  
    COMPLETED,  
    CANCELLED   
} OrderStatus;

 枚举类型的优缺点

使用枚举类型的好处是显而易见的:

  1. 增强代码可读性。枚举允许开发者为一组整数值赋予有意义的名字,使得代码更易于理解。
  2. 增强代码的可维护性。一旦需要做出增删修改,你只需要在枚举定义中更改它们,而不是在代码的多个地方进行搜索和替换。

但C语言的枚举类型也有很多限制和不足,主要是:

  1. 枚举类型的成员会被编译器当成整型(一般是int)处理,这意味着C语言的枚举类型不是类型安全的。
  2. 你可以将任何整数赋值给枚举类型变量,甚至不同枚举类型变量之间都可以相互赋值。
  3. 再比如,将枚举类型作为函数的形参,实际上还是可以传参整数值。

比如:

typedef enum {
    RED = 666,
    BLACK,
} Color;

int main(void) {
    // 枚举类型变量可以用任何整数赋值
    OrderStatus status = 100;
    Color c = RED;
    // 枚举类型之间可以互相赋值
    OrderStatus status2 = c;
    return 0;
}

为了避免这些潜在的问题,使用枚举类型枚举类型变量的赋值,应该使用枚举类型中定义的成员,不要使用整数进行赋值,更不应该用其它枚举类型进行赋值。

你可能感兴趣的:(c#,c++)