成员函数是类的一部分,其定义与普通函数一样,没有特殊之处。但是需要注意以下一些特点:

  • 编译器隐式的将在类内部定义的成员函数当做内联函数(不管有没有使用inline关键字)。
  • 在成员函数的声明或定义处指定inline关键字都是合法的。
  • 类的成员函数可以访问该类的private成员。
  • 使用类的对象来调用类的成员函数,如果是静态成员函数,还可以使用类名加::符号来调用。
  • 成员函数可以重载。

介绍完成员函数的基本概念,我们就来开始今天的正文:const成员函数和this指针以及mutable关键字。

1. this指针

每个成员函数(除static成员函数)都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象的地址,所以形参this是指向类对象的指针。因为static成员函数是类的组成部分,但不属于任何一个类对象,所以static成员函数没有this指针。在成员函数中,不必显式的使用this指针来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀的引用,都被假定为通过指针this实现的引用。看个例子:

class A
{
public:
	bool is_same(const A &inst) const
	{  return m_id == inst.m_id;	}

	bool is_same_ext(const A &inst) const
	{ return this->m_id == inst.m_id; }

private:
	int	m_id;
};

该例子中,is_same和is_same_ext其实是一模一样的。

NB:由于this指针是隐式定义的,因此不需要也不能在函数的形参表中包含this指针,但可以在函数体中显式的使用this指针。

那this指针有什么用呢?这里举个例子,看下面代码:我们定义了一个Screen类,其中move成员函数用于将光标移动到某个位置,set函数用于设置光标处的值。我们分别定义了两个版本。

class Screen
{
public:
	typedef std::string::size_type	index;

public:
	void move_dumm(index r, index c);
	void set_dumm(char);
	Screen&	move(index r, index c);
	Screen&	set(char);

private:
	std::string	contents;
	index	cursor;
	index	height, width;
};


/* 实现 */
void Screen::set_dumm(char c)
{
	contents[cursor] = c;
}

Screen& Screen::set(char c)
{
	contents[cursor] = c;
	return *this;
}

void Screen::move_dumm(index r, index c)
{
	index row = r * width;
	cursor = row + c;
}

Screen& Screen::move(index r, index c)
{
	index row = r * width;
	cursor = row + c;
	return *this;
}

我们对比以下上面代码中的set与set_dumm,move和move_dumm,其实他们实现的功能是一样的,但是不带dumm后缀的都返回了this的引用,这样有什么好处呢?好处就是我们可以将一些操作连接成一个序列,在一个表达式中实现:

Screen myScreen;
myScreen.move(4, 0).set('#'); //先将光标移到(4,0)位置,再将该处的值设置为'#'

其实上面这个语句等价于:

myScreen.move(4, 0);
myScreen.set('#');
或者
myScreen.move_dumm(4, 0);
myScreen.set_dumm('#');

显然,不带dumm后缀的实现比不带的用起来更灵活。当然this指针的用途不止于此,这里不再介绍。

2. const成员函数

我们看到,上面举的第一个例子中的两个成员函数后面都加了const,这种成员函数叫const成员函数(也称为常量成员函数。没有const的成为为普通成员函数或非const成员函数。),而那个const其实是修饰this形参的。这里有两个非常重要的区别:

  • 在普通的非const成员函数中,this的类型是一个指向类类型的const指针(指向const对象的指针,指针本身不能再改变,即指针初始化时指向的是什么就是什么,后面不能再指向其他对象),也就是说我们可以改变const所指向的值,但是不能改变this所保存的地址。这是非常合理的,也就是说我们可以通过this指针修改类的某些成员的值,但我们不能改变this指向该对象的特性,否则就失去了this的含义。
  • 在const成员函数中,this的类型是一个指向const类类型对象的const指针,即我们既不能改变this所指向的对象(不论该对象是否是const的),也不能改变this所保存的地址。不能从const成员函数返回指向类对象的普通引用,const成员函数只能返回*this作为一个const引用。

另外,需要注意两点:

  • const对象、指向const对象的指针或引用只能用于调用其const成员函数,不能调用非const成员函数。
  • 非const对象、非指向const对象的指针或引用既可以调用const成员函数,也可以调用非const成员函数。

上面的东西非常的抽象,我们看个例子:继续前面的例子,现在我们定义了一个display常量成员函数,用于在给定的ostream上打印屏幕的内容contens。因为打印contents不会改变对象,所以我们将display函数定义为const。这样根据前面const成员函数的定义,display内部的this指针将是一个const Screen*型的const,同时,它返回的类型也必须是const Screen&,正如下面代码中实现的那样。代码如下:

class Screen
{
public:
	typedef std::string::size_type	index;

public:
	Screen&	move(index r, index c);
	Screen&	set(char);

	const Screen& display(std::ostream &os) const
	{
		do_display(os);
		return *this;
	}

private:
	void do_display(std::ostream &os) const {	os << contents;		}

private:
	std::string	contents;
	index	cursor;
	index	height, width;
};

但这样定义是有问题的,比如下面的使用:

myScreen.display(cout).set('#');

这样使用是错误的,原因在于这个表达式实在由display返回的对象上运行set。但对象是const的(因为display是const成员函数,只能将this指针作为const引用返回),我们不能在const对象上面调用非const成员函数set。那如何解决这个问题呢?

可以使用基于const的重载:我们定义两个display操作,一个是const,另一个不是const,基于成员函数是否为const,可以重载一个成员函数;同样的,基于一个指针形参是否指向const,也可以重载一个函数。const对象只能使用const成员,非const对象可以使用任一成员,但非const版本是一个更好的匹配。下面是代码实现:

class Screen
{
public:
	typedef std::string::size_type	index;

public:
	Screen&	move(index r, index c);
	Screen&	set(char);

	const Screen& display(std::ostream &os) const
	{
		do_display(os);
		return *this;
	}
    
	Screen& display(std::ostream &os)
	{
		do_display(os);
		return *this;
	}


private:
	void do_display(std::ostream &os) const {	os << contents;		}

private:
	std::string	contents;
	index	cursor;
	index	height, width;
};

我们只是再重载实现了一个非const版本的display,这样将display嵌入到长表达式中去调用就不会有问题了。

3. mutable关键字

我们有时可能希望可以在const成员函数中改变对象的数据成员,那么我们就可以在数据类型声明前增加mutable关键字,这样我们变将这个数据成员声明为了可变数据成员。可变数据成员永远都不能为const,甚至当它是const对象的成员时也是如此。比如我们给Screen添加了一个新的可变数据成员access_ctr,用来跟踪调用Screen成员函数的频繁程度:

/*之前代码省略 */

private:
	void do_display(std::ostream &os) const 
	{	
		++access_ctr;		// 等效于:++this->access_ctr
		os << contents;		
	}

private:
	mutable size_t access_ctr;

/* 之后代码省略*/

虽然do_display是const成员函数,但我们仍然可以在它里面改变数据成员access_ctr的值。

个人观点:虽然C和C++中引入const带来了许多方便和增强了许多功能,但不得不说,const也带来了许多混乱。