为什么C++能做到函数重载,C语言不能?
名字改编(name mangling) - - - 见《Linux多线程服务端编程》10.3.1
例:
int func(int x, int y) {}
int func(int x, int y, int z) {}
int testfunc() {}
int main()
{
return 0;
}
g++ -C main.cpp -o main.o
nm main.o
运行结果如下:
00000000004004d6 T _Z4funcii
00000000004004e3 T _Z4funciii
00000000004004f3 T _Z8testfuncv
_Z4funcii 对应 func(int x, int y)
_Z4funciii对应 func(int x, int y, int z)
_Z是规定前缀,4是函数名的字符个数,i是参数类型int 的首字母(还有更复杂的命名,这里就不深入讨论了。)
可以看到:C++基于函数的入参类型、个数等对名称做了改编,实现了函数重载的特性。
函数重载条件
函数参数列表不同(类型、个数、顺序)
特别注意:函数返回类型不参与函数重载
通过改编的符号名找原函数名
如上例:
可以通过c++filt 命令查找符号文件对应的函数
应用场景一:工作中会遇到可执行程序依赖不同版本的动态库,导致可执行程序执行失败,这时可以通过该命令排查哪些函数不对。
例:
common.h
int add(int x, int y);
common.cpp
#include "common.h"
int add(int x, int y)
{
return x + y;
}
main.cpp
#include "common.h"
#include <iostream>
int main()
{
std::cout << add(4, 5) << std::endl;
}
通过 g++ common.cpp -fPIC -shared -o libcommon.so 生成动态库
export LD_LIBRARY_PATH=动态库所在绝对路径:$LD_LIBRARY_PATH
./main
当common.h和common.cpp的add函数发生了变更比如多了一个入参 int add(int x, int y, int z),编译出新的libcommon.so替换,但是main没有重新编译,这时候执行main会报错,这时候就可以通过c++filt找出哪些函数在调用方需要修改。
禁用名称改编
如果说不想要函数名被 改编,可以通过extern "C"的方式,让代码以C的方式编译
#ifdef __cplusplus
extern "C"
{
#endif
int func(int x, double y) {}
#ifdef __cplusplus
} // end of extern
#endif
int func(int x, int y) {}
int func(int x, int y, int z) {}
int main()
{
return 0;
}
执行结果如下:
00000000004004d6 T func
00000000004004e5 T _Z4funcii
00000000004004f2 T _Z4funciii
可以看到int func(int x, double y)并未发生名字改编
拷贝构造函数重载
注意点:①形参不能去掉引用符号,去掉在值传递的时候又需要拷贝构造临时对象,调用自身,导致无穷递归下去
②形参不能去掉const,去掉后Complex b(Complex(3, 5))这种场景,当用临时对象去构造时编译失败,因为非const引用无法绑定到临时对象
class Complex
{
public:
Complex(double dreal, double dimag)
: _dreal(dreal)
, _dimag(dimag)
{ }
// 去掉const会编译失败
Complex(const Complex& rhs)
: _dreal(rhs._dreal)
, _dimag(rhs._dimag)
{ }
void display() const
{
std::cout << "(" << _dreal <<"," << _dimag << ")" << std::endl;
}
private:
double _dreal;
double _dimag;
};
int main()
{
Complex b(Complex(3, 5));
b.display();
}
运算符重载
官方手册:https://zh.cppreference.com/w/cpp/language/operators
运算符重载规则
①重载操作符必须具有一个类类型或者是枚举类型的操作数
// 这里尝试对内置类型int进行重载,编译会报错error: ‘int operator+(int, int)’ must have an argument of class or enumerated type
int operator+(int x, int y)
{
return x + y;
}
②重载操作符不能改变他们的操作符优先级
③重载操作符不能改变操作数的个数
④不能创建一个新的运算符,例如不能定义operator** (···)来表示求幂
⑤C++要求赋值=,下标[],调用(), 和成员指向-> 操作符必须被定义为类成员操作符
不可重载运算符
成员访问运算符 | . |
成员指针访问运算符 | ., -> |
域运算符 | :: |
长度运算符 | sizeof |
条件运算符 | ?: |
预处理符号 | # |
运算符重载的方式
①普通函数的形式重载
②友元函数的形式重载
③成员函数的形式重载
// 以友元函数的形式重载运算符
class Complex
{
public:
Complex(double dreal, double dimag)
: _dreal(dreal)
, _dimag(dimag)
{ }
friend Complex operator+(const Complex & lhs, const Complex & rhs);
private:
double _dreal;
double _dimag;
};
Complex operator+(const Complex & lhs, const Complex & rhs)
{
return Complex(lhs._dreal + rhs._dreal, lhs._dimag + rhs._dimag);
}
// 以成员函数的形式重载
class Complex
{
public:
Complex(double dreal, double dimag)
: _dreal(dreal)
, _dimag(dimag)
{ }
// 相较于友元函数,成员函数第一个参数为this指针,因此只要设置一个参数作为右侧运算量
Complex operator+(const Complex & rhs)
{
return Complex(_dreal + rhs._dreal, _dimag + rhs._dimag);
}
private:
double _dreal;
double _dimag;
};
一般情况下:将双目运算符重载为友元函数,这样就可以使用交换律,比较方便,单目运算符一般重载为成员函数,因为直接对类对象本身进行操作
具体分析见 https://stackoverflow.com/questions/4622330/operator-overloading-member-function-vs-non-member-function
例1:
// 本例实现复数与整数相加,如果以成员函数重载operator+(int), Complex c3 = c1 + 10是可以的,编译器会将表达式c1 + 10转换成c1.operator+(10),但是如果是Complex c3 = 10 + c1,第一个操作数不是类类型,那么就要通过友元函数,如本例中的注释代码 friend Complex operator+(int i, const Complex &rhs);
class Complex
{
public:
Complex(double dreal, double dimag)
: _dreal(dreal)
, _dimag(dimag)
{
}
void display() const
{
if(_dreal == 0 && _dimag != 0)
cout << _dimag << "i" << endl;
else
{
cout << _dreal;
if(_dimag > 0)
cout << "+" << _dimag << "i" << endl;
else if(_dimag < 0)
cout << _dimag << "i" << endl;
else
cout << endl;
}
}
Complex operator+(const Complex &rhs)
{
return Complex(_dreal + rhs._dreal, _dimag + rhs._dimag);
}
Complex operator+(int i)
{
return Complex(_dreal + i, _dimag);
}
//friend Complex operator+(int i, const Complex &rhs);
private:
double _dreal;
double _dimag;
};
/*
Complex operator+(int i, const Complex &rhs)
{
return Complex(rhs._dreal + i, rhs._dimag);
}*/
int main()
{
Complex c1(1, 3);
c1.display();
Complex c2 = c1 + 10;
c2.display();
}
例2:
// 相比较于例1中的重载友元函数operator+(int, const Complex&),新增一个构造函数,让表达式10 + c1在执行时,将10隐式转换成Complex(10)对象,再调用operator+(const Complex& lhs, const Complex &rhs)即可。
class Complex
{
public:
Complex(double dreal, double dimag)
: _dreal(dreal)
, _dimag(dimag)
{ }
Complex(double dreal)
: _dreal(dreal)
, _dimag(0)
{ }
void display() const
{
if(_dreal == 0 && _dimag != 0)
cout << _dimag << "i" << endl;
else
{
cout << _dreal;
if(_dimag > 0)
cout << "+" << _dimag << "i" << endl;
else if(_dimag < 0)
cout << _dimag << "i" << endl;
else
cout << endl;
}
}
friend Complex operator+(const Complex &lhs, const Complex &rhs);
private:
double _dreal;
double _dimag;
};
Complex operator+(const Complex& lhs, const Complex &rhs)
{
return Complex(lhs._dreal + rhs._dreal, lhs._dimag + rhs._dimag);
}
int main()
{
Complex c1(1, 3);
c1.display();
Complex c2 = 10 + c1;
c2.display();
}
赋值运算符重载
class A {
public:
A& operator=(const A& rhs)
{
// 自赋值
if (&rhs != this) {
delete []this->_x;
this->_x = new char[strlen(rhs._x)];
strcpy(this->_x, rhs._x);
}
return *this;
}
private:
char* _x;
};
注意点:
①对于一个内含引用成员或者const成员的类,编译器并不会提供默认赋值运算符函数(详见Effective C++条款05:了解C++默默编写并调用哪些函数)
// 如本例 class A有一个引用成员_x,当进行赋值 bb = aa的时候,可以看到bb.nameValue和aa.nameValue会指向同一个string,但是C++并不允许引用指向不同的对象,bb.objectValue是个const成员,对于const成员的修改是不合法的,因此编译器不会提供默认赋值运算符函数,需要自己重载
template<class T>
class NameObject{
public:
NameObject(std::string& name, const T& value) : nameValue(name), objectValue(value)
{ }
/*
NameObject& operator=(const NameObject& rhs)
{ }*/
~NameObject()
{
}
std::string& nameValue;
const T objectValue;
};
int main()
{
std::string a("hello");
std::string b("world");
NameObject<int> aa(a, 2);
NameObject<int> bb(b, 3);
bb = aa;
}
②系统默认提供的赋值运算符是浅拷贝(同赋值拷贝函数也是浅拷贝)
下面的例子执行结果会报错double free or corruption (fasttop),可知编译器默认提供的赋值运算符是浅拷贝,即只是直接进行了赋值b._x = a._x,导致两个对象的成员指向同一个地址,析构的时候被重复释放了。因此对于这种情况需要自己重载进行深拷贝,避免double free
class A {
public:
A(const char* str) : _x(new char[strlen(str) + 1]()) { strcpy(_x, str); }
~A() { delete _x[];}
private:
char* _x;
};
int main()
{
A a("hello");
A b("world");
b = a;
}
③重载赋值运算符时返回值为该对象的引用(详见Effective C++ 条款10:令operator=返回一个reference to *this)
/* 本例可以看出如果重载oeprator=返回对象而不是引用
(1)operator=返回对象,会导致调用复制构造函数产生临时对象,会有开销,另外如果含有自己申请的堆空间且没有以深拷贝的方式重载复制构造函数会出问题
(2)(a = b) = c 这种连锁赋值场景与本意不一致
下例中返回对象结果是:a=1,b=1,c=2 返回对象的引用结果是:a=2,b=1,c=2
a = b执行完返回的是临时对象,再执行=c修改的不是a
*/
class Widget{
private:
int val;
public:
Widget(int a) : val(a) {}
Widget operator=(const Widget& rhs)
{
val = rhs.val;
return *this;
}
int getVal() { return val; }
};
int main()
{
Widget a(1),b(1),c(2);
(a = b) = c;
std::cout << "a=" << a.getVal() << ",b=" << b.getVal() << ",c=" << c.getVal();
}
④重载赋值运算符时处理自复制(详见Effective C++ 条款11:在operator=中处理自我赋值)
// 本例没有处理自复制,当进行自复制a = a时,由于*this和rhs是同一个对象,delete pb释放掉pb,该对象的pb就变成野指针了。
class Bitmap{};
class Widget{
private:
Bitmap* pb;
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
Widget a;
a = a;
前置后置++运算符重载
operator++对操作对象的状态进行了改变,因此建议以成员函数的形式进行重载
class Widget{
public:
Widget& operator++()
{
++val;
return *this;
}
// c++规定 后置++用一个int参数来区分前置后置
Widget operator++(int)
{
Widget tmp(*this);
++val;
return tmp;
}
private:
int val;
};
输入输出流运算符重载
只能重载为友元函数形式
// 对于输入输出流运算符而言,如果以成员函数形式进行重载,第一个参数是隐含的this参数,而输入输出流运算符的左操作数必须是流对象,因此输入输出流运算符只能重载为友元函数形式
class A {
friend std::ostream& operator<<(std::ostream& os,const A& a);
friend std::istream& operator>>(std::istream& is, A& a);
private:
int _x;
};
std::ostream& operator<<(std::ostream& os,const A& a)
{
os << a._x;
return os;
}
std::istream& operator>>(std::istream& is, A& a)
{
is >> a._x;
return is;
}
int main()
{
A aa;
std::istringstream ss("123");
ss >> aa;
std::cout << aa << std::endl;
}