C_Primer_Plus14.struct

结构和其他数据类型

  • 要点

struct, union, typedef
运算符: ., ->
C 结构,结构模板和结构变量
访问结构成员,编写处理结构的函数
联合和指向函数的指针

创建结构声明

描述书籍的结构体:

struct book {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};

该声明并未创建实际的数据对象,只描述了该对象由什么组成(有时称这为模板)。(C++ 中的模板更加强大)
结构模板可以定义在函数内部,也可以定义在函数外部。其作用域和变量类似,从它声明开始,到块结束为止。若声明在函数外,则具有文件作用域。

定义结构变量

定义以该结构为类型的变量:

struct book b1, b2, * pb;

定义变量时,编译器为变量分配空间。

以下三种种声明方式等价:

struct book b1;
// 等价于:
struct book {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
} b1;
// 等价于: (没有结构标记)
struct {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
} b1;

如果想要多次使用结构模板,则不要省略结构标记;或者,使用 typedef

初始化结构

struct book b1 = {
    "C Primer Plus",
    "Stephen Prata",
    12.6
};

如果要初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式
如果是自动存储期,初始化列表中的值可以不是常量

访问结构成员

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

本质上,. 的作用相当于book结构的下标

结构的初始化器

struct book b2 = {.value = 10.99};
struct book b3 = {
    .value = 33.5,
    .author = "James Broadfool",
    .title = "Rue for the Toad"
};
// 对特定成员的最后一次赋值才是它实际获得的值
struct book b4 = {
    .value = 18.9,      // 被 0.5 覆盖
    .author = "Obama",
    0.5
};

结构数组

struct book library[MAXBKS];
library[1].title = "C Primer Plus";

嵌套结构

在一个结构中包含另一个结构

struct names{
    char first[LEN];
    char last[LEN];
};
struct guy{
    unsigned long id;
    struct names handle;
    char favfood[LEN];
    char job[LEN];
    float income;
};

// 创建变量
struct guy fellow = {
    {"Ewen", "Villard"},
    "grilled salmon",
    "personality coach",
    69000.0
};

指向指针的结构

用指针操作结构的优点:

  • 指向结构的指针通常比结构更容易操控
  • 早期 C 的实现中,结构不能作为参数传递给函数,但可以传递指向结构的指针
  • 即使能传递一个结构,传递指针更有效率
  • 一些用于表示数据的结构中包含指向其他结构的指针
struct guy fellow[2] = {
    {
        { "Ewen", "Villard"},
        "grilled salmon",
        "personality coach",
        68112.00
    },
    {
        {"Rodney", "Swillbelly"},
        "tripe",
        "tabloid editor",
        232400.00
    }
};
struct guy * him;

// 指针访问成员变量方法一:
him = &fellow[0];
him->income += 10.0;

// 方法二:
him = &fellow[1];
(* him).income += 20;

向函数传递结构的信息

ANSI C 允许把结构作为参数使用,所以可以传递结构本身,也可以传递指针。如果只关心结构的一部分,则可以只传递结构成员。

传递结构成员

#include <stdio.h>
#define FUNDLEN 50

struct funds {
    char bank[FUNDLEN];
    double bankfund;
    char save[FUNDLEN];
    double savefund;
};
double sum(double, double);

int main(void){
    struct funds stan = {
        "Garlic-Melon Bank",
        4032.27,
        "Lucky's Savings and Loan",
        8543.94
    };
    printf("Stan has a total of $%.2f.\n",
        sum(stan.bankfund, stan.savefund) );
    return 0;
}
double sum(double x, double y){
    return(x + y);
}

如果想要在函数中改变成员的内容,应使用地址作为形参:

double modify(double* x);
// 调用:
modify(&stan.bankfund);

传递结构地址

#include <stdio.h>
#define FUNDLEN 50

struct funds {
    char bank[FUNDLEN];
    double bankfund;
    char save[FUNDLEN];
    double savefund;
};
double sum(const struct funds *);

