问题

C/C++编程有哪些没什么用,但让人不明觉厉又不至于太奇怪的写法?

回答
在 C/C++ 编程中,确实存在一些写法,它们本身可能不是最优的解决方案,甚至在大多数情况下是多余的,但却能让有一定经验的开发者眼前一亮,感到“不明觉厉”。这些写法往往巧妙地利用了语言的特性、预处理指令、或者是一些不常用的语法糖。同时,它们又不会像一些“炫技”般的操作那样显得过于怪异而难以理解。

下面我将从几个方面详细介绍这类“不明觉厉”的 C/C++ 写法:



一、 利用预处理指令和宏的“邪恶”艺术

预处理指令是 C/C++ 的一个强大但常常被低估的部分。它们在编译前执行,提供了强大的文本替换和条件编译能力。巧妙地运用它们,可以创造出一些令人惊叹的写法。

1. 带参数的宏模拟函数调用(但不是为了性能)

我们知道 `inline` 函数可以避免函数调用的开销,但有时候我们会用宏来达到类似的效果,但重点不在于性能,而在于简洁的语法或者更底层的控制。

例子:简单的字符串拼接宏

```c++
include

// 普通函数实现
std::string concatenate_strings(const std::string& s1, const std::string& s2) {
return s1 + s2;
}

// 宏实现
define CONCAT(s1, s2) s1 s2 // 是 token 粘贴运算符

int main() {
std::string hello = "Hello";
std::string world = "World";

// 令人费解的部分来了:
std::cout << CONCAT(hello, world) << std::endl; // 输出 HelloWorld

// 这里的 CONCAT(hello, world) 会被预处理器直接替换成 hello world
// 然后 运算符会将 hello 和 world 两个 token 粘合在一起,
// 形成一个新的 token "helloworld"。
// 如果你写成 CONCAT(hello, "_", world),它会变成 hello _ world。
// 这种写法通常不会用于实际的字符串拼接,因为它需要变量名是连续的。
// 更常见的用途是在定义变量名时。
return 0;
}
```

为什么不明觉厉?
`` 令牌粘贴运算符本身就不是日常使用的。
它的应用场景非常有限,这里仅仅是为了演示 `` 的能力,实际上 `hello + world` 更直观。
它让看起来像函数调用,但实际上是纯粹的文本替换。

例子:动态生成变量名(非常少见,且不推荐用于生产环境)

```c++
include
include

define DECLARE_VAR(name) int var_ name = 10; // 使用 生成变量名

int main() {
DECLARE_VAR(count); // 预处理后变成: int var_count = 10;
DECLARE_VAR(index); // 预处理后变成: int var_index = 10;

std::cout << "var_count: " << var_count << std::endl;
std::cout << "var_index: " << var_index << std::endl;
return 0;
}
```

为什么不明觉厉?
直接在运行时(或者说编译时)动态生成变量名,这在高级语言中很常见,但在 C++ 中通过宏实现,显得颇为“魔幻”。
这是一种非常规的变量声明方式,很容易导致代码混乱和难以维护。

2. 利用 `` 字符串化运算符

`` 运算符可以将宏的参数转换为字符串字面量。

例子:打印变量名和值

```c++
include

define PRINT_VAR(var) std::cout << var << ": " << var << std::endl;

int main() {
int age = 30;
std::string name = "Alice";

PRINT_VAR(age); // 预处理后变成: std::cout << "age" << ": " << age << std::endl;
PRINT_VAR(name); // 预处理后变成: std::cout << "name" << ": " << name << std::endl;

return 0;
}
```

为什么不明觉厉?
能够自动获取变量的名称并将其打印出来,这在调试时非常有用,但用宏实现,显得很巧妙。
`` 运算符的使用本身就带着一丝神秘感。

例子:更复杂的日志宏

