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

C++ 结构体深入解析

2022-04-215.2k 阅读

C++ 结构体基础

结构体的定义

在 C++ 中,结构体(struct)是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个单一的实体。结构体的定义语法如下:

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

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

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

这里,Student 结构体包含三个成员变量:name(字符串类型)、age(整型)和 gpa(双精度浮点型)。

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

一旦定义了结构体,就可以声明该结构体类型的变量。声明变量的方式与声明基本数据类型变量类似:

Student student1; // 声明一个 Student 类型的变量 student1

可以在声明变量的同时进行初始化:

Student student2 = {"Alice", 20, 3.5};

也可以使用成员初始化列表的方式进行初始化:

Student student3 {
    "Bob",
    21,
    3.8
};

访问结构体成员

通过结构体变量名和成员访问运算符(.)可以访问结构体的成员:

#include <iostream>
#include <string>

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

int main() {
    Student student = {"Charlie", 22, 3.9};
    std::cout << "Name: " << student.name << std::endl;
    std::cout << "Age: " << student.age << std::endl;
    std::cout << "GPA: " << student.gpa << std::endl;
    return 0;
}

结构体与内存布局

结构体的内存分配

结构体变量在内存中是连续存储的。每个成员变量按照定义的顺序依次存储。例如,对于前面定义的 Student 结构体,假设 std::string 占用 28 字节(不同编译器可能不同),int 占用 4 字节,double 占用 8 字节,那么 Student 结构体变量将占用 28 + 4 + 8 = 40 字节(这里不考虑内存对齐,实际情况可能不同)。

内存对齐

为了提高内存访问效率,编译器会对结构体成员进行内存对齐。内存对齐意味着结构体成员变量的存储地址会是其自身大小的倍数(通常是这样)。例如,int 类型的变量可能会从 4 的倍数的地址开始存储。

考虑以下结构体:

struct Example {
    char c; // 1 字节
    int i;  // 4 字节
    short s; // 2 字节
};

如果不进行内存对齐,Example 结构体将占用 1 + 4 + 2 = 7 字节。但由于内存对齐,c 后面会填充 3 个字节,使得 i 从 4 的倍数地址开始存储。因此,Example 结构体实际占用 1 + 3 + 4 + 2 = 10 字节(在某些编译器下,可能还会进一步对齐到 16 字节,这取决于编译器的设置)。

可以使用 #pragma pack(n) 指令来指定结构体的对齐方式,n 表示对齐字节数。例如,#pragma pack(1) 表示取消内存对齐,结构体成员将紧密排列。

#pragma pack(1)
struct Example2 {
    char c;
    int i;
    short s;
};
#pragma pack() // 恢复默认对齐

Example2 结构体在 #pragma pack(1) 作用下,将占用 1 + 4 + 2 = 7 字节。

结构体中的成员函数

成员函数的定义

结构体不仅可以包含成员变量,还可以包含成员函数。成员函数是定义在结构体内部或外部,用于操作结构体成员变量的函数。

在结构体内部定义成员函数:

struct Circle {
    double radius;

    double getArea() {
        return 3.14159 * radius * radius;
    }
};

在结构体外部定义成员函数,需要使用作用域解析运算符(::):

struct Rectangle {
    double width;
    double height;

    double getArea();
};

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

成员函数与 this 指针

在结构体的成员函数中,this 指针指向调用该成员函数的结构体对象。通过 this 指针,可以访问结构体的成员变量和其他成员函数。

struct Point {
    int x;
    int y;

    void move(int dx, int dy) {
        this->x += dx;
        this->y += dy;
    }
};

在上述 move 函数中,this->xthis->y 分别表示调用 move 函数的 Point 对象的 xy 成员变量。

结构体的继承

结构体继承的基本概念

C++ 中的结构体可以从其他结构体继承成员。继承允许创建一个新的结构体,它基于一个已有的结构体,并且可以添加新的成员或重写继承的成员。

结构体继承的语法如下:

