文章目录
前言
本文介绍结构和其他数据形式相关内容。
(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习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 定义:结构体声明"告诉"编译器有这样一种新类型,包含哪些成员,但编译器不会为这些成员分配存储空间;定义则用这个类型"申请"存储空间。
-
匿名结构体:省略结构体名称后,只能紧跟在结构体类型声明之后进行定义结构体变量。
补充:结构体的前置声明
- 基本语法:
struct 结构体名; // 前置声明 - 为什么需要结构体前置声明
结构体前置声明主要用于解决头文件循环包含问题:
// 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;
};
- 不完整类型的使用限制
- 只能用于指针:struct Student *s; 是合法的
- 不用于变量:struct Student s; 会导致编译错误
- 不能用于数组:struct Student arr[10]; 会导致编译错误
核心原因:编译器需要知道类型大小,C语言在编译时需要知道每个类型占用的内存大小,以便正确分配内存。不完整类型是编译器只知道类型存在,但不知道其具体结构的类型。
- 为什么只能用于指针?
指针的大小是固定的:在32位系统中指针是4字节,在64位系统中是8字节,与指针指向的类型无关
编译器不需要知道类型的具体结构:只需要知道这是一个指针类型,就可以分配指针所需的内存空间
1.2定义结构变量
在C语言中,结构体是一种用户自定义的数据类型,用于将不同类型的数据组合成一个有机的整体,便于程序处理。结构体本身不占用内存空间,只是一个数据类型的规范,而结构体变量才是根据该规范在内存中分配空间的实体。
1.2.1 初始化结构
结构体变量的初始化是在定义结构体变量时为其成员赋初值。
- 顺序初始化(C89/C90标准)
特点:必须按照结构体定义时成员的顺序初始化,未初始化的成员:自动初始化为0(整型为0,字符为’\0’,指针为NULL)
struct student s = {"张三", 20, 85.5};
- 指定初始化(C99+标准,推荐)
特点:可以按任意顺序初始化成员,未指定成员:自动初始化为0
struct student s = {
.name = "李四",
.age = 18,
.score = 92.0
};
- 定义后逐个赋值初始化
特点:适用于运行时动态赋值,字符数组:不能直接使用=赋值,需用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 其他结构特性
- 结构体数组
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;
}
- 为什么需要深拷贝?当结构体包含指针成员时,浅拷贝会导致:
- 内存泄漏风险:如果一个结构体释放了指针指向的内存,另一个结构体的指针会变成野指针
- 重复释放:两个结构体同时尝试释放同一块内存,导致段错误
- 数据不一致:一个结构体修改了指针指向的内容,另一个结构体也会受到影响
当结构体有属性在堆上创建时,直接赋值,释放时堆区上的属性会被重复释放,并且有内存泄漏的风险
- 浅拷贝 vs 深拷贝总结
| 情况 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 结构体无指针成员 | ✅ 安全,无问题 | ✅ 无必要,效果相同 |
| 结构体有指针成员 | ❌ 有严重问题 | ✅ 正确做法 |
| 内存管理 | 不安全,可能导致段错误 | 安全,每个结构体有独立内存 |
-
重要提示
- C语言默认赋值是浅拷贝:
struct2 = struct1;是浅拷贝 - 简单结构体(无指针):浅拷贝是安全的
- 复杂结构体(有指针):必须使用深拷贝,避免内存问题
- 知识库[5]强调:“如果结构体中有指针成员,尽量使用深拷贝”
- C语言默认赋值是浅拷贝:
-
实际应用
// 适用于有指针成员的结构体
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 使用结构体数组的函数
- 结构体数组基础
- 结构体定义与初始化
#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}
};
- 结构体数组函数的三种高效传递方式
- 传递结构体指针(推荐方式,最高效)
// 推荐方式:传递结构体指针(避免复制整个数组)
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更清晰,且避免了不必要的数组复制(尤其对大型数组)。
- 传递结构体指针数组(处理多个独立结构体)
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);
- 使用标准库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);
- 实用函数示例
- 计算平均分(带错误处理)
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;
}
- 高效排序(指针交换,避免结构体复制)
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倍以上。
- 深拷贝函数(处理字符串成员)
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;
}
}
}
}
- 最佳实践
-
始终使用指针传递结构体数组:
void func(struct Student *arr, int n); // 推荐而不是:
void func(struct Student arr[], int n); // 效率低 -
排序时交换指针:
// 交换指针(高效) struct Student *temp = &s[j]; s[j] = s[j + 1]; s[j + 1] = *temp; -
处理字符串成员时必须使用strcpy:
strcpy(dest->name, src->name); // 避免浅拷贝 -
总是检查数组大小:
if (n <= 0) { /* 错误处理 */ }
-
总结:C语言结构体数组函数的核心原则
-
指针传递:
void func(struct Student *arr, int n)永远优于void func(struct Student arr[], int n) -
排序技巧:使用标准库
qsort()或指针交换实现高效排序,避免结构体复制 -
错误处理:总是检查数组大小和边界条件
-
深拷贝:当结构体包含字符串等成员时,使用
strcpy进行深拷贝 -
指针数组:显式初始化指针数组,避免野指针
-
记住:在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.优化技巧
- 成员顺序优化
原则:将占用空间小的成员集中在一起,将对齐要求严格的成员放在前面
示例:
// 优化前(16字节)
struct Bad { char a; int b; char c; };
// 优化后(12字节)
struct Good { int b; char a; char c; };
- 嵌套结构体优化
嵌套结构体的对齐数 = min(编译器默认对齐数, 嵌套结构体最大成员大小)
嵌套结构体本身会按规则对齐
二. 联合体
联合体是C语言中的一种特殊数据类型,它允许在相同的内存位置存储不同的数据类型。联合体的所有成员共享同一段内存空间,这意味着在任意时刻,联合体只能存储其中一个成员的值。
2.1联合体的核心特点
- 内存共享:所有成员共享同一块内存空间
- 大小等于最大成员:联合体的大小等于其最大成员的大小
- 一次只存储一个值:同一时间只能存储一个成员的值
- 类型跟踪:使用时需要知道当前存储的是哪种类型的数据
2.2联合体类型的声明
- 基本语法
union 联合体名 {
成员1;
成员2;
...
};
- 声明示例
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.iValue | ptv.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及以后版本中可以。
986

被折叠的 条评论
为什么被折叠?



