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 }