结构和其他数据形式【由浅入深.c】

文章目录


前言

本文介绍结构和其他数据形式相关内容。

(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手——通义,DeepSeek,腾讯元宝等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)

一.结构体类型

  • 结构体变量与结构体指针的定义方式
1. 结构体类型与结构体变量分开定义(最基础方式)
// 定义结构体类型
struct Student {
    char name[50];
    int age;
    float score;
};
// 定义结构体变量(需用struct关键字)
struct Student stu1, stu2;
// 定义结构体指针(需用struct关键字)
struct Student *p1, *p2;  // 指针变量定义
特点:
语法清晰但冗长(每次定义变量/指针都要写struct)
指针定义需额外语句,必须用&初始化:
p1 = &stu1;  // 正确
// p1 = stu1; // 错误(不能直接赋值结构体变量名)




2. 结构体类型与结构体变量同时定义
struct Student {
    char name[50];
    int age;
    float score;
} stu1, stu2;  // 结构体变量
// 定义结构体指针(需额外语句)
struct Student *p1, *p2;
特点:
结构体定义与变量声明合并,代码紧凑
指针需单独定义(不能混在结构体定义中)
初始化示例:
p1 = &stu1;  // 正确





3. 无名结构体与结构体变量同时定义
struct {
    char name[50];
    int age;
    float score;
} stu1, stu2;  // 无结构体名
// 定义结构体指针(需重复结构体定义,不推荐!)
struct {
    char name[50];
    int age;
    float score;
} *p1, *p2;
特点:
不推荐用于指针:指针定义需重复结构体定义,冗余且易错
无法在其他位置复用类型(因无结构体名)
正确做法:避免使用无名结构体定义指针



4. 使用typedef为无名结构体取别名(推荐方式)
typedef struct {
    char name[50];
    int age;
    float score;
} Student;  // 为结构体类型取别名
// 定义结构体变量(无需struct)
Student stu1, stu2;
// 定义结构体指针(无需struct,简洁)
Student *p1, *p2;  // 指针定义
特点:
最常用且简洁(避免每次写struct)
指针定义与变量定义风格一致
初始化示例:
p1 = &stu1;  // 正确




5. 使用typedef定义结构体并指定名称(规范写法)
typedef struct Student {
    char name[50];
    int age;
    float score;
} Student;  // 结构体名与别名相同
// 定义结构体变量
Student stu1, stu2;
// 定义结构体指针
Student *p1, *p2;
特点:
C语言最佳实践(结构体名与别名一致)
指针定义无需额外关键字,与变量定义完全统一
推荐在项目中使用(清晰、无歧义)

struct Student { … } stu1, stu2, *p1, *p2; 这种写法:在某些编译器中可能被接受,但不是标准、清晰的写法.强烈建议不要使用
有tag的结构体(如 struct Student):不能在定义后直接定义指针
匿名结构体(如 struct { … }):可以在定义后直接定义指针

1.1 结构体类型的声明

结构体类型的声明是定义结构体数据结构的关键步骤。结构体是C语言中一种复合数据类型,可以将不同类型的数据组合成一个整体。

语法格式:

struct [结构体名称]
{
    类型名 成员1;
    类型名 成员2;
    // ...
    类型名 成员n;
};

说明:
struct是关键字,是结构体类型的标志
结构体名称是可选的(匿名结构体)
结构体声明是语句,后面必须加上分号
成员名是用户自定义标识符
示例:

// 带名称的结构体声明
struct student {
    char name[20];   // 姓名
    int age;         // 年龄
    float score;     // 分数
};

// 匿名结构体声明(不能直接定义变量,需配合typedef使用)
struct {
    int x;
    int y;
} point;

知识点:

  • 声明 vs 定义:结构体声明"告诉"编译器有这样一种新类型,包含哪些成员,但编译器不会为这些成员分配存储空间;定义则用这个类型"申请"存储空间。

  • 匿名结构体:省略结构体名称后,只能紧跟在结构体类型声明之后进行定义结构体变量。

补充:结构体的前置声明

  1. 基本语法:struct 结构体名; // 前置声明
  2. 为什么需要结构体前置声明
    结构体前置声明主要用于解决头文件循环包含问题:
// A.h
#include "B.h"
struct A {
    struct B *b;  // 需要B的定义,但B.h包含A.h
};


// B.h
#include "A.h"
struct B {
    struct A *a;  // 需要A的定义,但A.h包含B.h
};

上述代码会导致循环包含错误,解决方法是使用前置声明:

// A.h
struct B;  // 前置声明
struct A {
    struct B *b;
};

// B.h
struct A;  // 前置声明
struct B {
    struct A *a;
};
  1. 不完整类型的使用限制
    • 只能用于指针:struct Student *s; 是合法的
    • 不用于变量:struct Student s; 会导致编译错误
    • 不能用于数组:struct Student arr[10]; 会导致编译错误

核心原因:编译器需要知道类型大小,C语言在编译时需要知道每个类型占用的内存大小,以便正确分配内存。不完整类型是编译器只知道类型存在,但不知道其具体结构的类型。

  1. 为什么只能用于指针?
    指针的大小是固定的:在32位系统中指针是4字节,在64位系统中是8字节,与指针指向的类型无关
    编译器不需要知道类型的具体结构:只需要知道这是一个指针类型,就可以分配指针所需的内存空间

1.2定义结构变量

在C语言中,结构体是一种用户自定义的数据类型,用于将不同类型的数据组合成一个有机的整体,便于程序处理。结构体本身不占用内存空间,只是一个数据类型的规范,而结构体变量才是根据该规范在内存中分配空间的实体。

1.2.1 初始化结构

结构体变量的初始化是在定义结构体变量时为其成员赋初值。

  1. 顺序初始化(C89/C90标准)
    特点:必须按照结构体定义时成员的顺序初始化,未初始化的成员:自动初始化为0(整型为0,字符为’\0’,指针为NULL)
struct student s = {"张三", 20, 85.5};
  1. 指定初始化(C99+标准,推荐)
    特点:可以按任意顺序初始化成员,未指定成员:自动初始化为0
struct student s = {
    .name = "李四",
    .age = 18,
    .score = 92.0
};
  1. 定义后逐个赋值初始化
    特点:适用于运行时动态赋值,字符数组:不能直接使用=赋值,需用strcpy()函数,如果初始化列表少于成员数量,剩余成员会被初始化为0
    【在C语言中,字符数组(如char name[20])在内存中表现为一个固定大小的连续空间。数组名本身是一个指针常量(指向数组首元素的地址),不能作为赋值操作的左值。】

注意:
1.字符数组名是一个常量指针,不能被重新赋值。但可以在初始化时候直接赋值,如char a[10] = “hello”; // ✅ 正确:声明时初始化
2.数组名 在大多数情况下会 隐式转换 为指向数组首元素的常量指针,这个转换后的指针是 常量 的,所以不能重新赋值
3.数组名 在表达式中通常 转换为 指向首元素的常量指针, 可以通过这个"常量指针"来 访问和修改 数组元素,如:str[0] = ‘h’; 但不能 重新赋值 这个"常量指针"(改变其指向),也不能对数组名进行 算术运算(如 str++)

💡 如果需要改变"指向",应该使用真正的指针变量

struct student s;
strcpy(s.name, "王五");
s.age = 21;
s.score = 88.8;

1.2.2 访问结构成员

访问结构成员是使用结构体数据的基本操作。

