前言

本文整理自 OOP 课程的四次作业笔记,系统梳理了 C++ 面向对象编程的核心考点,包括:OOP 基础概念、命名空间、构造/析构函数、对象生命周期、继承体系、运算符重载以及多态机制。


一、OOP 基础与命名空间(HW1)

1.1 C++ 与面向对象

Q: 以下关于 C++ OOP 的说法哪个是错误的?

  • A. 可以不使用类来编写代码
  • B. 代码必须包含至少一个类
  • C. 类必须有成员函数
  • D. 代码中至少应声明一个对象

解析: C++ 是一门多范式(Multi-paradigm)语言。它不仅支持面向对象,还完全向下兼容 C 语言的面向过程编程。你可以写一个只有 main() 函数的 .cpp 文件,里面不定义任何类,代码依然能完美编译运行。


Q: 以下哪个特性支持开放递归(Open Recursion)?

  • A. 使用 this 指针
  • B. 使用指针
  • C. 使用值传递
  • D. 使用参数化构造函数

解析: “开放递归”指的是在类的内部,一个成员函数调用另一个成员函数。在 C++ 编译器底层,类内部的函数相互调用实际上都是通过隐藏的 this 指针(隐式执行 this->anotherFunction())来完成的。这也是多态中基类调用虚函数能正确路由到子类实现的关键机制。


Q: 关于类的成员函数,以下哪项是错误的?

  • A. 所有成员函数必须被定义
  • B. 成员函数可在类内部或外部定义
  • C. 成员函数不需要在类定义内部声明
  • D. 成员函数可以通过 friend 关键字成为另一个类的友元

解析: C++ 语法严格要求:所有成员函数必须在类的 {} 内部声明。你可以把函数体写在类的外面(使用 ClassName::),但必须先声明。否则编译器无法识别该函数属于这个类。


1.2 抽象与封装

抽象(Abstraction)原则的核心是:

  • C. 尽可能使用抽象以避免重复

解析: 当代码中出现多处类似逻辑时,DRY(Don’t Repeat Yourself)原则建议将这些重复部分抽象成公共函数、类或基类,大幅提高代码复用性和可维护性。


封装(Encapsulation)与抽象的区别:

  • A. 分别是绑定(Binding)和隐藏(Hiding)

解析: 这是一道经典区分题:

  • 封装侧重于绑定——把数据和方法捆绑成不可分割的整体
  • 抽象侧重于隐藏——向外界隐藏内部复杂机制,只暴露必要的接口

在流和文件的概念中:

  • A. 抽象被称为流(Stream),设备被称为文件(File)

解析: “流”是纯粹的逻辑抽象概念(如 cincout),而”文件”或显示器等是具体的物理设备。我们通过操作”流”这个抽象层来统一读写物理设备。


1.3 命名空间

命名空间的作用:

  • B. 将程序结构化为逻辑单元

解析: 命名空间类似于文件系统中的”文件夹”。在大型工程中,为防止不同模块产生命名冲突,使用命名空间将相关的类、对象、函数分组隔离。


匿名命名空间示例:

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
namespace {
int var = 10;
}
int main() {
cout << var; // 输出 10
}

匿名命名空间中的变量自动具有内部链接属性(类似 static 全局变量),作用域被严格限制在当前文件中,可直接通过变量名访问。


作用域解析示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
namespace extra {
int i;
}
void i() {
using namespace extra;
int i;
i = 9;
cout << i; // 输出 9
}
int main() {
enum letter { i, j };
class i { letter j; };
::i(); // :: 强制调用全局函数 i()
return 0;
}

::i() 前的 :: 告诉编译器忽略局部作用域里的枚举和类,直接调用全局函数。函数内部的局部变量 int i 优先级最高,屏蔽了命名空间里的 i


二、对象生命周期与类成员(HW2)

2.1 拷贝构造函数

拷贝构造函数的用途:

  • C. 拷贝对象以便将其传递给函数

解析: 当对象以值传递方式作为形参传入函数,或从函数中按值返回时,编译器会自动调用拷贝构造函数生成该对象的副本。


拷贝构造函数何时被调用?

  • C. 生成临时对象时

解析: 编译器在处理对象按值传参、按值返回或执行隐式类型转换时,会在栈区生成临时对象,此时必定触发拷贝构造函数。


拷贝构造函数可以设为 private 吗?

  • A. 是的,始终可以