int main(void){
    struct funds stan = {
        "Garlic-Melon Bank",
        4032.27,
        "Lucky's Savings and Loan",
        8543.94
    };
    printf("Stan has a total of $%.2f.\n", sum(&stan));
    return 0;
}
double sum(const struct funds * money){
    return(money->bankfund + money->savefund);
}

传递结构

#include <stdio.h>
#define FUNDLEN 50

struct funds {
    char bank[FUNDLEN];
    double bankfund;
    char save[FUNDLEN];
    double savefund;
};
double sum(struct funds moolah);

int main(void){
    struct funds stan = {
        "Garlic-Melon Bank",
        4032.27,
        "Lucky's Savings and Loan",
        8543.94
    };
    printf("Stan has a total of $%.2f.\n", sum(stan));
    return 0;
}
double sum(struct funds moolah){
    return(moolah.bankfund + moolah.savefund);
}

其他特性

把一个相同类型的结构赋值给另一个结构

o_data = n_data

// 用另一个结构初始化
struct names right_field = ("Ruthie", "George");
struct names captain = right_field;

函数返回结构

struct namect getinfo(void){
    struct namect temp;
    printf("Please enter your first name.\n");
    s_gets(temp.fname, NLEN);
    printf("Please enter your last name.\n");
    s_gets(temp.lname, NLEN);
    return temp;
}

结构不同于数组,想要返回一个数组,需要在动态存储区分配内存,在主调函数中销毁。而结构与普通类型用法相似,直接返回即可。

struct namect makeinfo(struct namect info){
    info.letters = strlen(info.fname) + strlen(info.lname);
    return info;
}
// 调用:
person = makeinfo(person);

函数在传递结构时,实际上是生成了一个副本,将副本传递给了函数,所以想要在函数中修改结构实参的成员,应返回修改后的结构。

使用结构指针

结构指针能提高执行速度,不用创建结构副本。如果不想在被调函数中修改结构成员,可以使用 const 关键字。

通常,对小型结构常使用值传递方式(传递结构),对大型结构常使用指针传递方式。

结构中的字符数组和字符指针

struct names{
    char first[20];
    char last[20];
};
struct pnames{
    char* first;
    char* last;
};
struct names veep = {"Talia", "Summers"};
struct pnames treas = {"Brad", "Fallingjaw"};

以上两种结构的区别是字符串存储位置的不同。第一个结构需要40个字节来存储,而第二个结构只需要2个地址(16个字节)来存储。字符常量都存储在常量区,当使用字符数粗时,常量区的内容拷贝到结构存储位置;当使用指针时,常量区的地址赋值给两个指针,共16个字节。

当使用常量,并且不需要改动常量内容时,尽量使用指针,可以节省空间。但是如果不是常量,需要对指针初始化,否则可能会导致程序崩溃。所以在使用指针时要谨慎。比如:

struct pnames attorney;
puts("Enter the last name of your attorney:");
scanf("%s", attorney.last);  // 指针未初始化,可能存在潜在风险

scanf() 把字符串放在 attorney.last 表示的地址上,而它是个未初始化的变量,有可能指向任何地址,可能会导致程序崩溃。

结构、指针和 malloc()

如果使用 malloc() 分配内存并使用指针存储该地址,那么结构中使用指针处理字符串就比较合理。

char temp[LEN];
printf("Enter your first name:\n");
s_gets(temp, LEN);
pst->fname = (char*) malloc(strlen(temp) + 1);

字符串 fname 存储在堆区,结构中存储这字符串的地址。

使用 malloc() 的同时应当配对地使用 free() 函数:

void getinfo(struct name* pst){
    char temp[LEN];
    s_gets(temp, LEN);
    pst->fname = (char*) malloc(strlen(temp) + 1);
    strcpy(pst->fname, temp);
    s_gets(temp, LEN);
    pst->lname = (char*) malloc(strlen(temp) + 1);
    strcpy(pst->lname, temp);
}


void cleanup(struct name* pst){
    free(pst->fname);
    free(pst->lname);
}

复合字面量和结构 (C99)

可以使用符合字面量创建一个数组作为函数的参数或赋给另一个结构:

void show_fino(struct book* pbook);


