C++函数高级特性_高质量C++/C编程指南

第八章 C++函数高级特性


对比于 C 语言的函数,C++增加了 重载(overloaded)内联(inline)constvirtual 四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数, constvirtual 机制仅用于类的成员函数。

8.1 函数重载

在C++中可以为两个或两个以上的函数提供相同的函数名称,只要参数类型不同,或参数类型相同而参数的个数不同,称为函数重载

重载函数的意义在于减少了函数名的数量,避免了名字空间的污染,提高了程序的可读性。

int my_max_i(int a,int b){ return a >b?a:b}

double my_max_d(double a,double b){ return a >b?a:b}

char my_max_c(char a,char b){ return a >b?a:b}

int main()
{
		int ix=my_max(12,23);
		double dx=my_max(12.23,34.45);
		char chx=my_max(‘a’,'b');

		return 0;
}

这些函数都执行了相同的一般性动作——都返回两个形参中的最大值;从用户的角度来看,只有一种操作,就是判断最大值。

这种词汇上的复杂性不是”判断参数中的最大值“问题本身固有的,而是反映了程序设计环境的一种局限性:在同一个域中出现的名字必须指向一个唯实体(函数体)。

函数重载把程序员从这种词汇复杂性中解放出来。

判断函数重载的规则

(1)如果两个函数的参数表相同,但是返回类型不同, 会被标记为编译错误:函数的重复声明。
(2)参数表的比较过程与形参名无关。

(3)如果在两个函数的参数表中,只有缺省实参不同,则第二个被视为第一个的重复声明。

(4)typedef为现有的数据类型提供了一个替换名,它并没有创建一个新类型 ,因此如果两个函数参数表的区别只在于一个使用了 typedef ,而另一个使用了与typedef相应的类型。则该参数表被视为相同的参数列表。

(5)当一个形参类型有constvolatile修饰时,如果形参是按值传递方式定义,在识别函数声明是否相同时,并不考虑constvolatile修饰符。

当一个形参类型有const或volatile修饰时,如果形参定义指针或引用时,在识别函数声明是否相同时,就要考虑constvolatile修饰符。

(6)如果在两个函数的参数表中,形参类型相同,而形参个数不同,形参默认值将会影响函数的重载。(二义性)

函数重载的解析步骤

(1)确定函数调用考虑的重载函数的集合,确定函数调用中实参表的属性。

(2)从重载函数集合中选择函数,该函数可以在(给出实参个数和类型)的情况下可以调用函数。

(3)选择与调用最匹配的函数。

C++ 如何做到函数重载

C++代码在编译时会根据参数列表对函数进行重命名,当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错。

名字粉碎(名字修饰)

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预编译(预处理)、编译、汇编、链 接。Name Mangling是一种在编译过程中,将函数名、变量名的名字重新命名的机制。

“C"或者"C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串

修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

C++编译时函数修饰约定规则

_cdecl调用约定:

(1)以”?”标识函数名的开始,后跟函数名;

(2)函数名后面以"@@YA"标识参数表的开始,后跟参数表;

(3)参数表以代号表示:

 X——void,

 D——char,

 E——unsigned char,

 F——short,

 H——int,

 |——unsigned int,

 J——long,

 K—— unsigned long,

 M ——float,

 N——double,

 _N——bool,

 PA——表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以”0”代 替,一个”0”代表一次重复。

(4)参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。

(5)参数表后以"@Z”标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。

extern关键字

extern放在变量和函数声明之前,表示该变量或者函数在别的文件中已经定义,提示编译器在编译时要从别的文件中寻找

extern ”C“:函数名以C的方式修饰约定规则

extern ”C++“:函数名以C++的方式修饰约定规则

如果 C++程序要调用已经被编译后的 C 函数,该怎么办?
假设某个 C 函数的声明如下:
void foo(int x, int y); 
该函数被 C 编译器编译后在库中的名字为_foo,而 C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用 C 函数。C++提供了一个 C 连接交换指定符号 extern“C”来解决这个问题。
例如:
extern “C” 
{ 
 void foo(int x, int y);// 其它函数 
} 
或者写成
extern “C” 
{ 
 #include “myheader.h” // 其它 C 头文件 
} 
这就告诉 C++编译译器,函数 foo 是个 C 连接,应该到库中找名字_foo 而不是找_foo_int_int。C++编译器开发商已经对 C 标准库的头文件作了 extern“C”处理,所以我们可以用#include 直接引用这些头文件

作用:

