专栏文章
浅谈 C++ 类语法
科技·工程参与者 19已保存评论 20
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 20 条
- 当前快照
- 1 份
- 快照标识符
- @mioj7q9g
- 此快照首次捕获于
- 2025/12/02 20:05 3 个月前
- 此快照最后确认于
- 2025/12/02 20:05 3 个月前
0.日志
迁移
因为原专栏出现了一些问题,所以迁移过来的。
希丰展
私信我。(我知道不会有人要的)
修订日志
[2024/9/29] 第 版草稿。很差,跟托答辩没有区别。
[2025/1/xx] 修改中……
[2025/2/12~15] 修改第 版,架构大改,备份希丰展。第 版通过审核!
更新了内容:
[2025/1/xx] 修改中……
[2025/2/12~15] 修改第 版,架构大改,备份希丰展。第 版通过审核!
更新了内容:
- 继承;
- 左右值初步。
[2025/4/13] 第 版通过审核!
[2025/7/16] 第 版编辑中!
更新了内容:
[2025/7/16] 第 版编辑中!
更新了内容:
this指针;final与override关键字;
[2025/7/19] 第 版通过审核(七月十六日未卜先知?)!
[2025/7/28] 第 版:备份希丰展,更改了部分表述。增添了鸣谢一部分。
[2025/7/30] 第 版:更新了希丰展的渲染,添加了第 8 章实战和第 7.3 节编译器特性。突破 字。
[2025/8/5] 第 版(纯多余):快要写一年了,庆祝一下!
[2025/8/13] 第 版:超级大改。 天后再看,已有 人二连,非常高兴,加班写。
更新摘要:
[2025/7/28] 第 版:备份希丰展,更改了部分表述。增添了鸣谢一部分。
[2025/7/30] 第 版:更新了希丰展的渲染,添加了第 8 章实战和第 7.3 节编译器特性。突破 字。
[2025/8/5] 第 版(纯多余):快要写一年了,庆祝一下!
[2025/8/13] 第 版:超级大改。 天后再看,已有 人二连,非常高兴,加班写。
更新摘要:
- 添加了“对象切片”,将
String中的重载+运算符改成了全局友元函数(但我连实现都不想写了,懒,也不算是吧)。将 7.3.3 的 CE 代码修改,将节 7.2 分成 节,增添了 7.2.2 运算符优先级; - 将第 6.4 节分成 节。
- 给每个(尽可能多的)名词添加了对应的英文;
- 删除了“跨行继承”(很难用到,,匪夷所思),删除了 7.2 节的“贴”部分;
- 改了希丰展;
- 更新了第 章;
- 突破 字
[2025/10/1] 第 版:对
move 函数的有误注解进行了修正。1. 简介
面向对象编程(OOP)是一种编程范式,它将数据和操作封装成对象,通过类来创建对象,用对象模拟现实世界中的实体。它具有封装性,隐藏内部细节;继承性,实现代码复用;多态性,同一操作针对不同对象有不同表现。这让程序结构更清晰、易维护和扩展。
当然,这篇文章不一定全面,类似“模板套模板与模板类套模板类”这类很多东西都没有办法写在这里,因为涉及到的东西太多了,还请各位谅解。
当然,这篇文章不一定全面,类似“模板套模板与模板类套模板类”这类很多东西都没有办法写在这里,因为涉及到的东西太多了,还请各位谅解。
2. 从基本开始——类
2.1 基本类模板
这是一个基本类的模板,放在这里,后续会用到。
CPPclass <类名>
{
private: //关键字 private 是一个访问权限标识符;
<属性与方法>
public: //关键字 public 是一个访问权限标识符;
<属性与方法>
protected: //关键字 protected 是一个访问权限标识符;
<属性与方法>
}; //这是类定义的结束,必须有这个分号。
2.2 三种访问权限标识符
在类中定义的函数、变量,我们应当称之为:方法(Methods)和属性(Attribute),下文也是如此。
2.2.1 公开访问
这个标识符是
public,公有成员在类内外1均可访问。用于定义类对外接口,像类的公共方法和数据成员,方便外部代码与之交互,实现对象功能调用。2.2.2 私有访问
这个标识符是
private,私有成员仅类内2可访问。用于隐藏类的实现细节,防止外部随意修改,保障数据安全与一致性,由类内方法间接操作。2.2.3 保护访问
这个标识符是
protected,保护成员在类及子类内可访问。为类的继承提供便利,允许子类访问父类部分成员3,实现代码复用与扩展同时保护关键成员。详见第 6 章。2.3 使用对象
当你需要定义一个变量,语法都是一样的:
CPP<类型名> <变量名>;
3. 使用方法和属性
定义方法和属性和一般程序定义函数与变量的方式一样,不过需要注意下面两种定义方法的方式:
方式 1
直接在类内提供方法的实现。
CPPclass UserClass
{
public:
void Debug(int Time)
{
<具体实现>
}
};
方式 2
在类内提供方法原型,在类外提供实现。
CPPclass UserClass
{
public:
void Debug(int Time);
};
void UserClass::Debug(int Time)
{
<具体实现>
}
这两种方式都可以。当然,调用时,和
CPPstruct 使用一样的语法:<对象>.<属性/方法>(<方法的参数>)
4. 渐渐提升——特殊方法
4.1 两种特殊的成员函数
- 构造函数(Constructor)是一种特殊的函数,用来在创建类对象时进行特殊的操作来构造这个对象:例如当我们定义了一个链表类,我们就应当先用头指针指向尾指针(这是特殊操作),并且长度初始为 。
那么,我们就需要知道这样的方法:构造函数。
它的语法是这样的: CPP(1)构造函数的函数名必须和类同名,并且前面不能有返回类型!class List { public: List(); //构造链表,这里就不实现了。 };
(2)你可以让编译器提供一个默认的构造函数,你也可以不编写这个函数,但是如果碰到跟底层的指针有关的,建议你还是老老实实地写你的构造函数吧。
(3)构造函数可以显式调用5,方式是这样的:List Event();,当然,构造函数也可以拥有参数,此时想要构造就必须显式构造。\ - 析构函数(Deconstructor)当程序结束后,这种函数进行垃圾回收或其他特殊操作,一般用于清理垃圾、整合内存。继续沿用上面的栗子,当你在程序结束后,我们消除链表里的每一个元素,此时就需要使用到析构函数。
它的语法是这样的: CPP(1)析构函数的函数名是构造函数的函数名加上一个波浪符,当然,它也不能拥有返回类型。class List { public: List(); //构造链表,这里就不实现了。 ~List(); //析构链表,这里就不实现了。 };
(2)如果不是跟指针有关的程序,编译器提供的析构函数就可以了,否则你得自己提供。但是,一旦跟指针有关系(除非你用 STL),必须写析构函数!
(3)析构函数不应当显式调用6!
4.2 再次提升:重载运算符
重载运算符(Operator Overloading)是一种方便~牛马~程序员写代码时的一个非常好用的方法。
首先我们要了解一个东西:运算符承受数(Operator Acceptance),意思是指:某一个运算符可以承载多少个操作数。
当然,还有很多运算符可以重载,见第 7 章。
首先我们要了解一个东西:运算符承受数(Operator Acceptance),意思是指:某一个运算符可以承载多少个操作数。
当然,还有很多运算符可以重载,见第 7 章。
- 几种一元运算符:
+,-,!,*,&,--和++。 - 以下是可以重载的二元运算符及其一般作用:
+
+运算符:用于对两个对象进行算术相加。 +-运算符:用于对两个对象进行算术相减。 +*运算符:用于对两个对象进行算术相乘。 +/运算符:用于对两个对象进行算术相除。 +%运算符:用于对两个对象进行算术相求余。 +>>和<<运算符:流运算符或位左右移运算符。 +&,|,^运算符:用于位的运算符,可以用来替代逻辑运算符。 - 所有的简写运算符都可以重载。
- 下标运算符
[]它拥有两种形式:读和写。- 使用这个运算符应当只填写一个参数。(它的运算符承受数是 )
- 应当提供两个函数形式:一个返回引用(用于写),另一个返回常量(用于读)。
- 唯一一种多元运算符
(),即函数调用运算符,它的最大承受数是 。
重载运算符的格式应当是这样的:
CPP<返回类型> operator <重载的运算符>(<参数列表>)
{
<具体实现>
}
- 如果你需要重载的运算符你用成员方法实现,那么参数列表中就会少一个参数。因为第一个操作数被隐含成
*this了,所以不能使用,但是在代码中应当使用*this来调用第一个操作数。 - 但是如果你使用友元方式(见4.3)重载运算符的时候,参数个数就是操作符承受数。
关于运算符重载的注意事项
+不要乱重载运算符,比如
* 号重载成交换两个对象,尽管语法上没错,但是看起来就很怪异,应当使用其他函数来替代,例如:Swap()。- 有些运算符千万不要重载 / 不建议重载:
+两种二元逻辑布尔运算符,即
&&和||这两种,重载这两个运算符会失去短路求和功能。 +不要重载,运算符,它叫序列运算符(Sequence Operator),它只适用于保证求值顺序从左至右。几乎没有什么正当理由需要重载它。 +不能重载.运算符,也就是成员运算符。 +不能重载.*运算符,也就是成员指针运算符。 +不能重载::运算符,域访问重载后会失去语义。 +关于指针的两种运算符new与delete说明一下:除非你很了解底层指针的工作原理,否则不要重载它们,因为这两种运算符包含 种基本形式,又有 种数组形式,可以重载的又有 种形式,并且重载过程很麻烦;而且可能与编译器的new或delete不兼容。 +不建议重载->运算符,重载这个运算符你需要重载上面两种运算符。 - 部分运算符不能用来使用友元重载,只能作为类成员重载。
- 重载的运算符不能也不会改变它的优先级!例如:
CPP
是这样的执行顺序:先执行cout<<a+b*c;*后执行+,最后执行<<。
4.3 友元函数
首先强调:友元(Friend)函数不是成员方法!它不能通过成员运算符(
声明友元函数的格式如下:
CPP.)来调用。声明友元函数的格式如下:
friend <返回类型> <函数名>(<参数列表>)
{
<具体实现>
}
但是有几点需要注意:
- 友元函数如果要声明为内联函数,应当使用
friend inline或是inline friend,但是编译器解析这两种函数定义时会有不同的步骤。解析步骤
一般的,编译器的解析方式都是从左往右的语法树元素解析。
例如friend inline会先解析friend作为友元,然后解析inline内联。
而inline friend会相反。 - 前面提到的 种特殊的成员函数都不可以成为友元函数,当然还包括一些运算符重载(例如
=)也不可以。 - 友元函数可以访问类的私有属性与方法!
4.4 转换函数
如果我们需要将自定义的类转换成另一种类,就需要用到转换函数。转换函数一般分为两种:转换成其他类或者是接受了其它类。
4.4.1 转换成其他类
这是模板:
CPPoperator <类型名>()
{
<具体实现>
}
注意:
<类型名>是函数名,正是由于此处指明了输出类型,所以该函数省略了返回值。编译器会根据函数名决定返回值类型。4.4.2 接受其他类
这是模板:
CPP<类名>(<类型名><替代名>)
{
<具体实现>
}
看上去很像构造函数的函数名,实际上,这种转换就是构造函数的变体。
4.4.3 提示
所有的转换函数都可以在后面加上
explicit 关键字,这个关键字要求转换必须显式。4.5 拷贝与移动
4.5.1 拷贝
拷贝(Copy),一般是由一个对象复制给另一个对象(分为深拷贝与浅拷贝,深拷贝是指:不仅拷贝对象里的内容,还会拷贝其指针,修改时相当于修改原对象;浅拷贝不拷贝原对象的指针,修改时不会修改原对象)一般的,如果生成默认构造函数,且在没有禁用该函数的情况下,会生成一个拷贝函数组:拷贝赋值(复制赋值读起来有点怪异,所以用拷贝)与拷贝构造。
拷贝构造,一般是一个临时值的复制时才会用到拷贝构造。同理,拷贝赋值是在复制对象并且运用了赋值运算符时才会触发。
拷贝构造,一般是一个临时值的复制时才会用到拷贝构造。同理,拷贝赋值是在复制对象并且运用了赋值运算符时才会触发。
4.5.2 移动
左右值概念见 7.1.
移动(Move)是将对象销毁前,将资源标记为另一个对象的简单、省空间的方法。生成条件同理。
所有的移动函数的参数都应当包含右值引用。因为有些左值不可以移动!
移动(Move)是将对象销毁前,将资源标记为另一个对象的简单、省空间的方法。生成条件同理。
所有的移动函数的参数都应当包含右值引用。因为有些左值不可以移动!
左值转右值
请注意,当你的函数头是像这么写的时候:
CPPvoid Function(UsClass &&Obj,...)
这意味着,你可以接受一个右值 & 左值的参数。当你的函数与移动有关,你会需要用到下面这个函数:
CPPstd::move()//左值转右值
//std::forward() 才是完美转发,感谢各路大佬指出!
//定义头长这样,头疼就跳过!
template<typename T>
constexpr typename std::remove_reference<T>::type&& move(T&& t)noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}//自行查资料理解,此处不予解释。
4.5.3 三五零法则
4.5.3.1 三五法则
当一个类需要显式定义以下任意一个特殊成员函数时,通常需要定义全部五个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11)
- 移动赋值运算符(C++11)
原因
因为如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,通常意味着它管理着某些资源(如内存、文件句柄等)。在这种情况下,移动操作通常也需要自定义以避免资源管理问题。
4.5.3.2 零法则
理想情况下,类不应自定义任何拷贝/移动操作或析构函数,而是依赖:
- 编译器自动生成的默认实现
- 使用智能指针等 RAII 类型管理资源
优势
遵循零法则的类更容易维护,更少出错,且能自动获得正确的拷贝、移动和析构语义。
4.6 自指针
在所有的成员函数(包括重载、移动、拷贝函数)都有(也只在成员函数中有)一个指针
CPPthis,它指向调用该方法的对象的指针7。
即有这样的一个代码片段:A.Method();
若在成员方法
Method 中,使用了 this,则 this 表示的就是 A。4.6.1 箭头法
如果使用
this,一定要注意:如果你不是使用 4.6.2 的解引用法,那你应该调用 this 的成员方法时,需要使用 -> 运算符,因为它是一个指针。4.6.2 解引用法
也可以使用
(*this). 来调用,两者差别不大。5. 一般模板
模板(Template),是 C++ 中重要的一环,它将面向对象编程与泛型编程很好的契合在了一起。
5.1 引入模板
先来看一个比较两个整型数中较大数的版本:
CPPint Max(int a,int b){return a>b?a:b}
但是如果需要再写一个比较两个浮点数的版本?你是不是想到了再写一个:
CPPdouble Max(double a,double b){return a>b?a:b}
如果还要编写超长整型呢?你应该会再写一个8:
CPPusing ll=long long;//在 C++11 中的新标准,允许这样使用类型别名
ll Max(ll a,ll b){return a>b?a:b}
如果有更多类型需要比较,你的函数可能会写的越来越多,所以,为了方便,添加了“模板”这一技术。
5.2 一般函数模板语法
一般模板的语法是这样的:
CPPtemplate<typename 模板类型名,typename 模板类型名,...>
<返回类型> <函数名>(<参数列表>)
{
<具体实现>
}
当然,上述代码中的
那么上面的
CPPtypename 也可以替换成 class。不过建议使用 typename 来向后兼容 C++ 版本。注意一点:一个模板定义头只能用于一个函数 / 类。那么上面的
Max 函数就可以编写成:template<typename Type>
Type Max(Type A,Type B){return A>B?A:B}
尽管以后有高精度数,也只需要编写一个
operator > 即可。5.3 可变长参数列表
在 C++11 有了参数安全的变长参数列表函数,此时就可以用像传统的
这个是变长参数函数的模板:
CPPprintf 函数一样,拥有可变参数模板。这个是变长参数函数的模板:
template<typename <类型替代名>>
<返回类型><函数名>(<第一对参数><参数包>...);
template<typename <类型替代名>>
<返回类型><函数名>(<一对参数>);
CPPtemplate<typename Tp>
Tp Max(Tp Numbers){return Numbers};
template<typename Tp,typename ...Targs>
Tp Max(Tp Numbers,Targs... Other){return Numbers>Max(Other...)?Numbers:Max(Other...)}
我们只看第二个模板函数:
(1)模板和函数原型中使用了 个省略号,这是参数包符号,表示回传很多参数。
(2)我们使用递归:每次都与后面的最大数去比(尽管效率很低下,因为不是记忆化递归)。
(1)模板和函数原型中使用了 个省略号,这是参数包符号,表示回传很多参数。
(2)我们使用递归:每次都与后面的最大数去比(尽管效率很低下,因为不是记忆化递归)。
5.4 一般模板类语法
考虑到有些程序需要设计一些不同类型但是实现基本一样的类,于是就有了“模板类”一说。
模板
CPPtemplate<typename 模板类型名,typename 模板类型名,...>
class <类名>
{
<类的成员>
};
注意,同函数模板一样,每一个模板定义头都只能对应一个函数 / 类。
注意
这个概念和上面的概念截然不同:
CPPclass UserClass
{
template<typename<类型名>>
<类型名><方法名>();
};
这个叫类内模板方法,但是一般用不到,用的更多的是上面的模板类。
5.5 模板实例化
模板实例化是 C++ 模板编程扯远了,扯到泛型编程了中的重要的一个部分,它让模板(函数模板或类模板)生成具体的函数或类。下面分别介绍函数模板和类模板的实例化。
5.5.1 函数模板实例化
函数模板定义了一个通用的函数,通过实例化可以生成针对特定类型的具体函数。
具体运用方法:
举个栗子:
CPP具体运用方法:
<已经定义的模板函数><类型名>(<其他参数>)。注意:类型名左右两边的 < 和 > 不可以漏掉。举个栗子:
return pow<int>(2,3);
此时程序返回的结果应该是整型数字 。
5.5.2 类模板实例化
有了函数模板实例化,就自然拥有类模板实例化,模板如下:
<已经定义的模板类><类型名> <对象名>。同样,类型名左右两边的尖括号不能省略。6. 继承
继承(Inheritance),同样也是 C++ 中重用代码的一个要点。
举栗子
青蛙是动物,所以青蛙拥有了动物的全部特点。
但是动物不都是青蛙,因为青蛙有青蛙其自身的特点。
但是动物不都是青蛙,因为青蛙有青蛙其自身的特点。
6.1 继承模板
这里是一个类继承的模板,放在这里后续章节会用到:
CPPclass UserClass:<继承方式><派生的父类>,<继承方式><派生的父类>,...
{
<具体的属性与方法>
}
6.2 公有继承
公有继承是面向对象编程里继承方式的一种,是构建类层次结构的重要手段。在公有继承中,派生类继承基类的成员,基类的公有成员和保护成员访问权限在派生类中保持不变,即公有成员仍可被外界访问,保护成员仍只能在类及其派生类内访问,而基类私有成员不可直接访问。
它体现“是一个”关系,意味着派生类对象属于基类对象的一种。借助公有继承,能有效复用基类代码,减少重复开发,还可实现多态,通过基类指针或引用调用派生类方法,提升程序的可维护性与扩展性。
它的语法结构只需要在模板的继承方式改成第 2 章学过的
它体现“是一个”关系,意味着派生类对象属于基类对象的一种。借助公有继承,能有效复用基类代码,减少重复开发,还可实现多态,通过基类指针或引用调用派生类方法,提升程序的可维护性与扩展性。
它的语法结构只需要在模板的继承方式改成第 2 章学过的
public 标识符就可以了。6.3 保护继承与私有继承
私有继承将父类的所有成员都隐藏起来,只允许子类通过继承来的方法进行间接访问;保护继承则介于两者之间,允许子类通过派生类或友元函数访问父类的公有和保护成员。但是由于几乎用不到,这里就不详述了。
6.4 虚函数
6.4.1 虚函数的要点
- 可在任何方法上添加
virtual关键字,例如:virtual void DoIt()。 - 一旦某个函数在基类中为虚函数,那么在子类将不会为非虚函数。
- 声明虚方法除了使程序慢一点点(查找虚函数表)以外,没有任何缺点。
当我们使用基类的引用或指针调用基类中定义的某个函数时,我们并不知道该函数真正的对象是什么类型(属于哪个类),因为它可能是一个基类的对象,也可能是一个子类的对象。
非虚函数和虚函数有一个很重要的区别: +虚函数总是在运行时解析10,而非虚函数恰恰相反,它从不在运行时解析,它编译时解析11。
非虚函数和虚函数有一个很重要的区别: +虚函数总是在运行时解析10,而非虚函数恰恰相反,它从不在运行时解析,它编译时解析11。
6.4.2 是否定义成虚函数
对于虚函数:
- 若拿不准要不要使某个方法为虚方法,就声明为虚方法。因为它除了慢一点,没有什么问题。
- 在实现一个多层次的类继承关系时,最基本的基类应该只需要虚方法(甚至是接下来要讲的纯虚方法)。
是否定义成虚函数的要点
当你发现某个方法虚函数与非虚函数的执行路径可能相同(说人话就是执行结果相同),但是两者的时间差距过大,还是用非虚函数比较好。(毕竟大家都分得清楚,是 Python 的解释速度快,还是 C++ 的编译速度快)
6.4.3 对象切片
对象切片问题
当派生类对象被以值传递的方式赋值给基类对象时,会发生对象切片(Object Slicing)。派生类特有的成员数据会被丢弃,只保留基类的部分。这不仅会导致数据丢失,还会使虚函数绑定失效。例如:
CPPclass 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 查找实际函数地址;
- 虚函数调用比普通函数调用多一次指针解引用操作,在性能关键代码中需注意;
- 虚函数表会增加每个对象的内存开销(通常增加一个指针大小)。
性能提示
虚函数调用比普通函数调用:
- 多一次指针解引用(访问虚表);
- 多一次内存访问(获取函数地址);
- 通常无法内联优化。 在性能关键代码中(如高频循环),应谨慎使用虚函数。
6.5 抽象的基类与纯虚函数12
6.5.1 纯虚函数
纯虚函数是一种特殊的虚函数。这类函数有且不能拥有实现!它的定义如下:
CPPvirtual <类型><名字>(<参数>)=0;
其中,
=0是纯虚函数的定义标识符。
纯虚函数必须在子类重新定义,否则子类也将会是一个抽象类。6.5.2 抽象类
抽象类是一种继承后的基类,此种类不应该定义变量。
关于抽象类
抽象类可以包含纯虚函数(不一定非要包含),但是包含纯虚函数的类必定是抽象类7。
6.6 改写方法
当你需要改写派生类派生于父类的一个函数,你应当这么做:
- 确保需要改写的方法,以下条件是否达到:
- 派生类中重写的函数必须与基类被重写函数的函数名、参数列表、返回类型(协变返回类型13除外)完全相同。
- 基类函数为虚函数,基类中的被重写函数需要用
virtual关键字声明为虚函数,派生类中重写的函数即使不写virtual关键字,也默认是虚函数。
- 将改写的方法的
()后面加上关键字override,表明这个方法被改写了。
但是
重写方法的访问权限14可以不同,但一般建议保持一致,避免混淆。
6.7 多重继承
多重继承即一个类继承多个父类。但是有几种情况要考虑。
6.7.1 讨论
- 菱形继承(跨行继承类似):多个类派生自一个类,然后都被继承至一个类。他们会拥有相同的属性或方法。此时,也应当使用虚继承。
6.7.2 解决
使用虚继承,只需要在继承标识符前加上
virtual 即可,就不会重叠继承。6.8 拒绝继承 / 改写
当一个类(比如最后的派生类)不能再被继承时,应当使用
final 关键字。6.8.1 类禁止继承
在类的定义后加上
CPPfinal,实例:class A final
{...};
class B:public A //发生编译错误
{...};
6.8.2 虚函数禁止改写
在虚函数的定义后加上
CPPfinal,实例:virtual void B(...)final{...}
当在虚函数后加上
final 关键字,将无法在派生类重写该函数。6.8.3 讨论15
声明:以下的回答均为 DeepSeek-R1 0528+yangfengzhao 的杂糅总结回答!
本回答由 AI 生成,内容仅供参考,请仔细甄别。
Q: 为什么不能禁用成员函数?
A: 禁用没有意义。
Q: 有性能优化吗?
A: 有,但不多。如果有虚表,那在查虚表时,如果这个函数是
本回答由 AI 生成,内容仅供参考,请仔细甄别。
Q: 为什么不能禁用成员函数?
A: 禁用没有意义。
Q: 有性能优化吗?
A: 有,但不多。如果有虚表,那在查虚表时,如果这个函数是
final,那就会进行一些(约快 13.1%)但不可忽略的性能优化。7. 其他的特性
7.1 左值 or 右值
+左值(lvalue):指那些表达式结束后依然存在的持久对象,有一个可以标识的内存地址,能用取地址符
& 获取地址。
+右值(rvalue):是表达式结束后不再存在的临时对象,通常没有可获取的内存地址。右值包含纯右值和将亡值,这里就不详述了。7.2 运算符重载方面
7.2.1 可以重载的运算符 VS 不可以重载的运算符
可以重载的运算符
以下是所有可以重载的运算符:
| 运算符类型 | 运算符 | 作用 |
|---|---|---|
| 算术运算符 | + | 加法运算,可用于自定义类型的加法操作 |
- | 减法运算,可用于自定义类型的减法操作 | |
* | 乘法运算,可用于自定义类型的乘法操作 | |
/ | 除法运算,可用于自定义类型的除法操作 | |
% | 取模运算,可用于自定义类型的取模操作 | |
| 自增自减运算符 | ++ | 自增操作,分为前置和后置自增,可用于自定义类型 |
-- | 自减操作,分为前置和后置自减,可用于自定义类型 | |
| 位运算符 | & | 按位与运算,可用于自定义类型的按位与操作 |
| | 按位或运算,可用于自定义类型的按位或操作 | |
^ | 按位异或运算,可用于自定义类型的按位异或操作 | |
~ | 按位取反运算,可用于自定义类型的按位取反操作 | |
<< | 左移运算,可用于自定义类型的左移操作 | |
>> | 右移运算,可用于自定义类型的右移操作 | |
| 逻辑运算符 | && | 逻辑与运算,可用于自定义类型的逻辑与操作,但重载时不具备短路特性 |
|| | 逻辑或运算,可用于自定义类型的逻辑或操作,但重载时不具备短路特性 | |
! | 逻辑非运算,可用于自定义类型的逻辑非操作 | |
| 比较运算符 | == | 相等比较,可用于自定义类型的相等判断 |
!= | 不相等比较,可用于自定义类型的不相等判断 | |
< | 小于比较,可用于自定义类型的大小比较 | |
> | 大于比较,可用于自定义类型的大小比较 | |
<= | 小于等于比较,可用于自定义类型的大小比较 | |
>= | 大于等于比较,可用于自定义类型的大小比较 | |
| 赋值运算符 | = | 赋值操作,可用于自定义类型的赋值 |
+= | 加赋值操作,可用于自定义类型的加赋值 | |
-= | 减赋值操作,可用于自定义类型的减赋值 | |
*= | 乘赋值操作,可用于自定义类型的乘赋值 | |
/= | 除赋值操作,可用于自定义类型的除赋值 | |
%= | 取模赋值操作,可用于自定义类型的取模赋值 | |
&= | 按位与赋值操作,可用于自定义类型的按位与赋值 | |
|= | 按位或赋值操作,可用于自定义类型的按位或赋值 | |
^= | 按位异或赋值操作,可用于自定义类型的按位异或赋值 | |
<<= | 左移赋值操作,可用于自定义类型的左移赋值 | |
>>= | 右移赋值操作,可用于自定义类型的右移赋值 | |
| 成员访问运算符 | -> | 成员指针访问运算符,可用于自定义类型的成员指针访问 |
->* | 指向成员指针的指针访问运算符,可用于自定义类型 | |
| 函数调用运算符 | () | 函数调用运算符,可用于自定义类型的函数调用 |
| 下标运算符 | [] | 下标访问运算符,可用于自定义类型的下标访问 |
| 逗号运算符 | , | 逗号运算符,可用于自定义类型的逗号操作 |
| 类型转换运算符 | () | 类型转换运算符,可用于自定义类型的类型转换 |
不可以重载的运算符
以下是所有不可以重载的运算符:
| 运算符 | 说明 |
|---|---|
. | 成员访问运算符,因为它指定了对象的成员,重载会破坏语言的基本语义 |
.* | 成员指针访问运算符,与 . 类似,重载会破坏语义 |
:: | 作用域解析运算符,用于指定命名空间或类的作用域,不允许重载 |
?: | 条件运算符,它的语法结构特殊,重载会使代码变得复杂且难以理解 |
sizeof | 求对象或类型大小的运算符,其结果在编译时确定,不能重载 |
typeid | 获取类型信息的运算符,用于运行时类型识别,不能重载 |
7.2.2 运算符的优先级
这一节描述运算符的优先级。(数越小,优先级越高)
| 优先级 | 运算符 | 名称 | 结合性 |
|---|---|---|---|
| 1 | :: | 作用域解析 | 无结合性 |
| 2 | () [] . -> ++ -- | 函数调用、下标、成员访问、后缀自增/减 | 左→右 |
| 3 | ++ -- + - ! ~ (type) * & sizeof | 前缀自增/减、一元加减、逻辑非、按位取反、强制类型转换、解引用、取地址、大小 | |
| 4 | .* ->* | 成员指针访问 | |
| 5 | * / % | 乘、除、取模 | |
| 6 | + - | 加、减 | |
| 7 | << >> | 位左移、位右移 | |
| 8 | <=> | 三路比较(C++20) | |
| 9 | < <= > >= | 关系比较 | |
| 10 | == != | 相等比较 | |
| 11 | & | 按位与 | |
| 12 | ^ | 按位异或 | |
| 13 | | | 按位或 | |
| 14 | && | 逻辑与 | |
| 15 | || | 逻辑或 | |
| 16 | ?: | 条件运算符 | 右→左 |
| 17 | = += -= *= /= %= <<= >>= &= ^= |= | 赋值及复合赋值 | |
| 18 | , | 逗号运算符 | 左→右 |
注意
这个表格仅供查看运算符的优先级,不是代表可以重载的运算符,要想查看可以重载的运算符,请看上一节 7.2.1。
7.3 编译器特性
7.3.1 [[noreturn]] 特性
这个特性只用于函数上,用来告诉编译器:这个函数执行完后,控制权不会交还给调用点。
栗子
CPP[[noreturn]]void SysExit()
{
exit(0);
}
int main()
{
SysExit();
}
7.3.2 [[nodiscard]] 特性
这个特性也只用于函数上,用于检查(强制检查)函数的返回值是否被使用过。
栗子
CPP[[nodiscard]]void UnUsedRetrnFunc()
{
return 42;//编译器会发出警告!
}
int main()
{
UnUsedRetrnFunc();
}
7.3.3 [[deprecated]] 特性
这个特性也只用于函数上,标记过时的接口。
栗子
CPP[[deprecated("Used New ...")]]void OldFunc()
{
return 42;
}
int main()
{
OldFunc();//编译器会发出警告!
}
7.3.4 [[likely]] / [[unlikely]] 特性
这个特性用于
if/else 语句上。栗子
CPPif(condition)[[likely]]
{
// 高概率执行的代码
}
else[[unlikely]]
{
// 低概率执行的代码
}
8. 实战栗子
这一章主要是实战,写一个
String 类。8.1 目标
整理思路,大概分这几块:
- 拼接、删除。
- 查找、替换子串。
- 切片。
- 转换到
int,float,double和char*。 - 其他函数(长度、首字母大小写、回文检测)。
8.2 实践
8.2.1 从 0 开始
先写一个基本框架好了:
CPP#include<iostream>
#include<cstring>
#include<algorithm>
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(String &Sub)const; // 返回第一个字符的位置
String Replace(String &Sub,String &Rhs);// 返回替换后的字符串,不进行赋值
String Silce(int Left,int Right); // 返回类似 Python [Start:Stop:Step] 的切片。
// 以下函数,由于篇幅关系,不作展示
void ToUpper();
void ToLower();
template<typename ToClass>
ToClass To();
const char* ToCStringing()const noexcept{ return Data;}
// 不实现了,太多重载了
friend String Connect(const String& Other)const;
friend String Connect(const char& Other)const;
friend String Connect(const char*& Other)const;
friend String Connect(const string& Other)const;
// 辅助方法
friend int Len(String &Refrence)const noexcept{return Refrence.Len;}
};
8.2.2 一步一步来,一步一步去
完整版不会放在结尾
如有优化建议、语法错误、需要完整代码等请私信。
我放在结尾有点像在水文章。
以下是构造函数(家族)和析构函数的实现。
需要自取,自行优化
CPPString::String()noexcept:Data(new char[1]{'\0'})Len(0){}
String::String(const char* CStringing)
{
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;
}
接下来实现重载运算符的 个方法。
需要自取,自行优化
CPPString& 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 operator==(const String& Other)const
{
return strcmp(Data, Other.Data)==0;
}
char& operator[](int Index)
{
if(Index<0||Index>=Len)throw std::out_of_range("Index out of range");
return Data[Index];
}
const char& operator[](int Index)const
{
if(Index<0||Index>=Len)throw std::out_of_range("Index out of range");
return Data[Index];
}
然后是几个功能性函数。
在这里,别滑走
CPPint Find(const String& Sub)const
{
if(Sub.Len==0)return 0;
for(int Index=0; Index <= Len-Sub.Len; ++Index)
{
bool Match=true;
for(int TmpInd=0; TmpInd<Sub.Len; ++TmpInd)
{
if(Data[Index+TmpInd]!=Sub.Data[TmpInd])
{
Match=false;
break;
}
}
if(Match)return Index;
}
return -1;
}
String Replace(const String& Sub, const String& Replacement)const
{
std::vector<int>Positions;
int SubLen=Sub.Len;
if(SubLen==0)return *this;
int Pos=0;
while(Pos<=Len-SubLen)
{
bool Found=true;
for(int Index=0;Index<SubLen;++Index)
{
if(Data[Pos+Index]!=Sub.Data[Index])
{
Found=false;
break;
}
}
if(Found)
{
Positions.push_back(Pos);
Pos+=SubLen;
}
else
{
Pos++;
}
}
int NewLen=Len+Positions.size()*(Replacement.Len-SubLen);
String Result;
delete[] Result.Data;
Result.Data=new char[NewLen+1];
Result.Len=NewLen;
int SourcePos=0;
int DetectPos=0;
for(int Pos:Positions)
{
int CopyLen=Pos-SourcePos;
std::copy(Data+SourcePos, Data+SourcePos+CopyLen, Result.Data+DetectPos);
DetectPos+=CopyLen;
std::copy(Replacement.Data, Replacement.Data+Replacement.Len, Result.Data+DetectPos);
DetectPos+=Replacement.Len;
SourcePos=Pos+SubLen;
}
std::copy(Data+SourcePos, Data+Len, Result.Data+DetectPos);
Result.Data[NewLen]='\0';
return Result;
}
String Silce(int Left, int Right)
{
if(Left<0)Left=0;
if(Right>Len)Right=Len;
if(Left>=Right)return String();
String Result;
Result.Len=Right-Left;
delete[] Result.Data;
Result.Data=new char[Result.Len+1];
std::copy(Data+Left, Data+Right, Result.Data);
Result.Data[Result.Len]='\0';
return Result;
}
9. 结语(广告)
广告位:浅谈 C++ 模板语法
上机练习(?)Swodniw 终端
非常感谢各位大佬看到这里。鄙人不才,才疏学浅。如有错误请私信指出。非常感谢您为我(们)的提出宝贵建议。也希望您能将这篇文章推荐给其他 OIer 阅读。
上机练习(?)Swodniw 终端
非常感谢各位大佬看到这里。鄙人不才,才疏学浅。如有错误请私信指出。非常感谢您为我(们)的提出宝贵建议。也希望您能将这篇文章推荐给其他 OIer 阅读。
10. 附录——拾遗珠玑
感谢这几位同学为这篇长达 字的文章提供鼓励 & 支持:Wendy_Hello_qwq,blue7628_N2O,Like_Amao,OIerror,Innate_Joker(AFO),Gcc_Gdb_7_8_1,Reserved_,liukangzhe2029,rpyluogu,SClan_Offical……
由于撰者还是个六年级蒟蒻,有误请私,不喜勿喷:
由于撰者还是个六年级蒟蒻,有误请私,不喜勿喷:
Footnotes
-
只要定义了这个对象,就可以在源程序的任意位置使用这个对象公有标识符下的所有成员。 ↩
-
指类内的所有成员可以访问,但是外部不可以访问。 ↩
-
具体来讲,就是指继承于父类的子类可以访问父类的保护与公有部分。 ↩
-
一般将这两种方法称为“成员函数”,还有这几种特殊的成员函数:移动构造(赋值)与复制构造(赋值)。 ↩
-
应当在代码中明显地调用:比如明显指出转换为整型、或者强调使用的构造函数。 ↩
-
如果你调用析构函数成功了,那么有两种可能:1. 你使用了函数分割范围块(即函数内有匿名大括号,这种括号不被语句使用,但是它们和函数一样拥有变量范围),此时你在大括号内定义了一个对象,当大括号的作用域结束时,就会调用析构函数。2. 你在重载
placement new运算符,此时必须显式调用。 ↩ -
在 C++11 就支持了
using指令更改。 ↩ -
这是因为其中一个函数要作为递归结束条件。 ↩
-
通俗来讲,就是将解释这个函数,一步一步运行。 ↩
-
也就是直接编译这个函数,不考虑运行时出现的偏差。 ↩
-
必须在子类重写该函数。 ↩
-
这意味着,父类的保护方法可以被改写为子类的公有方法;当然,父类的公有方法甚至可以被子类改写成私有方法,但是不建议这么做,因为有的时候几个很类似的函数原型在不同的权限里就很容易混淆。 ↩
-
由于需要注明使用了 GemLLM,所以还是要在附录里水几个字……(?)本文所用模型为 DeepSeek-R10528(老模型),参数按照官网推荐参数调制。推荐使用 V3.1 版本。 ↩
相关推荐
评论
共 20 条评论,欢迎与作者交流。
正在加载评论...