struct book b1 = (struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99};

show_info(&(struct book) {"C Primer Plus", "Stephen", 12.7});

符合字面量在所有函数外面时,具有静态存储期;在块中时具有自动存储期。符合字面量和普通初始化列表的语法规则相同。

伸缩型数组成员 (C99)

flexible array member
C99 新增特性,具有两个特性:

  • 伸缩型数组不会立即存在
  • 伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样

规则:

  • 伸缩型数组成员必须是结构的最后一个成员
  • 结构中必须至少有一个成员
  • 伸缩型数组的声明类似于普通数组,只是它的方括号中是空的
struct flex{
    size_t count;
    double average;
    double scores[];
};

声明一个 flex 类型的结构时,不能用 scores 做任何事,因为没有给这个数组预留存储空间。C99 的意图并不是声明 struct flex 类型的变量,而是希望你声明一个指向 struct flex 类型的指针,然后用 malloc() 类分配足够的空间来存储结构内的常规内容和伸缩型数组所需的额外空间。假设要声明一个 flex 结构,其中的数组长度为5:

struct flex* pf;
pf = malloc(sizeof(struct flex) + 5 * sizeof(double)));

通常,带有伸缩型数组的结构中,有一个对应的表示数组元素个数的成员,其类型是 size_t

如果要拷贝这种类型的结构,不要直接赋值或拷贝,应该使用 memcpy() 函数:

struct flex* pf1, * pf2;
...
// 错误:
*pf2 = *pf1;
// 正确: (need to check)
// memcpy(pf2, pf1, sizeof(struct flex));
// pf2->scores = pf1->scores;

使用带伸缩型数组的结构的限制:

  • 不能使用结构进行赋值或拷贝
  • 不要以按值方式把结构传递给函数,要把结构地址传递给函数
  • 不要使用带伸缩数组成员的结构作为数组成员或另一个结构成员

匿名结构

在嵌套结构中,如果内嵌的结构没有名称,则成它为匿名结构

struct person{
    int id;
    struct {
        char first[20];
        char last[20];
    };
};
// 声明并初始化变量
struct person ted = {8081, {"Ted", "Grass"}};
// 访问时,可把 first 和 last 看做 person 的成员:
puts(ted.first);

使用结构数组的函数

函数在使用结构时,应当能访问结构模板,即结构模板的定义在函数能访问到的作用域内。

把结构内容保存到文件中

存储在一个结构中的整套信息被称为 记录(record),单独的项被称为 字段(field)

保存结构到文件有许多方法,其中一个不方便的方法是使用 fprintf() 函数:

fprintf(pbooks, "%s %s %.2f\n", b1.title, b1.author, b1.value);

这种方法在检索时还存在问题,因为程序要知道一个字段结束和另一个字段开始的位置。虽然用固定字段宽度的格式可以解决这个问题,但仍显笨拙。

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

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

定位到 b1 结构变量开始的位置,并把结构中所有的字节都拷贝到与 pbooks 相关的文件中。sizeof(struct book) 告诉程序要拷贝的数据块的大小,数字 1 表示一次拷贝一块数据。对应的 fread() 函数该参数是指从文件中一次拷贝一块结构大小的数据,即一个记录。

以二进制表示法存储数据的缺点是,不同的系统可能使用不同的二进制表示法,所以数据文件可能没有移植性。甚至一个系统,不同的编译器设置也可能有不同的二进制布局。

保存结构

程序例:略

链式结构

结构的多种用途之一:创建新的数据形式。计算机用户已经开发出的数据形式比数组和简单结构更有效地解决特定问题:

  • 队列
  • 二叉树
  • 哈希表
  • 图表

他们都是由 链式结构(linked structure)组成。通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。指针把一个结构和另一个结构连接起来,并提供一种能遍历整个彼此链接的结构

这种链式结构比数组在查找时更加高效,遍历次数更少。

联合

union
一种数据类型,能在同一个内存空间中存储不同的数据类型。这里类似于共享内存,但真正存储时只能存储一个值。创建方式与创建结构相同,需要一个联合模板和联合变量:

union hold{
    int digit;
    double bigfl;
    char letter[10];
};
union hold fit;
union hold save[10];
union hold * pu;

一个 hold 类型的变量占10个字节,是以其中占空间最大的成员计算的。但是在存储时,只能存储其中一个成员。这样做的好处是能够用统一的类型存储不同类型的数据。

在数据库存储技术中,结构和联合都是常见的存储方法,联合一般用来节省空间,因为有一些数据项是互斥的,用同一块内存来存储能节省空间。其他用途,比如可以用于分离高低字节,也可以把值存储在一个联合中,然后用另一个成员查看内容,起到别名的作用。使用联合时,不必使用强制类型转换

表示互斥字段例子,用同一个字段表示汽车所属的个人或所属的租赁公司,用 status 字段表示私有还是租赁:

struct owner {
    char socsecurity[12];
    ...
};
struct leasecompany {
    char name[40];
    char headquarters[40];
    ...
};
union data {
    struct owner owncar;Unions: A Quick Look
    struct leasecompany leasecar;
};
struct car_data {
    char make[15];
    int status; // 0 = owned, 1 = leased
    union data ownerinfo;
    ...
};

初始化时,如果没有指定成员指示符,则默认是为第一个成员赋值:

union Data { int i; double x; char str[16]; };

union Data var1 = { 77 },
           var2 = { .str = "Mary" },
           var3 = var1,
           myData[100] = { {.x= 0.5}, { 1 }, var2 };
var1.x = 22.6;

枚举类型

enumerated type
枚举类型 声明符号名称来表示整形常量。只要能使用 int 类型的地方就可以使用枚举类型。枚举类型的目的是提高程序的可读性。声明方式:

enum spectrum {red, orange, yellow, green, blue, voilet};
enum spectrum color;

第一个声明创建了 spectrum 作为标记名,允许把 enum spectrum 作为一个类型名使用,花括号内枚举了所有可能的值。这些符号常量被称为 枚举符(enumerator)。第二个声明使 color 作为该类型的变量。color 的取值是第一个声明中花括号内所有可能的值。

color 的使用方法:

int c;
color = blue;
if (color == yellow){
    ...
}
for (color = red; color <= violet; ++color){
    ...
}

虽然枚举符是 int 类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以存储枚举常量。

C 的枚举类型特性不同于 C++,C 允许枚举变量使用 ++ 运算符,但是 C++ 不允许。

  • 枚举符默认从0开始计数,依次累加1
  • 也可以指定整数值,则后面的值从指定值开始依次累加
    • enum feline {cat, lynx=10, puma, tiger};

共享名称空间

C 语言使用 名称空间 标识程序中的各部分,即通过名称来识别。作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。比如,以下代码不会产生冲突:

struct rect {double x; double y};
int rect;

注意,C 语言中的命名空间和 C++ 中的命名空间不同,上述代码在 C++ 中会产生冲突,因为它把标记名和变量名放在相同的名称空间中。

typedef 简介

利用 typedef 可以为某一类型自定义名称。与 #define 类似,但有不同:

  • typedef 创建的符号受限于类型,不能用于值
  • typedef 由编译器解释,不是预处理器
  • 在受限范围内,typedef 比 #define 更灵活
typedef unsingned char BYTE;
BYTE x, y[10], *z;

也可应用于指针:

typedef char *STRING;
STRING name, sign;
// 等价于:
char *name, *sign;

如果使用 #define,则使用时意义会不同:

#define STRING char *
STRING name, sign;
// 等价于:
char *name, sign

上面声明的变量中 sign 为字符型变量,而不是一个字符指针

用 typedef 来命名一个结构类型时,可以省略该结构的标签:

typedef struct {double x; double y;} rect;
ret r1 = {3.0, .6};
ret r2;
// 等价于:
struct {double x; double y;} r1 = {3.0, .6};
struct {double x; double y;} r2;
r2 = r1;

这两个结构成员完全相同,所以 C 认为他们类型相同。

typedef 还可以用于给复杂的类型命名:

typedef char (* FRPTC ()) [5];

