张银峰的编程课堂

访问结构体成员

定义结构体,表示向程序引入了一种类型。

struct Cat
{
    char  name[64];
    int   color;
    int   age;
    float weight;
};

同内置类型一样,我们需要先定义此类型的变量,也称为创建结构体对象,之后才能使用它。

struct Cat cat;         // 定义Cat变量cat
struct Cat foo, bar;    // 定义Cat变量foo, bar
struct Cat cats[9];     // 定义Cat数组,可饲养9只猫

结构体的成员可称为字段、成员变量、属性;可以通过(.)运算符对这些属性进行读写:对象 . 属性

strcpy(cat.name, "laohu");  // 为cat对象的name字段设置值
foo.color = 0;              // 设置foo对象的color成员变量
cats[3].weight = 7.0f;      // 为cats[3]对象的weight属性设置值

这里是一个完整的示例程序。

#include <stdio.h>
#include <string.h>

struct Cat
{
    char  name[64];
    int   color;
    int   age;
    float weight;
};

const char *colors[] =
{
    "white",
    "yellow",
    "milk",
    "gray"
};

void print_cat(struct Cat my_cat)
{
    printf("%s - color: %s \t age: %d\n", my_cat.name, colors[my_cat.color], my_cat.age);
}

int main()
{
    // 创建Cat类型变量my_cat
    struct Cat my_cat;

    // 使用(.)运算符访问结构体成员
    strcpy(my_cat.name, "laohu");
    my_cat.color = 2;
    my_cat.age = 7;
    my_cat.weight = 15.2f;

    // 结构体变量作为函数参数传递
    print_cat(my_cat);

    return 0;
}

glimix.com

ZYF

我们可以在定义结构体变量的同时依次对成员初始化:对于未指定的字段,将默认以0值初始化。

  • 字符就是'\0'
  • int就是0
  • double就是0.0
  • 指针就是NULL
  • 字符数组就是所有元素为'\0'

对于数组:

  • 在完全指定字段的情况下,组与组之间的数据用','分隔;
  • 对于部分初始化,组与组之间用"{}"分隔,以界定当前对象初始化范围。

对于数组,不管在什么情形下,建议全部采用{}进行多个对象的分隔。

注意,未初始与初始化为0值是两个不同的概述。

int main()
{
    struct Cat cat1 = { "cat1", 1, 7, 15.2f };
    struct Cat cat2 = { "cat2",  2 };

    struct Cat cats[5] =
    {
        "cat1", 1, 7, 15.2f,    // 初始化所有字段
        "cat2", 2, 3, 10.0f,    // 两两之间以','分隔
       {"cat3"},                // 仅初始化名称字段,以{}分隔两个对象之间的边界。
       {"cat4", 0 }             // 否则名称"cat4"会被当作为cat3.color的初始值,进而构造类型错误。
                                // 没有为cat[4]指定任何值,默认初始化为0值。
    };

    /*
     * 同上,对象与对象之间使用{}限定,是良好编程的风格之一。
    struct Cat cats[5] =
    {
        { "cat1", 1, 7, 15.2f },
        { "cat2", 2, 3, 10.0f },
        { "cat3"              },
        { "cat4", 0           }
    };
    */

    print_cat(cat1);
    print_cat(cat2);

    print_cat(cats[2]);
    print_cat(cats[3]);
    print_cat(cats[4]);

    // 没有初始化,随机值,导致程序中断。
    struct Cat cat6;
    print_cat(cat6);

    return 0;
}

glimix.com

克隆体

同内置类型一样,传递给函数的结构体变量,也是实参的一个拷贝,即在函数内部改变结构体的成员,不会改变原数据。

#include <stdio.h>
#include <string.h>

struct Foo
{
    int a;
    float b;
    char s[64];
};

void change_foo(struct Foo foo)
{
    foo.a = 9;
    foo.b = 3.14f;
    strcpy(foo.s, "hello");
}

glimix.com

int main()
{
    struct Foo foo;
    foo.a = 0;
    foo.b = 0;

    change_foo(foo);

    printf("foo.a: %d\n", foo.a);
    printf("foo.b: %f\n", foo.b);
    printf("foo.s: %s\n", foo.s);

    return 0;
}

glimix.com

在change_foo()内部使用的foo,只是main()中foo的一个副本,由断点截图可以看到,函数内部对此副本的修改是有效的,但这不会影响main()中原始foo的值。这是C语言函数传递参数的特性:函数参数(形参)是实参的拷贝!对于结构体而言,这就涉及到效率问题。当一个结构体有大量的成员时,对所有成员的拷贝成本可能很高,解决的方法是使用结构体指针。

结构体指针

定义一个结构体指针的方法与内置指针类型相似,不过需要struct类型关键字:

struct Foo foo;             // 定义一个Foo对象foo
struct Foo *foo1 = &foo;    // 定义一个Foo指针foo1,初始化让其指向foo
struct Foo *foo2;           // foo2未初始化
foo2 = &foo;                // 通过赋值操作让foo2指向foo

结构体指针同样可以作为函数参数。把指针传递给函数时,在32位环境下仅需要拷贝4个字节,对于大的结构体,这在效率上有很大的提高。

void foo(struct Foo *foo);          // 参数foo是结构体Foo的指针
void bar(const struct Bar *bar);    // const代表我们在函数bar中不修改Bar的成员

通过结构体指针访问成员时,使用箭头 -> 运算符。

struct Foo *foo;
foo->a = 3;
foo->b = 2.0f;

也可以对指针解引用指针,进行成员访问。

(*foo).a = 3;   // *foo解引用取得foo对象,对对象就要使用(.)运算符。

示例程序将这些串联起来。

#include <stdio.h>
#include <string.h>

struct Foo
{
    int a;
    float b;
    char s[64];
};

void change_foo(struct Foo *foo)
{
    foo->a = 9;
    foo->b = 3.14f;
    strcpy(foo->s, "hello");
}

void print_foo(const struct Foo *foo)
{
    printf("Foo(%d, %.2f, %s)\n", (*foo).a, (*foo).b, (*foo).s);
}

int main()
{
    struct Foo foo;
    foo.a = 0;
    foo.b = 0;

    change_foo(&foo);
    print_foo(&foo);

    return 0;
}

glimix.com

ZYF