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

C++ class与struct的本质差异

2023-04-173.8k 阅读

C++ 中 classstruct 的基本定义

在 C++ 里,classstruct 都用于定义用户自定义的数据类型,也就是创建一种新的数据结构,把不同类型的数据组合在一起。

先看 struct 的定义示例:

struct Point {
    int x;
    int y;
};

这里定义了一个名为 Pointstruct,它包含两个成员变量 xy,都是 int 类型。

再看 class 的定义示例:

class Rectangle {
    int width;
    int height;
};

Rectangle 类同样包含两个 int 类型的成员变量 widthheight

从上述简单示例可看出,classstruct 在定义数据结构的形式上很相似,都是把相关的数据成员组合在一起。

成员访问权限的差异

  1. 默认访问权限
    • struct 的默认访问权限struct 的成员默认是 public 访问权限。这意味着在定义了一个 struct 后,外部代码可以直接访问其成员变量。例如:
struct Book {
    std::string title;
    std::string author;
};

int main() {
    Book myBook;
    myBook.title = "C++ Primer";
    myBook.author = "Stanley Lippman";
    return 0;
}

在上述代码中,在 main 函数里可以直接访问 Book 结构体的 titleauthor 成员变量,因为它们默认是 public 的。 - class 的默认访问权限class 的成员默认是 private 访问权限。例如:

class Circle {
    double radius;
public:
    double getArea() {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle myCircle;
    // myCircle.radius = 5.0; // 这行代码会报错,因为radius默认是private的
    double area = myCircle.getArea();
    return 0;
}

在这个 Circle 类中,radius 成员变量默认是 private 的,所以在 main 函数中直接访问 radius 会导致编译错误。只能通过 public 成员函数 getArea 来间接访问 radius 以计算面积。 2. 访问权限修饰符的使用 - 无论是 class 还是 struct,都可以使用 publicprivateprotected 访问权限修饰符来明确指定成员的访问权限。 - public:被 public 修饰的成员可以在类或结构体的外部被访问。例如,对于上述 Circle 类中的 getArea 函数,由于它是 public 的,所以在 main 函数中可以调用。 - privateprivate 修饰的成员只能在类或结构体内部被访问。像 Circle 类中的 radius 变量,在类外部无法直接访问。 - protectedprotected 修饰的成员和 private 类似,区别在于 protected 成员在类的派生类中可以被访问。例如:

class Shape {
protected:
    std::string color;
};

class Triangle : public Shape {
public:
    void setColor(const std::string& newColor) {
        color = newColor;
    }
};

int main() {
    Triangle myTriangle;
    // myTriangle.color = "red"; // 这行代码会报错,在外部不能访问protected成员
    myTriangle.setColor("red");
    return 0;
}

在上述代码中,Shape 类的 color 成员是 protected 的,Triangle 类继承自 Shape,在 Triangle 类内部可以访问 color 成员,而在外部则不能直接访问。

继承相关的差异

