C++ 中的封装
封装是面向对象编程(OOP)的核心概念之一。其思想是将数据成员和方法绑定成一个整体。
类可以隐藏实现细节,仅对外公开其他类所需的功能。通过将类的数据和方法设为私有,可以在不影响使用该类的代码的前提下,日后随意更改其内部表示或实现。
封装有助于提高代码的可维护性、可读性和易用性;同时,通过对变量赋值进行校验和控制,它还能保持数据的完整性。
1. C++ 中封装的实现方式
- 将变量声明为私有(
private):把类的数据成员设为私有,使其无法被外部直接访问,从而确保数据被隐藏。 - 使用
getter与setter:提供公共的 getter 和 setter 函数,以安全地访问和修改私有变量;这些方法内部可加入校验逻辑,确保只写入合法数据。 - 合理运用访问修饰符:数据成员用
private隐藏信息;向外部提供受控访问的成员函数则用public。
#include <iostream>
#include <string>
using namespace std;
// 表示一个程序员的类
class Programmer
{
private:
string name; // 私有变量
public:
// 获取私有数据的 getter 方法
string getName()
{
return name;
}
// 修改私有数据的 setter 方法
void setName(string newName)
{
name = newName;
}
};
int main()
{
Programmer p;
p.setName("Geek"); // 设置名字
cout << "Name=> " << p.getName() << endl; // 获取名字
return 0;
}
Name=> Geek
解释:
在上面的示例中,我们使用了封装机制,并通过 getter(getName) 和 setter(setName) 方法来访问和修改私有数据。这种封装机制保护了 Programmer 对象的内部状态,并提供了更好的控制和灵活性,来决定如何访问和修改 name 属性。
2.封装的最佳实践
将类的数据设为私有,以隐藏实现细节并降低耦合。使用 getter 和 setter 函数代替公共字段,以控制访问权限。确保只有合法值才能赋给私有变量。对于不应被外部修改的数据(如 ID),不要提供 setter 方法。
3.封装的优势
- 数据隐藏:封装限制了对私有类变量的直接访问,防止敏感数据被未授权访问。
- 提高可维护性:可以在不影响使用类的代码的前提下,更改类的内部实现。
- 增强安全性:封装允许在 setter 方法中进行验证和控制,防止无效或有害的值。
- 代码可重用性:封装后的类可以在不同程序中重用,而无需暴露内部细节。
- 更好的模块化:封装将数据和方法集中在一个类中,促进代码的组织与模块化。
4.封装的劣势
- 增加代码复杂度:为每个变量编写 getter 和 setter 函数会使代码变长、略为复杂。
- 性能开销:通过函数访问私有数据而非直接访问,会带来轻微的性能损失。
- 某些场景下灵活性降低:过度限制访问可能会妨碍其他类高效地扩展或使用该类。
C++ 中的多态性
“多态”一词的含义是“多种形态”。在 C++ 中,多态这一概念可以作用于函数和运算符:同一个函数名在不同情境下可以表现出不同的行为;同样,一个运算符在不同上下文中也可以执行不同的操作。
1. 编译期多态(Compile-Time Polymorphism)
又称“早绑定”或“静态多态”。在这种多态中,编译器在编译阶段就根据上下文决定函数或运算符的具体行为。它通过函数重载或运算符重载来实现。
A. 函数重载(Function Overloading)
函数重载是面向对象语言的一种特性:两个或多个函数可以同名,但只要它们的参数个数或类型不同,就能表现出不同的行为。这样的函数称为 重载函数 ,因此该机制叫做函数重载。
示例代码
#include <bits/stdc++.h>
using namespace std;
class Geeks {
public:
// 版本 1:相加两个整数
void add(int a, int b) {
cout << "Integer Sum = " << a + b << endl;
}
// 版本 2:相加两个浮点数
void add(double a, double b) {
cout << "Float Sum = " << a + b << endl;
}
};
int main() {
Geeks gfg;
gfg.add(10, 2); // 调用整数版本
gfg.add(5.3, 6.2); // 调用浮点版本
return 0;
}
Integer Sum = 12
Float Sum = 11.5
解释:
上面定义了两个同名函数 add(),但参数类型不同:一个接收 int,另一个接收 double。编译器在编译期根据实参类型选择正确的版本,使得同一个函数名可以适用于不同的数据类型。
B. 运算符重载(Operator Overloading)
C++ 允许为特定数据类型给运算符赋予新的含义,这种能力称为运算符重载。
例如,加法运算符 + 用于整数时表示相加,用于字符串时则可实现拼接;<< 和 >> 原本只是位左移/右移运算符,但在输入输出流中却被用作流插入/提取运算符,这正是运算符重载的体现。
示例代码
#include <iostream>
using namespace std;
class Complex {
public:
int real, imag;
Complex(int r, int i) : real(r), imag(i) {}
// 重载 '+' 运算符,实现两个复数相加
Complex operator+(const Complex& obj) {
return Complex(real + obj.real, imag + obj.imag);
}
};
int main() {
Complex c1(10, 5), c2(2, 4);
// 使用 '+' 运算符将 c1 与 c2 相加
Complex c3 = c1 + c2;
cout << c3.real << " + i" << c3.imag;
return 0;
}
12 + i9
解释: 默认情况下,诸如 +、- 等运算符并不支持用户自定义类型,因为它们不知道该对这些类型做什么。通过运算符重载,我们可以为这些运算符定义针对自定义类型的行为,就像上面为 Complex 类重载 + 一样。
注意:在 C++ 中,绝大多数运算符都可以被重载,但少数几个运算符
::(作用域解析)、.(成员访问)、.*(成员指针访问)、?:(条件)、sizeof(大小)
不允许重载,因为它们直接关系到语言的核心语义,为了保持代码清晰和编译器完整性,语言禁止对它们进行重载。
2. 运行时多态(Runtime Polymorphism)
又称“迟绑定”或“动态多态”。与编译期多态相反,函数调用的绑定不是在编译阶段决定,而是在程序运行时才确定。实现手段是虚函数(virtual function)+ 函数覆盖(overriding)。
A. 函数覆盖(Function Overriding)
当派生类重新定义了基类的某个成员函数时,就说该基类函数被“覆盖”{也叫:函数重写}。要想产生运行时多态,基类必须把该函数声明为虚函数。
示例代码
#include <bits/stdc++.h>
using namespace std;
class Base {
public:
// 虚函数
virtual void display() {
cout << "Base class function";
}
};
class Derived : public Base {
public:
// 覆盖基类的虚函数
void display() override {
cout << "Derived class function";
}
};
int main() {
Base* basePtr; // 基类指针
Derived derivedObj; // 派生类对象
basePtr = &derivedObj; // 让基类指针指向派生类对象
basePtr->display(); // 运行时决定调用哪个版本
return 0;
}
Derived class function
解释:基类 Base 把 display() 声明为虚函数,派生类 Derived 对其进行了覆盖。虽然通过基类指针 basePtr 调用 display(),但由于运行时才根据指针实际指向的对象类型(Derived)解析函数地址,最终执行的是派生类的版本,打印出 Derived class function。这就是运行时多态的典型表现。
编译期多态 vs 运行时多态
两者最核心的区别如下:
| 编译期多态(Compile-Time Polymorphism) | 运行时多态(Run-Time Polymorphism) |
|---|---|
| 又称静态绑定(static binding) | 又称动态绑定(dynamic binding) |
| 通过函数重载、运算符重载实现 | 通过虚函数 + 函数覆盖实现 |
| 由编译器在编译阶段决定调用哪个函数 | 程序运行阶段依据 vtable 动态决议 |
| 早绑定,速度更快 | 更灵活,但性能略低 |