封装是类自带的固有属性,就像一个盒子天然就可以分装东西
继承是类与类之间的一种关系表现,我们知道除了继承,类之间的关系还可以有关联、依赖、实现、聚合、组合,为什么只强调继承?私以为实现是继承的特例,而其他四种关系都属于将类放在不同位置的灵活使用,且C中的结构体本身也具有这些特性,它并不是C++新创造出来的,但继承不一样,继承是新的需要提前约定的规则。
继承之后资源分配的规则就是多态
类的固有属性(封装),与它类新关系(继承)以及继承衍生出来的资源分配规则(多态)就是c++之于c多出来的可以从面向过程转为面向对象的内容。
其他的像模版方法、
这个系列我们将就封装、继承、多态概念来展开,尽可能详尽且底层的将他们的原理性的东西展示出来! 话题内容包括但不限于:
面向对象(OOP)与面向过程编程(POP)相比,封装是其中的一个核心特性。封装不仅仅是将数据和行为捆绑在一起,更是通过隐藏实现细节、限制对数据的直接访问来提供一个更安全、易管理的代码结构。为了理解封装,我们需要逐步深入到它的本质,并在代码层面和理论上解释它。
在OOP中,封装是将数据和方法绑定到一个对象中,并通过控制数据的访问来保证对象内部的一致性和安全性。
封装的基本思想是隐藏内部实现细节,暴露必要的接口。封装有两个主要方面:
在C++中,封装是通过类
和访问修饰符(如public
、private
、protected
)来实现的。
下面是一个简单的C++示例,展示了如何通过封装保护数据和提供接口。
#include <iostream>
using namespace std;
class BankAccount {
private:
double balance; // 余额是私有的,外部不能直接访问
public:
// 构造函数,初始化账户余额
BankAccount(double initialBalance) {
if (initialBalance < 0) {
balance = 0;
} else {
balance = initialBalance;
}
}
// 提供一个公有方法来访问余额
double getBalance() const {
return balance;
}
// 提供一个公有方法来修改余额
void deposit(double amount) {
if (amount > 0) {
balance += amount;
} else {
cout << "Deposit amount must be positive." << endl;
}
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
cout << "Invalid withdrawal amount." << endl;
}
}
};
int main() {
BankAccount account(1000); // 创建一个初始余额为1000的账户
cout << "Initial balance: $" << account.getBalance() << endl;
account.deposit(500); // 存款500
cout << "After deposit: $" << account.getBalance() << endl;
account.withdraw(200); // 提款200
cout << "After withdrawal: $" << account.getBalance() << endl;
// 尝试直接访问balance(会出错)
// cout << "Direct balance access: $" << account.balance << endl; // 编译错误
return 0;
}
balance
,它存储账户余额。外部代码无法直接访问或修改这个余额。withdraw
方法中检查提款金额是否合理,确保余额不被非法提取。从底层的角度看,封装的实现通常依赖于内存布局和访问控制机制。在C++中,类的成员变量通常会在对象实例化时分配内存。通过访问控制(private
、public
)和get
、set
方法,编译器帮助开发者实现了对数据访问的精细控制。
private
和 public
只是影响这些成员在外部代码中的访问方式,实际的内存布局不会变化。
private
、public
和 protected
是由编译器支持的访问权限控制机制,确保类的私有数据只能通过特定的公有方法来修改。编译器会在编译时检查是否有非法访问的代码,防止程序出现不可预期的行为。
封装是面向对象编程的基础,它通过将数据和行为捆绑在一起,并限制外部对数据的访问,来保护对象的内部状态,提供更安全、灵活和易维护的代码结构。通过控制数据的访问和修改,我们能够保证数据的完整性和一致性,同时也能隐藏复杂的实现细节,简化外部接口的使用。
面向对象编程中的继承(Inheritance)是一个非常重要的概念,它允许一个类(子类)继承另一个类(父类)的方法和属性,从而避免代码重复,提高代码的复用性。继承是OOP的三大特性之一,另外两个特性是封装和多态。
继承是一种“is-a”(是一个)关系。例如,假设你有一个基类Animal
,然后你创建一个类Dog
继承自Animal
,那么Dog
就可以看作是Animal
的一个特例,继承了Animal
的一些属性和方法。
Dog
和Cat
都可以继承自Animal
,然后你可以根据需要为Dog
和Cat
添加各自的特殊行为。在C++中,继承通过class
和public
、protected
、private
修饰符来实现。
#include <iostream>
using namespace std;
// 基类(父类)
class Animal {
public:
void speak() {
cout << "Animal speaks!" << endl;
}
void move() {
cout << "Animal moves!" << endl;
}
};
// 派生类(子类)
class Dog : public Animal {
public:
void bark() {
cout << "Dog barks!" << endl;
}
// 重写父类方法
void speak() {
cout << "Dog barks loudly!" << endl;
}
};
int main() {
Animal animal;
animal.speak(); // 调用基类方法
animal.move(); // 调用基类方法
Dog dog;
dog.speak(); // 调用子类重写的方法
dog.move(); // 调用继承的父类方法
dog.bark(); // 调用子类自己的方法
return 0;
}
speak
和 move
,表示动物的行为。Animal
,除了继承 Animal
的 speak
和 move
方法外,Dog
还定义了一个新的方法 bark
,表示狗的行为。Dog
中重写了 speak
方法,使得狗发出的声音与其他动物不同。在底层,继承通过对象布局和指针偏移来实现。每个对象都有一个虚函数表(vtable),用于支持多态(如果使用了虚函数)。当你创建一个子类对象时,它不仅包含自己的数据成员,还会包含父类的数据成员(如果父类有数据成员的话)。
假设有以下类继承关系:
A
是基类,B
是从 A
继承的子类,C
是从 B
继承的子类。内存布局 | 说明 |
---|---|
A 类的成员 | 基类 A 中的成员数据存储在内存中 |
B 类的成员 | 子类 B 扩展的成员数据存储在内存中 |
C 类的成员 | 子类 C 扩展的成员数据存储在内存中 |
继承可以分为不同类型,常见的包括:
#include <iostream>
using namespace std;
// 基类1
class Animal {
public:
void move() {
cout << "Animal moves!" << endl;
}
};
// 基类2
class Mammal {
public:
void nurse() {
cout << "Mammal nurses!" << endl;
}
};
// 派生类
class Dog : public Animal, public Mammal {
public:
void bark() {
cout << "Dog barks!" << endl;
}
};
int main() {
Dog dog;
dog.move(); // 来自 Animal
dog.nurse(); // 来自 Mammal
dog.bark(); // 来自 Dog
return 0;
}
继承是OOP的重要特性,能够通过建立类的层次关系实现代码重用和扩展。它允许子类继承父类的行为和属性,并且能够扩展或修改这些行为。理解继承如何在底层实现、如何利用它来构建高效的程序,是掌握OOP的关键。
多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它允许不同类的对象通过相同的接口(方法名)来调用不同的实现。简单来说,多态使得不同类型的对象可以通过相同的接口执行不同的操作。多态性使得程序更加灵活和可扩展。
多态来源于两个希腊词根:“poly”(多)和“morph”(形态)。在OOP中,多态指的是同一个操作作用于不同类型的对象时,可以有不同的表现形式。最常见的多态形式是方法重写(overriding),即子类可以重写(覆盖)父类的方法。
运行时多态通常通过虚函数来实现。虚函数是基类中声明为 virtual
的函数,子类可以重写这个函数。当通过基类指针或引用调用该函数时,程序会根据对象的实际类型(而不是指针或引用的类型)来决定调用哪个函数实现。
虚函数是在父类中声明的成员函数,并使用 virtual
关键字修饰,表示这个函数可以在子类中被重写。
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
// 虚函数
virtual void speak() {
cout << "Animal speaks!" << endl;
}
virtual ~Animal() {} // 虚析构函数,避免内存泄漏
};
// 派生类
class Dog : public Animal {
public:
void speak() override { // 重写父类的 speak 方法
cout << "Dog barks!" << endl;
}
};
class Cat : public Animal {
public:
void speak() override { // 重写父类的 speak 方法
cout << "Cat meows!" << endl;
}
};
int main() {
// 父类指针指向不同的子类对象
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
// 调用虚函数
animal1->speak(); // 输出: Dog barks!
animal2->speak(); // 输出: Cat meows!
delete animal1; // 释放内存
delete animal2; // 释放内存
return 0;
}
speak
:在 Animal
类中被声明为虚函数,并在 Dog
和 Cat
类中重写了该函数。animal1
和 animal2
是指向 Animal
类型的指针,但它们分别指向 Dog
和 Cat
类型的对象。speak
方法时,C++ 会根据指针实际指向的对象类型来决定调用哪个函数(即 Dog
类的 speak
或 Cat
类的 speak
),这就是运行时多态。编译时多态指的是在编译阶段就可以确定调用哪个函数。编译时多态通常通过函数重载和运算符重载实现。
在同一个类中,可以定义多个同名的函数,只要它们的参数类型或参数个数不同。编译器会根据函数调用时传递的参数来决定调用哪个版本的函数。
#include <iostream>
using namespace std;
class Printer {
public:
// 函数重载
void print(int i) {
cout << "Printing integer: " << i << endl;
}
void print(double d) {
cout << "Printing double: " << d << endl;
}
void print(const char* str) {
cout << "Printing string: " << str << endl;
}
};
int main() {
Printer printer;
printer.print(10); // 输出: Printing integer: 10
printer.print(3.14); // 输出: Printing double: 3.14
printer.print("Hello!"); // 输出: Printing string: Hello!
return 0;
}
Printer
类中,定义了三个同名的 print
函数,但它们的参数类型不同(int
、double
、const char*
)。print
函数,这就是编译时多态。C++允许我们为自定义类型重载运算符,这也是一种编译时多态的表现。
#include <iostream>
using namespace std;
class Complex {
public:
int real;
int imag;
Complex(int r, int i) : real(r), imag(i) {}
// 运算符重载
Complex operator + (const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
void print() {
cout << real << " + " << imag << "i" << endl;
}
};
int main() {
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2; // 使用重载的 + 运算符
c3.print(); // 输出: 4 + 6i
return 0;
}
+
运算符,使其可以对 Complex
类型的对象进行加法操作。c1 + c2
时,编译器会调用重载的 operator +
函数来执行加法运算。多态的底层实现依赖于虚函数表(vtable)。每个包含虚函数的类,在编译时会生成一个虚函数表,其中存储着类的所有虚函数指针。当通过父类指针调用虚函数时,程序会查找虚函数表,找到对应的子类实现并调用。
多态是面向对象编程的核心特性之一,它通过相同的接口执行不同的实现。多态主要分为两种类型:
多态使得代码更加灵活和可扩展,有助于构建更易于维护和扩展的程序架构。
其他文章的链接还在编译中……
欢迎关注、点赞、收藏!更多系列内容可以点击专栏目录订阅,感谢支持,再次祝大家祉猷并茂,顺遂无虞!
若将文章用作它处,请一定注明出处,商用请私信联系我!