计算机二级cpp学习-选择题【二】

计算机二级cpp学习-选择题【二】

21、若有下面的函数调用:

fun(a+b, 3, max(n-l, b))则fun的实参个数是 (A)

A、3

B、4

C、5

D、6

本题考查的是函数的调用.

在C++中, <形参列表>是由逗号分开的, 分别说明函数的各个参数. 在fun()函数中它包括3个形参, a+b, 3和max(n-1,b); 当调用一个函数时, 实参与形参一对一地匹配, 所以实参个数也是3个.

故本题答案为A.


22、以下关键字不能用来声明类的访问权限的是 (B)

A、public

B、static

C、protected

D、private

本题考查的是类的定义。

类定义的一般格式如下:

cpp 复制代码
class<类名> {

public:
    <成员函数或数据成员的说明> // 公有成员, 外部接口;

protected:
    <数据成员或成员函数的说明> // 保护成员;

private:
    <数据成员或成员函数的说明> // 私有成员;
};

关键字public、private和protected称为访问权限修饰符, 他们限制了类成员的访问控制范围.

故本题答案为B.


23、在公有继承的情况下, 允许派生类直接访问的基类成员包括 (B)

A、公有成员

B、公有成员和保护成员

C、公有成员、保护成员和私有成员

D、保护成员

本题考查的是派生类.

派生类中的成员不能访问基类中的私有成员, 可以访问基类中的公有成员和保护成员, 此时派生类对基类中各成员的访问能力与继承方式无关, 但继承方式将影响基类成员在派生类中的访问控制属性.

故本题答案为B.


24、关于运算符重载, 下列表述中正确的是 ©

A、C++已有的任何运算符都可以重载

B、运算符函数的返回类型不能声明为基本数据类型

C、在类型转换符函数的定义中不需要声明返回类型

D、可以通过运算符重载来创建C++中原来没有的运算符

本题考查的是运算符重载.

运算符重载是针对C++中原有的运算符进行的, 不可能通过重载创造出新的运算符, 故选项D错误.

除了 .、.、->、::、?这5个运算符外, 其它运算符都可以重载, 故选项A错误.

运算符函数的返回类型可以声明为基本数据类型, 故选项B错误.

在重载类型转换符时, 由于运算符本身已经表示出返回值类型, 因此不需要返回值类型的声明.

故本题答案为C.

📖 逐项解析

A、C++已有的任何运算符都可以重载

  • 错误原因 : 有几个运算符是禁止重载 的, 包括:
    • .(成员访问运算符)
    • .*(成员指针访问运算符)
    • ::(作用域解析运算符)
    • ?:(条件运算符)
    • sizeof(返回对象大小)
    • typeid(返回类型信息)
  • 例外=(赋值运算符)、&(取地址运算符)、,(逗号运算符)虽然可以重载, 但一般不推荐, 容易写出反直觉的代码.

B、运算符函数的返回类型不能声明为基本数据类型

  • 错误原因 : 运算符函数的返回类型可以是任何类型 , 包括基本数据类型(如 intbooldouble 等).

  • 举例:

    cpp 复制代码
    int operator+(int a, int b); // 返回 int, 合法(虽然通常不这样做)
    bool operator==(const MyClass& a, const MyClass& b); // 返回 bool, 合法且常见

C、在类型转换符函数的定义中不需要声明返回类型

  • 正确原因 : 这是 C++ 的固定语法规则 , 类型转换运算符(如 operator int()operator double())的返回值类型就是它转换的目标类型本身 , 所以不需要 再写 voidint 等返回类型.

  • 正确写法

    cpp 复制代码
    class MyClass {
    public:
        operator int() { return value; }  // 正确: 没有返回类型
        // int operator int() { return value; }  // 错误:多余
    };

D、可以通过运算符重载来创建C++中原来没有的运算符

  • 错误原因 : 运算符重载不能发明新运算符 . 你只能重载 C++ 已经存在的运算符(如 +-*== 等), 不能创造出一个新的符号(比如 **+++).
  • 不能修改语法规则 : 重载也不能改变运算符的优先级结合性操作数个数.

25、关于关键字class和typename, 下列表述中正确的是 (A)

有争议

A、程序中的typename都可以替换为class

B、程序中的class都可以替换为typename

C、在模板形参表中只能用typename来声明参数的类型

D、在模板形参表中只能用class或typename来声明参数的类型

本题考查的是模板形参.

模板形参分为两种类型: 虚拟类型参数和常规参数. 虚拟类型参数须用typename或class定义. 常规参数用具体的类型修饰符(如int、double、char*等)定义, 故选项C和D错误.

在定义模板时, 关键字typename与class可以互相交换, 但在定义类时, 只能使用class. typename仅仅用于定义模板, 故程序中所有的typename都可以替换成class, 但对于用于类定义的class不能使用typename来替换.

故本题答案为A.

AI的解释

A、程序中的 typename 都可以替换为 class(绝对错误)

