在C++中,类(class)是构建面向对象程序的基本单元,它将数据和对数据的操作封装在一起。与C语言的结构体相比,C++的类不仅支持成员变量,还支持成员函数、访问控制和构造析构机制,是实现封装、继承和多态的基础。

一、C++类的基本定义语法
使用 class 关键字可以定义一个类。类体内部可以声明数据成员和成员函数,并通过访问说明符控制可见性。如果不写访问说明符,class 中的成员默认是 private,而 struct 默认是 public,这是二者最核心的区别。
下面是一个最简单的类定义示例,其中包含一个私有数据成员和两个公有成员函数:
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
void setInfo(const std::string& n, int a) {
name = n;
age = a;
}
void printInfo() {
std::cout << "姓名:" << name << ",年龄:" << age << std::endl;
}
};
int main() {
Student s;
s.setInfo("张三", 20);
s.printInfo();
return 0;
}
在上面的代码中,name 和 age 被声明为 private,外部无法直接修改,必须通过 setInfo 这类公有接口操作,这就实现了最基本的封装。如果试图在 main 中写 s.age = 20;,编译器会直接报错。
除了 class,C++ 也允许用 struct 定义类,但 struct 的默认访问权限是 public。在需要严格封装的场景下,通常推荐使用 class,以避免误把内部数据暴露给外部调用者。
二、构造函数与析构函数
类定义中不可或缺的是构造函数,它在对象创建时自动调用,用于初始化成员。如果类中没有显式定义构造函数,编译器会生成一个默认构造函数,但它不会给成员赋予有意义的初始值。
我们可以在类中定义带参数的构造函数,并借助初始化列表提升效率。析构函数则在对象销毁时调用,适合释放动态分配的资源。下面示例展示了构造与析构的基本写法:
#include <iostream>
#include <cstring>
class Buffer {
private:
char* data;
int size;
public:
Buffer(int s) : size(s) {
data = new char[s];
std::cout << "构造:分配了 " << s << " 字节" << std::endl;
}
~Buffer() {
delete[] data;
std::cout << "析构:释放了资源" << std::endl;
}
int getSize() const {
return size;
}
};
int main() {
Buffer buf(128);
std::cout << "缓冲区大小:" << buf.getSize() << std::endl;
return 0;
}
使用初始化列表 Buffer(int s) : size(s) 可以直接初始化成员,而不是先默认构造再赋值,对于非基础类型来说能减少一次多余操作。析构函数名前带波浪号,无参数无返回值,不能重载。
当类管理了堆内存、文件句柄或网络连接时,析构函数尤为重要。若缺少正确的释放逻辑,就会造成资源泄漏。现代 C++ 更推荐用智能指针替代原始指针,但理解构造析构仍是掌握类定义的前提。
三、访问控制与封装价值
C++ 提供三种访问说明符:public、private 和 protected。public 成员可被任意外部代码访问;private 仅类自身和友元可访问;protected 则允许派生类访问,但外部仍不可见。
合理设置访问层级,可以让类的调用者只关心接口而非实现。例如下面的类把内部计数隐藏起来,只暴露自增和获取方法:
#include <iostream>
class Counter {
private:
int value;
protected:
void resetBase() {
value = 0;
}
public:
Counter() : value(0) {}
void increase() {
value++;
}
int get() const {
return value;
}
};
int main() {
Counter c;
c.increase();
c.increase();
std::cout << "当前计数:" << c.get() << std::endl;
return 0;
}
通过将 value 设为 private,我们避免了外部随意篡改计数状态,也方便以后把实现改成线程安全版本而不影响调用方。protected 的 resetBase 虽不能被 main 直接使用,却能在后续继承体系中供子类复用。
封装带来的不只是安全性,还有可维护性。当业务逻辑变更时,只要公有接口不变,内部实现的调整就不会波及上层代码,这在大型项目中极为关键。
四、拷贝控制与常见误区
类定义还涉及拷贝构造和拷贝赋值。若类中含有指针成员,编译器生成的默认拷贝只会浅拷贝地址,导致两个对象指向同一块内存,析构时重复释放。此时必须自定义拷贝逻辑。
以下示例演示了浅拷贝引发的问题以及深拷贝的修正方式:
#include <iostream>
#include <cstring>
class Text {
private:
char* buf;
public:
Text(const char* s) {
buf = new char[strlen(s) + 1];
strcpy(buf, s);
}
// 深拷贝构造函数
Text(const Text& other) {
buf = new char[strlen(other.buf) + 1];
strcpy(buf, other.buf);
}
~Text() {
delete[] buf;
}
void show() {
std::cout << buf << std::endl;
}
};
int main() {
Text a("hello");
Text b = a;
a.show();
b.show();
return 0;
}
如果没有自定义的拷贝构造函数,b 会共享 a 的 buf 指针,两者析构时都会 delete 同一块内存,程序极可能崩溃。通过实现深拷贝,每个对象拥有独立资源,生命周期互不干扰。
在 C++11 之后,还可以用 = delete 禁止拷贝,或用移动语义提升性能。理解这些拷贝控制规则,是写出健壮类定义的重要一步,也能避免很多隐蔽的运行时错误。
五、从类定义看面向对象设计
定义类并不是简单地罗列变量和函数,而是对现实事物做抽象。好的类应当职责单一、接口清晰、内部状态可控。例如把“学生”抽象成 Student 类,把“缓冲区”抽象成 Buffer 类,都比散落全局变量更容易扩展和测试。
在真实项目中,我们常把类声明放在头文件,定义放在源文件,通过 #ifndef 守卫避免重复包含。这样既能缩短编译时间,也能隐藏实现细节。结合命名空间,还能有效规避符号冲突。
当你能熟练运用 class、访问控制、构造析构与拷贝控制时,就意味着已经跨过了 C++ 面向对象的大门。后续再学习继承、多态和模板,都是在类这一基石上继续搭建更复杂、更灵活的软件结构。