-
关于 C++,下面哪个描述是错误的?
C++ 程序从“诞生”到“消亡”,要经历四个阶段:编码、预处理、编译和运行
“编码”是 C++ 程序生命周期中最重要的阶段
C++ 支持“多范式”编程,各个编程范式彼此独立,不相互依赖
“面向过程”和“面向对象”是 C++ 中最基本的编程范式
A,正确,参见第 1 讲。 B,正确,编码阶段决定了后面三个阶段的质量。 C,错误,编程范式不是完全独立的,而是互相依赖互相渗透,没有绝对明确的界限。 D,正确,“面向过程”和“面向对象”是 C++ 最早支持的两个范式。
-
现代 C++ 内置了对并发、线程的支持,下面哪些说法是正确的?
call_once 能够避免函数被线程重复调用
thread_local 表示线程独占所有权,每个线程都会有变量的独立副本,读写不会互相影响
线程读写原子变量也要加锁,否则会有数据竞争
函数 async() 会启动一个线程,但不一定会立即启动
A,正确,call_once() 搭配 once_flag 共同实现。 B,正确,这是线程局部存储的作用。 C,错误,原子变量的读写都是原子的,不需要锁。 D,正确,async() 内部隐藏了线程的工作机制,可以传递参数强制要求立即启动。
-
C++ 程序起始于编码阶段,下面哪些关于它的描述是正确的?
应该多使用空格和空行,分隔开代码的逻辑段落,增强可读性
C++ 标准库采用的是“snake_case”命名法,但我们在实际开发中不必严格照搬
宏和常量的名字应该全大写,用来提醒代码阅读者
写注释不宜过多,应该点到为止,否则会增加注释的维护成本
A,正确,多留白,给大脑留出一点休息的时间。 B,正确,自定义类应该用 CamelCase,视觉效果更好。 C,正确,宏和常量是代码里的特殊角色,全大写更醒目。 D,正确,太多的注释可能会淹没代码,而且容易写错,误导阅读者。
-
关于网络通信,你觉得哪些描述是错误的?
使用 libcurl 的时候,如果想要自己处理 HTTP 响应数据,就必须编写回调函数
cpr 内部封装了 libcurl 的 easy 系列接口,可以直接从函数的返回值获取 HTTP 响应数据
ZMQ 只能使用 TCP 协议在进程间传输数据
使用 libcurl 和 cpr 开发不出 HTTP 服务端应用
A,正确,这是 libcurl 接口决定的,参见第 16 讲。 B,正确,cpr 让写 HTTP 客户端更直观、自然。 C,错误,ZMQ 还支持进程内通信,使用共享内存或者 Domain Socket。 D,正确,libcurl 和 cpr 只是 HTTP 客户端。
-
预处理阶段处理的是编码阶段产生的源码,下面关于它的哪一项描述是错误的?
预处理指令不属于 C++ 语言体系,不受 C++ 语法限制
“#include”只能包含后缀是“.h”“.hpp”的 C/C++ 头文件,
宏定义(#define)的文本替换太灵活,应当少用慎用
使用条件编译可以提早优化代码,产生最适合系统、编译环境的代码
A,正确,预处理使用自己的语法规则,不应用 C++ 语法。 B,错误,“#include”可以包含任意文件,不受后缀名限制。 C,正确,滥用宏会导致代码难以阅读理解。 D,正确,条件编译是 C/C++ 高效的秘笈之一。
-
编译阶段发挥作用的是编译器,生成二进制可执行程序,下面关于它的哪些描述是错误的?
可以在编译阶段使用一些特殊指令来操作编译器,也就是针对编译器编程
标记了“deprecated”属性的函数、类会被编译器禁止使用
标准里的属性比较少,可以自定义属性标签,指示编译器工作
static_assert 不会生成运行阶段可执行的代码
A,正确,参见第 4 讲。 B,错误,“deprecated”属性只会导致编译警告,函数和类仍然可以使用。 C,错误,属性标签是由编译器负责解释的,自定义标签编译器无法识别。 D,正确,静态断言只在编译阶段生效。
-
面向对象编程是 C++ 里的主流编程范式之一,下面哪些关于它的描述是正确的?
面向对象编程可以完美地模拟出现实世界中的对象关系
应当少用慎用继承,避免设计出过深的类层次
应当使用“委托构造”的特性来简化编写多个构造函数的工作
应该尽量使用 typedef 和 using 定义类型别名,解除与外部类型的强联系
A,错误,没有任何编程范式可以完美模拟现实世界。 B,正确,太深的类层次难以维护。 C,正确,委托构造比 init() 更高效。 D,正确,类型别名相当于桥接模式,隔离了类型的使用者和提供者。
-
自动类型推导是现代 C++ 的重要特性,下面的哪些说法是正确的?
自动类型推导不是必需的,没有它我们也能写出正确的 C++ 代码
auto 只能推导出“值类型”,不能推导出“引用类型”
auto 只能用于初始化,而 decltype 没有这个限制,可以用在任何地方
应当尽量使用“const auto&”的形式,避免拷贝的代价
A,正确,但自动类型推导可以简化程序员的工作,而且会引出很多新编程技巧、用法。 B,正确,参见第 6 讲,要推导出引用、指针需要加 & 和 *。 C,正确,auto 需要初始化语句里的表达式,而 decltype 自带表达式。 D,正确,单纯的 auto 会推导出值类型,调用拷贝构造函数,成本较高。
-
C++ 使用 const/volatile/mutable 来修饰变量,下面的哪些说法是错误的?
有 const 标记的常量其实也是变量,不是绝对不能修改的
“const int *”和“int* const”的意思相同,都表示指向常量的指针
const 成员函数无法修改类的成员变量
mutable 可以和 volatile 联合使用,一起修饰成员变量
A,正确,const 常量也是变量,可以修改,但修改通常会被优化掉,体现不出来。 B,错误,前者是指向常量的指针,后者指向的是变量,但指针是常量。 C,错误,const 成员函数可以修改 mutable 成员变量。 D,正确,mutable 与 volatile 不冲突,与 const 无法共存。
-
都说智能指针很“智能”,那么下面哪些说法是正确的?
智能指针是代理模式和 RAII 技术的具体实践
智能指针其实并不是指针,而是对象
unique_ptr 不允许拷贝,只能转移,任何时刻原始指针只能被一个 unique_ptr 管理
shared_ptr 的行为最接近原始指针,所以可以在任何地方替代原始指针,消灭内存泄漏
A,正确,参见第 8 讲和第 20 讲。 B,正确,智能指针重载了 * 和 ->,模拟了指针的行为。 C,正确,unique_ptr 是指针“独占”所有权,不能共享。 D,错误,shared_ptr 有少量的成本,而且有无法克服的循环引用风险,需要搭配 weak_ptr 才能获得最佳效果。
-
下面哪些说法 / 用法错误地理解了异常?
异常是错误码的一种替代方案,目的是更优雅地处理错误
抛出异常时只能使用 std::exception 或者它的子类
异常的抛出和处理有一定的代价,不应该滥用异常
有 noexcept 标记的函数绝对不会抛出异常
A,正确,参见第 9 讲。 B,错误,任何 C++ 类型都可以作为异常被抛出。 C,正确,处理异常需要展开函数调用栈。 D,错误,noexcept 的含义是请编译器优化消除异常的成本,不能禁止异常的发生。
-
lambda 表达式是 C++ 函数式编程的核心要素,下面的哪些描述是正确的?
lambda 表达式是变量,不是函数
lambda 表达式不允许嵌套定义,不能在一个 lambda 表达式里再定义另外一个 lambda 表达式
空方括号(“[]”)形式的 lambda 表达式没有捕获任何外部变量
可以在“[]”里混用引用捕获和值捕获
A,正确,lambda 表达式不是函数是变量,但可以像函数一样被调用。 B,错误,lambda 表达式不是函数,所以可以嵌套。 C,正确,[] 没有捕获,不能引用外部变量。 D,正确,这种方式让 C++ 的 lambda 表达式用起来更灵活。
-
关于 C++ 里的字符串,下面哪个说错了?
std::string 只能存储 char,不能很好地支持 Unicode
在“R”()””形式的原始字符串里,不需要考虑转义,写任何字符都没有问题
字符串的拷贝、修改代价比较高,应当尽量用 const string& 的方式来引用字符串
正则算法都是“只读”的,不会变动原字符串
A,正确,虽然不能很好地支持,但如果把 Unicode 看作是二进制字节数据,还是可以存储的。 B,错误,需要用界定符防止字符串里出现“R”()””。 C,正确,在 C++17 之后可以使用 string_view。 D,正确,参见第 11 讲。
-
C++ 标准容器是对数据结构的抽象和封装。关于标准容器,下面的哪个理解是正确的?
最常用的容器是 array 和 vector,速度最快,开销最低
set 和 map 只能按 Key 升序排列,因为模板参数限定了使用 less
无序容器基于散列表,所以性能一定比有序容器的红黑树好
容器里不能存放原始指针,只能存放智能指针
A,正确,它们本质上是连续存储的内存区域,所以效率最高。 B,错误,可以修改模板参数改变排序准则。 C,错误,坏的哈希函数可能会有更大的时间消耗和冲突几率。 D,错误,原始指针可以拷贝,符合值语义,可以放进容器,但需要用户自己管理指针的生命周期。
-
C++ 里的算法非常有用,那么下面哪些对它描述是正确的?
标准库里的算法本质上就是 for 或者 while 循环
算法通过迭代器间接操作容器
按考分取成绩前十名,应该使用算法 sort() 或者 stable_sort()
在有序容器上执行二分查找,使用算法 binary_search 就可以快速定位元素的位置
A,正确,参见第 13 讲。 B,正确,算法的入参都是迭代器,不会直接操作容器。 C,错误,最佳的算法是 partial_sort,只排序部分数据。 D,错误,binary_search 返回 bool,只能判断元素是否存在,不能定位。
-
下面关于序列化、数据交换的说法,哪些是正确的?
JSON 是纯文本格式,所以序列化效率不高,但可读性好
MessagePack 是二进制格式,不适合序列化有复杂结构的数据
使用 ProtoBuffer 前必须要先定义 IDL 文件
JSON、MessagePack 和 ProtoBuffer 这三种格式并无优劣之分,都有各自最适合的应用场景
A,正确,这是文本格式的基本特点。 B,错误,二进制格式与复杂数据结构无关,MessagePack 可以序列化任意数据类型。 C,正确,PB 是有模式的序列化格式。 D,正确,序列化格式没有绝对的优劣,文本格式不一定就是差,二进制格式不一定就是好,看应用场景。
-
C++ 可以搭配各种脚本语言,实现混合编程,下面哪些是关于脚本语言的错误说法?
C 语言也能开发 Python 扩展模块
pybind11 可以让 Python 调用 C++ 函数、类,但反过来不行,C++ 无法调用 Python 脚本
LuaJIT 的 ffi 功能非常方便易用,不仅可以调用 C 接口,也可以调用 C++ 接口
luaL_dostring()/luaL_dofile() 加载脚本只是形式上有差别,功能性能无差别
A,正确,Python 的标准接口就是 C 语言,pybind11 对 C 接口做了封装。 B,错误,pybind11 的功能是双向的,可以互相调用,但课程里只讲了 Python 调用 C++。 C,错误,LuaJIT 只能调用纯 C 接口,不支持 C++ 接口。 D,错误,luaL_dofile() 每次调用都会读磁盘载,效率较低。
-
性能分析是调优的基础,关于性能分析,下面哪些说法是正确的?
top 是最基本的性能分析工具,可以查看 CPU、内存、网络流量、磁盘 I/O 等很多指标
perf 的性能分析基于“采样”,所以得到的数据不可能非常准确,只能是个大概
gperftools 里包含 CPUProfiler 和 HeapProfiler,可用于分析 CPU 和内存
火焰图折叠了复杂的函数调用栈,比文本形式的统计列表更直观,是性能分析的首选
A,错误,top 只能看 CPU 和内存,其他指标要用别的工具。 B,正确,采样是有固定频率的,所以有可能会遗漏一些数据。 C,正确,参见第 18 讲。 D,正确,参见第 18 讲。
-
你认为下面关于设计模式和设计原则的说法哪些正确的?
设计模式主要用于面向对象编程,但也可以应用于其他范式
经典的设计模式有三大类:创建型模式、结构型模式和行为模式
设计原则比设计模式更泛化,没有可操作的“定式”“套路”
DRY 和 KISS 原则不属于面向对象设计
A,正确,设计模式是一种思想,虽然发源于面向对象,但已经扩展到了很多其他的范式和应用领域。 B,正确,GOF 里分为三类,其他的还有并发模式、存储模式。 C,正确,设计原则只是一些基本思想,所以没有具体的操作方式。 D,正确,DRY 和 KISS 不涉及面向对象,更多地偏向代码编写规范。
-
下面关于经典设计模式的说法,正确的是哪几个?
在 C++ 里要实现线程安全的单件必须使用 call_once+once_flag
只要是创建出对象的函数,就可以认为应用了工厂模式
一个对象可以即是适配器,又是代理
职责链模式的链条上至少要有一个对象,否则就无法处理请求
A,错误,单件也可以用静态局部变量实现,也是线程安全的。 B,正确,工厂模式的思想就是封装对象的创建。 C,正确,很多模式不是互相排斥的,而是可以重叠复合。 D,正确,空的链条不能处理请求,但链条里可以是空对象,不处理请求。