1. 使用点运算符(.),语法格式:
// 直接结构体变量
struct student s = {"张三", 20, 85.5};
printf("姓名: %s", s.name);  // 点运算符
2. 使用箭头运算符(->// 结构体指针
struct student *p = &s;
printf("姓名: %s", p->name);  // 箭头运算符
3. 等价写法
// p->name 等价于 (*p).name
printf("姓名: %s", (*p).name);

1.3 结构数组

1.3.1 声明结构数组

// 先定义结构体类型
struct student {
    char name[20];
    int age;
    float score;
};
// 声明结构数组
struct student class[5];  // 声明5个结构体元素的数组


// 初始化结构数组,顺序初始化
struct student class[5] = {
    {"张三", 20, 85.5},
    {"李四", 21, 90.0},
    {"王五", 19, 88.5},
    {"赵六", 22, 92.0},
    {"钱七", 20, 86.5}
};

1.3.2 标识结构数组的成员

// 访问结构数组元素的成员
printf("学生1姓名: %s", class[0].name);
printf("学生2年龄: %d", class[1].age);

1.4 嵌套结构

嵌套结构体的定义需要先定义内层结构体,然后在外部结构体中使用它作为成员。

1.4.1 基本定义示例

// 先定义内层结构体
struct Address {
    char street[30];
    char city[20];
    char state[2];
    int zipcode;
};

// 在外部结构体中嵌套使用内层结构体
struct Person {
    char name[50];
    int age;
    struct Address home_addr; // 嵌套结构体
};

1.4.2 嵌套结构体的另一种定义方式

也可以在外部结构体定义中直接定义内层结构体:

struct Person {
    char name[50];
    int age;
    struct {
        char street[30];
        char city[20];
        char state[2];
        int zipcode;
    } home_addr; // 匿名结构体(可以是匿名的,也可以是非匿名的。)
};

1.4.2 嵌套结构体的初始化

嵌套结构体的初始化需要逐层进行,按照嵌套层次从外到内进行初始化。

1 直接初始化
struct Person person1 = {
    "张三",
    25,
    {"北京路123号", "北京", "京", 100000}
};



2 嵌套初始化(使用结构体变量)
struct Address addr = {"上海路456号", "上海", "沪", 200000};
struct Person person2 = {
    "李四",
    30,
    addr
};


3 多层嵌套初始化
// 定义三级嵌套结构
struct Date {
    int year;
    int month;
    int day;
};
struct Employee {
    char name[50];
    struct Date birth_date;
    struct {
        char street[30];
        char city[20];
    } address;
};
// 初始化
struct Employee emp = {
    "王五",
    {1990, 5, 15},
    {"广州大道100号", "广州"}
};

1.4.3 访问嵌套结构体成员

访问嵌套结构体成员需要使用点运算符(.)逐层访问:
// 访问嵌套结构体成员
printf("Name: %s\n", person1.name);
printf("Street: %s\n", person1.home_addr.street);
printf("City: %s\n", person1.home_addr.city);
printf("Zipcode: %d\n", person1.home_addr.zipcode);

对于匿名嵌套结构体,访问方式相同:
printf("Street: %s\n", person2.home_addr.street);

1.4.4 结构体也可以嵌套数组

例如:

struct Set {
    int num;
    int fenpei;
    char xing[10];
    char ming[15];
};
struct Plane {
    int num;
    struct Set sets[10]; // 结构体数组
};
// 初始化
struct Plane planes[5] = {
    {1, {{1, 2, "李", "明"}, {3, 4, "王", "芳"}}},
    // ... 其他初始化
};

1.5指向结构的指针

1.5.1 声明和初始化结构指针

声明结构指针
声明一个指向结构的指针的语法格式为:struct 结构名称 *指针变量名;

例如,定义一个名为Student的结构,然后声明一个指向该结构的指针:

struct Student {
    int num;
    char name[20];
    char sex;
    float score;
};
// 声明结构指针
struct Student *pStudent;

初始化结构指针
结构指针的初始化主要有以下几种方式:

(1) 指向已存在的结构变量
struct Student stu1 = {1001, "Zhang", 'M', 90.5};
struct Student *pStudent = &stu1; // 指向已存在的结构变量


(2) 动态分配内存:使用malloc函数在堆上分配内存,然后将指针指向这块内存:
struct Student *pStudent = (struct Student *)malloc(sizeof(struct Student));
if (pStudent == NULL) {
    // 内存分配失败处理
}


(3) 使用类型别名简化声明:使用typedef可以简化结构指针的声明:
typedef struct {
    int num;
    char name[20];
    char sex;
    float score;
} Student;
// 声明结构指针
Student *pStudent;


(4) 通过类型转换初始化:在某些情况下,可能需要将其他类型的指针转换为结构指针:
char *str = "1001,Zhang,M,90.5";
struct Student *pStudent = (struct Student *)str;

1.5.2 用指针访问成员

箭头操作符(->)
访问 结构指针 所指向的结构体成员最常用的方式是使用箭头操作符->,其格式为:指针变量名->成员名
示例:

// 假设pStudent已初始化
pStudent->num = 1002;        // 赋值
printf("%d\n", pStudent->num); // 访问

与点操作符(.)的对比,使用指针访问结构体成员有以下两种方式:

1.使用箭头操作符(推荐):
pStudent->num;

2,使用解引用操作符(*)和点操作符:
(*pStudent).num;

箭头操作符->是C语言提供的简写形式,它等价于(*指针).成员,但更加简洁。
注意:由于操作符优先级的关系,(*pStudent).num需要括号,而pStudent->num不需要。

指针访问嵌套结构体成员

如果结构体中包含嵌套结构体,访问方式如下:
struct Date {
    int year;
    int month;
    int day;
};
struct Student {
    int num;
    char name[20];
    struct Date birthday;
};
// 初始化
struct Student stu = {1001, "Zhang", {1995, 8, 15}};
struct Student *pStudent = &stu;

// 访问嵌套结构体成员
printf("Birthday: %d-%d-%d\n", 
       pStudent->birthday.year, 
       pStudent->birthday.month, 
       pStudent->birthday.day);

指针和结构数组

当结构体是数组时,可以使用指针遍历数组:
struct Student students[3] = {
    {1001, "Zhang", 'M', 90.5},
    {1002, "Wang", 'F', 85.0},
    {1003, "Li", 'M', 92.5}
};
// 使用指针遍历结构数组
struct Student *p = students;
for (int i = 0; i < 3; i++) {
    printf("Student %d: %s, %c, %.1f\n", 
           p->num, p->name, p->sex, p->score);
    p++; // 指针移动到下一个元素
}

或者使用循环:
for (p = students; p < students + 3; p++) {
    printf("Student: %s, %c, %.1f\n", p->name, p->sex, p->score);
}

指针操作示例

#include <stdio.h>
#include <stdlib.h>
struct Student {
    int num;
    char name[20];
    char sex;
    float score;
};
int main() {
    // 动态分配结构体
    struct Student *pStudent = (struct Student *)malloc(sizeof(struct Student));
     // 使用指针访问成员
    pStudent->num = 1001;
    strcpy(pStudent->name, "Zhang");
    pStudent->sex = 'M';
    pStudent->score = 90.5;
      // 打印结构体成员
    printf("Number: %d\n", pStudent->num);
    printf("Name: %s\n", pStudent->name);
    printf("Sex: %c\n", pStudent->sex);
    printf("Score: %.1f\n", pStudent->score);
     // 释放内存
    free(pStudent);
     return 0;
}

指针的常见错误

1.忘记初始化指针:
struct Student *p; // 未初始化的指针
p->num = 1001;     // 会导致段错误


2.错误使用操作符:
(*pStudent).num = 1001; // 正确
*pStudent.num = 1001;   // 错误,因为*的优先级低于.


3.未正确释放内存:
struct Student *p = malloc(sizeof(struct Student));
// ... 使用
// 未释放内存,导致内存泄漏

1.6向函数传递结构的信息

1.6.1 传递结构成员

方法:将结构体的单个成员作为参数传递
示例:

struct Point {
    int x;
    int y;
};
// 传递单个成员
void printX(int x) {
    printf("x = %d\n", x);
}
int main() {
    struct Point p = {10, 20};
    printX(p.x);  // 传递x成员
    return 0;
}

适用场景:当只需要结构体的某个特定属性时,避免传递整个结构体。

1.6.2 传递结构的地址

方法: 传递结构体的指针(地址)

优点:

  • 避免复制整个结构体,提高性能(尤其对大型结构体)
  • 允许函数修改原始结构体
  • 避免内存泄漏风险(相比值传递)

示例:

struct Person {
    char name[50];
    int age;
};
void updateAge(struct Person *p, int newAge) {
    p->age = newAge;  // 修改原始结构体
}
int main() {
    struct Person person = {"John", 30};
    updateAge(&person, 35);
    printf("Updated age: %d\n", person.age);  // 输出35
    return 0;
}
使用->运算符:在指针传递中,使用->访问结构体成员,如p->age。

1.6.3 传递结构

方法: 传递结构体的副本(值传递

优点:

  • 简单直观
  • 函数内部修改不影响原始结构体
  • 适合小型结构体

缺点:

  • 对于大型结构体,复制整个结构体可能效率低下
  • 需要额外的内存开销

示例:

struct Rectangle {
    int width;
    int height;
};
void printArea(struct Rectangle r) {
    printf("Area: %d\n", r.width * r.height);
}
int main() {
    struct Rectangle rect = {10, 20};
    printArea(rect);  // 传递结构体副本
    return 0;
}

1.6.4 其他结构特性

  1. 结构体数组
struct Student {
    char name[50];
    int id;
};
// 结构体数组
struct Student students[10];
// 使用结构体数组的函数
void printStudents(struct Student *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%s (ID: %d)\n", arr[i].name, arr[i].id);
    }
}

2.结构体中的字符数组和字符指针

字符数组:存储在结构体内部,大小固定
struct Person {
    char name[50];  // 固定大小
};



字符指针:指向外部分配的内存,更灵活但需要管理内存
struct Person {
    char *name;  // 需要malloc分配内存
};
// 在函数中分配
void createPerson(struct Person *p, char *name) {
    p->name = (char *)malloc(strlen(name) + 1);
    strcpy(p->name, name);
}

1.6.5 结构和结构指针的选择

传递结构与传递结构指针的特性对比

特性传递结构(值传递)传递结构指针(地址传递)
内存开销需要复制整个结构体只传递指针(4/8字节)
修改能力无法修改原始结构体可以修改原始结构体
适用场景小型结构体,不需要修改大型结构体,需要修改
内存管理无额外内存管理需要小心管理指针内存
代码清晰度简单直观需要理解指针概念

选择建议:
小型结构体(<100字节):优先考虑值传递
大型结构体(>100字节):优先考虑指针传递
需要修改原始结构体:必须使用指针传递

1.6.6 结构、指针和 malloc ()

典型用例:当结构体包含指针成员时,需要使用malloc分配内存

结构体内部成员只要有指针,指针指向的内存空间必须手动分配,除非指针指向已存在的内存(如全局变量、静态变量、字符串常量),但是只读常量不能修改。
切记:除非你确定不对指针做任何改动,否则老老实实的手动分配内存

示例:

struct Person {
    char *name;
    int age;
};

vod createPerson(struct Person *p, char *name, int age) {
    p->age = age;
    p->name = (char *)malloc(strlen(name) + 1);
    strcpy(p->name, name);
}

void freePerson(struct Person *p) {
    free(p->name);
}

int main() {
    struct Person person;
    createPerson(&person, "John Doe", 30);
    printf("Name: %s, Age: %d\n", person.name, person.age);
    freePerson(&person);
    return 0;
}
重要提示:使用malloc分配内存后,必须在适当时候使用free释放,避免内存泄漏。

1.6.7 复合字面量和结构(C99)

复合字面量是C99标准引入的一项重要特性,它提供了一种直接创建匿名结构体或数组的简洁方法。复合字面量允许在代码中不定义临时变量的情况下,直接创建并初始化一个结构体或数组对象。

语法格式 为:(结构体类型/数组类型) { 初始化列表 }

示例

1.数组复合字面量:
(int[2]){10, 20}  // 创建一个包含两个整数的数组

2.结构体复合字面量:
struct Point { int x; int y; };
struct Point p = (struct Point){10, 20};  // 创建Point结构体

3.联合体
union Data {
    int i;
    float f;
    char str[20];
};
// 联合体复合字面量
union Data d = (union Data){.i = 100};
union Data d2 = (union Data){.f = 3.14f};

复合字面量的类型: 复合字面量的类型由括号中的类型决定
对于数组:(int[2])表示类型为"包含2个int的数组"
对于结构体:(struct Point)表示类型为"Point结构体"

重要特性:

  • 复合字面量是匿名的,没有名字
  • 编译器会自动计算数组大小,因此可以省略数组大小
    例如:(int[]){10, 20, 30}会被自动识别为int[3]

详细使用示例

1 数组复合字面量
int *arr = (int[]){10, 20, 30};  // 创建int数组,大小为3
特点:
数组大小由初始化列表中的元素个数决定
无需指定数组大小,编译器会自动计算
适用于临时创建数组,无需额外定义变量




2 结构体复合字面量
struct Point {
    int x;
    int y;
};
// 普通初始化
struct Point p1 = {10, 20};
// 复合字面量初始化
struct Point p2 = (struct Point){.x = 10, .y = 20};  // 指定初始化器
// 传递给函数
void printPoint(struct Point p) {
    printf("Point: (%d, %d)\n", p.x, p.y);
}
printPoint((struct Point){10, 20});  // 直接使用复合字面量作为参数


3 指定初始化器:C99还支持在复合字面量中使用指定初始化器,这使得初始化更清晰:
struct Person {
    char name[50];
    int age;
    float height;
};
// 使用指定初始化器的复合字面量
struct Person p = (struct Person){
    .name = "John Doe",
    .age = 30,
    .height = 1.75
};

复合字面量的生命周期
复合字面量创建的对象是临时的,其生命周期仅限于创建它的那个表达式。

void func(struct Point p) {
    // 处理点
}

int main() {
    // 正确用法:立即使用复合字面量
    func((struct Point){10, 20});  // 临时对象在函数调用中使用
    
    // 错误用法:试图保存临时对象
    struct Point *p = (struct Point *)malloc(sizeof(struct Point));
    *p = (struct Point){10, 20};  // 临时对象被复制到*p中,这是安全的
    
    // 但以下用法是错误的
    struct Point *q = &(struct Point){10, 20};  // 错误!临时对象在表达式结束后就销毁了
    return 0;
}
正确做法:如果需要保存复合字面量创建的对象,必须将其复制到一个持久存储位置(如分配的内存或结构体变量)。

复合字面量的优点

  • 代码简洁:无需定义临时变量
  • 提高可读性:直接在使用点初始化对象
  • 减少错误:避免因临时变量命名不一致导致的错误
  • 支持指定初始化器:使初始化更清晰,特别是对包含多个字段的结构体

注意事项

  • 临时性:复合字面量创建的对象是临时的,所以不能先创建然后在使用它,必须在创建的同时使用它,不能将其地址保存到指针中(除非先复制到持久存储中)。
  • 类型匹配:确保初始化列表与类型匹配。
  • C99及以上标准:复合字面量是C99标准引入的特性,旧版C编译器可能不支持。
  • 与普通结构体初始化的区别:普通初始化需要先定义变量,而复合字面量可以在表达式中直接使用。

1.6.8 伸缩型数组成员(C99)/柔性数组

伸缩型数组成员是C99标准引入的一项重要特性,允许在结构体中定义一个没有指定大小的数组。这个数组的大小可以在运行时根据需要动态确定,从而实现结构体后跟可变大小数组的内存布局。

伸缩型数组成员必须满足以下三个条件:

  • 必须是结构体的最后一个成员, 不能在伸缩型数组成员之后再声明其他成员。
  • 结构体中必须至少有一个其他成员,不能只有伸缩型数组成员,必须至少有一个具有固定大小的成员。
  • 数组的方括号必须为空,声明时不能指定数组大小,即[]是空的。(柔性数组的方括号里也可以有0,这实际上是一种标准的C99实现方式)

声明语法

struct flex {
    int count;             // 固定大小的成员
    double average;        // 固定大小的成员
    double scores[];       // 伸缩型数组成员(必须是最后一个)
};

工作原理
伸缩型数组成员的本质是一种内存布局技巧,它允许我们在分配内存时,为结构体的固定部分和数组部分分配连续的内存空间。C99标准的意图并不是直接声明struct flex类型的变量,而是希望声明一个指向struct flex类型的指针,然后使用 malloc 分配足够的空间,包含结构的固定部分和数组部分。

使用示例

1 基本使用

#include <stdio.h>
#include <stdlib.h>
struct flex {
    int count;
    double average;
    double scores[];  // 伸缩型数组成员
};
int main() {
    int numScores = 5;
    
    // 分配内存:结构体固定部分 + 数组部分
    struct flex *pf = (struct flex *)malloc(sizeof(struct flex) + 
                                         numScores * sizeof(double));    
    if (pf == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }   
    // 设置结构体成员
    pf->count = numScores;
    pf->average = 0.0;
    
    // 初始化数组元素
    for (int i = 0; i < numScores; i++) {
        pf->scores[i] = i * 10.0;  // 假设成绩
        pf->average += pf->scores[i];
    }
    pf->average /= numScores;
    
    // 打印结果
    printf("Number of scores: %d\n", pf->count);
    printf("Average score: %.2f\n", pf->average);
    
    // 释放内存
    free(pf);
    
    return 0;
}




2.一个更实用的例子:成绩计算
#include <stdio.h>
#include <stdlib.h>
struct grades {
    double scores[];  // 伸缩型数组成员
};
int main() {
    int n, i = 0;
    double total = 0;    
    printf("Please enter the number of your subjects!\n");
    scanf("%d", &n); 
    // 分配内存:数组部分的大小
    struct grades *p = (struct grades *)malloc(sizeof(struct grades) + 
                                            n * sizeof(double)); 
    if (p == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    } 
    // 读取每科成绩
    for (; i < n; i++) {
        scanf("%lf", &p->scores[i]);
        total += p->scores[i];
    } 
    printf("The average score is %.2f\n", total / n); 
    free(p);
    return 0;
}

重要特性与限制

1 不能进行结构赋值和拷贝
struct flex *pf1, *pf2;
// ...
*pf2 = *pf1;  // 不要这样做!
原因:结构赋值会复制整个结构,但伸缩型数组成员的大小是动态的,无法正确复制。



2 不能按值传递这种结构
void process(struct flex s) { /* ... */ }
// 不要这样做
process(*pf);
原因:按值传递相当于结构赋值,同样会遇到大小问题。



3 不能将这种结构作为其他结构的成员
struct container {
    struct flex data;  // 不要这样做
};
原因:这会导致结构体嵌套,但伸缩型数组成员的大小是动态的,无法确定。

内存布局
伸缩型数组成员的内存布局是连续的,结构体的固定部分后紧跟数组部分:

+-------------------+
|      struct       |
|      flex         |
|                   |
|  count (int)      |
|  average (double) |
|                   |
+-------------------+
|   scores[] (array)|
|                   |
+-------------------+

当使用malloc分配内存时,分配的大小是:sizeof(struct flex) + n * sizeof(double)

总结
伸缩型数组成员是C99中一个非常有用的特性,它允许在结构体中定义一个大小在 运行时 确定的数组。它的主要优点是:

  • 内存效率高:只分配实际需要的内存
  • 内存连续:结构体和数组在内存中是连续的
  • 简化内存管理:只需一次malloc调用

但需要注意其限制:

  • 必须是结构的最后一个成员
  • 不能进行结构赋值或按值传递
  • 不能作为其他结构的成员
  • 在需要存储可变长度数据的场景中,伸缩型数组成员提供了一个优雅而高效的解决方案。

1.6.9 匿名结构(C11)

匿名结构是C11标准引入的一项重要特性,它允许在结构体中嵌套一个没有名称的结构体。这使得嵌套结构体的成员可以直接作为外层结构体的成员来访问,从而简化了代码结构和访问方式。
语法:

struct outer {
    int member1;
    struct {  // 匿名结构(没有名称)
        int member2;
        char member3;
    };
};

使用匿名结构(C11)

struct person {
    int id;
    struct {  // 匿名结构
        char first[20];
        char last[20];
    };
};
// 初始化
struct person ted = {8483, {"Ted", "Grass"}};
// 访问
puts(ted.first);  // 直接访问

详细工作原理
匿名结构的核心思想是:将嵌套的结构体成员"提升"到外层结构体的命名空间中。这样,嵌套结构的成员可以直接通过外层结构体的实例来访问,就像它们是外层结构体的直接成员一样。

优点

  • 代码简洁:无需为嵌套结构体定义额外的名称
  • 提高可读性:访问嵌套成员时无需额外的层次
  • 减少嵌套层次:避免了多层嵌套的复杂性
  • 简化初始化:初始化语法保持不变,但访问更简单

实际应用示例

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
struct names {
    char first[20];
    char last[20];
};
struct person {
    int id;
    struct names name;  // 嵌套结构成员
};

// 初始化嵌套结构
struct person ted = {8483, {"Ted", "Grass"}};

// 在C11中,可以用嵌套的匿名成员结构定义person
struct person1 {
    int id;
    struct {
        char first[20];
        char last[20];
    };  // 匿名结构
};

// 初始化嵌套结构
struct person1 ted1 = {8483, {"Ted", "Grass"}};

int main(int argc, char *argv[]) {
    // 访问嵌套结构
    puts(ted.name.first);  // 传统方式
    
    // 访问匿名结构
    puts(ted1.first);      // 直接访问,无需通过嵌套名称
    
    system("pause");
    return 0;
}

重要限制与注意事项

限制原因正确用法
不能嵌套在另一个匿名结构中编译器无法识别类型作为结构体的直接成员
不能使用指定初始化器匿名结构没有名称按顺序初始化
C11标准支持C99及更早标准不支持使用C11编译器(-std=c11)
不能用于联合匿名结构体与匿名联合体是不同特性使用union { … };定义匿名联合体
不能进行结构赋值编译器将匿名结构视为不同类型不能直接赋值,需逐个成员赋值

使用匿名结构体的注意事项

匿名结构体在C11标准中引入,使用时需注意其特性与限制。明确其与匿名联合体的区别,避免在联合体中错误使用。初始化时需遵循顺序,不能依赖指定初始化器。

结构赋值操作需谨慎,直接赋值可能导致编译错误。推荐逐个成员赋值以确保类型安全。编译器支持是关键,需确认编译环境是否兼容C11标准。

补充:结构体的浅拷贝和深拷贝

浅拷贝:逐字节拷贝,只拷贝指针本身,不拷贝指针所指向的目标内容。

深拷贝:为结构体的指针成员分配独立堆区空间,然后拷贝内容。

  • 详细对比
特性浅拷贝深拷贝
定义按字节复制结构体内容为指针成员分配新内存并拷贝内容
指针处理只拷贝指针地址,不拷贝内容为指针分配新内存并拷贝内容
内存问题❌ 多次释放同一块内存✅ 每个结构体有自己的内存空间
适用场景结构体中没有指针成员结构体中指针成员
实现方式struct2 = struct1;需要手动实现拷贝函数
  • 浅拷贝示例(有问题)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *name;
    int age;
} Member;

