CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++语言面试问题集锦 目录与说明
  • 第一章 auto与类型推导
    • 问题1:解释auto类型推导!
    • 问题2:auto在何时会推导出不理想的类型?
    • 问题3:使用auto有哪些优点?
    • 问题4:在以下声明后,myCollection的类型是什么?
    • 问题5:什么是尾随返回类型(trailing return types)?
    • 问题6:解释一下decltype!
    • 问题7:何时使用decltype(auto)?
    • 问题8:将两个bool类型相加会得到什么数据类型?
  • 第二章 关键字static及其不同用法
  • 第三章 多态、继承和虚函数
  • 第四章 Lambda函数
  • 第五章 C++中如何使用 const 限定符
  • 第六章 Modern C++的一些最佳实践
  • 第七章 智能指针
  • 第八章 引用、万能引用等
  • 第九章 C++20相关问题
  • 第十章 特殊函数及数量规则
  • 第十一章 C++面向对象设计
  • 第十二章 程序质量
  • 第十三章 标准模板库
  • 第十四章 杂项
  • cppinterviewmostaskedquestions
zhangxf
2025-03-27
目录

第一章 auto与类型推导

# 第一章 auto与类型推导

在本章中,我们将学习C++的类型推导规则,以及如何使用C++11中引入的auto关键字。

# 问题1:解释auto类型推导!

auto类型推导通常与模板类型推导相同,但auto类型推导假定用大括号括起来的初始化器代表std::initializer_list,而模板类型推导并不持有这样的前提。

以下是几个示例:

int* ip;
auto  aip = ip;       // aip是一个指向整数的指针
const  int* cip;
auto  acip = cip;      // acip是一个指向常量整数的指针(值不能修改,但它所指向的内存地址可以修改)
const  int* const  cicp = ip;
auto  acicp = cicp;    // acicp仍然是一个指向常量整数的指针,指针的常量性被丢弃

auto  x = 27;          // (x既不是指针也不是引用),x的类型是int
const  auto  cx = x;   // (cx既不是指针也不是引用),cx的类型是const int
const  auto& rx = x;   // (rx是一个非万能引用),rx的类型是指向const int的引用
auto&& uref1 = x;      // x是int且为左值,所以uref1的类型是int&
auto&& uref2 = cx;     // cx是const int且为左值,所以uref2的类型是const int&
auto&& uref3 = 27;     // 27是int且为右值,所以uref3的类型是int&&

auto  x3 = { 27 };     // 类型是std::initializer_list<int>,值是{27}
auto  x4{ 27 };        // 类型是std::initializer_list<int>,值是{27}
// 在某些编译器中,类型可能会被推导为int,值为27。更多信息见注释。
auto  x5 = { 1, 2.0 }; // 错误!无法为std::initializer_list<T>推导T的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以看出,如果使用大括号初始化器,auto会强制创建一个std::initializer_list类型的变量。如果它无法推导出T的类型,代码就会被拒绝。

我们也看到,auto可以为指针推导出正确的类型,但为了得到引用,我们必须写成auto&。为了保持一致性,如果我们期望得到一个指针,也可以写成auto*。

在函数返回类型或lambda参数中使用auto意味着进行模板类型推导,而非auto类型推导。

参考资料:

  • Scott Meyers所著的《Effective Modern C++》¹
  • 《Modernes C++》² ¹https://amzn.to/38gK5bd ²https://www.modernescpp.com/index.php/c-insights-type-deduction

# 问题2:auto在何时会推导出不理想的类型?

“不可见”的代理类型会导致auto为初始化表达式推导出 “错误” 的类型。std::vector<bool>就是这样一种类型。如果有一个函数返回这种类型,而你只对其中一位信息感兴趣,想用auto声明一个变量来保存它,之后又想使用这个bool类型向量中的某个元素,这会产生未定义行为。

这听起来有点复杂,我们来看代码:

std::vector<bool> foo() {
    //  ...
}

void  bar(bool  b) {
    //  ...
}

auto  someBit = foo()[2];
bar(bits[2]); // 未定义行为
1
2
3
4
5
6
7
8
9
10

std::vector的[]运算符返回T&。std::vector<bool>是一种专门的向量形式,只包含位(bit),而C++不允许对位的引用。这里面的情况有点复杂(双关语),你可以在《Effective Modern C++》中详细了解。简而言之,如果你请求一个bool类型,会发生隐式转换。虽然推导出的类型取决于具体实现,但它会是一个指向临时对象的指针。这可不是人们想要的结果。

在这种情况下,最好要么根本不使用auto,要么使用Meyers所说的显式类型初始化器习惯用法。

