C++期末简答题汇总(往年)

大部分问题的答案在软件学院C++相关文章里应该都能找到答案,这里做一个合订本

C++历史

C和C++的关系

  1. C++完全包含C语言成分,C++支持C所支持的全部编程技巧(C的超集);同时C++还添加了OOP支持
  2. 任何C程序都能被C++用基本相同的方法编写,并具备相同的运行效率和空间
  3. C++还引入了重载、内联函数、异常处理等功能,对C中的过程化控制及其功能进行了扩充
  4. C++由以下4部分有机组成
    • C
    • OOP
    • STL
    • Inside-Model

表达式的值有哪些因素决定?表达式存在副作用吗

  1. 表达式指由操作数、操作符和标点符号(比如逗号运算符)组成的序列,代表一个计算过程
  2. 表达式的值受操作符、操作数、优先级、结合性、求值次序、副作用、短路求值和类型转换(隐式or显式)影响
  3. 副作用:改变程序状态(即可能改变操作数或其他一些变量的值)

C++多态类型

  • 静态多态
    • 一名多用(函数重载、操作符重载)
    • 模板编程(template)/ 泛型
  • 动态多态
    • 虚函数

解释指针类型和引用类型的差别

  1. 引用类型与指针类型都可以实现通过一个变量访问另一个变量,但访问的语法形式不同:引用采用直接访问形式,而指针采用间接访问形式
  2. 引用被创建是必须初始化(即必须绑定到变量上),而指针可以在任何时候初始化
  3. 不能有空引用,引用必须与合法变量绑定;但可以有空指针
  4. 一旦引用被初始化就不能改变绑定的变量,而指针可以随时改变指向的变量
  5. 从2 ~ 4可以看出引用比指针安全
  6. 传参时引用类型的实参是一个变量,而指针参数的实参是一个变量的地址(这条感觉怪怪的,难道指针就不是变量了吗?)

C++中继承与虚继承的差异

  1. 普通继承在面对多继承时可能会引入名冲突问题,每个继承路径都有自己的基类实例,而虚继承可以解决名冲突问题(形成一个菱形继承图“格”)
  2. 因为虚继承并没有真的继承基类的成员,而是生成了一个指向基类的指针,因此不会有名冲突问题(基类实际上只有一份,被所有虚继承的派生类共用;相当于虚基类被合并,只有一个副本)
  3. 虚继承的基类无法用static_cast<>()转成派生类
  4. 虚继承可能带来额外的性能开销
  5. 虚基类的构造函数优先于非虚基类的构造函数被调用

为什么实现两个版本的下标运算符重载

两个版本是一个带const,一个不带

class string {
char* p;
public:
explicit string(char* p1) {
p = new char[strlen(p1) + 1];
strcpy(p, p1);
}
// 版本1
char& operator[](int i) {
return p[i];
}

// 版本2
char operator[](int i) const {
return p[i];
}

virtual ~string() {delete[] p;}
};

需要分别对应两种*this指针,const T* thisT* this

且若某个调用产生了两个候选函数,而一个有const一个没有,若调用处不修改对象,则优先选择带有const的函数,因为const保证不修改,选const函数使得更多代码可以在const上下文中使用,避免非const函数隐式修改。

请简述右值引用与左值引用的异同

相同点:

  • 本质都是引用
  • 语法相似,左值引用是&,右值引用是&&
  • 传递效率都很高,无论是左值还是右值引用都避免了传值时的值拷贝
  • 两者都可以作为函数参数,允许直接操作被引用的对象

不同点:

  • 左值引用引用的是左值对象(即有名字、可持久存在的对象),而右值引用引用的是右值对象(即无名字、临时的对象,通常是表达式的结果)
    • 左值是可以取地址的实体,右值通常不能取地址
  • 左值引用常常用于需要对现有变量进行修改或访问的地方,而右值引用往往用于临时对象操作,比如移动语义(可以提高效率,避免不必要拷贝)
  • 左值引用提出绑定到长生命周期对象上,被引用的对象必须是有效的;右值引用通常绑定到短生命周期的临时对象上,右值引用延长了右值对象的生命周期(只要右值引用还在,该对象就还能访问)
  • 右值引用可以用于移动语义,高效转移资源
  • 在函数重载时左值引用和右值引用是不同的参数类型

Lambda表达式的作用并比较它与重载了函数调用操作符的函数对象的差别

  • lambda表达式作为匿名函数使用,避免了一些轻量函数使用时还要定义,避免繁琐步骤,更加简洁
  • lambda表达式不需要显式指明函数返回类型,函数对象需要
  • lambda表达式不需要先声明再使用(即用即定义),函数对象需要
  • lambda表达式实际上是用函数对象实现的(编译器生成一个匿名函数对象类,lambda表达式返回其实例,为右值)

