C++面向对象编程:从类与对象基础到封装、构造析构函数实战

📅 2026/7/15 17:44:41
C++面向对象编程:从类与对象基础到封装、构造析构函数实战
1. 从C到C为什么我们需要“类”和“对象”如果你是从C语言转过来的或者刚开始接触C看到“类”和“对象”这些词第一反应可能是这玩意儿和C语言里的结构体struct有啥区别不都是把一堆数据打包在一起吗我当初也是这么想的直到在实际项目中踩了几个大坑才真正明白面向对象OOP带来的不仅仅是语法糖而是一种全新的、更贴近现实世界问题建模的思维方式。想象一下你要用C语言写一个图形库里面有很多“矩形”。在C里你可能会定义一个结构体struct Rectangle里面有长、宽、坐标然后写一堆函数比如calculateArea(struct Rectangle* rect)drawRectangle(struct Rectangle* rect)。数据和操作这些数据的函数是分离的。这本身没问题但项目大了之后问题就来了你怎么确保每个操作矩形的函数都接收正确的结构体指针万一有人不小心把“圆形”的结构体指针传给了矩形的绘制函数呢编译器可不会报错运行时可能就崩了。更麻烦的是如果你想给矩形加一个“旋转”的功能你得去找到所有相关的函数确保它们都兼容新的数据结构这就像在管理一堆散落的乐高积木很容易出错。C的“类”就是为了解决这个问题而生的。它把数据成员变量和操作这些数据的函数成员函数捆绑在一起封装成一个整体。这个整体就叫“类”。而根据这个“蓝图”创建出来的一个具体实例就叫“对象”。回到矩形的例子在C里你会定义一个Rectangle类这个类内部不仅定义了长、宽、坐标还直接定义了计算面积、绘制自身的方法。数据和操作它的逻辑被“封装”在了一个黑盒里。你用的时候只需要创建一个Rectangle对象然后告诉它“嘿计算一下你自己的面积”或者“把自己画出来”。对象自己知道该怎么处理自己的数据。这种“对象自己负责自己”的思想就是面向对象的核心优势之一——封装。它带来了更好的代码组织性、安全性和可维护性。你不再需要把数据和函数分开管理它们天生就是一体。编译器也能进行更严格的类型检查因为一个Rectangle对象的方法天然就只能操作Rectangle对象的数据。所以学习C的类和对象绝不是多学几个新关键字那么简单。它是你从“面向过程”的线性思维转向“面向对象”的模块化、抽象化思维的关键一步。这一步迈稳了后面理解继承、多态这些更强大的特性才会水到渠成。2. 类的定义与对象的创建从蓝图到实物2.1 解剖一个最简单的类我们直接看代码这是理解类最直观的方式。下面定义了一个代表“盒子”的类class Box { public: // 访问修饰符先记住这里用public // 成员变量属性 double length; // 长度 double breadth; // 宽度 double height; // 高度 // 成员函数声明方法、行为 double getVolume(void); // 获取体积 void setDimensions(double len, double bre, double hei); // 设置尺寸 };我们来拆解一下这个“蓝图”class关键字这是定义类的标志就像struct定义结构体一样。类名Box遵循大驼峰命名法CamelCase是常见约定清晰易懂。public:这是一个访问修饰符。它决定了后面跟着的成员变量和函数在类的外部是否可以被访问。public意味着公开谁都可以用。除此之外还有private私有仅类内部可用和protected受保护涉及继承时用。初学者最容易忽略的一点就是忘记写访问修饰符导致后面编译出错。类定义里可以多次出现不同的访问修饰符用来区分不同部分的访问权限。成员变量length,breadth,height。这些是类的属性描述了“盒子”这个对象的状态。它们可以是任何C支持的数据类型。成员函数声明getVolume和setDimensions。这些是类的行为定义了“盒子”能做什么。注意这里只是在类内部进行了声明告诉编译器有这些函数但函数具体怎么实现定义可以写在类的外面。实操心得public放哪里很多教程例子喜欢把public:放在最前面把所有成员都设为公有。这在学习阶段没问题但在实际项目中是非常糟糕的做法。这完全破坏了封装性。良好的习惯是默认将所有成员变量设为private只将需要对外提供的接口函数设为public。这样外部代码无法直接修改对象内部数据必须通过你提供的公共函数你就能在函数内部加入数据校验、日志记录等逻辑保证对象状态的正确性和安全性。我们后续会详细讲封装。2.2 成员函数的定义分离的艺术通常我们把成员函数的声明放在类定义里头文件.h或.hpp而把定义实现放在单独的源文件.cpp里。这样做是为了分离接口和实现符合软件工程的思想。如何在类外定义成员函数呢需要用到作用域解析运算符::。// 成员函数 getVolume 的定义 double Box::getVolume(void) { return length * breadth * height; // 直接使用成员变量 } // 成员函数 setDimensions 的定义 void Box::setDimensions(double len, double bre, double hei) { // 通常这里会加入参数检查例如判断尺寸是否为正数 if (len 0 || bre 0 || hei 0) { // 在实际项目中这里应该抛出一个异常或进行错误处理 // 为了示例简单我们仅输出警告生产环境不要用cout处理错误 std::cout Warning: Dimensions should be positive! std::endl; return; } length len; // 将参数值赋给当前对象的成员变量 breadth bre; height hei; }关键点Box::这个双冒号告诉编译器getVolume和setDimensions函数是属于Box这个类的。没有这个前缀编译器会认为这是普通的全局函数。直接访问成员变量在成员函数内部你可以直接使用length,breadth,height这些名字它们指的就是调用这个函数的那个对象的成员变量。这里引出了一个核心概念this指针。实际上编译器在背后把函数调用box1.getVolume()翻译成了Box::getVolume(box1)并把box1的地址传了进去。在函数内部length等价于this-length。this是一个指向当前对象自身的常量指针。我们稍后会详细讲。2.3 创建对象让蓝图变成现实定义了类就像有了房子的设计图。创建对象才是真正盖房子。int main() { // 方式1在栈上创建对象最常见 Box box1; // 声明一个 Box 类型的对象 box1 Box box2; // 声明另一个对象 box2 // 方式2在堆上动态创建对象使用 new 关键字 Box* boxPtr new Box(); // boxPtr 是一个指向 Box 对象的指针 // ... 使用对象 ... // 对于动态创建的对象使用完毕后必须手动释放内存防止内存泄漏 delete boxPtr; boxPtr nullptr; // 好习惯释放后置空指针 return 0; }栈对象 vs 堆对象栈对象Box box1;生命周期由作用域决定。当main函数结束时box1和box2会自动被销毁内存自动回收。简单、安全是首选方式。堆对象new Box()生命周期由程序员控制。你必须用delete来释放内存否则会导致内存泄漏。通常用于需要动态管理生命周期、对象很大或需要跨作用域存在的情况。一个经典陷阱Box box3();是函数声明看看这段代码Box box3(); // 这行代码在干什么新手很容易以为这是在调用默认构造函数创建一个叫box3的对象。错了在C中这行代码被编译器解释为声明一个名为box3的函数该函数无参数返回一个Box类型的对象。这被称为“最令人烦恼的解析”Most Vexing Parse。要创建一个使用默认构造函数的对象正确写法就是Box box3;不加括号。如果构造函数需要参数则用Box box3(1.0, 2.0, 3.0);。2.4 访问对象的成员点运算符与箭头运算符创建了对象就要使用它。访问对象的成员包括变量和函数需要使用成员访问运算符。int main() { // 栈对象使用点运算符 . Box myBox; myBox.length 10.0; // 直接访问公有成员变量不推荐破坏封装 myBox.setDimensions(5.0, 3.0, 2.0); // 通过公有成员函数访问推荐 double vol myBox.getVolume(); std::cout Volume: vol std::endl; // 堆对象指针使用箭头运算符 - Box* boxPtr new Box(); boxPtr-setDimensions(4.0, 4.0, 4.0); // 等价于 (*boxPtr).setDimensions(...) vol boxPtr-getVolume(); std::cout Volume from pointer: vol std::endl; delete boxPtr; return 0; }.与-的选择如果变量是对象本身如myBox用点运算符.。如果变量是指向对象的指针如boxPtr用箭头运算符-。boxPtr-member等价于(*boxPtr).member即先对指针解引用得到对象再用点运算符。3. 访问控制与封装给类的成员上把锁前面我们一直用public现在来深入理解访问控制。这是面向对象设计封装性的基石。C提供了三个访问修饰符public公有在类的内部和外部都可以被任意访问。通常用于定义类的对外接口。private私有只能在类的内部被访问即类的成员函数内。类的外部代码无法直接访问私有成员。这是封装的关键用于隐藏对象的内部实现细节。类的成员变量默认就是private如果没写任何访问修饰符。protected受保护与private类似但有一个重要区别在涉及“继承”时派生类子类可以访问基类父类的protected成员但不能访问private成员。这个我们留到讲继承时再细说。让我们改造一下Box类实践一下良好的封装// box.h (头文件声明接口) class Box { private: // 将成员变量设为私有隐藏实现细节 double length; double breadth; double height; public: // 对外提供公共接口 // 构造函数用于初始化对象 Box(double len 1.0, double bre 1.0, double hei 1.0); // 公共接口函数 double getVolume() const; // const 成员函数承诺不修改对象状态 bool setDimensions(double len, double bre, double hei); // 设置尺寸返回是否成功 double getLength() const; // 获取长度的getter函数 // ... 可以添加其他getter如 getBreadth, getHeight // 注意我们没有提供 setLength 等直接设置单个变量的公有函数 // 因为改变单一维度可能导致体积计算错误除非业务逻辑允许。 // 我们只提供 setDimensions 来同时设置三个维度保证数据一致性。 }; // box.cpp (源文件实现细节) #include “box.h” #include iostream // 构造函数定义 Box::Box(double len, double bre, double hei) { // 构造函数内部可以直接访问私有成员 // 这里可以调用一个私有的初始化函数或者直接赋值 // 为了健壮性最好也做参数检查 if (len 0 || bre 0 || hei 0) { std::cerr “Error: Dimensions must be positive. Using default value 1.0.” std::endl; length breadth height 1.0; } else { length len; breadth bre; height hei; } } double Box::getVolume() const { return length * breadth * height; } bool Box::setDimensions(double len, double bre, double hei) { if (len 0 || bre 0 || hei 0) { return false; // 设置失败 } length len; breadth bre; height hei; return true; // 设置成功 } double Box::getLength() const { return length; }这样设计的好处数据保护外部代码无法直接myBox.length -5.0;因为length是private的。这防止了非法数据直接污染对象状态。接口统一所有对内部数据的修改都必须通过setDimensions这个公共函数。我们可以在这个函数里加入所有必要的校验逻辑比如检查是否为负数保证了数据的有效性。灵活性未来如果我们需要修改Box内部的数据存储方式比如改用数组存储三维数据只需要修改getVolume、setDimensions等少数几个成员函数的实现而外部调用这些函数的代码完全不需要改动。这就是封装带来的“接口与实现分离”的优势。const成员函数注意getVolume()和getLength()后面的const关键字。它表示这个函数不会修改调用它的对象的状态即不会修改任何成员变量。这有两个好处一是让代码的意图更清晰二是允许const对象调用这些函数const Box myBox; myBox.getVolume();是合法的而调用非const函数则不行。注意事项Getter/Setter 不是万能的不要盲目地为每一个私有成员变量都创建一对getXxx和setXxx函数。这可能会退化成一种“伪封装”。设计类的公共接口时应该思考的是“这个对象能提供什么服务”而不是“如何暴露它的所有数据”。如果某些数据只是内部状态外部根本不需要知道那就不要提供访问它的接口。4. 构造函数与析构函数对象的生与死对象从创建到销毁有两个特殊的成员函数会自动调用构造函数和析构函数。理解它们对于管理资源尤其是动态内存至关重要。4.1 构造函数对象的“出生证明”构造函数在对象创建时自动调用用于初始化对象的状态。它的名字与类名完全相同没有返回类型连void都没有。class Student { private: std::string name; int id; double score; public: // 1. 默认构造函数 (无参) Student() { name “Unknown”; id 0; score 0.0; std::cout “Default constructor called.” std::endl; } // 2. 带参数的构造函数 Student(std::string n, int i, double s) { name n; id i; score s; std::cout “Parameterized constructor called for ” name std::endl; } // 3. 使用初始化列表的构造函数更高效推荐 Student(std::string n, int i) : name(n), id(i), score(0.0) { // score被初始化为0.0 // 函数体可以为空初始化工作在冒号后面完成 std::cout “Constructor with initializer list called.” std::endl; } void display() { std::cout “Name: ” name “, ID: ” id “, Score: ” score std::endl; } }; int main() { Student stu1; // 调用默认构造函数 Student stu2(“Alice”, 1001, 95.5); // 调用带参构造函数 Student stu3(“Bob”, 1002); // 调用使用初始化列表的构造函数 stu1.display(); stu2.display(); stu3.display(); // 动态创建对象也会调用构造函数 Student* pStu new Student(“Charlie”, 1003, 88.0); delete pStu; return 0; }构造函数的几个关键点初始化列表在构造函数参数列表后以冒号开头是初始化常量成员、引用成员和类类型成员尤其当它们没有默认构造函数时的唯一途径。对于普通内置类型在初始化列表初始化也比在构造函数体内赋值效率稍高因为它直接初始化而赋值是先默认初始化再赋值。默认构造函数如果类没有定义任何构造函数编译器会自动生成一个空的默认构造函数。但如果你定义了任何一个构造函数比如带参数的编译器就不再提供默认构造函数。此时像Student stu1;这样的语句就会编译错误除非你显式地再写一个无参构造函数。explicit关键字用于修饰单参数的构造函数防止编译器进行隐式类型转换。例如如果你有一个Student(int id)的构造函数那么Student stu 1004;这句代码会隐式地将1004转换成Student对象。加上explicit后这种写法就会报错必须显式调用构造函数Student stu(1004);。这能避免很多意想不到的错误建议为单参数构造函数加上explicit。4.2 析构函数对象的“临终遗言”析构函数在对象销毁时自动调用用于清理对象占用的资源如释放动态内存、关闭文件等。它的名字是类名前加一个波浪号~没有参数也没有返回类型。class Buffer { private: char* data; size_t size; public: // 构造函数动态分配内存 Buffer(size_t sz) : size(sz) { data new char[size]; // 在堆上分配内存 std::cout “Buffer of size ” size “ allocated.” std::endl; } // 析构函数释放动态分配的内存 ~Buffer() { delete[] data; // 释放内存防止内存泄漏 data nullptr; // 好习惯防止悬空指针 std::cout “Buffer of size ” size “ deallocated.” std::endl; } // ... 其他成员函数 }; int main() { { Buffer buf(1024); // 进入作用域构造函数被调用 // 使用 buf... } // 离开作用域buf 被销毁析构函数自动调用 Buffer* pBuf new Buffer(2048); // 使用 pBuf... delete pBuf; // 手动删除析构函数被调用 // 如果不 delete内存泄漏 return 0; }析构函数的核心作用资源管理。这是C程序员必须牢记的“金科玉律”谁申请谁释放。在构造函数里new了内存在析构函数里就必须delete它。这个模式被称为RAIIResource Acquisition Is Initialization是C管理资源的基石。标准库中的std::vector,std::string等容器其内部都利用了RAII所以我们可以放心使用不用担心内存泄漏。实操心得三/五法则如果一个类需要自定义析构函数那么它很可能也需要自定义拷贝构造函数和拷贝赋值运算符合称“拷贝控制成员”。这是因为编译器自动生成的拷贝操作是“浅拷贝”按位复制如果类管理着动态内存浅拷贝会导致两个对象指向同一块内存析构时会被释放两次造成严重错误。这就是著名的“三法则”C11前或“五法则”C11后增加了移动构造函数和移动赋值运算符。对于Buffer类我们必须实现深拷贝的拷贝构造函数和拷贝赋值运算符或者将它们声明为 delete来禁止拷贝。这个话题较深属于进阶内容但初学者必须知道这个潜在陷阱。5.this指针对象自我的引用在类的非静态成员函数内部存在一个名为this的隐藏指针它指向调用该成员函数的那个对象。class MyClass { private: int value; public: void setValue(int value) { // 参数名和成员变量名相同 this-value value; // 使用 this 指针区分成员变量和局部参数 // 等价于 (*this).value value; } MyClass* getAddress() { return this; // 返回当前对象的地址 } void print() const { // 在 const 成员函数中this 是一个指向 const 对象的指针 (const MyClass*) std::cout “My value is: ” this-value std::endl; } }; int main() { MyClass obj1, obj2; obj1.setValue(10); obj2.setValue(20); std::cout “obj1 address: ” obj1.getAddress() std::endl; std::cout “obj2 address: ” obj2.getAddress() std::endl; obj1.print(); obj2.print(); return 0; }this指针的用途解决命名冲突当成员函数参数名与成员变量名相同时用this-来明确指代成员变量。返回对象自身用于实现链式调用。例如某些类的setter函数返回*this对象本身的引用这样就可以写成obj.setX(1).setY(2).setZ(3);。在成员函数中传递当前对象比如将当前对象作为参数传递给其他函数。this的本质它是一个常量指针对于MyClass类型的对象在非const成员函数中this的类型是MyClass* const指针本身是常量不能指向别的对象在const成员函数中类型是const MyClass* const指向常量的常量指针。6. 静态成员属于类而非对象有时候我们需要一些数据或函数与类本身相关联而不是与类的每个对象相关联。比如统计一个类总共创建了多少个对象。这时就需要静态成员。静态成员使用static关键字声明。它们具有以下特点属于类静态成员在类的所有对象中只有一份副本被所有对象共享。生命周期静态成员的生命周期贯穿整个程序运行期存储在静态存储区。访问方式既可以通过对象访问obj.staticMember也可以通过类名直接访问ClassName::staticMember后者更能体现其属于类的本质。class Counter { private: static int count; // 静态成员变量声明在类内 int id; // 普通成员变量每个对象独有 public: Counter() { id count; // 每创建一个对象count加1并赋予id std::cout “Object ” id “ created. Total count: ” count std::endl; } ~Counter() { --count; // 对象销毁时count减1 std::cout “Object ” id “ destroyed. Total count: ” count std::endl; } // 静态成员函数只能访问静态成员变量 static int getTotalCount() { return count; } int getID() const { return id; } }; // 静态成员变量必须在类外定义并初始化分配内存 int Counter::count 0; // 定义并初始化为0 int main() { std::cout “Initial count: ” Counter::getTotalCount() std::endl; // 通过类名访问 Counter c1; Counter c2; { Counter c3; std::cout “Count inside block: ” c3.getTotalCount() std::endl; // 通过对象访问 } // c3 离开作用域被销毁 std::cout “Final count: ” Counter::getTotalCount() std::endl; return 0; }关键点静态成员变量必须在类外部通常是源文件.cpp进行一次且仅一次的定义和初始化如int Counter::count 0;。这条语句为静态变量分配了内存。如果忘记定义链接时会报错“未定义的引用”。静态成员函数没有this指针因为它不与任何特定对象绑定。因此它不能直接访问类的非静态成员变量或函数只能访问静态成员。调用时不需要通过对象可以通过类名直接调用非常方便。用途常用于实现单例模式、管理类级别的资源、定义常量、作为工具函数等。7. 常见问题与避坑指南在实际编码中围绕类和对象会遇到很多看似简单却容易出错的地方。这里记录几个我踩过的坑和对应的排查思路。7.1 对象赋值与拷贝浅拷贝的陷阱这是新手甚至是有经验的程序员都容易犯错的地方。看下面的例子class ShallowCopyDemo { public: int* data; ShallowCopyDemo(int value) { data new int(value); } ~ShallowCopyDemo() { delete data; } // 注意我们没有自定义拷贝构造函数和拷贝赋值运算符 // 编译器会为我们生成默认的浅拷贝版本 }; int main() { ShallowCopyDemo obj1(42); ShallowCopyDemo obj2 obj1; // 拷贝初始化调用编译器生成的拷贝构造函数浅拷贝 std::cout “obj1.data: ” *obj1.data std::endl; // 42 std::cout “obj2.data: ” *obj2.data std::endl; // 42 *obj2.data 100; // 通过 obj2 修改 data 指向的值 std::cout “After modification via obj2:” std::endl; std::cout “obj1.data: ” *obj1.data std::endl; // 变成了 100问题出现 std::cout “obj2.data: ” *obj2.data std::endl; // 100 // 程序结束时obj1 和 obj2 的析构函数都会被调用 // obj1 先析构delete data; 内存被释放。 // obj2 再析构再次 delete data; 此时 data 指向的内存已经被释放导致“重复释放”错误程序崩溃。 return 0; }问题分析编译器生成的默认拷贝构造函数和拷贝赋值运算符执行的是浅拷贝或称“位拷贝”。它只是简单地将obj1的每个成员变量的值复制给obj2。对于指针成员data复制的是指针的值内存地址而不是指针指向的内存内容。于是obj1.data和obj2.data指向了同一块堆内存。数据共享通过其中一个对象修改数据另一个对象看到的数据也变了这通常不是我们想要的。重复释放两个对象析构时都会对同一块内存调用delete导致未定义行为通常是程序崩溃。解决方案实现深拷贝。当类管理着动态分配的资源如堆内存、文件句柄等时必须自定义拷贝构造函数和拷贝赋值运算符进行深拷贝。class DeepCopyDemo { private: int* data; public: DeepCopyDemo(int value) { data new int(value); } // 1. 自定义拷贝构造函数深拷贝 DeepCopyDemo(const DeepCopyDemo other) { data new int(*(other.data)); // 分配新内存并复制值 std::cout “Custom copy constructor called.” std::endl; } // 2. 自定义拷贝赋值运算符深拷贝 DeepCopyDemo operator(const DeepCopyDemo other) { if (this ! other) { // 重要防止自赋值 (a a) delete data; // 释放旧资源 data new int(*(other.data)); // 分配新内存并复制值 } std::cout “Custom copy assignment operator called.” std::endl; return *this; // 返回当前对象的引用支持链式赋值 (a b c) } ~DeepCopyDemo() { delete data; } void setValue(int value) { *data value; } int getValue() const { return *data; } };避坑指南记住“三/五法则”三法则C98/03如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个那么它很可能需要全部这三个。五法则C11及以后增加了移动构造函数和移动赋值运算符。管理资源的类通常需要处理这五个特殊成员函数。最简单的方法如果你不希望对象被拷贝比如std::unique_ptr可以将拷贝构造函数和拷贝赋值运算符声明为 delete。class NonCopyable { public: NonCopyable() default; NonCopyable(const NonCopyable) delete; // 禁止拷贝构造 NonCopyable operator(const NonCopyable) delete; // 禁止拷贝赋值 };7.2const的正确性const用得好能极大提高代码的健壮性。class ConstDemo { private: int mutableValue; mutable int mutableCounter; // 被 mutable 修饰即使在 const 函数中也可修改 public: ConstDemo(int val) : mutableValue(val), mutableCounter(0) {} // 非 const 成员函数可以修改成员变量 void modifyValue(int val) { mutableValue val; } // const 成员函数承诺不修改对象状态除了 mutable 成员 int getValue() const { // mutableValue 10; // 错误不能在 const 函数中修改非 mutable 成员 mutableCounter; // 正确可以修改 mutable 成员 return mutableValue; } // 重载根据对象是否是 const调用不同的版本 void print() { std::cout “Non-const print called.” std::endl; } void print() const { std::cout “Const print called.” std::endl; } }; int main() { ConstDemo obj(5); const ConstDemo constObj(10); obj.getValue(); // 可以非 const 对象可以调用 const 函数 obj.print(); // 调用非 const 版本的 print constObj.getValue(); // 可以const 对象只能调用 const 函数 constObj.print(); // 调用 const 版本的 print // constObj.modifyValue(20); // 错误const 对象不能调用非 const 函数 return 0; }关键点将不修改对象状态的成员函数声明为const这是一个好习惯。const对象只能调用const成员函数。可以使用mutable关键字修饰那些在逻辑上不改变对象“状态”但需要记录一些内部信息的成员变量如缓存、访问计数等使其在const函数中也可被修改。可以根据const重载成员函数为const和非const对象提供不同的行为。7.3 头文件中的类定义类的声明通常放在头文件.h或.hpp中。需要注意避免重复包含和循环依赖。box.h#ifndef BOX_H // 头文件守卫防止重复包含 #define BOX_H #include string // 包含必要的头文件 class Box { private: double length; double breadth; double height; public: Box(double len 1.0, double bre 1.0, double hei 1.0); double getVolume() const; bool setDimensions(double len, double bre, double hei); // 内联函数可以直接在头文件中定义 double getLength() const { return length; } double getBreadth() const { return breadth; } double getHeight() const { return height; } }; #endif // BOX_Hbox.cpp#include “box.h” #include iostream // 成员函数的定义放在.cpp文件中 Box::Box(double len, double bre, double hei) : length(len), breadth(bre), height(hei) { // 使用初始化列表更高效 if (len 0 || bre 0 || hei 0) { std::cerr “Invalid dimensions. Using 1.0.” std::endl; length breadth height 1.0; } } double Box::getVolume() const { return length * breadth * height; } bool Box::setDimensions(double len, double bre, double hei) { if (len 0 || bre 0 || hei 0) return false; length len; breadth bre; height hei; return true; }注意事项头文件守卫必须使用#ifndef、#define、#endif或#pragma once非标准但被广泛支持来防止同一个头文件被多次包含进同一个源文件导致重复定义错误。分离编译将类的声明接口放在.h文件定义实现放在.cpp文件。这样修改实现时只需要重新编译该.cpp文件而不需要重新编译所有包含该头文件的源文件提高了编译效率。内联函数短小、频繁调用的成员函数如简单的getter可以在类定义内部直接实现它们会隐式地成为内联函数inline。对于复杂的函数建议在.cpp文件中定义。