C++描述符delete与override


delete与override

优先使用delete关键字删除函数而非private却又不实现的函数

为什么使用delete?

​ 不想让别的开发者调用特定的函数,你只需要不声明这个函数就可以了。但有时候 C++为你声明了一些函数,如果你想阻止客户调用这些函数,就不是那么容易的事了。

​ 这种情况只有对“特殊的成员函数”才会出现,即这个成员函数是需要的时候C++自动生成的。

在C++98中阻止这类函数被使用的方法是将这些函数声明为private,并且不定义它们。

​ 将这些函数声明为私有来阻止客户调用他们。故意不定义它们是因为,如果有函数访问这些
函数(通过成员函数或者友好类)在链接的时候会导致没有定义而触发的错误。

​ 在C++11中,有一个更好的方法可以基本上实现同样的功能:用 =delete标识拷贝复制函数
和拷贝赋值函数为删除的函数deleted functions 。

delete的优点

​ 删除函数一个重要的优势是任何函数都可以是删除的,然而仅有成员函数才可以是私有的。

防止普通非成员函数中的隐式转化

​ C++继承于C意味着,很多其他类型被隐式的转换为in 类型,但是有些调用可以编译但是没有任何意义.

​ 举个例子,加入我们有个非成员函数,以一个整数位参数,然后返回这个参数是不是幸运数字:

bool isLucky(int number);

isLucky('a');
isLucky(true);
isLucky(3.5);

​ 如果幸运数字一定要是一个整数,我们希望能到阻止上面那种形式的调用。

​ 完成这个任务的一个方法是为想被排除出去的类型的重载函数声明为删除的:

bool isLucky(int number);

bool isLucky(char)=delete;
bool isLucky(bool)=delete;
bool isLucky(double)=delete;

​ 如果给float一个转换为int或者double的可能性, C++总是倾向于转化为double的。以float类型调用isLucky总是调用对应的double重载,而不是int类型的那个重载。结果就是将double类型的重载删除将会阻止float类型的调用编译。

阻止应该被禁用的模板实现

​ 假设你需要使用一个内嵌指针的模板:

template<typename T>
void processPointer(T* ptr);

​ 但是在指针家族中,有两个特殊的指针。

一个是 void* 指针,因为没有办法对它们解析引用,递增或者递减。

另一个是 char* 指针,因为它们往往表示指向C类型的字符串,而不是指向独立字符的指针。

​ 这些特殊情况经常需要特殊处理。如果希望不能以 void* 或者 char* 为参数调用该函数,只需要删除这些实现:

template<>
void processPointer<void*>(void*)=delete;

template<>
void processPointer<void*>(void*)=delete;

现在,使用 void* 或者 char* 调用该函数都是无效的,但使用 const void* 或者 const char* 调用也需要是无效的, 所以这些实现也需要被删除。(略)

如果类内部有一个函数模板,想通过声明它们为私有来禁止某些实现,是做不到的,因为赋予一个成员函数模板的某种特殊情况下的拥有不同于模板主体的访问权限是不可能的。

(可以声明 void* 和 char* 的版本却不实现它们 )

class MyClass
{
public:
	template<typename T>
	void function(T t);
private:
	template<>				//错误!!!
	void function(char);	
};

并且模板的特殊情况必须要写在命名空间的作用域内,而不是类的作用域内。

不过,这个问题对于删除函数是不存在的,它们也可以在类外被声明为是被删除的(也就是在命名空间的作用域内)

delete的注意事项

​ 删除的函数不能通过任何方式被使用

​ 方便起见,删除函数被声明为公有的,而不是私有的。这样设计的原因是,当客户端程序尝试使用一个成员函数的时候, C++会在检查删除状态之前检查可访问权限。当客户端代码尝试访问一个删除的私有函数时,一些编译器仅仅会警报该函数为私有,尽管这里函数的可访问性并不本质上影响它是否可以被使用。当把私有未定义的函数改为对应的删除函数时,牢记这一点是很有意义的,因为使这个函数为公有的可以产生更易读的错误信息。

小结:

  1. 优先使用删除函数而不是私有而不定义的函数
  2. 任何函数都可以被声明为删除,包括非成员函数和模板实现

扩展:

对于成员函数:

  1. 被delete了还是可以重载的
  2. 被delete了,里面写个默认参数没问题
  3. delete函数可以被继承的!!!(看IDE的报错)(但不能用)
  4. 私有的delete函数被类用户使用了会以“使用私有成员函数”的名义报错
  5. delete函数不能够被定义,只能够被声明

