C++知识总结目录索引
重载,简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
实际编程当中,有时候我们需要实现几个功能类似的函数,只是有些细节不同。例如Swap函数(用来交换两个变量的值),这两个变量有多种类型,可以是 int、float、char、bool 等,我们需要通过参数把变量的地址传入函数内部。如果是用C语言,我们要设计三个不同的函数,如下:
void Swap1(int *a, int *b); //交换 int 变量的值
void Swap2(float *a, float *b); //交换 float 变量的值
void Swap3(char *a, char *b); //交换 char 变量的值
这样写起来非常麻烦,但在C++中我们完全没必要这么写,C++支持重载,多个函数可以有同一个名字,只要他们的参数列表不同。
参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。
函数的重载必须遵循以下四条规则:
C++
如何实现重载?我们知道源代码(.cpp)翻译成可执行程序有四步——预处理、编译、汇编和链接。它们实现的功能是:
C++代码在编译时会根据参数列表对函数进行重命名,下面展示一下在Linux操作系统的g++编译器中函数名的转换。
//交换两个int变量的值
void Swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//交换两个float变量的值
void Swap(float *a, float *b)
{
float tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int n1 = 10;
int n2 = 100;
Swap(&a, &b);
float f1 = 1.1;
float f2 = 2.1;
Swap(&f1, &f2);
return 0;
}
用g++编译上述代码,然后使用objdump -d a.out > log.txt
把它的汇编代码写入到log.txt
文件,再用less
指令查看。我们来对比一下源代码中的函数名和汇编代码中的函数名:
void Swap(int* a, int* b)
–> _Z4SwapPiS
,
void Swap(float* x, float* y)
–> _Z4SwapPfS
。
通过比较我们推断出汇编代码的含义:作用域+(命名空间)+函数名+参数形式
在链接阶段,虽然代码中这几个函数名都是相同的,但是在编译系统作用下,这些函数已经根据C++函数名修饰规则重新生成了新的名字。
编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(OverloadResolution)。不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。
问:在C++程序中调用被C编译器编译后的函数,为什么要加 extern "C"
?
答:C++
语言支持函数重载,C语言不支持函数重载。函数被C++
编译器编译和被C编译器编译后生成的内部名字是不同的。C++
提供了C连接交换指定符号 extern "C"
来解决名字匹配问题(即二进制兼容性问题)。
C++之所以支持重载是因为它的函数名修饰规则。
先前说到编译器会根据传入的实参去逐个匹配,以选择对应的函数,那么它是如何匹配的呢?为了匹配到最适合的重载函数,需要依次按照下列规则来判断:
精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
使用标准转换匹配:如int 到double、double到int、double到long double、T* 到void*、int到unsigned int;
使用用户自定义匹配;
使用省略号匹配:类似printf中省略号参数
如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:
void func(short);
void func(long);
int main()
{
int i = 0;
func(i);
system("pause");
return 0;
}
这时候就会报如下错误:
3 IntelliSense: 有多个 重载函数 "func" 实例与参数列表匹配:
函数 "func(short)"
函数 "func(long)"
参数类型为: (int)
错误 2 error C2668: “f2”: 对重载函数的调用不明确
错误 1 error C2668: “f1”: 对重载函数的调用不明确
在C++中可以用operator关键字加上运算符来表示函数,叫做运算符重载。运算符重载函数形式比较特殊,运算符本身就是函数名。
下面是一个复数相函数
Complex Add(const Complex& n1, const Complex& n2);
使用运算符重定义后:
Complex operator+(const Complex& n1, const Complex& n2);
使用方法:
int main()
{
Complex a, b, c;
//...
c = Add(a, b); //调用普通函数
c = a + b; //调用运算符重载函数——“+”
运算符 | 规则 |
---|---|
所有一元运算符 | 建议重载为非静态成员函数 |
=、()、[]、->、* |
只能重载为非静态成员函数 |
+=、-=、/=、*=、&=、/=、-=、%=、>>=、<<= |
建议重载为非静态成员函数 |
其他所有运算符 | 建议重载为全局函数 |
.
。.*
。::
。? :
。sizeof()
和typeid()
。#
和##
等预处理操作符 。++
和 --
在C++标准中规定:当为一个类型重载++
或--
的前置版本时,不需要参数;当为一个类型重载++
或--
的后置版本时,需要一个int
类型的参数作为标志(即哑元,非具名参数),加这个int
形参唯一的作用就是和前置版本构成重载。
class AA
{
public:
AA(int aa1, int aa2); //构造函数
AA(const AA& aa); //拷贝构造函数
AA& operator++(); //前置++
AA operator++(int); //后置++,形参列表不同才能构成重载
void Show();
private:
int _a1;
int _a2;
};
//重载实现
AA& AA::operator++() //前置
{
_a1 += 1;
_a2 += 1;
return *this;
}
AA AA::operator++(int) //后置
{
AA ret = AA(*this);
_a1 += 1;
_a2 += 1;
return ret;
}
//调用
int main()
{
AA aa(10, 11);
aa.operator++(); //调用前置++
aa.operator++(0); //调用后置++
aa.operator++(int()); //也可以这样写,调用后置++
system("pause");
return 0;
}
我们比较上面代码的前置++和后置++实现,前置++实现只要对成员++,然后直接返回就可了;但是后置++不同,后置++必须先保存原来的值,再进行++运算,最后返回,别小看这两者之间只差了一个创建变量,当你类成员非常大的时候,效率上的差距就出来了。
如果仔细看的话,你会发现前置++的返回值是引用,后置++返回的是值,返回值是要创建一个临时变量的,这样效率差距就更大了,所以如果可以,我们尽量使用前置++。
这部分看书时很少提到,提到了也都说输入输出运算符是不可以重载成成员函数的(都是用友元实现的),下面我们试着实现一下。
class AA
{
public:
//...
ostream& operator<<(ostream& os); //输入输出运算符的重载
istream& operator>>(istream& is);
private:
int _a1;
int _a2;
};
//类外实现
ostream& AA::operator<<(ostream& os)
{
os << _a1 << endl;
os << _a2 << endl;
return os;
}
istream& AA::operator>>(istream& is)
{
is >> _a1;
is >> _a2;
return is;
}
int main()
{
AA bb(1, 1);
cout << bb; //(1)
bb << cout; //(2)
system("pause");
return 0;
}
调用输入输出的时候我们发现按照常规写法 (1) ,程序会报错,报错的信息也很燃:错误 1 error C2679: 二进制“<<”: 没有找到接受“AA”类型的右操作数的运算符(或没有可接受的转换)
。但当我们写成 (2) 这种形式时,程序是没问题的,可以正常运行。出现这种问题是因为this指针。
我们设计的函数是这样的ostream& operator<<(ostream& os);
,但实际上是这样的ostream& operator<<(AA* this, ostream& os);
this 指针已经强占了第一个形参的位置,但我们输入的是cout << bb
,这样传参就弄反了,所以要写成bb << cout
才能正确调用。
虽然上述代码我们已经基本实现输入输出运算符的重载了,但是这只是一个阉割版本,毕竟bb << cout
这种写法是没人认可的,最重要的是这种写法没法实现连续输出的。所以想要完美实现还是得使用友元函数来实现。