楼层: 首页/ 软件技术/ 现代 C++/ 面向对象 OOP:把数据和操作打包
3

面向对象 OOP:把数据和操作打包

Class · Inheritance · Polymorphism

C 的 struct 只能装数据。C++ 的 class 既能装数据、又能装操作这些数据的函数——把"是什么"和"能干嘛"绑成一个整体。封装、继承、多态是三大件,这一章讲清楚。

class 与 struct,构造与析构

一个最小的类

#include <string> #include <iostream> class Dog { private: // 私有:外面碰不到(封装) std::string name; public: // 公开:对外的接口 Dog(std::string n) : name(n) { // 构造函数:出生时调用 std::cout << name << " 出生了\n"; } ~Dog() { // 析构函数:销毁时自动调用,清理资源 std::cout << name << " 走了\n"; } void bark() { std::cout << name << ": 汪汪\n"; } }; int main() { Dog d("旺财"); d.bark(); } // main 结束,d 销毁,析构函数自动跑——这就是 RAII 的雏形

运行结果

旺财 出生了 旺财: 汪汪 旺财 走了

辨class 和 struct 的唯一区别

没有本质区别,唯一差别是默认访问权限:class 默认 private,struct 默认 public。习惯上:纯数据打包用 struct,带行为和封装用 class。

拷贝 vs 移动:深拷贝与偷资源

这是 C++ 最容易翻车的地方。当你的类管着一块堆内存(比如一个动态数组),默认的"浅拷贝"只会复制指针值,结果两个对象指向同一块内存,析构时 free 两次——double free,直接崩。C++11 的"移动"则是"把资源偷过来",不复制。

特殊函数什么时候调
拷贝构造用一个旧对象造一个新对象,深拷贝(复制一份资源)。
拷贝赋值已有对象 = 另一个对象。
移动构造(C++11)用一个即将销毁的临时对象造新对象:把它的资源偷过来,不复制。
移动赋值(C++11)同上,对象已存在时。
std::move(x)告诉编译器:"把 x 当临时对象,可以偷它的资源。"

继承与多态

多态的意思是:同一个调用,不同的对象有不同的表现。靠虚函数 virtual 实现。C++11 加了 override(显式声明我在重写父类函数)和 final(不许再被重写),让多态更安全。

虚函数 + override:动物叫出不同声

class Animal { public: virtual void speak() const { std::cout << "...\n"; } virtual ~Animal() = default; // 有虚函数就该有虚析构,否则派生类析构不调用 }; class Cat : public Animal { public: void speak() const override { std::cout << "喵\n"; } // override 显式重写 }; // 用父类指针指向子类对象,调用的是子类的 speak —— 这就是多态 Animal* a = new Cat(); a->speak(); // 输出 "喵",而不是 "..." delete a;

提纯虚函数、抽象类、菱形问题

纯虚函数:virtual void draw() = 0;——只定接口不给实现,含它的类就是抽象类,不能实例化,只能被继承。相当于"规定派生类必须实现这个方法"。

菱形继承:A 是爷爷,B、C 都继承 A,D 又同时继承 B 和 C——D 里就有两份 A,容易乱。解决办法是虚继承(virtual public)让共同祖先只留一份。实际项目里设计得当尽量别踩菱形。

静态成员与友元

静态成员是"所有对象共享的一份",不归属某个具体对象,相当于类级别的全局变量。友元则是"破格让某个函数/类访问我的私有成员",用多了破坏封装,慎用。

静态成员:统计造了几个对象

class Widget { static int count; // 静态成员声明:所有 Widget 共享一份 public: Widget() { count++; } // 每造一个,总数 +1 static int howMany() { return count; } // 静态成员函数,不用对象就能调 }; int Widget::count = 0; // 静态成员要在类外定义初始化一次 // Widget::howMany() 直接用类名调,不需要先 new 对象

继承中的构造析构顺序

派生类对象出生时,先造基类、再造派生类;销毁时反过来,先析构派生类、再析构基类。记住这个"洋葱"顺序,继承复杂对象时才不会乱。

阶段顺序
构造基类构造 → 成员对象构造 → 派生类自己的构造体。
析构反过来:派生类析构 → 成员析构 → 基类析构。
基类析构必须是 virtual

如果用 基类指针 delete 一个派生类对象,而基类析构不是 virtual,派生类的析构根本不会被调用——派生类的资源(比如它 new 出来的东西)泄漏。只要一个类可能被当基类用,就把它的析构函数声明为 virtual。这是铁律。

完美转发 std::forward 与拷贝省略(RVO/NRVO)

写泛型包装函数时,你想把收到的参数"原封不动"转发给下一个函数:值就是值、左值就是左值、右值就是右值。但普通传参会把它们都"降级"成左值。std::forward<T> 就是干这个的——配合"转发引用"T&&(注意是模板里的,不是右值引用)。

std::forward:泛型工厂函数里"原样转交"参数

template<class T, class... Args> shared_ptr<T> make(Args&&... args) { // 不用 forward 的话,args 即使是右值,到这里也变左值,失去移动机会 // forward<Args>(args)... 把每个参数"保持原来的值类别"再转交 return std::make_shared<T>(std::forward<Args>(args)...); }

和 std::move 的区别一句话:std::move 无条件把东西转成右值(告诉编译器"可以偷");std::forward 是有条件——参数本来是右值才转成右值,本来是左值就保持左值。写泛型转发用 forward,别处想显式移动用 move。

拷贝省略(copy elision / RVO / NRVO):很多时候你"看着像拷贝",其实编译器偷偷根本不拷贝:

std::string make() { std::string s = "hi"; return s; // NRVO:直接在调用者的栈上构造 s,连移动都省了 } // C++17 起,"按值返回临时对象"的拷贝省略是强制的: std::string s2 = std::string("hello"); // 直接构造,不拷贝临时对象

结论:别为了"怕拷贝"而到处传引用、写智能指针——现代编译器的 RVO/NRVO 会帮你优化掉。先按"值语义、简洁"写,遇到性能问题再局部改成移动/引用。