int main() {
    Member member1, member2;
    
    // 为指针分配内存
    member1.name = malloc(64);
    strcpy(member1.name, "John");
    member1.age = 25;
    
    // 浅拷贝:member2.name 指向与 member1.name 相同的内存
    member2 = member1; 
    
    // 问题:释放时会重复释放同一块内存
    free(member1.name);  // 释放第一次
    free(member2.name);  // 重复释放!段错误
    return 0;
}
  • 深拷贝示例(正确做法)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[20];
    char *pname;
    int age;
} Teacher;

void copyStruct2(Teacher *to, Teacher *from) {
    // 先拷贝基本成员
    strcpy(to->name, from->name);
    to->age = from->age;
    
    // 为指针成员分配新内存并拷贝内容
    to->pname = (char *)malloc(20 * sizeof(char));
    strcpy(to->pname, from->pname);
}

int main() {
    Teacher t1, t2;
    
    // 初始化t1
    strcpy(t1.name, "Teacher1");
    t1.pname = (char *)malloc(20 * sizeof(char));
    strcpy(t1.pname, "John");
    t1.age = 45;
    
    // 深拷贝
    copyStruct2(&t2, &t1);
    
    // 释放内存,不会重复释放
    free(t1.pname);
    t1.pname = NULL;
    free(t2.pname);
    t2.pname = NULL;
    
    return 0;
}
  • 为什么需要深拷贝?当结构体包含指针成员时,浅拷贝会导致:
    1. 内存泄漏风险:如果一个结构体释放了指针指向的内存,另一个结构体的指针会变成野指针
    2. 重复释放:两个结构体同时尝试释放同一块内存,导致段错误
    3. 数据不一致:一个结构体修改了指针指向的内容,另一个结构体也会受到影响

