文章目录
结构体一般定义到main函数外面;跨文件编写代码时,结构体则会定义在一个单独的文件里。总之,结构体应该是全局的,确保每个函数都可以使用它。
1. 创建和初始化
① 无typedef
#include<stdio.h>
// 创建结构体(结构体属于数据类型的一种)
struct Person{
// 成员
char name[50];
int age;
float height;
};
int main(){
// 初始化结构体变量
// struct person是变量类型,zhangsan是变量名
struct Person zhangsan = {"zhangsan", 29, 290.8};
// 创建结构体变量
struct Person wangwu;
return 0;
}
② 有typedef(提及匿名结构体)
#include<stdio.h>
typedef struct Person{
char name[50];
int age;
float height;
}Person;// Person是struct Person的别名
// // 匿名结构体(结构体通常是没有名字的,但是它会有一个别名)
// // 即简单写法:(如果不强调 Person是一个结构体,以后也不怎么会用到这个结构体,就可以这样简单写)
// typedef struct{
// char name[50];
// int age;
// float height;
// }Person;
int main(){
Person zhangsan = {"zhangsan", 29, 290.8};
Person wangwu;
return 0;
}
2. 访问结构体变量成员
#include<stdio.h>
typedef struct Person{
char name[50];
int age;
float height;
}Person;// Person是struct Person的别名
int main(){
Person zhangsan = {"zhangsan", 29, 290.8};
// 方式一: 通过.访问
printf("name is %s, age is %d, height is %.2f\n", zhangsan.name, zhangsan.age, zhangsan.height);
// 方式二: 通过指针访问
Person* zhangsan_ptr = &zhangsan;
printf("name is %s, age is %d, height is %.2f\n", zhangsan_ptr -> name, zhangsan_ptr -> age, zhangsan_ptr -> height);
return 0;
}
3. 案例一: 结构体作为函数参数、匿名结构体
#include<stdio.h>
typedef struct{
char name[30];
int id;
float score;
}Student;
void print_message(Student student);
void update_score1(Student student, float new_score);
void update_score2(Student* student, float new_score);
int main(void){
Student student1 = {"zhangsan", 12, 90.5};
printf("present message:\n");
print_message(student1);
printf("modified message:\n");
// 无效修改
update_score1(student1, 93);
print_message(student1);
// 有效修改
update_score2(&student1, 93);
print_message(student1);
return 0;
}
void print_message(Student student){
printf("name is %s\n", student.name);
printf("id is %d\n", student.id);
printf("score is %.2f\n", student.score);
}
void update_score1(Student student, float new_score){
student.score = new_score;
}
void update_score2(Student* student, float new_score){
student -> score = new_score;
}
5. 结构体作为函数返回值(值语义初始化结构体变量)
值语义(values semantics): 在软件工程中,描述了一个数据拷贝的行为, 然后这个数据还要返回一个副本给调用函数的地方。
优点:安全、简单。只有调用get_point函数时,main函数里的结构体变量(如:my_point)才会被初始化, 不需要直接在main函数给它赋值,降低了数据被意外篡改的风险;局部变量结构体副本P的值被传递到main函数结构体变量的时候, get_point函数就释放掉了,这就意味着旁人无法从外界获得结构体变量p值的地址(10和20的地址)从而去更改初始坐标(理论来说这是反外挂的第一层)。
应用场景:游戏中新手玩家的初始地图位置
工厂模式(面向过程中):此处的get_point函数扮演了类似工厂的角色,封装了Point 的实例。(这里的工厂是通过get_point函数实现的,而不是通过一个类和一个完整的结构体)。
#include<stdio.h>
// 通过值返回结构体
typedef struct{
int x;
int y;
}Point;
Point get_point();
int main(){
// get_point() 返回的副本被赋值给 my_point
// 这里 my_point 是一个全新的 Point 对象, 独立于 get_point() 内部的 p
Point my_point = get_point();
printf("point: (%d, %d)\n", my_point.x, my_point.y);
return 0;
}
Point get_point(){
// 值语义
Point p = {10, 20};
// 返回一个结构体副本(p的一个副本)
return p;
}
6. 结构体数组
#include<stdio.h>
#define POINT_COUNT 3
typedef struct{
int x;
int y;
}Point;
Point* get_default_point();
int main(){
// 写法一: 直接在main函数中赋值
Point points[2] = {
{1, 2},
{3, 4}
};
for(int i = 0; i < 2; ++i){
printf("point%d: (%d, %d)\n", i + 1, points[i].x, points[i].y);
}
printf("---------------------------------------\n");
// 写法二: 调用函数, 结构体数组作为函数返回值
Point* points_ptr = get_default_point();
// for(int i = 0; i < POINT_NUMBER; ++i)
for(int i = 0; i < 3; ++i){
printf("point%d: (%d, %d)\n", i + 1, points_ptr[i].x, points_ptr[i].y);
}
return 0;
}
Point* get_default_point(){
//static Point points[POINT_COUNT] = {
static Point points[] = {
{1, 2},
{3, 4},
{5, 6}
};
return points;
}
7. 嵌套结构体
#include<stdio.h>
typedef struct{
char street[50];
char city[50];
char country[50]; // 这个分号忘写了
}Address;
typedef struct{
char name[50];
int age;
Address address; // 这个分号忘写了
}Person;
// 上述两个分号不写虽然可以运行成功,但是会弹出警告
int main(void){
Person zhangsan = {
"zhangsan",
18,
{"XXXXX1", "XXXX", "China"}
};
printf("name: %s\n", zhangsan.name);
printf("age: %d\n", zhangsan.age);
printf("address: %s %s %s\n", zhangsan.address.street, zhangsan.address.city, zhangsan.address.country);
// 指针
Person* ptr = &zhangsan;
printf("name: %s\n", ptr->name);
printf("age: %d\n", ptr->age);
printf("address: %s %s %s\n", ptr->address.street, ptr->address.city, ptr->address.country);
return 0;
}
8. Enumberation枚举
枚举也是一种数据类型
枚举的好处:使程序逻辑更清晰
枚举广泛应用于定义状态、选项、命令等一组固定的值。例如:表示星期几的枚举、表示方向的枚举(上下左右)、表示HTTP状态码的枚举等
① 输出结果是下标
#include<stdio.h>
typedef enum{
MONDAY, // 0
TUESDAY,
WENSDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}Weekday;
int main(){
Weekday day = FRIDAY;
printf("%d\n", day);
printf("%d\n", SATURDAY);
return 0;
}
② 输出结果是字符串
#include<stdio.h>
typedef enum{
MONDAY, // 0
TUESDAY,
WENSDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}Weekday;
const char* check_date(Weekday day);
int main(){
// 定义一个函数查询
printf("%s\n", check_date(SUNDAY));
return 0;
}
const char* check_date(Weekday day){
switch (day)
{
case MONDAY: return "MONDAY";
break;
case TUESDAY: return "TUESDAY";
break;
case WENSDAY: return "TUESDAY";
break;
case THURSDAY: return "TUESDAY";
break;
case FRIDAY: return "TUESDAY";
break;
case SATURDAY: return "TUESDAY";
break;
case SUNDAY: return "SUNDAY";
break;
default:
break;
}
}
9. Union联合
联合也是一种特殊的数据类型。它允许在相同的内存位置存储不同的数据类型。联合体的所有成员共享一块内存空间,大小等于其最大成员的大小。(这就意味着在任意时刻,联合体只能存储一个成员的值)。
联合体应用场景:一个变量可能存储多种数据类型的数据,但是在一个给定时刻,只使用其中一种的数据类型,这样可以节省内存。(也就是说同一个变量一个名字它可能有多种类型,我要指定它在什么时候是什么类型)
与结构体的异同
联合体和结构体在C语言(以及其他编程语言,如C++)中都是用于将多个不同类型的数据组合在一起的数据结构,选择使用联合体还是结构体取决于具体的应用场景和内存使用的需求。
不同点:
① 内存分配:联合体共享内存,而结构体每个成员都有自己的内存(结构体的大小是其所有成员大小的总和加上可能的填充字节)。联合体修改一个成员的值会影响其他成员,而结构体可以同时访问和修改每个成员而不影响其他成员。
② 用法:联合体适合于需要节省内存且数据类型互斥的场景,而结构体适合于将相关数据组合在一起并同时需要访问多个成员的场景。
相同点:
① 都用于组合不同类型的数据
② 可以通过成员名访问其中的数据
// 情境: 用一个函数打印不同类型的数据,但是我们事先不知道数据的具体类型
// 解决方式: 自己定义一个数据类型,然后将数据按照不同的类型输出
// 实现了联合类型与枚举和结构体的三者连用
// 难点:如何将联合体、结构体、枚举三者结合
#include<stdio.h>
// 意味着Data这个联合体可以存储一个整数,也可以存储一个浮点数,也可以存储一个字符串,它取决于我们当前用到哪种数据类型
// 既然union可以存储不同的数据类型,那么我们就可以通过这种方式(例如: {.int_value = 10 } )来控制它什么时候是什么类型
typedef union{
int int_value;
float float_value;
char* string_value;
}Data;
// 使用枚举来标记存储联合体当中的数据类型
// 使用枚举来跟踪当前存储的数据类型是哪种类型(例如: TypeData data1 = { INT, {.int_value = 10} }意味着data1是INT类型, 对应联合体中的int类型)
typedef enum{
INT,
FLOAT,
STRING
}DataType;
// 结构体包含一个枚举类型成员(帮助明确当前数据的数据类型)和一个联合体类型成员
// 因此我们定义的这个TypeData可以称之为我们自己想要的类型(类似于面向对象中的泛型)。 不像int这种数据类型只能存整型数据, 此处的TypeData可以存储任意整型、浮点型、字符串型数据
typedef struct{
DataType type;
Data data;
}TypeData;
void print_data(TypeData* type_data);
int main(void){
// 写成{.int_value = 10}是因为int_value是data的一个成员变量
TypeData data1 = { INT, {.int_value = 10} };
TypeData data2 = { FLOAT, {.float_value = 3.149}};
TypeData data3 = { STRING, {.string_value = "Hello"}};
print_data(&data1);
print_data(&data2);
print_data(&data3);
return 0;
}
void print_data(TypeData* type_data){
switch (type_data -> type)
{
case INT : printf("Integer: %d\n", type_data -> data.int_value);
break;
case FLOAT : printf("Float: %f\n", type_data -> data.float_value);
break;
case STRING : printf("String: %s\n", type_data -> data.string_value);
break;
}
}
10. 案例(游戏设计):结构体、枚举、联合与多文件编程
① 过程
① 编写 game_types.h 头文件: 定义了一个职业类别枚举、一个敌人类别枚举和一个装备类别的枚举
② 编写 game_abilities.h 头文件: 定义一个联合,来统计游戏当中不同的角色和敌人会有不同的能力和属性(联合可以让我们在同一块内存地址上存储不同的数据类型,节省内存空间),例如一个法师可能会有一个表示魔法强度的浮点数,而一个战士可能会有一个表示体力的整数值。
③ 编写 game_structs.h 头文件:将枚举、联合、结构体三者连用
④ 编写 game_functions.h 头文件:定义函数原型
⑤ 编写 game_functions.c 源文件:定义函数具体内容
⑥ 编写 game.c 源文件:在该文件中编写main函数,作为所有程序的入口点
重难点:游戏架构(设计思路)
② 代码
game_types.h 头文件
#ifndef GAME_TYPES_H
#define GAME_TYPES_H
typedef enum {
Warrior, // 战士
Mage, // 法师
Rogue // 流氓
}CharacterClass;
typedef enum {
Boblin, // 妖怪
Troll, // 巨魔
Dragon // 巨龙
}EnemyType;
typedef enum {
WEAPON, // 武器
POTION, // 药剂
ARMOR // 盔甲
}ItemType;
#endif
game_abilities.h 头文件
#pragma once
#ifndef GAME_ABILITIES_H
#define GAME_ABILITIES_H
#include <stdint.h>
typedef union {
int32_t strength; // 用于战士的力量
float mana; // 用于魔法师的魔法值
int32_t stealth; // 用于流氓的隐藏能力
}Ability;
#endif // GAME_ABILITIES_H(拷贝过来方便导出该文件)
game_structs.h 头文件
#ifndef GAME_STRUCTS_H
#define GAME_STRUCTS_H
#include "game_abilities.h"
#include "game_types.h"
typedef struct {
char name[50];
CharacterClass char_class;
Ability ability;
int32_t level;
int32_t health;
int32_t exp;
}Player;
typedef struct {
EnemyType type;
Ability ability;
int32_t level;
int32_t health;
}Enemy;
typedef struct {
char name[50];
ItemType type;
int32_t power;
}Item;
#endif // !GAME_STRUCTS_H
game_functions.h 头文件
#ifndef GAME_FUNCTIONS_H
#define GAME_FUNCTIONS_H
#include "game_structs.h"
// 函数原型
void create_player(Player* player, const char* name, CharacterClass char_class);
Enemy generate_enemy(EnemyType type, int32_t level);
void battle(Player* player, Enemy* enemy);
void print_player_info(const Player* player);
void print_enemy_info(const Enemy* enemy);
void init();
#endif // !GAME_FUNCTIONS_H
game_functions.c 源文件
#include "game_functions.h"
#include <stdio.h>
#include <string.h>
#include <errno.h>
void create_player(Player* player, const char* name, CharacterClass char_class) {
//strncpy(player->name, name, sizeof(player->name) - 1);
确保字符串以NULL结尾
//player->name[sizeof(player->name) - 1] = '\0';
// 上述写法弹出警告: C4047 “函数” : “rsize_t”与“const char* ”的间接级别不同
// 换成以下写法(与头文件<errno.h>配套使用)
errno_t err;
err = strncpy_s(player->name, sizeof(player->name), name, _TRUNCATE);
if (err != 0) {
printf("Error copying name: %d\n", err);
}
player->char_class = char_class;
player->level = 1;
player->health = 100;
// 根据角色初始化特定的能力(可以用switch或表驱动法)
switch (char_class)
{
case Warrior:
player->ability.strength = 10;
break;
case Mage:
player->ability.mana = 50;
case Rogue:
player->ability.stealth = 20;
break;
}
}
Enemy generate_enemy(EnemyType type, int32_t level) {
Enemy enemy;
enemy.type = type;
enemy.level = level;
enemy.health = level * 20;
switch (type)
{
case Boblin:
enemy.ability.strength = 5* level;
break;
case Troll:
enemy.ability.strength = 10 * level;
break;
case Dragon:
// 此种写法可能会有丢失数据的现象
// enemy.ability.mana = 100 * level;
enemy.ability.mana = 100.0f * (float)level;
break;
}
return enemy;
}
void battle(Player* player, Enemy* enemy) {
printf("%s encounters a level %d %d! A battle begins...\n", player->name, enemy->level, enemy->type);
// 假设战斗总能赢
player->exp += 50;
printf("%s has defeated the enemy and gained 50 exp points!\n", player->name);
}
void print_player_info(const Player* player) {
printf("Player Info: \n");
printf("Name: %s, Class: %d, Level: %d, Health: %d\n", player->name, player->char_class, player->level, player->health);
}
void print_enemy_info(const Enemy* enemy) {
printf("Enemy Info: \n");
printf("Type: %d, level: %d, Health: %d\n", enemy->type, enemy->level, enemy->health);
}
void init() {
puts("Welcome to the Text RTG Game!\n");
Player player;
create_player(&player, "Hero", Warrior);
print_player_info(&player);
Enemy enemy = generate_enemy(Boblin, 10);
print_enemy_info(&enemy);
// 进入游戏循环(谁攻击谁、谁死了谁没死、谁加经验谁没加经验等)
// game_loop();
printf("Game Over!\n");
}
game.c 源文件
#include<stdio.h>
// 这两个文件引入以后,我们就可以整个初始化环境
#include "game_functions.h"
#include "game_structs.h"
int main(void) {
init();
return 0;
}
③ 运行结果
游戏实际开发过程中若要显示具体角色类别(而不是0、1等数字),就需要在枚举文件里去调整或者写一个表驱动法让0(或 1 等数字)等于一个角色的字符串(实现方式:定义一个函数或者定义一个字符串数组保存下标0、1等对应的角色名)。
为了方便起见,实际数据库存储的也是0、1等数字 (用到的时候再把这些数字转化成相应角色名的字符串就好了),而不是角色名的字符串。
④ 小结
游戏开发就是结构体、联合、枚举三者联合使用的一个比较经典的案例。通过这个案例,我们清晰地明白了在c语言中main是所有程序真正的入口点, 而且main函数里的代码应该尽可能少,最主要的代码应该在外层,用到这些代码时只需要在main函数里调用相关函数即可。真实的企业开发也是要分很多文件去写,而不是全部写到main函数里
注意:引用自己写的头文件要用双引号而不是尖括号
⑤ 完善方向
① 游戏开始时有一个死循环(实现情景:有些怪在打斗) – 可以写在一个文件里,后续在main函数里调用
② 增加设计元素:继续添加和修改该游戏的结构体、枚举、联合等部分内容(例如:为敌人增加经验值、等级等属性)
③ 借助函数为游戏添加各种各样的功能,与此同时,main函数要尽可能保证较少的代码量。之前学过的结构体、指针结构体等作为函数返回值,就是为了让我们可以在函数里初始化某些必要内容,main函数需要时只需要调用相关函数即可,没必要全写在main函数里,减少main代码量。
11. 总结
该部分所有内容的重点均在于如何设计和应用