1. 函数重载
1.1 参数的默认值
- C++中带参的函数在声明时,可以给形参赋值作为参数的默认值。当主函数调用该函数时,若传入的参数不足,子函数可以自动将默认参数补齐参数列表,执行子函数;
- 默认值是从右到左开始传入,即优先使用传参,若传参用完时才使用默认参数。若使用了一个默认参数,则参数列表中其后的参数都将使用默认参数;
int func1(int a, int b = 5); //函数的声明,b的默认参数值为5
int func2(int a = 0, int b = 5, int c)//不合法,若传参小于3个,会优先取代a和b的默认值,此时c无默认值,则意味着缺少参数;
int main()
{
printf("%d\n", func1(10)); //输出值为15
}
int func1(int a, int b)
{
return a + b;
}
int func2(int a, int b, int c)
{
return a + b + c;
}
1.2 占位参数
- 参数占位就是在参数定义时,参数列表中的只有参数类型而无参数名;
- 由于占位参数无参数名,所以函数体内部无法使用占位参数,占位参数是为了兼容C语言;
int func(int a, int ) //第二个参数为占位参数
{
return a;
}
int main()
{
printf("%d\n", func(5, 10)); //输出 5
return 0;
}
1.3 重载函数
1.3.1 函数重载概述
- C++中的函数重载就是函数名相同、参数列表不同(参数数量不同/类型不同/顺序不同)、返回值与函数体无关的多个同名函数;
int func()
{
return 0;
}
char func(int a) //参数数量不同
{
return a;
}
double func(int a, char b) //参数数量不同
{
printf("%d\n", a);
printf("%c\n", b);
return 0.0;
}
float func(char b, int a) //参数顺序不同
{
printf("%d\n", a);
printf("%c\n", b);
return 0.0;
}
- 重载函数匹配的准则
(1)精确匹配函数名
(2)在匹配的函数名中,精确匹配参数列表
(3)当函数指针与重载一起时,会精确匹配函数指针指向的函数类型
(4)除了通过指针函数重载之外,重载函数与返回值类型无关,只与参数和函数名相关;
double func(int a) //定义函数
{
return 5.0;
}
int func(int a, char b = 0) //定义重载函数
{
return 1;
}
int main()
{
typedef int (*FUNC)(int); //定义函数指针
FUNC p = func; //会报错,因为重载函数类型的候选者中,没有返回值类型可以匹配上函数指针的,所以无法匹配重载函数
typedef double (*FUNC1)(int);
FUNC1 p1 = func; //并不会报错,从函数名到参数列表到返回值类型逐步匹配最终锁定;
double c = p1(1);
printf("c = %lf\n", c); //输出 c = 5.00000000
return 0;
}
- 重载函数之间除了名称相同之外并无其他关系,各个函数之间存储的地址也不同,因此只通过函数名而忽视参数列表来访问该函数,编译器会报错;
int func(int a) //定义函数
{
return 0;
}
int func(int a, int b) //重载函数
{
return 0;
}
int main()
{
printf("%p\n", (int (*)(int, int))func); //打印两个重载函数的地址
printf("%p\n", (int (*)(int))func); //输出的结果并不相同,说明重载函数并非同一个函数
return 0;
}
1.3.2 重载函数的使用
- 重载函数与默认参数
当重载函数的参数中有默认参数时,则无法确认调用的是哪个参数时,就会报错;
int func(int a, int b = 5) //函数含有默认参数
{
return a + b;
}
int func(int a) //重载函数
{
return a;
}
int main(int argc, char **argv)
{
int n = func(1); //编译器会报错,因为两个函数都可以传入这个参数,无法确认调用的时哪个函数
printf("%d\n", n);
}
- 函数功能的扩展
通过相同名称而不同参数,来扩展同名函数的功能,本质上是不同函数,但使用时可能通过名字可知功能上的类似;
/*
*strcpy函数可以用来赋值字符串,但无法限制赋值的长度
*strncpy函数可以通过设置来限制赋值字符串的长度
*可以通过重载函数的方法,根据strncpy的函数来扩展strcpy函数的用法
*也省略了strncpy的调用
*/
// char *strcpy(char *dest, const char *src);
// char *strncpy(char *dest, const char *src, size_t n);
char * strcpy(char *dest, const char *src, size_t n ) //通过重载方式扩展strcpy的功能
{
return strncpy(dest, src, n); //其函数内部本质是调用了strncpy函数
}
int main()
{
char *str = "adfhajdhfjkhdfkjfkjfabcde";
char buf[10] = {0};
strcpy(buf, str); //此处会报错,因为赋值的字符串超出了数组的长度
strcpy(buf, str, 10); //由于扩展了功能此处不会报错,会输出前10个字符
printf("%s\n", buf);
}
- C编译方式无法编译重载函数
(1)因为再C++中,编译后的函数符号(函数名)取决于函数名和函数的参数共同决定,因此重载函数之间编译后的函数名也不相同;
(2)C编译方式不支持重载函数功能,因此编译后会造成函数符号同名,调用函数时就无法确定具体的函数,因此会报错。
2. 基本操作符的重载
2.1 重载运算操作符
- 重载不仅可以作用于函数之间,也可以发生在操作符之间,
+、 -、 *、 /
等;操作符重载也同函数重载一样,只在相同作用域中发生,即在类中重载则只能在类中使用。 - 若定义在全局中,将该重载函数设置为类中的友元函数,就可以类中也能使用。若全局与类中定义了相同的操作符重载函数,则默认使用类中的重载。
- 语法:将
operator 操作符
作为函数名,就可以实现对操作符的重载
/*
*本例的目的是实现复数的运算
*由于原有操作符+-等无法实现复数实部和虚部的运算,因此通过重载的方式来扩展这个功能
*复数的实部和虚部通过类的两个成员变量实现
*/
class complex{ //定义复数类
private:
int a; //假设为实部
int b; //假设为虚部
public:
complex(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
}
complex& operator +(const complex& s)//重载操作符函数的定义
{
complex s1;
s1.a = a + s.a; //实部与实部相加
s1.b = b + s.b; //虚部+虚部
return s1;
}
int getA() //功能函数
{
return a;
}
int getB()
{
return b;
}
~complex(){}; //析构函数
friend complex& operator -(const complex& s1, const complex& s2); //将全局操作符重载函数设为友元函数
};
complex& operator -(const complex& s1, const complex& s2) //在全局作用域中定义操作符重载函数
{
complex s3;
s3.a = s1.a - s2.a;
s3.b = s1.b - s2.b;
return s3;
}
int main()
{
complex s1(10, 20);
complex s2(3, 6);
complex s3 = s1 + s2; //使用类中操作符重载函数来进行复数运算
printf("a = %d\tb = %d\n", s3.getA(), s3.getB());
complex s4 = s1 - s2; //使用全局操作符重载函数来进行复数运算
printf("a = %d\tb = %d\n", s4.getA(), s4.getB());
}
2.2 重载赋值操作符
- C++标准库中默认提供了不同类型的赋值操作符的重载函数,这意味着相同类型之间都可以进行赋值;
- 但库提供的重载是浅层的赋值,像拷贝构造函数一样,若要进行深层赋值就需要通过重载赋值操作符;
- 赋值操作符与拷贝构造函数具有相同的意义,区别在于赋值操作符用于已经定义好的两个对象之间的赋值,而拷贝构造函数用于一个对象对另一个对象构造时的初始化。
class cls{ //定义类
int* m_p; //成员变量
public:
cls(){ //构造函数
m_p = new int;
}
cls(int i){
m_p = new int(i);
}
/* 若无拷贝构造函数和赋值操作符重载函数,这个程序会报内存错误
//因为此时编译器会调用系统默认的拷贝构造函数,
//析构函数释放空间时会释放两次同一块内存
cls(const cls &s)
{
m_p = new int(*(s.m_p));
}
cls& operator = (const cls &s)
{
if (this != &s)
{
delete m_p;
m_p = new int(*(s.m_p));
}
return *this;
}
*/
int* getP()
{
return m_p;
}
~cls(){
delete m_p;
cout<<"这是析构函数" << endl;
};
};
int main()
{
cls s1 = 1;
cls s2;
s2 = s1; //若无赋值操作符重载函数,此处会报错
cls s3(s1); //若无自定义拷贝构造函数,此处会报错
}
2.3 重载数组操作符
string
类对象也可以通过数组下标的方式来访问其中的字符,因为C++标准库中数组操作符[]
通过重载,使其具有了该功能。
int main()
{
string str = "a1b2c3d4";
int cnt = 0;
for (int i = 0; i < str.length(); i ++)
{
if (isdigit (str[i])) //通过下标访问str中的字符串
{
cnt++; //如果是数字字符,cnt++
}
}
cout << cnt << endl; //输出4
}
- 数组下标访问实际是指针访问
a[i] = *(a + i) = *(i + a) = i[a]
- 重载数组访问操作符
(1)因为要访问数组类中的成员变量,因此只能通过类的成员函数重载,不能通过全局函数;
(2)重载只能有且仅有一个参数;
(3)可以通过重载传入不同类型的参数;
class arrayReload{
int a[5];
public:
arrayReload()
{
for(int i = 0; i < 5; i ++)
{
a[i] = i;
}
}
int& operator [](int i) //数组访问操作符重载函数
{
return a[i]; //引用是为了可以赋值给a[i];
}
int& operator [](string str) //重载后可以通过字符串访问数组中的元素
{
if (str == "1st")
{
return a[0];
}
if (str == "2nd" )
{
return a[1];
}
}
};
int main()
{
arrayReload s1;
cout << s1[0] << endl; //通过下标访问
s1[0] = 10; //通过数组访问操作符给数组赋值
cout << s1["1st"] << endl; //通过字符串访问数组元素
}
2.4 重载函数操作符
- 函数对象就是通过定义一个类,通过定义该类的对象,用来取代函数的作用;
- 函数的对象本质上就是重载函数调用操作符
()
/*
*目的:每调用一次函数,就可以获得一个斐波那契数组的元素,并且该函数可以重复调用
*/
class cls{ //通过函数对象方法来实现要求
int a0;
int a1;
public:
cls(){
a0 = 0; //默认初始值为0
a1 = 1;
}
cls(int n){ //指定数列中第n项作为初始值
a0 = 0;
a1 = 1;
for (int i = 1; i < n; i ++)
{
int ret = a1;
a1 = a0 + a1;
a0 = ret;
}
}
int operator()() //重载函数调用操作符
{
int ret;
ret = a1;
a1 = a0 + a1;
a0 = ret;
return ret;
}
~cls(){};
};
int test() //通过函数方法来实现要求
{
int ret;
static int a0 = 0; //定义静态局部变量
static int a1 = 1;
ret = a1;
a1 = a0 + a1;
a0 = ret;
return ret;
}
int main()
{
for (int i = 0; i < 10; i ++)
{
cout << test() << endl;
}
cls s1;
for (int i = 0; i < 10; i ++)
{
cout << s1.operator()() << endl;
}
}
- 上例中
test
函数可以基本实现功能,但一旦开始就无法从头开始调用,也无法指定某个数字作为数组初始项。因此可以通过函数对象来实现函数无法实现的功能
3. 特殊操作符的重载
3.1 智能指针
3.1.1 普通指针的缺点
- 无法控制申请的内存的生命周期,需要手动才能释放;
- 若多个对象指向一个内存空间,则结束时会出现重复释放造成内存错误;
- 当对指针进行运算时,可能会出现野指针的情况;
3.1.2 重载指针操作符
- 为了解决上面的问题,C++可以通过定义类的方式来实现智能指针
- 智能指针本质通过定义指针类对象,来代替原来的指针;
- 在定义的指针类中,通过成员函数和指针操作符的重载,对普通指针的缺点进行改进;
- 指针操作符重载只能在类中进行,且不能包含参数,目前定义一个指针类只能面向一种类型的指针;
- 智能指针只能指向堆空间的地址,不能是栈空间,因为智能指针在使用结束后会自动释放,而栈空间在函数结束后也会自动释放,因此若指向栈空间,就会被释放两次,造成内存错误。
class cls{ //定义一个类
int a;
public:
cls(){ //构造函数
a = 0;
}
cls(int i){
a = i;
}
int getA() //功能函数:输出成员变量
{
return a;
}
};
class pointer{ //定义一个指针类
cls *ps; //成员变量为cls类的指针
public:
pointer(){ //构造函数
ps = NULL;
}
pointer(const cls* p){
ps = const_cast<cls*>(p);
}
pointer(const pointer &p) //拷贝构造函数
{
ps = p.ps; //当将指向的地址复制给另一个时
const_cast<pointer&>(p).ps = NULL; //取消其中一个的指向,避免多个指针指向同一个内存空间
}
cls& operator *() //重载指针操作符
{
return *ps;
}
cls* operator ->() //重载指针操作符
{
return ps;
}
cls* getPs() //功能函数:输出成员变量
{
return ps;
}
~pointer(){ //析构函数,释放指向的空间
cout << "delete" << endl;
delete ps;
}
};
int main()
{
pointer p1 = new cls(10); //将new的地址给定义的pointer类对象p1
cout << p1->getA() << endl;
cout << p1.getPs() << endl;
pointer p2 = p1; //用p1来初始化p2
cout << p1.getPs() << endl; //此时为空,避免了指向同一块内存
cout << p2.getPs() << endl; //为p1原来指向的空间
// p2++; //报错,避免了野指针
}
3.2 重载逻辑操作符
3.2.1 逻辑操作符概述
- 逻辑操作符用来进行逻辑判断,其参数只含有两种语义
true
和false
(C中无bool
类型,用0和非0表示),其结果也是两种语义中的一种; - 逻辑操作符具有短路属性,即当判断第一个参数就可知道结果后,就不再判断第二个参数。
int func(int i)
{
cout << i << endl;
return i;
}
int main()
{
int a = 0;
int b = 1;
if(func(0) && func(1)) //由于func(0) == 0;因此不会判断func(1),输出func(0)的值后直接结束判断
{
cout << "ture" << endl;
}
else
{
cout << "false" << endl; //输出false
}
if(func(0) || func(1)) //由于func(0) == 0;因此判断完后会继续判断func(1),因此输出为 0, 1;
{
cout << "ture" << endl; //输出为true
}
else
{
cout << "false" << endl;
}
}
3.2.2 逻辑操作符的重载
- 通过重载逻辑操作符,可以实现对象之间的逻辑判断
- 但重载的逻辑操作符会丢失短路属性,无法完全实现原生语义
class cls{ //定义类
int a;
public:
cls(){ //构造函数
a = 0;
}
cls(int i){
a = i;
}
int getA() const //获取成员变量函数
{
return a;
}
};
cls func(cls s1) //区别显示函数
{
cout << s1.getA() << endl;
return s1;
}
bool operator&& (const cls&s1, const cls& s2) //逻辑操作符重载函数
{
return s1.getA() && s2.getA();
}
bool operator|| (const cls&s1, const cls& s2)
{
return s1.getA() || s2.getA();
}
int main()
{
cls s1(0); //定义cls类对象
cls s2(1);
if (func(s1) && func(s2)) //进行对象之间的逻辑判断,输出1, 0
{
cout << "true" << endl;
}
else
{
cout << "false" << endl; //结果是flase
}
if (func(s2) || func(s1)) //进行对象之间逻辑判断,输出 0, 1
{
cout << "true" << endl; //结果是true
}
else
{
cout << "false" << endl;
}
}
- 程序分析:上例中的逻辑判断无短路属性,因为重载后
if (func(s1) && func(s2))
本质为operator&& (const cls&s1, const cls& s2)
,此从在函数是在对参数的进行调用后进行逻辑,因此必须先明确参数的值,而参数的调用先后顺序无法确定,因此不具有短路属性。
注意:在实际工程中应尽量避免重载逻辑操作符
(1)可以通过重载比较操作符来进行判断,即将参数与true进行判断;
(2)也可以通过成员函数来代替逻辑操作符重载函数;
3.3 重载逗号操作符
3.3.1 逗号操作符概述
- 逗号操作符可以将多个表达式连接成一个表达式,计算值的顺序是从左到右;
- 前n-1个表达式可以没有返回值,连接起来的n个表达式最终值为最后一个表达式的值。
void func(int i)
{
cout << "i = " << i << endl;
}
int main()
{
int a[3][3] = { //定义多维数组
(1, 2, 3),
(4, 5, 6),
(7, 8, 9),
};
int i = 0;
int j = 0;
while(i < 5)
func(i), //输出01234
i++; //因为此处是逗号,所以会从左向右的执行,相当于func(i); i++;
for (i = 0; i < 3; i ++)
{
for (j = 0; j < 3; j ++)
{
cout << a[i][j] << endl; //输出369 000 000
} //因为三位数组的初始化使用(),并通过,分隔
} //因此每一行只保留了最后一个值,因此相当于只初始化了三个值
(i, j) = 6;
cout << "i = " << i << endl; //输出为3,因为for循环使i变成了3
cout << "j = " << j << endl; //输出为6, 因为逗号从左向右执行,因此最终值为最右值。
return 0;
}
3.3.2 逗号操作符的重载
- 通过操作符重载,可以实现通过逗号操作符连接多个对象的操作;
- 逗号操作符重载与逻辑操作符重载一样,无法完全实现原生语义,因为逗号操作符无法保留从左到右计算值的顺序;
- 实际上,使用原生的逗号操作符就可以实现对象之间的连接,并且满足从左向右的顺序,因此无需重载逗号操作符。
class cls{
int a;
public:
cls(int i)
{
a = i;
}
int getA()
{
return a;
}
};
cls& operator ,( const cls& s1, const cls& s2) //重载逗号操作符函数
{
return const_cast <cls &>(s2);
}
cls func(cls i)
{
cout << "i = " << i.getA() << endl;
return i;
}
int main()
{
cls s1(10);
cls s2(20);
cls ss = (func(s1), func(s2)); //输出为20, 10;但按照从左到右因为10, 20
cout << ss.getA() << endl; //输出为20
return 0;
}
3.4 重载前置和后置操作符
3.4.1 基本概述
- 在理论上,前置操作符
++i
是使用i+1后的值,而i++
是先使用i的值后,再将i+1; - 但在实际工程中,原型的前置与后置在编译后无区别,是因为编译器进行了优化;
- 优化使得效率更高,但也同时失去了C/C++的原生语义,不可能通过反汇编得到源程序。
3.4.2 重载前置和后置操作符
- 前置操作符的本质是将i+1后再使用,使用的是i+1的值;后置本质是将i保存再一个局部变量中,使用局部变量的值,再将i+1;
- 重载前置操作符可以通过
operator++ ()
,重载后置需要加占位参数operator ++(int)
; - 由于是自增或自减,因此前置返回值必须是本身,而后置由于生成一个临时变量,因此返回值是同类型的临时变量;
- 虽然在原型中,前置和后置在编译时无区别,但重载后进行对象运算时前置和后置是有区别的,因为后置需要调用栈新生成一个临时对象,因此效率比前置低,在实际开发中,对于对象应尽量使用前置操作符。
class cls{
int a;
public:
cls(int i)
{
a = i;
}
int getA()
{
return a;
}
cls & operator ++() //重载前置操作符
{
++a;
return *this; //注意此处返回值为对象本身,因此用引用
}
cls operator ++(int) 重载后置操作符
{
cls ret(a);
a++;
return ret; //此处返回值是一个局部对象,因此不能用引用,否则当释放局部对象内存时,引用就会变成野指针。
}
};
int main()
{
cls s1(0);
cout << s1.getA() << endl; //输出0
cls s2 = s1++;
cout << s2.getA() << endl; //输出0
cout << s1.getA() << endl; //输出1
s1 = 0;
cls s3 = ++s1;
cout << s3.getA() << endl; //输出1
cout << s1.getA() << endl; //输出1
}
3.5 类的类型转换
3.5.1 基本类型的转换
- 类型之间的转换并不改变数据在内存中二进制的存储方式,改变的只是编译器对于数据的解析方式;
- 基本类型之间会进行隐式类型转换,默认原则为小转大
- 当两个类型不同的值进行运算时,会默认将小类型转为大类型再进行运算;
int main()
{
short s = 'a';
unsigned int ui = 1000;
int i = -2000;
double d = i;
cout << "d = " << d << endl; //输出为-2000;因为int->double是小转大;
cout << "ui = " << ui << endl; //输出为1000;
cout << "ui + i = " << ui + i << endl; //输出乱码,因为i会默认转换为ui类型,转换后通过unsigned int 的解析方式编程了乱码;
cout << "sizeof(s + 'b') = " << sizeof(s + 'b') << endl; //输出为4;按照小转大原则应输出为2,但编译器为了执行效率,直接转为4个字节。
return 0;
}
3.5.2 普通类型转换到类类型
- 编译器支持普通类型隐式转换成类类型,前提是类中有转换构造函数,且该转换构造函数的参数类型与要转的普通类型相同
- 转换构造函数就是有一个参数的构造函数,转换时编译器会自动将普通类型当做传入的参数来构造对象,就相当于将普通类型转为类类型;
class cls{
int a;
public:
cls()
{
a = 0;
}
cls(int i)
{
a = i;
}
int getA()
{
return a;
}
};
int main()
{
cls s1 = 10; //将int类型转换为cls类型
cout << s1.getA() << endl;
}
- 普通类型转换到类类型是编译器的优化处理,但实际中通常禁止将普通类型转为类类型,因此可以在转换构造函数前加
explicit
关键字禁止编译器的自动优化; - 加了
explicit
关键字后,编译器就不会自动优化,但若仍需要将转换,可通过强制类型转换的方式;
class cls{
int a;
public:
cls(){
a = 0;
}
explicit cls(int i) //添加explicit关键字,禁止编译器隐式转换普通类型与类类型
{
a = i;
}
int getA()
{
return a;
}
cls operator +(const cls& s)
{
cls ret;
ret.a = this->a + s.a;
return ret;
}
};
int main()
{
cls s1;
s1 = cls(10); //强制类型转换,将int->cls,实际上是手动调用构造函数生成一个临时对象
cout << s1.getA() << endl;
cls s2;
s2 = s1 + static_cast<cls>(15); //强制类型转换,对象之间的运算
cout << s2.getA() << endl;
}
3.5.3 类类型转换到其他类型
- 若要将类类型转换到普通类型或其他类类型,可以在类中定义类型转换函数来实现,当类型不一致时会自动调用类型转换函数;
operator type(){type ret; return ret;}
- 当源类中定义了类型转换函数,且转换的目标类的转换构造函数未加
explicit
关键字,则会报错。因为编译器无法判断是否使用哪一个函数进行转换,因此两个函数同时作用时会发生冲突;
class cls; //声明cls类
class cls1{ //定义一个类cls1
int a;
public:
cls1()
{
a = 0;
}
explicit cls1(int i) //转换构造函数
{
a = i;
}
cls1(const cls& s) //转换构造函数,可以接收cls类的对象;若不加explicit关键字,会与cls类中的类型转换函数发生冲突
{
a = 0;
}
int getA()
{
return a;
}
};
class cls{ //定义第二个类cls
int a;
public:
cls(){
a = 0;
}
explicit cls(int i)
{
a = i;
}
int getA()
{
return a;
}
operator int() //类型转换函数,将cls类型转换为int类型
{
return a;
}
operator cls1() //类型转换函数,将cls类型转换为cls1类型
{
cls1 ret(10);
return ret;
}
};
int main()
{
int a;
cls s1;
a = s1; //将cls类型对象赋值给int类型,实际上是s2 = s1.operator int()
cout << s1.getA() << endl;
cout << a << endl;
cls1 s2;
s2 = s1; //将cls类型对象赋值给s2类型,实际上是s2 = s1.operator cls1()
cout << s2.getA() << endl;
}
- 转换构造函数可以通过
explicit
来禁止隐式转换,但无法禁止自动调用类型转换函数,而在C++中应尽量减少编译器进行隐式转换的操作,因此通常采用定义并调用成员函数的方式进行显示类型转换;
//将上例中的operator cls1();函数修改为
cls1 toCls1()
{
cls1 s1;
return s1
}
//使用成员函数调用的方式进行类型转换
cls s1;
cls1 s2;
s2 = s1.toCls1();