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

C++ 结构体与联合体深入解析

2023-01-303.2k 阅读

C++ 结构体(Struct)基础

在 C++ 中,结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个单一的实体。结构体为我们提供了一种方便的方式来组织和管理相关的数据。

结构体的定义

结构体的定义使用 struct 关键字,其基本语法如下:

struct StructName {
    // 成员变量声明
    data_type member1;
    data_type member2;
    // 更多成员变量...
};

例如,我们定义一个表示学生信息的结构体:

struct Student {
    std::string name;
    int age;
    double grade;
};

在这个例子中,Student 结构体包含了三个成员变量:name(字符串类型)、age(整数类型)和 grade(双精度浮点数类型)。

结构体变量的声明与初始化

定义好结构体后,我们可以声明结构体类型的变量,并对其进行初始化。

  1. 声明变量
Student student1;

这里声明了一个 Student 类型的变量 student1

  1. 初始化变量
    • 传统初始化方式
Student student2 = {"Alice", 20, 3.8};

这种方式按照结构体成员声明的顺序依次提供初始值。

  • C++11 统一初始化
Student student3{"Bob", 21, 3.9};

统一初始化语法更加简洁,并且可以防止一些意外的类型转换。

  1. 成员访问: 一旦声明并初始化了结构体变量,我们可以通过成员访问运算符(.)来访问结构体的成员。
student1.name = "Charlie";
student1.age = 22;
student1.grade = 4.0;
std::cout << "Name: " << student1.name << ", Age: " << student1.age << ", Grade: " << student1.grade << std::endl;

结构体的内存布局

结构体在内存中的布局是一个重要的概念,它影响着结构体的大小以及数据的存储方式。

内存对齐

内存对齐是指结构体成员在内存中的存储地址按照一定的规则进行排列,以提高 CPU 访问内存的效率。编译器会在结构体成员之间插入一些填充字节(padding),使得每个成员的地址都满足特定的对齐要求。

例如,考虑以下结构体:

struct Example1 {
    char c;
    int i;
};

在 32 位系统上,char 类型通常占用 1 个字节,int 类型通常占用 4 个字节。由于 int 类型需要 4 字节对齐,编译器会在 c 后面插入 3 个填充字节,使得 i 的地址是 4 的倍数。因此,Example1 结构体的大小为 8 字节(1 字节的 c + 3 字节的填充 + 4 字节的 i)。

我们可以通过 offsetof 宏来查看结构体成员的偏移量,offsetof 定义在 <stddef.h> 头文件中。

#include <iostream>
#include <stddef.h>

struct Example1 {
    char c;
    int i;
};

int main() {
    std::cout << "Offset of c: " << offsetof(Example1, c) << std::endl;
    std::cout << "Offset of i: " << offsetof(Example1, i) << std::endl;
    std::cout << "Size of Example1: " << sizeof(Example1) << std::endl;
    return 0;
}

输出结果为:

Offset of c: 0
Offset of i: 4
Size of Example1: 8

结构体嵌套

结构体可以嵌套,即一个结构体可以包含另一个结构体作为成员。

struct Address {
    std::string street;
    std::string city;
    std::string state;
};

struct Person {
    std::string name;
    int age;
    Address address;
};

在这个例子中,Person 结构体包含了一个 Address 结构体类型的成员。结构体嵌套时,同样遵循内存对齐的规则。Person 结构体的大小计算需要考虑 Address 结构体内部成员的对齐以及 Address 结构体自身的对齐。

结构体中的成员函数

C++ 结构体不仅可以包含数据成员,还可以包含成员函数。成员函数可以访问结构体的成员变量,提供了一种封装和操作数据的方式。

成员函数的定义

  1. 在结构体定义内部定义
struct Circle {
    double radius;
    double getArea() {
        return 3.14159 * radius * radius;
    }
};

Circle 结构体中,getArea 是一个成员函数,它可以直接访问 radius 成员变量。

  1. 在结构体定义外部定义
struct Rectangle {
    double length;
    double width;
    double getArea();
};

double Rectangle::getArea() {
    return length * width;
}

当在结构体外部定义成员函数时,需要使用作用域解析运算符(::)来指定函数所属的结构体。

成员函数的调用

一旦定义了包含成员函数的结构体,我们可以通过结构体变量来调用成员函数。

Circle circle;
circle.radius = 5.0;
std::cout << "Circle area: " << circle.getArea() << std::endl;

Rectangle rectangle;
rectangle.length = 4.0;
rectangle.width = 3.0;
std::cout << "Rectangle area: " << rectangle.getArea() << std::endl;

结构体与类的关系

在 C++ 中,结构体和类有很多相似之处,但也存在一些重要的区别。

访问控制

  1. 结构体:结构体的成员默认是公共的(public),这意味着外部代码可以直接访问结构体的成员变量和成员函数。
struct Point {
    int x;
    int y;
};

Point point;
point.x = 10; // 合法,因为 x 是公共成员
  1. :类的成员默认是私有的(private),外部代码不能直接访问类的私有成员。
class Point {
    int x;
    int y;
};

Point point;
// point.x = 10; // 非法,因为 x 是私有成员

如果希望在类中定义公共成员,可以使用 public 关键字。

class Point {
public:
    int x;
    int y;
};

Point point;
point.x = 10; // 合法,因为 x 是公共成员

继承

结构体和类在继承方面也有一些差异。当结构体继承自另一个结构体或类时,默认的继承方式是公共继承(public)。

struct Base {
    int value;
};

struct Derived : Base {
    int otherValue;
};

这里 Derived 结构体以公共继承的方式从 Base 结构体继承。

