C++ 天坑指南

 

 

1. 寻找下面代码出现的问题

 1 //来自 https://www.zhihu.com/question/310052411/answer/2209276153
 2 #include <vector>
 3 #include <iostream>
 4 #include <iterator>
 5 
 6 using namespace std;
 7 
 8 int main()
 9 {
10     vector<int> vec(istream_iterator<int>(cin), istream_iterator<int>());
11     int a = vec.size();
12 }

  说真的,我看了许久也没看出问题,就算我将这部分代码复制到我本地的vs2012中。

  后来我换了一种写法:

 1 #include <vector>
 2 #include <iostream>
 3 #include <iterator>
 4 
 5 //using namespace std;
 6 
 7 int main()
 8 {
 9     std::vector<int> vec(std::istream_iterator<int>(std::cin), std::istream_iterator<int>());
10     int a = vec.size(); // boom
11 }

  编译器告诉我:

error C2751: “std::cin”: 无法限定函数参数的名称

  哦吼吼吼吼吼吼吼吼!原来 std::cin 是函数参数名!

  那么,std::istream_iterator<int> 就是函数类型!

  vec 是函数名!

  std::vector<int> 是函数返回值!

  std::vector<int> vec(std::istream_iterator<int>(cin), std::istream_iterator<int>()); 是函数声明!

  ……

  再换一种写法:

 1 #include <vector>
 2 #include <iostream>
 3 #include <iterator>
 4 
 5 //using namespace std;
 6 
 7 int main()
 8 {
 9     std::vector<int> vec(int(cin), int());
10     int a = vec.size();
11 }

  没错,它就是长这个样子的……

  原因在于,函数参数名是可以被括号括住的……

 

2. 解释以下代码的含义

1 void(*func(void(*)(int)))(int)
2 {
3 }

 

这个问题我在 C++ std::function实现 中写过了,实际上就是参数和返回值都是函数指针的函数。

 

3. 分别判断变量 a 与 b 的类型

1 //c++17
2 auto a = {1};
3 auto b{1};

a: std::initializer_list<int>

b: int

 

4. 以下代码输出什么 (vs2019, c++17)

1 const int a = 123;
2 int& b = const_cast<int&>(a);
3 b = 456;
4 
5 std::cout << a << ", " << b << std::endl;

输出: 123, 456  

 

好的,接下来判断以下代码的输出:

1 const int& a = 123;     
2 int& b = const_cast<int&>(a); 
3 b = 456;   
4     
5 std::cout << a << ", " << b << std::endl;

输出: 456, 456

 

然后还有这段代码的输出:

 1 class Item 
 2 {
 3 public:
 4     Item() : Item(0) {}
 5     Item(int a) : _a(a) {}
 6 
 7     int TestFunc() const 
 8     {
 9         Item& pThis = const_cast<Item&>(*this);
10         pThis.Change();
11         return _a;
12     }
13 
14     int TestFunc()
15     {
16         Change();
17         return _a;
18     }
19 
20 private:
21     void Change() { ++_a; }
22 
23 public:
24     int _a;
25 };
26 
27 int main()
28 {
29     Item iA(123);
30     const Item iB(123);
31     Item& iC = const_cast<Item&>(iB);
32     ++(iC._a);
33 
34     int c = iC._a;
35     int a = iA.TestFunc();
36     int b = iB.TestFunc();
37 
38     std::cout << a << ", " << b << ", " << c << std::endl;
39     
40     return 0;
41 }

输出:124, 125, 124

 造成以上的原因:常量折叠 - 高性能架构探索 (ccppcoding.com)

 

5. 寻找以下代码中的错误(这个属于常识性错误)

1 std::string str1 = "abcdef";
2 const char* sub1 = str1.substr(1, 2).c_str();
3 std::cout << sub1 << std::endl;

std::string 的 substr函数返回的是临时变量,用完即销毁,c_str()返回的指针自然也无效。

6. 尝试找出以下代码可能出现的问题

#include <thread>
#include <vector>

int main()
{
    bool isOver1 = false,
         isOver2 = false;
    
    std::vector<int> v1, v2;

    //线程1
    std::thread(
        [&]() {
            for (int i = 0; i < 1000; ++i)
                v1.push_back(i);
            
            //标记线程1处理完毕
            isOver1 = true;
        }
    ).detach();

    //线程2
    std::thread(
        [&]() {
            for (int i = 0; i < 1000; ++i)
                v2.push_back(i);
            
            //标记线程2处理完毕
            isOver2 = true;
        }
    ).detach();

    //等待两线程都处理完
    while (!(isOver1 && isOver2)) {}

    return 0;
}

