首页 > C++的骚操作总结
头像
肖战公关团队
编辑于 2020-12-21 12:38
+ 关注

C++的骚操作总结

牛客APP用户建议收藏后用电脑阅读,牛客APP的排版会出现很大的问题。

以下讨论的主流C++编译器,指MSVC、GCC、Clang这三种。

只用GCC,对其余编译器了解不多,有不对的地方可以指出。

关键字

typeid

适用范围:

  • 主流C++编译器
  • C++11

需要添加头文件<typeinfo>

#include <typeinfo>
#include <iostream>
int main() {
  std::cout << typeid(main).name() << std::endl;
  std::cout << typeid(main).hash_code() << std::endl;
}

以上代码输出

FivE
13234539132202047632

decltype

适用范围:

  • 主流C++编译器
  • C++11

decltype作为操作符,用于获取表达式的数据类型。C++11标准引入decltype,主要是为泛型编程而设计,以解决泛型编程中有些类型由模板参数决定而难以(甚至不可能)表示的问题。

#include <iostream>
#include <typeinfo>
#include <string>
int func(int x) {
  while (true);
}
float func(float x) {
  while (true);
}
int main() {
  decltype(func(0)) x = 0;
  std::cout << typeid(x).name() << std::endl;
  decltype(func(0.0f)) y = 0;
  std::cout << typeid(y).name() << std::endl;
  std::string str = "0";
  decltype(str) z = "0";
  std::cout << typeid(z).name() << std::endl;
}

以上代码输出

i
f
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

可以看出来decltype并不会真的去执行这个函数,而是在编译时就已经确定好了这个类型是什么。(因为C++是静态语言)

constexpr

使用范围:

  • 主流C++编译器
  • C++11

constexpr是C++11引入的关键字,用于编译时的常量与常量函数。

int get_five() {
  return 5;
}
int some_value[get_five() + 7]; // 创建包含12个整数的数组. C++03中非法,因为get_five() + 7不是常量表达式

C++11引入了关键字constexpr,允许编程者保证函数或对象的构造函数是编译时常量。上述代码可以改写为:

constexpr int get_five() {
  return 5;
}
int some_value[get_five() + 7]; // Create an array of 12 integers. Valid C++11

以上代码来源于维基百科

看上去好像没啥用,但下面这个就非常有用了。

众所周知,模板的参数需要编译时就要确定这个参数的值。如果要在模板中放入除了类以外的参数,就需要使用到constexpr。比如下面的这个类。

#include <iostream>
template <char const* const& name>
class A {
 public:
  A() {
    std::cout << name << std::endl;
  }
};

这个类是可以编译通过的,那么如何去实例化这个类呢?

#include <iostream>
#include <typeinfo>
template <char const* const& name>
class A {
 public:
  A() {
    std::cout << name << std::endl;
  }
};
char const* const name_a = "a";
constexpr char const* const name_b = "b";
constexpr char const* const name_c = "b";
int main() {
  A<name_a> a; // C++11编译错误 C++17会将name_a默认修饰为constexpr可以编译通过
  A<name_b> b; // C++11编译通过
  A<name_c> c;
  // 下面的两条语句都会输出0,因为这两个类不是同一个类,即使name_b和name_c的值看起来是一样的。
  std::cout << std::is_same_v< A<name_b>, A<name_c> > << std::endl;
  std::cout << (typeid(b) == typeid(c)) << std::endl;
}

上面的代码在C++11中会编译失败,在C++17中会输出以下内容:

a
b

可以看到constexpr在某些需要编译时是常量的场景很有用。

自动推导

适用范围:

  • 主流C++编译器
  • C++11

auto可以自动推导出函数类型。

#include <typeinfo>
#include <iostream>
int main() {
  auto a = "const char*";
  auto b = std::string("std::string");
  std::cout << typeid(a).name() << " " << typeid(b).name() << std::endl;
}

以上代码输出

PKc NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

宏定义

预定义的宏名

适用范围:

  • 主流C++编译器
  • C++98

__LINE__:在源代码中插入当前源代码行号。
__FILE__:在源文件中插入当前源文件名。
__DATE__:在源文件中插入当前的编译日期。
__TIME__:在源文件中插入当前编译时间。
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1。
__func__:所在函数的函数名。
__cplusplus:编译器版本标识。