而类在继承时,默认的继承方式是私有继承(private)。

class Base {
public:
    int value;
};

class Derived : Base {
public:
    int otherValue;
};

在这个例子中,Derived 类以私有继承的方式从 Base 类继承,Base 类的公共成员在 Derived 类中变为私有成员。

C++ 联合体(Union)基础

联合体是 C++ 中的另一种用户自定义数据类型,它允许在同一内存位置存储不同类型的数据。联合体的所有成员共享相同的内存空间,这意味着在任何时刻,只有一个成员的值是有效的。

联合体的定义

联合体的定义使用 union 关键字,其基本语法如下:

union UnionName {
    // 成员变量声明
    data_type member1;
    data_type member2;
    // 更多成员变量...
};

例如,定义一个简单的联合体:

union Data {
    int i;
    double d;
    char c;
};

在这个 Data 联合体中,idc 共享相同的内存空间。

联合体变量的声明与初始化

  1. 声明变量
Data data;

声明了一个 Data 类型的联合体变量 data

  1. 初始化变量: 联合体只能初始化其第一个成员。
Data data = {10}; // 初始化 i 为 10

这里初始化了联合体的 i 成员为 10。

  1. 成员访问: 可以像结构体一样通过成员访问运算符(.)来访问联合体的成员。
std::cout << "Value of i: " << data.i << std::endl;
data.d = 3.14;
std::cout << "Value of d: " << data.d << std::endl;
data.c = 'A';
std::cout << "Value of c: " << data.c << std::endl;

需要注意的是,在访问某个成员后,之前存储在其他成员中的值可能会被覆盖。

联合体的内存布局

联合体的内存布局与结构体有很大的不同,由于所有成员共享相同的内存空间,联合体的大小等于其最大成员的大小。

例如,对于前面定义的 Data 联合体:

union Data {
    int i;
    double d;
    char c;
};

在 64 位系统上,int 通常占用 4 字节,double 占用 8 字节,char 占用 1 字节。因此,Data 联合体的大小为 8 字节,即 double 类型的大小。

我们可以通过 sizeof 运算符来验证联合体的大小:

std::cout << "Size of Data union: " << sizeof(Data) << std::endl;

输出结果为:

Size of Data union: 8

联合体的使用场景

联合体在一些特定的场景下非常有用,以下是一些常见的应用场景。

节省内存空间

当我们需要在不同时刻使用不同类型的数据,但不需要同时存储这些数据时,联合体可以节省内存。例如,在一个表示图形的结构体中,可能有圆形和矩形两种类型的图形。圆形只需要半径,矩形需要长度和宽度。我们可以使用联合体来存储这些不同类型图形的相关数据。

struct Shape {
    char type; // 'c' 表示圆形,'r' 表示矩形
    union {
        double radius;
        struct {
            double length;
            double width;
        } rectangle;
    };
};

在这个 Shape 结构体中,联合体部分根据 type 的值来决定使用 radius 还是 rectangle 成员,从而节省了内存空间。

位操作

联合体可以用于位操作,通过将不同类型的数据映射到相同的内存空间,我们可以方便地对数据的不同位进行操作。例如,将一个整数拆分为字节进行操作。

union ByteSplit {
    int num;
    char bytes[4];
};

ByteSplit split;
split.num = 0x12345678;
std::cout << "Byte 0: " << std::hex << (int)split.bytes[0] << std::endl;
std::cout << "Byte 1: " << std::hex << (int)split.bytes[1] << std::endl;
std::cout << "Byte 2: " << std::hex << (int)split.bytes[2] << std::endl;
std::cout << "Byte 3: " << std::hex << (int)split.bytes[3] << std::endl;

在这个例子中,ByteSplit 联合体将 int 类型的 numchar 数组 bytes 共享内存空间,从而可以方便地访问 num 的各个字节。

结构体与联合体的对比

  1. 内存布局

    • 结构体:结构体的成员按照声明顺序依次存储在内存中,可能会有填充字节以满足内存对齐要求。结构体的大小是其所有成员大小之和加上填充字节的大小。
    • 联合体:联合体的所有成员共享相同的内存空间,联合体的大小等于其最大成员的大小。
  2. 数据存储

    • 结构体:可以同时存储多个成员的值,每个成员都有自己独立的内存位置。
    • 联合体:在任何时刻,只有一个成员的值是有效的,因为所有成员共享内存,对一个成员的赋值可能会覆盖其他成员的值。
  3. 使用场景

    • 结构体:适用于需要组织和管理相关数据的场景,每个数据成员都有其特定的含义,并且需要同时存在。例如,存储学生的多种信息。
    • 联合体:适用于需要在不同时刻使用不同类型数据,且不需要同时存储这些数据的场景,以节省内存空间,或者进行位操作等。
  4. 访问控制

    • 结构体:成员默认是公共的,可以直接被外部代码访问。
    • 联合体:成员默认也是公共的,与结构体类似。

通过深入理解结构体和联合体的特性、内存布局以及使用场景,我们可以在 C++ 编程中更有效地选择和使用这两种数据类型,以优化程序的性能和内存使用。无论是结构体在数据组织方面的优势,还是联合体在节省内存和位操作方面的特性,都为我们提供了丰富的编程手段,使我们能够更好地应对各种实际编程需求。在实际项目中,根据具体情况合理运用结构体和联合体,将有助于编写高效、简洁且易于维护的代码。同时,对于内存对齐等底层概念的掌握,也能帮助我们更好地理解程序在内存中的运行机制,从而排查和解决可能出现的内存相关问题。