简述C与C++混合编程时需要注意的问题

  • C++是C的超集,而C不支持OOP,不要在混合编程中出现class等面向对象关键字
  • 调C库函数时附加关键字extern C
  • 注意函数重载,C不支持
  • 注意求值顺序和副作用
  • 内存分配差异(new/malloc, delete/free)
  • 内存占用差异

内联函数的作用?随意使用的问题?阐述合理使用的建议

  1. 作用:
    • 提高可读性:编译器眼中没有函数但程序员不是,可以提高代码的可读性
    • 提高效率:编译器不再需要进行函数调用,而是直接顺序地执行等价代码,避免了函数调用的时间开销
  2. 问题:
    • 增大目标代码
    • 病态的换页
    • 降低指令快取装置的命中率
  3. 建议:
    • 适合于频率高、简单的小段代码
    • 不要滥用inline
    • 不要内联复杂的控制逻辑
    • 不能内联递归
    • 把内联函数扔头文件里

C++关键字const的几种使用方法,并给出示例

  1. 修饰基本数据类型,表示常量
const int a = 1;
  1. 修饰指针,两种,或者说三种
int a = 114;
const int* pa1 = &a; // 指针常量,指向空间的值不能改
int* const pa2 = &a; // 常量指针,指针本身值不能改
const int* const = &a; // 综上
  1. 修饰(成员)函数
int f(const& int); // 参数是常量
f(int&) const; // 函数内部不会修改成员变量,即在该函数中所有成员都是常量(编译器自动加const)
  1. 修饰对象

但要保证对象的所有变量都能在声明对象时被初始化(要么内部实现声明时初始化,要么构造函数实现)

class A {

public:
int p = 1;
};

const A a;

class A {

public:
int p;
A() {
// p = 1; 注释掉也行
}
};

const A a;

什么是纯虚函数、虚函数和非虚函数?合理定义三种成员函数的基本原则是什么?请给出一个你认为合理定义的实例

  • 纯虚函数
    • 只有函数接口会被继承
    • 派生类必须继承函数接口
    • 派生类必须提供实现代码
  • 一般虚函数
    • 函数的接口及缺省的实现代码都会被继承
    • 子类必须继承函数接口
    • 可以缺省实现代码
  • 非虚函数
    • 函数的代码及其实现代码都会被继承
    • 必须同时继承接口和实现代码,无法重写以提供自己的实现

原则:

  • 类的成员函数才能是虚函数
  • 静态成员函数不能是虚函数
  • 内联不能是虚函数
  • 构造不能是虚函数
  • 析构往往是虚函数

C++程序设计中,可以利用析构函数防止资源泄露,请给出模板auto_ptr的基本定义及使用实例

template <typename T>
class auto_ptr {
T* ptr;

public:
auto_ptr(): ptr(nullptr) {}
explicit auto_ptr(T* p): ptr(p) {}

T& operator*() {
return *ptr;
}

T& operator*() const {
return *ptr;
}

T* operator->() {return ptr;}

~auto_ptr() {
if (ptr != nullptr) {
delete ptr;
ptr = nullptr;
}
}
};

请阐述OOP中引入构造函数机制的原因,并请给出控制一个类创建实例个数的手段(举例)

单例模式、或者使用一个静态成员变量记录实例化的类数量,超过则拒绝实例化。

具体略

何为引用?其主要作用是什么?何时需要将一个函数的返回值类型定义为引用类型?如果随意将函数的返回值类型定义为引用类型会有什么危害?

  1. 定义:C++通过引用将一个左值和已有的变量绑定在一起(称为引用),引用和其绑定的变量使用相同的内存,被等价的修改,相当于变量的一个别名
  2. 作用:主要用于函数传参时代替指针,形参声明引用,实参传引用,可以在函数内部修改外部变量(通过引用)
  3. 什么时候返回:当希望提高效率避免对象值拷贝时,返回引用
  4. 危害:若返回值是引用,不能把局部变量返回,这是UB

你认为最有价值的C++程序设计应该遵守的5条规则,并简明分析其意义所在

C++赋予一个空类哪些成员函数

class Empty {
// Empty();
// ~Empty();
// Empty(const Empty&);
// Empty& operator=(const Empty&);
// Empty* operator&();
// const Empty* operator&() const;
};

更详细的见C++面向对象编程基础

define和template的区别

  • define不做类型检查,直接展开,template会类型检查
  • define的作用范围是全局的,除非用#undef结束定义
  • 模板很好的支持代码复用

面向对象程序设计的特点

封装、继承、多态,通过消息传递、对象间分工协作实现程序运转

什么是ADT?试以时间类型为例简要描述

Abstract Data Type 抽象数据类型

结构化编程需要关注抽象数据类型、联系、优先级、类型转换(强制、隐式、溢出?)、求值顺序、副作用

描述略