真相: 在 C++ 模板中, 如果我们要访问一个依赖于模板参数的内部嵌套类型(Dependent Type Name), 必须且只能使用 typename 来告诉编译器这是一个类型, 而不是一个静态成员变量, 在这种场景下, 绝对不能用 class 替换.

B、程序中的 class 都可以替换为 typename(绝对错误)

真相: 当你定义一个真实的类时(例如 class MyClass { ... }😉, 只能使用 class(或 struct), 这里绝对不能写成 typename MyClass { ... };.

C、在模板形参表中只能用 typename 来声明参数的类型(错误)

真相: 在声明模板参数时, template 和 template 是完全等价的, 两者都可以用.

D、在模板形参表中只能用 class 或 typename 来声明参数的类型(正确)

真相:在 C++98/11/14 标准中, 声明类型模板参数时, 确实有且只有这两个关键字可用.

(注:虽然 C++20 引入了 Concepts 可以用约束名替代, 但针对这道经典的传统考题, D 是毫无疑问的预期正确答案).

cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

// ==========================================
// 场景 1:两人完全等价的地方 (声明模板类型参数)
// ==========================================
// 这里用 class 还是 typename 没有任何区别, 全凭个人编码习惯
template <class T1, typename T2> 
void doNothing(T1 a, T2 b) {}

// ==========================================
// 场景 2:只能用 class 的地方 (定义具体的类)
// ==========================================
class MyEntity { // 绝对不能写成 typename MyEntity
public:
    int id;
};

// ==========================================
// 场景 3:只能用 typename 的地方 (声明依赖类型) ------ 证明 A 错误的核心!
// ==========================================
template <typename T>
void printFirstElement(const T& container) {
    // 【高能预警】
    // 编译器看到 T::const_iterator 时会发懵:
    // 这到底是一个叫 const_iterator 的静态变量? 还是一个类型?
    // 为了消除歧义,C++ 规定必须在前面加上 typename 明确宣告:"这是一个类型!"
    // 此时,如果你把 typename 换成 class, 编译器会直接报错!
    typename T::const_iterator it = container.begin(); 
    
    if (it != container.end()) {
        cout << *it << endl;
    }
}

int main() {
    vector<int> vec = {42, 100};
    printFirstElement(vec); // 输出 42
    return 0;
}

26、有如下程序

cpp 复制代码
#include<iostream>
#include<iomanip>
using namespace std;

int main() {
    cout << setprecision(3) << fixed << setfill('*') << setw(8);
    cout << 12.345 << setw(8) << 34.567;
    return 0;
}

若程序的输出式:

**12.345**34.567

则程序中下划线处遗漏的操作符是___

A、setprecision

B、fixed

C、setfill('*')

D、setw(8)

本题考查的是输入输出宽度的控制和输出精度的控制.

这段代码考察的是 C++ 中 iostream 流操纵符(Manipulator) 的执行顺序和持久性.

setw(int n); 设置输入输出的宽度.

setprecision(int n); 设置浮点数的精度(有效位数或小数位数);

setfill(char c); 设置填充字符.

由程序可以看出, 本题设置小数的输出宽度为8, 小数位数为3. 输出数据时, 如果数据宽度小于8, 则空闲的位置用*填充

故本题答案为D.

📖 逐步拆解执行流程

cpp 复制代码
cout << setprecision(3) << fixed << setfill('*') << setw(8);

这里设置了4个操纵符, 它们的作用范围分别是:

操纵符 作用 持久性
setprecision(3) 设置浮点数精度为3位 持久(一直生效)
fixed 设置固定小数点位数显示 持久(一直生效)
setfill('*') 设置填充字符* 持久(一直生效)
setw(8) 设置下一次输出的宽度为8个字符 仅对下一次输出生效(一次性)
cpp 复制代码
cout << 12.345;

此时生效的格式是:

  • 精度 = 3固定小数点12.345 显示为 12.345(正好3位小数);
  • 宽度 = 8 (setw(8) 生效), 当前内容 "12.345"6 个字符.
  • 宽度8, 内容6, 差 2 个字符 , 用填充字符 * 补齐 → 输出 **12.345.

⚠️ 注意:setw(8) 在这里是一次性的, 用完后立即失效.

cpp 复制代码
cout << setw(8) << 34.567;

这里又重新设置了一次宽度 setw(8), 因为刚才那个已经失效了.

  • 新的一次性宽度8.
  • fixedsetprecision(3) 仍然有效,所以 34.567 显示为 34.567(也是6个字符).
  • 宽度8, 内容6, 差2个字符, 填充字符还是 * → 输出 **34.567.

⚠️ 最容易踩的坑

  1. setw() 是一次性的 : 如果你连续输出多个数据, 每个数据前都必须重新写 setw(), 否则会按默认宽度0输出.
  2. setprecisionfixed 是持久的: 除非重新设置, 否则会一直影响后续所有浮点数输出.
  3. setfill 是持久的: 填充字符一旦设置, 后面所有需要填充的地方都会用它.
  4. 顺序陷阱 : 如果写成 cout << setw(8) << 12.345 << 34.567;, 第二个数字会因为 setw 已经失效, 直接输出 34.567(没有填充星号).

27、有如下程序

cpp 复制代码
#include<iostream>
#include<iomanip>
using namespace std;

class MyClass {
public:
    MyClass() {cout << 'A';}
    MyClass(char c) {cout << c;}
    ~MyClass() {cout << 'B';}
};

int main() {
    MyClass p1, *p2;
    p2 = new MyClass('X');
    delete p2;
    return 0;
}

执行这个程序, 屏幕上将显示输出 (D)

A、ABX

B、ABXB

C、AXB

D、AXBB

本题考查的是析构函数和构造函数.

在定义对象p1时由系统自动调用构造函数Myclass(), 输出字母A;

用new创建单个对象Myclass('X')时, 要根据参数调用相应的构造函数Myclass(charc): 输出字母X; 在执行delete时, 系统会自动调用析构函数MyClass(), 输出字母B, 当对象的生存周期即将结束时系统会自动调用析构函数MyClass, 输出字母B.

故本题答案为D.

AI的解释

这道题考察的是 C++ 中对象的生命周期 以及构造函数与析构函数的调用时机(特别是栈区对象与堆区对象的区别)

执行这个程序, 屏幕上将显示的输出结果是:AXBB

运行过程逐行解析

**1. MyClass p1, *p2;**

  • 动作: 声明了一个局部对象 p1 和一个指针 p2.
  • p1 是在栈(Stack)上创建的对象, 它的创建会自动调用默认构造函数 MyClass().
  • p2 只是一个指针变量, 没有实例化对象, 所以不会调用任何构造函数.
  • 当前输出: A

**2. p2 = new MyClass('X');**

  • 动作: 使用 new 关键字在堆(Heap)上动态分配内存, 创建了一个新的 MyClass 对象, 并将地址赋给指针 p2.
  • 因为传入了参数 'X', 所以会调用带参数的构造函数 MyClass(char c).
  • 当前输出: AX

**3. delete p2;**

  • 动作: 使用 delete 手动释放指针 p2 所指向的堆区内存.
  • 释放内存前, 编译器会自动调用该对象的析构函数 ~MyClass().
  • 当前输出: AXB

4. return 0; (以及随后的右大括号 })

  • 动作: main 函数准备结束.
  • C++ 规定, 当函数结束时, 分配在栈上的所有局部变量都会被自动销毁. 这里需要销毁的是局部对象 p1.
  • 销毁 p1 时, 会自动调用它的析构函数 ~MyClass().
  • (注: 指针 p2 本身作为局部变量也会被销毁, 但销毁指针本身不会触发对象的析构, 况且它指向的对象刚才已经被我们手动 delete 掉了)
  • 最终输出: AXBB

