news 2026/9/12 11:15:35

OI-wiki 语言篇:C++ 类(class)完全指南——从结构体进阶到 OOP 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OI-wiki 语言篇:C++ 类(class)完全指南——从结构体进阶到 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 的竞赛代码中,structclass都被大量用于组织复合数据。例如 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中完全成立。

定义类:语法与基本示例

类使用关键字classstruct定义。语法模板与实例:

class ClassName { ... }; // Example: class Object { public: int weight; int value; } e[array_length]; const Object a; Object b, B[array_length]; Object *c;

上例定义了一个名为Object的类,拥有两个成员元素weightvalue,并在}之后用该类型定义了一个数组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:"这一竞赛编码习惯的根源。

关于友元与派生类的基本概念

理解protectedprivate的差异,需要引入两个基础概念:

  • 友元(friend:使用friend关键字修饰某个函数或类,可以使得被修饰者在不成为成员函数或成员类的情况下,访问该类的私有(private)或受保护(protected)成员。简单来说,只要带有这个类的friend标记,就可以访问其私有或受保护成员元素。OI-wiki 代码中也有实例,例如 docs/geometry/code/convex-hull/convex-hull_1.cpp 用friend声明了Point的运算符重载函数(详见下文"重载运算符")。
  • 派生类(derived class):C++ 允许使用一个类作为基类,并通过基类派生派生类,派生类(根据特定规则)继承基类中的成员变量和成员函数,从而提高代码复用率。派生类与基类呈现 "is" 关系——如猫(派生类)"is" 哺乳动物(基类)。

在继承语境下,privateprotected的区别可以直观理解为:派生类可以访问基类的protected元素(public同理),但不能访问private元素

访问与修改成员元素的值

访问与修改成员元素的方法与结构体完全一致:

  • 对于变量,使用.符号:var.weight = 5;
  • 对于指针,使用->符号(等价于(*ptr).weight):ptr->weight = 5;

从源码看,OI-wiki 全站代码大量依赖这一访问方式,例如凸包实现中p[i].xu.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函数,以及一个更改成员元素weightchange_w函数。注意change_w采用"类内声明(void change_w(int);)+ 类外定义(void Object::change_w(int _weight) { ... })"的写法——和普通函数类似,成员函数也可以先声明、后定义。

若想调用varprint成员函数,使用var.print()即可。

从实战角度看,将数据与操作封装进同一个类,能显著提升代码的组织性与复用性。例如 docs/math/code/finite-field/finite-field_1.cpp 中实现有限域的FiniteField类,其成员变量p, k, mod与成员函数powinvpoly_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 + ba - ba * ba ^ 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 来源之一;
  • 如果需要自定义初始化的值,可以再定义(或重载)构造函数。
定义(或重载)构造函数的补充说明
  • 一般来说,默认构造函数是不带参数的,这区别于带参的构造函数;构造函数与默认构造函数的定义"大同小异",只是参数数量上的不同;
  • 构造函数可以被重载(首次定义当然也算定义);
  • 关键警告:如果已经定义了任意构造函数,编译器便不会再生成无参数的默认构造函数。这可能导致试图以默认方式(不填入初始化参数)构造变量的行为编译失败
  • 使用 C++11 或以上标准时,可以使用{}进行变量的初始化。
关于{}列表初始化

使用{}进行初始化时,会用到std::initializer_list这一轻量代理对象。其初始化步骤大致如下(简化版,详细内容可参见 cppreference 的 value initialization 文档):

  1. 尝试寻找参数中有std::initializer_list的构造函数,如果有则调用(调用完后不再进行下面的查找);
  2. 尝试将{}中的元素填入其他构造参数,如果能将参数按照顺序填满(默认参数也算在内),则调用该构造函数;
  3. 若无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,(C++11)

注意示例中的两种等价写法:一是类内使用初始化列表: 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 与竞赛中的使用要点

  1. 类是结构体的拓展classstruct在 C++ 中都是类说明符,差异主要体现在默认访问级别(class默认privatestruct默认public)与书写惯例上;
  2. 访问控制public/protected/private决定了成员在类内、派生类、友元与类外四种场景下的可访问性,是封装的基础;
  3. 成员函数让数据与操作内聚;重载运算符(尤其是operator<)是接入std::sort等 STL 设施的关键,OI-wiki 的莫队、凸包等实现都是范例;
  4. 构造函数控制初始化语义,注意"定义了带参构造函数后编译器不再生成默认构造函数"的陷阱,C++11 起可用{}std::initializer_list初始化;
  5. 析构函数用于释放动态内存,通常仅当类包含指针成员时才需重载;
  6. 赋值默认是浅拷贝,含指针成员时必须重载相应构造函数以避免共享地址引发的错误。

在 OI-wiki 中,类相关的代码散见于 docs/math/code、docs/geometry/code、docs/misc/code 等目录,读者可将本文与这些源码对照阅读,进一步体会类的实战用法。

参考资料

以下为本页对应的官方标准参考(原文档所列,供深入学习):

  1. cppreference: class
  2. cppreference: access
  3. cppreference: default_constructor
  4. cppreference: operators(运算符重载)
  5. cplusplus: Data structures(结构体教程)
  6. cplusplus: Special members(特殊成员函数)
  7. C++11 FAQ(Stroustrup)
  8. cplusplus: Friendship and inheritance(友元与继承)
  9. 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 11:15:28

牡丹芍药春季抹芽疏蕾技巧与养护要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 11:15:14

动态检索增强生成(RAG)技术解析与应用实践

1. 论文核心价值解析&#xff1a;动态检索增强生成的技术突破QR3AG这篇论文提出了一种创新性的动态检索增强生成框架&#xff0c;其核心价值在于解决了传统RAG&#xff08;检索增强生成&#xff09;系统中三个关键痛点&#xff1a;第一&#xff0c;传统静态检索机制存在"检…

作者头像 李华
网站建设 2026/9/12 11:15:07

Firecracker 如何配置 vhost-user 块设备并对接外部存储后端

Firecracker 如何配置 vhost-user 块设备并对接外部存储后端 【免费下载链接】firecracker Secure and fast microVMs for serverless computing. 项目地址: https://gitcode.com/GitHub_Trending/fi/firecracker 如果你的块设备 IO 需要在 Virtio 队列处理同层实现自定…

作者头像 李华
网站建设 2026/9/12 11:11:06

Android 16图形系统架构解析:从绘制到显示全链路拆解

做图形系统相关开发这些年&#xff0c;被问得最多的问题其实就一个&#xff1a;屏幕上的一张UI&#xff0c;到底是怎么从App里的代码变成像素的&#xff1f;尤其是到了Android 16这个版本&#xff0c;图形栈涉及的东西更多了&#xff0c;HDR、可变刷新率、多窗口、屏幕折叠形态…

作者头像 李华
网站建设 2026/9/12 11:10:22

移动端大语言模型压缩与优化技术解析

1. 项目背景与核心价值当Clawdbot在硅谷一夜爆红时&#xff0c;我正在调试一个本地化部署的LLM模型。手机突然弹出一条推送——这个仅用24小时就引发全球关注的项目&#xff0c;本质上在做一件极其简单却颠覆性的事&#xff1a;通过压缩和优化技术&#xff0c;将原本需要云端算…

作者头像 李华