(1)声明外部变量

在声明全局变量时,不同的文件在编译器编译时是不透明的,比如在A.c中定义int i,同时在B.c中定义int i,编译器编译时是不会报错的,但是当链接linking时会报错重复定义。当需要使用同一全局变量时,如:在A.c中定义了int i,在B.c中需要调用i,只需要在B.c中声明extern int i,表示该变量在别的文件中已经定义,编译时便不会出错,在linking…的时候会自动去查找定义过的变量i。

(2)extern函数声明

extern void fun()暗示该函数可能在别的文件中定义过,它和定义为void fun(),没什么区别,其用处在于复杂的项目用通过在函数前添加extern声明来取代用include" *.h"来声明函数。

(3)单方面修改函数原型

当声明extern void fun(int i,int j,int k)时,在之后的调用中如果按照是fun(x,y,z)的原型调用时是没有问题的,但是如果要对该函数进行修改比如输入参数,调用时为fun(x,y)此时编译器就会报错了,解决办法就是去掉extern,该头文件中声明void fun(int i,int j),并对该函数进行修改,之后在调用的文件中包含该函数所在的头文件" *.h"即可。

补充:

并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同

void Print(); // 全局函数 
class A 
{void Print(); // 成员函数 
}

::Print(); // 表示 Print 是全局函数而非成员函数
//如果类的某个成员函数要调用全局函数
Print,为了与成员函数 Print 区别,全局函数被调用时应加‘::’标志

隐式类型转换导致重载函数产生二义性

#include <iostream.h> 
void output( int x); // 函数声明 
void output( float x); // 函数声明 
 
void output( int x) 
{ 
	cout << " output int " << x << endl ; 
} 
 
void output( float x) 
{ 
	cout << " output float " << x << endl ; 
} 
 
void main(void) 
{ 
    int x = 1; 
    float y = 1.0; 
    output(x); // output int 1 
    output(y); // output float 1 
    output(1); // output int 1 
    // output(0.5); // error! ambiguous call, 因为自动类型转换 
    output(int(0.5)); // output int 0 
    output(float(0.5)); // output float 0.5 
}

由于数字本身没有类型,将数字当作参数时将自动进行类型转换(称为隐式类型转换)。语句 output(0.5)将产生编译错误,因为编译器不知道该将 0.5 转换成 int 还是 float 类型的参数

8.2成员函数的重载、覆盖与隐藏

8.2.1 重载与覆盖

成员函数被重载的特征:

  • 相同的范围(在同一个类中);
  • 函数名字相同;
  • 参数不同;
  • virtual 关键字可有可无。

覆盖是指派生类函数覆盖基类函数,特征是:

  • 不同的范围(分别位于派生类与基类);
  • 函数名字相同;
  • 参数相同;
  • 基类函数必须有 virtual 关键字。
//函数 Base::f(int)与 Base::f(float)相互重载,而 Base::g(void)被 Derived::g(void)覆盖。

#include <iostream.h> 
class Base 
{ 
public: 
    void f(int x){ cout << "Base::f(int) " << x << endl; } 
    void f(float x){ cout << "Base::f(float) " << x << endl; } 
    virtual void g(void){ cout << "Base::g(void)" << endl;} 
}; 
 
class Derived : public Base 
{ 
public: 
	virtual void g(void){ cout << "Derived::g(void)" << endl;} 
}; 
 
void main(void) 
{ 
    Derived d; 
    Base *pb = &d; 
    pb->f(42); // Base::f(int) 42 
    pb->f(3.14f); // Base::f(float) 3.14 
    pb->g(); // Derived::g(void) 
} 

8.2.2 隐藏

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数

  • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
#include <iostream.h> 
class Base 
{ 
public: 
    virtual void f(float x){ cout << "Base::f(float) " << x << endl; } 
    void g(float x){ cout << "Base::g(float) " << x << endl; } 
    void h(float x){ cout << "Base::h(float) " << x << endl; } 
}; 
class Derived : public Base 
{ 
public: 
    virtual void f(float x){ cout << "Derived::f(float) " << x << endl; } 
    void g(int x){ cout << "Derived::g(int) " << x << endl; } 
    void h(float x){ cout << "Derived::h(float) " << x << endl; } 
};