核心考点总结

这道题完美融合了两个核心概念:

对象分配方式 存储区域 构造函数调用时机 析构函数调用时机
MyClass p1; 栈区 (Stack) 运行到声明该变量的语句时自动调用. 离开该对象的作用域(例如函数结束的 } 处)时自动调用。
new MyClass(); 堆区 (Heap) 执行 new 表达式时调用. 必须显式执行 delete 时才会调用. 如果不 delete, 就会引发内存泄漏, 且永远不会调用析构函数.

28、有如下程序

cpp 复制代码
#include<iostream>
using namespace std;

/* 全局变量i, 存在于全局作用域 */
int i=1;
class Fun {
public:
    static int i;
    /*  只是return, 而没有修改 */
    int value() {return i-1;}
    int value() const {return i+1;}
};

/* 类的静态成员变量, 存在于类Fun的作用域中, 不属于任何一个具体的对象, 而是被该类的所有对象共享 */
int Fun::i = 2;

int main() {
    /* 局部变量i, 存在于main函数的局部作用域 */
    int i = 3;
    Fun fun1;
    const Fun fun2;
    /* 找到静态成员Fun::i, 值为2, 返回值是1; 其次这里通过类名Fun来访问静态成员变量, 得到2; 最后匹配到const成员函数 */
    cout << fun1.value() << Fun::i << fun2.value();
    return 0;
} 

若程序的输出结果是:

123

则程序中下划线处遗漏的句子是 (A)

A、cout << fun1.value() << Fun::i << fun2.value();

B、cout << Fun::i << fun1.value() << fun2.value();

C、cout << fun1.value() << fun2.value() << Fun::i;

D、cout << fun2.value() << Fun::i << fun1.value();

本题主要考查了常对象的一些概念.

由于i是类Fun的静态成员, 该成员被类的所有实例共享, 对于类中的静态成员函数, 可以通过类名::函数名的方法来调用, 并且该函数只能访问类中的静态成员.

在C++中, 对于常对象的成员函数调用, 将自动调用其常成员函数. 在本题中执行fun1.value(), 将调用原型为int value()的函数, i的值返回1;

执行fun2.value(), 将调用原型为int value() const的函数, i的值返回3; 执行Fun::i时, 调用类的静态成员i, i的值返回2.

