cpp面向对象(友元、运算符重载、虚函数)
友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
类的友元需要在类中声明,但是不必在类中定义。
友元一般在类的开头或者结尾集中声明。
友元函数
可以是其它类的成员函数,也可以是一个全局函数,如:
class A;
class B {
public:
void fun(A& obj) {
std::cout << obj.data << std::endl;
}
};
class A {
private:
int data;
public:
A(int value) : data(value) {}
friend void B::fun(A& obj);
};
这样,在fun函数作用域内,类A的私有成员是可以访问的,但需要注意,友元函数并不提供类A的this指针,所以this->data
这种访问是无效的。
友元类
类似的,只需要:
class A;
class B {
public:
void fun(A& obj) {
std::cout << obj.data << std::endl;
}
};
class A {
private:
int data;
public:
A(int value) : data(value) {}
friend B;
};
同样的,还是没有this指针,相比友元函数只是把允许类B访问类A的范围扩大到了整个类B。
注意
友元不可传递:A是B的友元,B是C的友元,不代表A是C的友元
友元不可继承:派生类不会继承基类的友元声明
友元不互反:A是B的友元,不代表B是A的友元
运算符重载
重载限制:
- 必须至少有一个操作数是用户定义的类型
- 不能违反原来的句法规则,例如+原本有两个操作数却重载为一个操作数,不能修改优先级等
- 不能创造新运算符
- 有些运算符不能重载
大部分可以重载为成员函数或者非成员函数(算术运算符、比较运算符等)
重载为成员函数:类的this指针会被绑定到运算符的左侧运算对象,重载函数的参数比运算符操作数少一个,例如a+b实际上是 a.operator+(b)
注意:这种方式有其局限性,假如说重载不发生在a对应的类中,那么a+b这种写法将会引发错误
重载为非成员函数:重载函数的参数数量与运算符操作数数量相等,例如a+b实际上是 operator+(a, b)
,为了在函数中访问类内的私有成员,需要在类中将该函数用friend关键字声明为友元函数
必须重载为成员函数:
- 赋值运算符 (
=
):必须重载为类的成员函数,用于定义对象的赋值行为。 - 下标运算符 (
[]
):必须重载为类的成员函数,用于实现对象的下标访问。 - 成员访问运算符 (
->
):必须重载为类的成员函数,用于实现对象指针成员的访问。
可以发现,这些运算符就算不重载,也有隐式的定义,如果重载为非成员函数会出现二义性的问题。
需要重载为非成员函数:
- 算术运算符 (
+
,-
,*
,/
, 等):通常重载为非成员函数,以支持对象与对象之间的运算,或者对象与基本数据类型之间的运算。 - 关系运算符 (
==
,!=
,<
,>
, 等):通常重载为非成员函数,以支持对象之间的比较。 - 流插入和流提取运算符 (
<<
,>>
):通常重载为非成员函数,以支持对象的输入和输出。
这些运算符如果重载为成员函数,就要修改类的源代码(增加成员函数),如果这些类是第三方的,我们仅仅想要在功能上进行一些拓展,那么重载为非成员函数显然是更好的选择。
虚函数
虚函数用virtual关键字声明,是cpp多态性的重要实现方式。
当然,即使不显式的用virtual声明,也不影响正常的函数重写。
静态绑定(编译时多态,static binding):可以在编译时就确定要调用哪个函数,编译器对非虚的函数使用这种方式(也是默认的方式)
动态绑定(运行时多态,dynamic binding):和虚函数有关系,将决定具体调用哪个函数的工作推迟到运行时(编译器通过插入一段代码来控制,不是说在运行时需要依赖编译器)。根据对象具体的类型(而非指向它的指针类型),去找相对应的函数。
虚函数的实现是编译器相关的,一般是为每个类维护一个虚表(vtbl),虚表中存放了该类所有的虚函数地址。派生类包含基类的虚表,如果在派生类中重写了基类的虚函数,那么派生类的虚表中的对应地址会由其基类的函数地址替换为派生类的函数地址。
override
:可以显式的声明对虚函数的重写,如果找不到基类中函数签名相同的函数,则编译器会报错
特别地,返回类型中存在继承关系,且是相应的指针或者引用,此时即使返回值不同,也是一种重写:
class Super {
public:
virtual Super* getThis() { return this; }
};
class Sub : public Super
{
virtual Sub* getThis() override { return this; }
};
final
:被final修饰的类不可以被继承,被修饰的函数不可以被重写
抽象类/纯虚函数
含有至少一个纯虚函数(pure virtual)的类是抽象类,纯虚函数只要加上=0即可:
virtual void foo() = 0;
纯虚函数在其声明处是不定义的,留给派生类进行继承,所以抽象类类似java中的接口(interface)。
抽象类不可以被实例化,继承了抽象类的类必须重写抽象类的所有纯虚函数。
虚继承
可以用于解决多继承中的菱形继承问题,例如:
class Base {
public:
Base() {
std::cout << "Base constructor\n";
}
};
class Derived1 : public virtual Base {
public:
Derived1() {
std::cout << "Derived1 constructor\n";
}
};
class Derived2 : public virtual Base {
public:
Derived2() {
std::cout << "Derived2 constructor\n";
}
};
class Diamond : public Derived1, public Derived2 {
public:
Diamond() {
std::cout << "Diamond constructor\n";
}
};
如果不使用虚继承,那么Diamond类中会有两份Base类的内容,显然不行。
解决方案是,Derived1和Derived2分别虚继承了Base,此时Base会被认为是Diamond的直接父类,而不是Derived1或者Derived2。
结果上看,Diamond正确继承了Derived1,Derived2,Base类的所有内容,且Base类只有一份内容,可以重写Base类的函数:
class Base {
public:
Base() {
std::cout << "Base constructor\n";
}
virtual void foo() {
std::cout << "no" << std::endl;
}
};
class Diamond : public Derived1, public Derived2 {
public:
Diamond() {
std::cout << "Diamond constructor\n";
}
void foo() override {
std::cout << "yes" << std::endl;
}
};
但是不可以重写Derived1类或者Derived2类的函数,以下代码无法通过编译:
class Derived1 : public virtual Base {
public:
Derived1() {
std::cout << "Derived1 constructor\n";
}
virtual void foo() {
std::cout << "no" << std::endl;
}
};
class Diamond : public Derived1, public Derived2 {
public:
Diamond() {
std::cout << "Diamond constructor\n";
}
// 编译器会报错
void foo() override {
std::cout << "yes" << std::endl;
}
};
Derived1的foo函数可以被Diamond正确继承的:
Diamond obj;
obj.foo(); // 可以正常调用Derived1继承来的函数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构