对于普通函数:

  1. 可以防止不想要的隐式转化
  2. 但删除一种类型的,可能会影响不止一种类型的隐式转化(会导致其他类型的不明确,ambiguous)
  3. 模板类的删除方式有两种,一种使用template,一种直接写出(暂时没发现会出啥问题)

使用override关键字声明覆盖的函数

在C++98中,如果要发生函数的覆盖,必须满足以下几个条件:

  • 基类中的函数必须是虚函数
  • 基类和派生类中函数的名字必须完全相同(析构函数除外)
  • 基类和派生类中的函数形参类型必须完全一样
  • 基类和派生类中的函数的常量特性必须完全一样
  • 基类和派生类中的函数返回值和异常声明必须是兼容的

在C++11中,还有一条:

  • 基类和派生类的引用修饰符必须完全一样

    (引用修饰符是为了实现限制成员函数仅用于左值或者右值)

class Widget
{
public:
    void doWork() &;	//只有*this为左值时,这个版本才会被调用
    void doWork() &&;	//只有*this为右值时,这个函数才会被调用
};

Widget makeWidget();	//工厂函数,返回右值
Widget w;				//正常对象(左值)

w.doWork();				//调用了Widget::doWork &
makeWidget().doWork();	//调用了Widget::doWork &&

正是因为覆盖有这么多的要求,所以一个很小的错误都可以造成很大的偏差。并且覆盖函数中出现的错误通常还是合法的,,但它导致的结果并不是你想要的。所以当你犯了某些错误的时候,你并不能依赖于编译器对你的通知。

例如下面这个例子,这些代码是完全合法的,但它一个虚函数都没有被覆盖——没有任何一个派生类的函数是和基类的对应函数绑定的。

class	Base{
public:
    virtual	void	mf1()	const;
    virtual	void	mf2(int	x);
    virtual	void	mf3()	&;
    void	mf4()	const;
};

class	Derived:	public	Base	{
public:
    virtual	void	mf1();
    virtual	void	mf2(unsigned	int	x);
    virtual	void	mf3()	&&;
    void	mf4()	const;
};

提示:

  • mf1在Base中声明常成员函数,但是在Derived中没有
  • mf2在Base中以int为参数,但是在Derived中以unsigned int为参数
  • mf3在Base中有左值修饰符,但是在Derived中是右值修饰符
  • mf4没有继承Base中的虚函数

因为声明派生类的覆盖函数很重要,又如此容易出错,所以C++11提供了一种方法来显示的标明派生类中的函数是为了改写基类的版本,为其加上override声明即可。

class	Derived:	public	Base {
public:
    virtual void mf1() override;

    virtual void mf2(unsigned int x) override;

    virtual void mf3() && override;

    virtual void mf4() const override;
};

这样,上面这些代码就无法通过编译了,并且编译器会把覆盖函数所有的问题揭露出来。

扩展:

与成员函数有关的几个描述符:

  1. 成员函数前面使用const 表示返回值为const
  2. 成员函数后面加 const表示函数不可以修改class的成员
  3. 如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。
  4. 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错

重载与覆盖

​ 覆盖是指派生类中如果存在重新定义的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致。覆盖函数和被覆盖函数只有函数体不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本。

成员函数被重载的特征:

(1)相同的范围(在同一个类中);

(2)函数名字相同(必须的,否则属于不同的函数了,更谈不上重载);

(3)参数类型或个数,至少有一种不同(如果都相同的话,就是函数的重定义了;还要注意的是:如果函数参数相同,仅返回值不同的话,不是重载函数,编译时会报“有歧义”的错误);

(4)virtual关键字可有可无(这个重载函数与虚函数一点关系没有,即使加上了也不是虚函数。如果在面试题中遇到了,纯粹是为了混淆你的思维,考察你对概念的理解)。

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

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同(这就属于重定义函数了);

(4)基类函数必须有virtual关键字(这就是虚函数了)。

令人迷惑的隐藏规则

本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆;如果基类函数有virtual关键字,就属于函数的覆盖了)。

关于虚函数与纯虚函数:

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义它为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。(这里好像有问题。。。)

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

虚函数只能借助于指针或者引用来达到多态的效果


文章作者: Immortalqx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Immortalqx !
评论
  目录