资讯动态

C++虚函数与抽象类:单元测验7-3核心考点与避坑指南

发布时间:2026/10/17 0:16:39 来源:尧图企业网站定制
最近收到不少正在复习《C语言程序设计下》的同学来问单元测验7-3的事问得最多的就是“虚函数、抽象类这几个点到底要掌握到什么程度”。正好我在某高校代过这门课的习题课也整理过一套针对这个单元测验的复习思路今天就把里面的关键考点、典型错题和我在批改作业时看到的高频翻车点一次性聊透。这篇文章不是把课件复述一遍更像是我站在出题人的角度拆一拆这套测验背后真正想考察的能力。本文适合这几类人看正在准备单元测验7-3的在读学生、自学C想检验多态掌握程度的初学者以及需要带学生复习的助教或讲师。内容围绕虚函数的运行机制、抽象类的设计意图、析构函数为什么必须加virtual、还有那些“编译能过但行为不对”的隐蔽坑展开。通篇以测验7-3的实际考察风格为主线尽量用大白话把抽象概念讲清楚也会给出一眼就能落地的自查方法帮助你在考场上少丢冤枉分。1. 单元测验7-3到底在考什么多态与虚函数的完整知识骨架1.1 第七章为什么总被看作C课程的分水岭先说个背景。C课程一般分成两半前半部分讲基本语法、函数、数组、指针以及类和对象的基本封装后半部分才真正进入继承、多态、模板这些“重武器”。单元测验7-3刚好卡在第七章的位置这一章在多数教材里对应的是继承与多态的综合应用考察重点非常明确你有没有建立起“面向对象”的思维方式而不是只会写几个带class的C语言程序。我自己批改作业的体感是前六章学得再扎实的人到第七章也容易懵。原因是多态这个特性跟前面的语法不太一样它强调的不是“我写了什么”而是“程序运行时会发生什么”。你可以把继承理解成“父子关系的确立”把多态理解成“同一个动作在不同子类里呈现出不同行为”。没有多态你只能用基类指针调用基类自己的函数有了多态你才能让基类指针指向不同的派生类对象并在这个指针上调用出“属于那个派生类”的函数版本。测验7-3里的题目基本就是围绕这个能力展开的。1.2 测验7-3核心覆盖点与常见考察方式根据我对这套单元测验的了解7-3的知识点分布可以总结成五块虚函数的基本声明与调用规则、纯虚函数与抽象类、动态联编与静态联编、虚析构函数以及override与隐藏的辨析。为了让复习更有针对性我整理了一张考点地图对应到常见出题形式考点说明常见考察方式虚函数声明virtual关键字的位置、虚函数能否是静态成员函数选择题判断正误动态联编通过基类指针/引用调用虚函数时的实际行为阅读程序写输出纯虚函数 0的含义、抽象类不能实例化代码改错、选择抽象类有纯虚函数不能用对象、但可以用指针和引用判断能否实例化虚析构函数基类析构函数加virtual才能正确释放派生类资源程序行为判断覆盖vs隐藏函数签名是否一致、有无virtual综合辨析题这套测验里最有区分度的题目基本集中在“输出结果”和“能否通过编译”两类上。读程序写输出的题考察的是你对动态联编的理解判断能否通过编译的题考察的是你对虚函数规则的边界条件是否清楚。比如静态成员函数能不能是虚函数、抽象类能不能实例化——这种判断题看似简单但特别能拉开分差因为很多同学是“背过规则但没真正理解”。2. 高频错误1virtual关键字不是“性能优化”而是“运行时路由开关”2.1 初学者最常见的理解偏差我在助教答疑时最常听到的一句话是“老师我加了virtual程序怎么变慢了”这反映出很多同学把virtual理解成了类似inline的“优化手段”。实际上恰恰相反虚机制带来的是运行时的间接跳转成本。你需要的不是“快”而是“正确”。用生活场景打个比方。你去车站买车票如果直接买“今天下午3点去某城的票”这是静态确定好的但如果买的是“某趟车次”你到候车室才知道是哪辆车——这有点像虚函数。编译器在看到基类指针调用虚函数时并不确定这个指针到底指向哪个派生类对象所以它把这个决定推迟到运行时由对象自身的“类型信息”来决定跳转到哪个函数实现。这个机制的专业说法叫动态联编也叫运行时多态。我见过不少同学手写代码时把基类函数加virtual但派生类的同名函数没有加virtual。其实C里派生类重写基类虚函数时可以省略virtual关键字因为虚属性会“继承”下去。不过从代码可读性和防错的角度我还是建议在派生类里也写上或者直接在函数后面加override让编译器帮你检查签名是否匹配。这是测验7-3选择题和写程序题经常会考察的细节。2.2 动态联编与静态联编的对比静态联编发生在编译期函数调用在编译时就能找到固定地址动态联编发生在运行期通过虚函数表来确定最终调用目标。这里我不想把虚函数表的细节讲得太深但你需要知道它的存在每个含有虚函数的类在编译时会生成一张虚函数表表中记录了该类所有虚函数的地址每个对象内部会藏一个虚函数表指针指向自己所属类的虚函数表。当基类指针指向派生类对象时它拿到的那个虚函数表指针指向的是派生类的表所以虚函数调用会进入派生类的实现。对比项静态联编动态联编发生时间编译期运行期依据指针/引用的静态类型对象实际类型性能更快无间接跳转有一次间接寻址开销典型语法普通函数、非虚成员函数通过基类指针/引用调用虚函数一道非常经典的测验题定义了Animal基类和Dog派生类speak()在基类中是虚函数派生类重写后通过Animal* p new Dog(); p-speak();调用输出结果就是Dog的版本如果把virtual去掉结果就会变成基类的Animal版本。这里的关键不是“指针类型”而是“对象是谁”——动态联编认的是对象不是指针。写这类题的判断我建议先问自己一句调用这个函数时实际对象是基类还是派生类如果是派生类并且函数是虚函数那就是派生类版本。class Animal { public: virtual void speak() { std::cout Animal speaks std::endl; } }; class Dog : public Animal { public: void speak() { std::cout Dog barks std::endl; } }; int main() { Animal* ptr new Dog(); ptr-speak(); // 输出 Dog barks因为 speak 是虚函数 delete ptr; // 注意此处基类析构函数需要是虚析构函数 return 0; }它考察的就是“有没有virtual输出拐不拐弯”的区别。如果你把speak()的virtual去掉输出就会变成Animal speaks这也就是很多同学把输出题做错的原因——代码看起来一样但少了virtual行为天差地别。3. 高频错误2抽象类“不能实例化”不是缺陷而是设计约束3.1 纯虚函数与抽象类的语法细节纯虚函数的写法是在函数声明后面加 0比如virtual void draw() 0;。这里的 0并不是函数返回0而是一个语法标记告诉编译器“这个函数我在当前类里不提供实现你给我留个接口位让派生类来填”。含有纯虚函数的类就是抽象类它不能用来创建对象。很多同学看到“抽象类不能实例化”就直接死记硬背其实这个约束背后有很实际的原因。抽象类扮演的是“模板”或者“契约”的角色它定义了一组接口但具体的实现细节往往依赖于子类。假如你允许Shape s;这样创建一个抽象类对象那么s.draw()该执行什么抽象类自己都不知道。所以C干脆规定抽象类只能作为基类只能通过指针或引用去操作它的派生类对象。3.2 派生类实现纯虚函数的判定一个派生类如果继承了抽象类它必须把基类所有纯虚函数都实现出来否则这个派生类自己仍然是一个抽象类依旧不能实例化。这里容易出错的点集中在“部分实现”和“签名不匹配”上。比如基类有virtual void show(int) 0;派生类写的是void show(double)看起来像是实现了其实函数签名不同这属于隐藏而不属于覆盖基类纯虚函数仍然没有被实现派生类就仍然是抽象类。还有一种情况派生类实现了全部纯虚函数但基类也提供了某个纯虚函数的内联实现。C允许给纯虚函数写函数体但对象依然不能实例化派生类依然必须重写。这个点比较冷门但单元测验7-3的判断题偶尔会考属于“看书不仔细就丢分”的题目。我的建议是看到纯虚函数第一反应不要想“它有没有函数体”而要想“这个类还能不能创建对象”。3.3 抽象类也能写出能跑的代码虽然抽象类不能实例化但抽象类的指针和引用非常有用。最常见的写法是用抽象类指针指向派生类对象然后调用虚函数实现统一接口管理。比如有一个Shape抽象基类派生出Circle和Rectangle你定义一个Shape* shapes[2]数组分别指向两个派生类对象再遍历数组调用draw()就能得到各自不同的绘制结果。这其实是多态最核心的用法也是单元测验7-3后面编程题或综合题的雏形。class Shape { public: virtual void draw() 0; virtual ~Shape() {} }; class Circle : public Shape { public: void draw() override { std::cout Draw Circle std::endl; } }; class Rectangle : public Shape { public: void draw() override { std::cout Draw Rectangle std::endl; } };要学会从设计角度理解抽象类它存在的意义不是为了“让自己能用”而是为了“让所有子类长得像一家人”。在答题时凡是看到选项里出现“抽象类可以实例化”或者“纯虚函数可以有函数体”都要多留个心眼前者必错后者则要结合上下文判断。4. 最容易翻车的三道典型题从错误选项倒推考点4.1 典型题1构造函数里调用虚函数为什么“多态失效”了这是测验7-3里出错率最高的选择题之一。题目往往给出一个基类和派生类二者各有同名的虚函数然后在基类构造函数里调用这个虚函数问输出是什么。很多同学想当然地认为“既然是虚函数就应该调用派生类版本”结果答案是调用基类版本直接选错。原因在于对象的构造顺序。创建派生类对象时先执行基类构造函数再执行派生类构造函数。在基类构造函数执行期间派生类部分还没有被初始化对象在C看来还处于“基类构造中”的状态此时虚函数调用不会走动态联编而是直接调用当前正在构造的这个类里的版本。你可以这样记构造函数运行时对象还没有“完整成型”所以虚函数表指针指向的仍然是基类的虚函数表。等到派生类构造函数执行完对象才真正变成派生类对象。这种题考的不是你会不会写代码而是你有没有真正理解对象的生命周期。我建议在做这类题时别急着选答案先画一条对象构造时间线基类构造 → 派生类成员构造 → 派生类构造。凡是在“基类构造”这个时间点调用虚函数答案就是基类版本。4.2 典型题2基类析构函数要不要加virtual程序崩溃就在一念之间另一道高频题问的是析构函数。给定一个基类指针指向new出来的派生类对象然后delete这个指针问程序会调用哪些析构函数。如果基类析构函数没有virtualdelete时只会调用基类的析构函数而不会调用派生类的析构函数结果就是派生类自己申请的资源比如堆内存、文件句柄没有被释放造成内存泄漏。只有基类析构函数是虚函数时delete基类指针才会先调用派生类析构函数再自动调用基类析构函数跟构造顺序刚好相反。这个知识点我每次上课都会强调只要一个类打算作为基类使用并且通过基类指针操作派生类对象就一定要给析构函数加virtual。注意析构函数加virtual和普通虚函数有一点不同——它是唯一一个在“函数签名不同但依然构成覆盖”的特殊场景因为派生类和基类的析构函数名字不同但C仍把它们看作覆盖关系。测验7-3里如果考这个概念通常会给你四种组合基类加/不加virtual、派生类加/不加virtual然后问delete时的调用序列。记住真正决定能否完整析构的是基类析构函数有没有virtual。4.3 典型题3static成员函数不能是虚函数原因是它没有this还有一道判断正误题也很经典静态成员函数能否用virtual修饰答案是否定的。因为虚函数机制依赖于对象自身的类型信息调用虚函数时需要有this指针去找到虚函数表而静态成员函数不属于任何对象没有this指针自然也就没办法完成动态联编。你可以把静态成员函数理解成“类级别的普通函数”它跟对象没有绑定关系跟多态完全是两条路线。我把这个知识点整理成几个“铁律”做选择题时可以快速排除虚函数必须是成员函数不能是全局函数。虚函数不能是静态成员函数。构造函数不能是虚函数析构函数可以。内联函数可以是虚函数但虚函数的动态联编特性会让内联变得几乎没有意义。这几条几乎覆盖了测验7-3所有关于virtual修饰范围的判断题。做题时看到选项里有static、friend友元函数不是成员函数也不能是虚函数这类词直接判断“不能是虚函数”属于稳妥的拿分项。5. 编译能过但多态失效函数签名不同导致的隐藏才是大坑5.1 const成员函数与虚函数一个容易被忽略的签名差异比“加了virtual没效果”更隐蔽的坑是函数签名不同导致的隐藏。我见过不少同学写代码时自信满满地以为自己在“重写虚函数”运行结果却还是基类版本改了半天发现是少了const或者参数类型不匹配。举个例子class Base { public: virtual void print() const { std::cout Base const print std::endl; } }; class Derived : public Base { public: void print() { // 少了 const签名不同 std::cout Derived print std::endl; } }; int main() { Base* p new Derived(); p-print(); // 输出 Base const print return 0; }这段代码能编译通过但运行结果是基类的版本。因为Derived里的void print()和Base里的void print() const不是同一个签名前者没有覆盖后者而是把基类的print“隐藏”了。由于这个隐藏是通过基类指针看不见的所以动态联编根本不会发生。const是函数签名的一部分这种细节平时写作业不觉得一旦到单元测验7-3的选择题里就成了区分高手和普通人的题目。5.2 覆盖、隐藏、重载三者之间的边界为了让这类题目不再失分我总结了一张对比表覆盖override、隐藏redeclare/hide、重载overload三者的区别一目了然关键词是否同名是否在同一作用域函数签名要求virtual要求重载 overload是必须在同一作用域参数列表不同无所谓覆盖 override是不同类基类/派生类参数列表与const完全一致基类必须有virtual隐藏 hide是不同类基类/派生类不同于覆盖的任意情况基类有无virtual都可以做题时只要按三步走就能判断先看是否同名再看作用域是否跨类最后比对函数签名。如果不同类、同名、签名完全一致且基类函数是虚函数那就是覆盖会发生多态如果不同类、同名但签名不一致那就是隐藏不会发生多态。测验7-3的最后一道综合题往往就是给出一个继承层次里面既有重载、又有隐藏、还有覆盖让你写出各个调用的输出结果。这种题想要拿满分你必须先学会用表格里的条件去“定性”再逐行推导输出。我特别建议你在编译器里实际做一次这种实验注释掉粘的override故意在派生类少写一个const看看编译器会不会报错。实际上C11的override关键字非常有用它能强制让编译器检查“这个函数是否真的覆盖了基类的虚函数”如果签名不一致编译直接报错。很多同学担心写override会麻烦但恰恰是它能在考前帮你自动发现签名不匹配的问题。测验7-3里如果遇到代码补全或改错类大题手动检查签名一致性再加override验证基本不会丢分。6. 考前的自查清单与最后的冲刺建议6.1 两分钟快速判断题目的五个问题复习到这个阶段最重要的是形成条件反射式的判断逻辑。我在课上带学生讲过一套“五连问”自查法做任何一道多态相关的选择题时按顺序过一遍答案自然出来被调用的函数是不是成员函数不是 → 不考虑多态。这个函数在基类中是否声明为virtual不是 → 不会发生动态联编。调用时通过什么方式发起通过基类指针或基类引用 → 才可能动态联编通过对象直接调用 → 仍然是静态联编。派生类中的同名函数签名是否与基类完全一致包括参数类型、const限定符 → 一致才是覆盖。派生类对象是否已经构造完成如果当前还在基类构造函数中 → 虚调用仍走基类版本。这套方法对付单元测验7-3的选择题和改错题足够了。比如看到“通过基类对象s 派生类对象复制赋值然后调用虚函数”第一反应就是不产生多态因为对象切片后虚函数表指针指向的已经是基类虚函数表了。这种题很容易骗到只记住“指针/引用触发多态”的人。6.2 我一直推荐的复习与实战方法最后说说我自己的复习习惯也是我一直推荐给学生的。第一把每一章的核心概念浓缩成一句不超过20个字的话写在卡片上。比如“虚函数运行时路由开关”“抽象类只给接口不给实现”。这句话你得能讲给自己听讲不清楚就是没理解透。第二动手把书本上的小例子自己敲一遍然后把virtual删掉、给析构函数去掉virtual、把派生类函数加个const分别观察运行结果和编译报错。这样做的效果比背十道题都来得快因为你会亲眼看到行为差异。单元测验7-3这套题难并不是因为语法复杂而是因为它要求你同时理解编译器的静态视角和运行时的动态视角。对我来说带这门课最深的体会是真正把多态学明白的同学写出来的代码都有一个共同特征——不依赖记忆去堆规则而是能从对象的实际类型出发推演行为。你如果能做到这一点应对测验7-3会非常轻松更关键的是后面学模板、STL时也会顺畅很多。考试前把这篇文章里的代码和表格过一遍再动手运行两遍比闷头刷题有效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