MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言结构体成员的访问与操作

2022-01-135.5k 阅读

结构体成员访问基础

在 C 语言中,结构体是一种自定义的数据类型,它允许将不同类型的数据组合在一起。一旦定义了结构体变量,就需要访问其成员来使用这些组合的数据。访问结构体成员最常用的方式是使用点运算符(.)。

假设有如下结构体定义:

struct Person {
    char name[50];
    int age;
    float height;
};

现在创建一个结构体变量并访问其成员:

#include <stdio.h>

struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person p1;
    // 使用点运算符访问并赋值成员
    strcpy(p1.name, "Alice");
    p1.age = 30;
    p1.height = 1.65;

    // 使用点运算符访问并打印成员
    printf("Name: %s\n", p1.name);
    printf("Age: %d\n", p1.age);
    printf("Height: %.2f\n", p1.height);

    return 0;
}

在上述代码中,p1struct Person 类型的变量。通过点运算符,我们可以直接访问 p1 的各个成员,如 p1.namep1.agep1.height。点运算符的左边是结构体变量名,右边是结构体成员名。这种方式直观且易于理解,是访问结构体成员最基本的方法。

通过指针访问结构体成员

除了使用点运算符通过结构体变量直接访问成员,还可以使用指针来访问结构体成员。当我们有一个指向结构体的指针时,需要使用箭头运算符(->)来访问结构体成员。

继续以上面的 struct Person 为例:

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

struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person p1;
    struct Person *pPtr = &p1;

    // 使用箭头运算符通过指针访问并赋值成员
    strcpy(pPtr->name, "Bob");
    pPtr->age = 25;
    pPtr->height = 1.75;

    // 使用箭头运算符通过指针访问并打印成员
    printf("Name: %s\n", pPtr->name);
    printf("Age: %d\n", pPtr->age);
    printf("Height: %.2f\n", pPtr->height);

    return 0;
}

在这段代码中,pPtr 是一个指向 p1 的指针。通过箭头运算符 ->,我们可以方便地访问 p1 的成员。pPtr->name 等价于 (*pPtr).name,箭头运算符实际上是一种语法糖,它使得通过指针访问结构体成员的代码更加简洁明了。这种方式在处理动态分配的结构体或者在函数中传递结构体指针时非常有用。

结构体成员的初始化与操作

初始化结构体成员

  1. 初始化列表方式 在定义结构体变量时,可以使用初始化列表来为结构体成员赋值。
struct Point {
    int x;
    int y;
};

struct Point p1 = {10, 20};

这里,p1x 成员被初始化为 10,y 成员被初始化为 20。初始化列表中的值按照结构体成员定义的顺序依次赋值。

  1. 部分初始化 也可以只初始化部分成员,未初始化的成员会被赋予默认值(对于数值类型为 0,对于指针类型为 NULL)。
struct Point {
    int x;
    int y;
};

struct Point p2 = {5};

此时,p2.x 被初始化为 5,p2.y 被初始化为 0。

  1. 嵌套结构体初始化 当结构体中包含其他结构体成员时,初始化会稍微复杂一些。
struct Point {
    int x;
    int y;
};

struct Rectangle {
    struct Point topLeft;
    struct Point bottomRight;
};

struct Rectangle rect = { {10, 10}, {20, 20} };

这里,recttopLeft 成员被初始化为 {10, 10}bottomRight 成员被初始化为 {20, 20}

结构体成员的操作

  1. 修改成员值 通过前面介绍的点运算符或箭头运算符,可以随时修改结构体成员的值。
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct Person p1 = {"Charlie", 28, 1.70};

    // 修改成员值
    p1.age = 29;
    p1.height = 1.72;

    return 0;
}
  1. 成员间的运算 结构体成员如果是数值类型,可以进行各种算术运算。
struct Dimensions {
    int length;
    int width;
};

int main() {
    struct Dimensions dim = {10, 5};
    int area = dim.length * dim.width;
    return 0;
}

这里通过访问 dimlengthwidth 成员计算出了面积。

结构体数组的成员访问与操作

  1. 定义与初始化结构体数组 可以定义结构体数组,数组中的每个元素都是一个结构体变量。
struct Book {
    char title[100];
    char author[50];
    int year;
};

struct Book library[3] = {
    {"C Programming Language", "Brian W. Kernighan", 1988},
    {"The Mythical Man - Month", "Frederick P. Brooks Jr.", 1975},
    {"Clean Code", "Robert C. Martin", 2008}
};
  1. 访问结构体数组的成员 要访问结构体数组中某个元素的成员,需要先指定数组索引,再使用点运算符或箭头运算符。
#include <stdio.h>
#include <string.h>