auto  highPriority = static_cast<bool>(features(w)[5]);
1

当你想要的类型精度低于函数返回的类型时,也会出现类似问题,比如当你知道可能的输出范围,想节省一些内存时。例如,一个函数返回double类型,但你想要float类型。使用auto不会进行隐式转换,但你可以用上述习惯用法强制转换。

这种习惯用法在处理代理类型时也很有用。

参考资料:

  • Scott Meyers所著的《Effective Modern C++》³
  • 维基百科⁴ ³https://amzn.to/38gK5bd ⁴https://en.wikipedia.org/wiki/Proxy_pattern

# 问题3:使用auto有哪些优点?

auto变量必须初始化,因此它们通常不会出现可能导致可移植性或效率问题的类型不匹配情况。auto还能让重构更轻松,并且通常比显式指定类型输入的内容更少。

auto变量主要可以提高正确性、性能、可维护性和健壮性。输入起来也更方便,但这是它最不重要的优点。

当你确实想显式指定类型时,可以考虑声明局部变量auto x = type{ expr };。这样做能表明代码是在明确请求转换,具有自文档化的作用,并且能保证变量被初始化,此外,它还能防止意外的隐式窄化转换。只有当你确实想要显式窄化时,才使用()而不是{}。

// 假设features(w)[5]返回的类型精度高于bool,
// 使用static_cast<bool>进行显式转换,防止auto推导错误类型
auto  highPriority = static_cast<bool>(features(w)[5]);
1
2
3

如果你担心可读性,前面提到的技巧会有帮助,否则你不必为此烦恼,现代的集成开发环境(IDE)会在你将鼠标悬停在变量上时显示其确切类型。

对于局部变量,如果你使用auto x = expr;这种声明方式,有很多优点:

  • 能保证你的变量被初始化。如果你忘了初始化,编译器会报错。
  • 没有临时对象和隐式转换,因此效率更高。
  • 使用auto能保证你使用正确的类型。
  • 在维护和重构时,无需更新类型。
  • 对于内置类型的算术运算,这是一种可移植地表示特定于实现的类型的最简单方法。这些类型可能因平台而异,使用auto还能确保不会意外出现有损的窄化转换。
  • 你可以省略难以书写的类型,比如lambda表达式、迭代器等。

参考资料:

  • Scott Meyers所著的《Effective Modern C++》⁵
  • Sutter’s Mill⁶ ⁵https://amzn.to/38gK5bd ⁶https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/

# 问题4:在以下声明后,myCollection的类型是什么?

auto myCollection = {1,2,3};
1

其类型是std::initializer_list<int>。int部分可能很容易理解,至于std::initializer_list,你要知道,在auto类型推导中,如果你使用大括号,有两种情况。

如果你在大括号之间什么都不写,会得到一个编译错误,因为编译器无法从<用大括号括起来的初始化列表>()推导出std::initializer_list<auto>。所以你得到的不是一个空容器,而是一个编译错误。

如果你在大括号之间至少有一个元素,类型将是std::initializer_list。

如果你想知道这个类型是什么,应该了解它是一个轻量级的代理对象,用于访问const T类型的对象数组。在以下情况会自动构造:

  • 当使用大括号初始化列表来列表初始化一个对象,且相应的构造函数接受一个std::initializer_list参数时。
  • 当大括号初始化列表用于赋值的右侧或作为函数调用参数,且相应的赋值运算符/函数接受一个std::initializer_list参数时。
  • 当大括号初始化列表绑定到auto时,包括在范围for循环中。

初始化列表可以用一对指针或一个指针和一个长度来实现。复制std::initializer_list被认为是浅拷贝,因为它不会复制底层对象。

参考资料:

  • C++参考:std::initializer_list⁷
  • C++参考:列表初始化⁸ ⁷https://en.cppreference.com/w/cpp/utility/initializer_list ⁸https://en.cppreference.com/w/cpp/language/list_initialization

# 问题5:什么是尾随返回类型(trailing return types)?

尾随返回类型允许在参数列表之后(在->之后)声明函数的返回类型:

auto  getElement(const  std::vector<int>& container, int  index) const  -> int ;
1

我们没有将int作为起始的返回类型,而是在该行开头使用auto关键字,并在末尾箭头(->)之后添加int作为返回类型。

借助尾随返回类型,例如,我们可以省略枚举(enum)的作用域。

假设我们有如下类定义:

