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

C++类与结构体的区别

2024-01-186.5k 阅读

一、定义和基本语法差异

在 C++ 中,类(class)和结构体(struct)都用于封装数据和相关操作。然而,它们在定义和基本语法上存在一些细微但重要的差别。

1. 成员访问权限默认值

  • :在类中,如果没有显式指定访问修饰符,成员的默认访问权限是 private。例如:
class MyClass {
    int privateData; // 默认为 private 权限
public:
    void setData(int value) {
        privateData = value;
    }
    int getData() {
        return privateData;
    }
};

这里的 privateData 变量只能在类的内部成员函数中访问,外部代码无法直接访问它。

  • 结构体:在结构体中,如果没有显式指定访问修饰符,成员的默认访问权限是 public。比如:
struct MyStruct {
    int publicData; // 默认为 public 权限
    void setData(int value) {
        publicData = value;
    }
    int getData() {
        return publicData;
    }
};

此时,publicData 变量在结构体外部可以直接访问,例如:

int main() {
    MyStruct s;
    s.publicData = 10; // 合法,因为 publicData 默认为 public 权限
    return 0;
}

2. 继承方式默认值

  • :当一个类从另一个类继承时,如果没有显式指定继承方式,默认是 private 继承。例如:
class Base {
public:
    int baseData;
};
class Derived : Base { // 默认为 private 继承
public:
    void accessBaseData() {
        baseData = 10; // 在 Derived 类内部可以访问,因为是继承过来的
    }
};
int main() {
    Derived d;
    // d.baseData = 20; // 非法,因为默认是 private 继承,baseData 在外部不可访问
    return 0;
}
  • 结构体:当一个结构体从另一个结构体或类继承时,如果没有显式指定继承方式,默认是 public 继承。例如:
class Base {
public:
    int baseData;
};
struct Derived : Base { // 默认为 public 继承
public:
    void accessBaseData() {
        baseData = 10; // 在 Derived 结构体内部可以访问
    }
};
int main() {
    Derived d;
    d.baseData = 20; // 合法,因为默认是 public 继承,baseData 在外部可访问
    return 0;
}

二、功能和设计理念差异

1. 面向对象设计理念

  • :类是 C++ 中面向对象编程的核心概念。它强调数据封装、继承和多态性。类通常用于创建复杂的、具有丰富行为和状态的对象。例如,在一个游戏开发中,一个 Character 类可能包含属性(如生命值、攻击力等)和行为(如移动、攻击等)。
class Character {
private:
    int health;
    int attackPower;
public:
    Character(int h, int ap) : health(h), attackPower(ap) {}
    void move(int x, int y) {
        // 实现移动逻辑
    }
    void attack(Character& target) {
        target.health -= attackPower;
    }
};
  • 结构体:结构体更多地用于封装简单的数据集合,侧重于数据的组织。它通常不包含复杂的行为,主要目的是方便地管理和传递相关的数据。比如,在一个图形库中,可能用结构体来表示一个点的坐标:
struct Point {
    int x;
    int y;
};

这里的 Point 结构体只是简单地封装了两个整数来表示点的位置,没有复杂的行为。

2. 内存布局和性能

  • :由于类支持复杂的特性,如虚函数、继承等,其内存布局相对复杂。当类包含虚函数时,会增加一个虚函数表指针(vptr),这会占用额外的内存空间。例如:
class BaseWithVirtual {
public:
    virtual void virtualFunction() {}
};
class DerivedFromVirtual : public BaseWithVirtual {
public:
    void virtualFunction() override {}
};

