浅谈 C++ 面向对象编程
Terminal11 · · 科技·工程
0.日志
可以骗二连吗?这对我真的很重要……
::::warning[迁移]
因为原专栏出现了一些问题,所以迁移过来的。
::::
::::success[修订日志]
[2024/9/29] 第
- 继承;
-
左右值初步。
[2025/4/13] 第
{3} 版通过审核!\ [2025/7/16] 第{4} 版编辑中!\ 更新了内容: this指针;-
final与override关键字;[2025/7/19] 第
{4} 版通过审核!\ [2025/7/28] 第{5} 版:备份希丰展,更改了部分表述。增添了鸣谢一部分。\ [2025/7/30] 第{5.1} 版:更新了希丰展的渲染,添加了第 8 章实战和第 7.3 节编译器特性。突破{20k} 字。\ [2025/8/5] 第{5.2} 版(纯多余):快要写一年了,庆祝一下!\ [2025/8/13] 第{6.1} 版:超级大改。{8} 天后再看,已有{40} 人二连,非常高兴,加班写。\ 更新摘要: - 添加了“对象切片”,将
String中的重载+运算符改成了全局友元函数(但我连实现都不想写了,懒,也不算是吧)。将 7.3.3 的 CE 代码修改,将节 7.2 分成{2} 节,增添了 7.2.2 运算符优先级; - 将第 6.4 节分成
{4} 节。 - 给每个(尽可能多的)名词添加了对应的英文;
- 删除了“跨行继承”(很难用到,
{FeSO_{4}} ,匪夷所思),删除了 7.2 节的“贴”部分; - 改了希丰展;
- 更新了第
{8} 章; -
突破
{23k} 字[2025/10/1] 第
{6.2} 版:对move函数的有误注解进行了修正。\ [2026/7/26] 第{7} 版:对错误的表述进行了修改,精简化(过去的章节 7.3 等被删去),重修了希丰展,重排了部分章节(实则是大调)。希望大家阅读愉快! :::: 请注意,上面所有的章节是指当时本文对应的章节,而非本文最新的章节,详细请参考右侧目录。1. 简介
面向对象编程(OOP)是一种编程范式,它将数据和操作封装成对象,通过类来创建对象,用对象模拟现实世界中的实体。它具有封装性,隐藏内部细节;继承性,实现代码复用;多态性,同一操作针对不同对象有不同表现。这让程序结构更清晰、易维护和扩展。\ 当然,这篇文章不一定全面,很多东西都没有办法写在这里,因为涉及到的东西太多了,还请各位谅解。
2. 从基本开始——类
2.1 基本类定义
这是一个基本类的定义,放在这里,后续会用到。
class <类名>{ private: //关键字 private 是一个访问权限标识符; <属性与方法> public: //关键字 public 是一个访问权限标识符; <属性与方法> protected: //关键字 protected 是一个访问权限标识符; <属性与方法> }; //这是类定义的结束,必须有这个分号。2.2 三种访问权限标识符
在类中定义的函数、变量,我们应当称之为:方法(Methods)和属性(Attribute)。访问这些数据需要各种权限,标记权限的叫做访问权限标识符 Access Specifier。
2.2.1 公开访问
公开访问(Public Access),这个标识符是
public,公有成员在类内外^1均可访问。用于定义类对外接口,像类的公共方法和数据成员,方便外部代码与之交互,实现对象功能调用。2.2.2 私有访问
私有访问(Private Access),这个标识符是
private,私有成员仅类内^2可访问。用于隐藏类的实现细节,防止外部随意修改,保障数据安全与一致性,由类内方法间接操作。2.2.3 保护访问
保护访问(Protected Access),这个标识符是
protected,保护成员在类及子类内可访问^3。为类的继承提供便利,基类的protected成员在派生类中可以被访问(无论是通过派生类对象还是派生类成员函数),但对于外部代码(非友元、非派生类)则不可见。详见第 6 章。2.3 使用对象
当你需要定义一个变量,语法都是一样的:
<类型名> <变量名>;3. 使用方法和属性
定义方法和属性和一般程序定义函数与变量的方式一样,不过需要注意下面两种定义方法的方式: ::::info[方式 1]{open} 直接在类内提供方法的实现。
class UserClass{ public: void Debug(int Time){ <具体实现> } };:::: ::::info[方式 2]{open} 在类内提供方法原型,在类外提供实现。
class UserClass{ public: void Debug(int Time); }; void UserClass::Debug(int Time){ <具体实现> }:::: 这两种方式都可以。调用时,使用:
<对象>.<属性/方法>(<方法的参数>)::::warning[注意事项]{open} 请注意,
class默认的访问权限标识符是private,而struct默认的访问权限标识符是public。 ::::4. 渐渐提升——特殊方法
4.1 两种特殊的成员函数
这里会介绍
{2} 种特殊的成员函数[^4],但实际上有{6} 个特殊的成员函数(在 C++ 17 前)。
- 构造函数(Constructor)是一种特殊的函数,用来在创建类对象时进行特殊的操作来构造这个对象:例如当我们定义了一个链表类,我们就应当先用头指针指向尾指针(这是特殊操作),并且长度初始为
{0} 。 那么,我们就需要知道这样的方法:构造函数。\ 它的语法是这样的:class List{ public: List(); //构造链表,这里就不实现了。 };(1)构造函数的函数名必须和类同名,并且前面不能有返回类型!\ (2)你可以让编译器提供一个默认的构造函数,你也可以不编写这个函数,但是如果碰到跟底层的指针有关的,建议你还是老老实实地写你的构造函数吧。\ (3)通过直接初始化(
List L(10);)或显式类型转换(static_cast<List>(10))来触发构造函数的调用[^5]。除非用explicit关键字修饰,否则编译器允许隐式构造(例如List L = 10;)。若希望禁止隐式转换,应在构造函数前加explicit。 - 析构函数(Destructor)在对象生命周期结束时调用(如离开作用域、
delete),用于释放资源。继续沿用上面的栗子,当对象的生命周期后,我们删除链表里的每一个元素,此时就需要使用到析构函数。\ 它的语法是这样的:class List{ public: List(); //构造链表,这里就不实现了。 ~List(); //析构链表,这里就不实现了。 };(1)析构函数的函数名是构造函数的函数名加上一个波浪符,当然,它也不能拥有返回类型。\ (2)如果不是跟指针有关的程序,编译器提供的析构函数就可以了,否则你得自己提供。但是,一旦跟指针有关系(除非你用 STL),必须写析构函数!\ (3)析构函数通常不应当显式调用[^6]!
4.2 再次提升:重载运算符
重载运算符(Operator Overloading)是一种方便写代码的一个非常好用的方法。\ 首先我们要了解一个东西:元数(Arity),意思是指:某一个运算符可以承载多少个操作对象。\ 当然,还有很多运算符可以重载,见第 7 章。
- 几种一元运算符:
+,-,!,*,&,--和++。 - 以下是可以重载的二元运算符及其一般作用:
+运算符:用于对两个对象进行算术相加。-运算符:用于对两个对象进行算术相减。*运算符:用于对两个对象进行算术相乘。/运算符:用于对两个对象进行算术相除。%运算符:用于对两个对象进行算术相求余。>>和<<运算符:流运算符或位左右移运算符。&,|,^运算符:用于位的运算符,可以用来替代逻辑运算符。
- 所有的简写运算符都可以重载,其元数为
{1} 。 - 下标运算符
[]应当只填写一个参数。(它的元数是{1} ) -
唯一一种多元运算符
(),即函数调用运算符,它的元数没有上限。重载运算符的格式应当是这样的:
<返回类型> operator <重载的运算符>(<参数列表>){ <具体实现> } - 如果你需要重载的运算符你用成员方法实现,那么参数列表中就会少一个参数。因为第一个操作数被隐含成
*this了,所以不能使用,但是在代码中应当使用*this来调用第一个操作数。 -
但是如果你使用友元方式(见4.3)重载运算符的时候,参数个数就是元数。
::::warning[关于运算符重载的注意事项]{open}
- 不要乱重载运算符,比如
*号重载成交换两个对象,尽管语法上没错,但是看起来就很怪异,应当使用其他函数来替代,例如:Swap()。 - 有些运算符千万不要重载 / 不建议重载:
- 强烈不建议重载这两种二元逻辑布尔运算符,即
&&和||这两种,重载这两个运算符会失去短路求值(Short-circuit Evaluation)功能。 - 不要重载
,运算符,它叫序列运算符(Sequence Operator),它只适用于保证求值顺序从左至右。几乎没有什么正当理由需要重载它。 - 不能重载
.运算符,也就是成员运算符。 - 不能重载
.*运算符,也就是成员指针运算符。 - 不能重载
::运算符,域访问重载后会失去语义。 - 关于指针的两种运算符
new与delete说明一下:除非你很了解底层指针的工作原理,否则不要重载它们,因为这两种运算符包含{5} 种基本形式,又有{3} 种数组形式,可以重载的又有{4} 种形式,并且重载过程很麻烦;而且可能与编译器的new或delete不兼容。 - 不建议重载
->运算符,该运算符通常用于实现智能指针或代理类,需要返回一个指针或支持->链式调用的对象,逻辑较为复杂。
- 强烈不建议重载这两种二元逻辑布尔运算符,即
- 运算符
= () [] ->只能作为类成员重载。 - 重载的运算符不能也不会改变它的优先级!例如:
cout<<a+b*c;是这样的执行顺序:先执行 *后执行+,最后执行<<。### 4.3 友元函数 首先强调:友元函数(Friend Function)不是成员方法!它不能通过成员运算符( .)来调用。\声明友元函数的格式如下: ```cpp line-numbers friend <返回类型> <函数名>(<参数列表>){ <具体实现> } ``` 但是有几点需要注意: - 前面提到的
{6} 种特殊的成员函数都是类内成员函数,而不可以声明为友元函数,当然还包括一些运算符重载(例如=)也不可以。 - 友元函数可以访问类的私有属性与方法!
4.4 转换函数
如果我们需要将自定义的类转换成另一种类,就需要用到转换函数(Conversion Function)。转换函数一般分为两种:转换成其他类或者是接受了其它类。
4.4.1 转换成其他类
这是模板:
operator <类型名>(){ <具体实现> }注意:
<类型名>是函数名,正是由于此处指明了输出类型,所以该函数省略了返回值。编译器会根据函数名决定返回值类型。4.4.2 接受其他类
这是模板:
<类名>(<类型名><替代名>){ <具体实现> }看上去很像构造函数的函数名,实际上,这种转换就是构造函数的变体。
4.4.3 提示
所有的转换函数都可以在后面加上
explicit关键字,这个关键字要求转换必须显式。4.5 拷贝与移动
接下来,会介绍两类特殊的成员函数:拷贝(Copy)与移动(Move)。
4.5.1 拷贝
拷贝(Copy),一般是由一个对象复制给另一个对象(分为深拷贝(Deep Copy)与浅拷贝(Shallow Copy)。
- 深拷贝则需自己实现拷贝构造和赋值运算符,重新分配内存并将原对象所管理的数据完整复制一份,使每个对象拥有独立的资源。
-
浅拷贝是指(默认拷贝构造函数和赋值运算符)仅复制对象的值类型成员,对于指针成员,只复制指针本身(即地址),不复制指针指向的数据。这会导致多个对象共享同一块动态内存,析构时重复释放(double free)或修改时相互影响。
拷贝构造,一般是一个临时值的复制时才会用到拷贝构造。同理,拷贝赋值是在复制对象并且运用了赋值运算符时才会触发。
4.5.2 移动
左右值概念见 8.1。\ 移动(Move)是将对象销毁前,将资源标记为另一个对象的简单、省空间的方法。\ 所有的移动函数的参数都应当包含右值引用。因为某些左值可以通过
std::move移动,但移动后状态有效但未指定! ::::info[左值转右值]{open} 请注意,当你的函数头是像这么写的时候:void Function(UsClass &&Obj,...)它能够接受一个右值(但它是左值),但它不能直接接受一个左值,除非左值被
std::move转换为右值。当你的函数与移动有关,你会需要用到下面这个函数://std::move() 无条件地将左值转右值引用,用于标识对象可被移动(转移资源),后续常配合移动构造函数或移动赋值运算符使用。 //std::forward() 有条件的转发,在模板中保持实参的原始值类别(左值或右值),实现“完美转发”。两者功能不同,不可混为一谈。请注意,即使形参是右值引用,一旦在函数体内被命名使用,它就是一个左值。如果需要继续转发其右值属性,应使用 std::move(对于最后一次使用)或std::forward(用于完美转发)。与拷贝操作不同,移动构造函数和移动赋值运算符的自动生成条件更为严格。它们仅当满足以下所有条件时才会被编译器隐式生成: -
- 该类未声明任何拷贝构造函数、拷贝赋值运算符或析构函数;
-
-
该类未声明任何移动构造函数或移动赋值运算符(即您没有显式定义它们)。
一旦您显式地定义了拷贝构造、拷贝赋值或析构函数中的任何一个,编译器就不会再自动生成移动操作。这意味着,如果您为管理资源的类编写了自定义析构函数,但忘记编写移动构造函数,那么该类将“丧失”移动语义。在使用
std::vector等容器时,这会退化为性能较低的拷贝操作。
-
- 最佳实践:当您需要自定义析构函数(通常意味着管理资源)时,请遵循“三五法则”或“零法则”,显式地定义或禁用(
=default/=delete)所有相关的拷贝和移动操作。4.5.3 三五零法则
4.5.3.1 三五法则
当一个类需要显式定义以下任意一个特殊成员函数时,通常需要定义全部五个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11)
- 移动赋值运算符(C++11)
::::info[原因]{open}
因为如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,通常意味着它管理着某些资源(如内存、文件句柄等)。在这种情况下,移动操作通常也需要自定义以避免资源管理问题。
::::
4.5.3.2 零法则
理想情况下,类不应自定义任何拷贝/移动操作或析构函数,而是依赖:
- 编译器自动生成的默认实现
- 使用智能指针等 RAII 类型管理资源
::::success[优势]{open}
遵循零法则的类更容易维护,更少出错,且能自动获得正确的拷贝、移动和析构语义。
::::
4.6
this指针this指针是作为隐含参数传递给非静态成员函数的。静态成员函数没有this指针。 这样的一个代码片段:A.Method();若在成员方法
Method中,使用了this,则this表示的就是A的地址。4.6.1 箭头法
如果使用
this,一定要注意:如果你不是使用 4.6.2 的解引用法,那你应该调用this的成员方法时,需要使用->运算符,因为它是一个指针。4.6.2 解引用法
也可以使用
(*this).来调用,两者差别不大。5. 一般模板
模板(Template),是 C++ 中重要的一环,它将面向对象编程与泛型编程很好的契合在了一起。
鉴于篇幅,本章讲的会跳的比较快。
5.1 函数模板
函数模板是这样的:
template<typename T1,...> <返回类型> <函数名>(<参数列表>){ <具体实现> }在对应类型
Type的函数模板实例化时,编译器会根据传入的参数类型生成对应的函数代码。\ 举栗子:template<typename T> T Add(T A,T B){ return A+B; //注意:这里的 + 运算符必须是 T 类型支持的运算符。(这是一种隐含的约束) } //对应的 int 会实例化为: int Add(int A,int B){ return A+B; } //对应的 double 会实例化成: double Add(double A,double B){ return A+B; }5.2 类模板
类模板是这样的:
template<typename T1,...> class <类名>{ <具体的属性与方法> };举栗子:
template<typename T> class List{...}; //对应的 int 会实例化成: class List<int>{...}; //对应的 double 会实例化成: class List<double>{...};如果没懂,可以看看这个:
vector<int> A; //实例化了一个 int 类型的 vector 类 vector<double> B; //实例化了一个 double 类型的 vector 类是不是很熟悉?事实上,
vector就是一个类模板。6. 继承
继承(Inheritance),同样也是 C++ 中重用代码的一个要点。 ::::info[举栗子]{open} 青蛙是动物,所以青蛙拥有了动物的全部特点。\ 但是动物不都是青蛙,因为青蛙有青蛙其自身的特点。 ::::
6.1 继承模板
这里是一个类继承的模板,放在这里后续章节会用到:
class UserClass:<继承方式><派生的父类>,<继承方式><派生的父类>,...{ <具体的属性与方法> }6.2 公有继承
公有继承是面向对象编程里继承方式的一种,是构建类层次结构的重要手段。在公有继承中,派生类继承基类的成员,基类的公有成员和保护成员访问权限在派生类中保持不变,即公有成员仍可被外界访问,保护成员仍只能在类及其派生类内访问,而基类私有成员不可直接访问。\ 它体现 “是一个”(Is-A) 关系,意味着派生类对象属于基类对象的一种。借助公有继承,能有效复用基类代码,减少重复开发,还可实现多态,通过基类指针或引用调用派生类方法,提升程序的可维护性与扩展性。\ 它的语法结构只需要在模板的继承方式改成第 2 章学过的
public标识符就可以了。6.3 保护继承与私有继承
私有继承是将基类的公有 / 保护成员变为派生类的私有成员,派生类内部仍可访问,只是外部无法访问。保护继承则介于两者之间,派生类内部(及友元)可以访问基类的公有和保护成员,但外部不可见。
6.4 虚函数
6.4.1 虚函数的要点
- 可在任何方法上添加
virtual关键字,例如:virtual void DoIt()。 - 一旦某个函数在基类中为虚函数,那么在子类将不会为非虚函数。
- 虚函数会增加对象大小(V-Ptr)、妨碍内联优化,在嵌入式或高性能场景中是劣势。
当我们使用基类的引用或指针调用基类中定义的某个函数时,我们并不知道该函数真正的对象是什么类型(属于哪个类),因为它可能是一个基类的对象,也可能是一个子类的对象。\ 非虚函数和虚函数有一个很重要的区别:
- 虚函数调用是运行时动态解析^7,非虚函数是编译时静态绑定^8。
6.4.2 是否定义成虚函数
对于虚函数:
- 如果类是为继承设计的,就谨慎标识虚方法。
-
在实现一个多层次的类继承关系时,最基本的基类应该只需要虚方法(甚至是接下来要讲的纯虚方法)。
::::warning[是否定义成虚函数的要点]{open} 当你发现某个方法虚函数与非虚函数的执行路径可能相同(说人话就是执行结果相同),但是两者的时间差距过大,还是用非虚函数比较好。(毕竟大家都分得清楚,是解释速度快,还是编译速度快) ::::
6.4.3 对象切片
::::error[问题]{open} 当派生类对象被以值传递的方式赋值给基类对象时,会发生对象切片(Object Slicing)。派生类特有的成员数据会被丢弃,只保留基类的部分。这不仅会导致数据丢失,还会使虚函数绑定失效。例如:
class Base{ public: virtual void Print()const{cout<<"Base\\Numbers";} }; class Derived:public Base{ public: void Print()const override{cout<<"Derived\\Numbers";} }; void Func(Base B){ B.Print(); // 总是调用Base::print(),即使传递的是Derived对象。 } int main(){ Derived D; Func(D); // 将D以值传递方式传递给Func,发生切片。 }因此,C++中的多态必须通过基类的指针或引用来实现,以避免切片问题。在函数参数中,应使用 Base&或Base*,而不是Base。#### 6.4.4 虚函数实现原理 虚函数通过虚函数表(V-Table)实现动态绑定: - 每个包含虚函数的类都有一个虚函数表;
- 表中按声明顺序存储虚函数指针;
- 在构造函数中设置指向 V-Table 的指针(V-Ptr);
- 调用虚函数时通过 V-Ptr 查找实际函数地址;
- 虚函数调用比普通函数调用多了指针解引用操作,在性能关键代码中需注意;
- 虚函数表会增加每个对象的内存开销(通常增加一个指针大小)。 ::::warning[性能提示]{open} 虚函数调用比普通函数调用:
- 多一次指针解引用(访问虚表);
- 多一次内存访问(获取函数地址);
- 通常无法内联优化。
在性能关键代码中(如高频循环),应谨慎使用虚函数。
::::
6.5 抽象的基类与纯虚函数^9
6.5.1 纯虚函数
纯虚函数是一种特殊的虚函数。纯虚函数通常只声明而不实现。但请注意,C++ 标准允许为纯虚函数提供定义(在类外),这在某些设计模式(如模板方法)中偶尔会被用到。它的定义如下:
virtual <类型><名字>(<参数>)=0;其中,
=0是纯虚函数的定义标识符。 纯虚函数必须在子类重新定义,否则子类也将会是一个抽象类。6.5.2 抽象类
含有至少一个纯虚函数的类称为抽象类,不能实例化。 ::::warning[关于抽象类]{open} 抽象类可以包含纯虚函数(不一定非要包含),但是包含纯虚函数的类必定是抽象类。 ::::
6.6 改写方法
当你需要改写派生类派生于父类的一个函数,你应当这么做:
- 确保需要改写的方法满足以下条件:
- 派生类中重写的函数必须与基类被重写函数的函数名、参数列表、返回类型(协变返回类型[^10]除外)完全相同。
- 基类函数为虚函数,基类中的被重写函数需要用
virtual关键字声明为虚函数,派生类中重写的函数即使不写virtual关键字,也默认是虚函数。
-
将改写的方法的
()后面加上关键字override,表明这个方法被改写了。::::info[但是]{open} 重写方法的访问权限^11可以不同,但一般建议保持一致,避免混淆。一般不推荐改变访问权限。 ::::
6.7 多重继承
多重继承(Multiple Inheritance) 即一个类继承多个父类。但是有几种情况要考虑。
6.7.1 讨论
- 菱形继承(Diamond Inheritance):多个类派生自一个类,然后都被继承至一个类。他们会拥有相同的属性或方法。此时,也应当使用虚继承。
6.7.2 解决
使用虚继承,只需要在继承标识符前加上
virtual即可,就不会重复继承。虚继承应被视为最后的手段,设计上应尽量避免菱形继承。6.8 拒绝继承 / 改写
当一个类(比如最后的派生类)不能再被继承时,应当使用
final关键字。6.8.1 类禁止继承
在类的定义后加上
final,实例:class A final{...}; class B:public A //发生编译错误{...};此时类
A无法被继承。6.8.2 虚函数禁止改写
在虚函数的定义后加上
final,实例:virtual void B(...)final{...}当在虚函数后加上
final关键字,将无法在派生类重写该函数。6.8.3 要点
- 在 C++ 中可以通过
=delete禁用某些成员函数(如拷贝构造),但无法通过final禁用单个非虚成员函数,它只用于终止继承链中的重写。 - 使用
final修饰虚函数时,编译器可能(取决于优化等级)进行去虚拟化(Devirtualization)优化,但具体性能提升取决于编译器和上下文,不宜给出具体数值。7. 实战栗子
这一章主要是实战,写一个
String类。7.1 目标
整理思路,大概分这几块:
- 拼接、删除。
- 查找、替换子串。
- 切片。
- 转换到
int,float,double和char*。 - 其他函数(长度、首字母大小写、回文检测)。
7.2 实践
不完全实现,此处提供一个基本框架。
7.2.1 从 0 开始
先写一个基本框架好了:
#include<iostream> #include<cstring> #include<algorithm> #include<vector> using namespace std; class String{ private: char* Data; // 指向字符数组的指针 int Len; // 字符数组的长度(假定不包含\0) public: String()noexcept; // 构造函数 String(const char* CStringing); // 由 char*构造函数 String(const String& Other); // 拷贝构造 String(String&& Other)noexcept; // 移动构造 ~String(); // 析构函数 // 赋值运算符 String& operator=(const String& Other); String& operator=(String&& Other)noexcept; bool operator==(const String& Other)const; char& operator[](int Index); const char& operator[](int Index)const; // 功能性函数 int Find(const String &Sub)const; // 返回第一个字符的位置。此处不予实现。 String Replaced(const String &Sub,const String &Rhs)const;// 返回替换后的字符串,不进行赋值。此处不予实现。 String Slice(int Left,int Right)const; // 返回类似 Python [Start:Stop:Step] 的切片。此处不予实现。 // 以下函数,由于篇幅关系。此处不予实现。 void ToUpper(); void ToLower(); const char* ToCString()const noexcept{return Data;} // 不实现了,太多重载了。 String Connect(const String& Other)const; String Connect(char Other)const; String Connect(const char* Other)const; // 辅助方法 friend int Len(const String &Reference)noexcept{return Reference.Len;} };7.2.2 请输入文本
::::success[完整版不会放在结尾]{open} 如有优化建议、语法错误、需要代码等请私信。 :::: 以下是构造函数(家族)和析构函数的实现。 ::::info[需要自取,自行优化]
String::String()noexcept:Data(new char[1]{'\0'}),Len(0){} String::String(const char* CString){ if(!CString){ Data=new char[1]{'\0'}; Len=0; } else{ Len=strlen(CString); Data=new char[Len+1]; std::copy(CString, CString+Len+1, Data); } } String::String(const String& Other):Len(Other.Len){ Data=new char[Len+1]; std::copy(Other.Data, Other.Data+Len+1, Data); } String::String(String&& Other)noexcept:Data(Other.Data), Len(Other.Len){ Other.Data=nullptr; Other.Len=0; } String::~String(){ delete[] Data; }:::: 接下来实现重载运算符的
{6} 个方法。 ::::info[需要自取,自行优化]String& String::operator=(const String& Other){ if(this!=&Other){ char* new_data=new char[Other.Len+1]; std::copy(Other.Data, Other.Data+Other.Len+1, new_data); delete[] Data; Data=new_data; Len=Other.Len; } return *this; } String& String::operator=(String&& Other)noexcept{ if(this!=&Other){ delete[] Data; Data=Other.Data; Len=Other.Len; Other.Data=nullptr; Other.Len=0; } return *this; } bool String::operator==(const String& Other)const{ return strcmp(Data, Other.Data)==0; } char& String::operator[](int Index){ if(Index<0||Index>=Len)throw std::out_of_range("Index out of range"); return Data[Index]; } const char& String::operator[](int Index)const{ if(Index<0||Index>=Len)throw std::out_of_range("Index out of range"); return Data[Index]; }::::
8. 附录——拾遗珠玑
8.1 左值 or 右值
- 左值(L-Value):指那些表达式结束后依然存在的持久对象,有一个可以标识的内存地址,能用取地址符
&获取地址。 - 右值(R-Value):是表达式结束后不再存在的临时对象,通常没有可获取的内存地址。右值包含纯右值和将亡值,这里就不详述了。
8.2 与运算符重载相关的
8.2.1 可以重载的运算符 VS 不可以重载的运算符
::::success[可以重载的运算符]{open} 以下是所有可以重载的运算符: | 运算符类型 | 运算符 | 作用 | | --- | --- | --- | | 算术运算符 |
+| 加法运算,可用于自定义类型的加法操作 | | ^ |-| 减法运算,可用于自定义类型的减法操作 | | ^ |*| 乘法运算,可用于自定义类型的乘法操作 | | ^ |/| 除法运算,可用于自定义类型的除法操作 | | ^ |%| 取模运算,可用于自定义类型的取模操作 | | 自增自减运算符 |++| 自增操作,分为前置和后置自增,可用于自定义类型 | | ^ |--| 自减操作,分为前置和后置自减,可用于自定义类型 | | 位运算符 |&| 按位与运算,可用于自定义类型的按位与操作 | | ^ |\|| 按位或运算,可用于自定义类型的按位或操作 | | ^ |^| 按位异或运算,可用于自定义类型的按位异或操作 | | ^ |~| 按位取反运算,可用于自定义类型的按位取反操作 | | ^ |<<| 左移运算,可用于自定义类型的左移操作 | | ^ |>>| 右移运算,可用于自定义类型的右移操作 | | 逻辑运算符 |&&| 逻辑与运算,可用于自定义类型的逻辑与操作,但重载时不具备短路特性 | | ^ |\|\|| 逻辑或运算,可用于自定义类型的逻辑或操作,但重载时不具备短路特性 | | ^ |!| 逻辑非运算,可用于自定义类型的逻辑非操作 | | 比较运算符 |==| 相等比较,可用于自定义类型的相等判断 | | ^ |!=| 不相等比较,可用于自定义类型的不相等判断 | | ^ |<| 小于比较,可用于自定义类型的大小比较 | | ^ |>| 大于比较,可用于自定义类型的大小比较 | | ^ |<=| 小于等于比较,可用于自定义类型的大小比较 | | ^ |>=| 大于等于比较,可用于自定义类型的大小比较 | | 赋值运算符 |=| 赋值操作,可用于自定义类型的赋值 | | ^ |+=| 加赋值操作,可用于自定义类型的加赋值 | | ^ |-=| 减赋值操作,可用于自定义类型的减赋值 | | ^ |*=| 乘赋值操作,可用于自定义类型的乘赋值 | | ^ |/=| 除赋值操作,可用于自定义类型的除赋值 | | ^ |%=| 取模赋值操作,可用于自定义类型的取模赋值 | | ^ |&=| 按位与赋值操作,可用于自定义类型的按位与赋值 | | ^ |\|=| 按位或赋值操作,可用于自定义类型的按位或赋值 | | ^ |^=| 按位异或赋值操作,可用于自定义类型的按位异或赋值 | | ^ |<<=| 左移赋值操作,可用于自定义类型的左移赋值 | | ^ |>>=| 右移赋值操作,可用于自定义类型的右移赋值 | | 成员访问运算符 |->| 成员指针访问运算符,可用于自定义类型的成员指针访问 | | ^ |->*| 指向成员指针的指针访问运算符,可用于自定义类型 | | 函数调用运算符 |()| 函数调用运算符,可用于自定义类型的函数调用 | | 下标运算符 |[]| 下标访问运算符,可用于自定义类型的下标访问 | | 逗号运算符 |,| 逗号运算符,可用于自定义类型的逗号操作 | | 类型转换运算符 |operator <Type>()| 类型转换运算符,可用于自定义类型的类型转换 | :::: ::::error[不可以重载的运算符]{open} 以下是所有不可以重载的运算符: | 运算符 | 说明 | | --- | --- | |.| 成员访问运算符,因为它指定了对象的成员,重载会破坏语言的基本语义 | |.*| 成员指针访问运算符,与.类似,重载会破坏语义 | |::| 作用域解析运算符,用于指定命名空间或类的作用域,不允许重载 | |?:| 条件运算符,它的语法结构特殊,重载会使代码变得复杂且难以理解 | |sizeof| 求对象或类型大小的运算符,其结果在编译时确定,不能重载 | |typeid| 获取类型信息的运算符,用于运行时类型识别,不能重载 | ::::8.2.2 运算符的优先级
这一节描述运算符的优先级(Operator Precedence) & 运算符的结合性(Associativity)。(数越小,优先级越高) | 优先级 | 运算符 | 名称 | 结合性 | |--------|--------|-------|-------| | 1 |
::| 作用域解析 | 无结合性 | | 2 |(),[],.,->,++,--| 函数调用、下标、成员访问、后缀自增/减 | 左→右 | | 3 |++,--,+,-,!,~,(type),*,&,sizeof| 前缀自增/减、一元加减、逻辑非、按位取反、强制类型转换、解引用、取地址、大小 | ^ | | 4 |.*,->*| 成员指针访问 | ^ | | 5 |*,/,%| 乘、除、取模 | ^ | | 6 |+,-| 加、减 | ^ | | 7 |<<,>>| 位左移、位右移 | ^ | | 8 |<,<=,>,>=,<=>(C++ 20) | 关系比较 | ^ | | 9 |==,!=| 相等比较 | ^ | | 10 |&| 按位与 | ^ | | 11 |^| 按位异或 | ^ | | 12 |\|| 按位或 | ^ | | 13 |&&| 逻辑与 | ^ | | 14 |\|\|| 逻辑或 | ^ | | 15 |?:| 条件运算符 | 右→左 | | 16 |=,+=,-=,*=,/=,%=,<<=,>>=,&=,^=,\|=| 赋值及复合赋值 | ^ | | 17 |,| 逗号运算符 | 左→右 | ::::warning[注意]{open} 这个表格仅供查看运算符的优先级,不是代表可以重载的运算符,要想查看可以重载的运算符,请看上一节 8.2.1。 ::::9. 结语
非常感谢各位大佬看到这里。鄙人不才,才疏学浅。如有错误请私信指出。非常感谢您为我的提出宝贵建议。也希望您能将这篇文章推荐给其他 OIer 阅读。
10. 鸣谢 & 注释
::::success[贡献者名单] 感谢这几位同学为这篇长达
{19k(19803)} 字的文章提供鼓励 & 支持:Wendy_Hello_qwq,blue7628_N2O,Like_Amao,OIerror,Innate_Joker(AFO),Gcc_Gdb_7_81,Reserved,liukangzhe2029,rpyluogu,SClan_Offical……\ :::: 有误请私,不喜勿喷: [^4]:除了这里介绍的构造和析构函数,C++ 还提供了其他特殊的成员函数,包括拷贝构造、拷贝赋值、移动构造和移动赋值(共{6} 个)。 [^5]:通过对象定义(如List L(10);)、显式类型转换(static_cast<List>(10))或隐式转换(除非被explicit禁止)来触发构造函数的执行。 [^6]:你应当在重载placement new等运算符时显式调用析构函数。 [^10]:指在类的继承体系中,当派生类重写(Override)基类的虚函数时,允许派生类返回基类返回类型的派生类版本。例如基类虚函数返回Base*,派生类重写后可以返回Derived*。(仅适用于指针或引用类型,且存在继承关系)