在debug模式下,此代码一般不会出现问题,但是在开启了优化之后的release模式,此段代码就会出现问题:

  即使两个线程都被处理完,while (!(isOver1 && isOver2)) {}  此循环也不会正常跳出。

  原因除了编译器的优化之外,还有CPU乱序执行作祟。

  正确处理方式是 :

    1. 在MSVC环境下,isOver1 与 isOver2加上 volatile 关键字,因为MSVC环境的volatile 同时拥有acquire和release含义的内存屏障,但非常不建议这样做。具体查看 C/C++ 中的 volatile - 知乎 (zhihu.com) 以及 volatile (C++) | Microsoft Learn

    2. 使用std::atomic 或者 std::mutex, std::lock_guard 与 std::condition_variable 组合的形式,使得在多线程情况下能够正常正确运行。具体查看 内存屏障(Memory Barrier)究竟是个什么鬼? - 知乎 (zhihu.com) 以及 std::memory_order - cppreference.com 以及 带你了解缓存一致性协议 MESI (qq.com)

  强烈建议在使用C++过程中彻底忘掉 volatile 关键字,要么用 std::atomic,要么用互斥锁,自旋锁等线程同步方式保证变量安全。

 

7. 观察以下代码,回答问题:

 1 int func()
 2 {
 3     int b = 123;
 4     return b;
 5 }
 6 
 7 int main()
 8 {
 9     int b = func();
10     {
11         int a = 234;
12         int c = a + b;
13     }
14     return 0;
15 }

Q: main 函数中的栈变量 a 与栈变量 c 是何时释放的?

  其实我对操作系统并不太熟,因为我一向执行应用层的工作,工作中很少遇到,但是最近在面试中折了一堆有关操作系统的知识,这里记录一下。

  乍一看,会认为是在c+b执行完后,跳出代码块之后释放的,但实际上并不是。

  

 1 b$ = 4
 2 a$1 = 36
 3 c$2 = 68
 4 main    PROC                        ; COMDAT
 5 
 6 ; 9    : {
 7 
 8 $LN3:
 9     push    rbp
10     push    rdi
11     sub    rsp, 328                ; 00000148H
12     lea    rbp, QWORD PTR [rsp+32]
13     lea    rcx, OFFSET FLAT:__8964ECFF_main@cpp
14     call    __CheckForDebuggerJustMyCode
15 
16 ; 10   :     int b = func();
17 
18     call    ?func@@YAHXZ                ; func
19     mov    DWORD PTR b$[rbp], eax
20 
21 ; 11   :     {
22 ; 12   :         int a = 234;
23 
24     mov    DWORD PTR a$1[rbp], 234            ; 000000eaH
25 
26 ; 13   :         int c = a + b;
27 
28     mov    eax, DWORD PTR b$[rbp]
29     mov    ecx, DWORD PTR a$1[rbp]
30     add    ecx, eax
31     mov    eax, ecx
32     mov    DWORD PTR c$2[rbp], eax
33 
34 ; 14   :     }
35 ; 15   :     return 0;
36 
37     xor    eax, eax
38 
39 ; 16   : }
40 
41     lea    rsp, QWORD PTR [rbp+296]
42     pop    rdi
43     pop    rbp
44     ret    0
45 main    ENDP

看见那两个push和两个pop了没?是在main函数开始时申请栈空间,main函数执行完才释放的。

 

8. 尝试说出以下代码产生的结果 (MSVC) :

#include <iostream>

int main()
{
    double a = 3.14 + 1e17 - 1e17;
    std::cout << a << std::endl;

    double b = 3.14 + (1e17 - 1e17);
    std::cout << b << std::endl;

    system("pause");
    return 0;
}

输出: 0     3.14

这是由于浮点数的精度造成的。请查看: 程序员必知之浮点数运算原理详解_tercel_zhang的博客-CSDN博客_浮点数运算

 

9. 有时编码中的疏忽大意,会产生难以理解的结果。注意查看以下代码,并尝试说出它的输出:

 1 #include <iostream>
 2 
 3 int main()
 4 {
 5     const unsigned int a = 1;
 6     const signed int b = -1;
 7 
 8     if (a > b)
 9         std::cout << "1 > -1" << std::endl;
10     else
11         std::cout << "1 <= -1" << std::endl;
12 
13     system("pause");
14     return 0;
15 }

输出: 1 <= -1

为什么?

在有符号数与无符号数之间运算时,C++隐式地将有符号数转成无符号数,然后进行计算。所以,结果显而易见……

可以使用 -Werror=... 将警告变成错误,比如此例中,可以在编译时加入参数 -Werror=sign-compare (好像是,我没测试)

 

9. 有时编码中的又一个疏忽大意,会产生另一个难以理解的结果。