故本题答案为A.

cpp 复制代码
#include<iostream>
using namespace std;

class Fun {
public:
    static int i;
    int value() { return i - 1; }
    int value() const { return i + 1; }
};

int Fun::i = 2; // 初始状态,所有对象共享的 i 是 2

int main() {
    Fun fun1;
    const Fun fun2;

    cout << "--- 初始状态 ---" << endl;
    cout << "fun1.value(): " << fun1.value() << endl; // 输出 1 (2-1)

    // ==========================================
    // 动作 1:直接通过类名修改静态变量
    // ==========================================
    Fun::i = 100; 
    
    cout << "\n--- 将 Fun::i 修改为 100 后 ---" << endl;
    // 此时再调用方法,里面使用的 i 已经全部变成了 100
    cout << "fun1.value(): " << fun1.value() << endl; // 输出 99 (100-1)
    cout << "fun2.value(): " << fun2.value() << endl; // 输出 101 (100+1)

    // ==========================================
    // 动作 2:通过对象实例去修改静态变量
    // ==========================================
    fun1.i = 500; // 语法完全合法(但通常不推荐这种写法,容易让人误以为修改的是对象的私有属性)
    
    cout << "\n--- 通过 fun1 将 i 修改为 500 后 ---" << endl;
    cout << "Fun::i 变成了: " << Fun::i << endl;      // 输出 500
    cout << "fun2 眼里的 i 也是: " << fun2.i << endl;   // 输出 500

    return 0;
}

29、有如下程序;

cpp 复制代码
#include <iostream>

using namespace std;

class Obj {
    static int i;
    public:
        Obj() {i++;}
        ~Obj() {i--;}
        static int getVal() {return i;}
};

int Obj::i = 0;
void f() {Obj ob2; cout << ob2.getVal();}

int main() {
    Obj ob1;
    f();                        /* Obj::i = 1 */
    Obj *ob3 = new Obj;         /* Obj::i = 2 */
    cout << ob3->getVal();
    delete ob3;
    cout << Obj::getVal();
    return 0;
}

程序的输出结果是 (D)

A、232

B、231

C、222

D、221

本题主要考查了c++中类的静态成员.

由于i是类Obj的静态成员, 该成员被类的所有实例共享.

当定义ob1时, 系统自动调用构造函数ob1, i的值将加1; 调用函数f时, 在定义ob2时系统会自动调用构造函数obj, i的值将再加1; 调用ob2.getVal后, 将i的值输出, 输出值为2; 当调用函数f即将结束时; 系统自动调用析构函数, i的值将减1; 当定义ob3时, 系统自动调用构造函数, i的值将加1, 调用cout<<getVal()后, 将i的值输出, 输出值为2, 调用delete ob3后将执行obj的析构函数, 执行后, i的值将减1;

Obj::getVal()为类的一个静态成员函数, 其作用是返回私有静态成员变量i的值.

故本题答案为D.


30、有如下程序

cpp 复制代码
#include <iostream>
using namespace std;
class Base {
    protected:
        Base() {cout << 'A';}
        Base(char c) {cout << c;}
};

class Derived : public Base {
    public:
        Derived(char c) {cout << c;}

};

int main()
{
    Derived d1('B');
    return 0;
}

执行这个程序屏幕上将显示 ©

A、B

B、BA

C、AB

D、BB

本题考查的是派生的构造函数和基类的构造函数的调用顺序.

建立派生类对象时, 构造函数的执行顺序如下:

  1. 执行基类的构造函数;
  2. 执行成员对象的构造函数;
  3. 执行派生类的构造函数.

派生类Derived由基类Base公有派生而来. 在派生类构造函数声明时系统会自动调用基类的缺省构造函数.

调用Derived dl('B')后, 执行类Derived的构造函数的Derived(char c)定义, 系统会自动调用基类的缺省构造函数Base(), 输出字母A; 再执行派生类的构造函数Derived(char c), 输出字母B.

故本题答案为C.

当我们执行 main 函数中的 Derived d1('B'); 时, 发生的事情如下:

1. 准备构造 Derived 对象

编译器看到你要用字符 'B' 来构造一个 Derived 类的对象, 于是准备调用 Derived(char c) 这个构造函数.

2. 核心考点: 构造基类, 再构造派生类

C++ 有一个严格的铁律: 在构造派生类对象之前, 必须先完成其基类部分的构造. 这就好比建楼, 必须先打好地基(Base), 才能建上层建筑(Derived).

3. 寻找并调用基类的构造函数(隐式调用)

那么问题来了, Derived 的构造函数到底该调用 Base 的哪一个构造函数呢?

观察代码:

cpp 复制代码
Derived(char c) { cout << c; } 

在这个构造函数中,并没有显式地告诉编译器该调用哪个基类构造函数 (也就是没有使用初始化列表, 比如 Derived(char c) : Base(c)).

在这种情况下, C++ 的规则是: 编译器会自动、隐式地去调用基类的默认构造函数(即没有参数的那个构造函数)