当结构体有属性在堆上创建时,直接赋值,释放时堆区上的属性会被重复释放,并且有内存泄漏的风险

  • 浅拷贝 vs 深拷贝总结
情况浅拷贝深拷贝
结构体无指针成员✅ 安全,无问题✅ 无必要,效果相同
结构体有指针成员❌ 有严重问题✅ 正确做法
内存管理不安全,可能导致段错误安全,每个结构体有独立内存
  • 重要提示

    1. C语言默认赋值是浅拷贝struct2 = struct1; 是浅拷贝
    2. 简单结构体(无指针):浅拷贝是安全的
    3. 复杂结构体(有指针)必须使用深拷贝,避免内存问题
    4. 知识库[5]强调:“如果结构体中有指针成员,尽量使用深拷贝”
  • 实际应用

// 适用于有指针成员的结构体
void deepCopy(Student *dest, Student *src) {
    // 拷贝基本成员
    dest->id = src->id;
    strcpy(dest->name, src->name);
    
    // 深拷贝指针成员
    dest->info = (char *)malloc(strlen(src->info) + 1);
    strcpy(dest->info, src->info);
}

记住:在C语言中,结构体的默认赋值是浅拷贝,当结构体包含指针成员时,必须实现深拷贝以避免内存问题。

补充:结构体间的赋值

