Effective C++ 阅读心得-条款03:尽可能使用const

最重要的话写在前面

1. 如果你希望某些变量,对象,参数等等是常量,是不可更改的,那么你最好使用const,因为编译器会帮助你达成你的目的

  • 只要你使用了const关键字,那么就是告诉编译器和其他程序员某些值应该保持不变。如果你的设计中需要达成这样的目标,你应该明确的说出来,让编译器来帮助你达成这个目的,并确保其他程序员出现更改某些常量值时,得到编译器的提醒。
  • 那么const可以可以施加于哪些事物上呢?

1.1 const对常量,指针的修饰

  • 对常量的修饰:在条款02中提到过const的一些使用方法,比如对常量的修饰,对类中的常量成员进行修饰,这里就不再阐述对常量的修饰了。
  • 对指针的修饰:这里就是老生常谈的问题了,常量指针和指针常量:
  • 常量指针:常量指针是指其所指向的对象是常量,即不能通过该指针修改所指向对象的值。常量指针的声明方式为:
int a = 10, b = 20;
const int* ptr = &a;  // 常量指针指向变量 a
ptr = &b;     // 修改指针变量,使其指向变量 b
*ptr = 30;    //错误!

指针ptr是一个常量指针,它一开始指向变量a,但是通过修改指针变量本身,使其指向了变量 b。由于指针本身并没有被声明为常量,因此它的值可以被修改,只有指针指向的地址是常量。
需要注意的是,a和b的值永远都可以被修改,只是a的值不可以通过指针解引用来修改

  • 指针常量:指针常量是指指针本身是常量,即不能修改该指针所指向的地址。下面是一个指针常量修改所指对象的值的例子:
int a = 10, b = 20;
int* const ptr = &a;  // 指针常量指向变量 a
*ptr = 30;   // 通过指针修改变量 a 的值
ptr = &b;    //错误

指针ptr是一个指针常量,它指向变量a,而且指针本身是常量,不能被修改。但是可以通过解引用指针ptr,即*ptr,来访问所指向的对象,从而修改变量a的值。
需要注意的是,指针常量指向的地址是常量,不能被修改,因此在声明指针常量时必须对其进行初始化。如果不对其进行初始化,则编译器会报错。

  • 常量指针和指针常量傻傻记不清const*前那这个指针修饰的就是常量,如果开头的是const*后那这个指针就是一个常量;如果const出现在*两边,那表示被指物和指针两者都是常量。就别记常量指针和指针常量的概念啦,知道怎么用就好啦。

1.2 const对STL迭代器的修饰

  • 希望迭代器本身是常量:使用const修饰迭代器:
std::vector<int> vec{1, 2, 3};
std::vector<int>::iterator const it = vec.begin();//这里const放在iterator前后都可以

// 使用 const 迭代器遍历容器中的元素
while (it != vec.end()) {
  std::cout << *it << " ";
  ++it; // 编译错误,迭代器指针是常量,不能修改其指向
}

// 可以使用 const 迭代器访问容器中的元素并修改其值
*it = 4; // 编译通过,可以修改迭代器指向的元素的值

it 是一个 const 迭代器指针,它不能修改其指向,但是可以通过该指针访问容器中的元素并修改其值。

  • 希望迭代器指向的内容是常量:使用const_iterator:
std::vector<int> vec{1, 2, 3};
std::vector<int>::const_iterator it = vec.begin();

// 使用 const_iterator 遍历容器中的元素
while (it != vec.end()) {
  std::cout << *it << " ";
  ++it;  //正确
}

// 不能使用 const_iterator 修改容器中的元素
*it = 4;  // 编译错误

在上面的示例中,it是一个const_iterato 迭代器,它被用于遍历容器vec中的元素。由于it是一个const_iterator,因此不能用于修改容器中的元素。

  • 另外插一嘴:C++11 引入了 cbegin()cend() 函数,它们返回的都是 const_iterator 迭代器,可以用于遍历容器中的元素,例如:
std::vector<int> vec{1, 2, 3};

// 使用 cbegin() 和 cend() 遍历容器中的元素
for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
  std::cout << *it << " ";
}

1.3 const对一般函数的修饰

  • 对函数返回值进行修饰的好处:零函数返回一个常量值,往往可以降低因客户错误而造成的意外,用书中的例子来说明:
class Rational { ... };
const Rational operator* (const Rational& lhs,const Rational& rhs);//返回了一个const对象