#include <iostream>
#define debug(x) std::cerr << "(" << __LINE__ << ")" << x << std::endl;
void GuessMyName() {
  std::cout << __func__ << std::endl;
}
int main() {
  std::cout << __FILE__ << "|"
            << __LINE__ << "|"
            << __DATE__ << "|"
            << __TIME__ << "|"
            << __STDC__ << "|"
            << __cplusplus << std::endl;
  GuessMyName();
  debug(1);
}

编译命令:g++ main.cpp -std=c++2a
以上代码输出

main.cpp|8|Oct  6 2020|12:09:57|1|201402
GuessMyName
(14)1

变量名变成可输出的字符串

适用范围:

  • 主流C++编译器
  • C++98
#include <iostream>
#define debug(x) std::cerr << "(" << __LINE__ << ")" << #x << " = " << x << std::endl;
int main() {
  const int var = 0;
  debug(var);
}

以上代码输出

(5)var = 0

变量名连接

适用范围:

  • 主流C++编译器
  • C++98
#include <iostream>
#define async(sync_function_name) async_##sync_function_name##_function()
void dosomething() {
  std::cerr << "sync" << std::endl;
}
void async_dosomething_function() {
  std::cerr << "async" << std::endl;
}
int main() {
  async(dosomething); // 即async_dosomething_function();
}

以上代码输出

async

可变参数

适用范围:

  • 主流C++编译器
  • C++98

printf()和fprintf()这些输出函数的参数是可变的,在调试程序时,你可能希望定义自己的参数可变的输出函数,那么可变参数宏会是一个选择。

宏可以像函数一样带有可变参数。

#define debug(format, ...) fprintf(stdout, format, __VA_ARGS__)

其中的__VA_ARGS__...的内容。

GCC还可以对__VA_ARGS__进行重命名即:

#define debug(format, args...) fprintf(stdout, format, args)

如果有上面所说的宏定义,那么debug("%s\n", "Hello world!")则会输出Hello world!

有一点需要注意,上述的宏定义不能省略可变参数,尽管你可以传递一个空参数,这里有必要提到"##"连接符号的用法。

"##"的作用是对token进行连接,在上例中,format__VA_ARGS__args即是token,如果token为空,那么不进行连接,所以允许省略可变参数(__VA_ARGS__args),对上述变参宏做如下修改:

#define LOG(format, ...) fprintf(stdout, format, ##__VA_ARGS__)
#define LOG(format, args...) fprintf(stdout, format, ##args)

编译时添加宏定义

适用范围:

  • GCC(也许其他编译器也可以做到这样的功能)
  • C++98

编译时可以使用-D选项。

例如对于以下代码:

int main() {
#if DEF == 114514
#error wow you can really dance
#endif
}

正常的编译是不会报错的。
但如果编译命令是g++ main.cpp -DDEF=114514编译器会看到第3行代码就会报错了。
相当于在代码的最前面添加了一行#define DEF 114514
如果省略等于号及其后面的内容类似于g++ main.cpp -DDEF则相当于在最前面添加了一行#define DEF。(也相当于DEF是1)

可变模板参数

适用范围:

  • 主流C++编译器
  • C++11
void _debug() {}
template<typename T, typename ...U>
void _debug(const T& x, const U&... y) {
  std::cerr << x << (sizeof...(y) ? ", " : "\n"); // 如果是最后一个参数输出"\n",否则输出", "
  _debug(y...);
}

上面代码的第5行可以自动识别使用的是哪一个_debug函数。写上如上的定义之后再在main函数中写_debug(0, "0", 0.0)便会在控制台打印出0, 0, 0

这一部分可以玩得很溜,暂时就写这么多。

STL

大部分STL容器的预处理

适用范围:

  • 主流C++编译器
  • C++17

pair

#include <iostream>
#include <map>
int main() {
  std::pair<int, int> Pair = {1, 2};
  std::cerr << Pair.first << " " << Pair.second << std::endl;
}

以上代码输出

1 2

vector

#include <iostream>
#include <vector>
int main() {
  std::vector<int> vec = {1, 2, 4};
  for (std::size_t i = 0; i < vec.size(); i++) {
    std::cout << vec.at(i) << std::endl;
  }
}

以上代码输出

1
2
4

map

#include <iostream>
#include <map>
int main() {
  std::map<int, int> dict = {{0, 1}, {1, 2}, {114514, 1919810}};
  for (std::map<int, int>::iterator iter = dict.begin(); iter != dict.end(); iter++) {
    std::cout << iter->first << ": " << iter->second << std::endl;
  }
}