```c++
include
include
include
include
include

// 获取当前时间戳的函数(简化版)
std::string get_timestamp() {
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf;
// 使用 localtime_r 以保证线程安全
ifdef _WIN32
localtime_s(&tm_buf, &now_c);
else
localtime_r(&now_c, &tm_buf);
endif
std::stringstream ss;
ss << std::put_time(&tm_buf, "%Y%m%d %H:%M:%S");
return ss.str();
}

define LOG(level, msg) do {
std::cout << "[" << level << "] [" << get_timestamp() << "] " << msg << std::endl;
} while(0)

int main() {
LOG(INFO, "System started successfully.");
LOG(ERROR, "Failed to connect to database.");
// 即使 LOG 内部有多个语句,dowhile(0) 也能让它像一个单语句一样使用
// 在 if/else 中避免意外行为。
return 0;
}
```

为什么不明觉厉?
结合了 `` 字符串化和 `dowhile(0)` 的技巧,创建一个功能强大且语法安全的日志宏。
日志信息包含了级别、时间戳和消息内容,用宏实现得非常简洁。
`dowhile(0)` 是一个常见的宏技巧,用于将多条语句封装成一个“语句”,以避免在 `if` 语句后面缺少大括号时出现问题。

3. 条件编译的妙用(非平台相关)

条件编译 (`ifdef`, `ifndef`, `if`, `else`, `elif`, `endif`) 通常用于处理不同平台或配置。但有时候可以用来“隐藏”一些特殊功能,或者创造出一些在特定条件下才出现的行为。

例子:一个“隐形”的调试版本

```c++
include

ifndef DEBUG_MODE
define DEBUG_MODE 0 // 默认非调试模式
endif

if DEBUG_MODE
define DEBUG_LOG(msg) std::cout << "[DEBUG] " << msg << std::endl;
else
define DEBUG_LOG(msg) // 在非调试模式下,宏什么都不做
endif

int main() {
DEBUG_LOG("Application is running..."); // 在 DEBUG_MODE=0 时,这行代码直接被移除

// 模拟一些正常操作
int x = 10;
int y = 20;
DEBUG_LOG("Intermediate value: " << (x + y)); // 同样被移除

std::cout << "Normal operation completed." << std::endl;
return 0;
}
```

为什么不明觉厉?
看起来就像是编译时就移除了某些代码,但代码本身是存在的。
通过一个宏就能控制一大块代码的生死,这种“动态”的静态行为很有趣。
这是一种比简单的注释掉代码更优雅的方式,因为它会从最终的可执行文件中完全移除,不产生任何性能开销。



二、 利用 C++ 的核心特性和语法糖

除了预处理器,C++ 本身的一些特性也可以被用来创造出令人眼前一亮的写法。

1. 模板元编程的艺术(非计算密集型)

模板元编程(Template Metaprogramming, TMP)是在编译时进行计算的一种技术。虽然常用于复杂的算法实现,但有时候也可以用来做一些简单的、但看起来很神奇的事情。

例子:编译时常量计算

```c++
include

template
struct Factorial {
static const int value = N Factorial::value;
};

template <>
struct Factorial<0> {
static const int value = 1;
};

int main() {
const int fact5 = Factorial<5>::value; // 编译时计算 5!
std::cout << "Factorial of 5 is: " << fact5 << std::endl; // 输出 120

// 这里的 fact5 的值在编译时就已经确定为 120 了,
// 在运行时只是一个常量引用。
return 0;
}
```

为什么不明觉厉?
在编译时计算出结果,而不是在运行时。这在某些场景下(例如生成查找表)可以优化运行时性能。
通过模板的递归实例化来完成计算,这是一种非常“数学化”的编程方式。
虽然 TMP 通常被认为是复杂的,但这种简单的阶乘计算展示了它的基础力量。

2. 利用 Lambda 表达式的“闭包”特性

Lambda 表达式在 C++11 之后变得非常流行,它们可以捕获外部变量,形成闭包。有时候,我们会用 lambda 来封装一些简单的逻辑,或者作为回调函数。

例子:使用 lambda 创建一次性使用的函数对象

