在 C 语言中,除了基础的数据类型,构造数据类型为我们处理复杂数据提供了强大工具,而位运算和内存管理则是优化程序性能、避免内存问题的关键。本文将从结构体数组出发,深入讲解共用体、枚举、位运算符及内存管理的核心知识与实践技巧。
一、结构体数组:批量处理复杂对象
结构体数组是由相同结构体类型元素组成的集合,非常适合批量管理具有多个属性的对象(如学生、员工等)。
1. 结构体数组的定义与初始化
定义形式:
结构体类型 数组名[元素个数];
,其中元素个数必须是常量。例如,定义一个包含 3 名学生的数组:
struct student { char name[32]; // 姓名 char sex; // 性别('m'/'f') int age; // 年龄 int score; // 成绩 }; struct student stu_array[3]; // 包含3个学生的数组
初始化方式:
- 全部初始化:按顺序为每个元素的成员赋值。
struct student stu_array[3] = { {"zhangsan", 'm', 19, 100}, {"lisi", 'f', 18, 90}, {"wanger", 'm', 19, 60} };
- 指定元素初始化:通过
[索引]
指定特定元素的成员,未指定的元素默认初始化为 0。struct student stu_array[3] = { [1] = {.name = "zhangsan", .score = 90} // 仅初始化第2个元素 };
- 全部初始化:按顺序为每个元素的成员赋值。
2. 结构体数组的访问与传参
- 元素访问:通过
数组名[索引].成员名
访问单个成员,例如stu_array[0].age = 20;
。 - 传参技巧:结构体数组传参时,推荐传递地址(指针),避免大结构体的拷贝开销。函数定义形式为:
- 示例:遍历打印学生信息
void print_students(struct student *pstu, int len) { for (int i = 0; i < len; i++) { printf("姓名:%s,年龄:%d\n", pstu[i].name, pstu[i].age); } }
3.实战代码
#include<stdio.h>
struct student {
char name[32];
char sex;
int age;
int score;
};
int GetAllstu(struct student *pstu,int len)
{
int i = 0;
for(i = 0;i < len;i++)
{
#if 0
//指针++ ->访问
gets(pstu->name);
scanf(" %c",&pstu->sex);
scanf("%d",&pstu->age);
scanf("%d",&pstu->score);
pstu++;
//指针+i ->访问
gets((pstu+i)->name);
scanf(" %c",&(pstu+i)->sex;
scanf("%d",&(pstu+i)->age);
scanf("%d",&(pstu+i)->score);
#endif
//直接用p[i] 访问
gets(pstu[i].name);
// scanf("%s",pstu[i].name);//不会接收 空格和'\0'
//字符串接收不用加'&'
scanf(" %c",&pstu[i].sex);
scanf("%d",&pstu[i].age);
scanf("%d",&pstu[i].score);
getchar();//加入 getchar()函数,清除每一次的 \n
}
return 0;
}
int PutAllstu(struct student *pstu,int len)
{
int i =0;
for(i = 0;i < len;i++)
{
printf("姓名:%s\n",pstu->name);
printf("性别:%c\n",pstu->sex);
printf("年龄:%d\n",pstu->age);
printf("成绩:%d\n",pstu->score);
pstu++;
#if 0
printf("姓名:%s\n",(*(pstu+i)).name);
printf("性别:%c\n",(*(pstu+i)).sex);
printf("年龄:%d\n",(*(pstu+i)).age);
printf("成绩:%d\n",(*(pstu+i)).score);
printf("姓名:%s\n",(pstu+i)->name);
printf("性别:%c\n",(pstu+i)->sex);
printf("年龄:%d\n",(pstu+i)->age);
printf("成绩:%d\n",(pstu+i)->score);
printf("姓名:%s\n",pstu[i].name);
printf("性别:%c\n",pstu[i].sex);
printf("年龄:%d\n",pstu[i].age);
printf("成绩:%d\n",pstu[i].score);
#endif
printf("===========================\n");
}
return 0;
}
int main(void)
{
struct student st[3] = {
{"zhangsan",'m',18,90},
{"lisi",'f',16,70},
{"wanger",'m',18,80},
};
struct student stu[3] = {
[0] = {
.name = "zhaowu",
.score = 80,
},
[2] = {
.name = "maliu",
.score = 70,
},
};
struct student s[3];
GetAllstu(s,3);
PutAllstu(s,3);
return 0;
}
#include<stdio.h>
union s {
char a;
short b;
int c;
};
int main(void)
{
union s s1;
s1.a = 'A';
printf("a:%c\n",s1.a);
s1.b = 100;
printf("b:%d\n",s1.b);
s1.c = 1000;
printf("c:%d\n",s1.c);
s1.a = 'A';
printf("a:%c\n",s1.a);
//上一个值会被下一个值覆盖
printf("s1.a = %c\n",s1.a);
printf("s1.b = %d\n",s1.b);
printf("s1.c = %d\n",s1.c);
return 0;
}
联合体内存共享
联合体s
的所有成员(char a
,short b
,int c
)共享同一块内存区域,内存大小由最大成员int c
(4字节)决定。任何成员被赋值都会覆盖其他成员的值。内存覆盖过程
分步解析代码操作(假设系统为小端字节序):s1.c = 1000;
1000的十六进制为0x000003E8
,小端存储模式在内存中的布局:低地址 -> 高地址 0xE8 0x03 0x00 0x00
s1.a = 'A';
(ASCII值为65 = 0x41)
仅覆盖内存的第一个字节(低地址):
此时内存实际值为十六进制数0x41 0x03 0x00 0x00 // 原0xE8被覆盖为0x41
0x00000341
。
读取成员时的解释
s1.a
:读取第1字节 →0x41
→ 字符'A'
(正确输出)。s1.b
:读取前2字节 →0x0341
(小端模式)
0x0341
十进制 = 3 × 256 + 65 = 833。s1.c
:读取全部4字节 →0x00000341
(小端)
0x341
十六进制 = 3 × 256 + 65 = 833。
二、共用体(联合体):共享内存的灵活类型
共用体(union)与结构体的最大区别是所有成员共享同一块内存空间,适合在不同场景下使用不同类型访问同一段数据。
1. 共用体的定义与特性
- 定义形式:
union 共用体名 { 数据类型1 成员1; 数据类型2 成员2; ... }; {insert\_element\_4\_}
- 核心特性:
- 所有成员共享同一块内存,空间大小等于最大成员的大小。
- 修改一个成员会覆盖其他成员的值(因内存共享)。
2. 经典应用:判断内存大小端
内存大小端是指多字节数据在内存中的存储顺序:
- 小端存储:低地址存放数据的低字节(如
0x11223344
在内存中为44 33 22 11
)。 - 大端存储:低地址存放数据的高字节(如
0x11223344
在内存中为11 22 33 44
)。
利用共用体判断大小端的代码:
#include<stdio.h>
union s {
char a;
short b;
int c;
};
int main(void)
{
union s s1;
s1.a = 'A';
printf("a:%c\n",s1.a);
s1.b = 100;
printf("b:%d\n",s1.b);
s1.c = 1000;
printf("c:%d\n",s1.c);
s1.a = 'A';
printf("a:%c\n",s1.a);
//上一个值会被下一个值覆盖
printf("s1.a = %c\n",s1.a);
printf("s1.b = %d\n",s1.b);
printf("s1.c = %d\n",s1.c);
return 0;
}
联合体内存共享
联合体s
的所有成员(char a
,short b
,int c
)共享同一块内存区域,内存大小由最大成员int c
(4字节)决定。任何成员被赋值都会覆盖其他成员的值。内存覆盖过程
分步解析代码操作(假设系统为小端字节序):s1.c = 1000;
1000的十六进制为0x000003E8
,小端存储模式在内存中的布局:低地址 -> 高地址 0xE8 0x03 0x00 0x00
s1.a = 'A';
(ASCII值为65 = 0x41)
仅覆盖内存的第一个字节(低地址):
此时内存实际值为十六进制数0x41 0x03 0x00 0x00 // 原0xE8被覆盖为0x41
0x00000341
。
读取成员时的解释
s1.a
:读取第1字节 →0x41
→ 字符'A'
(正确输出)。s1.b
:读取前2字节 →0x0341
(小端模式)
0x0341
十进制 = 3 × 256 + 65 = 833。s1.c
:读取全部4字节 →0x00000341
(小端)
0x341
十六进制 = 3 × 256 + 65 = 833。
三、枚举:增强代码可读性的常量集合
枚举(enum)用于定义一组命名常量,使代码更易读、维护。
1. 枚举的定义与特性
定义形式:
enum 枚举名 { 常量1, 常量2, ... }; {insert\_element\_8\_}
默认规则:
- 第一个常量默认值为 0,后续常量值为前一个 + 1。
- 可手动指定常量值,未指定的则延续前一个值。
示例:
enum week { MON, // 0 TUE, // 1 WED = 5, // 手动指定为5 THU // 6(5+1) };
2. 应用场景
枚举常用于表示状态、选项等固定值集合,例如:
enum status {
SUCCESS, // 0:成功
ERROR, // 1:错误
TIMEOUT // 2:超时
};
// 使用枚举使代码更清晰
int connect() {
if (/* 连接成功 */) return SUCCESS;
else if (/* 超时 */) return TIMEOUT;
else return ERROR;
}
3.实战代码
#include<stdio.h>
enum weekday {
//默认第一个数为0
//按顺序递增
MONDAY ,
TUESDAY,
WEDNESDAY = 9,
THURDAY,
FRIDAY,
SATURSDAY,
SUNDAY,
};
int main(void)
{
enum weekday day;
printf("请输入今天是周几:\n");
scanf("%d", (int *)&day);
//将枚举类型转换为int类型
switch (day)
{
case MONDAY:printf("尾号1和6限行\n");break;
case TUESDAY:printf("尾号2和7限行\n");break;
case WEDNESDAY:printf("尾号3和8限行\n");break;
case THURDAY:printf("尾号4和9限行\n");break;
case FRIDAY:printf("尾号5和0限行\n");break;
case SATURSDAY:
case SUNDAY:
printf("不限行\n");
}
return 0;
}
四、位运算符:直接操作内存的高效工具
位运算符用于对二进制位直接操作,在底层编程、性能优化中广泛应用。
1. 常用位运算符
运算符 | 含义 | 规则 |
---|---|---|
& | 按位与 | 对应位均为 1 则为 1,否则为 0(与 0 得 0) |
| | 按位或 | 对应位有 1 则为 1(或 1 置 1) |
^ | 按位异或 | 对应位不同则为 1,相同为 0 |
~ | 按位取反 | 0 变 1,1 变 0 |
<< | 左移 | 各二进制位左移 n 位,高位丢弃,低位补 0(等价于 ×2ⁿ) |
>> | 右移 | 各二进制位右移 n 位,低位丢弃,高位补符号位(等价于 ÷2ⁿ) |
2. 实用技巧
- 置位操作:将某一位设为 1(例如第 3 位):
int num = 0; num |= (1 << 3); // 1<<3 为0b1000,与num或运算后第3位变为1
- 清位操作:将某一位设为 0(例如第 2 位):
num &= ~(1 << 2); // ~(1<<2) 为0b11111011,与num与运算后第2位变为
- 不使用临时变量交换两数:
int a = 3, b = 5; a ^= b; // a = 3^5 b ^= a; // b = 5^(3^5) = 3 a ^= b; // a = (3^5)^3 = 5
#include<stdio.h>
int main(void)
{
int a = 100;
int b = 200;
//异或实现不使用中间变量交换
a = a ^ b;
printf("a = %d\n",a);
b = a ^ b;//a b b = a ^ 0
printf("b = %d\n",b);
a = a ^ b;//a b a b b = 0 ^ b
printf("a = %d\n",a);
printf("a = %d,b = %d\n",a,b);
return 0;
}
五、内存管理:避免泄露与优化性能
C 语言中,内存管理由程序员手动控制,堆区空间的申请与释放是重点。
1. 内存区域划分
程序运行时的内存分为以下区域:
- 栈区:存放局部变量,由操作系统自动管理(进入作用域时分配,离开时释放)。
- 堆区:由程序员手动申请(
malloc
)和释放(free
),空间较大,生命周期灵活。 - 数据段:存放全局变量、静态变量,编译时分配,程序结束后释放。
- 文本段:存放函数代码和指令,只读。
2. 堆区操作函数
malloc:申请堆区空间
void *malloc(size_t size); // size为字节数,成功返回首地址,失败返回NULL{insert\_element\_19\_}
示例:申请一个能存放 5 个 int 的数组
int *arr = (int*)malloc(5 * sizeof(int)); if (arr == NULL) { // 必须检查申请是否成功 printf("内存申请失败\n"); return -1; }
free:释放堆区空间
void free(void *ptr); // ptr为malloc返回的地址{insert\_element\_20\_}
注意:
- 释放后需将指针置为
NULL
,避免野指针(指向已释放空间的指针)。 - 不能重复释放同一空间,否则会导致程序崩溃。
- 释放后需将指针置为
3. 内存泄露及其避免
- 内存泄露:只申请堆区空间而不释放,导致可用内存逐渐减少,最终可能使程序崩溃。
- 避免方法:
- 遵循 “谁申请,谁释放” 原则。
- 使用完堆区空间后立即用
free
释放,并将指针置空。 - 复杂场景下可使用内存泄露检测工具(如 Valgrind)。
4.实战代码
1==============
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
//指针初始化为NULL
int *p = NULL;
char *s = NULL;
s = malloc(32);
p = malloc(4);
if(NULL == p || NULL == s)
{
printf("malloc failed\n");
return -1;
}
*p = 100;
// s = "hello world";//错误//字符串只读区
//str = hello world (虽然可以输出)
//free(): invalid pointer
//Aborted (core dumped)
//free(s) 试图释放只读区的字符串常量触发段错误
strcpy(s,"hello world");
printf("str = %s\n",s);
free(s);
printf("*p = %d\n",*p);
//需要释放堆区空间
free(p);
return 0;
}
2=================
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//封装函数实现字符串的输入
char *fun(void)
{
//char str[] = "hello world";//错误
//子函数运行结束后会被回收
char *str = NULL;
str = malloc(32);
if(NULL == str)
{
printf("malloc failed\n");
return NULL;
}
strcpy(str,"hello world");
return str;
}
int main(void)
{
char *p = NULL;
p = fun();
printf("p = %s\n",p);
free(p);//释放后再置为NULL
//防止产生野指针
//指针指向的内存已被释放,
//但指针仍保留原地址值。
p = NULL;
return 0;
}
六、综合运用
//有一个班的4个学生,有5课程。
// 1、求第一门课的平均分;
// 2、找出有两门以上课程不及格的学生,
// 输出他们的学号和全部课程成绩及平均分
// 3、找出平均分在90分以上或全部课程成绩
// 在85分以上的学生。
// 分别编写三个函数来实现以上三个要求
#include<stdio.h>
struct student{
char name[32];
int n;
int score[5];
};
float avg(struct student *ps,int stu)
{
int sum = 0;
int i = 0;
for(i = 0;i < 5;i++)
{
sum += ps[stu].score[i];
}
return (float)sum / 5.0;
}
int Averagescore(struct student *ps,int class)
{
int i = 0;
int sum = 0;
for(i = 0;i < 4;i++)
{
sum += ps[i].score[class];
}
printf("第%d门课的平均分 = %.2f\n",class+1,(float)sum / 5.0);
printf("==============================\n");
return 0;
}
int fun1(struct student *ps)
{
int i = 0;
int j = 0;
for(i = 0;i < 4;i++)
{
int un = 0;
int stu = -1;
for(j = 0;j < 5;j++)
{
if(ps[i].score[j] < 60)
un++;
if(un > 2)
{ stu = i;
break;
}
}
if(-1 != stu)
{ printf("%s有两门以上课程不及格\n",ps[i].name);
printf("学号:%d\n",ps[i].n);
printf("课程成绩:");
for(j = 0;j < 5;j++)
printf("%d ",ps[i].score[j]);
printf("\n平均分:%.2f\n",avg(ps,i));
printf("==============================\n");
}
}
return 0;
}
int fun2(struct student *ps)
{
int m = 0;
int i = 0;
int j = 0;
for(i = 0;i < 4;i++)
{
for(j = 0;j < 5;j++)
{
if(ps[i].score[j] <= 85)
break;
}
if(j == 5 || avg(ps,i) > 90)
{ if(m == 0)
printf("平均分在90以上或全部成绩在85以上的学生有:\n");
printf("%s ",ps[i].name);
m++;
}
}
if(m == 0)
printf("没有平均分在90以上或全部成绩在85以上的学生");
printf("\n");
printf("==============================\n");
return 0;
}
int main(void)
{
struct student s[4] ={
{"zhangsan",2001,{50,55,70,36,90}},
{"lisi",2002,{89,56,98,77,87}},
{"wanger",2003,{99,97,83,96,79}},
{"chenxi",2004,{85,33,24,94,34}},
};
Averagescore(s,0);
fun1(s);
fun2(s);
return 0;
}