C++函数详解(三)—— inline与constexpr函数、匹配和函数指针

目录

  • 1 inline 与 constexpr 函数
    • 1.1 内联函数
    • 1.2 constexpr函数
    • 1.3 把内联函数和 constexpr 函数放在头文件中
  • 2 函数匹配
  • 3 函数指针
    • 3.1 函数指针的定义
    • 3.2 使用函数指针
    • 3.3 函数指针作为形参
    • 3.4 函数指针作为返回值

1 inline 与 constexpr 函数

1.1 内联函数

在函数签名的返回值类型前加上关键字inline,指示编译器将此函数作为内联函数,即在每个调用它的地方将其“内联地”展开(可以理解为在调用它的地方将调用函数代码替换为函数内部的实现代码)。常常将需要频繁调用且规模较小的轻量函数定义为内联函数,其意义如下:

  • 对于实现轻量操作的小函数而言,将其内部的轻量操作封装为一个小函数,而不是在需要执行这些轻量操作的地方直接写操作本身的代码,有利于程序的封装和复用。
  • 但如果这种封装的小函数只是普通的函数,而程序中需要对其频繁调用。因为函数调用隐式地包含了很多耗费资源的额外工作(如传参初始化形参临时对象等),因此在频繁调用的情景下会造成不必要的资源浪费。
  • 定义为内联函数,既满足了封装性,又避免了实际调用时带来的资源损耗。

需要注意两点:

  • 在函数签名中声明inline只是向编译器发出内联展开的请求,编译器可以根据情况忽略这个请求。一般不支持内联递归函数,且一个75行以上的函数也不大可能在调用点内联展开。
  • 除了显式地声明 inline外,还存在隐式内联函数。当类的一个成员函数在类内声明处定义时,编译器就会将其自动地优化为内联函数,这就是典型的隐式内联

范例:

//内联版本的shorterStr函数(返回两个string对象中较短的那个)
inline const string & shorterStr(const string &s1, const string &s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

int main() {
	string s1 = "aaaa", s2 = "bb";
	cout << shorterStr(s1, s2) << endl; //由于shorter是内联函数,在编译过程中,此语句被编译器展开为以下的形式:
	cout << s1.size() <= s2.size() ? s1 : s2; << endl;
}

class Array {
public:
	void func() { //在类内声明处定义,此函数隐式内联
		cout << "Array func" << endl;
	}
	void func1(); //func1在类内声明,类外定义
	inline void func2(); //func2在类内显式声明为内联函数,类外定义
	void func3(); //func3在类内没有显式声明为inline,在类外定义时追加inline
};
void Array::func1() { //func1在类内声明,类外定义,不会隐式自动优化为内联函数
	cout << "Array func1" << endl;
}
void Array::func2() { //func2在类内显式声明为内联函数,类外定义,编译器会在调用func2时尝试将其内联展开
	cout << "Array func2" << endl;
}
inline void Array::func3() { //func3在类内没有显式声明为inline,在类外定义时追加inline,编译器也会在调用func3时尝试将其内联展开
	cout << "Array func3" << endl;
}

1.2 constexpr函数

constexpr 函数与一般函数的区别:

  • 函数的返回值类型和所有参数的类型都得是字面值类型。
  • 函数体中必须有且仅有一条return语句。
  • 在调用 constexpr 函数时,编译器事实上是将对 constexpr 函数的调用替换为其返回的结果值。因此,constexpr 函数也是隐式内联函数。

注意:允许 constexpr 函数的返回值并非一个常量,范例:

constexpr int new_sz() { //定义new_sz()为constexpr函数
	return 42; //函数的返回值类型和所有参数的类型都是字面值类型
}
constexpr size_t scale(size_t cnt) { //定义scale()为constexpr函数
	return new_sz()*cnt; //new_sz()返回值是字面值常量,如果cnt是常量表达式,则此处返回的也是常量表达式,如果cnt是非常量表达式,则此处返回的是非常量表达式
}

int main() {
	int arr[scale(2)]; //正确,scale(2)是常量表达式
	int i = 2;
	int a2[scale(i)]; //❌,i不是常量表达式,因此scale(i)返回的也不是常量表达式
	//当把scale函数用在需要常量表达式的上下文中时,由编译器检查其返回结果是否符合要求,如果其返回结果是非常量表达式则报错
}

1.3 把内联函数和 constexpr 函数放在头文件中

和其他函数不同,内联函数和 constexpr 函数可以在程序中多次定义。但对于某个给定的内联函数或 constexpr 函数来说,它的多个定义必须完全一致。因此需要将内联函数和 constexpr 函数定义在头文件中。

2 函数匹配

基础,略。但需特别注意以下例子出现的二义性调用:

//函数f的四种重载形式
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);