1. 相同类型结构体赋值
直接赋值:

struct Person p1 = {"John", 30};
struct Person p2;
p2 = p1;  // 整体赋值(内存复制)

原理:逐字节复制内存(等效于memcpy(&p2, &p1, sizeof(p1)))
优点:简洁、避免遗漏成员

2. 不同类型结构体赋值
不能直接赋值(编译错误):

struct A { int x; };
struct B { int y; };
struct A a = {10};
struct B b;
b = a;  // 错误!类型不匹配

正确做法:

// 逐个成员赋值
b.y = a.x;
// 或通过转换函数
void convert(struct A *src, struct B *dst) {
    dst->y = src->x;
}

3. 字符数组成员处理

错误:
struct Person p;
p.name = "John";  // 错误!赋值字符串地址而非内容
正确:
strcpy(p.name, "John");  // 复制内容

4. 指针成员处理
浅拷贝问题:

struct Person {
    char *name;
};
struct Person p1 = {strdup("John")};
struct Person p2 = p1;  // 浅拷贝

// 释放后p2.name失效
free(p1.name);  // 导致p2.name悬空指针

解决方案:深拷贝

p2.name = strdup(p1.name);  // 为新内存分配并复制

5. 结构体指针赋值

指针赋值(地址赋值):
struct Person *p = &p1;  // p指向p1的地址


结构体赋值(内容复制):
struct Person p2 = p1;  // 复制p1的内容到p2

6. 常见错误总结

错误类型风险解决方案
直接字符串赋值内存未分配导致崩溃使用strcpy/strncpy
不同类型结构体赋值数据错位或编译错误显式成员赋值或类型转换
指针成员浅拷贝双重释放或数据竞争手动深拷贝内存

7. 关键原则
相同类型:直接赋值(整体复制)
不同类型:必须逐个成员转换
字符串:必须用strcpy复制内容
指针:赋值是浅拷贝,需深拷贝处理
内存安全:注意指针成员的内存管理

提示:结构体赋值是内存复制,不是"对象复制"。理解这一点对避免内存错误至关重要。

1.6.10 使用结构体数组的函数

  • 结构体数组基础
  1. 结构体定义与初始化
#include <stdio.h>
#include <string.h>

// 定义结构体类型
struct Student {
    int id;
    char name[20];
    float score;
};

// 结构体数组定义与初始化
struct Student students[3] = {
    {1001, "Alice", 95.5},
    {1002, "Bob", 88.0},
    {1003, "Charlie", 92.5}
};
  • 结构体数组函数的三种高效传递方式
  1. 传递结构体指针(推荐方式,最高效)
// 推荐方式:传递结构体指针(避免复制整个数组)
void printStudents(struct Student *s, int n) {
    for (int i = 0; i < n; i++) {
        printf("ID: %d, Name: %s, Score: %.1f\n", 
               s[i].id, s[i].name, s[i].score);
    }
}

// 调用示例
printStudents(students, 3);

为什么推荐struct Student s[] 实际上是传递指针,但明确使用 struct Student *s 更清晰,且避免了不必要的数组复制(尤其对大型数组)。

  1. 传递结构体指针数组(处理多个独立结构体)
void processStudents(struct Student **s, int count) {
    for (int i = 0; i < count; i++) {
        printf("Student %d: %s\n", i + 1, s[i]->name);
    }
}

// 使用示例
struct Student *studentPtrs[3];
for (int i = 0; i < 3; i++) {
    studentPtrs[i] = &students[i];
}
processStudents(studentPtrs, 3);
  1. 使用标准库qsort()进行排序(高效且标准)