//用户的暴行
Rational a, b, c;
if(a * b = c) ... 
//如果用户只想在括号中实现 a * b == c 呢,然而用户打错了,如果 operator* 返回值不使用const修饰,这个式子将通过编译器的审查
  • 对函数参数进行修饰的好处
  1. 增加程序安全性:使用 const 修饰函数参数可以确保函数不会修改参数的值。这可以帮助程序员避免由于函数不当的修改导致的意外结果,从而使代码更加可靠和稳定。
  2. 提高代码可读性:使用 const 可以让函数的参数列表更加清晰,明确地表明哪些参数是只读的,哪些是可写的。这可以帮助其他程序员更好地理解代码,并更容易地找出潜在的错误。
  3. 扩大参数类型范围:使用 const 修饰函数参数可以扩大参数类型范围。例如,如果一个函数需要一个常量引用参数,那么可以使用 const 修饰来接受一个临时对象或字面量,这将使函数更加灵活。举个栗子:
//如果你的函数长这个样子
void foo(int& x) {
    // do something with x
}
//你要调用它必须使用一个变量去调用它,比如
int x = 2;
foo(x);//调用成功
//如果你想直接使用字面量去调用,那么结果将大失所望
foo(2);//编译错误

//如果你的函数参数加上了const
void foo(const int& x) {
    // do something with x
}
//想怎么调就怎么调
foo(2);//调用成功
  1. 提高程序效率:改善C++程序效率的一个根本方法是以pass by reference-to-const方式传递对象。

对于较大的对象,将它们传递给函数时,使用 const 可以减少对象的复制,因为函数不能修改参数,所以可以传递一个常量引用而不是一个复制。举个栗子,当传递较大的对象时,使用 const 可以减少对象的复制,从而提高程序的性能:

#include <string>

class Person {
public:
    Person(const std::string& name, int age) : name_(name), age_(age) {}
    std::string getName() const { return name_; }
    int getAge() const { return age_; }
private:
    std::string name_;
    int age_;
};

现在我们有一个函数,需要获取 Person 对象的名字和年龄:

void printPersonInfo(Person p) {
    std::cout << "Name: " << p.getName() << ", Age: " << p.getAge() << std::endl;
}

如果我们传递一个 Person 对象到 printPersonInfo 函数中,那么函数将会复制这个对象,因为函数需要修改它。然而,如果我们只需要获取对象的名字和年龄,那么传递一个常量引用参数将更加高效:

void printPersonInfo(const Person& p) {    //以`pass by reference-to-const`方式传递对象
    std::cout << "Name: " << p.getName() << ", Age: " << p.getAge() << std::endl;
}

我们将 Person 对象作为常量引用参数传递给函数,因为函数只需要获取对象的名字和年龄,并不需要修改它。使用常量引用可以减少对象的复制,从而提高程序的性能。
注意:一般来说,对于对象传递,函数中一般都会使用引用传递的形式,同时当对象非常小或者简单时,使用常量引用可能并不比直接复制对象更加高效,因为引用的创建和维护也需要一定的开销

1.4 const对成员函数的修饰

  • 抛砖引玉:成员函数被const修饰与否,能否实现重载。答案:能!
  • const成员函数能干点什么呢/const修饰成员函数的作用?
  1. 增加代码的可读性:使用const修饰的成员函数可以让调用者更清晰地了解该函数不会对对象状态造成改变(老生常谈,但不得不写)。
  2. 避免错误:在使用const修饰的成员函数中,如果不小心修改了成员变量的值,编译器会报错,避免了一些由于疏忽导致的错误。
  3. 提高效率:如果一个成员函数没有修改成员变量的值,那么它可以被多个线程同时调用,从而提高程序的效率。

需要注意的是,const修饰的成员函数仍然可以访问类中的mutable成员变量,因为mutable关键字可以允许在const函数中修改该成员变量的值。举个栗子:

class Example {
public:
    Example(int value) : value_(value) {}

    int GetValue() const {
        // 在const成员函数中修改mutable成员变量value_
        ++access_count_;
        return value_;
    }

private:
    int value_;
    mutable int access_count_ = 0; // 声明为mutable的成员变量
};

使用mutable关键字必须非常小心,因为它可能会破坏类的封装性,也可能会导致一些非预期的行为。只有在确信需要在const函数中修改某些状态时才应该使用mutable关键字。

  1. 操作const对象:如1.3第四点所示,改善C++程序效率的一个根本方法是以pass by reference-to-const方式传递对象,而const成员函数恰恰使操作const对象成为可能。举个栗子:
#include <iostream>

class Rectangle {
public:
    Rectangle(double width, double height) : width_(width), height_(height) {}

    double area() const {
        // 计算矩形的面积
        double Area = width_ * height_;
        std::cout << "const area(): " << Area << std::endl;
        return Area;
    }

    double area() {
        double Area = width_ * height_;
        std::cout << "non-const area(): " << Area << std::endl;
        return Area;
    }

private:
    double width_;
    double height_;
};

void getArea(const Rectangle& rect){
	rect.area();
}

int main() {
    const Rectangle rect1(5, 10);
    rect1.area(); // 调用 const 成员函数
    Rectangle rect2(5, 10);
    rect2.area();  // 调用 non-const 成员函数
    getArea(rect2);  // 调用 const 成员函数
    return 0;
}

