C++类中的一些细节(重载、重写、覆盖、隐藏,构造函数、析构函数、拷贝构造函数、赋值函数在继承时的一些问题)
1 函数的重载、重写(重定义)、函数覆盖及隐藏
其实函数重载与函数重写、函数覆盖和函数隐藏不是一个层面上的概念。前者是同一个类内,或者同一个函数作用域内,同名不同参数列表的函数之间的关系。而后三者是基类和派生类函数不同情况下的关系。
1.1 函数重载
正如上文说的函数的重载是指类内部,同名不同参数列表函数之间的关系。如下:
void show();
void show(int);
void show(int , double);
以上是多个同名参数列表不同的函数,这种情况就是函数重载。不同的函数返回值不作为判断函数重载的依据,比如如下两个函数会被判定为函数重定义。
void show();
int show();
编译器错误:“错误(活动) E0311 无法重载仅按返回类型区分的函数”。
不过函数重载要注意的是有默认参数和类型的隐式转换造成的问题,这里需要了解一下编译器是如何选择使用那个版本的函数的。其大致过程是
- 第一步,创建候选函数列表。其中包含与被调用函数的名称相同的函数
- 第二步,使用候选函数列表创建可行函数列表,这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参数类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配。
- 第三步,确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错(没有匹配项或者有多个匹配项都出错)
所以再重载函数的时候要注意有默认参数值和隐式转换,如下:
#include <iostream>
void show()
{
std::cout << "无参数" << std::endl;
}
void show(int n = 1)
{
std::cout << "有参数" << std::endl;
}
void show(double d)
{
std::cout << "有参数 double" << std::endl;
}
void show(double & d)
{
std::cout << "有参数 double 引用" << std::endl;
}
int mian()
{
show(); //E0308 有多个 重载函数 "show" 实例与参数列表匹配:
double d = 10;
show(d); //E0308 有多个 重载函数 "show" 实例与参数列表匹配
}
对于有默认参数值的情况不必多说,对于隐式转换则要注意在匹配的第三步编译器确认那些是最佳的。它查看为使函数调用参数与可行的时候选函数的参数匹配所需要进行的转换。通常从最佳到最差的顺序如下所述。
- 完全匹配
- 提升转换(如:char自动转换为int,float自动转换为double)
- 标准转换(如:int转换为char,long转换为double)
- 用户定义的转换(如:类种定义的转换构造函数)
进行完全匹配时,C++允许一些无关紧要的转换,如下表:
实参 | 形参 |
---|---|
Type | Type & |
Type & | Type |
Type[] | Type * |
Type(argument-list) | Type(*)(argument-list) |
Type | const Type |
Type | volatile Type |
Type * | const Type * |
Type * | volatile Type * |
1.2 函数覆盖
这个概念都是描述基类和派生类之间函数关系的。函数覆盖:是基类虚函数在派生类种被重新定义。如:
class Base
{
virtual void show()
{
...
}
}
class BasePlus : pulic Base
{
void show()
{
...
}
}
这种形式是基类函数有virtual关键字,且派生类与基类函数名相同,参数列表也相同。如果参数列表不相同的话就是函数的隐藏了。而这种类型其实就是派生类函数把基类函数隐藏了。总之就是,在派生类种重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。所以,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。如果基类声明被重载了,则应在派生类种重新定义所有的基类版本。因为如果只定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。如果不需要修改,则可以在派生类定义函数种只显示调用基类方法即可。
1.3 函数隐藏
函数隐藏则分两种情况,一种是基类有virtual关键字,但参数里列表不同。另一种是在基类中无virtual关键字,但是派生类中有同名函数(参数列表相不相同都无所谓)。这时如果派生类对象调用该函数则执行派生类的同名函数,所有基类的同名函数都会被隐藏。此时派生类是不能调用基类的任何同名函数的。如下:
class Base
{
public:
void show()
{
std::cout << "Base is running!" << std::endl;
}
void show(int n)
{
std::cout << "Base : " << n << std::endl;
}
};
class BasePlus:public Base
{
public:
void show()
{
std::cout << "BasePlus is running !" << std::endl;
}
};
int main()
{
BasePlus ob;
ob.show();
// ob.show(1); // 错误 1 error C2660: “BasePlus::show”: 函数不接受 1 个参数
system("pause");
return 0;
}
如果基类对象调用该函数则执行基类函数。如果是指向派生类对象的基类引用或指针则调用基类的函数。
1.4 小结
其实了解一下类的动态绑定就没有这么麻烦了,本来就是基类对象执行基类的函数,派生类对象执行派生类函数。如果基类有virtual关键字,则基类中则会有一个指向虚函数表的指针,此时如果派生类定义了同名函数,则虚函数标中的函数指针则定位到了派生类的函数。这就是函数覆盖的现象。至于函数隐藏则更简单了,没有virtual关键字的情况下都是静态联编,自然是基类对象调用基类函数,派生类对象调用派生类函数了。如果有virtual关键字时,但是参数列表不一样,自然会调用派生类的函数了。
2 构造函数、析构函数、拷贝构造函数、重载=在继承的时候的一些问题
2.1 构造函数和拷贝构造函数
构造函数不能是虚函数。创建派生类对象时,会先调用基类的构造函数,然后调用派生类的构造函数。如果基类没有默认构造函数,则基类需要通过参数列表显示地调用基类的构造函数,并传递参数给基类构造函数。
class Base_1
{
public:
Base_1()
{
std::cout << "Base_1 defaut Constructor" << std::endl;
}
};
class Base_2
{
public:
Base_2(int n)
{
std::cout << "Base_2 self define Constructor:" << n << std::endl;
}
};
class BasePlus_1 : public Base_1
{
public:
BasePlus_1()
{
std::cout << "BasePlus_1 defaut Constructor" << std::endl;
}
};
class BasePlus_2 : public Base_2
{
public:
BasePlus_2():Base_2(2) // 默认构造函数
{
std::cout << "BasePlus_2 defaut Constructor" << std::endl;
}
};
class BasePlus_3 : public Base_1
{
public:
BasePlus_3(int n)
{
std::cout << "BasePlus_3 self define Constructor:" << n << std::endl;
}
};
class BasePlus_4 : public Base_2
{
public:
BasePlus_4(int n):Base_2(n)
{
std::cout << "BasePlus_4 self define Constructor : " << n << std::endl;
}
};
int main()
{
BasePlus_1 ob_1;
std::cout << "------------------------------------" << std::endl;
BasePlus_2 ob_2;
std::cout << "------------------------------------" << std::endl;
BasePlus_3 ob_3(3);
std::cout << "------------------------------------" << std::endl;
BasePlus_4 ob_4(4);
system("pause");
return 0;
}
运行结果如下:
同样的拷贝构造函数也是如此的。不可是虚拟的,不可被继承。调用机制和构造函数一样。拷贝构造函数通常会在一下情况被调用:
- 用一个对象来初始化的时候
Base ob_0;
Base ob_1 = ob_0; // 此时会调用拷贝构造函数
- 用一个对象来构造另一个对象的时候
Base ob_0;
Base ob_1(ob_0); // 此时会调用拷贝构造函数
- 传递对象的时候
Base Base::fun()
{
....
return * this; // 此时会调用拷贝构造函数
}
- 创建临时对象时
Base ob_0;
Base(ob_0); // 此时会调用拷贝构造函数,该对象为临时对象
2.2 析构函数
析构函数应当是虚函数,除非类不用作基类。如果派生类对象销毁时,会调用派生类析构函数然后调用基类析构函数,这样运行时正确的。但如果基类的引用或指针指向派生类对象的时候,这时候该对象销毁的时候会只调用基类的析构函数,此时如果派生类构造函数里动态分配了内存的话,这部分内存就泄漏了。如果基类的析构函数是虚的,则会先调用派生类析构函数,然后再调用基类的析构函数。
class Base_3
{
public:
~Base_3()
{
std::cout << "Base_3 destructor" << std::endl;
}
};
class Base_4
{
public:
virtual ~Base_4()
{
std::cout << "Base_3 destructor" << std::endl;
}
};
class BasePlus_5 : public Base_3
{
public:
~BasePlus_5()
{
std::cout << "BasePlus_5 destructor" << std::endl;
}
};
class BasePlus_6 : public Base_4
{
public:
~BasePlus_6()
{
std::cout << "BasePlus_6 destructor" << std::endl;
}
};
int main()
{
Base_3 * pBase_3= new BasePlus_5;
Base_4 * pBase_4 = new BasePlus_6;
delete pBase_3;
std::cout << "------------------------------------" << std::endl;
delete pBase_4;
system("pause");
return 0;
}
运行结果如下:
通常应给基类提供一个虚析构函数,即使它并不需要析构函数
2.2 重载操作符=函数(赋值函数)
=操作符号与拷贝构造函数的调用情况是相同的,如果派生类没有重载该操作符的时候。会调用基类的操作符函数(无论是重载的还是默认的),如果派生类定义了则会调用派生类重载的函数。所以为了保持基类部分数据的正确赋值,必须在派生类的重载函数中显示调用基类的重载函数。如:
BasePlus & BasePlus::operator= (const BasePlus & ob)
{
Base::operator= (ob); // 显示调用基类构造函数
...
}
这里唯一不同的是,operator=(),函数可以是虚函数。那再什么情况下我们需要operator=是虚函数呢?只有在两个指向派生类对象的基类引用赋值的时候需要赋值派生类部分数据的时候我们希望通过多态调用派生类的赋值函数。但是很不幸目前这种操作实现不了,如下:
virtual Base & operator= (const Base & ob);
BasePlus & operator= (const BasePlus & ob);
虽然基类赋值函数是虚函数,但是派生类函数并不是该函数的重新定义。也许是编译器内部并没有把virtual Base & operator= (const Base & ob)映射到虚函数表的原因,总之两个指向派生类对象的基类引用赋值的时候调用的是基类的构造函数。如下:
// 基类
class Base
{
public:
virtual Base & operator= (const Base & ob)
{
std::cout << "BasePlus operator = is running" << std::endl;
return * this;
}
};
// 派生类
class BasePlus : public Base
{
BasePlus & operator= (const BasePlus & ob)
{
Base::operator= (ob);// 显示调用基类函数
std::cout << "BasePlus operator = is running" << std::endl;
return * this;
}
};
int main()
{
BasePlus objBasePlus_0;
BasePlus objBasePlus_1;
Base & obj_0 = objBasePlus_0;
Base & obj_1 = objBasePlus_1;
obj_0 = obj_1;
system("pause");
return 0;
}
输出结果如下:
C++类中的一些细节(重载、重写、覆盖、隐藏,构造函数、析构函数、拷贝构造函数、赋值函数在继承时的一些问题)的更多相关文章
- c++类大四个默认函数-构造函数 析构函数 拷贝构造函数 赋值构造函数
每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数).对于任意一个类A,如果不编写上述函数,C++编译器将自动为A 产生四个缺省的函数,例如: A ...
- CPP_类默认函数:构造函数,拷贝构造函数,赋值函数和析构函数
类默认函数:构造函数,拷贝构造函数,赋值函数和析构函数 // person.h #ifndef _PERSON_H_ #define _PERSON_H_ class Person{ public : ...
- C++中构造函数,拷贝构造函数和赋值函数的区别和实现
C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法.下面就详细比较下三者之间的区别以及它们的具体实现 1.构造函数 构造函数是一种特殊的类成员函数,是当创建一个类的对象 ...
- C++中的构造函数,拷贝构造函数,赋值函数
C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法.下面就详细比较下三者之间的区别以及它们的具体实现 1.构造函数 构造函数是一种特殊的类成员函数,是当创建一个类的对象 ...
- C++类构造函数、拷贝构造函数、复制构造函数、复制构造函数、构造函数显示调用和隐式调用
一. 构造函数是干什么的 class Counter { public: // 类Counter的构造函数 // 特点:以类名作为函数名,无返回 ...
- 类string的构造函数、拷贝构造函数和析构函数
原文:http://www.cnblogs.com/Laokong-ServiceStation/archive/2011/04/19/2020402.html 类string的构造函数.拷贝构造 ...
- C++中的构造函数,拷贝构造函数和赋值运算
关于C++中的构造函数,拷贝构造函数和赋值运算,以前看过一篇<高质量C++/C编程指南>的文章中介绍的很清楚,网上能搜索到,如果想详细了解这方面的知识可以参看一下这篇文章. 常见的给对象赋 ...
- C++的转换构造函数、拷贝构造函数、赋值运算符重载
1 转换构造函数 C++的转换构造函数是只有一个参数的构造函数.当程序试图将一个其他类型的对象或基本类型值赋给该类的一个待初始化对象时(如Person p="Dean";) ...
- 深入C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程总结
转自 http://www.jb51.net/article/37527.htm,感谢作者 #include "stdafx.h" #include <iostre ...
随机推荐
- 在linux中创建新用户-再次安装python
原来的阿里云python软件安装错了,用了root安装软件,搞得我后面的软件全部都要用root,软连接也搞不定,卸载也不好卸载.只能格式化,实例什么的都不用重建,系统也不用安装,直接创建用户就行了,磁 ...
- MyBatis 分页插件PageHelper 后台报错
今天遇到一个问题,使用MyBatis 分页插件PageHelper 进行排序分页后,能正常返回正确的结果,但后台却一直在报错 net.sf.jsqlparser.parser.ParseExcepti ...
- python 2 和python 3 中的编码对比
在 Python 中,不论是 Python2 还是 Python3 中,总体上说,字符都只有两大类: 通用的 Unicode 字符: (unicode 被编码后的)某种编码类型的字符,比如 UTF-8 ...
- 谷歌插件学习笔记:把iframe干掉……
好久不写博客了,感觉自己变得越来越懒了,是没有时间吗?不是,是自己变得越来越懒了,好多东西不愿意去总结了,可能也是学的不精总结不出来什么玩意儿.不过,一切都是借口.还是坚持学习,坚持写博客吧,虽然写的 ...
- 开启防火墙如何部署k8s
你可以不关闭防火墙,只需要开启这些端口就行了MASTER节点6443* Kubernetes API server 2379-2380 etcd server client API 10250 Kub ...
- 部署LVS-NAT模式调度器
创建集群服务器 [root@proxy ~]# yum -y install ipvsadm [root@proxy ~]# ipvsadm -A -t -s wrr 添加真实服务器 [root@pr ...
- Genymotion 配置
配置Android的SDK
- 交换机配置——VTP管理交换机的VLAN配置
一.实验目的:将S1配置成VTP-Server,S2配置成VTP-Transparent,S3配置成VTP-Client,S4配置成VTP-Client 二.拓扑图如下 三.具体步骤: (1)S1交换 ...
- 关于lower_bound()和upper_bound()
关于lower_bound()和upper_bound(): 参考:关于lower_bound( )和upper_bound( )的常见用法 注意:查找的数组必须要是排好序的.因为,它们查找的方式也是 ...
- centos6升级系统内核
1.升级系统内核查看内核版本: uname -r 2.6.32-573.8.1.el6.x86_64 导入elrepo的key: rpm --import https://www.elrepo.org ...