struct Book {
    char title[100];
    char author[50];
    int year;
};

int main() {
    struct Book library[3] = {
        {"C Programming Language", "Brian W. Kernighan", 1988},
        {"The Mythical Man - Month", "Frederick P. Brooks Jr.", 1975},
        {"Clean Code", "Robert C. Martin", 2008}
    };

    // 访问并打印第一个书的标题
    printf("Title of the first book: %s\n", library[0].title);

    // 使用指针访问并打印第二个书的作者
    struct Book *bookPtr = &library[1];
    printf("Author of the second book: %s\n", bookPtr->author);

    return 0;
}
  1. 操作结构体数组的成员 可以对结构体数组的成员进行各种操作,比如修改成员值,或者根据成员值进行筛选等。
#include <stdio.h>
#include <string.h>

struct Book {
    char title[100];
    char author[50];
    int year;
};

int main() {
    struct Book library[3] = {
        {"C Programming Language", "Brian W. Kernighan", 1988},
        {"The Mythical Man - Month", "Frederick P. Brooks Jr.", 1975},
        {"Clean Code", "Robert C. Martin", 2008}
    };

    // 将所有书的年份加 10
    for (int i = 0; i < 3; i++) {
        library[i].year += 10;
    }

    // 打印修改后的年份
    for (int i = 0; i < 3; i++) {
        printf("Book %d - Year: %d\n", i + 1, library[i].year);
    }

    return 0;
}

在这段代码中,我们通过循环访问结构体数组 library 的每个元素,并修改了 year 成员的值。

结构体作为函数参数时成员的访问与操作

  1. 结构体变量作为函数参数 当把结构体变量作为函数参数传递时,函数会得到结构体变量的一份拷贝。在函数内部对结构体成员的修改不会影响到原结构体变量。
#include <stdio.h>
#include <string.h>

struct Person {
    char name[50];
    int age;
};

void incrementAge(struct Person p) {
    p.age++;
}

int main() {
    struct Person p1 = {"David", 22};
    incrementAge(p1);
    printf("Age after function call: %d\n", p1.age);
    return 0;
}

在上述代码中,incrementAge 函数试图增加 p 的年龄,但由于传递的是拷贝,p1 的年龄并没有真正增加。

  1. 结构体指针作为函数参数 为了让函数能够修改原结构体变量的成员,可以传递结构体指针。
#include <stdio.h>
#include <string.h>

struct Person {
    char name[50];
    int age;
};

void incrementAge(struct Person *p) {
    p->age++;
}

int main() {
    struct Person p1 = {"Eve", 23};
    incrementAge(&p1);
    printf("Age after function call: %d\n", p1.age);
    return 0;
}

这里,incrementAge 函数通过指针 p 访问并修改了 p1age 成员,所以 p1 的年龄被成功增加。

  1. 函数返回结构体 函数也可以返回结构体。
#include <stdio.h>
#include <string.h>

struct Point {
    int x;
    int y;
};

struct Point addPoints(struct Point p1, struct Point p2) {
    struct Point result;
    result.x = p1.x + p2.x;
    result.y = p1.y + p2.y;
    return result;
}

int main() {
    struct Point p1 = {10, 20};
    struct Point p2 = {5, 15};
    struct Point sum = addPoints(p1, p2);
    printf("Sum of points: (%d, %d)\n", sum.x, sum.y);
    return 0;
}

在这个例子中,addPoints 函数接受两个 struct Point 类型的参数,并返回一个新的 struct Point,其成员是两个输入点对应成员的和。

结构体成员访问的内存布局相关理解

  1. 结构体成员的内存排列 结构体成员在内存中是按照定义的顺序依次排列的。例如,对于如下结构体:
struct Data {
    char c;
    int i;
    float f;
};

c 成员首先占用 1 个字节,接着 i 成员占用 4 个字节(假设 int 为 4 字节),f 成员占用 4 个字节。但是,由于内存对齐的原因,结构体实际占用的内存可能会大于所有成员大小之和。

  1. 内存对齐与成员访问效率 内存对齐是为了提高内存访问效率。现代计算机通常以特定的字节数(如 4 字节、8 字节)为单位访问内存。如果结构体成员的地址满足对齐要求,CPU 可以更高效地读取和写入数据。例如,int 类型通常要求 4 字节对齐,double 类型通常要求 8 字节对齐。当访问结构体成员时,内存对齐会影响到实际的内存地址和访问方式。

考虑如下结构体:

struct Data1 {
    char c;
    int i;
};

struct Data2 {
    int i;
    char c;
};

