计算机二级cpp学习------选择题【一】
选择题
1、数据库系统中支持安全性定义和检查的语言是 (A)
A、数据控制语言
B、数据定义语言
C、数据操纵语言
D、以上说法都不对
本题考查知识点是数据定义语言的概念.
数据定义语言(DDL): 该语言负责数据的模式定义与数据的物理存取构建
数据操纵语言(DML): 该语言负责数据的操纵, 包括查询及增、删、改等操作
数据控制语言(DCL): 该语言负责数据完整性、安全性的定义与检查以及并发控制、故障恢复等功能
所以本题答案为A.
数据控制语言(DCL, Data Control Language): 主要负责安全性和权限管理, 其核心关键字是 GRANT(授权)和 REVOKE(收回权限), 这正是用来定义和检查用户访问安全性的语句.
数据定义语言(DDL, Data Definition Language): 负责定义数据库的结构, 比如创建、修改或删除表、索引等(CREATE、ALTER、DROP), 它管的是框架, 不涉及权限安全.
数据操纵语言(DML, Data Manipulation Language): 负责对数据内容进行增删改查(INSERT、DELETE、UPDATE、SELECT), 它管的是内容, 不涉及权限控制.
因此, 专门负责安全性定义和检查的只能是数据控制语言(DCL).
2、在医院, 每个医生只属于一个诊疗科室, 而一名患者可在多个科室治疗。则实体医生和实体患者之间的联系是 (A)
A、多对多
B、多对一
C、一对多
D、一对一
本题考查知识点是实体的联系, 数据库实体联系模型(E-R图)中对业务逻辑的理解.
多对多联系表现为一个表中多条记录在相关表中有同样的多条记录与其匹配, 医生同一天可为多位患者看病, 而一名患者可在多个科室治疗, 实体医生和患者之间的联系是多对多, 所以本题答案为A.
-
从医生的角度看: 每个医生只属于一个科室, 但一名医生会接诊多名患者.
-
从患者的角度看: 一名患者可以在多个科室治疗, 意味着他/她会挂不同科室的号, 接触多名医生.
因为医生可以对应多个患者, 患者也可以对应多个医生, 所以实体医生和实体患者之间的联系是多对多关系.
E-R图
E-R图(Entity-Relationship Diagram, 实体-联系图)是数据库设计中用来描述现实世界事物及其关系的模型图.
一、E-R图的三大核心要素
E-R图由三个基本构件组成, 形状固定, 识别它们就能看懂任何E-R图:
| 图形 | 名称 | 代表含义 | 举例 |
|---|---|---|---|
| 矩形 (□) | 实体 | 客观存在且可区分的事物(名词) | 学生、医生、科室、汽车 |
| 椭圆形 (○) | 属性 | 实体或联系所具有的特征(名词) | 学号、姓名、年龄、工资 |
| 菱形 (◇) | 联系 | 实体与实体之间的关系(动词) | 选修(学生↔课程)、驾驶(员工↔汽车) |
特别强调 : 主键 (能唯一标识实体的属性)通常在属性下方加上下划线 , 例如
学号下面画一条横线.
二、联系类型的三种关系
菱形框里标注的联系, 其度数 (涉及实体个数)最常见的是二元联系, 即两个实体之间的关系. 根据数量对应, 分为三类:
| 联系类型 | 图形标记 | 含义 | 现实例子 |
|---|---|---|---|
| 1 : 1 (一对一) | 在两端箭头处标 1 |
实体A的一个实例最多对应实体B的一个实例, 反之亦然 | 员工 ↔ 办公桌(一个员工只能坐一张桌子, 一张桌子只能坐一个员工) |
| 1 : N (一对多) | 一端标 1,多端标 N |
实体A的一个实例对应实体B的多个实例, 但B的一个实例只对应A的一个实例 | 科室 ↔ 医生(一个科室有多名医生, 但一名医生只属于一个科室) |
| M : N (多对多) | 两端标 M 和 N |
实体A的一个实例对应实体B的多个实例, 反之亦然 | 学生 ↔ 课程(一个学生选多门课, 一门课被多名学生选) |
关系R经过运算 σ A = B ∧ C > 4 ∧ D > 3 ( R ) \sigma_{A=B \land C>4 \land D>3}(R) σA=B∧C>4∧D>3(R)的结果为 (A)