2. bitwise constnessconceptual constness到底是个什么概念

  • 这两个概念都是针对const成员函数而来的
  • bitwise const:成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。也就是说它不更改对象内的任何一个bit。这也是编译器履行的职责,但编译器有时候像个尽职尽责但不机灵的监管员。

举个书中的例子:

class CTextBlock {
public:
	...
	char& operator[] (std::size_t position) const // bitwise const声明,但其实不适当.
	{ return pText[position] ; }
private:
	char* pText;
};

细心的小伙伴可能会发现,这个例子中的operator[]好像不太对劲。那是因为这个类将operator[]声明成一个const成员函数,但是却返回了一个reference指向对象内部的值。这不扯犊子了么,我声明了一个const函数,就是希望const不更改类中的成员,但倒霉就倒霉在这个成员是个指针,bitwise const是可以保证这个指针本身不被更改,但是用户却能更改掉它所指向的对象。操作如下:

const CTextBlock cctb("Hello");//声明一个常量对象
char* p = &cctb[0];//获取指针
*p = 'A';//好了,现在cctb变成"Aello"了

所以啊,你想保证cctb的常量属性,你还是得依靠自己去查看代码逻辑是否如你所想,这便是logical constness

  • logical constness:指的是对象是否被视为在逻辑上不可更改。如果是,这意味着对象在其生命周期内不能被修改,即使在对象声明为const之前,它也不会被修改。这种约束通常由程序员在代码中实现,但编译器并不强制执行。
    想保证cctb的常量属性,你需要自己查看代码逻辑,使之符合你的设想:
class CTextBlock {
public:
	...
	const char& operator[] (std::size_t position) const // logical const声明
	{ return pText[position] ; }
private:
	char* pText;
};

另外,logical constness思想也可以释放掉bitwise constness的约束,这用到了上述的mutable关键字。

在实际情况下,大多数情况下这两个概念是一致的。但是,有些情况下,这两种约束可能会不一致,例如在使用const_cast时。因此,程序员需要确保在修改被声明为const的对象时遵循logical constness,而不是仅仅依靠编译器的bitwise constness检查

3. 在constnon-const成员函数中避免重复

  • 如果你的const函数和non-const函数有着实质等价的实现,那么请使用non-const版本调用const版本的函数来降低代码重复率叭。
  • 为什么使用non-const版本调用const版本?
  1. logical constness思想不允许:const版本调用non-const本身就违背logical constness思想,因为const版本函数承诺绝不改变其对象的逻辑状态,你如果调用了,那就是冒了对象逻辑状态改变的风险了(如果要调用,调用时必须现使用const_cast将对象也就是*this身上的const性质解放掉,这太可怕了),但non-const并不会受限。
  2. non-const调用const是安全的,可行的做法,下面来个书中的例子。
class TextBlock {
public:
	...
	const char& operator[] (std::size_t position) const //一如既往
	{
		...
		return text[position] ;
	}
	
	char& operator[] (std::size_t position)//现在只调用const operator[]
	{
		return
			const_cast<char&> (			//将op[]返回值的const转除
				static_cast<const TextBlock&> (*this)	//为*this加上const
					[position]			//调用const operator[]
			);
	}
private:
	char* text;
};

//使用
TextBlock cctb("hello");
char* p = &cctb[0];
*p = 'e';

要实现non-const调用const必须对自身*this进行转型,因为constnon-const的成员函数是重载的,想使用non-const成员函数,那就要使用non-const对象来调用,这时*this就是non-const版本的。

第一次转型是从non-constconst,可以使用const_cast操作符或者static_cast操作符,转型之后调用const版本成员函数。
调用之后返回const对象,使用const_castconst对象的const属性去掉。完美。来一份完整测试代码:

#include <iostream>
using namespace std;

class TextBlock {
public:
	TextBlock(char* c){
		text = c;
	}
		const char& operator[] (int position) const //一如既往
	{

			return text[position];
	}

	char& operator[] (int position)//现在只调用const operator[]
	{
		return
			const_cast<char&> (			//将op[]返回值的const转除
				const_cast<const TextBlock&> (*this)	//为*this加上const
				[position]			//调用const operator[]
		);
	}
private:
	char* text;
};

int main() {
	char* str = (char*)malloc(sizeof(char) * (strlen("Hello, world!") + 1));
	if (str == NULL) {
		// 处理内存分配失败的情况
		cout << "error";
		return 0;
	}
	else {
		strcpy_s(str,sizeof("Hello, world!"), "Hello, world!");
	}
	TextBlock cctb(str);
	char* p = &cctb[0];
	*p = 'e';
	cout << cctb[0];
	return 0;
}

打完收工!

点点关注不迷路~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值