// 比较函数,用于指定排序规则
int compare(const void *a, const void *b) {
    const struct Student *s1 = (const struct Student *)a;
    const struct Student *s2 = (const struct Student *)b;
    
    // 按分数降序排序
    if (s1->score < s2->score) return 1;
    else if (s1->score > s2->score) return -1;
    else return 0;
}

// 使用qsort进行排序
qsort(students, 3, sizeof(struct Student), compare);
  • 实用函数示例
  1. 计算平均分(带错误处理)
float calculateAverage(struct Student *s, int n) {
    if (n <= 0) {
        fprintf(stderr, "Error: Invalid student count (%d)\n", n);
        return 0.0f;
    }
    
    float total = 0;
    for (int i = 0; i < n; i++) {
        total += s[i].score;
    }
    return total / n;
}
  1. 高效排序(指针交换,避免结构体复制)
void sortStudents(struct Student *s, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (s[j].score < s[j + 1].score) {
                // 交换指针(O(1)操作,避免结构体复制)
                struct Student *temp = &s[j];
                s[j] = s[j + 1];
                s[j + 1] = *temp;
            }
        }
    }
}

性能对比:当结构体较大时(如包含数组或指针),交换指针比交换整个结构体快10倍以上。

  1. 深拷贝函数(处理字符串成员)
void deepCopyStudents(struct Student *dest, struct Student *src, int n) {
    for (int i = 0; i < n; i++) {
        dest[i].id = src[i].id;
        strcpy(dest[i].name, src[i].name); // 必须用strcpy,避免浅拷贝
        dest[i].score = src[i].score;
    }
}
  • 常见错误
// 错误:交换整个结构体(低效)
void sortError(struct Student s[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (s[j].score < s[j + 1].score) {
                struct Student temp = s[j]; // 低效!复制整个结构体
                s[j] = s[j + 1];
                s[j + 1] = temp;
            }
        }
    }
}
  • 最佳实践
  1. 始终使用指针传递结构体数组

    void func(struct Student *arr, int n); // 推荐
    

    而不是:

    void func(struct Student arr[], int n); // 效率低
    
  2. 排序时交换指针

    // 交换指针(高效)
    struct Student *temp = &s[j];
    s[j] = s[j + 1];
    s[j + 1] = *temp;
    
  3. 处理字符串成员时必须使用strcpy

    strcpy(dest->name, src->name); // 避免浅拷贝
    
  4. 总是检查数组大小

    if (n <= 0) { /* 错误处理 */ }
    
  • 总结:C语言结构体数组函数的核心原则

    1. 指针传递void func(struct Student *arr, int n) 永远优于 void func(struct Student arr[], int n)

    2. 排序技巧:使用标准库qsort()或指针交换实现高效排序,避免结构体复制

    3. 错误处理:总是检查数组大小和边界条件

    4. 深拷贝:当结构体包含字符串等成员时,使用strcpy进行深拷贝

    5. 指针数组:显式初始化指针数组,避免野指针

记住:在C语言中,“用指针传递,用指针排序,用strcpy深拷贝” 是处理结构体数组的黄金法则。这将确保你的代码高效、安全且易于维护。

1.7 结构体内存对齐

1.7.1 内存对齐的原因

  • 硬件要求
    某些硬件平台只能在特定地址访问特定类型数据,否则会抛出硬件异常
    未对齐的内存访问可能需要多次内存访问,而对齐的内存访问只需一次
  • 性能优化
    拿空间换时间:通过增加少量内存(填充字节)换取访问效率提升
    现代CPU对齐访问效率更高,非对齐访问可能产生性能惩罚

1.7.2内存对齐规则

1. 基本规则

  • 第一个成员:在偏移量为0的地址处
  • 其他成员:对齐到 min(编译器默认对齐数,成员大小) 的整数倍
    VS默认对齐数:8,GCC默认对齐数:成员自身大小
  • 结构体总大小:为最大对齐数的整数倍
  • 嵌套结构体:嵌套结构体对齐到自身最大对齐数的整数倍,内层结构体作为一个整体,其对齐数等于其成员中的最大对齐数,外层结构体的对齐数由所有成员(包括内层结构体)的最大对齐数决定。内层结构体的对齐数参与外层结构体的对齐计算。

2. 对齐数计算
对齐数 = min(编译器默认对齐数, 成员变量大小)
3.实例分析
示例1:基本结构体

struct Test1 {
    char c1;    // 1字节
    int i1;     // 4字节
    char c2;    // 1字节
    float f1;   // 4字节
};

内存布局:

```inform7 
偏移 0: [c1]       大小:1  (c1 起始)
偏移 1: [填充]     大小:1  ───┐
偏移 2: [填充]     大小:1    ├─ 3字节填充(为 i1 对齐)
偏移 3: [填充]     大小:1  ───┘
偏移 4: [i1-字节1] 大小:1  ───┐
偏移 5: [i1-字节2] 大小:1    ├─ i1(4字节)
偏移 6: [i1-字节3] 大小:1    │
偏移 7: [i1-字节4] 大小:1  ───┘
偏移 8: [c2]       大小:1  (c2 起始)
偏移 9: [填充]     大小:1  ───┐
偏移10: [填充]     大小:1    ├─ 3字节填充(为 f1 对齐)
偏移11: [填充]     大小:1  ───┘
偏移12: [f1-字节1] 大小:1  ───┐
偏移13: [f1-字节2] 大小:1    ├─ f1(4字节)
偏移14: [f1-字节3] 大小:1    │
偏移15: [f1-字节4] 大小:1  ───┘

c1: 偏移0,占1字节
i1: 需4字节对齐,偏移1→3填充3字节,偏移4开始,占4字节
c2: 偏移8,占1字节
f1: 需4字节对齐,偏移9→11填充3字节,偏移12开始,占4字节
总大小:16字节(需对齐到4的倍数)

示例2:优化后的结构体

struct Test2 {
    int i1;     // 4字节
    char c1;    // 1字节
    char c2;    // 1字节
    float f1;   // 4字节
};

内存布局:

```inform7 
偏移 0: [i1-字节1] 大小:1  ───┐
偏移 1: [i1-字节2] 大小:1    ├─ i1(4字节,对齐满足)
偏移 2: [i1-字节3] 大小:1    │
偏移 3: [i1-字节4] 大小:1  ───┘
偏移 4: [c1]       大小:1  (c1 起始)
偏移 5: [c2]       大小:1  (c2 起始)
偏移 6: [填充]     大小:1  ───┐
偏移 7: [填充]     大小:1    ├─ 2字节填充(为 f1 对齐)
偏移 8: [f1-字节1] 大小:1  ───┐
偏移 9: [f1-字节2] 大小:1    ├─ f1(4字节)
偏移10: [f1-字节3] 大小:1    │
偏移11: [f1-字节4] 大小:1  ───┘
 

i1: 偏移0,占4字节
c1: 偏移4,占1字节
c2: 偏移5,占1字节
f1: 需4字节对齐,偏移6→7填充2字节,偏移8开始,占4字节
总大小:12字节(比Test1节省4字节)

4.内存对齐计算步骤
确定成员偏移:第一个成员偏移0
后续成员偏移 = 上一个成员结束地址 + 填充(使偏移为对齐数的倍数)
计算总大小:总大小 = 最后一个成员结束地址

若总大小不是最大对齐数的倍数,末尾填充至其倍数