以上代码输出

0: 1
1: 2
114514: 1919810

还有其他的容器像queue、deque也可以使用(类似于vector)。

遍历

普通的

这个应该人尽皆知了吧。

适用范围:

  • 主流C++编译器
  • C++11
#include <iostream>
#include <vector>
int main() {
  std::vector<int> vec = std::vector<int> (3, 2);
  for (const int& item : vec) {
    std::cout << item << std::endl;
  }
}

以上代码输出

2
2
2

Structured Binding

适用范围:

  • 主流C++编译器
  • C++17
#include <iostream>
#include <map>
int main() {
  std::map<int, int> Map;
  Map.emplace(1, 0);
  Map.emplace(2, 1);
  Map.emplace(114514, 1919810);
  for (const auto& [key, value] : Map) {
    std::cout << key << ": " << value << std::endl;
  }
}

以上代码输出

1: 0
2: 1
114514: 1919810

当然这个Structured Binding不止可以用在遍历,例如:

#include <typeinfo>
#include <iostream>
#include <tuple>
int main() {
  std::tuple<int, char, int> tuple = {0, '_', 2};
  const auto& [a, b, c] = tuple;
  std::cout << typeid(a).name() << ' ' << a << '\n';
  std::cout << typeid(b).name() << ' ' << b << '\n';
  std::cout << typeid(c).name() << ' ' << c << '\n';
}

以上代码输出

i 0
c _
i 2

输出带颜色

适用范围:

  • GCC(Windows/Linux)
  • C++98

理论上Mac是和Linux一样的,但我没有这样的机器就不写了。

// color.h
#pragma once
#ifdef _WIN32
#include <iostream>
#include <windows.h>
inline std::ostream& purple(std::ostream &s) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_INTENSITY);
  return s;
}
inline std::ostream& blue(std::ostream &s) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
  return s;
}
inline std::ostream& red(std::ostream &s) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, FOREGROUND_RED | FOREGROUND_INTENSITY);
  return s;
}
inline std::ostream& green(std::ostream &s) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
  return s;
}
inline std::ostream& yellow(std::ostream &s) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY);
  return s;
}
inline std::ostream& white(std::ostream &s) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
  return s;
}
struct color {
  color(WORD attribute): m_color(attribute) {};
  WORD m_color;
};
template <class _Elem, class _Traits>
std::basic_ostream<_Elem, _Traits>&
operator << (std::basic_ostream<_Elem, _Traits>& i, color& c) {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTextAttribute(hStdout, c.m_color);
  return i;
}
#elif defined(linux)
#include <string>
const std::string black = "\033[30m";
const std::string red = "\033[31m";
const std::string green = "\033[32m";
const std::string yellow = "\033[33m";
const std::string blue = "\033[34m";
const std::string purple = "\033[35m";
const std::string azure = "\033[36m";
const std::string white = "\033[37m";
#endif

用法:先#include "color.h",然后如果想输出蓝色就std::cout << blue << sth << std::endl;sth是你想要输出的东西。

这个代码中,Windows下支持的颜色有purple、blue、red、green、yellow和white(其实还有其他颜色,只是我尝试之后发现这些颜色差别不明显,大家需要的可以自己调色)。Linux下支持的颜色有black、red、green、yellow、blue、purple、azure、white(有些颜色差别不是特别明显,和Windows类似)。

builtin函数

适用范围:

  • GCC
  • C++98

GCC提供了一系列的builtin函数,可以实现一些简单快捷的功能来方便程序编写,另外,很多builtin函数可用来优化编译结果。这些函数以"__builtin_"作为函数名前缀。

__builtin_popcount(x):x中1的个数。
__builtin_ffs(x):返回x中最后一个为1的位是从后向前的第几位。
__builtin_ctz(x):x末尾0的个数。x=0时结果未定义。
__builtin_clz(x):x前导0的个数。x=0时结果未定义。
__builtin_parity(x):x中1的个数的奇偶性。

还有许多的builtin函数可供使用,不用include任何头文件。但比较有用的就是上面的这些,运行的速度很快。

raw string

适用范围:

  • 主流C++编译器
  • C++11

