C++中的struct并不仅仅是C语言那种只能堆放数据字段的容器。它在语法层面上与class几乎完全等价:可以包含成员函数、构造函数、析构函数、静态成员,也可以使用继承、多态和访问控制。两者真正的区别只有默认访问权限——struct的成员默认是public,而class的成员默认是private。理解这一点之后,在结构体中写成员函数和构造函数就变得非常自然。本文会从结构体与类的差异切入,重点演示成员函数的类内定义、类外定义、构造函数初始化列表和委托构造,并说明实际使用中的一些细节。

一、struct 与 class 到底差在哪
很多人从C语言过渡到C++时,会把struct当作纯数据结构,把class当作面向对象类型。但在C++标准里,这两者可以声明完全相同的内容,唯一的差异是默认访问级别。下面这段代码定义了两个类型,一个用struct,一个用class,从行为上看它们可以做到完全一致。
#include <iostream>
struct PointS {
double x;
double y;
void show() const {
std::cout << x << "," << y << '\n';
}
};
class PointC {
double x;
double y;
public:
PointC(double x_, double y_) : x(x_), y(y_) {}
void show() const {
std::cout << x << "," << y << '\n';
}
};
int main() {
PointS s{1.0, 2.0};
s.show();
PointC c(3.0, 4.0);
c.show();
}
在PointS中,x、y没有写访问修饰符,但它们默认是public,因此外部可以通过聚合初始化直接赋值。而PointC里的x、y默认是private,必须提供一个公有构造函数才能完成初始化。如果去掉PointC的构造函数,PointC c(3.0,4.0)会因为无法访问私有成员而编译失败。这个例子说明,结构体和类的语法能力没有本质差异,只是默认开放程度不同。
这种默认权限的差别也体现在继承上。struct D : B默认是公有继承,而class D : B默认是私有继承。如果在团队里混用两种类型,最好保持清晰约定:偏数据的简单聚合用struct,需要封装不变量和复杂行为时用class。不要在同一个项目里仅凭个人偏好随意选择,否则阅读成本会上升。
二、成员函数可以在结构体内或结构体外实现
结构体的成员函数既可以直接写在结构体内部,也可以先在结构体里声明、再到外部补全定义。类内定义的成员函数通常会被编译器视为内联函数,适合短小、频繁调用的逻辑。类外定义则需要使用作用域解析运算符::,格式为返回类型 结构体名::函数名(参数表)。
#include <cmath>
#include <iostream>
struct Point {
double x;
double y;
void move(double dx, double dy) {
x += dx;
y += dy;
}
double length() const;
};
double Point::length() const {
return std::sqrt(x * x + y * y);
}
int main() {
Point p{3.0, 4.0};
p.move(2.0, 1.0);
std::cout << p.length() << '\n';
}
这里move采用了类内定义,length采用了类外定义。两者没有性能上的本质区别,真正的区别在于代码组织方式:如果结构体要放在头文件中供多个翻译单元使用,通常建议把较长的函数实现放到.cpp文件里,头文件只保留声明。这样修改实现时不会导致所有包含该头文件的文件重新编译。对于短小的getter、setter或简单计算,直接写在结构体内反而更直观。
成员函数还有一个隐式参数this,它指向调用该函数的对象。在const成员函数中,this的类型是const Point*,因此不能修改数据成员。上面length函数声明为const,就保证了计算长度不会改变点的坐标。如果你尝试在length里写x = 0,编译器会报错。这种约束可以提升接口安全性,也方便调用者判断哪些函数会修改对象。
此外,结构体同样支持静态成员函数。静态成员函数不属于某个具体对象,不能使用this,但可以直接通过Point::create这样的方式调用。比如可以定义一个静态工厂函数static Point origin(),返回坐标原点,这样比直接操作构造逻辑更清晰。
三、构造函数与初始化列表的写法
结构体可以有多个构造函数,包括默认构造函数、带参构造函数、拷贝构造函数等。初始化列表是C++中初始化成员变量的推荐方式,尤其是对于const成员、引用成员或没有默认构造函数的类类型成员,初始化列表几乎是唯一的选择。它直接调用成员的构造函数,而函数体内的赋值则是先默认构造再赋值,效率不同。
#include <iostream>
struct Point {
double x;
double y;
Point() : x(0.0), y(0.0) {
std::cout << "default constructor\n";
}
Point(double x_, double y_) : x(x_), y(y_) {}
Point(double v) : Point(v, v) {}
void print() const {
std::cout << "(" << x << ", " << y << ")\n";
}
};
int main() {
Point a;
Point b(2.0, 5.0);
Point c(7.0);
a.print();
b.print();
c.print();
}
第二个构造函数Point(double x_, double y_) : x(x_), y(y_)中,冒号后面的部分就是初始化列表。这里x(x_)表示用参数x_初始化成员x,而不是先创建x再赋值。第三个构造函数使用了C++11引入的委托构造,它把自己的初始化工作交给Point(v, v),这样就能复用双参数构造函数的逻辑,避免重复代码。
需要特别留意的是,成员初始化的顺序不是初始化列表里写的顺序,而是成员在结构体中声明的顺序。如果初始化列表里先写y再写x,但结构体声明顺序是x、y,那么x仍然会先被初始化。如果两个成员之间有依赖,可能会产生未定义行为。为了避免这种隐患,建议初始化列表的顺序与成员声明顺序保持一致,并尽量让每个成员的初始化独立于其他成员。
如果一个结构体没有显式定义任何构造函数,编译器会尝试生成默认构造函数、拷贝构造函数、移动构造函数等。一旦你定义了带参构造函数,编译器通常不会再自动生成默认构造函数,因此如果仍然需要Point a;这种写法,就必须手动补一个Point()。当结构体包含指针并管理动态资源时,还需要实现拷贝构造、拷贝赋值和析构函数,这就是常说的三法则或五法则。此时结构体虽然能用,但代码复杂度已经接近类,最好重新评估是否继续使用struct。
四、使用建议与常见误区
一个常见的误解是认为struct里不能有private成员,或者认为struct里写了函数就不再是结构体。实际上,C++的结构体可以自由使用private、protected,也可以有虚函数、继承和多态。下面的代码定义了两个几乎相同的结构体,唯一的差别是B含有一个虚函数。
#include <iostream>
struct A {
double x;
void f() {}
};
struct B {
double x;
virtual void f() {}
};
int main() {
std::cout << sizeof(A) << ' ' << sizeof(B) << '\n';
}
在大多数64位平台上,A的大小为8字节,B的大小为16字节。额外的8字节来自虚表指针。也就是说,普通成员函数不会增加对象体积,只有虚函数才会引入额外开销。这说明在结构体中添加普通成员函数不会让对象变重,不需要为了节省内存而把所有逻辑都移到结构体外。
另一个值得注意的问题是聚合初始化。C++11规定,具有用户提供的构造函数、私有或受保护的非静态数据成员、基类等特征的类型不再是聚合类型。如果一个简单结构体定义了构造函数,它就不再支持传统意义上的聚合初始化。虽然列表初始化Point p{1.0,2.0}仍可能调用构造函数,但语义已经不同。如果你的结构体需要与C代码交互,或者依赖聚合初始化的简洁性,请尽量少定义构造函数,保持数据成员的公开和简单。反之,如果希望强制用户通过构造函数创建对象,定义带参构造并隐藏默认构造是更好的做法。
总体来看,C++结构体完全可以包含函数,成员函数和构造函数的实现也没有特殊限制。选择struct还是class更多是一种语义表达:struct通常表示公开的、数据优先的类型;class通常表示封装优先、需要维护内部不变量和行为契约的类型。只要理解默认访问权限和聚合规则,在结构体中灵活使用函数并不会带来任何问题。