【C语言篇】结构体和其它数据形式

友情链接:C/C++系列系统学习目录

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上



🚀结构体类型

⛳一、概述

🎈(一)什么是结构,为什么要使用结构体

结构,就是程序员自定义的一种“数据类型”,是使用多个基本数据类型、或者其它结构,组合而成的一种新的“数据类型”

有的时候,我们需要表示一些复杂信息时,使用单纯的数据结构类型很不方便,比如:学生信息(学号,姓名,班级,电话,年龄),为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。

🎈(二)定义和使用结构体变量

1.结构的定义(声明)

在C primer plus一书中将此操作描述为声明

struct 结构名 {
	成员类型 成员名;
	成员类型 成员名;
	....
};

//例如:
struct book {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};
  • 该声明并未创建实际的数据对象,只描述了该对象由什么组成。〔有时,我们把结构声明称为模板,因为它勾勒出结构是如何储存数据的。如果读者知道 C++的模板,此模板非彼模板,C++中的模板更为强大。〕

  • 要以struct开头,它表明跟在其后的是一个结构

  • 最后要用分号,表示结构布局定义结束。

  • 各成员之间用分号隔开

  • 结构中的成员允许也是一个结构类型

    struct people {
        struct book a;
        struct book b;
        struct book c;
    };
    

拓展:

在C11中,可以用嵌套的匿名成员结构定义person:

struct person
{
 int id;
 struct {char first[20]; char last[20];}; // 匿名结构
};

初始化的方式相同:

struct person ted = {8483, {"Ted", "Grass"}};

但是,在访问ted时简化了步骤,使用first和last即可:

puts(ted.first);
2.定义结构变量

结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义。

struct book library;
  • 创建了一个结构变量library

  • 编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含 MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一 个名称library结合在一起

在这里插入图片描述

  • struct book所起的作用相当于一般声明中的int或 float。

  • 可以定义两个struct book类型的变量,或者甚至是指向struct book类型结构的指针

  • 声明结构的过程和定义结构变量的过程可以组合成一个步骤。 如下所示,组合后的结构声明和结构变量定义不需要使用结构标记,但是,如果打算多次使用结构模板,就要使用带标记的形式;或者,使用后面介绍的typedef。

    struct { /* 无结构标记 */
        char title[MAXTITL];
        char author[MAXAUTL];
        float value;
    }library;
    
    ----------------------------------------------------------------
    
    struct book library; //就计算机而言,该结构体变量的定义是以下声明的简化
    
    struct book { 
    	char title[MAXTITL]; 
    	char author[AXAUTL]; 
    	float value; 
    }library; /* 声明的右右花括号后跟变量名*/
    

    typedef 可以为某一类型自定义名称,这样便可以使用BOOK代替struct book使用,达到即使没有结构标签名也能多次使用结构模板来定义变量的目的

    typedef struct book { 
     char title[MAXTITL];
     char author[MAXAUTL];
     float value;
    }BOOK;
    
    BOOK library1;
    BOOK library2;
    

    用typedef来命名一个结构类型时,可以省略该结构的标签,但是当结构成员是本身的时候,必须要由结构体标识名来表示,这种情况就不能省略标签,不能用BOOK来表示

    typedef struct{ 
     char title[MAXTITL];
     char author[MAXAUTL];
     float value;
    }BOOK;
    
    //同样可以直接使用BOOK定义结构变量
    BOOK library;
    
    ------------------------------------------------------------------
    
    //当结构成员是本身的时候
    typedef struct{ 
     BOOK example;  //这样是不行的
    }BOOK;
    
    typedef struct book{ 
     struct book example;  //只能这样
    }BOOK;
    
3.访问结构成员

使用结构成员运算符——点(.)可以访问结构中的成员。例如:

library.value   //访问library的value部分。可以像使用任何float类型变量那样使用library.value。

s_gets(library.title, MAXTITL);
scanf("%f", &library.value);

本质上,.title、.author和.value的作用相当于book结构的下标。

当结构成员也是结构时,使用两个.即可:

people.a.title

🎈(三)结构的初始化