5.优化技巧

  1. 成员顺序优化
    原则:将占用空间小的成员集中在一起,将对齐要求严格的成员放在前面
    示例:
// 优化前(16字节)
struct Bad { char a; int b; char c; };

// 优化后(12字节)
struct Good { int b; char a; char c; };
  1. 嵌套结构体优化
    嵌套结构体的对齐数 = min(编译器默认对齐数, 嵌套结构体最大成员大小)
    嵌套结构体本身会按规则对齐

二. 联合体

联合体是C语言中的一种特殊数据类型,它允许在相同的内存位置存储不同的数据类型。联合体的所有成员共享同一段内存空间,这意味着在任意时刻,联合体只能存储其中一个成员的值。

2.1联合体的核心特点

  • 内存共享:所有成员共享同一块内存空间
  • 大小等于最大成员:联合体的大小等于其最大成员的大小
  • 一次只存储一个值:同一时间只能存储一个成员的值
  • 类型跟踪:使用时需要知道当前存储的是哪种类型的数据

2.2联合体类型的声明

  1. 基本语法
union 联合体名 {
    成员1;
    成员2;
    ...
};
  1. 声明示例
union Data {
    int i;        // 整型
    float f;      // 浮点型
    char str[20]; // 字符串
    }; 



union MyUnion;  // 提前声明,不指定成员

2.3 联合体的内存布局

联合体的内存布局是其核心特性。联合体的大小等于其最大成员的大小,因为所有成员共享同一段内存。

例如,对于上面的Data联合体:
int通常占4字节
float通常占4字节
char str[20]占20字节
因此,Data联合体的大小为20字节

2.3.1 联合体与结构体的内存布局对比

特性联合体(Union)结构体(Struct)
对齐要求所有成员中最高对齐要求所有成员中最高对齐要求
大小计算能容纳最大成员的最小值,且是最大对齐要求的整数倍。所有成员大小之和(需考虑对齐填充)
成员偏移量所有成员偏移量为0(共享内存)每个成员有独立偏移量(按声明顺序排列)
内存使用方式同一时间仅存储一个成员的值所有成员同时存储独立的值
修改成员的影响修改一个成员会覆盖其他成员的值修改成员不影响其他成员

关键差异说明

  • 内存分配:联合体仅分配最大成员所需的内存空间,所有成员共享同一地址;结构体分配所有成员内存总和(含对齐填充)。

  • 结构体:成员地址不同,总大小受编译器对齐规则影响,可能存在填充字节。

  • 联合体:成员从同一地址开始,同一时间仅能存储一个成员的值,大小为最大成员的对齐后尺寸。

2.3.2 对齐对联合体的影响

