C++ class与struct的本质差异
C++ 中 class
和 struct
的基本定义
在 C++ 里,class
和 struct
都用于定义用户自定义的数据类型,也就是创建一种新的数据结构,把不同类型的数据组合在一起。
先看 struct
的定义示例:
struct Point {
int x;
int y;
};
这里定义了一个名为 Point
的 struct
,它包含两个成员变量 x
和 y
,都是 int
类型。
再看 class
的定义示例:
class Rectangle {
int width;
int height;
};
此 Rectangle
类同样包含两个 int
类型的成员变量 width
和 height
。
从上述简单示例可看出,class
和 struct
在定义数据结构的形式上很相似,都是把相关的数据成员组合在一起。
成员访问权限的差异
- 默认访问权限
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
结构体的 title
和 author
成员变量,因为它们默认是 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
,都可以使用 public
、private
和 protected
访问权限修饰符来明确指定成员的访问权限。
- public
:被 public
修饰的成员可以在类或结构体的外部被访问。例如,对于上述 Circle
类中的 getArea
函数,由于它是 public
的,所以在 main
函数中可以调用。
- private
:private
修饰的成员只能在类或结构体内部被访问。像 Circle
类中的 radius
变量,在类外部无法直接访问。
- protected
:protected
修饰的成员和 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
成员,而在外部则不能直接访问。
继承相关的差异
- 默认继承方式
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
成员 getWheels
在 Car
类外部也无法访问,private
成员 wheels
更是不能在外部访问。
2. 显式指定继承方式
- 无论是 class
还是 struct
,都可以显式指定继承方式,即 public
、private
和 protected
继承。
- 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;
}
模板参数方面的差异
- 模板参数列表中的默认使用
- 在模板参数列表中,如果没有特别指定,
class
和typename
是等价的,可以互换使用来表示类型参数。例如:
- 在模板参数列表中,如果没有特别指定,
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 };
};
这里通过 struct
对 Fibonacci
相关的模板进行了针对偶数 N
的特化,展示了 struct
在模板特化中的另一种应用场景。
与 C 语言兼容性及历史背景差异
- 与 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 语言中那样简单地存储数据,又可以添加成员函数实现更多功能。
- class
:class
是 C++ 面向对象编程的核心概念,在 C 语言中不存在。class
强调封装、继承和多态等面向对象特性,它的默认访问权限、默认继承方式等设计都是为了更好地实现面向对象编程,与 C 语言的传统编程模式有较大差异。例如,C 语言中没有像 class
那样默认成员为 private
的概念。
2. 历史背景
- struct
:struct
在编程语言历史中出现较早,最初主要是作为一种简单的数据聚合方式,将不同类型的数据组合在一起。在 C 语言中广泛使用,C++ 继承并扩展了 struct
的功能。
- class
:class
是 C++ 为了实现面向对象编程而引入的概念,它将数据和操作数据的函数封装在一起,通过访问权限控制等机制提高了代码的安全性和可维护性。class
的设计理念使得 C++ 能够更好地支持大型软件项目的开发,实现更复杂的软件架构。
内存布局方面的差异
- 简单数据成员的内存布局
struct
:对于只包含简单数据成员(如基本数据类型)且没有继承、虚函数等复杂特性的struct
,其内存布局通常是按照成员声明的顺序依次排列。例如:
struct SimpleStruct {
int a;
char b;
short c;
};
在 32 位系统下,假设 int
占 4 字节,char
占 1 字节,short
占 2 字节,SimpleStruct
的大小可能是 4 + 1 + 2 = 7 字节,但由于内存对齐的原因,实际大小可能会被调整为 8 字节。内存布局大致如下:
偏移 | 内容 |
---|---|
0 - 3 | a (int) |
4 | b (char) |
5 - 6 | c (short) |
7 | 填充字节(为了内存对齐) |
- **`class`**:同样对于只包含简单数据成员且无复杂特性的 `class`,其内存布局规则与 `struct` 类似,也是按照成员声明顺序排列,并遵循内存对齐原则。例如:
class SimpleClass {
int a;
char b;
short c;
};
SimpleClass
的内存布局和 SimpleStruct
在这种情况下基本相同。
- 包含虚函数的内存布局
struct
:当struct
包含虚函数时,它会有一个虚函数表指针(vptr)。虚函数表指针通常位于struct
对象的开头。例如:
struct VirtualStruct {
virtual void virtualFunction() {}
int data;
};
在 32 位系统下,VirtualStruct
的内存布局可能如下:
偏移 | 内容 |
---|---|
0 - 3 | vptr(指向虚函数表) |
4 - 7 | data (int) |
虚函数表中存储了虚函数的地址,通过 vptr 可以找到对应的虚函数进行调用。
- class
:当 class
包含虚函数时,内存布局也会有 vptr,且位置同样通常在对象开头。例如:
class VirtualClass {
virtual void virtualFunction() {}
int data;
};
VirtualClass
的内存布局和 VirtualStruct
在这种情况下类似,都是开头为 vptr,然后是其他数据成员。
- 继承情况下的内存布局
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 - 3 | baseData (from BaseStruct) |
4 | derivedData |
5 - 7 | 填充字节(为了内存对齐) |
- **`class`**:对于 `class` 继承,内存布局规则类似,基 `class` 的内存布局在前,然后是派生 `class` 新增的成员。例如:
class BaseClass {
int baseData;
};
class DerivedClass : public BaseClass {
char derivedData;
};
DerivedClass
的内存布局和 DerivedStruct
在这种继承情况下类似。不过,如果涉及到不同的继承方式(public
、private
、protected
),访问权限的不同会影响外部对这些成员的访问,但内存布局本身不受访问权限影响。
实际应用场景的差异
- 数据聚合场景
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() {
// 日期格式化逻辑
}
};
- 面向对象编程场景
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 {
// 画圆逻辑
}
};
总结
通过以上多方面的分析可以看出,class
和 struct
在 C++ 中有很多相似之处,但也存在本质差异。struct
更侧重于数据聚合,默认的 public
访问权限和简单的语法使其在数据简单组合且需要广泛外部访问的场景中使用方便,同时保持了与 C 语言的兼容性。而 class
则是 C++ 面向对象编程的核心,默认的 private
访问权限、丰富的面向对象特性使其在构建复杂软件系统、实现严格的封装和继承体系等方面具有优势。在实际编程中,应根据具体需求合理选择使用 class
或 struct
,以充分发挥 C++ 的强大功能。