  1. 默认继承方式
    • struct 的默认继承方式:当一个 struct 继承另一个 struct 或者 class 时,默认的继承方式是 public 继承。例如:
struct Animal {
    std::string name;
};

struct Dog : Animal {
    int age;
};

int main() {
    Dog myDog;
    myDog.name = "Buddy";
    myDog.age = 3;
    return 0;
}

这里 Dog 结构体从 Animal 结构体继承,由于是 struct 的继承,默认是 public 继承,所以在 main 函数中可以直接访问从 Animal 继承来的 name 成员。 - class 的默认继承方式:当一个 class 继承另一个 class 或者 struct 时,默认的继承方式是 private 继承。例如:

class Vehicle {
    int wheels;
public:
    int getWheels() {
        return wheels;
    }
};

class Car : Vehicle {
    int doors;
};

int main() {
    Car myCar;
    // myCar.wheels = 4; // 这行代码会报错,因为默认是private继承
    // int wheels = myCar.getWheels(); // 这行代码也会报错,private继承使得getWheels函数在Car类外部不可访问
    return 0;
}

在这个例子中,Car 类从 Vehicle 类继承,默认是 private 继承,所以 Vehicle 类的 public 成员 getWheelsCar 类外部也无法访问,private 成员 wheels 更是不能在外部访问。 2. 显式指定继承方式 - 无论是 class 还是 struct,都可以显式指定继承方式,即 publicprivateprotected 继承。 - public 继承:在 public 继承中,基类的 public 成员在派生类中仍然是 public 的,protected 成员在派生类中仍然是 protected 的,private 成员在派生类中不可访问。例如:

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class PublicDerived : public Base {
public:
    void accessData() {
        publicData = 10;
        protectedData = 20;
        // privateData = 30; // 这行代码会报错,private成员在派生类中不可访问
    }
};

int main() {
    PublicDerived obj;
    obj.publicData = 5;
    // obj.protectedData = 15; // 这行代码会报错,protected成员在类外部不可访问
    return 0;
}
- **`private` 继承**:在 `private` 继承中,基类的 `public` 和 `protected` 成员在派生类中都变成 `private` 成员,`private` 成员在派生类中不可访问。例如:
class AnotherBase {
public:
    int publicVal;
protected:
    int protectedVal;
};

class PrivateDerived : private AnotherBase {
public:
    void useData() {
        publicVal = 100;
        protectedVal = 200;
    }
};

int main() {
    PrivateDerived pObj;
    // pObj.publicVal = 50; // 这行代码会报错,因为private继承后publicVal变为private
    return 0;
}
- **`protected` 继承**:在 `protected` 继承中,基类的 `public` 成员在派生类中变成 `protected` 成员,`protected` 成员在派生类中仍然是 `protected` 成员,`private` 成员在派生类中不可访问。例如:
class ThirdBase {
public:
    int publicInfo;
protected:
    int protectedInfo;
};

class ProtectedDerived : protected ThirdBase {
public:
    void manipulateData() {
        publicInfo = 1000;
        protectedInfo = 2000;
    }
};

class FurtherDerived : public ProtectedDerived {
public:
    void accessData() {
        publicInfo = 3000;
        protectedInfo = 4000;
    }
};

int main() {
    ProtectedDerived pDerived;
    // pDerived.publicInfo = 500; // 这行代码会报错,因为protected继承后publicInfo变为protected
    FurtherDerived fDerived;
    // fDerived.publicInfo = 600; // 这行代码不会报错,因为在FurtherDerived中publicInfo是protected的可以访问
    return 0;
}

模板参数方面的差异

  1. 模板参数列表中的默认使用
    • 在模板参数列表中,如果没有特别指定,classtypename 是等价的,可以互换使用来表示类型参数。例如:
template <class T>
class Stack {
    T* data;
    int top;
public:
    Stack() : top(-1) {
        data = new T[100];
    }
    ~Stack() {
        delete[] data;
    }
    void push(const T& value) {
        data[++top] = value;
    }
    T pop() {
        return data[top--];
    }
};

这里的 class T 也可以写成 typename T。 - 然而,在模板参数列表中使用 struct 时,它通常用于表示非类型参数或者特定的模板特化情况。例如,当定义一个模板类,它的模板参数是一个整数时:

template <int N>
struct Fibonacci {
    enum { value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value };
};

template <>
struct Fibonacci<0> {
    enum { value = 0 };
};

template <>
struct Fibonacci<1> {
    enum { value = 1 };
};

这里使用 struct 来定义模板类,用于计算斐波那契数列的值。通过模板特化,分别定义了 N 为 0 和 1 时的情况,然后通过递归计算其他 N 值对应的斐波那契数。 2. 模板特化中的使用 - class 在模板特化中的应用:当对一个模板类进行特化时,通常使用 class。例如,对于上述的 Stack 模板类,如果要为 bool 类型特化:

template <>
class Stack<bool> {
    bool* data;
    int top;
public:
    Stack() : top(-1) {
        data = new bool[100];
    }
    ~Stack() {
        delete[] data;
    }
    void push(const bool& value) {
        data[++top] = value;
    }
    bool pop() {
        return data[top--];
    }
};
- **`struct` 在模板特化中的应用**:同样,`struct` 也可以用于模板特化。例如,对于 `Fibonacci` 模板结构,也可以进行更复杂的特化,比如针对偶数 `N` 的特化:
template <int N>
struct FibonacciEven {
    enum { value = Fibonacci<N - 2>::value + Fibonacci<N - 4>::value };
};

template <>
struct FibonacciEven<0> {
    enum { value = 0 };
};

template <>
struct FibonacciEven<2> {
    enum { value = 1 };
};

这里通过 structFibonacci 相关的模板进行了针对偶数 N 的特化,展示了 struct 在模板特化中的另一种应用场景。

与 C 语言兼容性及历史背景差异

