2020年10月

一、const和成员函数的故事

const的用途有以下几种:

  1. 修饰全局、局部、成员变量
  2. 修饰成员函数

修饰变量的时候const限制了变量在整个程序运行期间都是不能修改的,而修饰成员函数的时候限制函数内不能修改数据成员,这应该是所有C++程序员都烂熟于心的准则。但除了这两条准则以外,const还有一些隐含的准则,是写代码的时候是很容易被忽视的。例如以下代码,相信很多人都无法一眼看出这段代码的问题在哪:

class Hero {
private:
    std::string name;
public:
    Hero(const std::string &name) :name(name) {
    }

    ~Hero() {}

    std::string GetName() {
        return name;
    }
};

void PrintHeroName(const Hero &h) {
    std::cout << h.GetName() << std::endl;
}

其实问题很简单:PrintHeroName函数的形参是一个const类型的Hero对象,但是在函数体内,这个const类型的Hero对象调用了一个非const的成员函数,这是不允许的。这种情况下,编译器会报错:

const_cast.cpp:19:18: error: 'this' argument to member function 'GetName' has type 'const Hero', but function is not marked const
    std::cout << h.GetName() << std::endl;
                 ^
const_cast.cpp:13:17: note: 'GetName' declared here
    std::string GetName() {
                ^
1 error generated.

如果想要在PrintHeroName函数中调用h.GetName方法,只需要把形参里的const去掉就可以了。或者把GetName修改成一个const属性的成员函数。

其实最开始我也没有发现这段代码的问题,作为亲手写下这段代码的始作俑者,我甚至还捉摸了一段时间。但这其实就是一个简单的用法,因为使用得过于习惯,并且自认为非常熟悉,所以就忽视了问题所在。

二、为什么要使用const_cast

const_cast的作用是去掉变量的const或者volatile限定符——这看起来很鸡肋,因为很多人都在想:我既然要去掉const限制,那我直接不使用const修饰不就完事了吗?何必多此一举?其实最开始我也是这么认为的,所以从来没有用过这个关键字。直到今天遇到上面的那个问题之后,才突然明白const_cast的真正用途。

上面的程序,把PrintHeroName形参的const去掉后,程序可以正常编译了。问题是,如果调用它的上层函数,Hero对象已经被const限定了,应该怎么办?例如:

void PrintHeroName(Hero &h) {
    std::cout << h.GetName() << std::endl;
}

void CreateHero(const Hero &h) {
    PrintHeroName(h);
}

可以看到,CreateHero的形参是const的,但PrintHeroName是非const的,无法在CreateHero函数中将h传递到PrintHeroName,因为不能直接将一个const对象转换成非const的。程序编译也会报错:

const_cast.cpp:23:5: error: no matching function for call to 'PrintHeroName'
    PrintHeroName(h);
    ^~~~~~~~~~~~~
const_cast.cpp:18:6: note: candidate function not viable: 1st argument ('const Hero') would lose const qualifier
void PrintHeroName(Hero &h) {
     ^
1 error generated.

这种情况下就要用到const_cast了,这里才是const_cast大展拳脚的地方:因为我们很明确能知道GetName()是不会修改任何成员对象的值的,所以可以在这里通过const_cast去掉Hero的const限定,使得程序可以正常往下调用。即,只要把h通过const_cast包裹起来就可以了:

void CreateHero(const Hero &h) {
    PrintHeroName(const_cast<Hero &>(h));
}

一、前言

昨天在公司做代码扫描,发现很多类似以下的代码都产生了告警,导致扫描不通过:

virtual int func() override {}

不通过的原因是:同时使用virtual和override关键字来修饰成员函数,virtual关键字是多余的,要删掉。

说实话,刚开始看到错误提示的时候有点懵,因为对这个特性并不是很了解(代码也不是我写的),所以一时之间也不知道到底是什么原因,只是贸然按照提示把virtual关键字删掉了(删掉了就好了),回来研究了一阵之后才搞明白。

二、override和final

2.1 用途

override和final是C++11中的新特性,主要用于类继承时对虚函数的控制:

  • override修饰子类成员函数,表明当前成员函数覆盖了父类的成员函数。
  • final修饰父类成员函数,表明当前成员函数不能被覆盖。

其实看到这里我心里有一个疑惑:加了virtual关键字就可以实现覆盖了,为什么要用override呢?C++ Primer对这个问题的解释是:

派生类可能定义了一个和父类名字相同但是形参列表不同的成员函数,对编译器而言这不是非法的,这可能就导致不可预期的错误。可能我们是想覆盖父类的函数,但是因为不小心弄错了,最后编译器也没能帮我们检查出来。

加上override关键字之后,如果子类的函数在父类没有相同的函数名以及形参定义,编译器会报错。这就避免了因为开发人员不小心导致的意外错误。

因此,总结来看,override的作用主要是:

  1. 减少程序员因为大意出错的可能性
  2. 提高代码可读性,读代码的人一看到override就能直观的知道当前函数是覆盖了父类的虚函数

2.2 示例

class Hero {
public:
    virtual void SkillR(Hero &b) {};
};
 
class Soldier : public Hero{
public:
    Soldier() {}
     
    void SkillR(Hero &b) override {}
};

以上定义了一个英雄类和一个继承于它的战士类,战士类继承了父类的R技能SkillR(),它的函数名和形参列表和父类一模一样,加上override之后是没有问题的。但是如果把SkillR的形参去掉,编译时就会报错。

override.cpp:15:19: error: non-virtual member function marked 'override' hides virtual member function
override.cpp:8:18: note: hidden overloaded virtual function 'Hero::SkillR' declared here: different number of parameters (1 vs 0)
    virtual void SkillR(Hero &b) {};
                 ^
1 warning and 1 error generated.

三、override和virtual

回到问题本身,为什么virtual碰到override会失效?

当我使用clion编写上面的IDE也提示virtual是多余的:

原因:

cppreference.com中找到override的定义为:

Specifies that a virtual function overrides another virtual function.

意思是说,override指定函数是一个覆盖了其他类虚函数的虚函数,它本身的定义就是一个虚函数。相当于override=virtual+重写,因此virtual关键字也就多余了。