C++经验(一)-- const关键字的用法

const允许我们指定一个语义约束(不该被改变的对象),编译器会强制实施这项约束。

const的作用也是比较多的。

  • 在class外修饰global或者namespace作用域中的常量。
  • 修饰在文件、函数、区块作用域中的被声明为static的变量。
  • 修饰classes内被声明为static或者non-static的成员变量。
  • 修饰指针或者指针所指物。
char test[] = "this is test.";

char* p = test;                               //non-const p non- const data
const char* p = test;                         //non-const p const data
char* const p = test;                         //const p non-const data
const char* const p = test;                   //const p const data

判断上面的类型,主要是看关键字const出现在指针 * 的左边还是右边,如果在左边,则表示指针所指的对象是const类型,如果出现在右边,则表示指针本身是const类型。

很多时候,如果指针指向的对象是常量,我们经常会碰到两种实现方式。

void display(const Widget* w);
void display(Widget const* w);

前面我们在学习stl的时候也提到过,stl中的迭代器其实相当于是一个指针(T*)。而stl中也已经声明了相应的const类型的迭代器(const_iterator)。这个时候我们需要注意const iterator。

使用const_iterator的场景一般都出现我们不想改变容器中的值,但需要遍历容器的时候。

typedef vector<double> DVec;
typedef vector<double>::iterator DVecIter;
typedef vector<double>::const_iterator DVecConstIter;

DVec dv;

for(int index = 0; index < 10; ++index)
{
    dv.push_back(index);
}

const DVecIter iter = dv.begin();
*iter = 10;
iter++;

DVecConstIter citer = dv.begin();
*citer = 11;
citer ++;

上面的例子中,我们分别定义了两个迭代器 iter 和 citer,对应const类型之后也就是 T* const 和 const T*。也就是说, iter 相当于是一个Double* const 的指针,赋值语句 *iter = 10; 能够正常执行,改变的是所指物的值。但是 iter++;不能通过编译。

citer相当于是一个const Double*的指针。他指向的对象是const类型的,赋值语句*citer = 11;是不能通过编译的,但是迭代器本身是可改变的。

让函数返回一个常量值,往往会降低编写代码所犯的低级错误而造成的意外。比如下面这个例子:

class Widget{...}

const Widget operator+ (const Widget& lhs, const Widget& rhs);

上面的例子我们返回了一个const类型的Widget对象,为什么呢?

Widget a, b, c;
(a + b) = c;

如果我们出现了上面这样的代码,我们可能是很难理解的吧。猜测一下这行代码的本意应该是两个对象相加之后和对象c对比较,而不是进行赋值。因为在这儿的赋值操作没有任何意义。

但是如果我们将operator+的返回值声明为const类型,则可以避免这种错误。题外话,如果我们需要和一个常量进行比较,习惯性地将常量写在比较 == 的左边,也可以避免这种错误。

我们在写代码的时候,经常会碰到const类型的成员函数,其目的是为了确认该成员函数可作用于const对象上。之所以重要,有两个理由:

  • 使类的接口更容易理解,可以很明显的看出来哪些函数可以改变对象内容,而哪些不可以。
  • 可操作const对象

我们也经常会有一个经验,使用pass by reference-to-const(const引用)方式传递对象,这样可以提高程序效率。

我们看下面这个例子:

#include<iostream>

using namespace std;
class TextBlack{
public:
	TextBlack(char* p) : pText(p)
	{
	}	
	const char& operator[](std::size_t position) const
	{
		return text[position];
	}
	
	char& operator[](std::size_t position)
	{
		return text[position];
	}
	
	void print() const
	{
		std::cout << text<< endl;
	}
private: 
	std::string text;
}; 

int main()
{
	const TextBlack cb("Hello");
	//cb[0] = 'J';
	
	TextBlack b("Hello");
	b[0] = 'J';
	
	cb.print();
	b.print();
	
	return 0;
} 