  1. 与 C 语言的兼容性
    • struct:C++ 中的 struct 很大程度上保留了与 C 语言的兼容性。在 C 语言中,struct 主要用于聚合数据,不支持成员函数、访问权限控制等面向对象特性。C++ 对 struct 进行了扩展,使其可以包含成员函数、访问权限修饰符等,但从 C 语言角度看,一个简单的 struct 定义在 C++ 中仍然可以像在 C 语言中一样使用。例如:
// C 语言代码
struct Point {
    int x;
    int y;
};

// C++ 代码
struct Point {
    int x;
    int y;
    int distanceFromOrigin() {
        return std::sqrt(x * x + y * y);
    }
};

在 C++ 中,Point 结构体既可以像在 C 语言中那样简单地存储数据,又可以添加成员函数实现更多功能。 - classclass 是 C++ 面向对象编程的核心概念,在 C 语言中不存在。class 强调封装、继承和多态等面向对象特性,它的默认访问权限、默认继承方式等设计都是为了更好地实现面向对象编程,与 C 语言的传统编程模式有较大差异。例如,C 语言中没有像 class 那样默认成员为 private 的概念。 2. 历史背景 - structstruct 在编程语言历史中出现较早,最初主要是作为一种简单的数据聚合方式,将不同类型的数据组合在一起。在 C 语言中广泛使用,C++ 继承并扩展了 struct 的功能。 - classclass 是 C++ 为了实现面向对象编程而引入的概念,它将数据和操作数据的函数封装在一起,通过访问权限控制等机制提高了代码的安全性和可维护性。class 的设计理念使得 C++ 能够更好地支持大型软件项目的开发,实现更复杂的软件架构。

内存布局方面的差异

  1. 简单数据成员的内存布局
    • struct:对于只包含简单数据成员(如基本数据类型)且没有继承、虚函数等复杂特性的 struct,其内存布局通常是按照成员声明的顺序依次排列。例如:
struct SimpleStruct {
    int a;
    char b;
    short c;
};

在 32 位系统下,假设 int 占 4 字节,char 占 1 字节,short 占 2 字节,SimpleStruct 的大小可能是 4 + 1 + 2 = 7 字节,但由于内存对齐的原因,实际大小可能会被调整为 8 字节。内存布局大致如下:

偏移内容
0 - 3a (int)
4b (char)
5 - 6c (short)
7填充字节(为了内存对齐)
- **`class`**:同样对于只包含简单数据成员且无复杂特性的 `class`,其内存布局规则与 `struct` 类似,也是按照成员声明顺序排列,并遵循内存对齐原则。例如:
class SimpleClass {
    int a;
    char b;
    short c;
};

SimpleClass 的内存布局和 SimpleStruct 在这种情况下基本相同。

  1. 包含虚函数的内存布局
    • struct:当 struct 包含虚函数时,它会有一个虚函数表指针(vptr)。虚函数表指针通常位于 struct 对象的开头。例如:
struct VirtualStruct {
    virtual void virtualFunction() {}
    int data;
};

在 32 位系统下,VirtualStruct 的内存布局可能如下:

偏移内容
0 - 3vptr(指向虚函数表)
4 - 7data (int)

虚函数表中存储了虚函数的地址,通过 vptr 可以找到对应的虚函数进行调用。 - class:当 class 包含虚函数时,内存布局也会有 vptr,且位置同样通常在对象开头。例如:

class VirtualClass {
    virtual void virtualFunction() {}
    int data;
};

VirtualClass 的内存布局和 VirtualStruct 在这种情况下类似,都是开头为 vptr,然后是其他数据成员。

