面向对象编程
面向对象编程
一、封装
1. 封装的概念与作用
封装是面向对象编程的核心特性之一,指将数据(属性)和操作数据的方法(函数)封装在一个类中,通过访问控制机制隐藏内部实现细节,仅对外暴露必要的接口。其核心作用包括:
- 信息隐藏:保护数据不被外部直接修改,避免程序逻辑混乱
- 数据一致性:通过方法控制数据的读写,确保数据符合业务规则
- 模块解耦:类作为独立模块,降低系统各部分的依赖关系
2. C++中的封装实现
(1)访问修饰符
C++通过public
、private
、protected
关键字实现封装:
- public:成员可被类内外任意代码访问(对外接口)
- private:成员仅能被类内成员函数访问(内部实现)
- protected:成员可被类内及子类成员函数访问(继承场景)
(2)属性封装示例
以下是一个学生类的封装示例,演示如何隐藏数据并提供安全的访问接口:
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string name; // 姓名(私有属性)
int age; // 年龄(私有属性)
double score; // 成绩(私有属性)
public:
// 构造函数
Student(string n, int a, double s) : name(n), age(a), score(s) {}
// 获取姓名(公共接口)
string getName() const {
return name;
}
// 设置姓名(公共接口,带业务逻辑校验)
void setName(string n) {
if (n.empty()) {
cout << "姓名不能为空!" << endl;
return;
}
name = n;
}
// 获取年龄(带范围校验)
int getAge() const {
return age;
}
// 设置年龄(带业务逻辑校验)
void setAge(int a) {
if (a < 0 || a > 150) {
cout << "年龄范围不合法!" << endl;
return;
}
age = a;
}
// 获取成绩
double getScore() const {
return score;
}
// 设置成绩(带业务逻辑校验)
void setScore(double s) {
if (s < 0 || s > 100) {
cout << "成绩范围应在0-100之间!" << endl;
return;
}
score = s;
}
// 显示学生信息(公共方法)
void display() const {
cout << "姓名:" << name << ",年龄:" << age
<< ",成绩:" << score << endl;
}
};
int main() {
// 创建学生对象
Student stu("张三", 18, 85.5);
stu.display(); // 正常显示
// 尝试直接修改私有属性(编译错误)
// stu.age = 200; // 错误:无法访问private成员
// 通过公共接口修改属性
stu.setAge(20);
stu.setScore(90.0);
stu.display(); // 显示修改后信息
// 测试业务逻辑校验
stu.setAge(-5); // 年龄不合法
stu.setScore(120); // 成绩不合法
return 0;
}
(3)封装的设计原则
- 单一职责原则:一个类只负责一项核心功能
- 接口隔离原则:对外接口应细化,避免大而全的接口
- 迪米特法则(最少知识原则):类间交互应尽量简单,不暴露内部细节
二、继承
1. 继承的概念与作用
继承是面向对象编程中类与类之间的一种关系,指子类(派生类)可以继承父类(基类)的属性和方法,从而复用代码并扩展功能。其核心作用包括:
- 代码复用:避免重复实现基类已有的功能
- 层次化设计:建立类的继承体系,体现"is-a"关系
- 多态基础:为多态特性提供语法前提
2. C++中的继承语法
1)继承的基本语法
class 子类名 : [继承方式] 基类名 {
// 子类成员声明
};
继承方式包括:
public
:基类的public
/protected
成员在子类中保持原有访问权限private
:基类的public
/protected
成员在子类中变为private
protected
:基类的public
/protected
成员在子类中变为protected
(2)单继承示例
以下是Person类与Student类的单继承示例:
// 06_inheritance.cpp
#include <iostream>
#include <string>
using namespace std;
// 基类:Person(人)
class Person {
protected:
string name; // 姓名(protected允许子类访问)
int age; // 年龄
public:
Person(string n, int a) : name(n), age(a) {}
void displayBasicInfo() const {
cout << "姓名:" << name << ",年龄:" << age << endl;
}
string getName() const {
return name;
}
void setAge(int a) {
age = a;
}
};
// 子类:Student(学生),public继承Person
class Student : public Person {
private:
string studentId; // 学号
double score; // 成绩
public:
// 子类构造函数需显式调用基类构造函数
Student(string n, int a, string id, double s)
: Person(n, a), studentId(id), score(s) {}
// 扩展基类功能:显示学生完整信息
void displayFullInfo() const {
displayBasicInfo(); // 调用基类方法
cout << "学号:" << studentId << ",成绩:" << score << endl;
}
// 子类特有的方法
void setScore(double s) {
if (s < 0 || s > 100) {
cout << "成绩范围不合法!" << endl;
return;
}
score = s;
}
};
int main() {
// 创建子类对象
Student stu("李四", 20, "S001", 88.5);
// 访问基类继承的方法
stu.displayBasicInfo(); // 调用基类方法
// 访问子类扩展的方法
stu.displayFullInfo(); // 调用子类方法
// 测试继承关系:子类对象可赋值给基类指针(向上转型)
Person* p = &stu; // 基类指针指向子类对象
p->displayBasicInfo(); // 调用基类方法(此时无法访问子类特有方法)
return 0;
}
3. 继承中的构造与析构顺序
- 构造顺序:先构造基类,再构造子类
- 析构顺序:先析构子类,再析构基类(与构造顺序相反)
// 07_constructor_destructor.cpp
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base构造函数" << endl; }
~Base() { cout << "Base析构函数" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived构造函数" << endl; }
~Derived() { cout << "Derived析构函数" << endl; }
};
int main() {
Derived d; // 输出顺序:Base构造 → Derived构造
return 0; // 输出顺序:Derived析构 → Base析构
}
4. 多继承与钻石问题
(1)多继承语法
C++允许一个子类继承多个基类:
class 子类 : public 基类1, public 基类2 {
// 成员声明
};
(2)钻石问题与虚基类
当多个基类继承自同一个祖先类时,会导致子类中出现多份祖先类成员,称为"钻石问题"。通过virtual
关键字声明虚基类可解决该问题:
// 08_diamond_problem.cpp
#include <iostream>
using namespace std;
// 虚基类:Animal
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
};
// 派生类:Bird和Mammal均虚继承Animal
class Bird : virtual public Animal {
public:
Bird(string n) : Animal(n) {}
};
class Mammal : virtual public Animal {
public:
Mammal(string n) : Animal(n) {}
};
// 多继承:Platypus同时继承Bird和Mammal
class Platypus : public Bird, public Mammal {
public:
Platypus(string n) : Animal(n), Bird(n), Mammal(n) {}
void display() {
cout << "动物名称:" << name << endl;
}
};
int main() {
Platypus p("鸭嘴兽");
p.display(); // 正确访问name(仅一份实例)
return 0;
}
5. 继承与组合的选择
- 继承(is-a关系):适用于子类"是一种"基类的场景(如Student是一种Person)
- 组合(has-a关系):适用于类"包含"另一个类的场景(如Car包含Engine)
// 组合示例:Car类包含Engine类
class Engine {
public:
void start() { cout << "引擎启动" << endl; }
};
class Car {
private:
Engine engine; // 组合Engine对象
public:
void startCar() {
engine.start(); // 通过组合对象调用方法
}
};
6. 继承中的访问控制总结
基类成员访问权限 | public继承 | private继承 | protected继承 |
---|---|---|---|
public | public | private | protected |
protected | protected | private | protected |
private | 不可访问 | 不可访问 | 不可访问 |
通过合理设计继承方式和访问修饰符,可灵活控制成员的可见范围,实现代码复用与封装的平衡。
三、多态
1. 函数重写和多态的概念
函数重写(虚函数覆盖)
如果将基类中的某个成员函数声明为虚函数,那么子类中与该函数具有相同原型的成员函数就也是虚函数,并且对基类中版本形成覆盖,即函数重写。多态的概念
如果子类提供了对基类虚函数有效的覆盖,那么通过 指向子类对象 的 基类指针(指针指向的实际上是子类对象,但指针的类型是基类(向上造型)),或者通过引用子类对象基类引用,调用该虚函数,实际被执行将是子类中的覆盖版本,而不再是基类中原始版本,这种语法现象被称为多态。多态的重要意义
多态的重要意义在于,一般情况下,调用哪个类的成员函数是由调用者指针或引用本身的类型决定的,而当多态发生时,调用哪个类的成员函数则由调用者指针或引用的实际目标对象的类型决定。这样一来,源自同一种类型的同一种激励,竟然可以产生多种不同的响应,也就是对于同一个函数调用,能够表达出不同的形态,即为多态。
//01shape.cpp
#include <iostream>
using namespace std;
class Shape{ //图形基类
public:
Shape(int x,int y):m_x(x),m_y(y){}
//void draw(void){
virtual void draw(void){ //虚函数
cout << "绘制图形: " << m_x << ',' << m_y << endl;
}
protected:
int m_x; //位置坐标
int m_y;
};
class Rect:public Shape{ //矩形
public:
Rect(int x,int y,int w,int h):Shape(x,y),m_w(w),m_h(h){}
void draw(void){ //自动变 虚函数
cout << "绘制矩形:" << m_x << ',' << m_y << ',' << m_w << ',' << m_h << endl;
}
private:
int m_w; //宽和高
int m_h;
};
//子类不写draw(),也可从基类中继承。
//子类写了draw(),会自动变虚函数,称为函数重写、虚函数覆盖。虚函数是产生多态的前提。
class Circle:public Shape{ //圆形
public:
Circle(int x,int y,int r):Shape(x,y),m_r(r){}
void draw(void){ //自动变 虚函数
cout << "绘制圆形:" << m_x << ',' << m_y << ',' << m_r << endl;
}
private:
int m_r; //半径
};
void render(Shape* buf[]){
for(int i=0;buf[i]!=NULL;i++)
buf[i]->draw(); //虚函数draw()会根据指针的类型,自动选择相应的函数调用。(多态)
}
int main(void){
Shape* buf[1024] = {NULL}; //类似于缓冲区
buf[0] = new Rect(1,2,3,4); //暗中向上造型了 Rect/Circle ==> Shape
buf[1] = new Rect(5,6,7,8);
buf[2] = new Circle(10,11,9);
render(buf); //存在的问题:调用的都是基类Shape的draw() //解决办法:虚函数--多态。
return 0;
}
2. 虚函数覆盖的条件
- 函数重写的要求(虚函数覆盖的条件)
只有类中的成员函数才能声明为虚函数,而全局函数、静态成员函数、构造函数都不能被声明为虚函数。(还有一个特殊的析构函数,后面会讲。)
只有在基类中以virtual关键字声明的虚函数,才能作为虚函数被子类覆盖,而与子类中的virtual关键字无关。
虚函数在子类中的版本和基类中版本要具有相同的函数签名,即函数名、参数表、常属性一致。
如果基类虚函数返回基本类型的数据,那么子类中的版本必须返回相同类型的数据。
如果基类虚函数返回 类类型 指针(A*)或引用(A&),那么允许子类中的版本返回 其子类类型 指针(B*)或引用(B&)
//02 polymorphic.cpp
#include <iostream>
using namespace std;
class A{};
class B:public A{};
class Base{
public:
virtual int func(int x = 0)const{ //基类必须加上virtual
cout << "Base::func" << endl;
return 666;
}
virtual A* foo(void){
cout << "Base::foo" << endl;
}
};
class Derived:public Base{
public:
virtual int func(int y = 0)const{ //子类加与不加virtual均可
cout << "Derived::func" << endl;
return 888;
}
B* foo(void){
cout << "Derived::foo" << endl;
}
};
int main(void){
Derived d;
Base* pb = &d; //pb称为指向子类对象的基类指针
pb->func();
pb->foo();
}
//函数重写的条件(虚函数覆盖的条件)
//1)基类必须加virtual
//2)函数名、参数表(与形参名字无关)、常属性相同
//3)基本类型的返回类型必须相同,否则编译出错。
// 基类虚函数 返回 A类类型 的指针或引用,子类版本中 可以返回 A的子类类型 的指针或引用
//回顾知识点:子类隐藏基类的成员,通过子类对象访问同名的成员时将优先访问子类自己的。
3. 多态的条件
- 产生多态语法的条件
多态的语法特性除了要满足函数重写的语法要求,还必须是通过指针或引用调用虚函数,才能表现出来。
调用虚函数的指针也可以是this指针,当使用子类对象调用基类中的成员函数时,该函数里面this指针将是一个指向子类对象的基类指针,再通过this去调用满足重写要求的虚函数同样可以表现多态的语法特性。
//03polymorphic.cpp
#include <iostream>
using namespace std;
class Base{
public:
Base(void){ //Derived d; 构造子类的过程:先构造基类子对象,再执行子类构造函数
this->func(); //执行基类构造函数的时候,子类对象还没有创建完成,仅能表现出基类的外观和行为。无法实现多态。
}
~Base(void){ //先执行子类对象的析构,再执行基类的析构函数。
this->func(); //执行基类的析构函数时,子类对象已经不是子类类型了仅能表现出基类的外观和行为。无法实现多态。
}
virtual void func(void){
cout << "Base::func" << endl;
}
void foo(void){ //void foo(Base* this)
func(); //this->func()
}
};
class Derived:public Base{
public:
void func(void){
cout << "Derived::func" << endl;
}
};
int main(void){
Derived d;
Base b = d; //用子类对象拷贝构造得到基类对象
b.func(); //直接用对象调用不会产生多态语法
Base& c = d;
c.func(); //指针、引用调用虚函数就可以产生多态。
//通过this指针在普通成员函数中调用虚函数可以产生多态语法
d.foo(); //foo(&d) 子类对象调用基类中的成员函数,这时的this指针指向子类对象。
//此时的调用相当于 void foo(Base* this = &d) this->func()
//重点掌握,绝大数多态都是通过this指针实现的。
//注:子类虽然可以继承基类的func函数,但是本质上func函数的代码只有一份,且在基类中,子类只是可以使用。
// 对于一个类来说,里面的this指针一定是当前类的类型。基类Base中的this指针一定是Base类型,但是调用对象可以是子类对象。
// 当调用对象是子类对象的时候,this指针就是一个指向子类Derived对象的基类类型Base指针,再通过它调用虚函数就可以体现多态语法。
return 0;
}
4. 纯虚函数和抽象类
- 纯虚函数
只如果一个虚函数仅表达抽象的行为,没有具体的功能,即只有声明没有定义,这样的虚函数被称为纯虚函数或抽象方法,其语法特性如下:
class 类名 {
public:
virtual 返回类型 函数名 (形参表) = 0;
};
//如01shape.cpp中基类Shape中的draw()函数,没有任何具体功能,仅仅表示抽象的绘图行为,真正绘图需要调用的是子类Circle、Rect中的draw()函数。因此对于该函数只要它的定义,作为多态的接口来使用,这种函数称为纯虚函数。纯虚函数因为仅仅作为接口使用,不需要其实现部分,可以将它的函数体去掉。但是直接去掉花括号,加分号改为声明,会报错。因此引入了在最后加上=0的语法。
class Shape{ //图形基类
public:
Shape(int x,int y):m_x(x),m_y(y){}
//virtual void draw(void){ //虚函数
// cout << "绘制图形: " << m_x << ',' << m_y << endl;
//}
virtual void draw(void)=0; //纯虚函数(纯粹表达虚拟的功能,没有具体实现)
protected:
int m_x; //位置坐标
int m_y;
};
抽象类和纯抽象类
如果类中包含了纯虚函数,那么这个类就是抽象类。
如果类中的所有的普通成员函数都是纯虚函数则可以称为纯抽象类。抽象类往往用来表示在对问题进行分析、设计的过程中所得出的抽象概念,是对一系列看上去不同,但本质上相同的具体概念的抽象描述
注:无论是直接定义,还是通过new运算符,抽象类永远不能实例化为对象。
即Shape类保护纯虚函数,是抽象类,抽象类不能实例化。
Shape s(1,2); //error
Shape* ps = new Shape(1,2); //error 栈、堆区都不能
5. 虚析构函数
注:前面所讲的虚函数,针对的是类中的普通成员函数,不包括构造函数、静态成员函数、全局函数,但有一个特殊的,是虚析构函数,用来解决之前案例中提到的内存泄漏问题。
·基类的析构函数不会自动调用子类的析构函数,所以当delete一个指向子类对象的基类指针,实际被执行的仅是基类的析构函数,子类的析构函数不会被执行,有内存泄漏的风险
class A{ ~A(void){ 基类的析构函数 } };
class B:public A{ ~B(void){ 子类的析构函数 } };
A* pa = new B;//pa即为指向子类对象的基类指针
delete pa;//仅会调用基类的析构函数,有内存泄漏风险.
·如果将基类的析构函数声明为虚函数,那么子类的析构函数就也是虚函数,并且可以对基类的析构函数形成有效的覆盖,也可以表现多态的语法特性。
·这时再delete一个指向子类对象的基类指针,实际被执行的将是子类的析构函数,子类的析构函数在执行结束后又会自动的调用基类的析构函数,避免了内存泄漏。
class A{ virtual ~A(void){ 基类的析构函数 } };
class B:public A{ ~B(void){ 子类的析构函数 } };
A* pa = new B;//pa即为指向子类对象的基类指针
delete pa;//实际调用子类的析构函数,避免了内存泄漏
//04polymorphic.cpp
#include <iostream>
using namespace std;
class Base{
public:
Base(void){
cout << "基类动态资源分配" << endl;
}
virtual ~Base(void){ //虚析构函数
cout << "基类动态资源释放" << endl;
}
};
class Derived:public Base{
public:
Derived(void){
cout << "子类动态资源分配" << endl;
}
~Derived(void){ //基类的析构函数变虚,子类析构函数自动变虚,可以对基类版本进行覆盖,体现多态
cout << "子类动态资源释放" << endl; //与之前的普通虚函数覆盖(重写)略有差别,之前要求函数名必须相同,但是虚析构函数没有这个要求,因为析构函数名不然不同。
}
};
int main(void){
Base* pb = new Derived; //向上造型 //基类动态资源分配、子类动态资源分配
//...模拟使用对象的一系列操作...//
delete pb; //pb->析构函数 //基类动态资源释放 内存泄漏
//解决办法1:向下造型(不好)
//解决方法2:基类析构函数前加上virtual修饰,变成虚析构函数。实现多态语法。
//子类动态资源释放 基类动态资源释放 (执行子类的版本,子类析构函数执行完,可以自动调基类的析构函数。)
return 0;
}