Effective C++ 学习笔记(item3)
Item 3: Use const whenever possible
const 在变量上的作用
const修饰一个变量表示这个变量不可变。在这里不好理解的有两点。
- 定义指针的时候要用两个const,且const在
*
左边,右边是有区别的。 - 在迭代器上const的使用方法。
- 对于第一个注意点,如果你能区分下面代码的定义含义,就可以跳过第一个问题了。如果const在
*
左边,那么const修饰的是指针指向的东西。如果在右边,那么修饰的是指针。
char const * pChar = "hello";
const char* pChar = "hello";
char* const pChar = "hello";
char const* const pChar = "hello";
- 对于第二个问题,要注意下面代码含义一样,都表示的是iter本身不能变化,但iter指向的内容是可以变化的。
const vector<int>::iterator iter;
vector<int>::iterator const iter;
- 如果要让iter指向的内容不可变化,使用这样的方式定义iter:
vector<int>::const_iterator iter;
此时iter自己可变,但iter指向的东西不允许变化。
const 在函数声明中的作用
声明函数的返回值是const
为一个函数的返回值声称const没有坏处。毕竟函数的返回值是个临时变量,在程序里常用来状态判断或者赋给其他新的变量。声明这个临时变量是const可以规避==
不小心写成=
的问题,举例如下:
string getmystring()
{
return "hello";
}
int main()
{
if(getmystring()[0]==’h’)
cout<<"first one is h";
if(getmystring()[0]='h')
cout<<"first one is h";
}
对于上述main函数中的第二个if语句实际上是写错的。但是getmystring不是const返回,所以编译器不会报错。但如果定义成const string getmystring(){...}
编译器就会帮助我们发现问题。
const修饰函数入参
只要你确认传入的入参,在整个函数过程不应该被改变。就使用const限定。
const修饰整个函数
const成员函数的作用:
- 可以区分哪些成员函数会修改class本身。 编译器在编译的时候能够限定const成员函数不会修改该类对象的任何专属成员。 静态变量是个例外,静态变量是多个类对象共有的,对他的修改不认为违反const成员函数的规则。 编译器的这套检查规则其实是有漏洞的。这个在引申章节介绍。
- 当某个对象是const的时候,就只能通过该对象调用他的const的成员函数。
- const成员函数和非const成员函数是不同的。编译器会按重载方式处理。
- 上述三点中的第二点作用很重要,c++传递效率最高的方式是通过reference-to-const,那么比如要把一个类的const reference传给一个函数当入参,在函数里面取到这个类const reference之后,想通过这个类的一些方法实现一些功能。如果这个类里面没有const成员函数,就没有办法做任何调用了。
- 在看第三点:下面的例子中这两个
getLimit
函数是不同的,当定义const对象Numbers的时候,只能调用const成员函数。但是非const Numbers对象在调用该函数的时候优先找非const的成员函数,如果没有的话,会继续尝试找const的成员函数。
class Number
{
int getLimit() const;
int getLimit();
}
编译器在检查const成员函数的时候能限定其实现没有对任何成员进行赋值。但是允许对static 成员变量操作修改。 mutable这个修饰符的作用我感觉很鸡肋。const成员函数是不允许修改类的成员变量的,但是如果要修改,可以用mutable来修饰这个变量。这不是自己给自己找麻烦吗,非得如此用non-const成员函数不是更直接吗。但一旦用non-const成员函数,我们就没办法使用reference to const的方式传递了。
了解non-const成员函数借用const兄弟成员函数的做法(了解下就OK,实际使用不多,当两个兄弟函数十分类似的时候,避免代码冗余的做法,但是我更倾向于“再抽象出一个函数”的做法实现。详见下面的引申章节。
item3的引申
编译器基于bitwise const原则编译const成员函数会有两个问题
- 作者认为const成员函数如果返回的是引用或者是指针。那么存在通过const对象调用该const成员函数获取引用/指针修改"指向的数据"的风险。
按作者Scott的描述,作者认为下面的代码是可能发生的。
class CTextBlock
{
public:
CTextBlock()
{str = "hello";}
char& operator[](std::size_t size) const
{return(str[size]);
private:
string str;
};
int main()
{
const CTextBlock testBlock;
testBlock[0] = 'J';
return 0;
}
按作者的描述,作者认为编译器在编译上述的const成员函数的时候,发现没有任何成员对象的赋值等变化操作,所以编译器根据bitwise const原则会编译通过。但实际上现在的编译器会发现你返回的是指针或者引用且返回值不是const,编译器会告警并阻止编译。这里权且认为作者那个时代的编译器还不过强大吧。
- 有的时候修改一些变量只是做缓存用的,并不是真正的改变对象。比如在对象内部有个变量用来时刻缓存对象的大小。这个缓存值会根据对象实际大小而更新,实际的对象没有变化。这种情况下编译器根据bitwise const原则会导致编译无法通过。
举例如下:
class CTextBlock
{
public:
CTextBlock()
{str = "hello";cashsize = str.size();}
string getString() const
{cashSize = str.size(); return str;}
private:
string str;
int cashSize;
};
int main()
{
const CTextBlock testBlock;
string tmp = testBlock.getString();
return 0;
}
上述代码是无法编译通过的,因为在const成员函数内部发生了cashSize = str.zize()
的赋值操作,编译器会报错并停止编译。但对于编程人员,从逻辑角度上说这个cash对外是不呈现的,从逻辑上说对象并没有发生外部特性变化。基于这个出发点,设计者又希望const成员函数能够修改一些不导致外部变化的一些变量修改。为了解决这种矛盾,作者Scott引荐了mutable。mutable修饰的变量允许在const成员函数中修改。
但我并不为这种补丁叫好,这种补丁在后续团队开发中不知道会引入多少麻烦,除非是你一个人的工程,否则这种补丁再补丁的做法我自己并不推荐。
const和非const函数高度相似时引入的代码重复问题
作者Scott在这个问题上描述了很多篇幅,最后给出的解决方法,并不是特别理想。作者也说了这种牵强的解决方法仅供参考。如果像作者那样,相同的功能一定要const成员函数来一份,非const成员函数也来一份,可以借鉴他提出的"在非const函数里面调用const成员函数的方法。但是这里我更喜欢用另一种方法。因为如果const和非const两个函数的功能一样,那么实际上是可以直接删除非const成员函数的。这样非const对象调用函数的时候,因为找不到非const函数,会最终调用const成员函数。
这里举两个代码实例分别说明作者的方法和我自己的方法
- 作者Scott的方法
class CTextBlock
{
public:
CTextBlock(0
{str = "hello";}
const char& operator[](std::size_t size) const
{ return (str[size]);
chr& operator[](std::size_t size)
{ return const_cast<char&>(static_cast<const CTextBlock&>(*this)[size]);//作者Scott用的转换调用方法}
private:
string str;
};
int main()
{
CTextBlock testBlock;
char*ptmp = &testBlock[0];//非const对象调用非const成员函数。而非const成员函数内部调用的是const成员函数
return 0;
}
- 我使用的方法
class CTextBlock
{
public:
CTextBlock(0
{str = "hello";}
const char& operator[](std::size_t size) const
{ return (str[size]);
//我的方法直接没有相同功能的非const成员函数
private:
string str;
};
int main()
{
CTextBlock testBlock;
char * ptmp=&(const_cast<char&>(testBlock[0]);//非const对象调用非const成员函数。但是没有找到,所以最终会调用const成员函数,对调用的csont返回在做处理
return 0;
}