解析: 将拷贝构造函数和赋值运算符声明为 private 且不提供实现,是打造 Non-copyable Object(如互斥锁、单例对象)的经典模式。如今更推荐使用 = delete


如果拷贝构造函数只接受非 const 参数:

  • B. 只有非常量对象才能被接受

解析: 如果拷贝构造函数没有 const 修饰(即 ClassName(ClassName& obj)),编译器基于类型安全,绝不允许传递 const 对象。因此它只能接受非常量对象。


2.2 析构函数

析构函数的调用方式:

  • C. 可以是隐式或显式的

解析: 析构函数通常在对象离开作用域时由系统隐式调用。但在使用 Placement new 等高级场景下,必须通过 obj.~ClassName() 显式调用以清理资源。


析构函数何时被调用?

  • D. 对象生命周期结束之前的那一刻

解析: 析构函数在对象生命周期即将终结但内存还未被回收的”最后一刻”执行,负责释放对象持有的外部资源(堆内存、文件句柄等)。


全局对象的析构顺序:

  • C. 逆序(Reverse)

解析: C++ 中对象的构造和析构遵循栈的 LIFO 逻辑。无论在局部函数还是全局存储区,析构顺序总是与构造顺序完全相反


2.3 静态成员

当没有对象存在时,要在类内部操作静态数据成员:

  • C. 必须使用静态成员函数

解析: 静态数据成员独立于任何对象存在。普通成员函数需要隐含的 this 指针才能调用,而静态成员函数不需要对象即可执行。


static 关键字的使用规则:

  • B. 只在类内部声明时使用,类外部定义时不使用

解析: 这是 C++ 的硬性规定。在类外部实现静态成员时,绝对不能再加 static 关键字,否则会导致链接错误。


2.4 this 指针

this 指针的本质:

  • D. 不是对象本身的一部分

解析: this 是编译器在调用非静态成员函数时隐式传递的第一个函数参数。它不存储在对象的物理内存布局中。对对象执行 sizeof(),结果不包含 this 指针。


成员函数调用的底层真相:

  • D. function(&object, parameter)

解析: 在编译器眼中,object.function(arg) 会被重写为 function(&object, arg),即将调用者的地址作为第一个隐藏参数传入——这就是 this 指针。


this 指针的典型用途:

  • B. 防范自我引用(Self-reference)

解析: 在重载赋值运算符时,必须防范自我赋值:if (this != &other) { ... }。否则释放旧内存时会连带销毁新数据。


2.5 new 与 delete

new 失败时会怎样?

  • C. 要么抛出异常,要么返回零

解析: 现代 C++ 中 new 失败默认抛出 std::bad_alloc。若使用 new (std::nothrow) Type,则返回 nullptr


delete 的执行顺序:

  • B. 先调用析构函数,再释放内存

解析: delete 分两步:第一步调用析构函数清理资源,第二步调用 operator delete 归还物理内存。


delete 一个不是由 new 分配的对象:

  • C. 会产生不可预测的错误

解析: 对栈或全局区的对象使用 delete 属于典型的未定义行为(UB),会导致难以预测的后果。


删除对象数组的正确语法:

  • A. delete[] objectName;

解析: 使用 new Type[N] 申请的数组必须用 delete[] 释放。漏掉 [] 会导致只析构第一个元素,其余元素资源泄漏。


2.6 默认参数与构造函数

若构造函数应同时支持无参和有参创建对象:

  • C. 使用全部带默认参数的构造函数

解析: 所有参数都有默认值的构造函数(如 ClassName(int a = 0, int b = 0))可同时充当无参和有参构造两种角色。


默认参数的铁律:

  • C. className(int x=0, char c); 会产生编译错误

解析: 函数的默认参数必须从右向左连续填充。绝不能把默认参数放在无默认值参数的左边。


三、继承与访问控制(HW3)

3.1 访问权限与继承方式

B 类私有继承 A 类,B 的友元函数能否访问 A 的私有成员?

  • C. 不能,因为友元函数只能访问友元类的私有成员

解析: 友元特权是不继承、不传递的。A 的 private 成员对 B 本身都不可见,B 的友元自然无法跨越两层去访问。


protected 继承导致的权限降级:

1
2
3
4
5
6
7
8
9
class A {
protected: int marks;
public:
A() { marks = 100; }
void disp() { cout << "marks=" << marks; }
};
class B : protected A { };
B b;
b.disp(); // 编译错误!

