完全指南——从结构体进阶到 OOP 实战)
OI-wiki 语言篇C 类class完全指南——从结构体进阶到 OOP 实战【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki在 OI-wiki 的 C 语言体系中类class是结构体的自然进阶它不仅聚合成员元素还允许定义成员函数是面向对象编程OOP中对象这一核心概念的直接载体。本文以 docs/lang/class.md 为骨架结合 OI-wiki 仓库中大量真实竞赛代码如凸包、莫队、有限域实现系统讲解类的定义、访问控制、成员函数、运算符重载、构造函数、析构函数与赋值语义帮助读者在算法竞赛中熟练运用类来组织数据、简化代码并为阅读 OI-wiki 全站代码打下基础。读完本文你将能够独立定义自己的类、正确重载运算符接入std::sort等 STL 设施并规避浅拷贝、隐式转换等经典坑点。从结构体到类OOP 的第一级台阶在 OI-wiki 的教程体系中结构体struct 被描述为一系列称为成员元素的组合体可视为自定义数据类型而类则更进一步不仅拥有成员元素还拥有成员函数。二者关系可以用一句话概括类是结构体的拓展。在面向对象编程OOP中对象就是类的实例也就是变量。类相当于一份蓝图而对象是根据蓝图生产出的具体实体——这与结构体变量是结构体类型的实例完全一致。值得注意的是 C 语言中的一个重要细节struct关键字定义的其实也是类。上文所说的结构体定义来自 C 语言C 出于历史原因保留并拓展了struct使其行为与class高度一致。这一历史渊源也解释了为什么 docs/lang/struct.md 中会特别注明本页描述的struct不同于 C 中struct在 C 中struct被扩展为类似class的类说明符。从实际应用看OI-wiki 的竞赛代码中struct与class都被大量用于组织复合数据。例如 docs/misc/code/mo-algo/mo-algo_1.cpp 中莫队算法的查询结构体struct query { int l, r, id; bool operator(const query x) const { // 重载运算符 if (l / maxn ! x.l / maxn) return l x.l; return (l / maxn) 1 ? r x.r : r x.r; } } a[N];这里struct拥有成员变量和成员函数operator正是 C 中struct即类的生动体现。稍后我们会看到同样的写法在class中完全成立。定义类语法与基本示例类使用关键字class或struct定义。语法模板与实例class ClassName { ... }; // Example: class Object { public: int weight; int value; } e[array_length]; const Object a; Object b, B[array_length]; Object *c;上例定义了一个名为Object的类拥有两个成员元素weight和value并在}之后用该类型定义了一个数组e。可以看到类的使用方式与结构体大同小异——一旦类型定义完成就可以像内置类型一样用它定义常量const Object a;、变量Object b;、数组Object B[array_length];和指针Object *c;。定义类的指针与定义结构体指针的方式完全相同使用类名*即可例如struct Edge { Edge* nxt; };该类指针通常用于构建链表、邻接表等竞赛中常见的数据结构。访问说明符public / protected / private与前文结构体示例不同类的定义中出现了public这属于访问说明符access specifier用于控制成员的可访问范围。C 提供三种访问说明符访问说明符类内访问派生类访问类外访问友元访问public✅✅✅✅protected✅✅❌✅private✅❌❌✅其具体语义为public该说明符之后的各个成员都可以被公开访问无论类内还是类外均可访问protected之后的成员可以被类内、派生类或友元的成员访问但类外不能访问private之后的成员只能被类内成员或友元访问不能被类外或派生类访问。关键区别在于默认访问级别对于struct所有成员默认public对于class所有成员默认private。这正是两种关键字除语法外最重要的行为差异也是宁可显式书写public:这一竞赛编码习惯的根源。关于友元与派生类的基本概念理解protected与private的差异需要引入两个基础概念友元friend使用friend关键字修饰某个函数或类可以使得被修饰者在不成为成员函数或成员类的情况下访问该类的私有private或受保护protected成员。简单来说只要带有这个类的friend标记就可以访问其私有或受保护成员元素。OI-wiki 代码中也有实例例如 docs/geometry/code/convex-hull/convex-hull_1.cpp 用friend声明了Point的运算符重载函数详见下文重载运算符。派生类derived classC 允许使用一个类作为基类并通过基类派生出派生类派生类根据特定规则继承基类中的成员变量和成员函数从而提高代码复用率。派生类与基类呈现 is 关系——如猫派生类is 哺乳动物基类。在继承语境下private与protected的区别可以直观理解为派生类可以访问基类的protected元素public同理但不能访问private元素。访问与修改成员元素的值访问与修改成员元素的方法与结构体完全一致对于变量使用.符号var.weight 5;对于指针使用-符号等价于(*ptr).weightptr-weight 5;从源码看OI-wiki 全站代码大量依赖这一访问方式例如凸包实现中p[i].x、u.back().y等见 docs/geometry/code/convex-hull/convex-hull_1.cpp。成员函数让数据活起来成员函数member function顾名思义就是类中所包含的函数。它是类区别于纯数据聚合体的根本特征。OI-wiki 中常见的成员函数包括vector.push_back(); set.insert(); queue.empty();成员函数的定义与普通函数几乎无异既可以在类内直接定义也可以在类内声明、类外定义。类外定义时需要以类名::限定函数所属class Class_Name { ... type Function_Name(...) { ... } }; // Example: class Object { public: int weight; int value; void print() { cout weight endl; return; } void change_w(int); }; void Object::change_w(int _weight) { weight _weight; } Object var;上例中的Object类有一个打印成员元素的print函数以及一个更改成员元素weight的change_w函数。注意change_w采用类内声明void change_w(int); 类外定义void Object::change_w(int _weight) { ... }的写法——和普通函数类似成员函数也可以先声明、后定义。若想调用var的print成员函数使用var.print()即可。从实战角度看将数据与操作封装进同一个类能显著提升代码的组织性与复用性。例如 docs/math/code/finite-field/finite-field_1.cpp 中实现有限域的FiniteField类其成员变量p, k, mod与成员函数pow、inv、poly_gcd等紧密关联形成了一个自洽的数学抽象。重载运算符为自定义类型赋予原生语义何为重载C 允许编写者为名称相同的函数或运算符指定不同的定义这称为重载overload。其规则要点如下如果同名函数的参数种类、数量中的一者或多者两两不相同则这些同名函数被看作是不同的如果两个同名函数的区别仅仅是返回值类型不同则无法重载此时编译器会拒绝编译如果在调用时不会出现混淆指无法根据所填参数种类和数量唯一判断被调用函数常发生在具有默认参数的函数中编译器会根据调用时填写的参数判断应调用哪个函数这一过程被称为重载解析overload resolution。重载运算符可以部分程度上代替函数简化代码。OI-wiki 给出经典示例——定义一个向量类并重载*、、-分别代表向量内积、向量加、向量减class Vector { public: int x, y; Vector() : x(0), y(0) {} Vector(int _x, int _y) : x(_x), y(_y) {} int operator*(const Vector other) const { return x * other.x y * other.y; } Vector operator(const Vector) const; Vector operator-(const Vector) const; }; Vector Vector::operator(const Vector other) const { return Vector(x other.x, y other.y); } Vector Vector::operator-(const Vector other) const { return Vector(x - other.x, y - other.y); } // 关于第4、5行表示为x、y赋值具体实现参见后文构造函数。该例中重载运算符的模板大致可分为两部分/*类定义内重载*/ 返回类型 operator符号(参数){...} /*类定义内声明在外部定义*/ 返回类型 类名称::operator符号(参数){...}其中operator*在类内直接定义并返回内积标量int而operator、operator-采用类内声明 类外定义的形式返回新的Vector。运算符重载是竞赛代码中让自定义类型接入 STL 的关键。对于自定义的类如果重载了某些运算符一般来说只需要重载这个比较运算符便可以使用相应的 STL 容器或算法例如sort。OI-wiki 中莫队算法的查询结构体正是这样做的见 docs/misc/code/mo-algo/mo-algo_1.cpp通过重载operator实现分块排序随后直接sort(a, a m)排序查询区间。std::sort的第三参量自定义比较函数/比较器也可以达到类似效果详见 docs/basic/stl-sort.md。另一个值得借鉴的实战写法来自 OI-wiki 的凸包代码 docs/geometry/code/convex-hull/convex-hull_1.cpp使用friend声明友元函数来重载、-、*点乘、^叉乘乃至输入并将它们定义为template class T struct Point的成员实现了模板化、类型安全的二维几何运算template class T struct Point { T x, y; Point(T x 0, T y 0) : x(x), y(y) {} friend Point operator(const Point a, const Point b) { return {a.x b.x, a.y b.y}; } friend Point operator-(const Point a, const Point b) { return {a.x - b.x, a.y - b.y}; } // 点乘 friend T operator*(const Point a, const Point b) { return a.x * b.x a.y * b.y; } // 叉乘 friend T operator^(const Point a, const Point b) { return a.x * b.y - a.y * b.x; } friend istream operator(istream is, Point p) { return is p.x p.y; } };可以看到重载后的Point可以直接书写a b、a - b、a * b、a ^ b等数学表达式让几何算法的代码与数学记号高度一致可读性大幅提升。C 中可以被重载的运算符全集如下注意部分运算符如::、.、.*、?:、sizeof不可重载下表未列出 - * / % ^ | ~ ! - * / % ^ | ! || -- , -* - () [] new new [] delete delete []构造函数在实例化时设定初始值默认构造函数Default Constructor为在实例化变量时设定初始值需要定义默认构造函数class ClassName { ... ClassName(...)... { ... } }; // Example: class Object { public: int weight; int value; Object() { weight 0; value 0; } };该例定义了Object的默认构造函数在实例化Object类型变量时将全部成员元素初始化为0。关于隐式默认构造函数的规则需要特别注意若没有显式定义任何构造函数编译器会自动生成一个隐式默认构造函数并根据成员元素的类型进行初始化与定义内置类型变量的行为相同在这种情况下内置类型的成员元素是未初始化的访问未初始化变量的结果是未定义的并不知道会返回何值——这是竞赛中常见的隐性 Bug 来源之一如果需要自定义初始化的值可以再定义或重载构造函数。定义或重载构造函数的补充说明一般来说默认构造函数是不带参数的这区别于带参的构造函数构造函数与默认构造函数的定义大同小异只是参数数量上的不同构造函数可以被重载首次定义当然也算定义关键警告如果已经定义了任意构造函数编译器便不会再生成无参数的默认构造函数。这可能导致试图以默认方式不填入初始化参数构造变量的行为编译失败使用 C11 或以上标准时可以使用{}进行变量的初始化。关于{}列表初始化使用{}进行初始化时会用到std::initializer_list这一轻量代理对象。其初始化步骤大致如下简化版详细内容可参见 cppreference 的 value initialization 文档尝试寻找参数中有std::initializer_list的构造函数如果有则调用调用完后不再进行下面的查找尝试将{}中的元素填入其他构造参数如果能将参数按照顺序填满默认参数也算在内则调用该构造函数若无private成员元素则尝试在类外按照元素定义顺序或下标顺序依次赋值。下面是一个结合默认构造函数、带参构造函数与{}初始化的完整示例class Object { public: int weight; int value; Object() { weight 0; value 0; } Object(int _weight 0, int _value 0) { weight _weight; value _value; } // the same as // Object(int _weight,int _value):weight(_weight),value(_value) {} }; // the same as // Object::Object(int _weight,int _value){ // weight _weight; // value _value; // } Object A; // ok Object B(1, 2); // ok Object C{1, 2}; // ok,(C11)注意示例中的两种等价写法一是类内使用初始化列表: weight(_weight), value(_value) {}初始化成员二是将构造函数放到类外定义写为Object::Object(int _weight, int _value) { ... }。初始化列表在 C 中通常比函数体内赋值更高效直接初始化而非默认构造 赋值竞赛代码中也常见这种风格例如 docs/geometry/code/convex-hull/convex-hull_1.cpp 的Point(T x 0, T y 0) : x(x), y(y) {}。关于隐式类型转换与 explicit有时会写出如下代码class Node { public: int var; Node(int _var) : var(_var) {} }; Node a 1;这看上去不符合逻辑——一个int类型怎么能转化为Node类型但编译器不会给出error提示。原因在于赋值时首先将1作为参数调用Node::Node(int)然后调用默认的复制拷贝函数完成赋值。但大多数情况下编写者希望编译器对此报错。这时可以在构造函数前追加explicit关键字告诉编译器必须显式调用构造函数class Node { public: int var; explicit Node(int _var) : var(_var) {} };也就是说加上explicit后Node a 1;将会报错而Node a Node(1);不会——后者显式调用了构造函数。不过在算法竞赛中为避免此类情况常用的做法是加强对代码的规范程度从源头上避免写出依赖隐式转换的代码。析构函数管理动态内存的生命周期变量销毁是编程中不可避免的问题——每一个变量都将在其作用范围结束时走向销毁。但对于已经指向动态申请内存的指针来说指针变量销毁时不会自动释放所指向的内存需要手动释放动态内存。如果类的成员元素包含指针同样会遇到这个问题此时需要用到析构函数来手动释放动态内存。析构函数Destructor将在该变量被销毁时被自动调用。其重载方法形同构造函数但需要在类名前加~class Object { public: int weight; int value; int* ned; Object() { weight 0; value 0; } ~Object() { delete ned; } };上例中Object拥有指针成员ned在构造函数之外重载了析构函数~Object()在对象销毁时执行delete ned;释放动态内存。竞赛实践中的经验法则是默认定义的析构函数通常对算法竞赛已经足够通常只有在成员元素包含指针时才需要重载析构函数。为类变量赋值复制语义与浅拷贝陷阱默认情况下对类变量赋值时会按照对应成员元素的赋值规则逐个进行。也可以使用类名称()或类名称{}作为临时变量来进行赋值// A,tmp1,tmp2,tmp3类型为Object tmp1 A; tmp2 Object(...); tmp3 {...};两种临时变量写法的细微差别在于前者Object(...)只是调用了复制构造函数copy constructor后者{...}在调用复制构造函数前会先调用默认构造函数。另一个必须牢记的重要语义是浅拷贝shallow copy默认情况下进行的赋值都是对应元素间的浅拷贝。如果成员元素中有指针则赋值完成后两个变量的成员指针具有相同的地址——这意味着修改或释放其中一个会影响另一个甚至引发重复释放double free等严重问题。如需解决指针问题或进行更多自定义操作需要重载相应的构造函数复制构造函数、移动构造函数等。关于构造函数constructor的更多内容可参见文末参考资料第 6 条。总结类在 OI-wiki 与竞赛中的使用要点类是结构体的拓展class与struct在 C 中都是类说明符差异主要体现在默认访问级别class默认privatestruct默认public与书写惯例上访问控制public/protected/private决定了成员在类内、派生类、友元与类外四种场景下的可访问性是封装的基础成员函数让数据与操作内聚重载运算符尤其是operator是接入std::sort等 STL 设施的关键OI-wiki 的莫队、凸包等实现都是范例构造函数控制初始化语义注意定义了带参构造函数后编译器不再生成默认构造函数的陷阱C11 起可用{}与std::initializer_list初始化析构函数用于释放动态内存通常仅当类包含指针成员时才需重载赋值默认是浅拷贝含指针成员时必须重载相应构造函数以避免共享地址引发的错误。在 OI-wiki 中类相关的代码散见于 docs/math/code、docs/geometry/code、docs/misc/code 等目录读者可将本文与这些源码对照阅读进一步体会类的实战用法。参考资料以下为本页对应的官方标准参考原文档所列供深入学习cppreference: classcppreference: accesscppreference: default_constructorcppreference: operators运算符重载cplusplus: Data structures结构体教程cplusplus: Special members特殊成员函数C11 FAQStroustrupcplusplus: Friendship and inheritance友元与继承cppreference: value initialization值初始化对应{}初始化步骤的完整规则关联阅读结构体struct、STL 排序std::sort、C 语言入门总览【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考