目录
宏函数
预处理器#include
内存泄漏
内存对齐
堆与栈
Malloc 和 New
Inline
宏函数
宏函数,宏函数,实际上就是让宏像函数一样被使用。宏函数以函数形式的方式进行入参,但是返回结果是通过表达式求值得到。话说的抽象,我们来实际分析一个。
#define squared(num) ( ( num ) * ( num ) ) int main(){return squared(5); }
可以看到当我们预处理文件结束后,程序自动被替换为:
int main(){return 25; }
也就是说,我们的宏函数的运行结果将会在编译期间就得到了结果,将处理的结果直接cv到调用处,嗨!这个是一个麻烦的事情。
考虑一个经典的问题:
#define wrong_sqr(X) X * X int main() {return wrong_sqr(3 + 2) }
调用方预期的是——调用的是(3 + 2)^2
,但是却得到了11,这是为何呢?回到我上面加粗的那句话,直接替换到调用处,由于我们没有对X添加括号表达优先级,导致产生的奇奇怪怪的bug
int main() {return 3 + 2*3 + 2; }
很好,令人烦闷,所以我不得不这样做:
#define sus_sqr(X) (X) * (X) int main() {return sus_sqr(3 + 2) }
看起来没问题了吗?这个例子看起来没问题了,但是我们仍然推介在最外层再次添加一个()
,保证我们的计算单元是独立的
#define squared(num) ( ( num ) * ( num ) ) int main(){return squared(5); }
也就是我们最开始的这个写法。
上面这个例子已经让我们看到宏函数之用处。这不禁想让人问:他跟函数调用有什么区别呢?
首先,在没有展开优化的情况下,宏函数毫无疑问的相对函数调用开销小。思考一下,函数调用需要首先将参数mov进入寄存器,然后jmp到对应的函数入口,做参数入栈准备,计算结束后又弹栈jmp返回地址。对于宏函数,则是把事情放到编译期间,做全局的直接替换得到。这个事情有点模板元编程的味道了,当然这就跟我们的主题相去甚远了。
但是,宏函数毛病很大,如你所见,为了保证优先级,我们需要思考添加很多次()
保证不会出现优先级计算乱套,如果我们的参数有很多,那该怎么办呢?因此,在今天编译器优化愈发良好的今天,不推介使用宏函数来提升性能。
对于最新版本的编译器,没必要做任何更改,编译器自动裁决判定函数是否可以直接内联减少开销。(开优化,如果不开的话对于GCC考虑使用force-inline强制内联)
老旧的办法是这样的
inline double MAKE_SQUARE(x) {return x * x}
添加尚未变化含义的inline来指示编译器采取内联行动。
对于C++用户,考虑使用C++11以上的constexpr来采取常量表达式求解,从而获取跟宏函数一样卓越的性能,同时降低心智开销。
static constexpr SQUARE(X) {return X * X;}
预处理器#include
预处理器是预处理阶段下指挥编译器干活的几个指令,不同编译器对大大小小的预处理器可能略有不同,笔者比较熟知的就是内存对齐的开始和结束中,msvc和gcc的就不一样。具体的可以参考撰写《高效C/C++调试》的大佬的详细阐述。但是,常见的#define, #include
等等,行为完全按照标准进行。
#include做的事情实在是简单:直接将文件cv到#include的地方。如果看官想要求证,笔者推介您直接尝试
#include <stdio.h> int main(){ }
就这样放着,然后使用编译器只做预处理,你就会高兴的发现自己的代码多出了一大堆,仔细一瞧就是文件咔的一下贴在了文件的第一行——哈哈,这下也成写几千行代码的人了,可惜是CV出来的。
常见的#include有两种格式:
#include <stdio.h> // I #include "CCVector.h" // II
很好,区别是什么呢,编译器搜索路径的优先级不同。同志们都知道:编译器对于<>
包含的文件,优先按照如下路径扫描:
-
在编译器设置的include路径内搜索
-
在全局的系统路径下查找
-
最后,不舍的看一眼自己的工程目录看看(不递归查找)没有就给你抛错误:No Such File Or Directary
也就是说,我们认为,#include<>
用在使用了标准库和第三方库的时候,使用比较高效,编译器将会优先在外部寻找,提升查找效率
那#include""
如何呢?很简单,那就是优先看看自己家的工程目录,也就是说,对于隶属于项目自身的模块查找最高效!
内存泄漏
HOLY SHIT!看到这个就会不自觉的头大,这个问题在没有自动构造与析构的C语言下简直就是一场灾难(当然是标准通用的,GCC存在辅助析构构造的mark),任何编写或者使用过大型库的同志们不可避免的出现过这样的代码范式:
... ptr->do_init(); ... ptr->erase_self();
这中间就会申请释放资源,如果我们忘记,那就会出现一些在堆上申请的资源没有被释放,咋着?内存泄漏了,程序没办法追踪这些撒把的内存了
最快的内存泄漏方式是这样的:
int main() {{void* block = malloc(sizeof(byte));} }
你会发现在{}
外面完全没办法拿到地址了,block的内容已经被销毁覆盖了。
所以,请务必保证自己创建的每一个对象都有始有终!
内存对齐
这个玩意有趣,内存对齐实际上最广泛的用在通信协议设计上,我们都知道,一个结构体内部各成员都是按照所有成员的最大字节数对齐的,举个例子
struct PlainBuffer{char data1;int data2;double data3; }
-
第一个成员在与结构体变量偏移量为0的地址处。(即结构体的首地址处,即对齐到0处)
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
-
结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
-
如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
不用猜,就是16个字节大小。
data1对齐到0地址字节处,而data2则是对齐到后面的4的位置,data3对齐到7的位置,总共占据16个字节,没毛病!这样做的原因是提升取数的效率,是一个经典的空间换时间的操作。
另外,#pragma pack
可以调整结构体对齐方式,请参考这位博主写的:结构体内存对齐(如何计算结构体的大小)_按照结构体在内存中的对齐规则,下列结构体类型变量占用内存的大小为( ) struct te-CSDN博客
堆与栈
我留意到一些博主很贴心的阐述了堆与栈在计算机操作系统层次和数据结构层次的区别。在数据结构上我们谈到堆与栈还是说的是大小根堆(树的变种)和FIFO栈。这里我们重点阐述的是进程运行期间的内容。
嘿!这里的栈的工作方式跟FIFO栈的一致,都是FIFO的结构,由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等。
堆就是存放那些希望超越局部变量的,希望声明周期可以动态的调整了的数据的地方,由我们手动分配和释放, 若开发人员不释放,程序结束时由操作系统回收,分配方式类似于链表。
Malloc 和 New
C/C++双修的人可以很快的Get到他们的区别。我们知道,C++是一个重视面对对象的程序语言。讲求将对象按照内存分派 + 初始化的方式进行构造,结束后进行结束操作 + 内存销毁。这就出来了:Malloc是New的第一步!
new T <-> malloc(sizeof(T)), T(); delete T <-> ~T(), free(T)
笔者推介程序设计的时候也仿照着来,这个顺序下来程序不容易出错。
Inline
浅谈 C++ 中的 inline (1) - 知乎 (zhihu.com)
笔者强推这篇文章,做了求证。这里简单的结合我的经验谈谈。
首先,在编译器尚不完备的过去,编译器需要听从程序员的指令对函数进行可能的内联操作,伴随着编译器的进步,越来越多的编译器在一定的现代C/C++语境下,开始使用自己的估算程序估算内联是否可以带来内联操作。因此,inline在今天慢慢退步成拥有其他含义的关键字,就比如说弱链接(__weak?)的事情。不过这个事情,如您所见,不同编译器的行为并不一致,因此遵循STD C是一个比较好的选择——即声明 + 实现同时存在的时候,函数体短小精悍的时候使用inline,笔者喜欢这样使用:
// In Module: Debug Print static void __pvt_debug_print_impl(){printf("Debug!");// ... } static void __pvt_release_print_impl(){printf("Release");// ... } static inline void __pvt_print(){ #ifdef DEBUG__pvt_debug_print_impl(); #else__pvt_release_print_impl(); #endif } void Module_Tell_Mode() {__pvt_print();// ... }
更多的,梭哈cppref: inline specifier - cppreference.com