于是, 编译器偷偷调用了 Base().

执行 Base() 内部的代码:**输出字符 A**.

(注:基类的构造函数是 protected 的, 派生类有权限访问并调用它, 所以这里完全合法)

4. 执行派生类自己的构造函数体

基类构造完毕(地基打好了), 现在回过头来执行派生类构造函数 Derived(char c) 内部的代码.

此时传入的参数 c'B'

执行内部代码: **输出字符 B**.

最终屏幕上的输出连起来就是:AB

如果我想输出 BB 该怎么改

需要使用初始化列表(Initializer List)显式地调用基类的有参构造函数.

修改后的 Derived 类如下:

cpp 复制代码
class Derived : public Base {
    public:
        // 使用 : Base(c) 显式告诉编译器: 请用参数 c 去调用基类的有参构造函数!
        Derived(char c) : Base(c) {
            cout << c;
        }
};

如果是这样写的代码, 再执行 Derived d1('B');, 输出结果就会变成: BB


31、有如下类定义:

cpp 复制代码
class MyBase {
    int k;
    public:
        MyBase(int n = 0) : k(n) {}
        int value() const {return k;}

};

class MyDerived : MyBase {

    int j;
    public:
        MyDerived(int i): j(i) {}
        /* 可以访问k吗? */
        int getK() const {return k;}
        int getj() const {return j;}
};

编译时发现有一处语法错误, 对这个错误最准确的描述是 (A)

A、函数getK试图访问基类的私有成员变量k

B、在类MvDerived的定义中, 基类名MvBase前缺少关键字public、protected或private

C、类MyDerived缺少一个无参的构造函数

D、类MyDerived的构造函数没有对基类数据成员k进行初始化

本题考查的是派生类访问权限,

在一个派生类中, 其成员由两部分构成:

一部分是从基类继承得到的, 另一部分是自已定义的新成员, 所有这些成员仍然分为公有、私有和保护三种访问属性. 其中, 从基类继承下来的全部成员构成派生类的基类部分, 这部分的私有成员是派生类不能直接访问的, 公有成员和保护成员则是派生类可以直接访问的.

故本题答案为A.


32、在一个派生类对象结束其生命周期时

A、先调用派生类的析构函数后调用基类的析构函数

B、先调用基类的析构函数后调用派生类的析构函数

C、如果基类没有定义析构函数, 则只调用派生类的析构函数

D、如果派生类没有定义析构函数, 则只调用基类的析构函数

本题考查的是派生类析构函数的执行顺序.

当主函数结束时, 派生类析构函数的执行顺序是: 先执行派生类的析构函数, 再执行成员对象的析构函数, 最后执行基类的析构函数. 故本题答案为A.


33、有如下的运算符重载函数定义

double operator +(int i, int k) {return double(i+k);}

但定义有错误, 对这个错误最准确的描述是

A、+只能作为成员函数重载, 这单的+是作为非成员函数重载的

B、两个int型参数的和也应该是int型, 而这里将+的返回类型声明为double

C、没有将运算符重载函数声明为某个类的友元

D、C++己经提供了求两个int型数据之和的运算符+, 不能再定义同样的运算符

本题者查的是运算符的重载.

在c++中可以对用户自定义类的运算符进行重载, 但不能诸如int:char,double等内置类型运算符进行重载.

故本题答案为D.

AI的解释

这道题考察的是 C++ 中运算符重载的最基本底线(限制条件).

一、不能造反的内置运算符

C++ 赋予了程序员极大的自由来重载运算符, 但有一条绝对不可逾越的红线:

重载运算符的参数列表中, 至少必须有一个是用户自定义的类型(如 classstructenum)

  • 真相: 对于内置的基础数据类型(如 intdoublechar指针 等), C++ 编译器已经内置了它们之间的所有运算规则, 绝对不能改变内置类型的运算行为.

题目中的代码: double operator +(int i, int k), 两个参数全是内置的 int 类型, 这直接触碰了红线.

二、逐一驳斥错误选项

A、+只能作为成员函数重载...(错误)

  • 反驳: 只有 =, (), [], -> 这四个必须是成员函数. + 号不仅可以作为非成员函数(全局函数)重载, 甚至在很多时候, 为了支持交换律(例如 对象 + 整数整数 + 对象), 我们更强烈推荐把它重载为非成员函数.

B、两个int型的和也应该是int型, 而这里声明为double(错误)

  • 反驳: 运算符重载的返回值类型是完全自由的 . 虽然出于良好的编程习惯, 我们通常会让返回值符合常理(比如加法返回相同类型), 但在语法上, 就算你写个重载让 + 号返回一个 std::string, 编译器也是完全允许的, 报错的根本原因不在于返回值, 而在于参数类型.

C、没有将运算符重载函数声明为某个类的友元(错误)

  • 反驳: ** 友元(friend)仅仅是为了 获取访问私有成员(private)的特权**. 如果一个全局的运算符重载函数只需要访问类的 public 接口, 它完全不需要声明为友元. 更何况, 这道题的参数都是 int, 连都没有.
