文章目录
- 一、运算符重载
- 二、类的继承
- 1.类的继承
- 2.虚函数
一、运算符重载
在C++中,operator
关键字用于重载运算符,使得类的实例可以使用内置的操作符(如+
、-
、*
、/
等)进行操作。
运算符重载的特性:
-
重载不能发明新的运算符,不能改变运算的优先级与结合性,通常不改变运算含义
重载运算符不能改变其原有的优先级和结合性。重载的运算符应保持原有运算符的基本含义。
-
函数参数个数与运算操作数个数相同,至少一个为类类型
-
除
operator()
外其它运算符不能有缺省参数 -
可以选择实现为成员函数与非成员函数
运算符可以作为类的成员函数实现,也可以作为非成员函数(友元函数)实现。如果运算符重载为成员函数,
*this
通常表示第一个操作数,即当前对象。 -
对于比较运算符
==
和三元比较运算符<=>
(C++20),它们通常不使用*this
作为第一个操作数,因为它们用于比较两个对象。
重载运算符分类:
详细内容可参考:https://en.cppreference.com/w/cpp/language/operators
根据重载特性,可以将运算符进一步分为
-
可重载且必须实现为成员函数的运算符( =,[],(),-> 与转型运算符)
-
可重载且可以实现为非成员函数的运算符
-
可重载但不建议重载的运算符( &&, ||, 逗号运算符)
C++17 中规定了相应的求值顺序但没有方式实现短路逻辑
-
不可重载的运算符(如
? :
运算符)
对称运算符通常定义为非成员函数:
在C++中,对称运算符(如+
、-
、*
、/
等)通常定义为非成员函数(友元函数)以支持首个操作数的类型转换。
示例:对称运算符作为非成员函数
class Rational {
public:
int numerator; // 分子
int denominator; // 分母
// 构造函数
Rational(int n = 0, int d = 1) : numerator(n), denominator(d) {
// 简化分数
}
// 重载 + 运算符作为友元函数
friend Rational operator+(const Rational& lhs, const Rational& rhs);
};
// 实现 + 运算符
Rational operator+(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator * rhs.denominator + rhs.numerator * lhs.denominator,
lhs.denominator * rhs.denominator);
}
Rational
类表示有理数,我们重载了+
运算符来实现两个有理数的加法。由于+
是一个对称运算符,我们将其作为非成员函数实现,并通过友元函数声明允许它访问Rational
类的私有成员。
移位运算符一定要定义为非成员函数:
在C++中,移位运算符(<<
和 >>
)一定要定义为非成员函数,因为移位运算符是对称的,且它们通常需要与输入输出流一起使用。对于输入输出流(如std::ostream
和std::istream
),移位运算符被重载以允许将数据流式传输到输出或从输入。
示例:重载移位运算符以支持自定义类的输入输出
#include <iostream>
class MyClass {
private:
int val;
public:
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
// 实现将对象obj输出到流os的逻辑
os << obj.val;
return os;
}
friend std::istream& operator>>(std::istream& is, MyClass& obj) {
// 实现从流is读取数据到对象obj的逻辑
is >> obj.val;
return is;
}
};
int main() {
MyClass obj;
std::cout << obj << std::endl; // 使用重载的<<运算符输出对象
std::cin >> obj; // 使用重载的>>运算符输入对象
return 0;
}
赋值运算符也可以接收一般参数:
在C++中,赋值运算符(=
)通常用于将一个值或一个对象的状态赋给另一个对象。赋值运算符可以重载为类的成员函数,也可以重载为非成员函数(例如,全局函数或友元函数)。当重载赋值运算符时,它通常接收一个参数,这个参数的类型是该类的类型或者是一个可接受的赋值的类型(合理即可)。
operator [] 通常返回引用:
在C++中,operator[]
用于提供对容器或类似数组结构中元素的访问。通常,operator[]
重载返回一个引用,这样做有几个原因:
- 可变访问:返回引用允许用户修改通过
operator[]
访问的元素的值。 - 效率:返回引用而不是值的拷贝可以提高性能,因为不需要复制元素。
- 一致性:对于像数组这样的数据结构,用户期望能够通过下标访问和修改元素,返回引用提供了这种直观的操作方式。
- 链式调用:返回引用允许使用链式语法,例如,可以连续为数组的多个元素赋值。
示例:重载operator[]
以返回引用
#include <iostream>
class MyArray {
private:
int* data;
size_t size;
public:
MyArray(size_t sz) : size(sz) {
data = new int[sz];
}
// 析构函数,释放内存
~MyArray() {
delete[] data;
}
// operator[] 重载,返回对数据的引用
int& operator[](size_t index) {
return data[index];
}
// 常量版本的 operator[],返回一个 const 引用
int& operator[](size_t index) const {
return data[index];
}
};
int main() {
MyArray arr(10);
arr[0] = 10; // 使用重载的 operator[] 修改元素
int value = arr[0]; // 使用重载的 operator[] 访问元素
std::cout << value << std::endl;
return 0;
}
自增、自减运算符的前缀、后缀重载方法:
在C++中,自增(++
)和自减(--
)运算符可以作为成员函数重载,以适应自定义类型的操作。自增和自减运算符有两种形式:前缀(++it
)和后缀(it++
)。
示例:自增运算符重载
class Counter {
private:
int value;
public:
// 构造函数
Counter(int val) : value(val) {}
// 前缀自增运算符重载
Counter& operator++() {
++value; // 增加值
return *this; // 返回当前对象的引用
}
// 后缀自增运算符重载
Counter operator++(int) {
Counter temp = *this; // 保存当前状态的副本
++value; // 增加值
return temp; // 返回保存状态的副本
}
};
示例:自减运算符
// 自减运算符的重载方式与自增类似,只是将增加操作替换为减少操作。
class Counter {
private:
int value;
public:
// 构造函数
Counter(int val) : value(val) {}
// 前缀自减运算符重载
Counter& operator--() {
--value; // 减少值
return *this; // 返回当前对象的引用
}
// 后缀自减运算符重载
Counter operator--(int) {
Counter temp = *this; // 保存当前状态的副本
--value; // 减少值
return temp; // 返回保存状态的副本
}
};
后缀自增自减涉及到拷贝,因此,如果能使用前缀自增自减就不要尝试使用后缀自增自减。
解引用运算符(*
)和成员访问运算符(->
)重载:
在C++中,解引用运算符(*
)和成员访问运算符(->
)通常与指针一起使用,以访问所指向对象的成员。对于自定义类型,可以重载operator*
和operator->
来模拟指针的行为,这在实现智能指针或其他类似指针的类时非常有用。
注意:
- “ .” 运算符不能重载
- “→” 会递归调用“→”操作
示例:
#include <iostream>
class MySmartPointer {
private:
int* ptr;
public:
MySmartPointer(int* p) : ptr(p) {}
// 解引用运算符重载
int& operator*() {
return *ptr; // 返回对指向对象的引用
}
int& operator*() const {
return *ptr; // 返回对指向对象的引用
}
// 成员访问运算符重载
MySmartPointer* operator->() {
return this; // 返回指向对象的指针
}
int val = 5;
};
int main()
{
//解引用运算符重载的使用
int x = 100;
MySmartPointer msp(&x);
std::cout << *msp << std::endl; //100
*msp = 101;
std::cout << *msp << std::endl; //101
//成员访问运算符重载的使用
std::cout << msp->val << std::endl; //5
}
函数调用运算符重载:
使用函数调用运算符构造可调用对象,其参数个数不确定。
示例:
#include <iostream>
class FunCall {
private:
int val;
public:
FunCall(int p) : val(p) {}
int operator() ()
{
return val;
}
int operator() (int x, int y, int z)
{
return val + x + y + z;
}
bool operator() (int input)
{
return val < input;
}
};
int main()
{
FunCall obj(100);
std::cout << obj() << std::endl; //100
std::cout << obj(1, 2, 3) << std::endl; //106
std::cout << obj(101) << std::endl; //1
}
类型转化运算符重载:
在C++中,类型转换运算符允许类的实例通过调用特定的成员函数来进行类型转换。这是通过重载operator type()
来实现的,其中type
是目标转换的类型。
类型转换运算符的一些关键点:
-
函数声明:类型转换运算符被声明为
operator type()
,其中type
是内置类型或枚举类型。 -
隐式转换:类型转换运算符允许隐式类型转换,这意味着编译器可以在需要时自动调用这个运算符。
-
explicit关键字:使用
explicit
关键字可以避免隐式类型转换,使得类型转换必须显式进行。class MyClass { public: explicit operator bool() const { return some_condition; } };
-
单参数构造函数:单参数构造函数也可以隐式地引入类型转换。如果一个类有一个接受单个参数的构造函数,编译器可以自动使用这个构造函数来进行类型转换。
-
避免歧义和意外行为:类型转换运算符应该小心设计,以避免引入歧义或意料之外的行为。特别是,应该避免创建可以隐式转换为不相关类型或具有多重含义的类型的转换运算符。
-
explicit bool的特殊性:用于条件表达式时会进行隐式类型转换
示例:
#include <iostream>
class Percentage {
private:
double value;
public:
Percentage(double val) : value(val) {}
// 类型转换运算符,允许隐式转换为double
operator double() const {
return value;
}
// 显式类型转换运算符
explicit operator int() const {
return static_cast<int>(value);
}
//使用类型转换运算符在条件表达式中隐式的转换为bool
operator bool() const {
return value > 1;
}
};
int main() {
Percentage p(35.7);
// 隐式转换为double
double d = p;
// 显式转换为int
int i = static_cast<int>(p);
// 使用类型转换运算符在条件表达式中
if (p) {
// 条件为真,因为转换为bool的结果是true
}
return 0;
}
C++ 20 中对 == 与 <=> 的重载:
-
==
运算符重载:当你为类重载了
operator==
来比较两个对象是否相等时,编译器可以自动为你生成operator!=
,表示不等。class MyClass { int value; public: //==支持交换律 bool operator==(const MyClass& other) const { return value == other.value; } }; // 对于MyClass,编译器将自动提供operator!=。
-
<==>
运算符重载<=>
运算符允许你定义对象之间的全面比较逻辑,包括小于、等于、大于。#include <iostream> class MyClass { int value; public: auto operator<=>(const MyClass& other) const { if (value < other.value) return std::strong_ordering::less; if (value > other.value) return std::strong_ordering::greater; return std::strong_ordering::equal; } };
- 隐式交换操作数:
<=>
运算符可以交换操作数的位置,这意味着你可以定义一个只比较当前对象与另一个对象的版本,编译器将自动处理另一个顺序。 - 返回类型:
<=>
运算符的返回类型是std::strong_ordering
、std::weak_ordering
或std::partial_ordering
,这些类型都定义在<compare>
头文件中。std::strong_ordering
:表示可以确定对象之间的顺序关系,即要么<
要么>
。std::weak_ordering
:表示可以确定对象之间的顺序关系,但存在等价关系。std::partial_ordering
:表示只能确定部分顺序关系,可能存在无法比较的情况。
- 隐式交换操作数:
二、类的继承
1.类的继承
在C++中,类的继承是一种多态性机制,它允许一个类(派生类或子类)继承另一个类(基类或父类)的属性和行为。继承关系是一种“是一个”(is-a)的关系。
以下是类继承的一些关键概念和要点:
基本语法:
class Base {
// 基类成员
};
class Derived : public Base {
// 派生类成员
};
注意:继承部分不是类的声明
继承方式:
- public继承:最常用的继承方式,基类的公有(public)和保护(protected)成员在派生类中保持原有的访问级别。通常采用public继承。
- protected继承:基类的公有和保护成员在派生类中变为派生类的保护成员。
- private继承:基类的公有和保护成员在派生类中变为派生类的私有成员。
使用基类的指针或引用可以指向派生类对象:
使用基类指针或引用指向派生类对象的过程称为向上转型(upcasting),这是安全的并且不需要显式的类型转换。
Derived d;
Base* basePtr = &d;
Base& baseRef = d;
静态类型与动态类型:
- 静态类型:在编译时确定的类型,用于类型检查和作用域规则。
- 动态类型:在运行时确定的类型,与对象的实际类型相关联。可以使用
dynamic_cast
进行向下转型(downcasting),但需要基类有虚函数。