《C++高效编程》笔记
标签(空格分隔):《C++高效编程》
本博客是看《C++高效编程》的笔记,还在更新中。。。
基本编程语句
if…else if…
我们在写if…else if…语句时,为了减少运行量,书上列举了几个要点:
- 有多个else if时把运行最快的,最可能发生的,放到最前面。(相当于剪枝)
- 如果判断条件中要进行实质性操作,把其放到最前面。比如:if(c = getnextchar() && k>10){…}
转移表(Jump Table)
个人理解:转移表相当于一个函数表,它相当于一个函数指针数组。【高级c++内容呀】
#include<cstdio>
#include<iostream>
using namespace std;
int add1(int n)
{
return n+1;
}
int add2(int n)
{
return n + 2;
}
int add3(int n)
{
return n+3;
}
int main()
{
//转移表定义
typedef int (*functs)(int c);
functs JumpTable[] = {add1, add2, add3};
int res1 = JumpTable[0](3);
int res2 = JumpTable[1](3);
int res3 = JumpTable[2](3);
printf("%d\n", res1);
printf("%d\n", res2);
printf("%d\n", res3);
return 0;
}
现在我们可以使用转移表遍历调用函数,或者根据条件调用函数【我们可以用switch…case…语句来代替转移表,之后会讲到它们的运行速度比较】
转移表的缺点:转移表必须是连续的,也就是无法定义值为NULL,而且无法实现像switch…case…中类似default的功能。
swich…case…
书上列举了几个swich…case…要点,感觉和if…else…差不多,就不讲了。
总结与比较
我们先比较不进行函数调用时if…else…与swich…case…的区别。(转移表只能进行函数调用,因此不比较)
if(x==1)...;
else if(x==2)...;
else if(x==3)...;
。。。
与
swich(x)
{
case:1
...;
case:2
...;
case:3
...;
}
当x==k时,两个选择语句的耗时情况大致如下:(具体真实数据在书上,我懒得抄了,就编了一些)
k | if..else…耗时 | swich…case…耗时 |
---|---|---|
1 | 1 | 4 |
2 | 2 | 4 |
3 | 3 | 4 |
4 | 4 | 4 |
5 | 5 | 4 |
结论:case的时间不随判断情况在代码块的哪部分而改变,if判断越是靠前的条件越快执行(因此我们要把最可能的情况写前面)。书上提到了一种优化方案,就是:在case语句前使用1~2个if…else…语句,这样可以互补。
有函数调用时三者的区别
直接给结论好了。
k | if..else…耗时 | swich…case…耗时 | 转移表耗时 |
---|---|---|---|
1 | 1 | 4 | 2 |
2 | 2 | 4 | 2 |
3 | 3 | 4 | 2 |
4 | 4 | 4 | 2 |
5 | 5 | 4 | 2 |
因此,转移表>case>if
当需要稳定查找时间时,转移表是最佳选择。
数组
虽然转移表查找快,但在某些方面数组才是大爷中的大爷【书上说他快得无与伦比】,因此在做acm题时,我们一般先预处理打一份答案表。
函数
函数调用
一个简短的相加程序解析。
int add(int a, int b, int c)
{
return a+b+c;
}
void main(void)
{
int a = 1, b = 2, c = 3;
int res = add(a, b, c);
}
调用函数add()产生的汇编程序1
mov eax, DWORD PTR _c$[ebp] ;把数值'c'存入寄存器eax中
push eax ;把寄存器eax压入堆栈中
mov ecx, DWORD PTR _b$[ebp] ;把数值'b'存入寄存器ecx中
push ecx ;把寄存器ecx压入堆栈中
mov edx, DWORD PTR _a$[ebp] ;把数值'a'存入寄存器edx中,由于ebx有其他特殊用途,我们没有用ebx
push edx ;把寄存器edx压入堆栈中
call ?add@@YAHHHH@Z ;调用add()
可以看到开始,我们把c,b,a通过寄存器放入堆栈中(注意顺序)。
当我们执行最后一行—add()函数时,会产生一个程序计数器,在任何时候程序计数器都会指向处理器检索程序指令的地址。因为有程序计数器,我们可以在函数结束后继续进行调用函数后的程序。
进入函数时add()的操作
push ebp ;把基指针放在堆栈中
mov ebp, esp ;新的基指针值
函数执行时会产生一个基指针,还会用到堆栈。
基指针:该指针告诉处理器在何处可以找到局部变量。(局部变量也会压入栈中)
函数运行时的操作
mov eax, DWORD PTR _a$[ebp]; 寄存器eax中的数值'a'
add eax, DWORD PTR _b$[ebp]; 在eax中添加'b'
add eax, DWORD PTR _c$[ebp]; 在eax中添加'c'
这是三个变量的加法。
函数退出时的操作
pop ebp; ebp恢复为原值
ret 0 ; 从函数中返回
捕获返回结果时的操作
add esp, 12; 3个变量出栈,每个变量4字节
mov DWORD PTR _res$[ebp], eax; eax包含该函数
宏
宏相对于函数,可以避免函数在调用堆栈时的开销。缺点大家当然也知道,难于阅读且易出错。
有人会说,要想宏用得好,只要多加括号就可以了。这里举一个未预料到的宏设计。
#define MAX(a,b) ((a)<(b)?(b):(a))
void main(void)
{
int a = 10;
char b = 12;
short c = MAX(a, b); // a = 10, b = 12, c = 12 (确定)
c = MAX(a++, b++); // a = 11, b = 14, c = 13 (不确定)
}
优点:
- 避免函数调用开销。
缺点:
- 难以阅读(在某些方面),很难检查出错
内联函数
内联函数与宏差不多,本质是代码替换。会花费多余内存,但比宏更不容易出错。
关于内存,还有一点就是:我们可以把递归的算法转化为迭代,内存占用少,不容易RE。
参数传递
我们在传递参数时往往有4种写法:
- 值传递
- 引用传递(实质是指针)
- 指针
- 全局变量
其中最快的是全局变量,但它吃内存。
值传递无法修改传入变量的值。
虚函数*
你们知道虚函数是如何实现多态的吗?可能绝大多数人只知道虚函数能实现多态,却不知道它是如何实现的!
我们用关键字virtual向编译器表明我们需要虚函数,这时我们使用一种叫后联编技术(late binding),这意味着在编译过程中并不确定程序运行时调用何种函数,而在运行中确定调用何种函数。
上述工作如何完成的呢?
- 在编译器中产生的代码中添加虚函数表(Virtual Table ,VT)和虚函数指针(Virtual Table Pointer,VPTR)
- 其次,在程序运行时,添加虚函数表和虚函数表指针的代码。
比如有一个这样的代码:
class base
{
public:
virtual int area(int k)=0;
};
class spere:public base
{
public:
int area(int l){return l*l;}
};
class circle:public base
{
public:
int area(int r){return 3.14*r*r;}
}
我们首先会在编译器产生的代码中添加虚函数表,比如在base中添加一个虚函数表
//假想操作
typedef vtable (*vptr)();
vtable vTable[] = {spere:area, circle:area};
然后在运行时添加调用代码
vtable * vptr = p[0]->vptr;
void (*funtionPtr)() = vptr[PrintNameIndex];
functionPtr();