三、 代码示例:正确的非成员重载

如果真的想写一个全局的 + 重载, 必须保证至少有一个参数是我们自己写的类.

cpp 复制代码
#include <iostream>
using namespace std;

// 自定义一个类
class MyInt {
public:
    int value;
    MyInt(int v) : value(v) {}
};

// ==========================================
// 合法的重载: 至少有一个参数是自定义类型 MyInt
// ==========================================
// 返回值类型哪怕是 double, 语法上也是完全合法的(反驳了选项 B)
double operator+(int left, const MyInt& right) {
    return static_cast<double>(left + right.value);
}

// ==========================================
// 非法的重载:(就是题目中的代码)
// ==========================================
// 如果你取消下面这行注释, 编译器会报错:
// error: overloaded 'operator+' must have at least one parameter of class or enumeration type
// double operator+(int i, int k) { return double(i+k); }

int main() {
    MyInt obj(10);
    
    // 调用合法的全局重载 operator+(int, MyInt)
    double result = 5 + obj; 
    
    cout << "结果: " << result << endl; // 输出 15
    return 0;
}

34、语句ofstream f("SALARY.DAT", ios_base::app); 的功能是建立流对象f, 并试图打开文件SALARY.DAT与f关联, 而且 (B)

A、若文件存在, 将其置为空文件; 若文件不存在, 打开失败

B、若文件存在, 将文件指针定位于文件尾; 若文件不存在, 建立一个新文件

C、若文件存在, 将文件指针定位于文件首; 若文件不存在, 打开失败

D、托文件存在, 打开失败; 若文件不存在, 建立一个新文件

本题考查的是文件流的输出.

ofstream f("SALARY.DAT", ios_base::app);

是以ios_base::app方式打开文件, 若文件存在, 将文件指针定位于文件尾; 若文件不存在, 建立一个新文件.

故本题答案为B.


35、有如下程序

cpp 复制代码
#include <iostream>
using namespace std;

class A {
    public:
        virtual void func1() {cout << "A1";}
        void func2() {cout << "A2";}
};

class B : public A {
    public:
        void func1() {cout << "B1";}
        void func2() {cout << "B2";}
};

int main()
{
    A *p = new B;
    p->func1();
    p->func2();

    return 0;
}

运行此程序, 屏幕上将显示输出 ©

A、B1B2

B、A1A2

C、B1A2

D、A1B2

本题考查的是派生类.

派生类B由基类A公有继承而来. 调用p->func1()后, 执行派生类B的函数void func1(); 调用p->func2后, 执行基类的函数void func2, 因为虚拟函数是根据对象的实际类型调用, 非虚拟函数是根据指针类型调用.

故通过指针p调用func2时将直接调用基类中的void func2.

故本题答案为C.


36、有如下程序:

cpp 复制代码
class Base {
    public:
        int data;
};

class Derived1 : public Base {};
class Derived2 : protected Base {};

int main()
{
    Derived1 d1;
    Derived2 d2;

    d1.data = 0;        // 1
    d2.data = 0;        // 2

    return 0;
}

下列关丁程序编译结果的描述中, 正确的是 ©

A、①②皆无编译错误

B、①有编译错误, ②无编译错误

C、①无编译错误, ②有编译错误

D、①②皆有编译错误

本题主要考查了类成员的访问控制.

类中提供了3种访问控制权限:

  1. 公有(public)
  2. 私有(private)
  3. 和保护(protected)

其中,公有类型定义了类的外部接口, 任何一个外部的访问都必须通过外部接口进行; 私有类型的成员只允许本类的成员函数访问, 来自类外部的任何访问都是非法的; 保护类型介于公有类型和私有类型之间, 在继承和派生时可以体现出其特点.

派生类从基类的继承方式包括3种: 公有继承(public)、私有继承(private)和保护继承(protected).

在一个派生类中, 其成员由两部分构成:

一部分是从基类继承得到的, 另一部分是自已定义的新成员, 所有这些成员仍然分为公有(public)、私有(private)和保护(protected)三种访问属性.

继承方式将影响基类成员在派生类中的访问控制属性, 基类中公有成员和保护成员在派生类中的访问控制属性将随着继承方式而改变; 派生类从基类公有继承时, 基类的公有成员和保护成员在派生类中仍然是公有成员和保护成员; 派生类从基类私有继承时, 基类的公有成员和保护成员在派生类中都改变为私有成员; 派生类从基类保护继承时, 基类的公有成员在派生类中改变为保护成员, 基类的保护成员在派生类中仍为保护成员.

本题中派生类Derived1公有继承基类Base, 基类Base的公有数据成员data在派生类Derived1中仍然为公有成员.

通过派生类Derived1的对象d1可以直接访问公有成员data, 因此①没有编译错误.

派生类Derived2保护继承基类Base, 基类Base的公有数据成员data在派生类Derived2中改变为保护成员, 通过派生类Derived2的对象d2不可以直接访问保护成员data. 因此②有编译错误.