在 32 位系统下,BaseWithVirtual 类的对象大小通常会比其成员变量的总大小多 4 字节(用于存储 vptr)。在 64 位系统下,会多 8 字节。

  • 结构体:结构体的内存布局相对简单,通常按照成员变量声明的顺序依次排列,没有额外的隐藏开销(除非使用了一些特殊的指令,如 #pragma pack 来改变对齐方式)。例如:
struct SimpleStruct {
    int a;
    char b;
    short c;
};

假设 int 占 4 字节,char 占 1 字节,short 占 2 字节,在默认对齐方式下,SimpleStruct 的大小通常为 8 字节(4 + 1 + 3(补齐) = 8)。这种简单的内存布局使得结构体在内存访问和数据传递时可能具有更高的效率,特别是在处理大量数据时。

三、构造函数和析构函数

1. 构造函数

  • :类的构造函数用于初始化对象的成员变量。它可以有多种形式,包括默认构造函数、带参数的构造函数和拷贝构造函数等。例如:
class MyClassWithConstructors {
private:
    int data;
public:
    MyClassWithConstructors() : data(0) {} // 默认构造函数
    MyClassWithConstructors(int value) : data(value) {} // 带参数的构造函数
    MyClassWithConstructors(const MyClassWithConstructors& other) : data(other.data) {} // 拷贝构造函数
};
  • 结构体:结构体同样可以有构造函数。虽然传统的 C 语言风格的结构体没有构造函数,但在 C++ 中,结构体可以像类一样定义构造函数。例如:
struct MyStructWithConstructors {
    int data;
    MyStructWithConstructors() : data(0) {}
    MyStructWithConstructors(int value) : data(value) {}
    MyStructWithConstructors(const MyStructWithConstructors& other) : data(other.data) {}
};

不过,由于结构体通常用于简单的数据集合,其构造函数往往不像类的构造函数那样复杂,主要用于基本的数据初始化。

2. 析构函数

  • :类的析构函数用于在对象销毁时执行清理操作,比如释放动态分配的内存。例如:
class MyClassWithDestructor {
private:
    int* dynamicData;
public:
    MyClassWithDestructor() {
        dynamicData = new int(0);
    }
    ~MyClassWithDestructor() {
        delete dynamicData;
    }
};
  • 结构体:结构体也可以有析构函数,用于类似的清理操作。但由于结构体一般不涉及复杂的资源管理,其析构函数相对较少使用。例如:
struct MyStructWithDestructor {
    int* dynamicData;
    MyStructWithDestructor() {
        dynamicData = new int(0);
    }
    ~MyStructWithDestructor() {
        delete dynamicData;
    }
};

四、模板使用差异

1. 模板类和模板结构体

  • :模板类是 C++ 中强大的泛型编程工具。通过模板类,可以定义一种通用的类结构,其数据类型在使用时才确定。例如:
template <typename T>
class Stack {
private:
    T* data;
    int top;
    int capacity;
public:
    Stack(int cap) : top(-1), capacity(cap) {
        data = new T[capacity];
    }
    ~Stack() {
        delete[] data;
    }
    void push(T value) {
        if (top < capacity - 1) {
            data[++top] = value;
        }
    }
    T pop() {
        if (top >= 0) {
            return data[top--];
        }
        return T();
    }
};

这里定义了一个模板类 Stack,可以用于创建不同数据类型的栈。

  • 结构体:模板结构体同样用于泛型编程,其语法和功能与模板类类似。例如:
template <typename T>
struct Pair {
    T first;
    T second;
    Pair(T f, T s) : first(f), second(s) {}
};

模板结构体 Pair 可以用于存储两个相同类型的数据。

2. 模板特化

  • :类模板可以进行特化,以针对特定的数据类型提供不同的实现。例如:
template <>
class Stack<bool> {
private:
    char data;
    int top;
public:
    Stack(int cap) : top(-1) {
        data = 0;
    }
    ~Stack() {}
    void push(bool value) {
        if (top < 7) {
            data |= (value << top);
            top++;
        }
    }
    bool pop() {
        if (top >= 0) {
            top--;
            return (data >> top) & 1;
        }
        return false;
    }
};

这里对 Stack<bool> 进行了特化,针对布尔类型提供了更紧凑的存储方式。

  • 结构体:结构体模板也可以进行特化。例如:
template <>
struct Pair<bool> {
    bool first;
    bool second;
    Pair(bool f, bool s) : first(f), second(s) {}
    bool combined() {
        return first && second;
    }
};

这里对 Pair<bool> 进行了特化,增加了一个 combined 成员函数。

五、与 C 语言的兼容性

1. 结构体的兼容性

  • C++ 中的结构体在很大程度上与 C 语言兼容。C 语言风格的结构体定义在 C++ 中仍然有效,并且可以在 C++ 代码中使用。例如:
// C 语言风格的结构体定义
struct CStyleStruct {
    int value;
};
int main() {
    CStyleStruct s;
    s.value = 10;
    return 0;
}

这种兼容性使得在将 C 代码移植到 C++ 时相对容易,并且可以在 C++ 代码中方便地与 C 库进行交互。

2. 类的不兼容性

  • 类是 C++ 特有的概念,在 C 语言中不存在。因此,C++ 中的类无法直接在 C 语言环境中使用。如果需要在 C 和 C++ 之间共享代码,通常需要通过特定的接口来实现。例如,可以使用 C 语言风格的函数接口来封装 C++ 类的功能,然后在 C 代码中调用这些函数。
// C++ 代码
class MyClassForCInterop {
private:
    int data;
public:
    MyClassForCInterop(int value) : data(value) {}
    int getData() {
        return data;
    }
};
extern "C" {
    MyClassForCInterop* createMyClass(int value) {
        return new MyClassForCInterop(value);
    }
    int getMyClassData(MyClassForCInterop* obj) {
        return obj->getData();
    }
    void deleteMyClass(MyClassForCInterop* obj) {
        delete obj;
    }
}

然后在 C 代码中可以这样调用:

// C 代码
#include <stdio.h>
// 假设这些函数声明在一个头文件中
MyClassForCInterop* createMyClass(int value);
int getMyClassData(MyClassForCInterop* obj);
void deleteMyClass(MyClassForCInterop* obj);
int main() {
    MyClassForCInterop* obj = createMyClass(10);
    int data = getMyClassData(obj);
    printf("Data: %d\n", data);
    deleteMyClass(obj);
    return 0;
}

六、运算符重载

1. 类的运算符重载

  • 类可以重载各种运算符,以实现自定义的操作。例如,重载 + 运算符用于两个类对象相加:
class Vector {
private:
    int x;
    int y;
public:
    Vector(int a, int b) : x(a), y(b) {}
    Vector operator+(const Vector& other) {
        return Vector(x + other.x, y + other.y);
    }
};

然后可以这样使用:

int main() {
    Vector v1(1, 2);
    Vector v2(3, 4);
    Vector result = v1 + v2;
    return 0;
}
  • 运算符重载在类中可以实现丰富的功能,比如重载 << 运算符用于输出对象的内容,重载 [] 运算符用于像数组一样访问对象的成员等。

2. 结构体的运算符重载

  • 结构体同样可以重载运算符。例如,对于一个表示二维点的结构体,可以重载 + 运算符来实现点的坐标相加:
struct Point {
    int x;
    int y;
    Point operator+(const Point& other) {
        return Point(x + other.x, y + other.y);
    }
};

然后可以:

int main() {
    Point p1 = {1, 2};
    Point p2 = {3, 4};
    Point result = p1 + p2;
    return 0;
}

虽然结构体和类在运算符重载方面的语法相似,但由于结构体的设计理念侧重于数据集合,其运算符重载通常用于更简单的、与数据操作直接相关的功能。

七、友元关系

1. 类的友元

  • 类可以定义友元函数或友元类,使得这些友元能够访问类的私有成员。例如:
class MyClassWithFriend {
private:
    int privateData;
public:
    MyClassWithFriend(int value) : privateData(value) {}
    friend void printPrivateData(MyClassWithFriend& obj);
};
void printPrivateData(MyClassWithFriend& obj) {
    printf("Private data: %d\n", obj.privateData);
}

这里 printPrivateData 函数是 MyClassWithFriend 类的友元,能够访问其私有成员 privateData

2. 结构体的友元

  • 结构体同样可以定义友元。例如:
struct MyStructWithFriend {
    int privateData;
    MyStructWithFriend(int value) : privateData(value) {}
    friend void printPrivateData(MyStructWithFriend& obj);
};
void printPrivateData(MyStructWithFriend& obj) {
    printf("Private data in struct: %d\n", obj.privateData);
}

不过,由于结构体的成员默认是 public,友元在结构体中的使用相对较少,主要用于一些特殊的访问控制需求。

八、多重继承与虚拟继承

1. 类的多重继承与虚拟继承

  • 多重继承:类可以从多个基类继承,这种方式称为多重继承。例如:
class A {
public:
    void functionA() {}
};
class B {
public:
    void functionB() {}
};
class C : public A, public B {
public:
    void functionC() {}
};

这里 C 类从 A 类和 B 类继承,它可以使用 A 类和 B 类的成员函数。然而,多重继承可能会导致菱形继承问题,即当一个类从多个路径继承同一个基类时,会出现数据冗余和命名冲突等问题。

  • 虚拟继承:为了解决菱形继承问题,C++ 引入了虚拟继承。例如:
class Base {
public:
    int sharedData;
};
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
class Final : public Derived1, public Derived2 {};

在这种情况下,Final 类只会有一份 Base 类的 sharedData 成员,避免了数据冗余。

2. 结构体的多重继承与虚拟继承

  • 结构体同样支持多重继承和虚拟继承,语法与类相同。例如:
struct AStruct {
    void functionAStruct() {}
};
struct BStruct {
    void functionBStruct() {}
};
struct CStruct : public AStruct, public BStruct {
    void functionCStruct() {}
};
struct BaseStruct {
    int sharedDataStruct;
};
struct Derived1Struct : virtual public BaseStruct {};
struct Derived2Struct : virtual public BaseStruct {};
struct FinalStruct : public Derived1Struct, public Derived2Struct {};

但由于结构体通常用于简单的数据组织,多重继承和虚拟继承在结构体中的使用场景相对较少,因为复杂的继承关系可能会破坏结构体简单直观的特点。

九、总结类与结构体区别的实际应用场景

  1. 数据传输和存储:在需要高效地存储和传输简单数据集合时,结构体是很好的选择。例如,在网络通信中,数据包的格式可以用结构体来定义,因为结构体的内存布局简单,便于直接在网络流中进行序列化和反序列化。而类由于可能存在复杂的成员函数和隐藏的开销,不太适合这种场景。
  2. 复杂对象建模:当需要创建具有复杂行为和状态的对象,并且需要充分利用面向对象编程的特性(如封装、继承、多态)时,类是首选。例如,在开发一个大型的游戏引擎中,各种游戏角色、场景元素等都可以用类来建模,通过继承和多态实现不同角色的差异化行为。
  3. 与 C 代码交互:如果项目中需要与 C 代码进行交互,结构体由于与 C 语言的兼容性,可以方便地在 C 和 C++ 之间共享数据。而类则需要通过特定的接口来实现与 C 代码的交互。
  4. 泛型编程:在泛型编程中,模板类和模板结构体都有广泛应用。但类模板更适合实现复杂的泛型算法和数据结构,而结构体模板则常用于简单的数据封装,如 std::pair 等标准库中的结构体模板。

通过对 C++ 中类和结构体在各个方面的区别分析,开发者可以根据具体的需求和场景,选择最合适的方式来进行编程,以实现高效、可读且易于维护的代码。