class Wine  {
public :
    enum  WineType  { WHITE, RED, ROSE, ORANGE }; 
    void  setWineType(WineType wine_type); 
    WineType getWineType() const ; 
    //...
private :
    WineType _wine_type;
};
1
2
3
4
5
6
7
8
9

不用写成:

Wine::WineType Wine::getWineType() {
    return  _wine_type;
}
1
2
3

我们可以这样写:

auto Wine::getWineType() -> WineType {
    return  _wine_type;
}
1
2
3

在该行开头,编译器无法知晓作用域,因此我们必须写出Wine::WineType。而当我们在末尾声明返回类型时,编译器已经知道我们处于Wine类的作用域内,所以无需重复该信息。

根据作用域的名称不同,这样做可能会节省一些字符,但至少不必重复类名。

使用尾随返回类型更具说服力的一个原因是,当返回类型取决于参数类型时,它可以简化函数模板。下一个问题中会对此进行更多介绍。

# 问题6:解释一下decltype!

decltype是一个关键字,用于查询变量或表达式的类型。它在C++11中被引入,主要用于泛型编程,在泛型编程中,要表达依赖于模板参数的类型通常很困难,甚至是不可能的。

给定一个名称或表达式,decltype会告知你该名称或表达式的类型。

让我们用一些变量和函数调用来试试:

const  int&& foo(); 
const  int  bar(); 
int  i; 
decltype(foo()) x1; 
decltype(bar()) x2; 
decltype(i) x3; 
1
2
3
4
5
6

这并不意外。再试试一些表达式:

struct A  {
    double  x;
};
const A* a; 
decltype(a->x) y;
decltype((a->x)) z = y;
1
2
3
4
5
6

结果仍符合预期。

或许decltype的主要用途是在声明函数模板时,当函数的返回类型取决于其参数的类型。一个常见的用例是简单的加法运算:两个可能不同类型的值相加,结果可能是多种不同类型,尤其是涉及运算符重载时。

template  <class  T , class  U>
auto  add(T const& t, U const& u) -> decltype(t+u) {
    return  t+u;
}
1
2
3
4

在C++14中,增加了函数返回类型推导这一特性,所以我们可以直接使用auto。

decltype对于与lambda相关的类型也很有用:

auto  f = [](int  a, int  b) -> int  {
    return  a * b;
};
decltype(f) g = f;
1
2
3
4

# 问题7:何时使用decltype(auto)?

decltype(auto)这个习惯用法是在C++14中引入的。与auto类似,它从初始化器中推导类型,但它使用decltype规则进行类型推导。

正如我们在上一个问题中提到的,它允许在泛型代码中转发返回类型。你甚至可以对返回类型使用auto类型推导,甚至可以像这样返回一个引用,或者将返回类型声明为const:

auto  const& Example(int  const& i) {
    return  i;
}
1
2
3

另一方面,在泛型代码中,你必须能够在不知道处理的是引用还是值的情况下,完美转发返回类型。decltype(auto)赋予了你这种能力:

template<class  Fun , class... Args>
decltype(auto) foo(Fun fun, Args&&... args) {
    return  fun(std::forward(args)...);
}
1
2
3
4

如果你只想用auto来实现相同的功能,就必须为上述示例函数声明不同的重载,一个auto返回类型总是推导为纯值,另一个auto&返回类型总是推导为引用类型。

当你希望对初始化表达式应用decltype类型推导规则时,它也可以用于声明局部变量。

Widget w; 
const Widget& cw = w; 
auto  myWidget1 = cw; 
decltype(auto) myWidget2 = cw; 
1
2
3
4

# 问题8:将两个bool类型相加会得到什么数据类型?

下面这段代码片段的输出是什么:

#include  <iostream> 
auto  foo(bool  n, bool  m) {
    return  n + m;
}

int  main() {
    bool  a = true;
    bool  b = true;
    auto  c = a + b;
    std::cout << c << ", " << typeid(c).name() << '\n';
    std::cout << foo(a, b) << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12

答案是c和foo()的返回值都等于2。显然,它们的类型不是bool,而是int。

为了理解发生了什么,最好的办法是将代码复制粘贴到CppInsights上,它会进行源到源的转换。这有助于你从编译器的角度查看源代码。

你会看到这样的代码:

int  c = static_cast<int>(a) + static_cast<int>(b);
1

foo函数的情况也非常类似。所以我们的两个bool类型变量a和b都被提升了,在相加之前它们被转换为了整数。

上次更新: 2025/03/27, 20:29:48
C++语言面试问题集锦 目录与说明
第二章 关键字static及其不同用法

← C++语言面试问题集锦 目录与说明 第二章 关键字static及其不同用法→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式