故本题答案为C.


37、有如下程序:

cpp 复制代码
#include<iostream>
using namespace std;


class Base1 {
    public:
    Base1(int d) {cout << d;}
    ~Base1() {}
};

class Base2 {
    public:
    Base2(int d) {cout << d;}
    ~Base2() {}
};

class Derived : public Base1, Base2 {
    public:
        Derived(int a, int b, int c, int d) : Base1(b), Base2(a), b1(d), b2(c) {}

    private:
        int b1;
        int b2;
};

int main() {
    Derived d(1, 2, 3, 4);

    return 0;
}

运行时的输出结果是 ()

A、1234

B、2134

C、12

D、21

本题主要考查了派生类构造函数的执行顺序.

建立派生类对象时, 构造函数的执行顺序如下:

  1. 执行基类的构造函数, 调用顺序按照各个基类被继承时声明的顺序(自左向右);
  2. 执行成员对象的构造函数, 调用顺序按照各个成员对象在类中声明的顺序(自上而下);
  3. 执行派生类的构造函数.

本题中派生类Derived有两个处于同一层次的基类Base1和Base2(自左向右), 无成员对象.

在主函数中创建派生类对象d时, 先调用基类Base1的构造函数, 输出实参b的值, 即2; 再调用基类Base2的构造函数, 输出实参a的值, 即1. 最后调用派生类的缺省构造函数. 故运行时的输出结果是21.

故本题答案为D.


38、有如下程序:

cpp 复制代码
#include <iostream>
using namespace std;

class Base {
    public:
    virtual void function1() {cout << '0';}
    void function2() {cout << '1';}
};

class Derived : public Base{
    public:
        void function1() {cout <<'2';}
        void function2() {cout <<'3';}
};
int main()
{
    Base *p = new Derived();
    p->function1();
    p->function2();

    return 0;
}

运行时输出结果是 (B)

A、01

B、21

C、03

D、23

本题主要考查了虚函数和多态性.

在成员函数声明的前面加上virtual修饰, 即把该函数声明为虚函数.

在派生类中可以重新定义从基类继承下来的虚函数. 除少数特殊情况外, 在派生类中重定义虚函数时, 函数名、形参表和返回值类型必须保持不变. 虚函数在派生类被重定义后无论是否用virtual修饰都是虚函数. 在c++中, 一个基类指针(或引用)可以用于指向它的派生类对象, 而且通过这样的指针(或引用)调用虚函数时, 被调用的是该指针(或引用)实际所指向的对象类的那个重定义版本.

基类中的实函数也可以在派生类中重定义, 但重定义的函数仍然是实函数. 在实函数的情况下, 通过基类指针(或引用)所调用的只能是基类的那个函数版本, 无法调用到派生类中的重定义函数. 也就是说, 尽管调用的语法形式可能是相同的, 但对实函数的任何形式的调用都是非多态的.

本题中,基类Base有两个公有成员函数: 虚函数function1和实函数function2, 派生类Derived中重新定义了这两个函数. 在函数main中, 第一条语句定义了基类指针p并初始化指向派生类对象, 第二条语句通过基类指针p调用虚函数function1, 被调用的是该指针实际指向的对象类Derived的那个重定义版本, 输出字符2, 第三条语句通过基类指针p调用实函数function2, 被调用的是基类的那个版本, 输出字符1.

故本题答案为B.


39、若为Fraction类重载前增1运算符++, 应在类体中将其声明为 (A)

A、Fraction& operator ++();

B、Fraction& operator ++ (int);

C、friend Fraction& operator ++ ();

D、friend Fraction& operator++(int);

本题主要考查了增1运算符的重.

C++中除了,、.、->、::、?这五个运算符外, 其他运算符都可以重载.

运算符的重载形式有两种: 重载为类的成员函数和重载为类的友元函数.

大部分运算符既可以重载为类的成员函数, 又可以重载为友元函数.

当运算符重载为类的成员函数时, 函数的参数个数比原来的操作数个数要少一个(后置++--除外).

因此Fraction类重载单目前置增1运算符为成员函数时, 没有参数, 声明的语法形式为:Fraction& operator ++();, 选项A正确, 选项B 错误.

当重载为类的友元函数时, 参数个数与原操作数个数相同, 形参为操作数.

因此Fraction类重载单目前置增1运算符为友元函数时, 有一个形参, 选项C错误, 形参为Fraction类的对象, 而不是int, 选项D错误.

故本题答案为A.

AI解释

这道题考察的是 C++ 中单目运算符(特别是自增 ++ 和自减 --)的重载规则 , 尤其是如何区分前缀(前增 1, 即 ++x)和后缀(后增 1, 即 x++).

一、 核心考点:前缀 vs 后缀的区别

