《Effective C++》《让自己习惯C++——3、尽可能的使用const》

发布时间:2024年01月19日

1、term3:Use const whenever possible

const允许你定义一个语义约束,指定一个"不该被改动的”对象。

(1)const 修饰指针,迭代器,函数返回值。

const修饰指针

char greeting[] = "Hello";
char* p = greeting;
const char* p = greeting; 		  //指向字符常量的指针,p指向的字符不能修改
char* const p = greeting; 		  //指向字符的常量指针,p本身的值不能够被修改
const char* const p = greeting;   //指向字符常量的常量指针,p指向的字符,p本身都不能被修改

为了方便理解:
在第一个声明中,p 是一个指针,它指向一个 const char。
在第二个声明中,p 是一个 const 指针,它指向一个 char。
在第三个声明中,p 是一个const指针,它指向一个const char。
const修饰迭代器
举个栗子:

std::vector<int> vec //整数类型的std::vector
...
const std::vector<int>::iterator iter = vec.begin();
//定义一个常量迭代器,相当于T* const
//迭代器不得指向不同的东西,但他所指向的东西的值是可以改动的;
*iter = 10; //正确
++iter;		//错误
std::vector<int>::const_iterator cIter = vec.begin();
//声明一个常量迭代器,相当于const T*
//迭代器不能修改容器中的元素,但是可以修改迭代器的指向;
*citer = 10;	//错误
++citer;		//正确

const修饰函数返回值

const int max(int a,int b){
	return (a > b ? a:b) ;
}
int c = 7;
max(a,b) = c;
//将一个常量的值,赋值给一个函数调用的返回值是没有意义的;
//const关键字在这里确保了函数不会进行任何可能影响程序状态
//的修改,并且使得函数的输出具有确定性。

(2)const修饰成员函数

首先要明确const在成员函数中的作用:
逻辑常量性(Logical constness):
作用: const 关键字表示该成员函数在逻辑上不会修改对象的状态。这是一种承诺,告诉编译器和其他程序员,调用这个成员函数不会改变对象的内部数据成员。
实现: 在 const 成员函数中,你不能修改非 mutable 数据成员,也不能调用非 const 成员函数。这样确保了对象在逻辑上的不变性。
允许 const 对象调用:
作用: 将 const 关键字应用于成员函数使得可以在常量对象上调用这个函数。如果一个对象被声明为 const,那么只能调用它的 const 成员函数,而不能调用普通的非 const 成员函数。
实现: 在 const 成员函数中,编译器会将 this 指针视为指向常量对象的指针,这意味着在这个函数中不能修改对象的非 mutable 成员。
我举个栗子:

class TextBlock{
public:
...
const char& operator[](std::size_t position)const
{return text[position];}
char& operator[](std::size_t position)
{return text[position];}
private:
std::string text;
};

TextBlock tb("hello");
std::cout << tb[0]; //调用non-const TextBlock::operator[]
TextBlock ctb("world");
std::cout << ctb[0]; //调用const TextBlock::operator[]

在介绍之前先引入两个概念:Bitwise Constness和Logical Constness
Bitwise Constness:
定义: “Bitwise constness” 关注的是对象在二进制表示层面是否保持不变。在这个概念中,如果对象被声明为 const,那么其底层二进制表示不应该发生变化。
实现: 通常通过将成员函数声明为 const 来实现 “bitwise constness”,以确保在 const 对象上调用这些函数时,对象的二进制表示不会发生变化。这样的函数不应修改任何非 mutable 的成员。
Logical Constness:
定义: “Logical constness” 关注的是对象的逻辑状态是否保持不变。在这个概念中,即使对象的物理状态(内部数据成员的值)发生了改变,它在外部的视角下仍然是常量。
实现: 通常通过将成员函数声明为 const,并在这些函数中不修改对象的逻辑状态。这允许在 const 对象上调用这些函数,保持对象在外部视角下的常量性。
区别:
“Bitwise constness” 关注对象底层二进制表示的变化,主要通过成员函数的 const 修饰符来实现,限制对数据成员的修改。
“Logical constness” 关注对象的逻辑状态的变化,同样通过成员函数的 const 修饰符来实现,限制对逻辑状态的修改。
在很多情况下,这两个概念是相关的,因为 “logical constness” 的实现通常涉及不修改对象的物理状态,从而保持 “bitwise constness”。然而,它们的重点略有不同,前者侧重于二进制表示,而后者更关注对象在逻辑上的不变性。