```c++
include
include
include

int main() {
std::vector nums = {1, 2, 3, 4, 5};

int multiplier = 2;
int offset = 10;

// 使用 lambda 在 std::for_each 中进行复杂操作,并捕获变量
std::for_each(nums.begin(), nums.end(),
[&](int n) { // [&] 捕获所有外部变量,包括 multiplier 和 offset
std::cout << n << " " << multiplier << " + " << offset
<< " = " << (n multiplier + offset) << std::endl;
});

// 也可以捕获特定变量
int limit = 3;
auto is_greater_than_limit = [&](int val) {
return val > limit;
};

// 使用这个 lambda 作为谓词
auto it = std::find_if(nums.begin(), nums.end(), is_greater_than_limit);
if (it != nums.end()) {
std::cout << "First number greater than " << limit << " is: " << it << std::endl;
}

return 0;
}
```

为什么不明觉厉?
Lambda 表达式的简洁语法本身就很有吸引力。
能够方便地将逻辑和所需数据打包在一起,作为参数传递。
尤其是在 `std::algorithm` 系列函数中使用 lambda,可以写出非常简洁和富有表达力的代码。

3. 重载操作符的非传统用法

操作符重载通常用于类,使其行为更像内置类型(如 `+` 用于向量加法)。但有时候,我们可以用它来模拟一些特殊的语法,或者让代码更“简洁”(或者说更难以理解)。

例子:使用重载的 `<<` 运算符实现更易读的日志或输出

```c++
include
include
include

// 一个简单的日志类
class Logger {
public:
// 重载 << 运算符,使其可以接受不同类型的参数
Logger& operator<<(const std::string& msg) {
std::cout << msg;
return this;
}
Logger& operator<<(int value) {
std::cout << value;
return this;
}
Logger& operator<<(double value) {
std::cout << value;
return this;
}
// ... 可以继续重载其他类型

// 一个特殊的标记,表示行尾,并换行
Logger& endl() {
std::cout << std::endl;
return this;
}
};

// 全局的 Logger 实例
Logger log;

int main() {
int count = 5;
double pi = 3.14159;

// 使用重载的 << 运算符,像数据库查询一样输出日志
log << "Processing item " << count << " with value " << pi << log.endl();

return 0;
}
```

为什么不明觉厉?
将传统的 `std::cout << ... << std::endl;` 写法,变成了一个更像自定义 DSL(领域特定语言)的风格。
链式调用 `<<` 运算符,以及自定义的 `endl()` 方法,都给人一种“这不是普通的输出”的感觉。
这种方式可以提高代码的可读性,但如果重载得太多或太随意,也会增加理解难度。

4. 利用 `std::initializer_list` 的灵活性

`std::initializer_list` 是 C++11 引入的,允许我们用 `{}` 初始化容器或对象。但它也可以被用来作为函数参数,提供一种非常简洁的调用方式。

例子:一个更灵活的 `print_all` 函数

```c++
include
include
include

void print_all(std::initializer_list items) {
std::cout << "Printing items: ";
for (const auto& item : items) {
std::cout << item << " ";
}
std::cout << std::endl;
}

void print_all_numbers(std::initializer_list numbers) {
std::cout << "Sum of numbers: ";
int sum = 0;
for (int num : numbers) {
sum += num;
}
std::cout << sum << std::endl;
}

int main() {
print_all({"Hello", "C++", "World"}); // 直接用花括号传递列表
print_all_numbers({1, 2, 3, 4, 5}); // 简洁的数字列表传递

// 也可以使用 std::initializer_list temp = {10, 20, 30}; print_all_numbers(temp);
// 但直接传递更简洁

return 0;
}
```

为什么不明觉厉?
函数的调用方式非常直观和简洁,就像是直接传递一个列表一样。
它避免了创建临时数组或向量的麻烦,尤其是在只需要临时使用一组值时。
`std::initializer_list` 本身也是 C++11 的一个新特性,很多人可能不太熟悉它的实际应用。



三、 结合的技巧和非常规的思路

有些写法是将上述的技巧结合起来,或者使用一些不太直观但有效的 C++ 特性。

1. 使用 `auto` 和返回类型推导的“魔法”

`auto` 的广泛使用在一定程度上让代码更简洁,但如果结合返回类型推导,可以创造出一些令人费解但又充满魅力的代码。

例子:使用 `auto` 返回一个 lambda,捕获了周围的变量