初始化变量和数组如下:

int count = 0; 
int fibo[7] = {0,1,1,2,3,5,8};

使用在一对花括号中括起来的初始化列表进行初始化, 各初始化项用逗号分隔。

struct book library = {
    "The Pious Pirate and the Devious Damsel",
    "Renee Vivotte",
    1.95
};

结构中含有结构的初始化:

struct people {
    {"The Pious Pirate and the Devious Damsel","Renee Vivotte",1.95},
    {"THE ABC","QI",2.5},
    {"THE CDE","CHEN",3.0}
};

拓展:指定初始化器

C99和C11为结构提供了指定初始化器,定义的时候我们可以指定初始化的属性,VS/VC不支持,但gcc支持

//可以按照任意顺序使用指定初始化器
struct book library = {
 .title = "The Pious Pirate and the Devious Damsel",
 .value = 1.95
};

⛳二、结构体数组

声明(定义)结构数组:

struct book library[MAXBKS];

以上代码把library声明为一个内含MAXBKS个元素的数组。数组的每个元素都是一个book类型的数组。数组名library本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。

在这里插入图片描述

⛳三、结构体指针

为何要使用指向结构的指针:

  1. 就像指向数组的指针比数组本身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容易操控。
  2. 在一些早期的C实现中,结构不能作为参数传递给函数,但 是可以传递指向结构的指针。
  3. 即使能传递一个结构,传递指针通常更有效率。
  4. 一些用于表示数据的结构中包含指向其他结构的指针。

用法:

  1. 声明(定义)和初始化结构指针

    struct guy * him;
    

    该声明并未创建一个新的结构,但是指针him现在可以指向任意现有的guy类型的结构。例如:

    //如果barney是一个guy类型的结构,
    him = &barney;
    

    如果 fellows 是一个结构数组,数组名就是该数组的地址即首元素的地址,所以,要让 him 指向fellows[0],可以这样写:

    him = fellows
    him = &fellow[0];
    //him指向fellow[0],him+ 1指向fellow[1]
    
  2. 用指针访问成员

    (1)使用 -> 符

    如果him == &barney,那么him->income 即是 barney.income
    如果him == &fellow[0],那么him->income 即是 fellow[0].income
    

    (2)直接解引

    如果him ==&fellow[0],那么*him == fellow[0],因此,可以做以下替代:
    fellow[0].income == (*him).income
    //必须要使用圆括号,因为.运算符比*运算符的优先级高。
    

⛳四、使用结构体传递值

(一)形参是结构变量成员

struct funds {
    char bank[FUNDLEN];
    double bankfund;
    char save[FUNDLEN];
    double savefund;
};

double sum(double x, double y)
{
	return(x + y);
}

struct funds stan = {
    "Garlic-Melon Bank",
    4032.27,
    "Lucky's Savings and Loan",
    8543.94
};
sum(stan.bankfund, stan.savefund));

(二)形参使用结构体指针

如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址:

double sum(const struct funds * money) //由于该函数不能改变指针所指向值的内容,所以把money声明为一个指向const的指针。
{
	return(money->bankfund + money->savefund);
}

sum(&stan.bankfund);//注意,必须使用&运算符来获取结构的地址。和数组名不同,结构体名不是结构体变量的地址

(三)形参直接使用结构变量

一个结构比一个 单独的值复杂,以前的C实现不允许把结构作为参数传递给函数。ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。

double sum(struct funds moolah)/* 参数是一个结构 */
{
	return(moolah.bankfund + moolah.savefund);
}

sum(stan);

(四)返回值使用结构体

现在的C(包括ANSI C),函数不仅能把结构本身作为参数传递,还能 把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信

struct programer{
    char name[32];
    int age;
    int salary;
};

struct programer add_salary(struct programer p, int num){
    p.salary += num;
    return p;
}

struct programer xiaoniu;

strcpy(xiaoniu.name, "小牛");
xiaoniu.age = 28;
xiaoniu.salary = 20000;
    
xiaoniu = add_salary(xiaoniu, 5000);

⛳五、其它

