简介面向对象程序设计C模拟试卷以单份PDF形式呈现面向正在学习C、准备课程期末考或考研复试笔试的学生也适合教师作为命题与课堂练习的参考素材。压缩包仅含1个文件为232KB的PDF文档篇幅紧凑便于打印或在电脑、手机上随时翻阅。试卷设定90分钟考试时间题型覆盖简答题、填空题、单项选择题与读程题并附有参考答案涉及break与continue的作用、函数重载的实现原则、this指针、纯虚函数与抽象类、throw与catch异常处理、运算符重载的两种形式、派生类继承方式、静态数据成员与对象数组等高频考点。读程题部分给出完整源码与运行结果可用来检验对strcpy、strcat、基类指针调用虚函数等细节的掌握程度。目前已有887人学习下载适合想在考前集中刷题、快速定位薄弱知识点的读者。1. 一份 C 面向对象模拟试卷能当什么样的自测标尺90 分钟、100 分、五大题——简答 10 分、填空 20 分、单选 20 分、读程 20 分、编程 30 分这是这份《面向对象程序设计 C》模拟试卷的骨架。它不是把知识点摊开讲一遍的复习提纲而是一份逼你输出的卷子读程题给代码让你写运行结果编程题直接让你把类定义和实现补齐。真正卡人的地方通常不在概念本身而在我以为我懂了手写出来是错的。适合两类人翻。一类是在校生期末前想找一套覆盖继承、多态、重载、异常处理、静态成员的成套题另一类是工作几年、平时写业务代码但很久没碰this指针、私有继承、对象数组这些语言细节的人简历上写着熟悉 C被追问私有继承后基类公有成员在派生类里算什么访问级别却答不利索。这份卷子的选择题专挑这类边角读程题专挑求值顺序和静态联编。下面按题号顺序拆先讲判定规则再给能直接编译运行的等价代码最后说清答案为什么是这样、放到现代编译器上又会发生什么。2. 函数重载、this 指针与控制流简答题里的三条判定线2.1 break 与 continue 的语义边界简答题第一题问得客气答案也短但选择题 3.3 里那句break 使流程跳出当前 if 语句才是真正的坑。先把行为跑出来#include iostream int main() { for (int i 0; i 5; i) { if (i 1) continue; // 结束本次迭代直接去执行 i if (i 4) break; // 跳出当前最内层循环 std::cout i ; } std::cout \n; // 输出0 2 3 }continue跳到的位置是循环的步进表达式for里是iwhile里是条件判断所以它不会跳过计数器自增——这是新手最常见的死循环来源。break的作用域只有两个最内层循环或者switch。嵌套两层循环想从内层直接跳出外层标准做法是加一个bool done标志逐层判断后退出或者用goto跳到循环外的标签处。if本身既不是循环也不是switch没有当前 if 语句这个可跳出的作用域。所以当if外面套着循环时break跳出的是那层循环跟if没关系。干扰项错就错在把break的从属关系挂到了if上。2.2 函数重载的三条判定线重载的规则一句话能说完同一作用域内函数名相同参数列表必须不同。但不同这个词的边界编译器判得比人细。两个声明是否构成重载判定依据void f(int);与void f(int, int);是参数个数不同void f(int);与void f(double);是参数类型不同void f(int, double);与void f(double, int);是参数顺序不同void f(int);与int f(int);否重定义报错返回类型不参与重载void f(int);与void f(const int);否重定义报错顶层 const 在形参类型中被忽略void f(int*);与void f(int);是指针与引用是不同类型void f(int);与void f(int);是但易二义传变量时两个都可行编译器报 ambiguous两处最容易翻车。一是默认参数和重载打架void f(int a);与void f(int a, int b 0);同时存在时调用f(1)会直接报二义因为两个都能匹配。二是const修饰的是指针本身还是指针指向的对象void f(char*)与void f(const char*)是合法重载而void f(char*)与void f(char* const)是同一个函数。底层机制是名字修饰name mangling编译器把参数类型编码进符号名所以_Z1fi和_Z1fd是两个不同符号。C 语言没有这套编码符号名里只有函数名这也是为什么头文件里给 C 函数声明要裹extern C——一旦裹上那个函数就不能重载了。2.3 this 指针在赋值运算符里的显式用法this是编译器在每个非静态成员函数里隐式传进来的第一个参数类型是指向当前对象的指针。平时看不见但写资源管理类时躲不开#include utility class Buffer { int* data_; int size_; public: explicit Buffer(int n) : data_(new int[n]()), size_(n) {} Buffer(const Buffer o) : data_(new int[o.size_]), size_(o.size_) { for (int i 0; i size_; i) data_[i] o.data_[i]; } Buffer operator(const Buffer o) { if (this o) return *this; // 自赋值保护 delete[] data_; // 先释放旧资源 size_ o.size_; data_ new int[size_]; for (int i 0; i size_; i) data_[i] o.data_[i]; return *this; // 返回 *this 而非 this } ~Buffer() { delete[] data_; } };三个点值得单独说。this不可重新赋值它是Buffer* const不能写this p但改*this允许。return *this返回的是对象本身配合引用返回类型才能支持a b c这种链式赋值如果返回Buffer按值每次赋值都要拷贝一次。自赋值保护不是可选的delete[] data_执行完如果o就是自己后面读o.data_读的是已经释放的内存。如果改用 copy-and-swap——先拷贝一份临时对象再swap交换成员——自赋值天然安全代码也短一截代价是多一次分配。两种写法都算合格答案但面试时最好说明各自的开销。3. 继承权限、静态成员与抽象类选择题错得最多的三块3.1 三种继承方式的访问级别矩阵选择题 3.4、3.8 连考两遍继承说明出题人知道这里是重灾区。规则其实只有一句继承方式取基类中原访问级别和继承方式中更严格的那个。基类中的访问级别public 继承后在派生类中protected 继承后private 继承后publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可访问不可访问不可访问分清两个不同的主体派生类的成员函数能不能访问和派生类的对象能不能访问。private 继承下基类的公有和保护成员在派生类里变成私有成员派生类的成员函数仍然可以直接调用它们但外部通过派生类对象一个都访问不到。选择题 3.8 的干扰项就是把这两件事混为一谈。所以派生类只继承了基类的公有和保护成员这个说法是错的——私有成员也被继承了只是派生类里无论如何访问不到只有基类的成员函数和友元能碰。至于派生类的缺省继承方式是 private 继承只在class关键字下成立struct的默认继承是 public。3.2 静态数据成员的类外初始化与共享语义选择题 3.6 那句静态数据成员不是所有对象所共用的是标准错项静态成员属于类而不是某个对象所有对象共享同一份存储。#include iostream class Counter { static int total_; // 类内只声明 int id_; public: Counter() : id_(total_) {} // 每个对象构造时累加 static int total() { return total_; } // 静态成员函数没有 this }; int Counter::total_ 0; // 类外定义并初始化不加 static int main() { Counter a, b, c; std::cout Counter::total() \n; // 3 std::cout a.total() \n; // 3通过对象调用也合法 }两个细节类外定义绝对不能再写static关键字否则就变成内部链接的另一个变量初始化必须在类外写除非是constexpr或 C17 起的inline static。C17 之后可以直接inline static int total_ 0;在类内搞定头文件里也不用再补定义。另外静态成员函数没有this指针所以不能是const成员函数也不能直接访问非静态成员。3.3 纯虚函数、抽象类与异常处理的组合填空题 2.1、2.2 分别考纯虚函数和异常这两块拼起来才是完整的接口设计#include iostream #include stdexcept class Shape { public: virtual ~Shape() default; virtual double area() const 0; // 纯虚函数无函数体 }; class Circle : public Shape { double r_; public: explicit Circle(double r) : r_(r) { if (r 0.0) throw std::invalid_argument(radius must be positive); } double area() const override { return 3.14159265358979 * r_ * r_; } }; int main() { try { Circle c(-1.0); // 构造函数抛异常 } catch (const std::invalid_argument e) { std::cout caught: e.what() \n; } }含纯虚函数的类是抽象类不能实例化Shape s;直接编译不过Shape* p new Shape;也一样但可以定义Shape*和Shape指向派生类对象——这是多态接口的标准姿势。析构函数必须是虚的否则delete基类指针时派生类的析构不会执行r_这类成员若换成裸指针就泄漏了。异常处理的三个关键字各司其职throw抛出catch按类型匹配捕获try划定监控范围。注意 catch 按从左到右顺序匹配派生类异常的catch必须写在基类前面否则永远轮不到catch (...)能兜住所有类型但拿不到异常对象通常只用来做最后一道防线。还有一道选择题 3.10 涉及类模板干扰项是类模板只能有类型参数。这是错的模板参数可以是int、bool、枚举甚至指针这类非类型参数std::arrayint, 10里的10就是非类型参数它必须在编译期确定。4. 读程题复现把四段代码真跑一遍读程题给 20 分是整张卷子里最能拉开差距的部分。四道题分别考指针与字符串库函数、静态联编、默认参数求值、私有继承下的转发机制各不相同。题号卷面答案核心机制4.1xycdBCDstrcat返回目标串首地址strcpy从s2开始覆盖4.212fun非虚函数按指针静态类型调用4.3sum110 sum215 sum320默认参数只补缺省实参嵌套调用自内向外算4.4(1, 2)/5,6/(6, 9)private 继承 转发函数改的是基类私有成员4.1 strcat/strcpy 与指针偏移卷面代码里p1 abcd这种把字符串字面量赋给char*的写法在 C11 之后已经是非法转换得改成const char*。而strcat会往目标缓冲区写入写进只读的字面量内存会直接段错误。想复现原题结果必须给一块可写缓冲区#include cstring #include iostream int main() { char buf[16] abcd; // 可写替代原题的字符串字面量 const char* p2 ABCD; char s[50] xyz; // buf2 指向 cd拼接 p21 指向的 BCD 后得 cdBCD std::strcpy(s 2, std::strcat(buf 2, p2 1)); std::cout s \n; // xycdBCD }拆开看strcat(dst, src)把src追加到dst末尾返回的是dst的首地址不是拼接后的新串所以strcat(buf2, p21)的返回值就是buf2此时缓冲区内容变成cdBCD。strcpy(s2, cdBCD)从s的第 3 个字节开始写入覆盖掉原来的zs前两个字符xy保留结果是xycdBCD。注意strcat不检查目标缓冲区剩余空间s只有 50 字节拼出来的长度一旦超过就静默越界写。工程里用strncat并手动补\0或者干脆用std::string。4.2 非虚函数的静态联编class Base { public: void fun() { std::cout 1; } // 非虚 }; class Derived : public Base { public: void fun() { std::cout 2 std::endl; } }; int main() { Derived a; Base* p a; (*p).fun(); // 输出 1 a.fun(); // 输出 2 并换行 }fun没有virtual所以调用哪个版本完全由指针/对象的静态类型决定编译期就定死了。p的静态类型是Base*调用Base::fun输出1a的静态类型是Derived调用Derived::fun输出2并换行。最终输出12加一个换行。如果Base::fun加上virtual两次调用都会走Derived::fun结果变成22。这正是隐藏和覆盖的区别。4.3 默认参数与嵌套调用的求值int add(int a, int b 5); // 默认参数写在声明处 int main() { int m 5; cout add(m); // 5 5 10 cout add(m, add(m)); // 5 10 15 cout add(m, add(m, add(m))); // 5 15 20 } int add(int x, int y) { return x y; } // 定义处不能再写 5默认参数只能在一处指定通常放在声明里定义处再写一次就是编译错误。嵌套调用按由内向外求值最内层add(m)用默认值补成add(5,5)10中间层add(m,10)15最外层add(m,15)20。这里没有副作用所以实参求值顺序不影响结果一旦实参里带自增或数组下标比如f(i, i)结果就是未指定行为换个编译器就换个数。4.4 私有继承下的转发函数class A { int a, b; public: A(int i, int j) : a(i), b(j) {} void Move(int x, int y) { a x; b y; } void Show() { std::cout ( a , b ) std::endl; } }; class B : private A { // 私有继承 int x, y; public: B(int i, int j, int k, int l) : A(i, j), x(k), y(l) {} void Show() { std::cout x , y std::endl; } void fun() { Move(3, 5); } // 派生类成员可以直接调基类公有函数 void f1() { A::Show(); } // 转发到基类的 Show }; int main() { A e(1, 2); B d(3, 4, 5, 6); e.Show(); // (1, 2) d.fun(); // 改的是 d 内部的 A 子对象a6, b9 d.Show(); // 5 ,6 d.f1(); // (6, 9) }d.fun()调用的是从基类继承来的Move改的是d里的A子对象不是d自己的x、y。d.Show()是派生类新定义的版本输出5 ,6d.f1()通过公有成员函数f1转发到A::Show输出更新后的(6, 9)。外部代码不能直接写d.A::Show()因为私有继承之后基类部分对外不可见。5. 编程题落地去重函数与 Point/Circle 派生实现5.1 deleteARR 的双指针写法以及它藏起来的前提卷面给的参考程序用的是双索引原地压缩k指向已确认的不重复序列末尾j向后扫描#include iostream #include algorithm // 原地去重只处理相邻重复元素返回不重复元素个数 int deleteARR(int a[], int count) { if (count 1) return count; // 空数组或单元素直接返回 int k 0; for (int j 1; j count; j) { if (a[k] ! a[j]) a[k] a[j]; // 发现新值就搬到 k1 } return k 1; } int main() { int a[] {1, 1, 2, 2, 3, 3, 3}; int n deleteARR(a, 7); for (int i 0; i n; i) std::cout a[i] ; // 1 2 3 std::cout \n; }时间 O(n)、空间 O(1)k始终落后于j被覆盖的位置都是已经处理过的不会丢数据。但这段代码有个容易被忽略的前提它只删除相邻的重复元素。输入{1, 2, 1}返回 3数组原样不动。想真正全局去重先排序再压缩或者用哈希表记录出现过的值// 方案一排序 std::uniqueO(n log n)会打乱原顺序 int dedupBySort(int a[], int count) { std::sort(a, a count); return static_castint(std::unique(a, a count) - a); } // 方案二哈希表保序去重O(n) 时间 O(n) 额外空间 #include unordered_set int dedupStable(int a[], int count) { std::unordered_setint seen; int k 0; for (int i 0; i count; i) { if (seen.insert(a[i]).second) a[k] a[i]; // insert 返回是否为新元素 } return k; }面试里如果只写第一种方案最好主动补一句这只处理相邻重复否则很容易被判成没审题。5.2 Point/Circle 派生实现与浮点精度编程题第二题要求从Point派生出Circle并算面积。卷面参考程序里return 3.14.159 * r * r;是明显笔误那个小数点位置不对正确常量是3.14159。更要紧的是返回类型写成了floatr稍大一点就有可见误差#include iostream class Point { int x_, y_; public: Point(int x, int y) : x_(x), y_(y) {} int getx() const { return x_; } int gety() const { return y_; } virtual ~Point() default; // 基类析构要虚便于多态删除 }; class Circle : public Point { double r_; public: Circle(int x, int y, double r) : Point(x, y), r_(r) {} double getr() const { return r_; } double area() const { // 用 double 而非 float static constexpr double kPi 3.14159265358979323846; return kPi * r_ * r_; } };Point的x_、y_是私有成员Circle的成员函数访问不到只能通过getx()、gety()这两个公有接口拿——这是封装的本意也是这道题想验的点。构造函数初始化列表里显式写Point(x, y)因为基类没有默认构造函数不写就编译不过。area声明成const成员函数说明它不修改对象状态const Circle对象也能调用。5.3 一套可复现的编译自测命令把上面几段代码分别存成文件后用这几条命令过一遍比肉眼对答案可靠# 严格模式编译把警告当线索看不要一上来就 -Werror g -stdc17 -Wall -Wextra -g paper.cpp -o paper ./paper # 检查 4.1 那类越界写和未定义行为这类题在答案里看不出问题跑起来就崩 g -stdc17 -g -fsanitizeaddress,undefined paper.cpp -o paper_asan ./paper_asan # 只看预处理和模板展开确认重载选中的到底是哪个符号 g -stdc17 -E paper.cpp | grep -n add调试器里下断点看静态类型比读代码更直观gdb ./paper之后break main、run再用ptype p打印p的静态类型就能确认 4.2 里调的到底是Base::fun还是Derived::fun。在 VS Code 里想省事就在.vscode/tasks.json里把g -stdc17 -Wall -Wextra -g固化成build任务c_cpp_properties.json的compilerPath指向同一个 gincludePath加上工作区目录智能提示报的错和编译器报的错才能对得上——路径优先级配错时头文件飘红但g能编过就是这套配置没对齐。Windows 上偶尔会遇到另一种情况源码在自己机器上编得过拿别人编译好的可执行文件跑却提示缺运行库那和源码无关装对应版本的 Microsoft Visual C Redistributable 即可。至于pycharm error: microsoft visual c 14.0 is required这类报错本质是某个 Python 包在装阶段要调本地编译器装完 Build Tools 再重装该包即可跟这份试卷里的 C 语法题不是一回事。用-Wall -Wextra编译 4.1 的原始写法会直接收到ISO C forbids converting a string constant to char*的警告而把p1改成const char*之后strcat的第一个参数又过不了——编译器其实已经替你把这道读程题的两个知识点都圈出来了。本文还有配套的精品资源点击获取