struct BaseStruct {
    // 成员变量和成员函数
};

struct DerivedStruct : public BaseStruct {
    // 新的成员变量和成员函数
};

例如:

struct Shape {
    std::string name;
};

struct Rectangle : public Shape {
    double width;
    double height;

    double getArea() {
        return width * height;
    }
};

这里,Rectangle 结构体继承自 Shape 结构体,Rectangle 对象将拥有 Shape 结构体的 name 成员变量,同时还拥有自己的 widthheight 成员变量和 getArea 成员函数。

访问控制与继承

在继承中,访问控制决定了基类成员在派生类中的可访问性。有三种访问控制修饰符:publicprivateprotected

  • public 继承:基类的 public 成员在派生类中仍然是 publicprotected 成员在派生类中仍然是 protectedprivate 成员在派生类中不可访问。
  • private 继承:基类的 publicprotected 成员在派生类中变为 privateprivate 成员在派生类中不可访问。
  • protected 继承:基类的 publicprotected 成员在派生类中变为 protectedprivate 成员在派生类中不可访问。

例如:

struct Base {
    public:
        int publicMember;
    protected:
        int protectedMember;
    private:
        int privateMember;
};

struct PublicDerived : public Base {
    void accessMembers() {
        publicMember = 10; // 可访问
        protectedMember = 20; // 可访问
        // privateMember = 30; // 不可访问
    }
};

struct PrivateDerived : private Base {
    void accessMembers() {
        publicMember = 10; // 可访问,但在 PrivateDerived 中变为 private
        protectedMember = 20; // 可访问,但在 PrivateDerived 中变为 private
        // privateMember = 30; // 不可访问
    }
};

struct ProtectedDerived : protected Base {
    void accessMembers() {
        publicMember = 10; // 可访问,但在 ProtectedDerived 中变为 protected
        protectedMember = 20; // 可访问,在 ProtectedDerived 中仍然是 protected
        // privateMember = 30; // 不可访问
    }
};

结构体与类的区别

成员访问权限默认值

在 C++ 中,结构体和类非常相似,但有一些重要的区别。其中一个区别是成员访问权限的默认值。在结构体中,成员默认是 public 的,而在类中,成员默认是 private 的。

struct StructExample {
    int data; // 默认 public
};

class ClassExample {
    int data; // 默认 private
};

继承方式默认值

另一个区别是继承方式的默认值。结构体默认使用 public 继承,而类默认使用 private 继承。

struct StructBase {};
struct StructDerived : StructBase {}; // 默认为 public 继承

class ClassBase {};
class ClassDerived : ClassBase {}; // 默认为 private 继承

尽管有这些区别,但在实际编程中,结构体通常用于简单的数据聚合,而类用于更复杂的对象建模,包含更多的行为和数据封装。

结构体在面向对象编程中的应用

数据封装与抽象

结构体可以用于实现数据封装和抽象。通过将相关的数据和操作封装在结构体中,可以隐藏内部实现细节,只提供必要的接口给外部使用。

例如,我们可以定义一个表示栈的结构体:

struct Stack {
    int data[100];
    int top;

    Stack() : top(-1) {}

    void push(int value) {
        if (top < 99) {
            data[++top] = value;
        }
    }

    int pop() {
        if (top >= 0) {
            return data[top--];
        }
        return -1; // 表示栈为空
    }
};

这里,Stack 结构体封装了栈的数据(data 数组和 top 指针)和操作(pushpop 函数),外部代码只需要使用这些接口,而不需要了解栈的内部实现细节。

多态性

虽然结构体本身不直接支持多态性,但结合继承和虚函数可以实现多态行为。例如,我们可以定义一个表示图形的结构体层次结构:

struct Shape {
    virtual double getArea() = 0;
};

struct Circle : public Shape {
    double radius;

    Circle(double r) : radius(r) {}

    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

struct Rectangle : public Shape {
    double width;
    double height;

    Rectangle(double w, double h) : width(w), height(h) {}