//调用f
f(5.6); //调用的重载形式是void f(double, double = 3.14); 虽然void f(int);也可以通过参数转型来实现匹配,但相比于void f(double, double = 3.14)形式,多一层转型操作
f(42, 2.56); //二义性调用,编译报错

在以上例子的第二个调用中,如果考虑第一个实参我们认为f(int, int)更匹配,它只需要将第二个输入参数2.56做自动类型转换转为int就可以。如果考虑第二个实参则会认为f(double, double)更匹配,因为它只需要将第一个输入参数42做自动类型转换转为double就可以。综上,两个候选函数的匹配误差代价相同,编译器会报二义性调用错误。

3 函数指针

3.1 函数指针的定义

函数指针指向的是函数,而不是像一般指针那样指向对象。函数指针也有其指向的特定类型,这种“类型”取决与函数的返回值类型和形参类型(也可以说某个函数的类型就是其返回值类型和形参类型),与函数名无关。
想要声明一个指向某函数的函数指针,只需要保留函数类型,用指针来替换函数名即可,前后格式类似于声明一个指向数组的指针,例如:

bool lengthCompare(const string &s1, const string &s2); //将该函数的类型看作 bool (const string &, const string &)

bool (*pf) (const string &, const string &); //声明一个指向函数的指针,指针名为pf,pf所指向的函数的参数列表是两个const string的引用,返回值是bool类型,此处pf仅声明,未初始化

注意解读上例中pf的声明格式:

  • pf 前面有 *,并用括号括在一起,说明 pf 是个指针
  • 右侧是形参列表,表示 pf 指向的是函数,函数的形参列表是两个 const string 引用
  • 左侧是 pf 指向函数的返回值类型

3.2 使用函数指针

  • 当把函数名作为一个值来使用时(比如用在赋值语句右侧),该函数名会自动转换为指针。
  • 可以直接使用执行函数的指针来调用该函数,无需先解引用指针再用调用符调用函数。
  • 指向不同函数类型的指针之间不存在转换规则。但可以将函数指针置为nullptr或值为0的整型常量表达式,表示该指针不指向任何函数。
  • 使用重载函数时,上下文必须清晰界定会匹配到哪个函数实现。如果定义了指向重载函数的指针,则编译器会通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个实现精确匹配

范例:

//接上例代码
pf = lengthCompare; //令pf指向名为lengthCompare的函数,编译器将右侧的函数名自动转为指针,等价于下一行语句
pf = &lengthCompare;

bool b1 = pf("Hello", "good"); //通过函数指针pf来调用它指向的函数lengthCompare
bool b2 = (*pf)("Hello", "good"); //上一句的等价形式,表明无需先解引用指针再调用
bool b3 = lengthCompare("Hello", "good"); //上一句的等价形式,直接调用函数本身

string::size_type sumLength(const string &, const string &);
bool cstringCompare(const char *, const char *);
pf = 0; //正确,pf置为0表明其不指向任何函数
pf = sumLength; //❌,类型不匹配,试图用一个指向 bool (const string &, const string &)类型函数的指针指向一个 string::size_type (const string &, const string &)类型的函数
pf = cstringCompare; //❌,类型不匹配,试图用一个指向 bool (const string &, const string &)类型函数的指针指向一个 bool (const char *, const char *)类型的函数

void ff(int *); //声明函数ff,其类型为void (int *)
void ff(unsigned int); //重载函数ff,其类型为void (unsigned int)
void (*pf1) (unsigned int) = ff; //定义一个指向函数的指针pf1,其指向的函数类型为void (unsigned int)类型,并使其指向ff的该类型重载形式void ff(unsigned int)
void (*pf2) (int) = ff; //❌,ff的所有重载形式中没有类型为void (int)的
double (*pf3)(int *) = ff; //❌,ff的所有重载形式中没有类型为double (int *)的 