尽管联合体的大小由最大成员( 最大成员"指的是对齐要求最高的成员,而不是大小最大的成员。 )决定,但编译器仍会考虑对齐要求。对齐的目的是提高内存访问效率

联合体的对齐要求:

  • 联合体的对齐方式要适合于联合中所有类型的成员。这意味着联合体的对齐要求是其所有成员中对齐要求最高的那个。
  • 联合体的大小:联合体的大小要大到足够容纳大小最大的成员,并且其大小必须是其对齐要求的整数倍。
  • 联合体的起始地址:联合体的起始地址需满足其对齐要求。例如,若联合体的对齐要求是8字节(来自double成员),则联合体的起始地址必须是8的倍数。
  • 填充字节:编译器可能在联合体末尾添加填充字节以满足对齐要求。
  • 成员偏移量:联合体的所有成员相对于基地址的偏移量都为0(即所有成员共享同一段内存)。

联合体大小的计算

union DATE {
    char a;          // 1字节,1字节对齐
    int i[5];        // 20字节,4字节对齐
    double b;        // 8字节,8字节对齐
};

对齐要求:double要求8字节对齐 → 联合体对齐要求为8字节
大小最大的成员:int i[5](20字节)
联合体大小:20字节不是8的整数倍 → 需要填充到24字节(8 × 3 = 24)

2.4 匿名联合

匿名联合是C语言中允许直接访问联合成员的特性,无需通过联合名称。
语法特点

struct PTValue {
    POINT ptLoc;  // 其他成员
    union {        // 匿名联合 - 没有指定名称
        int  iValue;
        long lValue;
    };
};//并非只能在结构体中使用,其他情况也可
PTValue ptv;
ptv.iValue = 100;  // 直接访问联合成员,无需通过联合名称

在这个例子中,iValue和lValue可以直接通过ptv来访问,而不需要像普通联合那样通过ptv.unionName.iValue的方式。
与普通联合的区别

特性普通联合匿名联合
声明方式union MyUnion { ... };union { ... };(无名称)
访问方式myUnion.iValueptv.iValue(直接访问)
适用场景需要命名联合时需要直接访问成员时

主要限制

  • 成员冲突:匿名联合成员不能与外部成员重名

  • 初始化:需要使用指定初始化器(C99),以确保程序明确初始化哪个成员

struct PTValue ptv = {  .iValue = 100 }; // 明确初始化联合成员iValue
  • C/C++差异:C++支持匿名联合但不支持匿名结构

注意:联合体与结构体在成员访问语法上完全一致:
✅ . 运算符:用于直接访问联合体变量的成员
✅ -> 运算符:用于通过指针访问联合体成员
✅ 可以嵌套:联合体可以包含在结构体中,反之亦然
✅ 语法相同但语义不同:结构体各成员独立,联合体成员共享内存
关键区别记住:
结构体:所有成员都有自己的存储空间
联合体:所有成员共享同一块存储空间,同一时间只能使用一个成员


三. 枚举类型

3.1 枚举类型的声明

枚举类型是一种用户自定义的数据类型,用于表示一组有限的、具有特定含义的常量。在C语言中,枚举类型使用enum关键字声明。

基本语法:

enum 枚举名 {
    枚举值1,
    枚举值2,
    ...
};
//定义枚举类型后,大括号后面必须加一个分号

1. 默认值规则
枚举值的默认值是从0开始依次递增的整数:第一个枚举值默认为0,第二个默认为1,依此类推
// 声明表示一周的枚举类型
enum Weekdays {
    Sunday,   // 默认值为0
    Monday,   // 默认值为1
    Tuesday,  // 默认值为2
    Wednesday,
    Thursday,
    Friday,
    Saturday
};



2.自定义初始值
Sunday 的值为1,Monday 的值为2,Tuesday 的值为3,以此类推
// 自定义初始值,从1开始
enum Weekdays {
    Sunday = 1,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};



3.也可以为多个枚举值指定不同的值:Wednesday = 6,Thursday = 7,Friday = 8,Saturday = 9
enum Weekday {
    Sunday = 1,
    Monday = 3,
    Tuesday = 5,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};

3.2 枚举类型的声明

1.声明枚举变量
enum Weekdays today; // 先定义枚举,后定义变量
today = Monday;      // 赋值


2.在定义时声明变量
enum Weekdays {
    Sunday = 1,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
} today; // 直接定义变量


3.省略枚举名称
// 省略枚举名,直接定义变量
enum {
    Sunday = 1,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
} today;


4.使用枚举值
// 赋值和比较
enum Weekdays today = Tuesday;
if (today == Monday) {
    printf("今天是星期一\n");
}

// 在switch语句中使用
switch (today) {
    case Sunday:
        printf("星期日\n");
        break;
    case Monday:
        printf("星期一\n");
        break;
    // 其他情况
}

3.3 共享名称空间

枚举类型的一个重要特性是枚举类型和枚举值共享同一个名称空间。这意味着:枚举值可以直接使用,无需添加枚举类型名作为前缀

例如:Monday可以直接使用,无需写成Weekdays.Monday
枚举类型名和枚举值在同一命名空间中,这可能导致命名冲突,例如:

enum Color { RED, GREEN, BLUE };
int RED = 100; // 错误:RED已作为枚举值使用

与C++中的枚举类不同,C语言的枚举类型不提供作用域限制,所有枚举值都在全局命名空间中

3.4枚举的遍历

C语言中枚举类型默认是连续的,可以进行遍历:

#include <stdio.h>
enum Weekday {
    Sunday = 1,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};
int main() {
    enum Weekday day;
    for (day = Sunday; day <= Saturday; day++) {
        printf("Day: %d\n", day);
    }
    return 0;
}

注意:如果枚举不连续(如enum { A=0, B=10, C=11 }),则遍历可能不按预期工作。

3.5 注意事项

  • 枚举类型在C语言中被当作整数类型处理,通常与int大小相同
  • 枚举值本质上是整数常量,可以进行整数运算:
enum Color { RED, GREEN, BLUE };
enum Color c = RED;
printf("%d", c + 1); // 输出: 1 (即GREEN)

四. typedef 简介

typedef 是 C/C++ 语言中的一个关键字,用于为已有的数据类型定义一个新的别名。它并不创建新的类型,而是为现有类型提供一个更易读、更简洁的名称。

4.1 typedef 的基本概念

typedef 的核心作用是简化复杂类型声明,提高代码的可读性和可维护性。通过使用 typedef,我们可以为复杂的类型定义一个简单的名称,使代码更加清晰易懂。
基本语法:typedef 原类型名 新类型名;

4.2 typedef 的主要用途

提高代码可读性
通过为数据类型定义有意义的别名,使代码更易于理解。例如:

typedef unsigned int UINT;
UINT count = 0;  // 比 unsigned int count = 0; 更清晰

简化复杂类型声明
对于复杂的数据类型,如结构体、指针和函数指针,typedef 可以大大简化声明:

// 为结构体定义别名
typedef struct {
    int x;
    int y;
} POINT;
POINT p1 = {10, 20};  // 直接使用POINT,无需struct

代码重构更简单
如果需要更改底层数据类型,只需修改 typedef 的定义,而不需要在代码中所有使用该类型的地方进行修改:

typedef long DOUBLE_PRECISION;  // 初始定义
// 后来需要修改为float
typedef float DOUBLE_PRECISION;

定义与平台无关的类型
typedef 可以用来定义与机器无关的类型,使代码在不同平台上更容易移植:

// 在支持long double的平台上
typedef long double REAL;
// 在不支持long double的平台上
typedef double REAL;
// 在不支持double的平台上
typedef float REAL;

与结构体结合使用
typedef 经常与结构体一起使用,为结构体定义新类型名,避免每次声明结构体变量时都写 struct 关键字:

typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;
Employee emp1;  // 不需要写 struct Employee

4.3 typedef 的常见用法示例

1. 为基本数据类型起别名

typedef int myint;      // 为int起别名
typedef float real;     // 为float起别名
typedef char mychar;    // 为char起别名
typedef unsigned int uint; // 为unsigned int起别名

说明:最简单的用法,用更易读的名称替换基本类型。

2. 为结构体类型起别名

// 传统方式
struct Point {
    int x;
    int y;
};

// 用typedef起别名
typedef struct Point {
    int x;
    int y;
} Point;

// 更简洁的方式
typedef struct {
    int x;
    int y;
} Point;

说明:定义结构体后,使用typedef可以避免每次声明变量时都写struct Point

3. 为指针类型起别名

typedef int *IntPtr;    // 为int*起别名
typedef char *PChar;    // 为char*起别名
typedef void *VoidPtr;  // 为void*起别名

说明:为指针类型定义别名,避免重复写*,提高代码可读性。

4. 为函数指针类型起别名

// 传统方式
int (*funcPtr)(int, int);

// 用typedef起别名
typedef int (*FuncPtr)(int, int);
FuncPtr funcPtr;

说明:函数指针的声明非常复杂,typedef可以简化这种声明。

5. 为二维数组类型起别名

// 传统方式
int matrix[3][4];

// 用typedef起别名
typedef int Matrix[3][4];
Matrix matrix;

说明:为二维数组类型起别名,简化二维数组的声明。

6. 为指向二维数组的指针起别名

// 传统方式
int (*ptr)[4] = matrix;

// 用typedef起别名
typedef int (*MatrixPtr)[4];
MatrixPtr ptr = matrix;

说明:二维数组名实际上是指向一维数组的指针,使用typedef可以简化这种复杂指针的声明。

7. 为数组指针起别名

// 传统方式
int (*arrPtr)[5] = &arr;

// 用typedef起别名
typedef int (*IntArrayPtr)[5];
IntArrayPtr arrPtr = &arr;

说明:为数组指针(指向数组的指针)起别名。

8. 为指针数组起别名

// 传统方式
int *arr[5];

// 用typedef起别名
typedef int *IntPtrArray[5];
IntPtrArray arr;

说明:为指针数组(数组中的元素是指针)起别名。

9. 为枚举类型起别名

// 传统方式
enum Color { RED, GREEN, BLUE };

// 用typedef起别名
typedef enum { RED, GREEN, BLUE } Color;
Color c = RED;

说明:为枚举类型起别名,简化枚举变量的声明。

10. 为联合体类型起别名

// 传统方式
union Data {
    int i;
    float f;
    char str[20];
};

// 用typedef起别名
typedef union {
    int i;
    float f;
    char str[20];
} Data;
Data data;

说明:为联合体类型起别名,简化联合体变量的声明。

11. 为复杂类型组合起别名

// 为结构体指针起别名
typedef struct Node {
    int value;
    struct Node *next;
} Node;

// 为指向结构体的指针起别名
typedef Node *NodePtr;

说明:为结构体和指针的组合类型起别名,简化递归数据结构的声明。

12. 为函数指针数组起别名

// 传统方式
int (*funcArray[5])(int, int);

// 用typedef起别名
typedef int (*FuncPtr)(int, int);
typedef FuncPtr FuncArray[5];
FuncArray funcArray;

说明:为函数指针数组起别名,简化函数指针数组的声明。

13. 为指针的指针起别名

typedef int **IntPtrPtr;
IntPtrPtr p;

说明:为指针的指针(二级指针)起别名。

14. 为C++模板类型起别名(C++特有)

template <typename T>
struct MyStruct {
    T value;
};

// 为模板实例化起别名
typedef MyStruct<int> IntStruct;
IntStruct s = {5};

说明:在C++中,typedef可以用于模板实例化。

15. 为常量指针起别名

typedef const int *ConstIntPtr;
ConstIntPtr p;

说明:为指向常量的指针起别名,提高代码可读性。


4.4 typedef 与 #define 的区别

特点对比表

特点typedef#define
处理方式由编译器处理由预处理器处理
类型检查有类型检查没有类型检查
作用域有作用域无作用域
用途用于类型定义用于文本替换

关键区别示例:

// 使用typedef
typedef char* STRING;
STRING s1, s2;  // s1和s2都是char*类型

// 使用#define
#define STRING char*
STRING s1, s2;  // s1是char*,s2是char(不是指针)

注意事项
typedef 并不创建新的类型,它只是为现有类型提供了一个新名称。
通常新类型名使用大写字母,以提醒用户这是类型别名。
typedef 是编译时处理的,而 #define 是预处理时处理的。
typedef 不能用于定义枚举类型(在C中),但C99及以后版本中可以。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值