A、(c, c, 11, 4)
B、(e, e, 6, 1)
C、(a, a, 2, 4)
D、(a, a, 2, 4)和(e, e, 6, 1)
运算符号含义 σ F ( R ) \sigma_{F}(R) σF(R)的意思是从关系 R R R中, 选出所有满足条件 F F F(公式)的元组(行), 可以把它理解为 Excel 里的筛选功能.
题目中的条件是: A = B ∧ C > 4 ∧ D > 3 A=B \land C>4 \land D>3 A=B∧C>4∧D>3
∧ 是逻辑且的意思, 意味着必须同时满足以下三个条件:
-
条件1: A = B(同一行中, A列的值必须等于B列的值)
-
条件2: C > 4(C列的值必须大于4)
-
条件3: D > 3(D列的值必须大于3)
(\land)(且)是交集, 必须全对; (\lor)(或)是并集, 对一个就行.
4、在线性表的链式存储结构中, 其存储空间一般是不连续的, 并且 ©
A、前件结点的存储序号小于后件结点的存储序号
B、前件结点的存储序号大于后件结点的存储序号
C、前件结点的存储序号可以小于也可以大于后件结点的存储序号
D、以上选项都不对
本题考查知识点是线性表的链式存储结构.
在线性表的链式存储结构中, 存储数据结构的存储空间可以不连续, 各数据结点的存储顺序与数据元素之间的逻辑关系可以不一致, 而数据元素之间的逻辑关系是由指针域来确定的. 在使用链表时, 关心的只是它所表示的线性表中数据元素之间的逻辑顺序, 而不是每个数据元素在存储器中的实际位置, 即前件结点的存储序号可以小于也可以大于后件结点的存储序号。所以本题答案为C.
链式存储(比如单链表)的逻辑特点是: 数据元素在物理内存中的存放位置是任意的、不连续的.
每个结点除了存数据, 还会存一个 指针(或叫链域), 用来指向下一个结点的实际物理地址.
因此, 前件结点(前驱)和后件结点(后继)在内存中的存储序号(地址编号)没有任何大小关系.
核心结论: 链表中前驱的地址可以小于后继的地址(比如前驱在内存低地址, 后继在高地址), 也完全可以大于后继的地址(比如前驱在高地址, 后继在低地址). 只要指针能指到对方, 顺序就由指针决定, 和地址大小无关.
(前件序号 < 后件序号): 这是顺序存储结构(比如数组)的特征, 数组在内存中连续分配, 所以前驱元素确实在较低的地址, 后继在较高的地址.
5、某二叉树中有15个度为1的结点, 16个度为2的结点, 则该二树中总的结点数为 ©
A、32
B、46
C、48
D、49
本题考查的知识点是二又树
在任意一棵二叉树中, 度为0的结点(即叶子结点)总是比度为2的结点多一个, 总结点个数就是度为0的加上度为1的再加上度为2的结点, 即总结点个数17 + 15 + 16 = 48.
所以本题答案为C.
📖 详细解析
二叉树(Binary tree)的核心公式: 在任意一棵二叉树中, 叶子结点(度为0的结点)总数 比 度为2的结点总数 多 1 个, 公式为:
n₀ = n₂ + 1
其中, n₀ 是度为0的结点(叶子), n₂ 是度为2的结点.
度为2的树和二叉树没多大关系, 二叉树严格区分左右子树
数据结构合集 - 二叉树&完全二叉树(定义, 性质)_哔哩哔哩_bilibili

满二叉树

完全二叉树

满二叉树是完全二叉树的特例.
二叉树性质








6、某系统结构图如下图所示

