有时要传很多数据
如何使用联合
C语言可以处理很多不同类型的数据:小数字、大数字、浮
点数、字符与文本。但现实世界中的事物往往需要一条以上
的数据来记录。比如:
/* 打印目录项 */
void catalog(const char *name, const char *species, int teeth, int age)
{
printf("%s is a %s with %i teeth. He is %i\n",
name, species, teeth, age);
}
/* 打印贴在水缸上的标签 */
void label(const char *name, const char *species, int teeth, int age)
{
printf("Name:%s\nSpecies:%s\n%i years old, %i teeth\n",
name, species, teeth, age);
}
int main()
{
catalog("Snappy", "Piranha", 69, 4);
label("Snappy", "Piranha", 69, 4);
return 0;
}
两个不同的函数都传了
相同的4条
数据,显然这种方式是累赘的。怎么才能解决这个问题?
用结构创建结构化数据类型
如果需要把一批数据打包成一样东西,就可以使用结构
(struct)。 struct 是structured data type(结构化数据类型)
的缩写。有了结构,就可以像下面这样把不同类型的数据写
在一起,封装成一个新的大数据类型:
struct fish {
const char *name;
const char *species;
int teeth;
int age;
};
结构与数组有些相似,除了以下两点:
1.结构的大小固定。
2.结构中的数据都有名字。
给结构赋值
和新建数组很像,你只需
要保证每条数据按照它们在结构中定义的顺序出现即可:
struct fish snappy = { "Snappy", "Piranha", 69, 4};
问: fish结构会保存字符串吗?
答: 在这个例子中不会,这里的
fish结构中只保存了字符串指针,也
就是字符串的地址,字符串保存在存
储器中其他位置。
问: 但还是可以把整个字符串保存在结构中吧?
答: 对,只要把字符串定义成字
符数组就行了,像char name[20];。
只要把“鱼”给函数就行了
现在,你只要把新的自定义数据传给函数就行了,而不必传
递一大批零散的数据。
/* 打印目录项 */
void catalog(struct fish f)
{
...
}
/* 打印贴在水缸上的标签 */
void label(struct fish f)
{
...
}
int main(){
struct fish snappy = { "Snappy", "Piranha", 69, 4};
catalog(snappy);
label(snappy);
}
把参数封装在结构中,代码会更稳定。
把数据放在结构中传
递有一个好处,就是
修改结构的内容时,
不必修改使用它的函数。比如
要在fish中多加一个字段:
struct fish {
const char *name;
const char *species;
int teeth;
int age;
- // 新增字段
int favorite_music;
};
catalog()和label()知道有
人会给它们一条fish,但却不
知道fish中现在有了更多的数
据,它们也不关心,只要fish
有它们需要的所有字段就行了。
这就意味着,使用结构,不但
代码更好读,而且能够更好地
应对变化------>早期的面向对象思想体现。
使用“.”运算符读取结构字段
struct fish snappy = { "Snappy", "piranha", 69, 4};
printf("Name = %s\n", snappy.name); // snappy.name这是snappy中的name属性
问: 数组变量就是一个指向数组的指针,那么结构变量是一个指向结构的指针吗?
答: 不是,结构变量是结构本身的名字。
问: 结构就相当于其他语言中的类?
答: 它们很相似,但在结构中添加方法可就没那么
容易了。
存储器中的结构
在定义结构时,你并没有让计算机在存储器中创建任何东西,
只是给了计算机一个模板,告诉它你希望新的数据类型长什
么样子。
struct fish {
const char *name;
const char *species;
int teeth;
int age;
};
当定义新变量时,计算机则需要在存储器中为结构的实例创建
空间,这块空间必须足够大,以装下结构中的所有字段:
struct fish snappy = { "Snappy", "Piranha", 69, 4};
那么当把一个结构变量赋给另一个结构变量时会发生什么?
计算
机会创建一个全新的结构副本,也就是说,计算机需要再分配一
块存储器空间,大小和原来相同,然后把每个字段都复制过去。
struct fish snappy = { "Snappy", "Piranha", 69, 4};
struct fish gnasher = snappy;
切记:为结构变量赋值相当于叫计算机复制数据。
注意:字符串字段复制的是指向字符串的
指针,而非字符串本身。
当把一个结构变量赋给
另一个结构变量,计算
机会复制结构的内容。如果结构中
含有指针,那么复制的仅仅是指针
的值,像这里,gnasher和snappy
的name和species字段指向相同字
符串。
结构中的结构
既然结构可以用现有数据类型创建数据类型,也就能用其他
结构创建结构。举个例子:
struct preferences {
const char *food;
float exercise_hours;
};
struct fish {
const char *name;
const char *species;
int teeth;
int age;
struct preferences care; //结构中的结构,即嵌套
};
上述代码向计算机描述了一个结构中的结构。你可以像之前
一样用数组语法创建变量,但现在可以在数据中包含结构中
的结构。
struct fish snappy = { "Snappy", "Piranha", 69, 4, { "Meat", 7.5}};
一旦把结构组合起来,就可以使用一连串的“.”运算符来访
问字段:
printf("Snappy 喜欢吃 %s", snappy.care.food);
printf("Snappy 喜欢锻炼 %f hours", snappy.care.exercise_hours);
为什么要嵌套定义结构?
之所以要这么做是为了对
抗复杂性。通过使用结
构,我们可以建立更大的
数据块。通过把结构组合
在一起,我们可以创建
更
大的数据结构。本来你只
能用int、short,但有
了结构以后,就可以描述
十分复杂的东西,比如网
络流和视频图像。
用typedef为结构命名
在C语言中可以为结构创建别名,你只要在 struct 关键字前
加上 typedef ,并在右花括号后写上类型名,就可以在任何
地方使用这种新类型。cell_
phone
是结构名,phone
是类型名。
一般一个就够了,为了简便通常会省略结构名。
typedef struct cell_phone { // 这里的
cell_phone 可以省略int cell_no;
const char *wallpaper;
float minutes_of_charge;
} phone; // phone将成为“struct cell_phone”的别名。
... phone p = {5557879, "sinatra.png", 1.35};
示例代码:
#include <stdio.h>
struct exercise {
const char *description;
float duration;
};
struct meal {
const char *ingredients;
float weight;
};
typedef struct {
struct meal food;
struct exercise exercise;
} preferences;
typedef struct fish { // 这里的fish 是结构名,如果有了类型名,这个结构名可以省略
const char *name;
const char *species;
int teeth;
int age;
preferences care;
} fish; // 这里的fish是类型名,后续可以直接像int ,long 那样去用
void catalog(struct fish f) {
printf("%s is a %s with %i teeth. He is %i '\n",
f.name,f.species,f.teeth,f.age);
}
/* 打印贴在水缸上的标签 */
void label(struct fish f) {
printf("Name:%s\nSpecies:%s\n%i years old, %i teeth\n",
f.name, f.species, f.teeth, f.age);
printf("Feed with %4.2f lbs of %s and allow to %s for %2.2f hours\n",
f.care.food.weight, f.care.food.ingredients,
f.care.exercise.description,f.care.exercise.duration);
}
int main() {
// struct fish snappy = {
fish snappy = {
"Snappy",
"Piranha",
64,
4,
{ { "meat",0.2},{ "swim in the jacuzzi",7.5}}
};
catalog(snappy);
label(snappy);
return 0;
}
运行结果:
问: 结构字段在存储器中是紧挨着摆放的吗?
答: 有时两个字段之间会有小的
空隙。
问: 为什么?
答: 计算机总是希望数据能对齐
字边界(word boundary)。如果计算
机的字长是32位,就不希望某个变量
(比如short)跨越32位的边界保存。
问: 所以计算机会留下一道空隙,然后在下一个32位字开始的地方保存short?
答: 是的。
问: 也就是说,每个字段都占用一整个字?
答: 不一定,计算机在两个字段
之间留出空隙仅仅是为了防止某个字
段跨越字边界。如果几个字段能放在
一个字中,计算机就会那么做。
问: 为什么计算机如此在意字边界?
答: 计算机按字从存储器中读
取数据,如果某个字段跨越了多个
字,CPU就必须读取多个存储器单元,
并以某种方式把读到的值合并起来。
问: 这样会很慢吗?
答: 会很慢。
问: 在Java那样的语言中,如果我把对象赋给变量,它不会复制对象,仅仅复制引用,为什么C语言不这样做?
答: 在C语言中,所有赋值都会
复制数据,如果你想复制数据的引用,
就应该赋指针。
如何更新结构
先看个例子:
#include <stdio.h>
typedef struct {
const char *name;
const char *species;
int age;
} turtle;
void happy_birthday(turtle t)
{
t.age = t.age + 1;
printf("Happy Birthday %s! You are now %i years old!\n",
t.name, t.age);
}
int main()
{
turtle myrtle = { "Myrtle", "Leatherback sea turtle", 99};
happy_birthday(myrtle);
printf("%s's age is now %i\n", myrtle.name, myrtle.age);
return 0;
}
运行结果:
奇怪的是,我明明在happy_birthday函数中将age的值+1了,可为什么还是99?
代码克隆了乌龟
原因是在C语言中,参数是按值传递给函数的。也就是说,当调用函
数时,传入函数的值会赋给形参,因此当把一个结构
赋给另一个时,
结构的值就被复制
到了新结构中。所以
这段代码等价于:
void happy_birthday(turtle t)
{
...
}
...
happy_birthday(myrtle);
- // 上面代码等价于下面
==> turtle t = myrtle;
搞了半天 ,原来你修改的只是副本的值,原结构还是原封不动。那想修改原结构的值咋办?
你需要结构指针
如果想让函数更新结构变量,就不能
把结构作为参数传递,因为这样做仅仅是将数据的副本复制给
了函数。
取而代之,你可以传递结构的地址----指针,如:
void happy_birthday(turtle *t) //表示“有人要给我一个结构指针(地址)
{
...
}
...
happy_birthday(&myrtle); // 你将把myrtle变量的地址传给函数
修改之前的错误代码:
#include <stdio.h>
typedef struct {
const char *name;
const char *species;
int age;
} turtle;
turtle happy_birthday(turtle *t) {
(*t).age = (*t).age + 1;
printf("Happy Birthday %s! You are now %i years old!\n",
(*t).name, (*t).age);
// 注意 (*t).age != *t.age = *(t.age)。
// return t;
}
// t->age = (*t).age,“表示这是由指针t指向的结构体中的 age字段”,因此下面还可以这样写
turtle happy_birthday_new(turtle *t) {
t->age = t->age + 1; // 注意有的编译器这样写报错,要写成(*t)->age才行,如androidstudio的ndk
printf("Happy Birthday %s! You are now %i years old!\n",
t->name, t->age);
// 注意 (*t).age != *t.age = *(t.age)。
// return t;
}
int main() {
turtle myrtle = { "Myrtle", "Leatherback sea turtle", 99};
// myrtle = happy_birthday(myrtle);
happy_birthday(&myrtle);
printf("%s's age is now %i\n", myrtle.name, myrtle.age);
return 0;
}
运行结果:
注意(*t).age和*t.age的区别!
为什么 *t 外面一定要加括号?
因为 (*t).age 与 *t.age
完全是两个不同的表达式。
表达式 *t.age 等于 *(t.age) 。请思考一下表达式 *(t.
age) 的含义。它代表“ t.age 这个存储器单元中的内容”,
但 t.age 不是存储器单元。
另一种表示结构指针的方法,它更易于阅读。
t -> age 代表 (*t).age。有的编译器是(*t)->age。
即:“指针->字段”等于“(*指针).字段”。ƒ“->”表示法省掉了括号,代码更易阅读。
代码示例:
怎么能让你得到字符串“GOLD!”
#include <stdio.h>
typedef struct {
const char *description;
float value;
} swag;
typedef struct {
swag *swag;
const char *sequence;
} combination;
typedef struct {
combination numbers;
const char *make;
} safe;
int main() {
swag gold = { "GOLD!", 1000000.0};
combination numbers = {&gold, "6502"};
safe s = { numbers, "RAMACON250"};
printf("password= %s",s.numbers.swag->description);
}
运行结果:
同一类事物,不同数据类型
可以用结构来模拟现实世界中错综复杂的事物,但有些数据不止
一种数据类型:
假如想记录某样东西的“量”,既可以用个数,也可以用重量,
或者用容积。所以可以在一个结构中创建多个字段:
typedef struct {
...
short count;
float weight;
float volume;
...
} fruit;
这不是好主意,原因有以下几点:
1.结构在存储器中占了更多空间。
2.用户可能设置多个值。
3.没有叫“量”的字段。
要是能这样就好了:
定义一种叫“量”的数据类型,然后根据特
定的数据决定要保存个数、重量还是容积。
在C语言中,可以用联合做到这点。
联合可以有效使用存储器空间
每次创建结构实例,计算机都会在存储器中相继摆放字段,联合则不同。当定义联合时,计算机只为其中一个字段分
配空间。假设你有一个叫 quantity 的联合,它有三个字
段,分别是 count 、 weight 和 volume ,那么计算机就会
为其中最大的字段分配空间,然后由你决定里面保存
什么
值。无论设置了 count 、 weight 和 volume 中的哪个字段,
数据都会保存在存储器中同一个地方。
typedef union { // 联合看起来很像结构,但用的是union关键字
short count;
float weight;
float volume;
} quantity;
1.C89方式
如果联合要保存第一个字段的值,就可以用C89表示
法,只要用花括号把值括起来,就可以把值赋给联合
中第一个字段。
quantity q = { 4};
2.指定初始化器
指定初始化器(designated initializer)按名设置联合字
段的值:
quantity q = {.weight=1.5};
3.“点”表示法
第三种设置联合值的方法是在第一行创建变量,然后
在第二行设置字段的值。
quantity q;
q.volume = 3.7;
切记,无论用哪种方法设置联合的值,都只会保存一
条数据。联合只是提供了一种让你创建支持不同数据
类型的变量的方法。
问: 为什么联合的大小取决于最长
的字段?
答: 计算机需要保证联合的大小固
定。唯一的办法就是让它足够大,任何
一个字段都能装得下。
问: 为什么C89表示法只能设置
第一个字段?如果我传给联合float值,
为什么不把它设为第一个float字段?
答: 这么做是为了避免歧义。假
设你有一个float字段和一个double
字段,那么计算机应该把{2.1}保存成
float还是double呢?
每次都把值保
存在第一个字段中,你就知道数据是怎
么初始化的。
注意:
可以用“指定初始化器”按名
设置结构和联合字段,它属于
C99标准。绝大多数现代编译
器都支持“指定初始化器”。
typedef struct {
const char *color;
int gears;
int height;
} bike;
bike b = {.height=17, .gears=21};
联合常和结构一起用
创建联合相当于创建新的数据类型,也就是说可以在任何地
方使用它的值,就像使用整型或结构那样的数据类型。例如,
可以把联合和结构结合起来:
typedef struct {
const char *name;
const char *country;
quantity amount; // 联合
} fruit_order;
- // 这里连用了两次指定标识符。.amount用来初始化结构中的字段,.weight用来初始化.amount中的字段。
fruit_order apples = { "apples", "England", .amount.weight=4.2};
printf("This order contains %2.2f lbs of %s\n", apples.amount.weight, apples.name);
问题:可以在联合中保存各种可能的值,但保存以后,就无法知道它的类型。
编译器不会记录你在联合中设置或读取过哪些字段。我们完全
可以设置一个字段,读取另一个字段,但有时这会造成很严重
的后果。
#include <stdio.h>
typedef union {
float weight;
int count;
} cupcake;
int main() {
cupcake order = { 2}; // 程序员无意中设置了weight,而不是count。
printf("Cupcakes quantity: %i\n", order.count); // 明明设置了weight,却读取了count。
return 0;
}
运行结果:
很明显不是想要的结果。
你需要某种方法记录我们在联合中保存了什么值。C
程序员常用的一种技巧是创建枚举。
枚举变量保存符号
有时你不想保存数字或文本,而是想保存一组符
号。如果你想记录一周中的某一天,只想保存MON
DAY、TUESDAY、WEDNESDAY……这些符号。你不需
要保存文本,因为一共只有七种不同的取值。
这就是为什么要发明枚举的原因。
有了枚举,就可以创建一组这样的符号:
enum colors { RED, GREEN, PUCE}; // 可以用typedef为类型起个名字。
//注意:结构与联合用分号(;)来分割数据项,而枚举用逗号。
用 enum colors 类型定义的变量只能设为列表中的某个
关键字。可以像下面这样定义 enum colors 变量:
enum colors favorite = PUCE;
在幕后,计算机会为列表中的每个符号分配一个数字,枚
举变量中也只保存数字
。枚举不仅让
代码更易于阅读,同时它也能够防止你把值保存错误。
比如你把PUCE写成PUSE就不会编译通过.
代码示例:
#include <stdio.h>
typedef enum {
COUNT,POUNDS,PINTS
} unit_of_measure;
typedef union {
short count;
float weight;
float volume;
} quantity;
typedef struct {
const char *name;
const char *country;
quantity amount;
unit_of_measure units;
} fruit_order;
void display(fruit_order order) {
printf("This order contains ");
if(order.units == PINTS)
printf("%2.2f pints of %s\n",order.amount.volume,order.name);
else if(order.units == POUNDS )
printf("%2.2f lbs of %s\n", order.amount.weight, order.name);
else
printf("%i %s\n", order.amount.count , order.name);
}
int main() {
fruit_order apples = { "apples", "England", .amount.count=144, COUNT };
fruit_order strawberries = { "strawberries", "Spain", .amount.weight =17.6, POUNDS};
fruit_order oj = { "orange juice", "U.S.A.", .amount.volume=10.5, PINTS };
display(apples);
display(strawberries);
display(oj);
return 0;
}
运行结果:
有时你想控制某一位
假设你需要一个结构,其中有很多表示“是”或“非”的值,
可以用一些 short 或 int 来创建结构:
typedef struct {
short low_pass_vcf;
short filter_coupler;
short reverb;
short sequential;
...
} synth;
这样做完全可行,但问题是,真/假的值只需要一位就能表
示, short 字段占了多得多的空间,太浪费了,要是结构的字
段的值能只用一位表示就好了。
这就是发明位字段( bitfield )的原因。
位字段的位数可调
可以用位字段(bitfield)指定一个字段有多少位。例
如,可以把结构写成这样:
typedef struct {
unsigned int low_pass_vcf:1; // :1 表示该字段只使用1位存储空间。
unsigned int filter_coupler:1; // 注意:每个字段都必须是unsigned int
unsigned int reverb:1;
unsigned int sequential:1;
...
} synth;
如果你有一连串的位字段,计算机会放在一起,以节省
空间,也就是说如果有8个1位的位字段,计算机就会把
它们保存在一个字节中。
注意:只有出现在同一个结构中,位字段才能节省空间。
如果编译器发现结构中
只有一个位字段,还是会把它填充
成一个字,这就是为什么位字段总
是组合在一起。
如何选择位数?
位字段不仅可以用来保存一连串真/假值,还可以用来保存小范围的数字,
例如一年中的十二个月。假设想在某个结构中保存月份(0到11的值),
就可以用一个4位的位字段来保存,为什么?因为4位可以保存0到15,而
3位只能保存0到7。
unsigned int month_no:4;
问: 位字段就是为了节省空间的吗?
答: 不仅仅是为了节省空间,如
果需要读取低层的二进制信息,位字
段就会非常有用。
问: 能举个例子吗?
答: 比如要读写某类自定义二进
制文件。