在实际项目中,偶尔会出现运行情况与预想情况不一致的现象,这可能是忽略了编译器对你代码的改动与优化(当然,你写错代码的可能性更高)。不管怎么样,我们还是来看看编译器到底偷偷给你整了什么好活吧。
1. 短函数自动转为inline内联函数
当函数较短且不存在for/while等循环条件时,编译器会默认将其转换为内联函数展开,提高对应的执行效率,减少调用函数的堆栈消耗。
2. sizeof操作符
直接在编译阶段根据参数类型推断大小,如果是函数则不会调用(如sizeof(func(a));),根据函数返回值类型推断结果就好了。即在目标程序中,这里已经被替换为一个常数了。这个算是常见笔试题的考点了,如果对sizeof有正确认知的话一般不会出错。
3. 右值优化
c++11编译会优化函数返回值,采用右值传参直接转移内存数据。该优化可以通过编译参数-fno-elide-constructors关闭,默认开启。此项开启后,就不会按照原先预想的调用拷贝构造函数等操作(虽然实际并无害处,效率更高了),而是转让内存所有权。
4. 无效代码优化
未被使用的变量或者函数不会编译进目标程序中。如函数仅声明未实现,未被调用的情况下是可以编译通过的。此处的“未被调用”一般指编译器通过main函数或者全局变量/静态变量等进行推导,判断其是否存在可能被调用的代码执行路径。
但在实际项目中,可能会存在一些原因导致函数被误判为“未被调用”,其本身又未被实现,这就导致程序编译能通过,但在实际运行过程中找不到函数实现最终崩溃。
5. ++操作
这里涉及一个比较常见的笔试题,"a=++a+a++"。首先声明一点,在C语言中这种单条语句多次修改同一变量值的操作视为未定义行为(undefined behavior),结果往往由编译器决定,因此不推荐实际项目中使用这种写法,可读性也不高。
不过在一般情况下,++a和a++可以等同于以下函数:
// ++a
int incrby_bf(int &a) {
a = a + 1;
return a;
}
// a++
int incrby_af(int &a) {
a = a + 1;
return a - 1;
}
即a=++a+a++;可以替换为a=incrby_bf(a)+incrby_af(a);。此处也有一个需要注意的点:incrby_bf和incrby_af函数的执行顺序由编译器决定,并不是固定从左到右计算的。因此如果两个函数存在前后执行的依赖关系,建议把语句拆成两部分执行。
更详细的内容可以查看这位大佬的博客:用异或来交换两个变量是错误的,其中还采用了实际的测试数据进行考究。
6. 构造函数推断
在c++中,编译中会根据变量定义时赋值的参数自动判断对应的构造函数,而不是先走默认构造函数再进行赋值,增加了额外的赋值操作。在以下代码中,变量tmp(显式调用)与tmp2(隐式调用)的定义操作是等同的。
class TestClass {
public:
TestClass() {
puts("default constructor");
}
TestClass(const char *str) {
puts("chars constructor");
}
};
int main() {
TestClass tmp("hello");
TestClass tmp2 = "hello";
return 0;
}
但如果参数的推断类型存在中间转换,目前一些编译器暂时无法识别,如以下代码:
class TestClass {
public:
TestClass() {
puts("default constructor");
}
TestClass(const std::string &str) {
puts("string constructor");
}
};
int main() {
TestClass tmp("hello");
TestClass tmp2 = "hello";
return 0;
}
显式调用的tmp可以直接调用string做参数的构造函数,而隐式调用的tmp2则会出现编译报错(无法在TestClass类中找到const char*类型的构造函数)。从该处可以得知,编译器的构造函数推断仅限于完全一致的参数类型,存在中间转换的参数是不支持的。
c++编译器对代码做的改动其实非常多,毕竟编译器开发人员也不是吃干饭的,总会根据实际情况进行优化和调整。在本篇博客中只是提到了少数的几点编译器处理操作,之后还会继续更新补充呦。
奇怪的知识又增加了!