该系统结构图中最大扇入是 ©
A、0
B、1
C、2
D、3
本题考查知识点是扇入和扇出, 考察的是软件工程中系统结构图的扇入概念.
扇入, 是直接调用该模块的上级模块的个数, 扇入的大小代表着该模块被调用的频繁度, 扇入越大表明该模块使用度高, 扇入小表明该模块被调用的机率低. 所以该系统结构图中最大扇入是功能3.1的扇入数, 即为2, 所以本题答案为C.
扇入 (Fan-in): 指直接调用该模块的上级模块个数, 简单说, 就是有多少个上级直接连着它.
扇出 (Fan-out): 指该模块直接调用的下级模块个数.
7、下面不属于对象主要特征的是 (D)
A、对象唯一性
B、对象依赖性
C、对象继承性
D、对象持久性
本题者查知识点是对象主要特征
面向对象方法中最基本的概念是对象, 它的基本特点有: 唯一性、抽象性、继承性、多态性, 所以本题答案为D.
在面向对象(Object-Oriented)的理论体系中, 一个对象通常被定义为具有以下三个核心特征:
- 对象唯一性: 每个对象都有一个唯一的标识(类似于人的身份证号), 即使两个对象的所有属性值都相同, 它们在内存中也是不同的实体;
- 对象分类性(或称抽象性): 将具有相同属性和操作的对象归纳(抽象)成类;
- 对象继承性: 子类可以继承父类的属性和操作, 这是软件复用的重要手段.
- 对象持久性 指的是将对象的状态保存到数据库或磁盘文件中, 以便在程序关闭后重新加载. 这属于存储机制, 是持久化框架(如 Hibernate、MyBatis)探讨的范畴.
- 而面向对象的基本特征只关心对象在内存运行期间的性质(唯一性、分类性、继承性、封装性、多态性等), 不涉及数据如何永久保存.
8、设有表示公司和员工及雇佣的三张表, 员工可在多家公司兼职, 其中公司C(公司号, 公司名, 地址, 注册资本, 法人代表, 员工数), 员工S(员工号, 姓名, 性别, 年龄, 学历), 雇佣E(公司号, 员工号, 工资, 工作起始时间). 其中表C的键为公司号. 表S的键为员工号, 则表E的键(码)为 (A)
A、公司号, 员工号
B、员工号, 工资
C、员工号
D、公司号、员工号、工资
本题者查知识点是关键字.
能唯一标识实体的属性集称为码(关键字), 码也称为关键字, 是表中若干属性的属性组, 其值唯一标识表中的一个元组. 所以本题答案为A.
9、大学生学籍管理系统中有关系模式S(S#, Sn, Sg, Sd, Sa), 其中属性S#、Sn、Sg、SD、Sa分别是学生学号、姓名、性别、系别和年龄, 关键字是S#. 检索全部男生姓名的表达式为 (B)
A、 σ S g = ′ 男 ′ ( S ) \sigma_{Sg = '男'}(S) σSg=′男′(S)
B、 π S n ( σ S g = ′ 男 ′ ( S ) ) \pi_{Sn}(\sigma_{Sg = '男'}(S)) πSn(σSg=′男′(S))
C、 π S # ( σ S g = ′ 男 ′ ( S ) ) \pi_{S\#}(\sigma_{Sg = '男'}(S)) πS#(σSg=′男′(S))
D、 σ S n > 20 ( S ) \sigma_{Sn > 20}(S) σSn>20(S)
本题考查知识点是关系运算.
检索全部为男生的姓名表达式, 选项A是男生; 选项B是男生的姓名. 所以本题答案为B.
题目要求检索全部男生的姓名.
-
筛选条件: 性别为
男(对应属性 Sg) -
显示内容: 姓名(对应属性 Sn)
-
先筛选( σ \sigma σ): 从整个学生表 S S S中选出所有满足
性别为男的记录; -
再投影( π \pi π): 从筛选结果中, 只取出
姓名这一列.
(\sigma) 和 (\pi) 是关系代数里的两个筛选工具, 专门用来处理数据库表格(关系)的.
1. (\sigma)(西格玛, Sigma) ------ 负责筛选行
- 通俗叫法 :
行筛选器或过滤器. - 功能 : 从表格中挑出满足某些条件的行(记录).
- 读法 : (\sigma_{\text{条件}}(\text{表名})) 读作
从某张表中选出满足某条件的行.
举个例子 :
有一张学生表, 你想看所有男生的信息.
- 用 (\sigma) 表达: (\sigma_{\text{性别}=\text{'男'}}(\text{学生表}))
- 结果: 表格会只显示性别为
男的那些行, 但所有列(学号、姓名、性别、年龄)都还在.
2. (\pi)(派, Pi)------ 负责筛选列
- 通俗叫法 :
列筛选器. - 功能 : 从表格中只取出你关心的列(字段), 去掉其他列.
- 读法 : (\pi_{\text{列名1, 列名2}}(\text{表名})) 读作
从某张表中只取出这几列.
举个对应的例子 :
你想看所有人的姓名和年龄, 不关心学号和性别.
- 用 (\pi) 表达: (\pi_{\text{姓名, 年龄}}(\text{学生表}))
- 结果: 表格只剩
姓名和年龄这两列, 但所有行都还在.
3. 两者合起来用
题目要求检索全部男生的姓名:
- 先用 (\sigma) 筛选行 : 只留下性别为
男的行 → (\sigma_{\text{性别}=\text{'男'}}(\text{学生表})) - 再用 (\pi) 筛选列 : 从这些行里只取出
姓名这一列 → (\pi_{\text{姓名}}(\sigma_{\text{性别}=\text{'男'}}(\text{学生表})))
这就相当于:
先按条件挑出几行, 再从这几行里挑出几列.
4. 两个符号的对比记忆
| 符号 | 名称 | 操作对象 | 效果 | 口诀 |
|---|---|---|---|---|
| (\sigma) | 西格玛 | 行(记录) | 保留所有列, 只减少行数 | 行条件 |
| (\pi) | 派 | 列(字段) | 保留所有行, 只减少列数 | 列投影 |
10、在线性表的顺序存储结构中, 其存储空间连续, 各个元素所占的字节数 (A)
A、相同, 元素的存储顺序与逻辑顺序一致
B、相同, 但其元素的存储顺序可以与逻辑顺序不一致
C、不同, 但元素的存储顺序与逻辑顺序一致
D、不同, 且元素的存储顺序可以与逻辑顺序不一致
本题考查知识点是线性表的顺序存储结构.
线性表的顺序存储结构具有两个基本特点:
- 线性表中所有元素所占的存储空间是连续的;
- 线性表中各元素在存储空间中是按逻辑顺序依次存放的
所以本题答案A.
11、下列有关内联函数的叙述中, 正确的是 ©
A、内联函数在调用时发生控制转移
B、内联函数必须通过关键字inline来定义
C、内联函数是通过编译器来实现的
D、内联函数函数体最后一条语句必须是return语句
本题考查的是内联函数(inline function).
在c++中使用inline关键字来定义内联函数, inline关键字放在函数定义中函数类型之前, 不过编译器会将在类的说明部分定义的任何函数都认定为内联函数, 即使它们没有inline说明.
一个内联函数可以有, 也可以没有return语句. 内联函数在程序执行时并不产生实际函数调用, 而是在函数调用处将函数代码展开执行, 内联函数是通过编译器来实现的, 故本题答案为C.
📖 逐项解析
A、内联函数在调用时发生控制转移 ❌
- 错误原因 : 这是普通函数调用 的特征. 普通函数调用时, 程序需要跳转到函数体所在的内存地址去执行, 执行完再跳转回来, 这个过程叫
控制转移(有压栈、跳转、返回的开销). - 内联函数的做法 : 编译器在编译时, 会直接把函数体的代码**
复制粘贴**到调用处, 像宏一样展开.不发生控制转移, 所以没有函数调用的开销.
B、内联函数必须通过关键字 inline 来定义 ❌(存在争议, 但在考试中通常判错)
- 严格来说 : 在 C++ 中, 用
inline关键字只是向编译器提一个建议(请求) , 编译器可以忽略这个请求(比如函数体太长或包含循环时). - 反过来 : 即使你不写
inline, 如果函数定义在类体内(成员函数), 或者编译器觉得它适合内联, 编译器也可能自动将其内联展开. - 考试观点 : 大多数考试强调
inline只是请求, 不是强制命令, 且现代编译器有优化决定权, 所以必须通过这个说法太绝对, 故判错.
C、内联函数是通过编译器来实现的 ✅
- 正确原因 : 内联函数不是 通过运行时机制(如函数调用栈)实现的, 而是在编译阶段 由编译器决定是否进行
代码替换. 如果编译器接受内联请求, 它会直接修改生成的机器码. 所以, 内联完全依赖编译器的处理.
D、内联函数函数体最后一条语句必须是 return 语句 ❌
- 错误原因 : 这是对函数语法的误解.
void类型的函数(无返回值)最后一条语句通常不需要return.- 即使有返回值的函数,
return语句也可以出现在函数体的中间 (比如在if分支里), 不一定非在最后. - 内联函数首先是
函数, 遵循普通函数的所有语法规则, 没有最后必须是return这种限制.
🧠 核心考点总结
| 考点 | 结论 |
|---|---|
| 本质 | 内联是编译时的代码替换, 不是运行时跳转. |
| 关键字作用 | inline 只是建议, 编译器有权忽略. |
| 适用场景 | 函数体短小 (通常 1~5 行)、无循环/递归, 适合内联. |
| 与宏的区别 | 内联是真正的函数(有类型检查、作用域), 宏是预处理时的文本替换(无类型检查). |
12、下列情况中, 不会调用拷贝构造函数的是 (B)
A、用一个对象去初始化同一类的另一个新对象时
B、将类的一个对象赋值给该类的另一个对象时
C、函数的形参是类的对象, 调用函数进行形参和实参结合时
D、函数的返回值是类的对象, 函数执行返回调用时
本题考查的是拷贝构造函数.
拷贝构造函数通常在以下3种情况下会被调用:
- 用类的一个己知的对象去初始化该类的另一个正在创建的对象;
- 采用传值调用方式时, 对象作为函数实参传递给函数形参;
- 对象作为函数返回值.
故本题答案为B.
📖 逐项解析
A、用一个对象去初始化同一类的另一个新对象时 ✅(会调用)
- 典型场景 :
ClassA obj2 = obj1;或者ClassA obj2(obj1); - 原因 : 这是创建新对象, 发生在对象还未存在的时候, 必须调用拷贝构造函数来初始化新对象.
B、将类的一个对象赋值给该类的另一个对象时 ❌(不会调用)
- 典型场景 :
obj2 = obj1;(注意: 这里obj2在赋值之前已经存在了). - 原因 : 这不是
初始化, 而是赋值. 此时不会调用拷贝构造函数 , 而是调用赋值运算符重载函数operator=. - 一句话辨别 : 如果对象已经存在(已经构造好了), 再用
=就是赋值, 走赋值运算符; 如果对象还没存在, 用=就是初始化, 走拷贝构造函数.
C、函数的形参是类的对象, 调用函数进行形参和实参结合时 ✅(会调用)
- 典型场景 :
void func(ClassA a); func(obj1); - 原因 : 实参
obj1传递给形参a时, 形参a是新创建的局部对象, 需要用实参来初始化它, 所以调用拷贝构造函数.
D、函数的返回值是类的对象, 函数执行返回调用时 ✅(会调用)
- 典型场景 :
ClassA func() { ClassA temp; return temp; } - 原因 : 函数返回时, 会将局部对象
temp的值复制给一个临时的匿名对象(或直接复制给接收者), 这个复制过程需要调用拷贝构造函数.(现代编译器可能会做返回值优化(RVO)避免拷贝, 但在考试理论中默认会调用.)
⚠️ 核心区别: 初始化和赋值
| 操作类型 | 代码示例 | 对象状态 | 调用的函数 |
|---|---|---|---|
| 初始化 | ClassA obj2 = obj1; |
obj2 还未存在, 正在创建 |
拷贝构造函数 |
| 赋值 | obj2 = obj1; |
obj2 已经存在(已构造好) |
赋值运算符重载 operator= |
记忆口诀 :"新人用拷贝,旧人用赋值。"(新人=新创建的对象; 旧人=已经存在的对象)
🧠 拷贝构造函数调用时机的完整清单
以下四种情况一定会调用拷贝构造函数(理论上):
- 用已有对象初始化 新对象(
T a = b;或T a(b);) - 函数传参(实参 → 形参, 按值传递对象)
- 函数返回值(返回对象, 按值返回)
- 容器中放入对象时(如
vector.push_back(obj), 本质也是拷贝)
而对象间的赋值(=), 只要左右两边都已经存在, 就不属于上述任何一种, 调用的是赋值运算符.
13、下列有关继承和派生的叙述中, 正确的是 (D)
A、如果一个派生类私有继承其基类, 则该派生类中的成员不能访问基类的保护成员
B、派生类的成员函数可以访问基类的所有成员
C、基类对象可以赋值给派生类对象本题考查的是派生类、抽象类
D、如果派生类没有实现基类的一个纯虚函数, 则该派生类是一个抽象类
-
派生类中的成员不能访问基类中的私有成员, 可以访问基类中的公有成员和保护成员.
-
此时派生类对基类中各成员的访问能力与继承方式无关, 但继承方式将影响基类成员在派生类中的访问控制属性.
-
拥有纯虚函数的类称为抽象类, 如果一抽象类的派生类没有重定义来自基类的某个纯虚函数, 则该函数在派生类中仍然是纯虚函数, 这就使得该派生类也成为抽象类. 如果派生类中给出了基类纯虚函数的实现, 则该派生类就不再是抽象类了, 它是一个可以建立对象的具体类了.
-
基类对象不可以赋值给派生类对象, 但是派生类对象可以赋值给基类对象.
故本题答案为D.
选项逐一解析
A、如果一个派生类私有继承其基类, 则该派生类中的成员不能访问基类的保护成员(错误)
真相: 继承方式(public、protected、private)只会影响基类成员在派生类中的对外可见性(即派生类的对象或进一步派生的类能看到什么), 而不会影响派生类内部对基类成员的访问权限.
只要基类的成员是 public 或 protected 的, 无论派生类采用哪种继承方式, 派生类内部的成员函数都可以直接访问它们.
B、派生类的成员函数可以访问基类的所有成员(错误)
真相: 派生类永远无法访问基类的 private(私有)成员. private 成员是基类独有的, 即便是它的亲儿子(派生类)也无权直接插手, 只能通过基类提供的 public 或 protected 接口来间接访问.
C、基类对象可以赋值给派生类对象(错误)
真相: 在 C++ 中, 允许将派生类对象赋值给基类对象(这叫对象切片 Object Slicing, 派生类特有的部分会被切掉), 但绝不允许将基类对象赋值给派生类对象.
因为派生类通常包含基类没有的额外成员变量. 如果允许基类赋值给派生类, 派生类独有的那些成员将处于未初始化或未定义的危险状态, 编译器会直接拦截这种行为.
D、如果派生类没有实现基类的一个纯虚函数, 则该派生类是一个抽象类(正确)
真相: 包含至少一个纯虚函数(形如 virtual void func() = 0;)的类叫作抽象类. 抽象类不能实例化(不能创建对象).
当派生类继承抽象基类时, 它会把基类的纯虚函数一并继承过来. 如果派生类没有重写(实现)这个纯虚函数, 那么这个派生类自身也就包含了一个纯虚函数, 因此它也自动成为一个抽象类.
代码示例验证
cpp
#include <iostream>
using namespace std;
// ==========================================
// 针对选项 A 和 B 的测试类
// ==========================================
class Base {
private:
int privateData = 1;
protected:
int protectedData = 2;
public:
int publicData = 3;
};
// 派生类私有继承基类
class DerivedPrivate : private Base {
public:
void testAccess() {
// 验证 A(错误): 派生类内部可以访问基类的 protected 和 public 成员, 即使是私有继承
cout << "访问 protected: " << protectedData << endl; // 完全合法
cout << "访问 public: " << publicData << endl; // 完全合法
// 验证 B(错误): 派生类永远无法访问基类的 private 成员
// cout << privateData << endl; // 编译错误!'privateData' is a private member of 'Base'
}
};
// ==========================================
// 针对选项 C 的测试类
// ==========================================
class SimpleBase {
public:
int baseVal;
};
class SimpleDerived : public SimpleBase {
public:
int derivedVal; // 派生类特有的数据
};
void testAssignment() {
SimpleBase b;
SimpleDerived d;
// 派生类赋值给基类 (合法, 发生对象切片)
b = d;
// 验证 C(错误): 基类赋值给派生类
// d = b; // 编译错误!no viable overloaded '='
}
// ==========================================
// 针对选项 D 的测试类
// ==========================================
// 包含纯虚函数,是抽象类
class AbstractBase {
public:
virtual void doSomething() = 0; // 纯虚函数
};
// 派生类 1:没有实现纯虚函数
class AbstractDerived : public AbstractBase {
// 依然没有实现 doSomething()
};
// 派生类 2:实现了纯虚函数
class ConcreteDerived : public AbstractBase {
public:
void doSomething() override {
cout << "实现了纯虚函数!" << endl;
}
};
void testAbstract() {
// AbstractBase base; // 编译错误!不能实例化抽象类
// 验证 D(正确):派生类没有实现纯虚函数,它也是抽象类,不能实例化
// AbstractDerived ad; // 编译错误!AbstractDerived 也是抽象类
// 实现了纯虚函数的派生类,可以正常实例化
ConcreteDerived cd; // 完全合法
cd.doSomething();
}
int main() {
DerivedPrivate dp;
dp.testAccess();
testAbstract();
return 0;
}
14、下列运算符不能重载为友元函数的是 (A)
A、= () \[\] ->
B、+ - ++ --
C、> < >= <=
D、+= -= *= /=
本题考查的是运算符的重载
在C++中, 可以通过两种方式对运算符进行重载:
一种是类成员函数的方式;
一种是友元函数的方式.
= () \[\] ->以及所有的类型转换运算符只能作为成员函数重载, 不能重载为友元函数, 故本题答案为A.
public、private、protected
🏠 访问权限(public / private / protected)
假设有一个 "父亲类", 里面有三种财产:
| 权限修饰符 | 通俗比喻 | 谁能访问? |
|---|---|---|
| public(公共) | 客厅 | 任何人(类内部、外部、子类、其他类)都能进. |
| private(私有) | 卧室(带保险柜) | 只有自己(类内部) 能进, 儿子(子类) 和 外人 都无权进入. |
| protected(受保护) | 家庭书房 | 自己(类内部) 能进, 儿子(子类) 也能进, 但外人(其他类) 无权进入. |
🧬 继承方式(public / protected / private 继承)
继承就是把父亲的财产传给儿子. 但用什么方式继承 , 决定了这些财产到了儿子手里, 权限会被降级成什么样子.
我们假设父亲类里有这三样东西, 看看三种继承方式的效果:
| 父亲里的权限 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public(客厅) | 在儿子那里还是 public | 在儿子那里降级为 protected | 在儿子那里降级为 private |
| protected(书房) | 在儿子那里还是 protected | 在儿子那里还是 protected | 在儿子那里降级为 private |
| private(卧室) | 儿子永远看不见(无权访问) | 儿子永远看不见(无权访问) | 儿子永远看不见(无权访问) |
记忆口诀:
继承只降级, 不升级.
- public 继承: 权限基本不变(最常用, 符合"is-a"关系).
- protected 继承: 所有 public 变成 protected.
- private 继承: 所有东西全变成 private(儿子继承后想再传给孙子?没门了)
🧑💻 成员函数可以访问什么
这里只分两种情况, 和继承方式无关, 只看权限修饰符本身:
| 函数所在位置 | 能访问当前类的 public? | 能访问当前类的 protected? | 能访问当前类的 private? |
|---|---|---|---|
| 本类的成员函数 | ✅ 能 | ✅ 能 | ✅ 能(自己家卧室随便进) |
| 子类的成员函数 | ✅ 能(如果是 public 或 protected 继承来的) | ✅ 能(protected 就是为了给子类用的) | ❌ 不能(父亲的卧室上锁了) |
| 外部的普通函数(main 函数等) | ✅ 能(对象.公共属性) | ❌ 不能 | ❌ 不能 |
什么是友元(Friend)
在 C++ 的面向对象编程中, 类通常会把核心数据设为 private(私有), 只有类自己的成员函数才能访问, 这就家里的保险箱,只有自己(成员函数)有密码, 友元机制, 就是给信任的人配了一把备用钥匙.
如果一个全局函数(或另一个类)被声明为当前类的 friend, 那么这个非成员函数就可以直接访问当前类的 private 和 protected 成员.
核心特征: 友元函数不是类的成员函数它没有 this 指针, 但它拥有和成员函数一样的访问特权.
为什么选项 A 不能用友元重载
C++ 规定, 以下四个运算符必须作为类的非静态成员函数重载:
-
= (赋值运算符)
-
() (函数调用运算符)
-
\[\] (下标运算符)
-
-> (成员访问运算符)
背后的深层原因(为什么 C++ 祖师爷要这么设计):
这四个运算符都有一个强烈的共同点: 它们的操作极其依赖于左操作数的具体对象实例.
以赋值运算符 = 为例, 如果允许把它重载为全局的友元函数 operator=(A, B), 那么编译器可能会为了参数匹配, 对左操作数 A 进行隐式类型转换. 这会导致类似 10 = myObject; 这种违背直觉且极度危险的语法在某些极端情况下被编译器放行.
为了保证这四个运算符的左操作数永远是该类自身的一个确定对象(即永远有一个合法的 this 指针指向左操作数), C++ 强制要求它们必须是成员函数.
(注:选项 B、C、D 中的运算符既可以作为成员函数重载, 也可以作为友元函数重载, 通常推荐作为友元重载以支持对称的隐式转换.)
代码示例
cpp
#include <iostream>
using namespace std;
class MyNumber {
private:
int value;
public:
// 构造函数
MyNumber(int v = 0) : value(v) {}
// ==========================================
// 1. 友元的应用:重载 + 号 (选项 B 中的运算符)
// ==========================================
// 声明为友元, 这是一个全局函数, 但能访问 private 的 value
friend MyNumber operator+(const MyNumber& lhs, const MyNumber& rhs);
// ==========================================
// 2. 必须是成员: 重载 = 号 (选项 A 中的运算符)
// ==========================================
// 赋值运算符必须是成员函数, 返回自身的引用以支持连续赋值 a = b = c
MyNumber& operator=(const MyNumber& rhs) {
if (this != &rhs) { // 防止自我赋值
this->value = rhs.value;
}
cout << "调用了成员函数 operator=" << endl;
return *this;
}
// ==========================================
// 3. 错误示范 (选项 A 中的运算符强行用友元)
// ==========================================
// 如果你取消下面这行代码的注释, 编译器会直接报错:
// error: overloaded 'operator=' must be a non-static member function
// friend MyNumber& operator=(MyNumber& lhs, const MyNumber& rhs);
void print() const {
cout << "Value: " << value << endl;
}
};
// 友元函数 operator+ 的具体实现(在类外实现, 不需要 MyNumber:: 前缀)
MyNumber operator+(const MyNumber& lhs, const MyNumber& rhs) {
// 因为是友元,所以可以直接访问 .value
return MyNumber(lhs.value + rhs.value);
}
int main() {
MyNumber num1(10);
MyNumber num2(20);
// 测试友元重载的 +
MyNumber num3 = num1 + num2;
// 测试成员函数重载的 =
MyNumber num4;
num4 = num3;
num4.print(); // 输出 Value: 30
return 0;
}
为什么要使用友元
在 C++ 中, 对于双目运算符(如 +、-、*、/), 你完全可以选择将它们重载为类的成员函数, 而不使用友元(friend).
这两者的核心区别在于参数的数量和调用的方式:
-
作为友元函数(全局): 需要传入两个参数(左操作数 lhs 和右操作数 rhs);
-
作为成员函数: 只需要传入一个参数(右操作数 rhs). 因为左操作数就是调用这个函数的对象自身(隐藏的 this 指针).
使用成员函数重载+
cpp
#include <iostream>
using namespace std;
class MyNumber {
private:
int value;
public:
// 构造函数
MyNumber(int v = 0) : value(v) {}
// ==========================================
// 作为【成员函数】重载 + 号 (无需 friend)
// ==========================================
// 注意:这里的 const 表示这个函数不会修改调用对象(左操作数)自身的值
MyNumber operator+(const MyNumber& rhs) const {
// this->value 是左边对象的值,rhs.value 是右边对象的值
return MyNumber(this->value + rhs.value);
}
void print() const {
cout << "Value: " << value << endl;
}
};
int main() {
MyNumber num1(10);
MyNumber num2(20);
// 这里的 num1 + num2 本质上会被编译器转换为:
// num1.operator+(num2)
MyNumber num3 = num1 + num2;
num3.print(); // 输出 Value: 30
MyNumber num(10);
/* 实现MyNumber和普通整数int相加, 有MyNumber(int v), 这个单参数构造函数, 编译器把int隐式转换为MyNumber对象 */
MyNumber res1 = num + 5; // 编译通过!等价于 num.operator+(MyNumber(5))
/* 失去了交换律 */
MyNumber res2 = 5 + num; // 编译报错!整数 5 不是对象,不能调用 5.operator+(num)
return 0;
}
15、关于在调用模板函数时模板实参的使用, 下列叙述正确的是 (D)
答案有争议
A、对于虚拟类型参数所对应的模板实参, 如果能从模板函数的实参中获得相同的信息, 则都可以省略
B、对于虚拟类型参数所对应的模板实参, 如果他们是参数表中的最后的若干参数, 则都可以省略
C、对于虚拟类型参数所对应的模板实参, 若能够省略则必须省略
D、对于常规参数所对应的模板实参, 任何情况下都不能省略
本题考查的是函数模板中模板实参的省略
对于虚拟类型参数所对应的模板实参, 如果从模板函数的实参表中获得的信息已经能够判定其中部分或全部虚拟类型参数, 而且它们又正好是参数表中最后的若干参数, 则模板实参表中的那几个参数可以省略. 反之, 对于某个模板实参, 如果从模板函数的实参表中无法获得相同的信息, 就不能省略; 或者虽然能够获得同样的信息, 但在它后面还有不能省略的实参, 则其自身还是不能省略, 故选项A和B错误.
对于虚拟类型参数所对应的模板实参, 若能够省略可以省略, 也可以不省略, 故选项C错误.
常规参数的信息无法从模板函数的实参表中获得, 因此在调用时必须显式的说明.
故本题答案为D.
虚拟类型参数: 对应 C++ 标准中的类型模板参数(Type Template Parameter), 即通过 class T 或 typename T 声明的参数.
常规参数: 对应 C++ 标准中的非类型模板参数(Non-type Template Parameter), 即类似 int N 这样的具体类型参数.
选项逐一解析
A、对于虚拟类型参数所对应的模板实参, 如果能从模板函数的实参中获得相同的信息, 则都可以省略(正确)
真相: 这就是 C++ 中非常核心的模板实参推导(Template Argument Deduction)机制. 当我们调用模板函数时, 如果编译器能够根据你传入的函数实参, 自动推断出模板类型 T 是什么, 你在调用时就可以不写 .
B、对于虚拟类型参数所对应的模板实参, 如果他们是参数表中的最后的若干参数, 则都可以省略(错误)
真相: 能否省略, 取决于编译器能否推导出它的类型或它是否有默认值, 跟它在参数表里排在最前面还是最后面没有必然关系. 如果排在最后的模板参数无法被推导, 且没有默认参数, 就绝对不能省略.
C、对于虚拟类型参数所对应的模板实参, 若能够省略则必须省略(错误)
真相: C++ 赋予了程序员极大的自由. 即使编译器能够自动推导出类型, 你依然可以显式地写出模板实参. 这通常用于强制类型转换(例如传入的是 int, 但你强制要求按 double 处理).
D、对于常规参数所对应的模板实参, 任何情况下都不能省略(错误 - 也就是你选错的原因)
真相: 非类型模板参数(常规参数)在某些特定情况下是可以被推导出来并省略的, 最经典的场景就是传递 C 风格数组时, 编译器可以自动推导出数组的长度 N.
cpp
#include <iostream>
using namespace std;
// ==========================================
// 1. 验证 A 和 C: 类型模板参数的推导与显式指定
// ==========================================
template <typename T>
void printValue(T val) {
cout << "Value: " << val << endl;
}
// ==========================================
// 2. 验证 B: 位置在最后的参数, 如果无法推导也必须写明
// ==========================================
template <typename T, typename U>
void doSomething(T val) { // 注意: 形参里只有 T, 没有 U. U 无法被推导.
U temp = static_cast<U>(val);
cout << "Converted: " << temp << endl;
}
// ==========================================
// 3. 验证 D: 非类型模板参数(常规参数)可以被省略(推导)
// ==========================================
// 这里的 N 就是题目说的`常规参数` (非类型模板参数)
template <typename T, int N>
void printArraySize(T (&arr)[N]) { // 接收数组的引用
cout << "数组的大小 N 是: " << N << endl;
}
int main() {
// ------------------------------------------
// 验证 A (正确): 编译器从实参 10 推导出 T 是 int, 省略了 <int>
// ------------------------------------------
printValue(10);
// ------------------------------------------
// 验证 C (错误): 即使能推导, 我也可以不省略, 显式指定为 double
// ------------------------------------------
printValue<double>(10);
// ------------------------------------------
// 验证 B (错误): U 是最后的参数, 但它无法被推导, 所以不能省略
// ------------------------------------------
// doSomething(10); // 编译报错!无法推导 U
doSomething<int, double>(10); // 正确写法, 必须显式指定
// ------------------------------------------
// 验证 D (错误): N 被成功推导并省略!
// ------------------------------------------
int myArr[5] = {1, 2, 3, 4, 5};
// 这里没有写 <int, 5>, 编译器通过 myArr 自动推导出了 T 为 int, N 为 5
printArraySize(myArr);
return 0;
}
16、下列关于输入流类成员函数getline()的描述中, 错误的是 (B)
A、该函数是用来读取键盘输入的字符串的
B、该函数读取的字符串长度是受限制的
C、该函数读取字符串时, 遇到终止符便停止
D、该函数读取字符串时, 可以包含空格
本题考查的是文件流的输入输出
函数getline(char_type* s, streamsize n, char_type delim)的作用是从当前位置开始提取字符存入s所指向的具有n个字节的字符空间. 字符的提取与存储在遇到下列情况时停止:
(1)已提取并存储了n-1个字符;
(2)到流尾, 无字符可提取;
(3)下一个要提取的字符等于结束标志delim.
故本题答案为B
在 C++ 中, 叫做 getline 的函数其实有两个完全不同的版本:
全局函数 std::getline(std::cin, str): 搭配 std::string 使用. 它的内部会自动动态扩容内存, 因此读取的字符串长度是不受人为固定限制的(仅受限于计算机的物理内存和 string::max_size()).
输入流类的成员函数 std::cin.getline(char_array, size): 搭配 C 风格的字符数组 char\[\] 使用. 为了防止内存溢出, 它强制要求传入一个长度限制参数, 读取的长度是严格受限的.
A、该函数是用来读取键盘输入的字符串的(正确描述)
getline 最常见的用法就是结合 std::cin(标准输入流), 从键盘读取用户敲入的一整行文本.
B、该函数读取的字符串长度是受限制的(出题人眼里的错误描述)
如上所述, 出题人意指配合 std::string 使用的 getline, 其容量会随输入自动增长, 没有固定的上限.
C、该函数读取字符串时, 遇到终止符便停止(正确描述)
getline 默认遇到换行符 \n (即用户按下回车键)就会停止读取. 它甚至允许你自定义终止符, 比如 getline(cin, str, '#') 会在遇到 # 号时停止.
D、该函数读取字符串时, 可以包含空格(正确描述)
这是 getline 和传统的 cin >> 最大的区别. cin >> 遇到空格、Tab 或换行就会停止, 而 getline 会把整行(包括里面的空格)全部吃进去, 非常适合读取包含空格的英文句子或姓名.
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
// ==========================================
// 1. 全局函数 std::getline (搭配 std::string)
// 考试题目真正想考察的对象: 长度不受限, 能读空格
// ==========================================
string dynamicString;
cout << "请输入一段包含空格的长句子 (按回车结束): ";
// 能够读取空格,遇到 \n 停止, 长度自动动态扩展 (对应选项 C, D 以及反驳 B)
getline(cin, dynamicString);
cout << "【string 全局 getline】你输入了: " << dynamicString << "\n\n";
// ==========================================
// 2. 成员函数 cin.getline (搭配 C 风格数组)
// 题干字面上的对象, 长度严格受限
// ==========================================
const int MAX_LENGTH = 10; // 限制最多容纳 10 个字符(包含末尾的 '\0')
char buffer[MAX_LENGTH];
cout << "请输入一段文本 (限制最多读入 " << MAX_LENGTH - 1 << " 个字符): ";
// 必须传入最大长度限制, 防止数组越界 (说明真正的成员函数是受限的)
cin.getline(buffer, MAX_LENGTH);
cout << "【cin.getline 成员函数】你输入了: " << buffer << endl;
// 如果你输入了超过 9 个字符, cin.getline 会截断输入, 并将剩余字符留在输入缓冲区中.
return 0;
}
17、下列符号中, 正确的C++标识符是 (D)
A、enum
B、2b
C、foo-9
D、_32
本题考查的是标识符.
标识符是一个以字母或下划线开头的, 由字母、数字、下划线组成的字符串.
标识符不能与任意一个关键字同名.
故本题答案为D.
18、下列语句中, 错误的是 (D)
A、const int buffer=256;
B、const double *point;
C、int const buffer=256;
D、double *const point;
本题考查的是符号常量定义.
常量变量
const int buffer = 256; 声明常量buffer, 常量变量, 一旦初始化后, 不能修改;
指向常量的指针, 指针可变, 指向的内容不可变
const double *point; //声明常量指针*point, *point不可变, 但point的值可以改变;
定义了一个不可修改的整型常量
int const buffer = 256; //声明常量tbuffer
缺少初始化, 编译错误
double *const point; // 声明的point是常量, *point可变, 但point的值不可以改变, point不是外部的, 必须在声明初始化常量对象. 可改为: double aa = 123. 45; double *const point = &aa;
const修饰的变量(包括常量指针), 必须在声明式初始化, 因为之后不能再改变它的值.
故本题答案为D.
19、if语句的语法格式可描述为:
格式1: if(<条件>)<语句> 或
格式2: if(<条件>)<语句1>else<语句2>
关于上面的语法格式, 下列表述中错误的是 (A)
A、<条件>部分可以是个if语句, 例如if(if(a==0)...)
B、<语句>部分可以是一个if语句, 例如if(...) if()...
C、如果在<条件>前加上逻辑运算符! 并交换<语句1>和<语句2>的位置, 语句功能不变
D、<语句>部分可以是一个循环语句, 例如if(.)while(...)...
本题考查的是if语句
if为关键字, <条件>通常是一个表达式;
if子句和else子句可以是任何类型的语句, 当然也可以是if.else语句本身和while语句;
if else语句的基本执行过程是: 首先计算<条件>的值, 如果此值不为0真, 则执行<语句1>, 然后忽略<语句2>, 而去执行if语句之后的下一条语句; 如果此值为0假; 则执行<语句2>, 然后忽略<语句1>, 然后继续执行if语句之后的下一条语句.
故本题答案为A.
20、有如下说明
int a[10] = {1,2,3,4,5,6,7,8,9,10}, *p=a;
则数值为9的表达式是 (B)
A、*p+9
B、*(p+8)
C、*p+=9
D、p+8
本题考查的是指针与数组.
数组的下标是从0到数组的长度减1.
*(p+i)是取数组的第i+1个元素, 即下标为i的元素. 因此*(p+8)取数组的第9个元素, 即元素9.
故本题客案为B.