🎈(一)结构体赋值

现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也 就是说,如果n_data和o_data都是相同类型的结构,可以这样做:

o_data = n_data; // 把一个结构赋值给另一个结构

这条语句把n_data的每个成员的值都赋给o_data的相应成员。即使成员是数组,也能完成赋值。另外,还可以把一个结构初始化为相同类型的另一 个结构:

struct names right_field = {"Ruthie", "George"};
struct names captain = right_field; // 把一个结构初始化为另一个结构

🎈(二)结构和结构指针的选择

把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这 种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。 被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSI C 新增的const限定符解决了这个问题。

把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。

🎈(三)结构中的字符数组和字符指针

struct names {
    char first[LEN];
    char last[LEN];
};

//可替代为:
struct pnames {
    char * first;
    char * last;
};

struct names veep = {"Talia", "Summers"};
struct pnames treas = {"Brad", "Fallingjaw"};
printf("%s and %s\n", veep.first, treas.first);
  1. 对于struct names类型的结构变量veep,以上字符串都储存在结构内部, 结构总共要分配40字节储存姓名

  2. 对于struct pnames类型的结构变量 treas,以上字符串储存在编译器储存常量的地方。结构本身只储存了两个地址,在我们的系统中共占16字节。尤其是,struct pnames结构不用为字符串分配任何存储空间。

    注意:

    struct names accountant;
    struct pnames attorney;
    puts("Enter the last name of your accountant:");
    scanf("%s", accountant.last);
    puts("Enter the last name of your attorney:");
    scanf("%s", attorney.last); /* 这里有一个潜在的危险 */
    

    对于律师(attorney),scanf()把字符串放到attorney.last表示的地址上。由于这是未经初始化的变量,地址可以是任何 值,因此程序可以把名放在任何地方。如果走运的话,程序不会出问题,至 少暂时不会出问题,否则这一操作会导致程序崩溃。实际上,如果程序能正常运行并不是好事,因为这意味着一个未被觉察的危险潜伏在程序中。

    因此如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题。

对于注意中的问题,如果使用malloc()分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理:

struct namect {
    char * fname; // 用指针代替数组
    char * lname;
    int letters;
};

void getinfo (struct namect * pst)
{
    char temp[SLEN];
    printf("Please enter your first name.\n");
    s_gets(temp, SLEN);
    // 分配内存储存名
    pst->fname = (char *) malloc(strlen(temp) + 1);
    // 把名拷贝到已分配的内存
    strcpy(pst->fname, temp);
    printf("Please enter your last name.\n");
    s_gets(temp, SLEN);
    pst->lname = (char *) malloc(strlen(temp) + 1);
    strcpy(pst->lname, temp);
}

要理解这两个字符串都未储存在结构中,它们储存在 malloc()分配的内存块中。

🎈(四)把结构内容保存到文件中

由于结构可以储存不同类型的信息,所以它是构建数据库的重要工具。 例如,可以用一个结构储存雇员或汽车零件的相关信息。最终,我们要把这 些信息储存在文件中,并且能再次检索。数据库文件可以包含任意数量的此 类数据对象。储存在一个结构中的整套信息被称为记录(record),单独的 项被称为字段(field)。

#define MAXTITL40
#define MAXAUTL40
struct book {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};
//如果pbook标识一个文件流,那么通过下面这条语句可以把储存在struct book类型的结构变量primer中的信息输出:
fprintf(pbooks, "%s %s %.2f\n", primer.title,primer.author, primer.value);

对于一些结构(如,有 30个成员的结构),这个方法用起来很不方便。另外,在检索时还存在问题,因为程序要知道一个字段结束和另一个字 段开始的位置。虽然用固定字段宽度的格式可以解决这个问题(例如,“%39s%39s%8.2f”),但是这个方法仍然很笨拙。

更好的方案是使用fread()和fwrite()函数读写结构大小的单元。例如:

fwrite(&primer, sizeof(struct book), 1, pbooks);

