-
C 风格的枚举还存在 “成员名称全局有效” 和 “可以隐式转换为整型” 的缺陷
但因为 C++ 提供了 enum class 风格的枚举类型,解决了这两个问题,因此这里不再额外讨论。
(一)宏
宏这个东西,完全就是针对编译器友好的,编译器非常方便地在宏的指导下,替换源代码中的内容。但这个玩意对程序员(尤其是阅读代码的人)来说是极其不友好的,由于是预处理指令,因此任何的静态检测均无法生效。一个经典的例子就是:
#define MUL(x, y) x * y
void Demo() {
int a = MUL(1 + 2, 3 + 4); // 11
}
因为宏就是简单粗暴地替换而已,并没有任何逻辑判断在里面。
宏因为它很 “好用”,所以非常容易被滥用,下面列举了一些宏滥用的情况供参考:
-
用宏来定义类成员
#define DEFAULT_MEM \
public: \
int GetX() {return x_;} \
private: \
int x_;
class Test {
DEFAULT_MEM;
public:
void method();
};
这种用法相当于屏蔽了内部实现,对阅读者非常不友好,与此同时加不加 DEFAULT_MEM 是一种软约束,实际开发时极容易出错。
再比如这种的:
#define SINGLE_INST(class_name) \
public: \
static class_name &GetInstance() { \
static class_name instance; \
return instance; \
} \
class_name(const class_name&) = delete; \
class_name &operator =(const class_name &) = delete; \
private: \
class_name();
class Test {
SINGLE_INST(Test)
};
这位同学,我理解你是想封装一下单例的实现,但咱是不是可以考虑一下更好的方式?(比如用模板)
-
用宏来屏蔽参数
#define strcpy_s(dst, dst_buf_size, src) strcpy(dst, src)
这位同学,咱要是真想写一个安全版本的函数,咱就好好去判断 dst_buf_size 如何?
-
用宏来拼接函数处理
#define COPY_IF_EXSITS(dst, src, field) \
do { \
if (src.has_##field()) { \
dst.set_##field(dst.field()); \
} \
} while (false)
void Demo() {
Pb1 pb1;
Pb2 pb2;
COPY_IF_EXSITS(pb2, pb1, f1);
COPY_IF_EXSITS(pb2, pb1, f2);
}
这种用宏来做函数名的拼接看似方便,但最容易出的问题就是类型不一致,加入 pb1 和 pb2 中虽然都有 f1 这个字段,但类型不一样,那么这样用就可能造成类型转换。试想 pb1.f1 是 uint64_t 类型,而 pb2.f1 是 uint32_t 类型,这样做是不是有可能造成数据的截断呢?
-
用宏来改变语法风格
#define IF(con) if (con) {
#define END_IF }
#define ELIF(con) } else if (con) {
#define ELSE } else {
void Demo() {
int a;
IF(a > 0)
Process1();
ELIF(a < -3)
Process2();
ELSE
Process3();
}
这位同学你到底是写 python 写惯了不适应 C 语法呢,还是说你为了让代码扫描工具扫不出来你的圈复杂度才出此下策的呢~~
(二)共合体
共合体的所有成员共用内存空间,也就是说它们的首地址相同。在很多人眼中,共合体仅仅在 “多选一” 的场景下才会使用,例如:
union QueryKey {
int id;
char name[16];
};
int Query(const QueryKey &key);
上例中用于查找某个数据的 key,可以通过 id 查找,也可以通过 name,但只能二选一。
这种场景确实可以使用共合体来节省空间,但缺点在于,共合体的本质就是同一个数据的不同解类型,换句话说,程序是不知道当前的数据是什么类型的,共合体的成员访问完全可以用更换解指针类型的方式来处理