```c++
include
include // std::function

auto create_adder(int base) {
// 返回一个 lambda 函数,该 lambda 捕获了 base
return [base](int add_val) {
return base + add_val;
};
}

int main() {
auto add_5 = create_adder(5); // add_5 现在是一个可以调用并返回 (5 + val) 的 lambda

std::cout << "5 + 10 = " << add_5(10) << std::endl; // 输出 15

// 这里的 create_adder 返回的类型是编译器才能确定的具体 lambda 类型,
// 但通过 auto,我们能够方便地接收它。
// 如果不使用 auto,可能需要 std::function 来声明返回类型,
// 但那会引入额外的开销和复杂性。

return 0;
}
```

为什么不明觉厉?
函数返回的不是一个已知的命名类型,而是一个匿名的 lambda 类型。
通过 `auto` 接收,使得函数工厂模式变得异常简洁。
隐藏了返回类型的具体实现细节,但保证了功能的正确性。

2. 利用空指针(`nullptr`)的成员访问 (非常危险,极不推荐)

虽然这是非常危险且不应该在生产代码中使用的技巧,但它确实能引起“不明觉厉”的效果。它利用了 C++ 允许在空指针上访问非虚拟成员函数的特点。

例子:调用一个非虚拟成员函数而不创建对象

```c++
include

class MyClass {
public:
void greet() {
std::cout << "Hello from MyClass!" << std::endl;
}
static void static_greet() {
std::cout << "Hello from static method!" << std::endl;
}
};

int main() {
// 正常调用
MyClass obj;
obj.greet();
MyClass::static_greet();

// “不明觉厉”的部分:
// 在 nullptr 上调用非虚拟成员函数
// (非虚拟成员函数不涉及 vptr 和虚函数表,它们是在编译时绑定的)
// 这本质上是在编译时将函数指针绑定到代码段,运行时直接执行。
static_cast( &MyClass::greet )(); // C++11 之后,指向成员函数的指针需要特殊处理
// 或者更直接一点(依赖编译器特性,可能不完全标准):
// ((MyClass)nullptr)>greet(); // 这种写法在一些编译器中有效,但非常危险且不便携

// 对静态成员函数,这种写法没意义,直接用类名访问即可
// MyClass::static_greet();

return 0;
}
```

为什么不明觉厉?
在 `nullptr`(空指针)上调用成员函数,这与直觉完全相悖。
它表明了 C++ 对非虚拟成员函数的底层处理方式:本质上是函数指针。
再次强调:这个技巧极度危险,会带来未定义行为,并且严重影响代码的可读性和可维护性,切勿在实际项目中使用。 它的“不明觉厉”仅在于展示了语言的底层运作原理。



总结

这些“不明觉厉”的 C/C++ 写法,往往具备以下特点:

巧妙利用语言特性:例如预处理指令的强大能力、模板的编译时计算、Lambda 的闭包特性、操作符重载的自定义行为等。
追求简洁或特定表达力:虽然可能不是最高效的,但能以一种非常简洁或独特的方式实现某个功能或达到某种表达效果。
有一定的门槛:需要一定的 C++ 知识储备才能理解其背后的原理。
不至于太奇怪:它们通常不会导致编译错误,也不会明显违反代码规范(除了上面那个极度危险的例子)。读者在看到后,经过思考或查阅资料,是能够理解的。

这些写法在某些特定场景下可以提高代码的趣味性、表达力或简洁性,但对于大多数日常开发而言,清晰、可读、可维护的代码仍然是第一原则。了解这些技巧,可以在遇到特定问题时提供一些创新的思路,但盲目模仿则可能适得其反。

网友意见

user avatar

Placement New

普通的new 操作符,假设有个类叫 class Foo

       Foo *p = new Foo();     

这行代码实际上干了两件事,首先分配一块内存,然后调用Foo的构造函数

但是有时候,我们已经有一块内存,不需要再分配,只需要调用构造函数,但是C++语法是不允许直接调用构造函数的

       p->Foo::Foo(); // 编译错误error: cannot call constructor ‘Foo::Foo’ directly     

那么想在已经存在的内存上构建对象,就要用到 Placement New,实际上就是帮你调用下构造函数

       void *p = malloc(sizeof(Foo)); Foo *bar = new (p) Foo();     

这个特性不能说绝对没用,但是极少会用到。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有