在 C++ 中, 为了区分前缀 ++ 和后缀 ++ 这两个完全一样的符号, 祖师爷 Bjarne Stroustrup 想出了一个绝妙的哑元(Dummy)机制:

  1. 前缀版本(前增 1, ++x):
  • 行为: 先将对象自身加 1, 然后返回修改后的对象本身.
  • 参数: 不需要额外参数.
  • 返回值: 返回对象自身的引用(Fraction&), 以支持连续操作(如 ++++x).
  1. 后缀版本(后增 1, x++):
  • 行为: 先保存对象当前的原始状态, 然后将对象自身加 1, 最后返回刚才保存的原始状态的副本.
  • 参数: 编译器强制要求带一个 int 类型的哑元参数(只用于占位区分, 毫无实际意义).
  • 返回值: 必须返回一个新的对象副本(Fraction), 绝对不能返回引用(因为返回的局部副本在函数结束时就会销毁).
二、 选项逐一剖析

A、Fraction& operator++(); (正确)

  • 作为成员函数, 它没有额外的参数, 代表它是前缀版本.
  • 返回值为 Fraction&, 符合前缀版本返回自身引用的规范.

B、Fraction& operator++(int); (错误)

  • 括号里有个 int, 这标志着它是后缀版本(后增 1).
  • 此外, 它的返回值写成了引用 Fraction&, 这也是不对的. 后缀版本必须返回对象的值(Fraction).

C、friend Fraction& operator++(); (错误)

  • friend 意味着这是一个非成员函数(全局函数).
  • 全局函数没有隐藏的 this 指针. 如果要重载单目运算符, 必须显式传入一个参数 来代表要操作的对象(应写为 friend Fraction& operator++(Fraction& x);). 由于这里参数为空, 语法上完全不成立.

D、friend Fraction& operator++(int); (错误)

  • 同理, 作为友元非成员函数, 它缺失了操作对象. 正确的后缀友元重载应该是 friend Fraction operator++(Fraction& x, int);.
三、 代码示例:一次性看懂前缀与后缀

这段代码完整展示了在 Fraction(分数)类中, 如何规范地重载前增 1 和后增 1.

cpp 复制代码
#include <iostream>
using namespace std;

class Fraction {
private:
    int numerator;   // 分子
    int denominator; // 分母

public:
    Fraction(int n = 0, int d = 1) : numerator(n), denominator(d) {}

    // ==========================================
    // 1. 前增 1 (前缀 ++x) -> 对应选项 A
    // ==========================================
    Fraction& operator++() {
        // 行为:自身分子加上分母 (相当于分数 + 1)
        numerator += denominator;
        
        // 返回自身对象的引用
        return *this; 
    }

    // ==========================================
    // 2. 后增 1 (后缀 x++) -> 注意那个 int 哑元
    // ==========================================
    Fraction operator++(int) { // 这里的 int 纯粹是为了和前缀区分, 不用写变量名
        // 第一步:克隆一个修改前的原始对象
        Fraction temp = *this; 
        
        // 第二步:修改自身 (可以直接复用前缀版本)
        ++(*this); 
        
        // 第三步:返回之前保存的副本 (注意返回值类型是 Fraction,不是引用)
        return temp; 
    }

    void print() const {
        cout << numerator << "/" << denominator << endl;
    }
};

int main() {
    Fraction f1(1, 2); // 1/2

    cout << "初始状态 f1: ";
    f1.print(); // 输出 1/2

    // 测试前增 1 (先加 1, 再返回新值)
    Fraction f2 = ++f1; 
    cout << "\n执行 ++f1 后:" << endl;
    cout << "f1: "; f1.print(); // 输出 3/2
    cout << "f2: "; f2.print(); // 输出 3/2

    // 测试后增 1 (先返回旧值, 自身再加 1)
    Fraction f3 = f1++; 
    cout << "\n执行 f1++ 后:" << endl;
    cout << "f1: "; f1.print(); // 输出 5/2 (自身已经变了)
    cout << "f3: "; f3.print(); // 输出 3/2 (拿到的是改变前的旧值)

    return 0;
}

40、有如下程序:

cpp 复制代码
#include <iomanip>
#include <iostream>
using namespace std;

int main{
    cout << setfil1(*) << setw(6) <<123 << 456;
    return 0;

}

运行时的输出结果是 ©

A、***123***456

B、***123456***

C、***123456

D、123456

本题主要考查了输入输出的格式控制.

setw(int n): 设置输入输出宽度, 宽度是指最小输出宽度. 当实际数据宽度小于指定的宽度时, 多余的位置用填充字符添满; 当实际数据的宽度大于设置的宽度时, 仍按实际的宽度输出.

注意:宽度设置的效果是只对一次输入或输出有效. 在完成了一个数据的输入或输出后. 宽度设置自动恢复为0(表示按数据实际宽度输入输出)

setfill(char c): 设置填充字符. 只有在设置了宽度的情况下, 字符填充操作才有意义. 设置的填充字符一直有效, 直到再次设置填充字符时为止.

本题中设置填充字符为*, 设置输出宽度为6, 输出宽度只对输出项123有效, 对输出项456无效, 输出项456按实际宽度输出, 故本程序运行结果为***123456.

故本题答案为C.