C++的一些特性(1)

这一章讲一下 C++ 比 C 多的一些东西吧,不然有时候真的感觉 C 和 C++ 区别不大的。

一些其它的函数定义

内联函数

内联函数是指在调用的时候不使用 call,而是用函数的代码替换这里的函数调用,可以节省这里时间上的效率损失,常见于逻辑简单,需要频繁调用的函数,这里省下的时间开销就很大,在函数定义前加关键字 inline 可以把函数声明为内联。当然,逻辑复杂的函数也可以使用 inline,但是编译器不一定会听,编译器会综合考虑要不要对这个函数使用内联。

裸函数

在函数定义前加关键字 __deplspec(naked) 可以把函数声明为裸函数,裸函数是指什么都不具备的函数,不会自动给你压栈,也不会自动给你返回,什么都需要自己做。

一般来说,使用naked函数时需要注意以下问题:

  1. 函数必须显式返回。一般通过 __asm ret的内嵌汇编指令返回。
  2. 不可以通过任何方式使用局部变量。若声明一个局部变量,并在代码中为其赋值,则会更改父函数中相应位置的局部函数的值。
  3. 只能通过esp引用参数。因为子函数继承了父函数的ebp寄存器,所以只能通过esp引用参数。
  4. naked 属性仅与函数的定义相关,不能在函数原型中指定。不能用于函数指针,不能用于数据定义。

比如 C 语言定义一个这样的函数

1
2
3
 _declspec(naked) void add(int a, int b) {
a += b;
}

在编译完成之后转到反汇编可以看到

相比不加 naked 定义,少了压栈和退栈以及返回的代码,也就是说各方面操作要我们内联汇编完成。

这个的用途我也是能想到的,就比如我要对一段代码进行 inline hook,一半情况下我的操作是把我的代码先转为字节码存到数组里面,再用 VirtualProtect 函数赋予可执行权限之后,jmp 跳转到字节数组里面,这就很不方便,因为我要改某些方面的代码就会很困难,而有了裸函数我可以直接写汇编代码然后用这个函数作为跳板执行,后期维护也比较方便。

类型推导

我们上面说过用 typeid 函数可以获取变量类型,同时我们需要定义一个跟一个变量类型相同的变量类型可以使用

1
decltype(var1) var2;

的形式去得到,这样我们就定义了一个跟 var1 类型一致的 var2 变量,这个变量类型是通过推导得到的。

类型推导还有一种方式是使用关键字 auto,比如我们经常用的,iterator,在遍历 vector 或者是 map 的时候都是写的 auto p,因为那个变量类型过于长,我们得这么写:vector<int>::iterator 不如一个 auto 省事,但是它们原理一致都是通过类型推导得到。

函数返回类型后置

这也是 C++ 的一个特性,似乎跟 python 差不多,我记得 python 是这样的写法。就是我们在定义函数的时候一般是返回值开头的,但是 C++ 运行你放后面,用这种写法:

1
2
3
auto test()->int{
//someting
}

两者结合一下可以让模板变得更加灵活。

比如加法它不仅可以支持相同类型,也可以支持不同类型,那么我们来实现一个加法函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 template<typename T1,typename T2>
auto test(T1 a, T2 b) -> decltype(a + b) {
return a + b;
}

int main() {
int a[] = { 1,2,3,4,5 };
int* p = a;
int k = 2;

int* p1 = test(p, k);
std::cout << *p1 << std::endl;

}

这个 test 函数实现了加法,但是它更为灵活,不像之前一样两个变量类型只能返回其中一个,在这里无论是调用 test(p,k) 还是交换调用,最终都会返回一个 int* 类型,因为 int* + int 得出的变量类型就是 int *

当然前置也可以写的,只不过需要改改,没有那么灵活,因为我们在前面还没有定义上参数名,因此我们不能使用 decltype(a+b),我们只能用两个类型去实例化一个匿名对象相加,得到的结果给了 decltype 作为参数。

1
2
3
4
 template<typename T1,typename T2>
decltype(T1() + T2()) test(T1 a, T2 b){
return a + b;
}