上面的例子中,我们对类TextBlack提供了两个不同返回类型的operator[]方法。并且对这两种方法都进行了测试。

上面被注释掉的操作cb[0] = 'J';在调用 cb[0]的时候,也就是调用 operator[] 函数的本身是没有错的,错的是对operator[]函数返回值 const char& 的赋值。

同样的,针对返回值是 non-const 的char& 类型的operator[]也要同样注意,如果返回值是一个char类型的时候,调用 b[0] = ‘J’; 也是错误的,编译器会返回下面错误;

lvalue required as left operand of assignment

这是因为如果函数的返回类型是内置类型,改动函数的返回值是不合法的。纵使合法,C++以by-value返回对象,意味着被改动的其实是 b.text[0]的副本,而不是其本身。

如果我们稍微修改下上面的例子呢?

char& operator[](std::size_t position) const
{
    return text[position];
}

const TextBlack cb("Hello");
char* p = &cb[0];
*p = 'J';

这样会不会const类型cb的值呢?实惠改变的。

上面我们一直在强调的是在const成员函数中不能修改成员变量的值。 那么看下下面的例子。

class TextBlack{
	
public:
	TextBlack(string p) : pText(p)
	{
	}	

	std::size_t length() const;
	
private: 
	std::string pText;
	std::size_t m_len;
}; 

std::size_t TextBlack::length() const
{
	if(0 == m_len)
	{
		m_len = pText.length();
	}
     return m_len;
}

上面的例子我们定义了一个TextBlack类的const成员函数length,但是在该函数的实现中对成员变量m_len进行了赋值,这是不允许的。

其实m_len的修改对于TextBlack类的const对象来说都是可接受的,但是在编译的时候,编译器是不允许的。

这种情况下,我们可以利用C++中和const有关的一个摆动场 mutable(可变的)来实现。

mutable std::size_t m_len{0};

在实际编写代码的时候,我们经常会顾及代码的简洁度,其实在上面的第一个例子中,虽然只有简单的一行,也能看到某些代码其实是有重复的,假设我们在实际应用中,需要同时实现下面的函数。

const char& operator[](std::size_t position) const
{
    ...  //一些其他的操作
    return text[position];
}

char& operator[](std::size_t position)
{
      ...  //一些其他的操作
    return text[position];
}

其中对于一些其他的操作,在const和non-const函数中是一样的,如果写两遍,代码会很长,并且重复性的代码比较多,这可能是我们在很多时候是不能接受的。最简单的方式可能就是将这些操作抽出来,做一个另外的成员函数,然后在这两个函数中调用,虽然代码精简了很多,但还是不可避免的有部分是重复的。

而我们真正需要实现的是,实现一次operator[],但是会调用两次。也就是说,我们必须要使其中的一个调用另一个。

我们在很早的时候已经说过C++的强制类型转换,这里,我们也能够通过常量性转移除来实现。

const char& operator[](std::size_t position) const
{
     ...  //一些其他的操作
    return text[position];
}

char& operator[](std::size_t position)
{
    return const_cast<char&>(static_cast<const TextBlack&>(*this)[position]);
}

我们具体看下上面的实现。

char& operator[](std::size_t position)

首先将 non-const*this 对象通过 static_cast 进行安全的强制转换成const 对象,然后调用 const的成员函数。

最后再将转换后的对象的const属性去除。const_cast操作是会去除一个const 对象的const属性。

如果我们可以让 non-const对象调用 const成员函数,那么反过来呢?是不是也可以。

其实const函数承诺绝不会改变其对象的逻辑状态,但是non-const函数并没有,如果反过来调用,就会违背这项原则。

所以使用const成员函数调用non-const成员函数是错误的,因为对象可能被改变。

  • 将某些对象或者变量声明为const类型,可让编译器帮助我们检查错误用法。const可作用于任何作用于内的对象、函数参数、函数返回类型、成员函数本体。
  • 如果const和non-const成员函数有实质上的等价,则应使non-const调用const成员函数,可避免代码重复。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值