函数 Derived::f(float)覆盖了 Base::f(float)。 有virtual
函数 Derived::g(int)隐藏了 Base::g(float),而不是重载。 
函数 Derived::h(float)隐藏了 Base::h(float),而不是覆盖。无virtual
隐藏作用
  • 写语句 pd->f(10)的人可能真的想调用 Derived::f(char *)函数,只是他误将参数写错了。有了隐藏规则,编译器就可以明确指出错误。否则,编译器会静悄悄地将错就错,程序员将很难发现这个错误,流下祸根。
  • 假如类 Derived 有多个基类(多重继承),有时搞不清楚哪些基类定义了函数 f。如果没有隐藏规则,那么 pd->f(10)可能会调用一个出乎意料的基类函数 f。尽管隐藏规则看起来不怎么有道理,但它的确能消灭这些意外。
class Base 
{ 
public: 
	void f(int x); 
}; 

class Derived : public Base 
{ 
public: 
	void f(char *str); 
}; 

语句 pd->f(10)的本意是想调用函数 Base::f(int),但是 Base::f(int)不幸被 Derived::f(char *)隐藏了。由于数字 10不能被隐式地转化为字符串,所以在编译时出错。
void Test(void) 
{ 
    Derived *pd = new Derived; 
    pd->f(10); // error 
}

8.3 参数的缺省值

有一些参数的值在每次函数调用时都相同,书写这样的语句会使人厌烦。C++语言采用参数的缺省值使书写变得简洁(在编译时,缺省值由编译器自动插入)。

  • 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。

    void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中 
     
    void Foo(int x=0, int y=0)// 错误,缺省值出现在函数的定义体中 
    {}
    
    //函数的实现(定义)本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体中。
    //参数的缺省值可能会改动,显然修改函数的声明比修改函数的定义要方便。
    

使用参数的缺省值并没有赋予函数新的功能,仅仅是使书写变得简洁一些。 它可能会提高函数的易用性,但是也可能会降低函数的可理解性。所以我们只能适当地 使用参数的缺省值,要防止使用不当产生负面效果。

8.4 运算符重载

参考

为什么会有运算符重载?

运算符,例如+,-,*,/只能对基本类型的常量或者变量进行运算,不能用于对象之间的运算,但在实际编程过程中会存在很多自定义的类型需要进行运算操作,此时原本的基本运算符不在适用,就需要对其进行拓展,即运算符重载,从而进行这些对象之间的运算.

什么是运算符重载?

“重载"——重新定义——创建运算符函数——对原本的运算符函数重载

运算符函数的定义与其他函数的定义类似,惟一的区别是运算符函数的函数名是由关键字operator和其后要重载的运算符符号构成的.

运算符重载格式:

<返回类型说明符> operator <运算符符号>(<参数表>) { <函数体> }

举例:复数类的运算符+,-,*,/重载

class Complex//复数类
{
	int Real;
	int Image;
public:
	Complex(int r=0,int i=0) :Real(r),Image(i)//构造函数
	{
		cout << "Creat Complex" << this << endl;
	}
	~Complex()//析构函数
	{
		cout << "Destroy Complex" << this << endl;
	}
	void Print()const//输出打印方法
	{
		cout << "Real: " << Real << "Image: " << Image << endl;
	}
    
    //+重载
	Complex operator +(const Complex& c) const
	{
		return Complex(this->Real + c.Real, this->Image + c.Image);
	}

	//-重载
	Complex operator -(const Complex& c) const
	{
		return Complex(this->Real - c.Real, this->Image - c.Image);
	}
    
	///重载
	Complex operator /(const Complex& c) const
	{
		return Complex((Real * c.Real + Image * c.Image) / (c.Real * c.Real + c.Image * c.Image),
			(Image * c.Real - Real * c.Image) / (c.Real * c.Real + c.Image * c.Image));
	}

	//*重载
	Complex operator *(const Complex& c)const
	{
		return Complex((this->Real * c.Real) - (this->Image * c.Image), (this->Image * c.Real) + (this->Real * c.Image));
	}
    
}

运算符重载时所需要遵守的一些规则

image-20231025150413027

1.不能够重载的运算符:

"."//类关系运算符;
".*"//成员指针运算符;
"sizeof"//;
"::"//作用域运算符;
"?:"//三目运算符;

2.运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择原则。

3.重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。

4.重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。

5.运算符重载不能改变该运算符用于内部类型对象的含义。(不会覆盖原始的定义,是一种拓展——函数重载)

它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。

6.运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用重载运算符。

运算符重载时的this指针

当运算符重载为类的成员函数时,函数的参数个数比原来的操作数要少一个(后置单目运算符除外),这是因为成员函数用this指针隐式地访问了类的一个对象,它充当了运算符函数最左边的操作数。