虽然 Data1Data2 包含相同的成员,但由于成员顺序不同,它们的内存布局也不同。Data1 中的 c 占用 1 个字节后,为了满足 i 的 4 字节对齐,会在 c 后填充 3 个字节,所以 Data1 总共占用 8 个字节。而 Data2i 先占用 4 个字节,c 再占用 1 个字节,总共占用 5 个字节,但为了满足结构体整体的对齐要求,可能也会填充 3 个字节,最终占用 8 个字节。

这种内存布局的差异会影响到结构体成员的访问效率。在编写代码时,合理安排结构体成员的顺序可以优化内存使用和访问效率。

  1. 通过内存地址访问结构体成员(指针运算) 在了解结构体的内存布局后,可以通过指针运算来访问结构体成员。假设我们有如下结构体:
struct MyStruct {
    int a;
    char b;
    short c;
};

如果有一个 struct MyStruct 类型的指针 ptr,我们可以通过指针运算来访问成员。ptr 指向结构体的起始地址,ptr->a 可以直接访问 a 成员。如果要访问 b 成员,可以通过 ((char *)ptr + sizeof(int)) 来获取 b 的地址,然后再进行解引用操作 *((char *)ptr + sizeof(int))。同样,对于 c 成员,可以通过 ((short *)((char *)ptr + sizeof(int) + sizeof(char))) 来获取其地址并访问。不过,这种方式非常底层且容易出错,通常在特定的系统编程或优化场景下才会使用。

联合体与结构体成员访问的对比

  1. 联合体的基本概念 联合体也是一种自定义数据类型,它允许不同类型的数据共享同一块内存空间。例如:
union Data {
    int i;
    float f;
    char c;
};

这里,union Dataifc 成员共享同一块内存,其大小取决于最大成员的大小(通常 floatint 为 4 字节,char 为 1 字节,所以 union Data 大小为 4 字节)。

  1. 联合体成员访问与结构体成员访问的区别
    • 内存占用:结构体成员各自占用独立的内存空间,其大小是所有成员大小之和(考虑内存对齐)。而联合体成员共享内存,其大小取决于最大成员的大小。
    • 访问方式:结构体成员访问通过点运算符(.)或箭头运算符(->),可以同时访问和修改不同成员的值。而联合体在某一时刻只能有一个成员处于有效状态,对一个成员的赋值会覆盖其他成员的值。例如:
union Data {
    int i;
    float f;
    char c;
};

int main() {
    union Data u;
    u.i = 10;
    printf("i: %d\n", u.i);
    u.f = 3.14;
    printf("f: %f\n", u.f);
    u.c = 'A';
    printf("c: %c\n", u.c);
    // 此时u.i和u.f的值已经被覆盖
    return 0;
}

在这个例子中,对 u.f 赋值后,u.i 的值就无效了,因为它们共享同一块内存。而在结构体中,不同成员可以同时保持各自有效的值。

  1. 适用场景 结构体适用于需要将不同类型的数据组合在一起,并且每个数据都有其独立意义的场景,如表示一个人的信息(姓名、年龄、身高)。联合体适用于需要在不同时刻使用不同类型数据,但这些数据不会同时使用的场景,例如表示一个设备的状态,有时用整数表示状态码,有时用字符表示简单的状态描述。

结构体嵌套时成员的访问与操作

  1. 结构体嵌套的定义 结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。例如:
struct Address {
    char street[100];
    char city[50];
    int zipCode;
};

struct Employee {
    char name[50];
    int age;
    struct Address empAddress;
};

这里,struct Employee 包含一个 struct Address 类型的成员 empAddress

  1. 访问嵌套结构体成员 访问嵌套结构体成员需要使用多个点运算符。例如:
#include <stdio.h>
#include <string.h>

struct Address {
    char street[100];
    char city[50];
    int zipCode;
};

struct Employee {
    char name[50];
    int age;
    struct Address empAddress;
};

int main() {
    struct Employee emp;
    strcpy(emp.name, "John Doe");
    emp.age = 35;
    strcpy(emp.empAddress.street, "123 Main St");
    strcpy(emp.empAddress.city, "Anytown");
    emp.empAddress.zipCode = 12345;

    printf("Employee Name: %s\n", emp.name);
    printf("Employee Age: %d\n", emp.age);
    printf("Employee Address - Street: %s\n", emp.empAddress.street);
    printf("Employee Address - City: %s\n", emp.empAddress.city);
    printf("Employee Address - Zip Code: %d\n", emp.empAddress.zipCode);

    return 0;
}

在上述代码中,通过 emp.empAddress.street 这样的方式来访问嵌套结构体 empAddressstreet 成员。

  1. 操作嵌套结构体成员 可以对嵌套结构体成员进行各种操作,与普通结构体成员操作类似。例如,可以修改嵌套结构体成员的值,或者根据其值进行判断等。