C++11新增的另一种类型是原始字符串raw。原始字符串中的字符表示的就是自己。例如"\n"表示的不是换行符,而是两个字符:斜杠和n。因此在屏幕上显示时,将显示的是\n这两个字符。另外,在字符串中使用"时候,不需要使用转义,即",而是直接使用。因此原始字符串就不能用""来限定开头和末尾,而是用"(和)"用作定界符,并使用前缀R来标识原始字符串。

我们可以使用R"(C:\Windows\Fonts)"来表示字符串C:\Windows\Fonts

原始字符串还可以自定义定界符,默认定界符是"(和)"。因此若想要在字符串中允许)",则必须自定义定界符。例如R"+*("(I'm raw string.)")+*"表示的字符串是"(I'm raw string.)"

想要输出换行符直接在代码里面换行即可。

#include <string>
#include <fstream>
int main() {
  std::ofstream output("output");
  std::string str = R"+*("(I'm raw string.)"
  new line)+*";
  output << str;
  output.close();
}

以上代码输出到文件是这样的

"(I'm raw string.)"
  new line

带有初始化程序的if

适用范围:

  • 主流C++编译器
  • C++17

if(init; condition)的意思是先执行init语句,然后判断condition语句是否为true。这样做可以使某些变量的作用域缩小而不用加额外的大括号修饰作用域。

#include <iostream>
#include <map>
int main() {
  std::map<int, int> dict = {{0, 0}, {2, 1}};
  for (int i = 0; i <= 2; i++) {
    if (std::map<int, int>::const_iterator iter = dict.find(i); iter != dict.end()) {
      std::cout << iter->first << " " << iter->second << std::endl;
    } else {
      std::cout << "ub: " << iter->first << std::endl;
    }
    // std::cout << iter->first << " " << iter->second << std::endl; // iter在作用域外
  }
}

以上代码输出

0 0
ub: 2
2 1

User-Defined Literals

适用范围:

  • 主流C++编译器
  • C++11

代码来自bakezq评论区,感谢这位同学。

#include <iostream>
struct Weight {
  long double g = 0;
  void show() {
    std::cout << "Weight: " << g << " g" << std::endl;
  }
};
Weight operator "" _kg(long double num) {
  return {num * 1000};
}
int main() {
  auto w = 25.2_kg;
  w.show();
}

以上代码输出

Weight: 25200 g

一个好用的debug宏

适用范围:

  • 主流C++编译器
  • C++11

可以使用可变参数宏、可变模板参数、输出带颜色这三个内容写一个好用的debug宏。

#define LOG(...)                                                     \
do {                                                                 \
  std::cerr << red << "(" << __LINE__ << ")" << ""#__VA_ARGS__" = "; \
  _debug(__VA_ARGS__);                                               \
  std::cerr << white;                                                \
} while (false)

在main函数中有以下代码LOG(0, "1", 2.0);就会在控制台输出红色的字:

(23)0, "1", 2.0 = 0, 1, 2

小括号中间的内容是所在行号,之后接着变量名,等于号后面是你要输出的值。

你的程序没办法使用控制台?输出到流里即可。(_debug函数也要改造一下)

#define LOG(file_stream, ...)                                   \
do {                                                            \
  file_stream << "(" << __LINE__ << ")" << ""#__VA_ARGS__" = "; \
  _debug(file_stream, __VA_ARGS__);                             \
} while (false)

需要编译时开关控制?

#ifdef DEBUG
#define LOG(file_stream, ...)                                        \
do {                                                                 \
  std::cerr << red << "(" << __LINE__ << ")" << ""#__VA_ARGS__" = "; \
  _debug(__VA_ARGS__);                                               \
  std::cerr << white;                                                \
} while (false)
#else
#define LOG(file_stream, ...) do { } while (false)
#endif

如果前面没有#define DEBUG,那么就会跳过这条语句。否则进行正常的打日志操作。

需要动态开关控制?

#define LOG(...)                                                       \
do {                                                                   \
  if (debug_mode) {                                                    \
    std::cerr << red << "(" << __LINE__ << ")" << ""#__VA_ARGS__" = "; \
    _debug(__VA_ARGS__);                                               \
    std::cerr << white;                                                \
  }                                                                    \
} while (false)

为什么要用do-while语句?
因为这样可以使得LOG宏和普通的函数用法一模一样(强制加分号,宏内部不会影响到其他语句)。

你还可以配合上__FILE____func__等预定义宏进行你想要的打日志方式。

全部评论

(11) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

近期精华帖

热门推荐