Appearance
第6章 面向对象编程基础
🎯 本章学习目标
通过本章的学习,你将能够:
- 理解面向对象思想:知道面向对象 vs 面向过程的区别
- 掌握类与对象:理解类是模板、对象是实例,能定义类和创建对象
- 理解封装:掌握访问控制(public/private/protected)和封装的意义
- 掌握构造与析构:理解构造函数和析构函数的作用和调用时机
- 理解继承:掌握继承的概念,知道子类如何复用父类代码
- 理解多态:掌握虚函数、重写、动态绑定的概念
- 掌握this指针:理解this指针的作用和用法
- 区分访问权限:清楚public/private/protected的区别和继承时的变化
- 理解构造顺序:知道继承时构造函数和析构函数的调用顺序
- 应对真题题型:能解决选择、判断、代码填空等各类OOP题目
🗺️ 本章知识图谱
面向对象编程基础
├── 基本概念
│ ├── 面向过程 vs 面向对象
│ ├── 类(Class)= 模板
│ ├── 对象(Object)= 实例
│ └── 三大特性:封装、继承、多态
├── 封装
│ ├── 访问控制
│ │ ├── public(公开)
│ │ ├── private(私有,默认)
│ │ └── protected(保护)
│ ├── 成员变量(属性)
│ ├── 成员函数(方法)
│ ├── getter/setter
│ └── 意义:隐藏实现,暴露接口
├── 构造与析构
│ ├── 构造函数
│ │ ├── 对象创建时自动调用
│ │ ├── 默认构造(无参)
│ │ ├── 有参构造
│ │ └── 初始化列表
│ └── 析构函数
│ ├── 对象销毁时自动调用
│ ├── ~类名()
│ └── 释放资源
├── 继承
│ ├── 定义:子类复用父类的属性和方法
│ ├── class 子类 : 继承方式 父类
│ ├── 继承方式
│ │ ├── public 继承(最常用)
│ │ ├── private 继承
│ │ └── protected 继承
│ ├── 构造顺序:父→子
│ └── 析构顺序:子→父
├── 多态
│ ├── 定义:同一接口,不同实现
│ ├── 条件
│ │ ├── virtual 虚函数
│ │ ├── 子类重写(override)
│ │ └── 父类指针/引用指向子类对象
│ ├── 动态绑定(运行时决定)
│ └── 虚函数表 vtable
├── this 指针
│ ├── 指向当前对象
│ ├── 区分同名变量(成员变量 vs 局部变量)
│ └── 链式调用(return *this)
└── 综合应用
├── 类的设计
├── 对象协作
└── 代码复用一、面向对象 vs 面向过程:一句话理解
| 思维方式 | 核心思想 | 比喻 |
|---|---|---|
| 面向过程 | 按步骤解决问题,关注"怎么做" | 自己做蛋炒饭:洗锅→切葱→打蛋→翻炒 |
| 面向对象 | 把问题拆解为多个对象协作,关注"谁来做" | 点外卖:厨师对象负责炒菜,骑手对象负责配送 |
一句话总结:面向过程是"我一步步做",面向对象是"我让不同的对象各干各的"。
二、类与对象
2.1 什么是类和对象
- 类(Class):一种用户自定义的数据类型,是一组具有相同属性和行为的对象的"模板"或"图纸"。
- 对象(Object):类的具体实例,是真实存在的个体。
cpp
// 定义一个 Student 类
class Student {
public:
string name; // 成员变量(属性)
int age; // 成员变量(属性)
void study() { // 成员函数(行为)
cout << name << "正在学习" << endl;
}
};
int main() {
Student s1; // 创建对象 s1
s1.name = "小明";
s1.age = 17;
s1.study(); // 输出:小明正在学习
return 0;
}2.2 访问控制:public / private / protected
| 访问修饰符 | 类内部 | 类外部(通过对象) | 派生类 |
|---|---|---|---|
public | ✅ 可访问 | ✅ 可访问 | ✅ 可访问 |
private | ✅ 可访问 | ❌ 不可访问 | ❌ 不可访问 |
protected | ✅ 可访问 | ❌ 不可访问 | ✅ 可访问 |
⚠️ 高频易错点:C++ 中 class 的成员默认是 private,而 struct 的成员默认是 public。这是考试常考的选择题陷阱!
2.3 构造函数
构造函数在创建对象时自动调用,用于初始化成员变量。特点:函数名与类名相同,无返回值类型。
cpp
class Student {
private:
string name;
int age;
public:
// ① 默认构造函数(无参)
Student() {
name = "未知";
age = 0;
cout << "默认构造" << endl;
}
// ② 有参构造函数
Student(string n, int a) {
name = n;
age = a;
cout << "有参构造" << endl;
}
// ③ 拷贝构造函数(用已有对象初始化新对象)
Student(const Student &other) {
name = other.name;
age = other.age;
cout << "拷贝构造" << endl;
}
};
int main() {
Student s1; // 调用默认构造
Student s2("小红", 18); // 调用有参构造
Student s3(s2); // 调用拷贝构造(复制 s2)
return 0;
}⚠️ 高频易错点:
- 如果你定义了任意构造函数(如有参构造),编译器不再自动生成默认构造函数。
- 拷贝构造函数的参数必须是引用
const Student &other,不能是值传递,否则会无限递归!
2.4 析构函数
析构函数在对象销毁时自动调用,用于释放资源。特点:函数名为 ~类名,无参数,无返回值。
cpp
class Student {
public:
Student() { cout << "构造" << endl; }
~Student() { cout << "析构" << endl; } // 析构函数
};考试要点:局部对象在离开作用域时析构;
new创建的对象需要delete来手动析构。
三、封装
3.1 为什么要封装
封装的核心目的:隐藏内部实现细节,只暴露必要的接口。
类比:你用遥控器看电视,不需要知道电视内部电路怎么工作,只需要按一下按钮就行。
3.2 如何实现封装
用 private 隐藏数据 + 提供 public 的 getter/setter 接口:
cpp
class BankAccount {
private:
double balance; // 余额:不允许外部直接访问
public:
BankAccount(double b) : balance(b) {}
// getter:读取余额
double getBalance() const {
return balance;
}
// setter:存取款操作,加入合法性检查
void deposit(double amount) {
if (amount > 0) balance += amount;
else cout << "存款金额必须大于0" << endl;
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance)
balance -= amount;
else
cout << "取款失败" << endl;
}
};封装的好处:外部无法直接将余额改为负数或随意篡改,必须通过我们提供的方法来操作,保证了数据的安全性。
四、继承
4.1 什么是继承
继承允许一个类(派生类/子类)获得另一个类(基类/父类)的属性和方法,实现代码复用。
4.2 单继承语法
cpp
class Animal { // 基类(父类)
protected:
string name;
public:
Animal(string n) : name(n) {
cout << "Animal构造" << endl;
}
void eat() {
cout << name << "在吃东西" << endl;
}
};
class Dog : public Animal { // 派生类(子类),public继承
private:
int age;
public:
Dog(string n, int a) : Animal(n), age(a) { // 调用父类构造函数
cout << "Dog构造" << endl;
}
void bark() {
cout << name << "在汪汪叫" << endl;
}
};
int main() {
Dog d("旺财", 3); // 输出:Animal构造 → Dog构造
d.eat(); // 继承自父类:旺财在吃东西
d.bark(); // 子类自己的方法:旺财在汪汪叫
return 0;
}4.3 构造函数调用顺序(必考!)
创建派生类对象时,构造函数的调用顺序:
- 先调用父类构造函数(按继承顺序)
- 再调用成员对象的构造函数(按声明顺序)
- 最后执行派生类构造函数体
析构顺序与构造相反:先析构子类,再析构父类。
⚠️ 高频易错点:如果父类没有默认构造函数(只有有参构造),子类构造函数必须在初始化列表中显式调用父类的有参构造函数,否则编译报错!
4.4 protected 的作用
protected 成员对外部是不可见的,但对派生类是可访问的。在继承体系中非常实用——既能保护数据不被外部随意修改,又允许子类访问。
五、多态
5.1 什么是多态
多态:同一个函数调用,作用于不同类型的对象,产生不同的行为。
简单说:父类指针指向子类对象时,调用被重写的虚函数,会根据实际对象类型执行对应的版本。
5.2 虚函数与函数重写
cpp
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
// 虚函数:用 virtual 声明
virtual void speak() {
cout << name << "发出声音" << endl;
}
};
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
// 重写(override)父类的虚函数
void speak() override {
cout << name << "说:汪汪!" << endl;
}
};
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void speak() override {
cout << name << "说:喵喵!" << endl;
}
};5.3 动态绑定(晚绑定)
cpp
int main() {
Animal *p1 = new Dog("旺财");
Animal *p2 = new Cat("咪咪");
p1->speak(); // 输出:旺财说:汪汪!
p2->speak(); // 输出:咪咪说:喵喵!
delete p1;
delete p2;
return 0;
}虽然 p1、p2 都是 Animal* 类型,但程序在运行时会根据实际指向的对象类型来调用对应的 speak() 版本——这就是动态绑定。
⚠️ 关键条件:
- 父类函数必须声明为
virtual- 子类必须重写(函数名、参数列表、返回值类型与父类相同)
- 必须通过指针或引用调用,才能触发多态
⚠️ 如果没有 virtual:即使子类重写了函数,通过父类指针调用时只会执行父类版本(静态绑定),不会有多态效果!
5.4 虚函数表(了解概念)
- 每个含有虚函数的对象内部都有一个虚函数表指针(vptr),指向该类的虚函数表(vtable)。
- 虚函数表记录了该类所有虚函数的地址。
- 运行时,程序通过 vptr 查 vtable,找到正确的函数地址来调用——这就是动态绑定的底层机制。
| 场景 | 调用哪个版本 |
|---|---|
| 没有 virtual | 看指针/引用类型(静态绑定) |
| 有 virtual | 看实际对象类型(动态绑定) |
六、this 指针
this 是一个隐含在每个非静态成员函数内部的指针,指向调用该函数的当前对象。
cpp
class Student {
private:
string name;
public:
Student(string name) {
this->name = name; // 区分参数 name 和成员变量 name
}
void printName() {
cout << this->name << endl; // this->name 等价于成员变量 name
}
Student& setAge(int age) {
// 链式调用:返回 *this
return *this;
}
};this 指针的常见用途:
- 区分同名:当成员变量与参数同名时,用
this->xxx表示成员变量 - 返回自身引用:支持链式调用(如
obj.setA(1).setB(2))
七、综合示例:图形计算器
下面是一个完整示例,综合展示类、继承、多态:
cpp
#include <iostream>
#include <string>
using namespace std;
// ========== 基类 ==========
class Shape {
protected:
string name;
public:
Shape(string n) : name(n) {
cout << "[构造] " << name << endl;
}
virtual ~Shape() {
cout << "[析构] " << name << endl;
}
// 纯虚函数,使 Shape 成为抽象类
virtual double area() const = 0;
void display() const {
cout << name << " 的面积 = " << area() << endl;
}
string getName() const { return name; }
};
// ========== 派生类:圆形 ==========
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : Shape("圆形"), radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
// ========== 派生类:矩形 ==========
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : Shape("矩形"), width(w), height(h) {}
double area() const override {
return width * height;
}
};
// ========== 主函数 ==========
int main() {
// 用父类指针数组管理不同形状
Shape *shapes[3];
shapes[0] = new Circle(5.0);
shapes[1] = new Rectangle(4.0, 6.0);
shapes[2] = new Circle(3.0);
cout << "===========" << endl;
// 多态:同一调用,不同行为
for (int i = 0; i < 3; i++) {
shapes[i]->display();
}
// 释放内存(触发析构)
cout << "===========" << endl;
for (int i = 0; i < 3; i++) {
delete shapes[i];
}
return 0;
}
/* 输出结果:
[构造] 圆形
[构造] 矩形
[构造] 圆形
===========
圆形 的面积 = 78.5398
矩形 的面积 = 24
圆形 的面积 = 28.2743
===========
[析构] 圆形
[析构] 矩形
[析构] 圆形
*/这个示例涵盖了:
- ✅ 类的定义(成员变量、成员函数、访问控制)
- ✅ 构造函数与析构函数
- ✅ 继承(Circle/Rectangle 继承自 Shape)
- ✅ 多态(
area()通过父类指针调用,实际执行子类版本);display()本身不是虚函数,始终执行Shape::display(),但它在内部调用的虚函数area()会根据实际对象类型动态绑定。 - ✅ 虚函数(
virtual area())与纯虚函数(= 0) - ✅ protected 成员在子类中的使用
八、高频易错点 & 考试常见考法汇总
📝 选择题高频考点
| 考法 | 要点 |
|---|---|
| 继承语法判断 | class Dog : public Animal 的正确写法;注意冒号和继承方式 |
| 构造函数调用顺序 | 创建子类对象:父类构造 → 成员对象构造 → 子类构造体 |
| 析构函数调用顺序 | 与构造相反:子类析构 → 成员对象析构 → 父类析构 |
| 虚函数调用结果 | 有 virtual + 指针/引用调用 → 动态绑定;否则看指针类型 |
| 默认访问权限 | class 默认 private,struct 默认 public |
| 拷贝构造触发时机 | 值传递参数、用对象初始化新对象、函数返回对象(值返回) |
| 纯虚函数与抽象类 | 含有纯虚函数(= 0)的类不能实例化 |
| this 指针 | 只存在于非静态成员函数中;静态成员函数没有 this |
❌ 常见错误代码
cpp
// 错误1:父类无默认构造,子类未显式调用
class Base {
public:
Base(int x) {}
};
class Derived : public Base {
public:
Derived() {} // ❌ 编译错误!必须写 Derived() : Base(0) {}
};
// 错误2:忘记 virtual,多态失效
class Base {
public:
void show() { cout << "Base"; } // 缺少 virtual!
};
class Derived : public Base {
public:
void show() { cout << "Derived"; }
};
// Base *p = new Derived();
// p->show(); // 输出 Base(不是 Derived!因为没有 virtual)
// 错误3:拷贝构造函数值传递导致递归
class A {
public:
A(A other) {} // ❌ 必须用引用:A(const A &other)
};九、📝 实战练习
练习题 1(程序阅读题)
题目: 阅读以下 C++ 代码,程序的输出结果是?
cpp
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "A"; }
~Base() { cout << "a"; }
};
class Derived : public Base {
public:
Derived() { cout << "B"; }
~Derived() { cout << "b"; }
};
int main() {
Derived d;
return 0;
}A. ABab B. ABba C. BAab D. BAba
🔑 点击查看答案与解析
答案: B
**解析:**创建 Derived 对象时,先调用父类 Base 构造函数输出 A,再调用子类构造函数输出 B。程序结束时析构顺序与构造相反,先调用子类析构输出 b,再调用父类析构输出 a。所以输出为 ABba。
考点:构造函数和析构函数的调用顺序。
练习题 2(程序阅读题)
题目: 阅读以下 C++ 代码,程序的输出结果是?
cpp
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() { cout << "???"; }
};
class Dog : public Animal {
public:
void speak() { cout << "汪"; }
};
class Cat : public Animal {
public:
void speak() { cout << "喵"; }
};
int main() {
Animal *arr[2];
arr[0] = new Dog();
arr[1] = new Cat();
for (int i = 0; i < 2; i++) {
arr[i]->speak();
}
return 0;
}A. ?? B. 汪喵 C. ??? D. 编译错误
🔑 点击查看答案与解析
答案: B
解析:Animal 类的 speak() 被声明为 virtual(虚函数)。Dog 和 Cat 分别重写了 speak()。通过父类指针 Animal* 调用 speak() 时,触发动态绑定,根据实际对象类型执行对应的函数版本。arr[0] 指向 Dog 对象,输出"汪";arr[1] 指向 Cat 对象,输出"喵"。
考点:虚函数与多态机制。
练习题 3(填空题)
题目: 请补全以下代码中缺失的部分(标记为 ______),使程序输出 Tom is 20 years old:
cpp
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n), age(a) {}
string getName() const { return ______; } // (1)
int getAge() const { return ______; } // (2)
};
int main() {
Person p("Tom", 20);
cout << p.getName() << " is " << p.getAge() << " years old" << endl;
return 0;
}🔑 点击查看答案与解析
练习题三 答案:
- (1)
name - (2)
age
解析:getName() 和 getAge() 是 Person 类的成员函数,可以直接访问类的私有成员变量 name 和 age。这是封装的典型用法:通过 public 的 getter 函数来访问 private 的数据成员。
考点:封装——通过 getter 访问私有成员。
练习题 4(单选题)
题目: 面向对象编程的三大特性是?( )
A. 封装、继承、多态
B. 封装、抽象、重载
C. 继承、多态、模板
D. 封装、继承、友元
🔑 点击查看答案与解析
答案: A
解析: 面向对象的三大特性:
- 封装:把数据和操作打包,隐藏内部实现,只暴露接口
- 继承:子类继承父类的属性和方法,实现代码复用
- 多态:同一接口,不同实现(通过虚函数实现)
选 A。
💡 记忆口诀:封装是"遮起来",继承是"拿过来",多态是"变样子"。
练习题 5(判断题)
题目: 在 C++ 的 class 中,成员的默认访问权限是 public。( )
🔑 点击查看答案与解析
答案: 错误 ❌
解析: C++ 中 class 的默认访问权限是 private(私有),不是 public。
- class:默认 private
- struct:默认 public(C++ 中 struct 和 class 几乎一样,只是默认访问权限不同)
🔥 高频考点:这是选择题和判断题的常客。记住:class 默认私有,struct 默认公有。
练习题 6(单选题)
题目: 当创建一个子类对象时,构造函数和析构函数的调用顺序是?( )
A. 构造:先父后子;析构:先子后父
B. 构造:先子后父;析构:先父后子
C. 构造:先父后子;析构:先父后子
D. 构造:先子后父;析构:先子后父
🔑 点击查看答案与解析
答案: A
解析:
- 构造函数:先调用父类构造,再调用子类构造(先有父亲才有孩子)
- 析构函数:先调用子类析构,再调用父类析构(孩子先走,父亲断后)
可以这样记:构造:父→子;析构:子→父。
选 A。
🍎 生活比喻:建房子先打地基(父类),再盖上层(子类);拆房子从上往下拆(先拆子类),最后拆地基(父类)。
练习题 7(单选题)
题目: 下列哪个不是 C++ 实现多态的必要条件?( )
A. 基类函数声明为 virtual
B. 子类重写(override)基类的虚函数
C. 用基类指针或引用指向子类对象
D. 函数必须是 public 的
🔑 点击查看答案与解析
答案: D
解析: C++ 多态的三个必要条件:
- ✅ 基类函数声明为 virtual(虚函数)
- ✅ 子类重写(override)该虚函数
- ✅ 用基类指针/引用指向子类对象
三者缺一不可!
选项 D:函数不一定必须是 public 的,private 的虚函数也可以实现多态(虽然不常见)。所以 D 不是必要条件。
选 D。
💡 多态 = virtual + 重写 + 父类指针/引用。三个条件都满足时,调用哪个版本在运行时才决定(动态绑定)。
练习题 8(单选题)
题目: 关于 this 指针,下列说法错误的是?( )
A. this 指针指向当前对象
B. this 指针可以用来区分成员变量和同名的局部变量
C. 静态成员函数也有 this 指针
D. 链式调用(返回 *this)是 this 的常见用法
🔑 点击查看答案与解析
答案: C
解析:
- A 正确 ✅:this 是一个特殊的指针,指向调用成员函数的那个对象
- B 正确 ✅:当参数名和成员变量名相同时,用 this->成员变量 来区分
- C 错误 ❌:静态成员函数属于类,不属于某个具体对象,所以没有 this 指针
- D 正确 ✅:成员函数返回 *this,可以实现链式调用(a.setX(1).setY(2))
选 C。
💡 记忆:静态的东西是"类级别的",非静态的是"对象级别的"。this 指向对象,所以静态函数没有 this。
十、📊 考点总结表
| 考点 | 重要程度 | 出题形式 | 易错点 |
|---|---|---|---|
| 面向对象三大特性 | ⭐⭐⭐⭐⭐ | 选择/判断 | 封装、继承、多态 |
| 类与对象的关系 | ⭐⭐⭐⭐ | 选择/判断 | 类是模板,对象是实例 |
| 访问控制(public/private/protected) | ⭐⭐⭐⭐⭐ | 选择/判断/代码填空 | class默认private,struct默认public |
| 封装的意义 | ⭐⭐⭐ | 选择/判断 | 隐藏实现,暴露接口 |
| 构造函数 | ⭐⭐⭐⭐⭐ | 选择/程序阅读 | 对象创建时自动调用 |
| 析构函数 | ⭐⭐⭐⭐ | 选择/程序阅读 | 对象销毁时自动调用;~类名() |
| 继承的概念 | ⭐⭐⭐⭐ | 选择/判断 | 子类复用父类代码 |
| 构造/析构顺序 | ⭐⭐⭐⭐⭐ | 选择/程序阅读 | 构造:父→子;析构:子→父 |
| 多态的三个条件 | ⭐⭐⭐⭐⭐ | 选择/判断 | virtual + 重写 + 父类指针/引用 |
| 动态绑定 vs 静态绑定 | ⭐⭐⭐⭐ | 选择/判断 | 多态是动态绑定(运行时) |
| this 指针 | ⭐⭐⭐⭐ | 选择/代码填空 | 指向当前对象;静态函数没有this |
| 初始化列表 | ⭐⭐⭐ | 选择/代码填空 | 构造函数冒号后面的部分 |
⭐ 必背速记卡
┌─────────────────────────────────────────────┐
│ 面向对象编程 · 必背速记卡 │
├─────────────────────────────────────────────┤
│ 1. 面向对象三大特性:封装、继承、多态 │
│ 2. 类是模板,对象是实例 │
│ 3. class默认private,struct默认public │
│ 4. 封装:隐藏实现细节,暴露公共接口 │
│ 5. 构造函数:对象创建时自动调用 │
│ 6. 析构函数:对象销毁时自动调用,~类名() │
│ 7. 继承:子类复用父类的属性和方法 │
│ 8. 构造顺序:先父后子 │
│ 9. 析构顺序:先子后父 │
│ 10. 多态三条件:virtual + 重写 + 父类指针 │
│ 11. 多态 = 动态绑定(运行时决定调用谁) │
│ 12. this指针:指向当前对象 │
│ 13. this区分同名成员变量和局部变量 │
│ 14. 静态成员函数没有this指针 │
│ 15. 链式调用:return *this │
└─────────────────────────────────────────────┘