Appearance
《C++面向对象程序设计(甲)》期末试卷B (精选03)
一、判断题(对的打"√",错的打"×")(每题 1 分,共计 10 分)
- cout 的默认输出对象是键盘,cin 的默认输入对象是屏幕。
查看答案与解析
答案:×
解析: cout 的默认输出对象是屏幕(标准输出流),cin 的默认输入对象是键盘(标准输入流)。题目将两者颠倒了。
cout= character output,输出到屏幕cin= character input,从键盘输入
难度: ⭐ 考点: #输入输出流 #cout #cin
💡 学习锦囊
📖 相关公式与知识点:
cout:标准输出流对象,默认输出到屏幕(stdout)cin:标准输入流对象,默认从键盘读取(stdin)cerr:标准错误流,无缓冲输出到屏幕clog:标准日志流,有缓冲输出到屏幕
易错点
- 容易混淆 cin/cout 的方向,记住:in = 输入(从键盘读),out = 输出(到屏幕写)
🔄 举一反三
- 下列关于 C++ 输入输出的描述,正确的是( )
- A. cin 是输出流对象
- B. cout 是输入流对象
- C. cerr 是无缓冲的标准错误流
- D. clog 是无缓冲的标准日志流
查看练习答案与解析
答案:C 解析:cin 是输入流对象(A 错),cout 是输出流对象(B 错),cerr 是无缓冲的标准错误流(C 对),clog 是有缓冲的标准日志流(D 错)。
- 使用 new 运算符创建数组时,可以为该数组指定初始值。
查看答案与解析
答案:√
解析: 在 C++11 及以后标准中,使用 new 运算符创建数组时,可以通过列表初始化指定初始值:
int *p = new int[5]; // 不指定初始值(传统写法)
int *p = new int[5](); // 值初始化为0(C++11起)
int *p = new int[5]{1,2,3}; // 列表初始化(C++11起),未指定的元素初始化为02
3
注意:在 C++98/03 标准中,new 创建数组确实不能指定初始值,此题答案取决于考试所基于的 C++ 标准版本。
难度: ⭐⭐ 考点: #new运算符 #动态内存分配 #数组初始化
💡 学习锦囊
📖 相关公式与知识点:
new 类型[大小]:分配数组,不能指定初始值(传统C++)new 类型(值):分配单个对象,可指定初始值delete[] 指针:释放 new 分配的数组
易错点
new int[5]与new int(5)的区别:前者分配5个int的数组,后者分配1个int并初始化为5- 释放数组必须用
delete[],不能用delete
🔄 举一反三
- 下列关于 new 运算符的描述,正确的是( )
- A. new 创建的对象一定在栈上
- B. new 失败时一定返回 NULL
- C. new 可以自动计算所需内存大小
- D. new 和 malloc 完全等价
查看练习答案与解析
答案:C 解析:new 在堆上分配内存(A 错);new 失败时默认抛出
bad_alloc异常(B 错);new 根据类型自动计算所需内存大小(C 对);new 会调用构造函数,malloc 不会(D 错)。
- 内联函数的定义必须出现在第一次调用内联函数之前。
查看答案与解析
答案:√
解析: 内联函数的定义必须在第一次调用之前出现。编译器需要将内联函数的代码直接嵌入到调用处(内联展开),如果编译器在调用点还看不到函数定义,就无法进行内联展开。
难度: ⭐⭐ 考点: #内联函数 #inline #编译优化
💡 学习锦囊
📖 相关公式与知识点:
- 内联函数:编译时将函数体嵌入调用处,减少函数调用开销
- 定义位置:必须在第一次调用之前
- 与宏定义的区别:内联函数有类型检查,宏定义没有
易错点
- 内联函数只是对编译器的建议,编译器可以选择不内联
- 在类体内定义的成员函数默认为内联函数
🔄 举一反三
- 下列关于内联函数的描述,错误的是( )
- A. 内联函数的调用不需要栈操作
- B. 内联函数可以包含循环语句
- C. 内联函数的定义必须出现在调用之前
- D. 内联函数一定会被编译器内联展开
查看练习答案与解析
答案:D 解析:
inline只是对编译器的建议,编译器有权决定是否真正内联。
- 在用 class 定义一个类时,数据成员和成员函数默认的访问权限是 public。
查看答案与解析
答案:×
解析: 用 class 关键字定义类时,默认访问权限是 private,不是 public。class 与 struct 的唯一区别就是默认访问权限不同:class 默认 private,struct 默认 public。
难度: ⭐ 考点: #class关键字 #访问权限 #class与struct的区别
💡 学习锦囊
📖 相关公式与知识点:
class默认访问权限:privatestruct默认访问权限:public- 三种访问权限:public、protected、private
易错点
- 面试/考试常考:class 默认 private,struct 默认 public
🔄 举一反三
- 下列关于 class 和 struct 的描述,正确的是( )
- A. class 不能定义成员函数
- B. struct 不能使用继承
- C. class 默认继承方式是 private
- D. struct 不能使用模板
查看练习答案与解析
答案:C 解析:class 和 struct 都可以定义成员函数、使用继承、使用模板。class 的默认继承方式是 private,struct 的默认继承方式是 public。
- 构造函数可以设置默认参数。
查看答案与解析
答案:√
解析: 构造函数可以设置默认参数,与普通函数的默认参数规则一致。需要注意:带全部默认参数的构造函数与无参构造函数不能同时存在,否则产生二义性。
难度: ⭐ 考点: #构造函数 #默认参数 #函数重载
💡 学习锦囊
📖 相关公式与知识点:
- 构造函数默认参数:与普通函数默认参数规则相同
- 默认参数必须从右向左连续设置
- 注意避免与重载构造函数产生二义性
易错点
- 带全部默认参数的构造函数与无参构造函数不能同时存在
🔄 举一反三
- 下列关于构造函数默认参数的描述,错误的是( )
- A. 构造函数的所有参数都可以设置默认值
- B. 默认参数必须从右向左连续设置
- C. 带全部默认参数的构造函数可以和无参构造函数共存
- D. 默认参数可以在声明中指定
查看练习答案与解析
答案:C 解析:带全部默认参数的构造函数与无参构造函数不能共存,否则产生二义性。
- 类的析构函数的作用是对象的初始化。
查看答案与解析
答案:×
解析: 析构函数的作用是对象销毁时的清理工作,不是初始化。初始化是构造函数的作用。
- 构造函数:对象创建时调用,用于初始化
- 析构函数:对象销毁时调用,用于清理(释放内存、关闭文件等)
难度: ⭐ 考点: #析构函数 #构造函数 #对象生命周期
💡 学习锦囊
📖 相关公式与知识点:
- 构造函数:对象创建时自动调用,完成初始化
- 析构函数:对象销毁时自动调用,完成清理工作
- 析构函数名:
~类名(),无参数,无返回值
易错点
- 析构函数不能重载,构造函数可以重载
- 析构函数的作用是"清理"而非"初始化"
🔄 举一反三
- 下列关于析构函数的描述,正确的是( )
- A. 析构函数可以有参数
- B. 析构函数可以重载
- C. 析构函数在对象销毁时自动调用
- D. 析构函数可以有返回值
查看练习答案与解析
答案:C 解析:析构函数无参数、不能重载、无返回值,在对象销毁时自动调用。
- 只有常成员函数才可以操作常对象。
查看答案与解析
答案:√
解析: 常对象(const 对象)只能调用常成员函数(const 成员函数)。因为常对象的成员数据不能被修改,而非常成员函数可能修改成员数据,编译器无法保证安全性。
难度: ⭐⭐ 考点: #常对象 #常成员函数 #const修饰符
💡 学习锦囊
📖 相关公式与知识点:
- 常对象:
const ClassName obj;,其成员数据不可修改 - 常成员函数:
void func() const;,承诺不修改成员数据 - 常对象只能调用常成员函数
mutable关键字修饰的成员变量可以在 const 函数中修改
易错点
- 常对象可以调用构造函数和析构函数(它们不受 const 限制)
🔄 举一反三
- 下列关于 const 成员函数的描述,错误的是( )
- A. const 成员函数不能修改非 mutable 成员
- B. const 对象可以调用非 const 成员函数
- C. const 成员函数和非 const 成员函数可以构成重载
- D. const 成员函数的 this 指针类型是 const ClassName*
查看练习答案与解析
答案:B 解析:const 对象不能调用非 const 成员函数。
- 模板类与类模板的意义完全相同。
查看答案与解析
答案:×
解析:
- 类模板:是一个模板,是用于生成类的蓝图/框架。如
template<typename T> class Stack {...} - 模板类:是类模板经过实例化后生成的具体的类。如
Stack<int>
template<typename T> // 类模板
class Stack { T data[100]; };
Stack<int> intStack; // 模板类(实例化后)2
3
难度: ⭐⭐ 考点: #类模板 #模板类 #模板实例化
💡 学习锦囊
📖 相关公式与知识点:
- 类模板:模板定义,含模板参数
- 模板类:类模板实例化后的具体类
易错点
- 类模板 → 模板(未实例化);模板类 → 具体类(已实例化)
🔄 举一反三
- 下列描述正确的是( )
- A. 类模板和模板类是同一概念
- B. 类模板实例化后得到模板类
- C. 模板类实例化后得到类模板
- D. 类模板不能被实例化
查看练习答案与解析
答案:B 解析:类模板是模板定义,实例化后得到具体的模板类。
- 若类 Y 是类 X 的私有派生类,类 Z 是类 Y 的公有派生类,则类 Z 不能访问类 X 的公有成员和保护成员。
查看答案与解析
答案:√
解析: 私有继承的传递效应:
- 类 Y 私有继承类 X → X 的 public/protected 成员在 Y 中变为 private
- 类 Z 公有继承类 Y → Y 的 private 成员在 Z 中不可访问
因此类 Z 无法访问类 X 的公有成员和保护成员。
难度: ⭐⭐⭐ 考点: #私有继承 #继承访问控制 #成员访问权限
💡 学习锦囊
📖 相关公式与知识点:
- 公有继承:基类 public→public,protected→protected
- 保护继承:基类 public/protected→protected
- 私有继承:基类 public/protected→private
思路分析
分析继承链中的访问权限,需要逐层推导。
易错点
- 私有继承使所有可继承成员变为 private,后续派生类无法再访问
🔄 举一反三
- 若类 Y 是类 X 的保护派生类,类 Z 是类 Y 的公有派生类,则类 Z 能否访问类 X 的公有成员?
查看练习答案与解析
答案:能 解析:Y 保护继承 X → X 的 public/protected 在 Y 中变为 protected → Z 公有继承 Y → Y 的 protected 在 Z 中仍为 protected → Z 可以访问。
- C++ 中设置虚基类的目的是实现运行时的多态。
查看答案与解析
答案:×
解析: 虚基类的目的是解决菱形继承中的二义性问题,不是实现运行时多态。
- 虚基类:解决多继承中同一基类被间接多次继承导致的二义性
- 虚函数:实现运行时多态
两者都用了 virtual 关键字,但用途完全不同。
难度: ⭐⭐ 考点: #虚基类 #虚函数 #菱形继承 #多态
💡 学习锦囊
📖 相关公式与知识点:
- 虚基类:
class Derived : virtual public Base,解决菱形继承二义性 - 虚函数:
virtual void func(),实现运行时多态 - 菱形继承:A → B, A → C, B+C → D,D 中有两份 A 的成员
- 虚基类使 D 中只保留一份 A 的成员
易错点
- 虚基类 ≠ 虚函数,两者用途完全不同
🔄 举一反三
- 下列关于 virtual 关键字的描述,正确的是( )
- A. 虚基类和虚函数的作用相同
- B. 虚基类用于解决多继承中的二义性
- C. 虚函数用于解决菱形继承问题
- D. virtual 只能用于成员函数
查看练习答案与解析
答案:B 解析:虚基类解决多继承二义性(B 对),虚函数实现运行时多态(C 错),virtual 还可用于虚基类声明(D 错)。
二、单项选择题(每题 2 分,总计 20 分)
- 下列关于面向对象概念的描述中,错误的是( )
- A.面向对象方法比面向过程方法更加先进
- B.面向对象方法中使用了一些面向过程方法中没有的概念
- C.面向对象方法替代了结构化程序设计方法
- D.面向对象程序设计方法要使用面向对象的程序设计语言
查看答案与解析
答案:C
解析: 面向对象方法并没有替代结构化程序设计方法,两者各有适用场景,在实际开发中常常结合使用。
- A 正确:面向对象方法确实更先进
- B 正确:引入了类、对象、继承、多态等概念
- C 错误:面向对象没有替代结构化程序设计,两者互补共存
- D 正确:需要使用支持面向对象特性的语言
难度: ⭐ 考点: #面向对象 #面向过程 #程序设计方法
💡 学习锦囊
📖 相关公式与知识点:
- 面向对象三大特征:封装、继承、多态
- 面向过程:以函数为中心
- 面向对象:以对象为中心
- 两者关系:互补而非替代
易错点
- "替代"一词过于绝对
🔄 举一反三
- 下列关于程序设计方法的描述,正确的是( )
- A. 面向对象方法完全优于面向过程方法
- B. 面向对象方法中不包含任何面向过程成分
- C. 面向对象和面向过程可以结合使用
- D. C 语言不能编写任何面向对象的程序
查看练习答案与解析
答案:C 解析:两种方法各有适用场景(A 错),面向对象中类的方法本身就是面向过程的(B 错),两者可以结合使用(C 对)。
- 在函数的引用调用中,函数的实参和形参分别应是( )
- A.变量值和变量
- B.地址值和指针
- C.变量名和引用
- D.地址值和引用
查看答案与解析
答案:C
解析:
- A:变量值和变量 → 值调用
- B:地址值和指针 → 指针调用
- C:变量名和引用 → 引用调用
- D:地址值和引用 → 不是标准引用调用
void swap(int &a, int &b) { int t = a; a = b; b = t; }
swap(x, y); // 实参是变量名,形参是引用2
难度: ⭐ 考点: #引用调用 #参数传递 #引用
💡 学习锦囊
📖 相关公式与知识点:
- 值调用:实参→形参复制,形参修改不影响实参
- 引用调用:形参是实参的引用(别名),修改形参即修改实参
- 指针调用:传递地址,通过解引用修改实参
思路分析
区分三种参数传递方式:看形参类型——普通变量是值调用,& 是引用调用,* 是指针调用。
🔄 举一反三
- 下列函数声明中,属于引用调用的是( )
- A.
void f(int x) - B.
void f(int *x) - C.
void f(int &x) - D.
void f(const int x)
查看练习答案与解析
答案:C 解析:A、D 是值调用,B 是指针调用,C 是引用调用。
- A.
void Set(A &a);是类 A 中一个成员函数的说明,其中A &a的含义是( )- A.类 A 的对象引用 a 作该函数的参数
- B.类 A 的对象 a 的地址值作函数的参数
- C.表达式变量 A 与变量 a 按位与作函数参数
- D.指向类 A 对象指针 a 作函数参数
查看答案与解析
答案:A
解析:A &a 表示 a 是类 A 对象的引用。& 在声明中表示引用,不是按位与(C 错),不是取地址(B 错),不是指针(D 错)。
难度: ⭐ 考点: #引用 #成员函数参数 #&运算符
💡 学习锦囊
📖 相关公式与知识点:
A &a:引用声明,a 是 A 类型对象的别名A *a:指针声明&三种含义:引用声明、取地址、按位与
易错点
&在类型声明中和在表达式中的含义完全不同
🔄 举一反三
- 设有声明
void f(int &x),下列调用正确的是( )- A.
f(10) - B.
f(&10) - C.
int a = 5; f(a) - D.
int a = 5; f(&a)
查看练习答案与解析
答案:C 解析:引用参数必须绑定到左值(变量),不能绑定到常量(A、B 错)。
f(a)正确(C 对),f(&a)传递地址类型不匹配(D 错)。 - A.
- 下列关于常成员的描述中,错误的是( )
- A.常成员是用关键字 const 说明的
- B.常成员有常数据成员和常成员函数两种
- C.常数据成员的初始化是在类体内定义它时进行的
- D.常数据成员的值是不可以改变的
查看答案与解析
答案:C
解析: 常数据成员不能在类体内定义时初始化,必须通过构造函数的初始化列表进行初始化。
class MyClass {
const int x; // 常数据成员
public:
MyClass() : x(10) {} // ✅ 必须通过初始化列表初始化
};2
3
4
5
难度: ⭐⭐ 考点: #常数据成员 #const #初始化列表
💡 学习锦囊
📖 相关公式与知识点:
- 常数据成员:
const 类型 名;,必须在初始化列表中初始化 - 常成员函数:
void func() const;,不能修改非 mutable 成员 - 引用成员也必须在初始化列表中初始化
易错点
- 常数据成员和引用成员都必须在初始化列表中初始化
🔄 举一反三
- 下列代码中,正确初始化常数据成员的是( )
- A.
Test() { a = 10; } - B.
Test() : a(10) {} - C. 类体内
const int a = 10; - D.
Test(int x) { a = x; }
查看练习答案与解析
答案:B 解析:常数据成员必须在初始化列表中初始化(B 对)。A、D 在函数体中赋值不是初始化,C 传统 C++ 不合法。
- A.
- 下列关于 this 的描述中,错误的是( )
- A.this 是一个由系统自动生成的指针
- B.this 指针是指向对象的
- C.this 指针在用对象引用成员函数时系统创建的
- D.this 指针只能隐含使用,不能显式使用
查看答案与解析
答案:D
解析: this 指针可以显式使用。常见场景:this->member 显式访问成员,return *this; 返回当前对象(链式调用)。
难度: ⭐⭐ 考点: #this指针 #成员函数 #隐式与显式使用
💡 学习锦囊
📖 相关公式与知识点:
- this 指针:隐含于每个非静态成员函数中,指向调用该函数的对象
- 类型:
ClassName *const this(常指针) - const 成员函数中:
const ClassName *const this - 常见显式使用:区分同名参数与成员、链式调用
易错点
- this 是常指针,不能改变指向
- 静态成员函数没有 this 指针
🔄 举一反三
- 下列关于 this 指针的描述,正确的是( )
- A. this 指针可以被修改指向其他对象
- B. 静态成员函数中可以使用 this 指针
- C. this 指针的类型取决于是否为 const 成员函数
- D. 全局函数中可以使用 this 指针
查看练习答案与解析
答案:C 解析:this 是常指针(A 错),静态成员函数没有 this(B 错),const 成员函数中 this 为
const ClassName *const(C 对),全局函数没有 this(D 错)。
- 下列关于运算符重载的描述中,错误的是( )
- A.运算符重载不改变优先级
- B.运算符重载后,原来运算符操作不可再用
- C.运算符重载不改变结合性
- D.运算符重载函数的参数个数与重载方式有关
查看答案与解析
答案:B
解析: 运算符重载后,原来的运算符操作仍然可用。重载只是增加了运算符的新功能,并不覆盖原有功能。例如重载了 + 用于自定义类后,int + int 仍然正常工作。
难度: ⭐⭐ 考点: #运算符重载 #重载规则
💡 学习锦囊
📖 相关公式与知识点:
- 运算符重载规则:不改变优先级、结合性、操作数个数
- 不能创建新运算符
- 不能重载的运算符:
..*::?:sizeof
易错点
=()[]->只能通过成员函数重载
🔄 举一反三
- 下列运算符中,不能被重载的是( )
- A.
+ - B.
= - C.
:: - D.
()
查看练习答案与解析
答案:C 解析:
::不能被重载。不能重载的运算符:..*::?:sizeof。 - A.
- 下列关于派生类的描述中,错误的是( )
- A.派生类至少有一个基类
- B.一个派生类可以作另一个派生类的基类
- C.派生类的构造函数中应包含直接基类的构造函数
- D.派生类默认的继承方式是 public
查看答案与解析
答案:D
解析: 派生类使用 class 关键字定义时,默认的继承方式是 private,不是 public。
class Derived : Base {}; // 默认 private 继承
struct Derived : Base {}; // 默认 public 继承2
难度: ⭐ 考点: #派生类 #继承方式 #默认继承
💡 学习锦囊
📖 相关公式与知识点:
- class 默认继承方式:private
- struct 默认继承方式:public
易错点
- 与 class 默认成员访问权限为 private 一致,class 默认继承方式也是 private
🔄 举一反三
- 下列代码中,继承方式为 private 的是( )
- A.
class D : public B {}; - B.
class D : protected B {}; - C.
class D : B {}; - D.
struct D : B {};
查看练习答案与解析
答案:C 解析:class 未指定继承方式时默认为 private。
- A.
- 下列运算符中,不可以重载的是( )
- A.&&
- B.&
- C.[ ]
- D..*
查看答案与解析
答案:D
解析:.*(成员指针访问运算符)不能被重载。C++ 中不能重载的运算符:. .* :: ?: sizeof
难度: ⭐ 考点: #运算符重载 #不可重载运算符
💡 学习锦囊
📖 相关公式与知识点:
- 不可重载运算符:
..*::?:sizeof - 只能成员函数重载:
=()[]->
思路分析
记忆口诀:"点星域问sizeof"
🔄 举一反三
- 下列运算符中,只能通过成员函数重载的是( )
- A.
+ - B.
<< - C.
= - D.
==
查看练习答案与解析
答案:C 解析:
=()[]->只能通过成员函数重载。 - A.
- 下列的成员函数中,纯虚函数是( )
- A.
virtual void f1() = 0; - B.
void f1() = 0; - C.
virtual void f1() {} = 0; - D.含非法标记的声明
- A.
查看答案与解析
答案:A
解析: 纯虚函数的正确声明格式:virtual 返回类型 函数名(参数列表) = 0;
- A 正确:标准纯虚函数声明
- B 错误:缺少
virtual - C 错误:纯虚函数声明中不能同时包含函数体
{}和= 0(纯虚函数可以在类外单独定义函数体,但声明中不能同时出现) - D 错误:非法声明
难度: ⭐ 考点: #纯虚函数 #抽象类 #virtual
💡 学习锦囊
📖 相关公式与知识点:
- 纯虚函数:
virtual 返回类型 函数名(参数) = 0; - 含纯虚函数的类为抽象类,不能创建对象
- 派生类必须实现所有纯虚函数才能成为具体类
易错点
= 0必须出现在声明中
🔄 举一反三
- 下列关于纯虚函数的描述,正确的是( )
- A. 纯虚函数可以有函数体
- B. 含纯虚函数的类可以创建对象
- C. 纯虚函数不需要 virtual 关键字
- D. 纯虚函数必须在基类中实现
查看练习答案与解析
答案:A 解析:C++11 起纯虚函数可以有函数体(A 对),含纯虚函数的类是抽象类不能创建对象(B 错),必须有 virtual(C 错),不需要在基类中实现(D 错)。
- 下列关于抽象类的描述中,错误的是( )
- A.抽象类中至少应该有一个纯虚函数
- B.抽象类可以定义对象指针和对象引用
- C.抽象类通常用作类族中最顶层的类
- D.抽象类的派生类不再是抽象类
查看答案与解析
答案:D
解析: 抽象类的派生类不一定不再是抽象类。如果派生类没有实现基类中的所有纯虚函数,则派生类仍然是抽象类。
难度: ⭐⭐ 考点: #抽象类 #纯虚函数 #派生类
💡 学习锦囊
📖 相关公式与知识点:
- 抽象类:含至少一个纯虚函数的类
- 抽象类不能创建对象,但可以定义指针和引用
- 派生类必须实现所有纯虚函数才能成为具体类
易错点
- 只有实现了所有纯虚函数的派生类才不是抽象类
🔄 举一反三
- 下列关于抽象类的描述,正确的是( )
- A. 抽象类不能有构造函数
- B. 抽象类不能有数据成员
- C. 抽象类的派生类如果实现了所有纯虚函数,则可以创建对象
- D. 抽象类只能有一个纯虚函数
查看练习答案与解析
答案:C 解析:抽象类可以有构造函数(A 错)、数据成员(B 错),可以有多个纯虚函数(D 错),派生类实现所有纯虚函数后成为具体类可创建对象(C 对)。
三、程序填空题(每空 2 分,共计 12 分)
- 请在下面程序的横线处填上适当内容,以使程序完整,并使程序的输出为:
Base's cons.
Derived's cons.
Derived's des.
Base's des.2
3
4
#include <iostream>
class Base {
public:
Base() {cout << "Base's cons." << endl;}
(1)______________________
};
class Derived : public Base {
public:
Derived() {cout << "Derived's cons." << endl;}
(2)______________________
};
int main() {
Base *Ptr = (3)______________________
delete ptr;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
查看答案与解析
答案:
(1) virtual ~Base() {cout << "Base's des." << endl;}
(2) ~Derived() {cout << "Derived's des." << endl;}
(3) new Derived;
解析:
根据输出分析:
Base's cons.→ 创建 Derived 对象时先调用 Base 构造函数Derived's cons.→ 再调用 Derived 构造函数Derived's des.→ delete 时先调用 Derived 析构函数Base's des.→ 再调用 Base 析构函数
(1) 定义 Base 的虚析构函数(必须加 virtual,否则通过基类指针 delete 派生类对象时不会调用 Derived 的析构函数),(2) 定义 Derived 析构函数,(3) 创建 Derived 对象。
难度: ⭐⭐ 考点: #构造与析构顺序 #虚析构函数 #基类指针
💡 学习锦囊
📖 相关公式与知识点:
- 构造顺序:基类构造 → 派生类构造
- 析构顺序:派生类析构 → 基类析构
- 虚析构函数:
virtual ~Base() {},确保通过基类指针 delete 派生类对象时正确调用派生类析构函数
易错点
- 如果 Base 的析构函数不是虚函数,
delete ptr只会调用 Base 的析构函数,造成内存泄漏
🔄 举一反三
- 如果 Base 的析构函数不是 virtual,执行
Base *p = new Derived; delete p;会怎样?查看练习答案与解析
答案:只会调用 Base 的析构函数,不会调用 Derived 的析构函数,造成内存泄漏。 解析:没有虚析构函数时,
delete p根据指针类型静态绑定调用 Base 的析构函数。
- 实现下列求字符串长度的函数。
int strlen(char *str) {
int len;
len = 0;
while (*str) {
(3)______________________
(4)______________________
}
return len;
}2
3
4
5
6
7
8
9
查看答案与解析
答案:
(3) len++;
(4) str++;
解析:
while (*str):当 str 指向的字符不为'\0'时继续循环- (3)
len++:每遍历一个字符,长度计数器加 1 - (4)
str++:指针后移,指向下一个字符
难度: ⭐ 考点: #指针运算 #字符串遍历 #strlen实现
💡 学习锦囊
📖 相关公式与知识点:
- 字符串以
'\0'结尾 *str获取指针指向的字符str++指针后移一个字符位置
思路分析
字符串长度 = 非结束符字符的个数。遍历字符串,计数加1,指针后移。
🔄 举一反三
- 实现字符串复制函数
void strcpy(char *dest, char *src)查看练习答案与解析
答案:
cppvoid strcpy(char *dest, char *src) { while (*src) { *dest = *src; dest++; src++; } *dest = '\0'; }1
2
3
4解析:遍历源字符串逐字符复制,最后添加结束符。更简洁写法:
while (*dest++ = *src++);
- 下面程序通过把类 Distance 声明为类 Point 的友元类来实现计算两点之间的距离。请在下面程序的横线处填上适当字句,以使程序完整。
#include <iostream>
#include <cmath>
using namespace std;
class Point {
double X, Y;
public:
Point(double x, double y) {X = x; Y = y;}
(5)______________________
};
class Distance {
public:
double Dis(Point& p1, Point& p2);
};
double Distance::Dis(Point& p1, Point& p2) {
double t;
(6)______________________
return t;
}
int main() {
Point p(10, 10), q(20, 20);
Distance d;
cout << d.Dis(p, q) << endl;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
查看答案与解析
答案:
(5) friend class Distance;
(6) t = sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));
解析:
(5) 声明 Distance 为 Point 的友元类,使 Distance 可以访问 Point 的私有成员 X 和 Y。
(6) 使用两点间距离公式:$d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}$
代入 p(10,10) 和 q(20,20):$d = \sqrt{(10-20)^2 + (10-20)^2} = \sqrt{200} \approx 14.14$
难度: ⭐⭐ 考点: #友元类 #两点间距离 #私有成员访问
💡 学习锦囊
📖 相关公式与知识点:
- 友元类声明:
friend class ClassName; - 友元类的所有成员函数都可以访问该类的私有成员
- 两点间距离公式:$d = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$
易错点
- 友元关系是单向的
- 友元关系不能继承
- 友元破坏封装性,应谨慎使用
🔄 举一反三
- 如果不用友元类,还有什么方式可以让 Distance 访问 Point 的私有成员?
查看练习答案与解析
答案:提供公有的 getter 函数。 解析:
cppclass Point { double X, Y; public: double getX() const { return X; } double getY() const { return Y; } };1
2
3
4
5
6然后使用
p1.getX()代替p1.X,保持封装性。
四、阅读程序题(共计 30 分)
- 阅读该程序,给出程序的输出结果。(6 分)
#include <iostream>
class Test {
static int n;
public:
Test() {n += 2;}
~Test() {n -= 3;}
static int GetNum() {return n;}
};
int Test::n = 1;
int main() {
Test t, *p;
p = &t;
cout << "n=" << p->GetNum() << endl;
p = new Test;
delete p;
cout << "n=" << Test::GetNum() << endl;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
查看答案与解析
答案:
n=3
n=22
解析:
int Test::n = 1;→ n = 1Test t;→ 构造函数 n += 2 → n = 3p = &t;→ p 指向 t(不调用构造函数)p->GetNum()→ 输出 n=3p = new Test;→ 构造函数 n += 2 → n = 5delete p;→ 析构函数 n -= 3 → n = 2Test::GetNum()→ 输出 n=2
难度: ⭐⭐ 考点: #静态成员变量 #构造与析构 #对象生命周期
💡 学习锦囊
📖 相关公式与知识点:
- 静态成员变量:属于类,所有对象共享一份
- 必须在类外初始化:
类型 类名::变量名 = 初始值;
思路分析
追踪静态变量:每次构造 +2,每次析构 -3,按执行顺序逐步计算。
易错点
Test t, *p;只创建了一个对象 t,指针 p 不调用构造函数p = &t;不创建新对象
🔄 举一反三
- 如果将
delete p;这行去掉,程序输出什么?查看练习答案与解析
答案:
n=3 n=51
2解析:去掉
delete p后不调用析构函数,n 不减 3,为 5。但会导致内存泄漏。
- 阅读该程序,给出程序的输出结果。(6 分)
#include <iostream>
class A {
int num;
public:
A() {num = 0; cout << "A default constructor" << endl;}
A(int n) {num = n; cout << "A constructor,num=" << num << endl;}
~A() {cout << "A destructor,num=" << num << endl;}
};
int main() {
A a, *p;
p = new A(2);
a = *p;
delete p;
cout << "Exiting main" << endl;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
查看答案与解析
答案:
A default constructor
A constructor,num=2
A destructor,num=2
Exiting main
A destructor,num=22
3
4
5
解析:
A a;→ 默认构造,num=0,输出A default constructorp = new A(2);→ 带参构造,num=2,输出A constructor,num=2a = *p;→ 赋值操作(非拷贝构造),a.num 变为 2,无输出delete p;→ 析构,num=2,输出A destructor,num=2- 输出
Exiting main - a 离开作用域 → 析构,a.num=2,输出
A destructor,num=2
难度: ⭐⭐⭐ 考点: #构造函数 #析构函数 #赋值与拷贝 #对象生命周期
💡 学习锦囊
📖 相关公式与知识点:
- 赋值操作 vs 拷贝构造:
a = *p是赋值,A a(*p)是拷贝构造 - 赋值不调用构造函数,只执行逐成员复制
易错点
a = *p是赋值不是拷贝构造,不产生构造函数输出- 赋值后 a.num 变为 2,析构时输出 num=2
🔄 举一反三
- 如果将
a = *p;改为A a2(*p);,程序输出有什么变化?查看练习答案与解析
答案:多一个 a2 的析构输出:
A default constructor A constructor,num=2 A destructor,num=2 Exiting main A destructor,num=2 A destructor,num=21
2
3
4
5
6解析:a2 通过拷贝构造创建(num=2),在 main 结束后析构。
- 阅读该程序,给出程序的输出结果。(6 分)
#include <iostream>
class S {
public:
S() { PC = 0; }
S(S &s) { PC = s.PC; for(int i = 0; i < PC; i++) elements[i] = s.elements[i]; }
void Empty() { PC = 0; }
int IsEmpty() { return PC == 0; }
int IsMemberOf(int n);
int Add(int n);
void Print();
private:
int elements[100], PC;
};
int S::IsMemberOf(int n) {
for(int i = 0; i < PC; i++)
if(elements[i] == n) return 1;
return 0;
}
int S::Add(int n) {
if(IsMemberOf(n)) return 1;
else if(PC == 100) return 0;
else { elements[PC++] = n; return 1; }
}
void S::Print() {
cout << '{';
for(int i = 0; i < PC - 1; i++)
cout << elements[i] << ',';
if(PC > 0) cout << elements[PC - 1];
cout << '}' << endl;
}
int main() {
S a;
cout << a.IsEmpty() << endl;
a.Print();
S b;
for(int i = 1; i <= 5; i++) b.Add(i);
b.Print();
cout << b.IsMemberOf(3) << endl;
cout << b.IsEmpty() << endl;
for(i = 6; i <= 10; i++) b.Add(i);
S c(b);
c.Print();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
查看答案与解析
答案:
1
{}
{1,2,3,4,5}
1
0
{1,2,3,4,5,6,7,8,9,10}2
3
4
5
6
解析:
S a;→ PC=0a.IsEmpty()→ PC==0 返回 1,输出1a.Print()→ PC=0,输出{}S b;→ PC=0for(i=1; i<=5; i++) b.Add(i)→ 添加 1,2,3,4,5,PC=5b.Print()→ 输出{1,2,3,4,5}b.IsMemberOf(3)→ 3 在集合中,返回 1,输出1b.IsEmpty()→ PC=5≠0,返回 0,输出0for(i=6; i<=10; i++) b.Add(i)→ PC=10S c(b)→ 拷贝构造,复制 b 的所有元素c.Print()→ 输出{1,2,3,4,5,6,7,8,9,10}
难度: ⭐⭐⭐ 考点: #集合类 #拷贝构造函数 #成员函数
💡 学习锦囊
📖 相关公式与知识点:
- 集合类 S:用数组存储元素,PC 记录元素个数
- Add:先检查是否已存在,不存在则添加(保证元素不重复)
- Print:按
{e1,e2,...,en}格式输出
易错点
- Add 函数中元素已存在时返回 1 但不重复添加
- Print 函数最后一个元素后不加逗号
- PC=0 时 Print 输出
{}
🔄 举一反三
- 如果在
S c(b);之后执行c.Add(3); c.Print();,输出什么?查看练习答案与解析
答案:
{1,2,3,4,5,6,7,8,9,10}解析:3 已存在,Add 不重复添加,集合内容不变。
- 阅读该程序,给出程序的输出结果。(6 分)
#include <iostream>
class Format {
public:
virtual void header() {cout << "This is a head" << endl;}
virtual void footer() {cout << "This is a footer" << endl;}
virtual void body() {cout << "This is a body" << endl;}
void display() {header(); body(); footer();}
};
class MyFormat : public Format {
public:
void header() {cout << "This is my header" << endl;}
void footer() {cout << "This is my footer" << endl;}
};
int main() {
Format *p;
p = new Format;
p->display();
delete p;
p = new MyFormat;
p->display();
delete p;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
查看答案与解析
答案:
This is a head
This is a body
This is a footer
This is my header
This is a body
This is my footer2
3
4
5
6
解析:
p = new Format; p->display();- p 指向 Format 对象,调用 Format 的虚函数
- 输出:
This is a head/This is a body/This is a footer
p = new MyFormat; p->display();- p 指向 MyFormat 对象,虚函数动态绑定
- header():MyFormat 重写了 →
This is my header - body():MyFormat 未重写 →
This is a body(基类版本) - footer():MyFormat 重写了 →
This is my footer
难度: ⭐⭐ 考点: #虚函数 #运行时多态 #动态绑定
💡 学习锦囊
📖 相关公式与知识点:
- 虚函数:
virtual void func(),支持运行时多态 - 动态绑定:通过基类指针/引用调用虚函数时,根据实际对象类型选择函数版本
- 未重写的虚函数:派生类使用基类的版本
思路分析
分析虚函数调用的关键:看指针实际指向的对象类型,而非指针的声明类型。
易错点
- display() 不是虚函数,但内部调用的虚函数仍会动态绑定
- MyFormat 只重写了 header() 和 footer(),body() 使用基类版本
🔄 举一反三
- 如果将 Format 类中的
virtual void header()的 virtual 去掉,p = new MyFormat; p->display();的输出会怎样?查看练习答案与解析
答案:
This is a head This is a body This is my footer1
2
3解析:header() 不再是虚函数,
p->header()根据指针类型(Format*)静态绑定调用 Format::header()。footer() 仍是虚函数,正常动态绑定。
- 阅读该程序,给出程序的输出结果。(6 分)
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
void DisplayVector(vector<T>& vecInput) {
for(typename vector<T>::iterator iElement = vecInput.begin();
iElement != vecInput.end(); ++iElement)
cout << *iElement << ' ';
cout << endl;
}
int main() {
vector<int> vecIntegers;
vecIntegers.push_back(50);
vecIntegers.push_back(1);
vecIntegers.insert(vecIntegers.begin(), 987);
vecIntegers.insert(vecIntegers.begin(), 1001);
cout << "Vector contains " << vecIntegers.size() << " elements: ";
DisplayVector(vecIntegers);
vecIntegers.pop_back();
cout << "After a call to pop_back()" << endl;
cout << "Vector contains " << vecIntegers.size() << " elements: ";
DisplayVector(vecIntegers);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
查看答案与解析
答案:
Vector contains 4 elements: 1001 987 50 1
After a call to pop_back()
Vector contains 3 elements: 1001 987 502
3
解析:
vector<int> vecIntegers;→ 空向量 {}push_back(50)→push_back(1)→insert(begin(), 987)→ 在开头插入 987 →insert(begin(), 1001)→ 在开头插入 1001 →- 输出:
Vector contains 4 elements: 1001 987 50 1 pop_back()→ 删除末尾元素 1 →- 输出:
After a call to pop_back() - 输出:
Vector contains 3 elements: 1001 987 50
难度: ⭐⭐ 考点: #STL容器 #vector #迭代器 #模板函数
💡 学习锦囊
📖 相关公式与知识点:
push_back(val):在末尾添加元素pop_back():删除末尾元素insert(pos, val):在指定位置插入元素begin():返回指向首元素的迭代器size():返回元素个数
思路分析
注意 insert(begin(), val) 是在开头插入,每次插入都会使已有元素后移。push_back 在末尾添加。
易错点
insert的第一个参数是迭代器位置,不是索引pop_back()删除的是最后一个元素
🔄 举一反三
- 如果在上述程序中,在
pop_back()之后执行vecIntegers.insert(vecIntegers.begin() + 1, 500);,vector 的内容是什么?查看练习答案与解析
答案:{1001, 500, 987, 50} 解析:pop_back 后 vector 为 {1001, 987, 50},
begin() + 1指向第二个位置(987 前),插入 500 后为 {1001, 500, 987, 50}。
五、编程题(共计 28 分)
- 按下列要求编程(12 分)
定义一个描述矩形的类 Rectangle,包括的数据成员有宽(width)和长(length),并实现如下功能函数:
(1)矩形对象初始化; (2)计算矩形周长; (3)计算矩形面积; (4)改变矩形大小; (5)重载插入运算符(<<),实现矩形的长、宽、周长和体积的输出。
查看答案与解析
答案:
#include <iostream>
using namespace std;
class Rectangle {
private:
double width;
double length;
public:
Rectangle(double w = 0, double l = 0) : width(w), length(l) {}
double Perimeter() const {
return 2 * (width + length);
}
double Area() const {
return width * length;
}
void Resize(double w, double l) {
width = w;
length = l;
}
friend ostream& operator<<(ostream& os, const Rectangle& r);
};
ostream& operator<<(ostream& os, const Rectangle& r) {
os << "Length: " << r.length << endl;
os << "Width: " << r.width << endl;
os << "Perimeter: " << r.Perimeter() << endl;
os << "Area: " << r.Area() << endl;
return os;
}
int main() {
Rectangle r(3, 4);
cout << r << endl;
r.Resize(5, 6);
cout << r << endl;
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
解析:
- 构造函数:
Rectangle(double w = 0, double l = 0)实现初始化,带默认参数 - 周长:
Perimeter()返回 $2 \times (width + length)$ - 面积:
Area()返回 $width \times length$ - 改变大小:
Resize(double w, double l)修改 width 和 length - 重载 <<:使用友元函数实现,输出长、宽、周长和面积(题目说"体积",但矩形是2D图形,应为面积)
难度: ⭐⭐ 考点: #类的定义 #运算符重载 #友元函数 #构造函数
💡 学习锦囊
📖 相关公式与知识点:
- 矩形周长:$P = 2 \times (w + l)$
- 矩形面积:$A = w \times l$
- 重载
<<运算符:必须使用友元函数,返回ostream&支持链式调用 - 构造函数初始化列表:
Rectangle(double w, double l) : width(w), length(l) {}
思路分析
重载 << 的标准模式:friend ostream& operator<<(ostream& os, const ClassName& obj),返回 os 引用以支持链式调用。
易错点
<<重载必须返回ostream&引用<<重载不能用成员函数(左操作数是 ostream)- 题目说"体积",但矩形是二维图形,应为"面积"
🔄 举一反三
- 如果要给 Rectangle 类添加重载
>>运算符(从键盘输入长和宽),如何实现?查看练习答案与解析
答案:
cppfriend istream& operator>>(istream& is, Rectangle& r) { is >> r.length >> r.width; return is; }1
2
3
4解析:
>>重载也使用友元函数,第二个参数不能是 const(需要修改),返回istream&支持链式输入。
- 设计一个 double 类型的数组类模板(CDblArray),要求 CDblArray 可以进行如下操作:(16 分)
(1)数组对象中存储元素的个数可以在定义的时候任意设定; (2)可以重新设置数组对象中存储元素的个数(Resize); (3)可以通过下标运算符返回数组元素,并通过异常机制,处理下标越界异常; (4)可以利用已知数组对象对另一个数组对象赋值和初始化; (5)可以返回当前数组对象中存储元素的个数(Size)。
查看答案与解析
答案:
#include <iostream>
#include <stdexcept>
using namespace std;
template<typename T>
class CDblArray {
private:
T *data;
int size;
public:
CDblArray(int s = 0) : size(s) {
if(size > 0)
data = new T[size];
else
data = nullptr;
}
CDblArray(const CDblArray& other) : size(other.size) {
if(size > 0) {
data = new T[size];
for(int i = 0; i < size; i++)
data[i] = other.data[i];
} else {
data = nullptr;
}
}
~CDblArray() {
delete[] data;
}
CDblArray& operator=(const CDblArray& other) {
if(this != &other) {
delete[] data;
size = other.size;
if(size > 0) {
data = new T[size];
for(int i = 0; i < size; i++)
data[i] = other.data[i];
} else {
data = nullptr;
}
}
return *this;
}
void Resize(int newSize) {
delete[] data;
size = newSize;
if(size > 0) {
data = new T[size];
for(int i = 0; i < size; i++)
data[i] = T();
} else {
data = nullptr;
}
}
T& operator[](int index) {
if(index < 0 || index >= size)
throw out_of_range("Index out of range!");
return data[index];
}
const T& operator[](int index) const {
if(index < 0 || index >= size)
throw out_of_range("Index out of range!");
return data[index];
}
int Size() const {
return size;
}
};
int main() {
CDblArray<double> arr(5);
for(int i = 0; i < arr.Size(); i++)
arr[i] = i * 1.5;
CDblArray<double> arr2 = arr; // 拷贝构造
CDblArray<double> arr3;
arr3 = arr; // 赋值
arr2.Resize(10);
cout << "arr2 size: " << arr2.Size() << endl;
try {
cout << arr[10] << endl; // 越界访问
} catch(const out_of_range& e) {
cout << "Exception: " << e.what() << endl;
}
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
解析:
- (1) 任意设定元素个数:构造函数
CDblArray(int s = 0)接受大小参数 - (2) Resize:
Resize(int newSize)重新分配内存 - (3) 下标运算符 + 异常:
operator[]检查越界,抛出out_of_range异常 - (4) 赋值和初始化:拷贝构造函数和赋值运算符重载
- (5) Size:
Size()返回当前元素个数
难度: ⭐⭐⭐ 考点: #类模板 #动态内存管理 #运算符重载 #异常处理 #拷贝构造 #赋值运算符
💡 学习锦囊
📖 相关公式与知识点:
- 类模板:
template<typename T> class ClassName - 动态内存管理:new/delete 配对使用
- 拷贝构造函数:深拷贝,避免浅拷贝问题
- 赋值运算符重载:注意自赋值检查和内存释放
- 异常处理:
throw out_of_range("msg")+try/catch - 三法则(Rule of Three):如果定义了析构函数、拷贝构造函数、赋值运算符之一,则应定义全部三个
思路分析
设计数组类模板的关键步骤:
- 数据成员:指针 + 大小
- 构造/析构:分配/释放内存
- 拷贝构造/赋值:深拷贝
- 下标运算符:边界检查 + 异常
- Resize:重新分配内存
易错点
- 赋值运算符重载必须检查自赋值(
if(this != &other)) - 赋值前必须释放原有内存(
delete[] data) - 拷贝构造和赋值必须实现深拷贝
- Resize 会丢失原有数据(如需保留,应先复制再释放)
🔄 举一反三
- 如果要实现 Resize 时保留原有数据,应该如何修改?
查看练习答案与解析
答案:
cppvoid Resize(int newSize) { T *newData = new T[newSize]; int minSize = (size < newSize) ? size : newSize; for(int i = 0; i < minSize; i++) newData[i] = data[i]; for(int i = minSize; i < newSize; i++) newData[i] = T(); delete[] data; data = newData; size = newSize; }1
2
3
4
5
6
7
8
9
10
11解析:先分配新内存,复制原有数据(取新旧大小的较小值),新空间初始化为默认值,再释放旧内存,更新指针和大小。