(1) 双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。

(2) 前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。

(3) 后置单目运算符重载为类的成员函数时,函数要带有一个整型形参。

调用成员函数运算符的格式如下:

<对象名>.operator <运算符>(<参数>) 它等价于 <对象名><运算符><参数> 例如:a+b等价于a.operator +(b)

8.5 函数内联

内联参考

https://blog.csdn.net/cxy_zjt/article/details/124776420?spm=1001.2014.3001.5502

参考2

当程序执行函数调用时,系统要建立栈空间,保护现场,传递参数以及控制程序执行的转移等等, 这些工作需要系统时间和空间的开销。

函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码。CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行。

函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视

为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数。

call代表着函数调用,此时会存在函数的压栈等一系列操作

在这里插入图片描述

使用inline关键字修饰函数my_add()之后,发现汇编代码中没有了call操作,说明省去了一系列函数调用操作(压栈、跳转、退栈和返回操作等),而是在寄存器种直接替换操作,提高了效率。

内联函数inline如何工作

对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。

关键字 inline必须与函数定义体放在一起才能使函数成为内联,仅将 inline放在函数声明前面不起任何作用

void Foo(int x, int y); 
inline void Foo(int x, int y) // inline 与函数定义体放在一起 
{}

inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。 一般地,用户可以阅读函数的声明,但是看不到函数的定义。声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联

#include <iostream>
using namespace std;
 
//声明内联函数
void swap1(int *a, int *b); //也可以添加inline,但编译器会忽略
 
int main()
{
    int m, n;
    cin>>m>>n;
    cout<<m<<", "<<n<<endl;
    swap1(&m, &n);
    cout<<m<<", "<<n<<endl;
 
    return 0;
}
 
//定义内联函数
inline void swap1(int *a, int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

宏定义与内联函数

内联函数可以看作是宏函数的升级版本,将宏函数的优点保留,将缺点去掉。

C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在 C++ 程序中,应该用内联函数取代所有宏代码,“断言 assert” 恐怕是唯一的例外。

assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。为了不在程序的 Debug 版本和 Release版本引起差别,assert 不应该产生任何副作用。如果 assert 是函数,由于函数调用会引起内存、代码的变动,那么将导致 Debug 版本与 Release 版本存在差异。所以 assert 不是函数,而是宏。

//宏定义的缺点
1.不方便调试宏。(因为预编译阶段进行了替换)
	由于宏是预编译程序来处理,其替换的函数不会进入到符号表中,所以在运行时,不会带来额外的时间和空间开销,而函数会在运行时执行压栈出栈的操作,存在函数调用的开销。所以宏是不可以调试的,而函数可以进行单步调试。
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查。
    
#define MAX(a,b) (a)>(b)?(a):(b)
    
result = MAX(i,j)+2;
//被预处理器扩展为 
result = (i)>(j)?(i):(j)+2;

//由于运算符"+"比运算符"?:"的优先级高,所以上述语句并不等价于
result = ((i)>(j)?(i):(j))+2;

如果把宏代码改写成:
#define MAX(a,b) ((a)>(b)?(a):(b))
此时可以解决优先级问题,但是:
result = MAX(i++,j);
//被预处理器扩展为 
result = (i++)>(j)?(i++):(j) //在同一个表达式中i被两次求值。
宏定义与内联函数的区别
  • 内联函数在编译时展开,带参的宏在预编译时展开。

  • 内联函数直接嵌入到目标代码中,带参的宏是简单的做文本替换。

  • 内联函数有类型检测、语法判断等功能,宏只是替换。

  • 内联函数可以调试,而宏定义不可以。

  • 内联函数可以访问类的成员变量,宏定义不能。

  • 在类中声名同时定义的成员函数,拥有内联属性。

  • 内联函数是函数,宏不是函数。

内联函数总结

内联函数是针对C语言中宏定义的优化,把这个以空间换时间的优化从预编译阶段调整到了编译阶段,所以增加了对使用了内联机制的函数的类型安全检查,或者进行自动类型转换等操作

内联以代码膨胀(拷贝)为代价,仅仅省区了函数调用的开销,从而提高程序的执行效率。(开销指的是参数的压栈、跳转、退栈和返回操作)

  • 如果函数体内代码比较长,使用内联将导致可执行代码膨胀过大。
  • 如果函数体内出现循环或者其他复杂的控制结构,那么执行函数体内代码的时间将比函数调用的开销大得多。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值