看看以下有关静态全局变量的bug:

 1 //test.h
 2 
 3 #pragma once
 4 
 5 #include <iostream>
 6 #include <vector>
 7 
 8 class Test
 9 {
10 public:
11     Test();
12     ~Test(){}
13 
14     //Push a number to '_datas'.
15     void Push(int n);
16 
17     //Get the size of '_datas'.
18     int GetSize() const { return _datas.size(); }
19 
20 public:
21     std::vector<int> _datas;
22 };
23 
24 static Test G_Test;
25 
26 struct Run
27 {
28     Run()
29     {
30         G_Test.Push(1);
31     }
32 };

  一个Test类,里面存在一个 std::vector 类型的成员变量 _datas ,两个成员函数 Push 和 GetSize 分别对成员变量 _datas 进行写和读。然后,创建了一个 static 修饰的全局变量 G_Test .

  下面还有一个结构体  Run, 调用  Run();  就能为全局变量 G_Test 的成员 _datas 增加一个元素1。

  然后,为了让 G_Test 默认就有一个元素1, 所以,在 Test 的构造函数中调用了  Run(); :

  

 1 //test.cpp
 2 
 3 #include "test.h"
 4 
 5 Test::Test()
 6 {
 7     Run();
 8 }
 9 
10 void Test::Push(int n)
11 {
12     _datas.push_back(n);
13 }

  

  在主函数中,直接获得 G_Test 成员的size,于是调用了:

 1 #include <iostream>
 2 #include <vector>
 3 
 4 #include "test.h"
 5 
 6 int main(int, char**) 
 7 {
 8     int s = G_Test.GetSize();
 9     std::cout << "size: " << s << std::endl;
10     
11     return 0;
12 }

  有人(说的就是我自己)可能会想当然地认为输出 “size: 1”,但实际输出却是“size: 0”.

  具体的原因稍微有些绕,这需要一步一步去分析:

    1. 首先,G_Test 声明于 test.h 中,并同时被 test.cpp 和 main.cpp include,所以,可以想到,在 main.o 和 test.o 中各有一份 G_Test,并且他们的地址不相同;

     但另外一个问题出现了:push 的操作是在构造函数中进行的,即便是拥有两份 G_Test,那么两份 G_Test 一定都会经过初始化,只要经过初始化,那么对象中的 _datas 一定是被 push 了元素,怎么还会 GetSize 出 0 ?

    2. 既然想到 main.o 和 test.o 中各有一份 G_Test,接下来的问题就简单了。这里假设 main.o 中的 G_Test 命名为 "G_Test_M",test.o 中的命名为 "G_Test_T",而 G_Test_M 与 G_Test_T一定有初始化的先后顺序:

      2.1 首先,G_Test_M 先行初始化,初始化中调用 Run(); Run(); 中又调用 G_Test.Push(1); 但注意,这里的 G_Test.Push(1); 其实是 G_Test_T 的 Push(1),并非 G_Test_M 的。

        此时 G_Test_T.GetSize() 为 1, G_Test_M.GetSize() 为 0;

      2.2 然后,G_Test_T 开始初始化,初始化过程中同样调用了成员变量的构造,使得 G_Test_T 中的 _datas 被初始化。

        此时 G_Test_T.GetSize() 为 0, G_Test_M.GetSize() 为 0;

      2.3 G_Test_T 初始化过程中调用Run(),Run() 中调用了 G_Test_T.Push(1)。

        此时 G_Test_T.GetSize() 为 1, G_Test_M.GetSize() 为 0;

    3. 上述都是在 main 函数之前进行的操作,之后才开始进入 main 函数,调用 G_Test.GetSize(); 这里的 G_Test 就是 G_Test_M。

 

  为了探究上述过程,可通过输出log的方式追踪调用信息。

  这个问题是我在测试静态递归的时候遇到的,其实用单例模式加上外部调用 Init 函数是最为保险的方式。但是当时是写测试代码,于是就手懒了一下,结果就遇见了这个问题。所以记录一下,不然明天可能就忘了。

  在此依然提醒自己,在原本就很复杂的机制下,C++每次更新还是越来越臃肿,现代C++更是恐怖。"想当然"式地敲代码势必会带来一些问题,而这些问题是难以发现的,浪费大量时间。所以在正常工作中还是稳健为主,做好单元测试,架构还是先在纸上画好图再去编码。若要提升自己,想去尝试一些新的内容也未尝不可,探究其中的内在运作原理至关重要,只有知晓其内在才能对各种情况游刃有余。

 

