Skip to content

第6章 面向对象编程基础 ​


🎯 本章学习目标 ​

通过本章的学习,你将能够:

  1. 理解面向对象思想:知道面向对象 vs 面向过程的区别
  2. 掌握类与对象:理解类是模板、对象是实例,能定义类和创建对象
  3. 理解封装:掌握访问控制(public/private/protected)和封装的意义
  4. 掌握构造与析构:理解构造函数和析构函数的作用和调用时机
  5. 理解继承:掌握继承的概念,知道子类如何复用父类代码
  6. 理解多态:掌握虚函数、重写、动态绑定的概念
  7. 掌握this指针:理解this指针的作用和用法
  8. 区分访问权限:清楚public/private/protected的区别和继承时的变化
  9. 理解构造顺序:知道继承时构造函数和析构函数的调用顺序
  10. 应对真题题型:能解决选择、判断、代码填空等各类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 构造函数调用顺序(必考!) ​

创建派生类对象时,构造函数的调用顺序:

  1. 先调用父类构造函数(按继承顺序)
  2. 再调用成员对象的构造函数(按声明顺序)
  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 指针的常见用途:

  1. 区分同名:当成员变量与参数同名时,用 this->xxx 表示成员变量
  2. 返回自身引用:支持链式调用(如 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

解析: 面向对象的三大特性:

  1. 封装:把数据和操作打包,隐藏内部实现,只暴露接口
  2. 继承:子类继承父类的属性和方法,实现代码复用
  3. 多态:同一接口,不同实现(通过虚函数实现)

选 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++ 多态的三个必要条件:

  1. ✅ 基类函数声明为 virtual(虚函数)
  2. ✅ 子类重写(override)该虚函数
  3. ✅ 用基类指针/引用指向子类对象

三者缺一不可!

选项 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                 │
└─────────────────────────────────────────────┘

百炼成钢,融会贯通