    double getArea() override {
        return width * height;
    }
};

通过这种方式,可以使用基类指针或引用来调用不同派生类对象的 getArea 函数,实现多态性:

#include <iostream>
#include <vector>

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5));
    shapes.push_back(new Rectangle(4, 6));

    for (Shape* shape : shapes) {
        std::cout << "Area: " << shape->getArea() << std::endl;
    }

    for (Shape* shape : shapes) {
        delete shape;
    }

    return 0;
}

结构体与模板

结构体模板的定义

C++ 模板允许我们编写通用的代码,结构体也可以使用模板。结构体模板的定义语法如下:

template <typename T>
struct Pair {
    T first;
    T second;
};

这里,Pair 是一个结构体模板,T 是模板参数。可以使用不同的数据类型实例化这个模板:

Pair<int> intPair;
intPair.first = 10;
intPair.second = 20;

Pair<std::string> stringPair;
stringPair.first = "Hello";
stringPair.second = "World";

模板特化

有时候,我们需要为特定的数据类型提供不同的实现,这就需要使用模板特化。例如,对于 Pair 结构体模板,我们可以为 std::string 类型提供一个特化版本,使其在比较两个字符串时忽略大小写:

template <>
struct Pair<std::string> {
    std::string first;
    std::string second;

    bool equalIgnoreCase() {
        std::string lowerFirst = first;
        std::string lowerSecond = second;
        std::transform(lowerFirst.begin(), lowerFirst.end(), lowerFirst.begin(), [](unsigned char c) { return std::tolower(c); });
        std::transform(lowerSecond.begin(), lowerSecond.end(), lowerSecond.begin(), [](unsigned char c) { return std::tolower(c); });
        return lowerFirst == lowerSecond;
    }
};

这样,当使用 Pair<std::string> 时,就可以使用 equalsIgnoreCase 函数:

Pair<std::string> strPair;
strPair.first = "Hello";
strPair.second = "HELLO";
if (strPair.equalsIgnoreCase()) {
    std::cout << "The strings are equal (ignoring case)." << std::endl;
}

结构体在实际项目中的应用案例

游戏开发中的应用

在游戏开发中,结构体常用于表示游戏对象的属性和状态。例如,一个表示角色的结构体可以包含角色的位置、生命值、攻击力等信息:

struct Character {
    int x;
    int y;
    int health;
    int attackPower;

    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }

    void takeDamage(int damage) {
        health -= damage;
        if (health < 0) {
            health = 0;
        }
    }
};

多个角色对象可以组成一个游戏场景,通过操作这些结构体对象来实现游戏的各种逻辑。

数据存储与传输

在网络编程或文件存储中,结构体可以用于定义数据的格式。例如,在网络通信中,我们可以定义一个结构体来表示数据包:

struct Packet {
    int type; // 数据包类型
    char data[1024]; // 数据内容

    Packet(int t, const char* d) : type(t) {
        std::strncpy(data, d, 1024);
    }
};

这样可以方便地将数据打包发送或从接收到的数据中解包。

图形处理

在图形处理中,结构体可用于表示图形元素。例如,一个表示二维点的结构体在图形绘制和变换中经常用到:

struct Point2D {
    float x;
    float y;

    Point2D operator+(const Point2D& other) {
        return {x + other.x, y + other.y};
    }
};

通过重载运算符,可以方便地对这些点进行数学运算,用于图形的平移、缩放等操作。

总之,C++ 结构体是一种非常强大和灵活的数据类型,在各种编程领域都有广泛的应用。深入理解结构体的特性和用法,对于编写高效、可维护的 C++ 程序至关重要。无论是简单的数据聚合,还是复杂的面向对象编程和模板编程,结构体都能发挥重要作用。在实际项目中,根据具体需求合理使用结构体,可以提高代码的可读性、可维护性和性能。通过对结构体内存布局、成员函数、继承、与类的区别以及在不同领域应用的详细解析,希望读者能够对 C++ 结构体有更深入的认识,并在实际编程中熟练运用。