来举个栗子:

class CTextBlock{
public:
	...
	std::size_t length() const;
private:
	char* pText;
	std::size_t textLength;
	bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
	if(!lengthIsValid){
		textLength = std::strlen(pText);
		lengthIsValid = true;			//error!!!
		}
		return textLength;
}
//在const成员函数内不能赋值给textLength和lengthIsValid

用mutable释放掉non-static成员变量的bitwise constness的约束:

class CTextBlock{
public:
	...
	std::size_t length() const;
private:
	char* pText;
	mutable std::size_t textLength;
	mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
	if(!lengthIsValid){
		textLength = std::strlen(pText);
		lengthIsValid = true;			//这样就是没问题的
		}
		return textLength;
}

const成员函数不能改变(除static)成员变量的值,同理const对象不可以调用non-const函数。但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址,则不算事const函数,但能通过bitwise测试。
解决方法:使用mutable可以消除non-static成员变量的bitwise constness约束。
举一个其他栗子:

#include <iostream>
using namespace std;
class MyClass{
public:
	//带有mutable 关键字的非静态成员变量
	mutable int mutableData;
	//构造初始化函数 mutableData
	Myclass(int data):mutableData(data){}
	//const成员函数,逻辑上保持不变性,但允许修改mutableData
	int getData() const{
		//修改mutableData,不违反const成员函数的规定
		mutableData++;
		return mutableData
	}
}
int main(){
	Myclass obj(42);
	cout << "Initial data: " <<obj.getData()<<endl;
	const Myclass constobj(100);
	cout << "Const object data: "<<constObj.getData()<<endl;
	return 0;
}

这个函数运行后的输出结果是:

Initial data: 43
Const object data: 101

在const 和non-const成员函数中避免重复
但是很遗憾mutable不能解决所有问题,假设你要做若干操作,同时放进const和non-const的operator[]中,就会发生代码重复的问题。
举个栗子:

class TextBlock{
public:
		...
		const char& operator[](std::size_t position) const
		{
		...		//边界检验
		...		//日志记数据访问
		...		//检验数据完整性
		return text[position];
		}
		char& operator[](std::size_t position)
		{
		...		//边界检验
		...		//日志记数据访问
		...		//检验数据完整性
		return text[position];
		}
private:
		std::string text;
};

这段代码的问题很多,代码重复伴随的维护,代码膨胀等问题。最好的方法是只实现一次,但是调用的时候灵活一些。从内容看两个operator[]做的工作一样,但是留下哪个呢,留下non-const的那个,因为不论谁调用non-const operator[],都一定要首先有个non-const对象。所以利用non-const operator[]调用 const是一个避免代码重复的安全做法。
举个栗子:

class TextBlock{
public:
	...
	const char& operator[](std::size_t position) const //保持不变
	{
		...		//边界检验
		...		//日志记数据访问
		...		//检验数据完整性
		return text[position];
	}
	char& operator[](std::size_t position)
	{
		return
			const_cast<char&> //移除第一次转型添加的const
				(static_cast<const TextBlock&>(*this)[position]);
				//将TextBlock类型转化cosnt TextBlock
				//使得能够调用const operator[]
	}
	...
}

这份代码有2个转型操作,第一次用来为*this 添加const,这就告诉operator[]调用const的版本,第二次从const operator[]的返回值中移除const。

2、总结

书山有路勤为径,学海无涯苦作舟。

3、参考

3.1 《More Effective C++》

文章来源:https://blog.csdn.net/zwh1298454060/article/details/135678329
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。