#include <stdio.h>
#include <string.h>

struct Address {
    char street[100];
    char city[50];
    int zipCode;
};

struct Employee {
    char name[50];
    int age;
    struct Address empAddress;
};

void updateEmployeeCity(struct Employee *emp, const char *newCity) {
    strcpy(emp->empAddress.city, newCity);
}

int main() {
    struct Employee emp;
    strcpy(emp.name, "Jane Smith");
    emp.age = 32;
    strcpy(emp.empAddress.street, "456 Elm St");
    strcpy(emp.empAddress.city, "Oldcity");
    emp.empAddress.zipCode = 67890;

    printf("Before update - City: %s\n", emp.empAddress.city);
    updateEmployeeCity(&emp, "Newcity");
    printf("After update - City: %s\n", emp.empAddress.city);

    return 0;
}

在这个例子中,updateEmployeeCity 函数通过指针访问并修改了嵌套结构体 empAddresscity 成员。

结构体自引用与成员访问

  1. 结构体自引用的定义 结构体可以包含指向自身类型的指针,这种情况称为结构体自引用。例如:
struct Node {
    int data;
    struct Node *next;
};

这里,struct Node 包含一个指向 struct Node 类型的指针 next,这在链表等数据结构中非常常见。

  1. 通过自引用指针访问成员 通过自引用指针可以构建复杂的数据结构,并访问其中的成员。例如,构建一个简单的链表并访问节点成员:
#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

int main() {
    struct Node *head = (struct Node *)malloc(sizeof(struct Node));
    head->data = 10;
    head->next = (struct Node *)malloc(sizeof(struct Node));
    head->next->data = 20;
    head->next->next = NULL;

    struct Node *current = head;
    while (current != NULL) {
        printf("Data: %d\n", current->data);
        current = current->next;
    }

    // 释放内存
    current = head;
    struct Node *temp;
    while (current != NULL) {
        temp = current;
        current = current->next;
        free(temp);
    }

    return 0;
}

在上述代码中,通过 head 指针构建了一个简单的链表,并通过 next 指针遍历链表,访问每个节点的 data 成员。自引用结构体在构建递归数据结构(如树)时也非常有用,通过自引用指针可以方便地访问和操作子节点的成员。

结构体位域成员的访问与操作

  1. 位域的定义 位域允许在结构体中以位为单位定义成员。例如:
struct Flags {
    unsigned int flag1: 1;
    unsigned int flag2: 1;
    unsigned int flag3: 2;
};

这里,flag1flag2 各占 1 位,flag3 占 2 位。位域主要用于节省内存空间,特别是在处理大量标志位的情况下。

  1. 访问位域成员 访问位域成员与普通结构体成员类似,使用点运算符或箭头运算符。
#include <stdio.h>

struct Flags {
    unsigned int flag1: 1;
    unsigned int flag2: 1;
    unsigned int flag3: 2;
};

int main() {
    struct Flags f;
    f.flag1 = 1;
    f.flag2 = 0;
    f.flag3 = 3;

    printf("flag1: %d\n", f.flag1);
    printf("flag2: %d\n", f.flag2);
    printf("flag3: %d\n", f.flag3);

    return 0;
}

在上述代码中,通过点运算符访问并赋值位域成员。需要注意的是,位域成员的取值范围由其定义的位数决定,如 flag3 定义为 2 位,其取值范围为 0 到 3。

  1. 操作位域成员 可以对位域成员进行逻辑运算等操作。例如:
#include <stdio.h>

struct Flags {
    unsigned int flag1: 1;
    unsigned int flag2: 1;
    unsigned int flag3: 2;
};

void toggleFlags(struct Flags *f) {
    f->flag1 =!f->flag1;
    f->flag2 =!f->flag2;
    f->flag3 = (f->flag3 + 1) & 3;
}

int main() {
    struct Flags f = {1, 0, 2};
    printf("Before toggle - flag1: %d, flag2: %d, flag3: %d\n", f.flag1, f.flag2, f.flag3);
    toggleFlags(&f);
    printf("After toggle - flag1: %d, flag2: %d, flag3: %d\n", f.flag1, f.flag2, f.flag3);

    return 0;
}

toggleFlags 函数中,对 struct Flags 的位域成员进行了逻辑取反和加法取模等操作。位域成员的操作需要特别注意其取值范围和位运算的特点。

通过对以上结构体成员访问与操作的各个方面进行深入探讨,希望读者对 C 语言中结构体的使用有更全面和深入的理解,能够在实际编程中灵活运用结构体来构建高效、灵活的数据结构。