  1. 继承情况下的内存布局
    • struct:在 struct 继承的情况下,派生 struct 的内存布局是基 struct 的内存布局在前,然后是派生 struct 自己新增的成员。例如:
struct BaseStruct {
    int baseData;
};

struct DerivedStruct : BaseStruct {
    char derivedData;
};

假设在 32 位系统下,BaseStruct 大小为 4 字节(baseData 占 4 字节),DerivedStruct 的大小可能是 4 + 1 = 5 字节,但考虑内存对齐可能为 8 字节。其内存布局大致为:

偏移内容
0 - 3baseData (from BaseStruct)
4derivedData
5 - 7填充字节(为了内存对齐)
- **`class`**:对于 `class` 继承,内存布局规则类似,基 `class` 的内存布局在前,然后是派生 `class` 新增的成员。例如:
class BaseClass {
    int baseData;
};

class DerivedClass : public BaseClass {
    char derivedData;
};

DerivedClass 的内存布局和 DerivedStruct 在这种继承情况下类似。不过,如果涉及到不同的继承方式(publicprivateprotected),访问权限的不同会影响外部对这些成员的访问,但内存布局本身不受访问权限影响。

实际应用场景的差异

  1. 数据聚合场景
    • struct:当主要目的是聚合数据,并且这些数据需要在外部广泛访问时,struct 是一个很好的选择。例如,在图形处理中定义一个表示颜色的结构体:
struct Color {
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

在这种情况下,Color 结构体的成员需要在很多地方被直接访问,以设置和获取颜色值,使用 struct 的默认 public 访问权限很方便。 - class:虽然 class 也可以用于数据聚合,但由于其默认的 private 访问权限,需要额外设置成员为 public,相对来说使用 struct 更简洁直接。不过,如果在数据聚合的同时需要一些特定的操作和更严格的访问控制,class 可能更合适。例如,一个表示日期的类,除了存储年、月、日数据外,还需要一些验证和格式化操作:

class Date {
    int year;
    int month;
    int day;
public:
    Date(int y, int m, int d) : year(y), month(m), day(d) {}
    bool isValid() {
        // 日期验证逻辑
    }
    std::string format() {
        // 日期格式化逻辑
    }
};
  1. 面向对象编程场景
    • class:在面向对象编程中,class 是核心工具。它通过封装、继承和多态实现复杂的软件架构。例如,实现一个游戏角色的继承体系:
class Character {
protected:
    std::string name;
    int health;
public:
    Character(const std::string& n, int h) : name(n), health(h) {}
    virtual void attack() {}
    virtual void defend() {}
};

class Warrior : public Character {
public:
    Warrior(const std::string& n, int h) : Character(n, h) {}
    void attack() override {
        // 战士攻击逻辑
    }
    void defend() override {
        // 战士防御逻辑
    }
};

class Mage : public Character {
public:
    Mage(const std::string& n, int h) : Character(n, h) {}
    void attack() override {
        // 法师攻击逻辑
    }
    void defend() override {
        // 法师防御逻辑
    }
};

这里通过 class 实现了一个游戏角色的继承体系,利用虚函数实现多态,不同的派生类有不同的攻击和防御行为。 - struct:虽然 struct 也可以通过扩展支持面向对象特性,但在这种复杂的面向对象编程场景下,class 的语法和特性更符合面向对象编程的习惯和要求。不过,在一些简单的面向对象场景中,如果不需要严格的访问控制,struct 也可以使用。例如,定义一个简单的图形基类和派生类:

struct Shape {
    virtual void draw() {}
};

struct Circle : Shape {
    int radius;
    void draw() override {
        // 画圆逻辑
    }
};

总结

通过以上多方面的分析可以看出,classstruct 在 C++ 中有很多相似之处,但也存在本质差异。struct 更侧重于数据聚合,默认的 public 访问权限和简单的语法使其在数据简单组合且需要广泛外部访问的场景中使用方便,同时保持了与 C 语言的兼容性。而 class 则是 C++ 面向对象编程的核心,默认的 private 访问权限、丰富的面向对象特性使其在构建复杂软件系统、实现严格的封装和继承体系等方面具有优势。在实际编程中,应根据具体需求合理选择使用 classstruct,以充分发挥 C++ 的强大功能。