10. 有关关键字 constexpr 在类中成员的使用 (C++17)

  注意,以下内容在C++11,C++14,C++17的表现可能不同,这里使用 C++17

  对于 constexpr 修饰的基础类型变量很好理解,变量需要立即初始化,并且赋值右表达式是常量表达式

 

   对于 constexpr 修饰的自定义类型变量,或者成员函数,这种情况就很具有迷惑性。  

  constexpr 说明符声明可以在编译时对函数或变量求值,但这似乎不是一定的,在某些时候,它有些类似 inline 的约定——即使函数存在 inline 约束,函数也未必是内联的,取决于编译器的判断。

 

  先看 constexpr 修饰的成员函数

 1 #include <iostream>
 2 
 3 class Box 
 4 {
 5 public:
 6     Box(int w, int h) 
 7         : _w(w), _h(h)
 8     {
 9     }
10 
11     Box()
12         : Box(0, 0)
13     {
14     }
15 
16     constexpr int GetW() const { return _w; }
17     constexpr int GetH() const { return _h; }
18 
19     constexpr void SetW(int w) { _w = w; }
20     constexpr void SetH(int h) { _h = h; }
21 
22 public:
23     int _w, _h;
24 };
25 
26 int main() 
27 {
28     int w = 2;
29     Box box(2, 1);
30 
31     std::cout << box.GetW() << std::endl;
32     box.SetW(3);
33     std::cout << box.GetW() << std::endl;
34     box.SetW(4);
35 
36     int a = box.GetW();
37     std::cout << a << std::endl;
38 
39     return 0;
40 }

  上述代码中,变量 box 为运行时,但 SetW 函数声明为 constexpr,却依然能够更改运行时变量 box._w,并且不存在编译错误,输出结果也是预期的 2 3 4

 

  再考虑 constexpr 修饰的自定义类型变量

 1 #include <iostream>
 2 
 3 class Box 
 4 {
 5 public:
 6     constexpr Box(int w, int h) 
 7         : _w(w), _h(h)
 8     {
 9     }
10 
11     Box()
12         : Box(0, 0)
13     {
14     }
15 
16     constexpr int GetW() const { return _w; }
17     constexpr int GetH() const { return _h; }
18 
19     constexpr void SetW(int w) { _w = w; }
20     constexpr void SetH(int h) { _h = h; }
21 
22 public:
23     int _w, _h;
24 };
25 
26 int main() 
27 {
28     int w = 2;
29     constexpr Box box(2, 1);
30 
31     std::cout << box.GetW() << std::endl;
32     box.SetW(3);    //Error! Object type is: const Box
33     std::cout << box.GetW() << std::endl;
34     box.SetW(4);    //Error! Object type is: const Box
35 
36     int a = box.GetW();
37     std::cout << a << std::endl;
38 
39     return 0;
40 }

  如若自定义类型变量被 constexpr 修饰,那么它的构造函数必须同样是 constexpr 的,同时此变量被认为是 const 变量,无法调用非 const 函数。

  但是,这种情况下并非完美,比如在 const 函数中修改成员变量的值:

 1 #include <iostream>
 2 
 3 class Box 
 4 {
 5 public:
 6     constexpr Box(int w, int h) 
 7         : _w(w), _h(h)
 8     {
 9     }
10 
11     Box()
12         : Box(0, 0)
13     {
14     }
15 
16     constexpr int GetW() const { return _w; }
17     constexpr int GetH() const { return _h; }
18 
19     constexpr void SetW(int w) const { _w = w; }
20     constexpr void SetH(int h) const { _h = h; }
21 
22 public:
23     mutable int _w, _h;
24 };
25 
26 int main() 
27 {
28     int w = 2;
29     constexpr Box box(2, 1);
30 
31     std::cout << box.GetW() << std::endl;
32     box.SetW(3);    //OK
33     std::cout << box.GetW() << std::endl;
34     box.SetW(4);    //OK
35 
36     int a = box.GetW();
37     std::cout << a << std::endl;
38 
39     return 0;
40 }

  这里将 SetW 函数声明为 const 函数,为了能够修改成员变量,将 Box 类的成员变量修饰为 mutable,此时,SetW 函数能够正常被调用,输出结果也是预期的 2 3 4

  由此看出,constexpr 并非如想象一般严格。所以,在 C++20 中,诞生了两个新的关键字:

    consteval : 它仅可修饰函数,带有求值动作的调用这个函数的表达式必须为常量表达式

    constinit : 它仅可修饰变量,表示此变量为静态变量或线程存储期(Thread-local)变量,不能为局部变量。其初始化表达式必须是一个常量表达式。

 1 #include <iostream>
 2 
 3 consteval int calc(int a)
 4 {
 5     return a + 1;
 6 }
 7 
 8 constinit int g = 3;    //OK
 9 
10 int main() 
11 {
12     int w = 2;
13     constinit int x = 3;    //Error! 'constinit' can only be applied to a variable with static or thread storage duration
14     constexpr int y = 4;
15     
16     calc(10086);    //OK
17     calc(w);    //Error! The value of variable "w" cannot be used as a constant
18     calc(x);    //Error! The value of variable "x" cannot be used as a constant
19     constinit static int z = calc(10086);   //OK
20     calc(y);    //OK
21     
22     return 0;
23 }

 

  

 

posted on 2021-11-17 15:38  __Even  阅读(114)  评论(0编辑  收藏  举报

导航