把 FRPTC 声明为一个函数类型,该函数返回一个指针,该指针指向内含5个 char 类型元素的数组

使用 typedef 时要记住,它并没有创建新类型,它只是为某个已存在的类型增加了一个方便使用的标签。

其他复杂声明

除了简单的用其他字符串代替类型名,还有一些复杂的形式:

符号 含义
* 表示一个指针
() 表示一个函数
[] 表示一个数组

优先级

优先级规则:

  • 数组名后的 [] 和 函数名后的 () 优先级相同,他们都比 * 优先级高

例:

int board[8][8];
int ** ptr;
int * risks[10];     // 内含10个元素的数组,每个元素是一个 int 指针
int (* rusks)[10];   // 指向数组的指针,数组内含10个 int 值
int * oof[3][4];     // 3*4 的二维数组,每个元素都指向一个 int 值
int (* uuf)[3][4];   // 指向一个 3*4 二维数组的指针
int (* uof[3])[4];   // 一个内含3个指针的数组,每个指针指向一个内含4个 int 值的数组

对于函数:

char * fump(int);        // 返回字符指针的函数
char (* frump)(int);     // 指向函数的指针,函数的返回类型为 char
char (* flump[3])(int);  // 一个内含3个元素的数组,每个元素都是一个指针,每个指针都指向一个函数

分析数组的小窍门:(ps)
分析他们时可以把中括号往前移动,放在类型后面,然后就一目了然:
int board[8][8] => int[8][8] board
int (*oof)[3][4] => int[3][4] (*oof): 这是个指针,指向的类型是 int[3][4]
int (* uof[3])[4] => int[4] (*[3] uof): 括号为优先,首先是有一个内含3个指针的数组 (*[3] uof),然后每个指针指向一个内含4个int值的数组 int[4]
char (* frump[3])(int) => char(*[3] frump)(int): 一个内含3个指针的数组,每个指针指向函数 frump

typedef 声明数组类型

typedef int arr5[5];
typedef arr5 * p_arr5;
typedef p_arr5 arrp10[10];

arr5 togs;    // 内含5个int值的数组
p_arr5 p2;    // 指向上述数组的指针
arrp10 ap;    // 内含10个上述指针的数组

函数和指针

为什么要创建指向函数的指针这种复杂且难以理解的玩意?有何用处?通常,函数指针常用作另一个函数的参数,告诉该函数要使用哪一个函数。有点类似于OOP中的多态。

使用方法:

// 函数原型:
void ToUpper(char*);
void ToLower(char*);
int round(double);

// main() 函数中:
void (* pf)(char*);
pf = ToUpper;       // 有效
pf = ToLower;       // 有效
pf = round;         // 无效
pf = ToLower();     // 无效
// 调用:
char mis[] = "Nina Metier";
pf = ToUpper;
(* pf)(mis);        // 语法1
pf = ToLower;
pf(mis);            // 语法2

由于历史原因,贝尔实验室的C和 UNIX 的开发者采用第1种语法,而伯克利的 UNIX 推广者却采用第2种语法。K&R C 不允许第二种形式。但是为了与旧代码兼容,ANSI C 认为这两种语法等价,后续的标准也延续了这种矛盾的和谐。

函数指针作为函数参数:

void show(void (*pf)(char*), char* str);

void (* pf)(char*);
pf = ToLower;
show(ToUpper, "hello world!");
show(pf, "hello world!");

void show(void (*pf)(char *), char* str){
    pf(str);        // 有效
    (* pf)(str);    // 有效
}

show() 函数的第一个参数是一个函数指针,第二个参数是一个字符指针。这种方法可以实现传入不同的函数,有点类似于回调函数,也有点类似OOP中的多态。

对函数使用 typedef:

typedef void (* V_FP_CHARP)(char * );
void show(V_FP_CHARP fp, char *);

V_FP_CHARP pfun = ToUpper;
V_FP_CHARP arpf[4] = {ToUpper, ToLower, Transpose, Dummy};
posted @ 2020-08-15 22:12  keep-minding  阅读(137)  评论(0)    收藏  举报