C++对C的扩展4:函数扩展
内联函数的引出
在C++出现之后,使用预处理宏会出现两个问题:
- 第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。
- 第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类类的成员函数。
为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function).
内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。
预处理宏的缺陷
问题一:
#define ADD(x,y) x+y
inline int Add(int x,int y){
return x + y;
}
void test(){
int ret1 = ADD(10, 20) * 10; //希望的结果是300
int ret2 = Add(10, 20) * 10; //希望结果也是300
cout << "ret1:" << ret1 << endl; //210
cout << "ret2:" << ret2 << endl; //300
}
问题二:
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y){
return x < y ? x : y;
}
void test02(){
int a = 1;
int b = 3;
//cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}
问题三:
预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。
inline内联函数
C++中的const常量可以替代宏常数定义,如:
const int A = 3; 代替 #define A 3
C++中是否有解决方案替代宏代码片段呢?(替代宏代码片段就可以避免宏的副作用!)
C++中推荐使用内联函数(inline关键字声明)替代宏代码片段
inline int func(int a){return ++;}
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
注:内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求
类内部的内联函数
为了定义内联函数,通常必须在函数定义前面放一个inline关键字。
但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。
class Person{
public:
Person(){ cout << "构造函数!" << endl; }
void PrintPerson(){ cout << "输出Person!" << endl; }
}
构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。
宏替换和函数调用区别
#include "iostream"
using namespace std;
#define MYFUNC(a, b) ((a) < (b) ? (a) : (b)) //宏替换
inline int myfunc(int a, int b) //函数调用
{
return a < b ? a : b;
}
int main()
{
int a = 1;
int b = 3;
//int c = myfunc(++a, b); //a=2,b=3,c=2
int c = MYFUNC(++a, b); //宏替换并展开 ((++a) < (b) ? (++a) : (b))
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
system("pause");
return 0;
}
- 说明1:
必须inline int myfunc(int a, int b)和函数体的实现,写在一块
- 说明2:
- C++编译器可以将一个函数进行内联编译
- 被C++编译器内联编译的函数叫做内联函数
- 内联函数在最终生成的代码中是没有定义的
- C++编译器直接将函数体插入在函数调用的地方(在使用时编译器将函数名替换成内联函数的代码块)
- 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)
- 说明3:
C++编译器不一定准许函数的内联请求!:
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
- 说明4:
- 内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等)
- 内联函数是对编译器的一种请求,因此编译器可能拒绝这种请求
- 内联函数由编译器处理,直接将编译后的函数体直接插入到调用的地方
- 宏代码片段 由预处理器处理, 进行简单的文本替换,没有任何编译过程
- 说明5:
- 现代C++编译器能够进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译
- 另外,一些现代C++编译器提供了扩展语法,能够对函数进行强制内联
- 如:g++中的__attribute__((always_inline))属性
- 说明6:
C++中内联编译的限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 不能对函数进行取址操作
- 函数内联声明必须在调用语句之前
编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。
因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。
结论:
1. 内联函数在编译时直接将函数体插入函数调用的地方
2. inline只是一种请求,编译器不一定允许这种请求
3. 内联函数省去了普通函数调用时压栈,跳转和返回的开销
函数的默认参数
C++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。
void TestFunc01(int a = 10, int b = 20){
cout << "a + b = " << a + b << endl;
}
//注意点:
//1. 形参b设置默认参数值,那么后面位置的形参c也需要设置默认参数
void TestFunc02(int a,int b = 10,int c = 10){}
//2. 如果函数声明和函数定义分开,函数声明设置了默认参数,函数定义不能再设置默认参数
void TestFunc03(int a = 0,int b = 0);
void TestFunc03(int a, int b){}
int main(){
//1.如果没有传参数,那么使用默认参数
TestFunc01();
//2. 如果传一个参数,那么第二个参数使用默认参数
TestFunc01(100);
//3. 如果传入两个参数,那么两个参数都使用我们传入的参数
TestFunc01(100, 200);
return EXIT_SUCCESS;
}
注意点:
- 函数的默认参数从左向右,如果一个参数设置了默认参数,那么这个参数之后的参数都必须设置默认参数。
- 如果函数声明和函数定义分开写,函数声明和函数定义不能同时设置默认参数。
函数的占位参数
C++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
void TestFunc01(int a,int b,int){
//函数内部无法使用占位参数
cout << "a + b = " << a + b << endl;
}
//占位参数也可以设置默认值
void TestFunc02(int a, int b, int = 20){
//函数内部依旧无法使用占位参数
cout << "a + b = " << a + b << endl;
}
int main(){
//错误调用,占位参数也是参数,必须传参数
//TestFunc01(10,20);
//正确调用
TestFunc01(10,20,30);
//正确调用
TestFunc02(10,20);
//正确调用
TestFunc02(10, 20, 30);
return EXIT_SUCCESS;
}
函数重载
能使名字方便使用,是任何程序设计语言的一个重要特征!
函数重载(Function Overload):
- 用同一个函数名定义不同的函数
- 当函数名和不同的参数搭配时函数的含义不同
实现函数重载的条件:
- 同一个作用域
- 参数个数不同
- 参数类型不同
- 参数顺序不同
//1. 函数重载条件
namespace A{
void MyFunc(){ cout << "无参数!" << endl; }
void MyFunc(int a){ cout << "a: " << a << endl; }
void MyFunc(string b){ cout << "b: " << b << endl; }
void MyFunc(int a, string b){ cout << "a: " << a << " b:" << b << endl;}
void MyFunc(string b, int a){cout << "a: " << a << " b:" << b << endl;}
}
//2.返回值不作为函数重载依据
namespace B{
void MyFunc(string b, int a){}
//int MyFunc(string b, int a){} //无法重载仅按返回值区分的函数
}
注:函数返回值不是函数重载的判断标准
原因: 当编译器能从上下文中确定唯一的函数的时,如int ret =func(),这个当然是没有问题的。然而,我们在编写程序过程中可以忽略他的返回值。那么这个时候,假如一个函数为 void func(int x);另一个为int func(int x);当我们直接调用func(10),这个时候编译器就不确定调用那个函数。所以在c++中禁止使用返回值作为重载的条件。
注意: 函数重载和默认参数一起使用,需要额外注意二义性问题的产生。
void MyFunc(string b){
cout << "b: " << b << endl;
}
//函数重载碰上默认参数
void MyFunc(string b, int a = 10){
cout << "a: " << a << " b:" << b << endl;
}
int main(){
MyFunc("hello"); //这时,两个函数都能匹配调用,产生二义性
return 0;
}
函数重载的调用准则
- 将所有同名函数作为候选者
- 尝试寻找可行的候选函数
- 精确匹配实参
- 通过默认参数能够匹配实参
- 通过默认类型转换匹配实参
- 匹配失败
- 最终寻找到的可行候选函数不唯一,则出现二义性,编译失败。
- 无法匹配所有候选者,函数未定义,编译失败。
重载函数在本质上是相互独立的不同函数(静态链编)
函数重载是发生在一个类中里面
函数重载遇上函数默认参数
//当函数默认参数遇上函数重载会发生什么
int func(int a, int b, int c = 0)
{
return a * b * c;
}
int func(int a, int b)
{
return a + b;
}
//1个参数的允许吗
int func(int a)
{
return a + b;
}
int main()
{
int c = 0;
c = func(1, 2); // 存在二义性,调用失败,编译不能通过
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
函数重载和函数指针结合
/*
函数重载与函数指针
当使用重载函数名对函数指针进行赋值时
根据重载规则挑选与函数指针参数列表一致的候选者
严格匹配候选者的函数类型与函数指针的函数类型
*/
int func(int x) // int(int a)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
typedef int(*PFUNC)(int a); // int(int a)
//方法2:typedef int (PFUNC)(int a);
//方法3:int (*PFUNC)(int a)
int main()
{
int c = 0;
PFUNC p = func;
//方法2:PFUNC *p = func;
//方法3:*PFUNC = func;
c = p(1);
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
extern “C”浅析
以下在Linux下测试:
C函数: void MyFunc(){} ,被编译成函数: MyFunc
C++函数: void MyFunc(){},被编译成函数:_Z6Myfuncv
通过这个测试,由于c++中需要支持函数重载,所以c和c++中对同一个函数经过编译后生成的函数名是不相同的,这就导致了一个问题,如果在c++中调用一个使用c语言编写模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误,以上例,c++中调用MyFunc函数,在链接阶段会去找Z6Myfuncv,结果是没有找到的,因为这个MyFunc函数是c语言编写的,生成的符号是MyFunc。
那么如果我想在c++调用c的函数怎么办?
extern "C"的主要作用就是为了实现c++代码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而不是按c++的方式。
MyModule.h
#ifndef MYMODULE_H
#define MYMODULE_H
#include<stdio.h>
#ifdef __cplusplus
extern "C"{
#endif
void func1();
int func2(int a,int b);
#ifdef __cplusplus
}
#endif
#endif
MyModule.c
#include"MyModule.h"
void func1(){
printf("hello world!");
}
int func2(int a, int b){
return a + b;
}
TestExternC.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#if 0
#ifdef __cplusplus
extern "C" {
#if 0
void func1();
int func2(int a, int b);
#else
#include"MyModule.h"
#endif
}
#endif
#else
extern "C" void func1();
extern "C" int func2(int a, int b);
#endif
int main(){
func1();
cout << func2(10, 20) << endl;
return EXIT_SUCCESS;
}