定位到 primer 结构变量开始的位置,并把结构中所有的字节都拷贝到 与 pbooks 相关的文件中。sizeof(struct book)告诉函数待拷贝的一块数据的大小,1 表明一次拷贝一块数据。带相同参数的fread()函数从文件中拷贝一块结构大小的数据到&primer指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。

🎈(五)结构体内存对齐

🚀联合类型(union)

⛳一、概述

联合(union)也是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。

union hold {
    int digit;
    double bigfl;
    char letter;
};

union hold fit; // hold类型的联合变量
union hold save[10]; // 内含10个联合变量的数组
union hold * pu; // 指向hold类型联合变量的指针
  1. 结构和联合都是由多个不同的数据类型成员组成,但在任何同⼀时刻,联合中只存放了⼀个被选中的成员(所有成员共⽤⼀块地址空间),⽽结构的所有成员都存在(不同成员的存放地址不同)。
  2. 对于联合的不同成员赋值,将会对其它成员重写,原来成员的值就不存在了,⽽对于结构的不同成员赋值是互不影响的。
  3. 第1个声明创建了一个单独的联合变量fit。编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型。

⛳二、初始化联合

有 3 种初始化的方法:把一个联合初始化为另一个同类型的联合;初始 化联合的第1个元素;或者根据C99标准,使用指定初始化器:

union hold valA;
valA.letter = 'R';
union hold valB = valA; // 用另一个联合来初始化
union hold valC = {88}; // 初始化联合的digit成员
union hold valD = {.bigfl = 118.2}; // 指定初始化器

⛳三、使用联合

fit.digit = 23; //把 23 储存在 fit,占2字节
fit.bigfl = 2.0; // 清除23,储存 2.0,占8字节
fit.letter = 'h'; // 清除2.0,储存h,占1字节

点运算符表示正在使用哪种数据类型。在联合中,一次只储存一个值。 即使有足够的空间,也不能同时储存一个char类型值和一个int类型值。和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符:

pu = &fit;
x = pu->digit; // 相当于 x = fit.digit

注意:

fit.letter = 'A';
flnum = 3.02*fit.bigfl; // 错误

以上语句序列是错误的,因为储存在 fit 中的是 char 类型,但是下一行 却假定 fit 中的内容是double类型。

🚀枚举类型(enum)

⛳一、概述

枚举是 C/C++语言中的一种基本数据类型,它可以用于声明一组常数。当一个变量有几个固定的可能取值时,可以将这个变量定义为枚举类型。 比如,你可以用一个枚举类型的变量来表示季节,因为季节只有 4 种可能的取值:春天、夏 天、秋天、冬天。

(一)枚举类型的定义:

enum 枚举名 {枚举元素 1,枚举元素 2,……};

enum Season {spring, summer, autumn, winter};
  • 可以用枚举类型(enumerated type)声明符号名称来表示整型常量(实际上,enum 常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)

  • Season为标记名,把enum Season作为一个类型名使用,花括号内的标识符枚举了spectrum变量可能有的值。这些符号常量被称为枚举符,假设定义一个enum Season类型的变量s,然后便可以这样用:

    s = spring;
    if (s == yellow)
    ...;
    

    枚举符是int类型的符号常量,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。例如,Season的枚举符范围是 0~3,所以编译器可以用unsigned char来表示color变量。

  • 默认情况下,枚举列表中的常量都被赋予为从0、1、2开始

拓展:名称空间(命名空间)

struct rect { double x; double y; };
int rect; // 在C中不会产生冲突

C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别。作用域是名称空间概念的一部分:

  • 两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。
  • 名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与 普通变量使用的空间不同。这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。
  • 尽管如此,以两种不同的方式使用相同的标识符会造成混乱。另外, C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中。

(二)枚举变量的定义:

前面只是定义了枚举类型,接下来就可以利用定义好的枚举类型定义变量,跟结构体一样, 有 3 种方式定义枚举变量

  1. 先定义枚举类型,再定义枚举变量

    enum Season {
    	spring,  //0
    	summer,  //1
    	autumn,  //2
    	winter   //3
    };
    enum Season s;
    

    可以为枚举常量指定整数值:

    enum Season {
    	spring,  
    	summer = 100,  
    	autumn, 
    	winter   
    };
    

    如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续递增的值。以上spring的值为0(默认),summer的值为100,autumn和winter的值为101和101

  2. 定义枚举类型的同时定义枚举变量

    enum Season {
    	spring, 
    	summer, 
    	autumn, 
    	winter
    }s;
    
  3. 省略枚举名称,直接定义枚举变量

    enum Season {
    	spring, 
    	summer, 
    	autumn, 
    	winter
    }s;
    

⛳二、使用枚举

(一)赋值:可以给枚举变量赋枚举常量或者整型值

#include <stdio.h>
int main() {
    // 1.定义枚举类型
    enum Season {spring, summer, autumn, winter} s;
    
    // 2.定义枚举变量
    s = spring; // 等价于 s = 0;
    printf("%d\n", s);
    s = winter;//等价于 s = 3;
    printf("%d\n", s);
    return 0;
}

//打印结果 :
0
3

(二)遍历枚举元素

enum Season {spring, summer, autumn, winter} s;
// 遍历枚举元素
for (s = spring; s <= winter; s++) {
	printf("枚举元素:%d \n", s);
}
输出结果: 枚举元素:0
枚举元素:1
枚举元素:2
枚举元素:3

🚀typedef类型定义

⛳一、概述

typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称。这方面与#define类似,但是两者有3处不同:

  1. 与#define不同,typedef创建的符号名只受限于类型,不能用于值。
  2. typedef由编译器解释,不是预处理器。
  3. 在其受限范围内,typedef比#define更灵活。

用法:

假设要用BYTE表示1字节的数组。只需像定义个char类型变量一样定义BYTE,然后在定义前面加上关键字typedef即可:

typedef unsigned char BYTE;

//随后,便可使用BYTE来定义变量:
BYTE x, y[10], * z;
  • 该定义的作用域取决于typedef定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域。
  • 通常,typedef定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号缩写。当然,也可以用小写: typedef unsigned char byte

作用:

  1. 简化写法,表明意图:

    • 比如用BYTE代替unsigned char表明你打算用BYTE类型的变量表示数字,而不是字符码。

    • 可以把typedef用于结构:

      typedef struct complex {
          float real;
          float imag;
      } COMPLEX;
      

      然后便可使用COMPLEX类型代替complex结构来表示复数。用typedef来命名一个结构类型时,可以省略该结构的标签:

      typedef struct {
      	double x; 
      	double y;
      } rect;
      
  2. typedef常用于给复杂的类型命名。例如, 下面的声明:

    //定义复杂类型的函数
    //把FRPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含 5个char类型元素的数组 
    typedef char (* FRPTC ()) [5];
    
    char (* fun())[5]{ ... }
    FRPTC *pf;    // pf即为指向该函数类型的函数指针
    pf = fun;
    
    //定义指向特定类型函数的指针的别名:
    //新建一个类型PFI,是指向有两个参数、参数类型为char*、返回值为int的函数的指针
    typedef int(*PFI)(char *,char *);
    
    int strcmp(char *s,char *t);
    
    PFI = strcmp;
    
  3. 提高程序的可移植性:

    C标准规定sizeof和time()返回整数类型,但是让实 现来决定具体是什么整数类型。其原因是,C 标准委员会认为没有哪个类型 对于所有的计算机平台都是最优选择。所以,标准委员会决定建立一个新的类型名(如,time_t),并让实现使用typedef来设置它的具体类型。我们移植的时候,只需要修改它的typedef部分即可

⛳二、typedef与#define的不同之处

typedef与#define的主要目的都是为了让代码更加简洁,都有文本替换的功能,二者的不同之处在于,typedef由于可以被编译器理解,所以文本替换也比宏要强大的多,宏只能做简单的文本替换,例如:

typedef char * STRING;

STRING name, sign;
//相当于:
char * name, * sign;

//如果是:
#define STRING char *

STRING name, sign;
//将被翻译成:
char * name, sign;
//这导致只有name才是指针。

行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈七.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值