3.3 函数指针作为形参

和数组类似,虽然不能直接定义函数类型的形参,但形参可以是指向函数的指针

  • 如果一个函数的形参定义为指向函数的指针,则此时形参看起来时函数类型,实际是当成指针来使用
  • 可以直接把函数名作为实参使用,此时该函数名会自动转换为指向该函数的指针。
  • 将形参直接声明为函数指针会显得十分冗长,可以使用类型别名来做简化。

范例:

const string & shorterStr(const string &s1, const string &s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

void useBigger(const string &s1, const string &s2, const string & pf(const string &, const string &)); //useBigger函数的第三个参数是函数(类型),第三个参数的参数名就是pf,代表一个const string & (const string &, const string &)类型的函数,它(pf)会自动转化为指向该函数的指针
//以下为useBigger的等价声明方式,区别是在其第三个参数声明处,显式地将其定义为函数指针
void useBigger(const string &s1, const string &s2, const string & (*pf)(const string &, const string &)); //此时第三个参数声明中的pf(参数名)显式地是一个指针,指向一个const string & (const string &, const string &)类型的函数

//使用useBigger函数时,直接在第三个参数处传入函数名,该函数名会自动转换成指向该函数的指针
useBigger(s1, s2, shorterStr); //自动将函数名shorterStr转换为指向该函数的指针

//使用类型别名来简化函数指针形参
//以下的Func和Func2是函数类型的别名
typedef bool Func(const string &, const string &); //用Func作为函数类型bool (const string &, const string &)的类型别名
typedef decltype(shorterStr) Func2; //与上一行等价,用Func2作为shorterStr函数的类型名的别名
//以下的FuncP和FuncP2是指向函数的指针类型的笔名
typedef bool (*FuncP)(const string &, const string &); //用FuncP作为指向函数类型bool (const string &, const string &)的指针类型的别名
typedef decltype(shorterStr) *FuncP2; //与上一行等价,用FuncP2作为指向shorterStr函数的指针类型的别名,需要特别注意的是,decltype(shorterStr)返回的结果是函数类型,所以要在该结果前加上 *才能得到指针
//使用类型别名的简化useBigger的结果
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, FuncP2);
//以上两行声明语句声明的是同一个函数,在第一条语句中,编译器自动将Func表示的函数类型转换为指向该函数类型的函数指针

3.4 函数指针作为返回值

和数组类似,虽然不能把函数本身作为某个函数的返回值返回,但可以返回指向函数类型的指针。需要特别注意的是,编译器不会自动地将函数类型的返回值当作指针处理,这要求我们必须显式地把返回类型写成指针形式
声明一个返回值为函数指针的函数有三种写法,分别是直接声明、使用类型别名、使用尾置返回类型。

//方法1,直接声明一个返回函数指针的函数f1,非常不建议这样写
int (*f1(int))(int *, int); //按从内到外的顺序理解此声明语句。首先看f1后跟有形参列表,f1(int),说明f1是个函数;再看f1前面有*,说明f1返回的是指针,因此(*f1(int))得到的是一个指针类型,该指针类型后面跟(int *, int),说明指针的类型本身也包含形参列表,因此这是一个指向函数的指针,它所指向的函数的返回值是int(最开始的int)

//方法2,使用类型别名
using F = int (int *, int); //定义F为函数类型int (int *, int)的类型别名
using PF = int (*)(int *, int); //定义PF为指向函数的指针类型的别名,该指针所指的函数类型为int (int *, int)
PF f1(int); //声明一个名为f1的函数,它的参数列表是int,返回值是PF类型,即一个指向int (int *, int)类型函数的函数指针
F f1(int); //❌,这是错误的f1声明写法,F是函数类型的类型别名,f1作为一个函数,其不能直接返回一个函数类型作为返回值,只能返回指向该函数类型的指针(如PF)
F *f1(int); //正确,显式地指定返回类型是指向F指代的函数类型的指针

//方法3,使用尾置返回类型,推荐
auto f1(int) -> int (*)(int *, int); 

你可能感兴趣的:(C++,c++,开发语言,后端)