由于 protected 继承,A 中原有的 public 接口 disp() 在 B 中被降级为 protected,外部无法通过对象直接调用。


哪个访问修饰符能让基类的 public 成员在派生类中变得”安全”?

  • D. private 和 protected

解析: 两种继承方式都会将继承来的公有接口降级,从而切断外部对象直接访问的可能。


3.2 继承类型全景

继承模式 拓扑结构 核心描述
Single level (单继承) 1 对 1 一个派生类只拥有唯一一个直接基类(A → B)
Multilevel (多级继承) 垂直链 派生类继续派生子类,代代相传(A → B → C)
Hierarchical (层次继承) 1 对多 一个基类同时派生多个子类(A → B 且 A → C)
Hybrid (混合继承) 网状 多种继承方式结合,导致菱形问题的罪魁祸首

多重继承(Multiple Inheritance):一个子类同时拥有多个直接基类。正是它引发了混合继承中的菱形危机。解决方案是使用 virtual 关键字进行虚继承


菱形问题(Diamond Problem)的根本原因:

  • A. 同名方法造成二义性和冲突

解析: 子类通过不同路径继承了顶层基类的同名方法/变量,编译器面临两条等价的查找路径,无法决断,抛出二义性错误。


3.3 向上转型

基类指针可以用派生类地址初始化,是因为:

  • A. 派生类到基类的指针隐式转换

解析: 派生类对象在内存中必定包含完整的基类子对象,因此 C++ 原生支持将派生类指针隐式转换为基类指针——这被称为向上转型(Upcasting),是实现多态的基石。


四、运算符重载与多态(HW4)

4.1 运算符重载基础

运算符重载为成员函数时:

  • 左操作数充当隐含的 *this 指针
  • 右操作数作为显式函数参数传入
  • 在函数体内部可直接访问左操作数的成员,右操作数必须通过参数名访问

4.2 运算符重载方式选择

运算符类别 示例 必须 Member 必须 Friend 两者皆可
类的核心行为 =, [], (), -> ✓ 是 ✗ 否 ✗ 否
标准 IO 流操作 <<, >> ✗ 否 ✓ 是 ✗ 否
修改对象自身状态 +=, -=, ++, -- ✗ 否 ✗ 否 ✓ (推荐 Member)
对称计算与比较 +, -, ==, < ✗ 否 ✗ 否 ✓ (推荐 Friend)
底层语法解析 ., ::, ?:, sizeof 严禁重载

必须用成员函数的四个运算符记忆口诀: 等号、中括号、小括号、箭头——这四个是类的”私有财产”。

流操作符为何用友元: 为了保持 cout << obj 的直觉写法,左操作数必须是 ostream 对象,因此只能写成全局函数。

对称运算符推荐友元: 友元允许左右操作数都进行隐式类型转换(obj + 55 + obj 均可编译)。


4.3 多态

多态的定义: 不同对象接收到相同的消息时,产生不同的行为

从运行角度分类:

类型 机制 决断时机
静态多态 函数重载、运算符重载、模板 编译期
动态多态 继承 + 虚函数 运行期

联编(Binding)的两种方式:

  • 静态联编(早期联编):编译时确定函数调用与函数体的绑定
  • 动态联编(晚期联编):运行时基于指针或引用所指向对象的实际类型,通过虚函数表(vtable)确定调用

4.4 拷贝构造 vs 赋值运算符

这是考试和面试中最经典的陷阱。核心判断标准只有一个:赋值号左边的对象是否已经存在?

拷贝构造 赋值运算符
动作 初始化 赋值
时机 正在创建全新对象 对象早已存在
典型代码 A d = a; c = a;(c 已声明)
隐式触发 按值传参、按值返回

防坑口诀: 带类型名声明的 =(如 A d = a;)是在”生孩子”,必定调用拷贝构造;没有类型名、单独使用的 =(如 c = a;)是在”换衣服”,必定调用赋值重载。


附:C++ OOP 知识点速查

概念 一句话总结
封装 绑定数据与方法为整体
抽象 隐藏内部细节,只暴露接口
继承 子类复用父类的属性和行为
多态 同一接口,不同实现
命名空间 组织代码为逻辑单元,避免命名冲突
this 指针 编译器隐式传入的调用者地址,不占对象内存
静态成员 属于类本身,独立于任何实例
虚继承 解决菱形继承中重复基类子对象的问题
友元 不继承不传递的特权访问