《C++编程惯用法——高级程序员常用方法和技巧》——2.7 Const

简介:

本节书摘来自异步社区出版社《C++编程惯用法——高级程序员常用方法和技巧》一书中的第2章,第2.7节,作者: 【美】Robert B. Murray ,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.7 Const

许多C++程序员在开始使用const时都是用它来定义一些常数;例如将:

//C版本:
#define BUFF_LENGTH 1024
int buffer[BUFF_LENGTH];

写成:

//C++版本:
const int BUFF_LENGTH = 1024;
int buffer[BUFF_LENGTH];

这样做可以获得的好处是:编译器(或者其他工具)可以知道BUFF_LENGTH的名字和类型。而且这样做不会给程序的执行时间和尺寸上带来什么额外的开销,如果我们不去使用BUFF_LENGTH的地址的话,编译器也就不需要为它申请专门的存储空间。

然而,const能做的并不仅仅是定义常数那么简单。通过声明一个“指向常量的指针”,我们就可以确保该指针指向的对象不会被改变。在本节中,我们将向读者展示指向常量的指针(或者引用)是如何影响我们的代码的。

请记住一点:将某件事物声明为const(或者是指向const的指针或者引用)会引起额外的编译期检测,但它并不会导致编译器产生额外的代码。

2.7.1 常量引用参数

在C++中,函数参数是以值的方式进行传递的。这意味着在被调用的函数中,存在着一份实际参数的拷贝;我们在被调函数中对该拷贝进行的改变不会反映到调用函数中去。

当一个自定义类型的对象以传值的方式传递到函数中去时,通过调用该类型的复制构造函数,它将和函数的其他实参一样被复制。当函数返回时,编译器会产生一些代码(调用析构函数)来摧毁这份拷贝。这样的复制操作可能会造成额外的开销,尤其是当被调函数又将该对象传递给其他函数时更是如此。在这种情况下,每个最外层的调用都可能会产生该对象的好几份拷贝(以及相应的构造和析构代码)。

在大多数情况下,即便对象在概念上是以传值的方式传递给函数,被调函数可能也不需要有着它自己的对于该对象的独立拷贝,它可以提供一个常量引用来使用这个对象:

//正确但运行缓慢的代码:
//不必要的Telephone_number的拷贝:
void
dial(Telephone_number tn){
//此处忽略细节
}

//改善后的代码:使用了一个常量引用:
void
dial(const Telephone_number& tn){
//此处忽略细节
}

上面的第二份代码的速度要快一些,因为它避免了对输入参数进行复制并在函数返回时摧毁该复制的操作。此处使用const阻止了被调函数无意中对调用函数中对象的值进行修改。

使用引用给用户带来了一些重要的语法甜头:他们不再需要去记忆哪些参数是以指针的方式传递给了函数,哪些参数是以值的方式传递给了函数。如果在概念上来说,参数是以值的方式传递给函数,我们就可以以上面的方式来写调用语句,而“参数是以引用的方式传递给函数”这个实现细节就被隐藏起来了。

2.7.2 常量参数和常量指针

不同的C++程序员对于const的态度不一样。有些人把它看作是在编译期间寻找bug的一个重要工具;其他人则认为相对它的好处来说,const带来的麻烦要更多一些,并因此不去使用它。如果我们正在编写将要被他人使用的代码,第二种态度就不适合我们:即使我们自己不使用const,其他使用我们编写的类的人可能也会使用它,因此我们不得不将一些适当的事物声明为const以允许其他人的使用。

在那些接受指针参数的函数中,这种情况出现得最多。如果函数只是通过指针来读取(它并不会向被指向的对象存储或者更改它的内容),在函数声明时,我们应该将该参数声明为一个指向常量的指针:

class String {
publiC:
   String(const char*="");
//此处忽略细节
};

上面的声明保证了String的构造函数不会通过它的指针参数进行存储活动。(任何试图在String的构造函数中对它进行的存储活动都将导致一个编译期错误。)如果我们把String的构造函数声明为带有一个类型为char而不是const char的参数,那么所有有着常量指针的用户就无法用这个构造函数来构建一个String对象:

//在String.h中:
class String {
public:
   String(char* = ""); //应该为"const char*"
//此处忽略细节
};

//在用户代码中:

main() {
   const char* hello = "hello world";
   String s(hello);    //编译期错误:
                //找不到合适的String构造函数
}

同样的情况也适用于接受引用参数的函数:如果函数不会通过引用来存储内容,那么它应该接受一个常量引用作为其参数。常量引用参数也使得将一个指向未命名的临时对象的引用传递给函数成为了可能。

Thing get_a_thing();

void look_at_thing(const Thing&);

void change_thing(Thing&);

look_at_thing(get_a_thing ());   //OK
change_thing(get_a_thing());    //编译期错误

对于change_thing的调用会产生一个编译期的错误:将一个未命名的临时对象作为一个非常量指针传递给函数是非法的。如果在被调函数中对引用参数的值进行了修改,但调用函数却忽略了这种修改,我们认为这种行为是一种bug;因此C++中也就增添了这么一条规则来禁止这种bug的产生。如果我们真的想那么做的话,我们就必须得创建一个具名对象:

Thing t(get_a_thing());
change_thing (t);  //OK

声明一个指向常量的指针关注的是该指针,而不是该指针指向的空间。编译器并不能确保被指向的数据不会被改变;它能确保的是,数据不会是通过该指针被改变的。我们仍然可以使用其他方式来改变被指向的对象的值:

Void
do_callback(const int* ip, void(*callback)()) {
   cout << *ip << endl;
   (*callback)();
   cout << *ip<< endl;
}

即使ip是一个指向const int的指针,我们仍然不能保证这两次打印的数字会是一样的。如:

int i = 5;
void 
bump_i(){
   ++i;
}

main(){
   do_cal1back (&i, bump_i);
}

它就将打印:

5
6

在两次打印之间,那个回调函数会修改i的值。

一个语法上的小缺点
当我们在do_callback中通过函数指针来调用那个回调函数时,我们先对函数指针进行解引用,然后再调用该函数:

(*cal1back)();

然而,如果我们直接通过()操作符来“调用”该函数,我们将得到和前面一样的结果:

callback();

这两种方式得到的代码将完全一样。

这只是一个语法上的甜头而已,我并不推荐大家这么做。当我们使用第一种形式时,我们可以很清楚地知道函数是通过函数指针来调用的;如果我们使用第二种形式,我们必须了解“函数”的类型实际上也就是“函数指针”的类型。相比而言,少输入几个字符所得来的好处还不值得我们去为它而生成难以理解的代码。

2.7.3 常量成员函数

那些不会修改对象值的函数应该被声明为const(详情参见后面的“回顾”)。这使得其他人可以使用我们编写的类来创建常量对象,并使用编译器来确保对这些对象所进行的成员函数调用不会修改它的值。

在未命名的临时对象上调用非常量成员函数
即使我们不能传递一个指向非常量未命名临时对象的引用,我们还是可以合法地在一个未命名的临时对象上调用一个非常量成员函数:

class String {
public:
   void capitalize(); //非常量成员函数
};
//…
String make_up_name();
make_up_name().capitalize();  // 可以但不应该这么调用

这种做法凸显了C++语言定义的一个失误,我期望ISO/ANSI C++标准委员会能够在随后的标准制定过程中把它给改正;不管如何,我们都不应该使用这种用法。如果我们希望从对象来调用一个非常量的成员函数,我们必须明确地定义这个对象并给它一个名字:

String name(make_up_name ());
name.capitalize();//稍好的做法

这将保证该名字所代表的对象在程序离开当前定义它的语句块前都不会被摧毁。

回顾:常量成员函数

通过在函数的声明体和定义体的参数列表后面添加关键字const,我们可以把一个成员函数声明为一个常量成员函数:

//在String.h中:
class String {
public:
   //此处忽略细节
   int langth() const;
   void capitalize(); //非常量成员函数
};
//在String.c中:
int 
String::length() const {
//此处忽略细节
}
 
void 
String::capitalize() { //非常量成员函数
//此处忽略细节
}

我们只能对常量对象调用常量成员函数:

const String S("hello");
int len = s.length(); //OK
s.capitalize();//编译期错误:对常量对象调用非常量成员函数
 
String t("world");    //非常量String
len = t.length();     //OK
t.capitalize();      //OK

在常量成员函数的定义体中,对象的所有数据成员都是常量,“this”指针也是一个“指向常量对象的常量指针”,而不是“指向对象的常量指针”。
会改变对象状态的常量成员函数
C++的语言规则确保:除了明确地使用了类型转换,常量成员函数不会修改对象的状态(数据成员)。然而,某些在概念上为常量的操作可能也会改变对象中某些成员的值;对于这种情况,我们应该把它们作为实现细节向用户隐藏起来。通过使用这种方法,用户就可以在不清楚实现细节会修改对象中的某些私用数据成员的情况下,对一个常量对象进行某些在概念上来说不会更改对象状态的操作。

例如,假设我们正在使用的Complex类中存储有值的极坐标形式。我们可能需要将它的笛卡儿坐标的值也缓冲在对象中,以节约以后对它们重复计算所带来的运行时间。现在,每个对象都包含有一个布尔值,我们用它来判断缓冲值是否有效:

typedef unsigned char Boolean;
class Complex {
private:
   double r,theta;
   double real_cache,imag_cache;
   Boolean real_cache_valid;
   Boolean imag_cache_valid;
public:
   Complex(double real,double imag);
   double real_part()const;
   void real_part(double);

   double imag_part()const;
   void  imag_part(double);
};

我们在构造函数中将缓冲值标志位设为0,以表明缓冲值目前是无效的:

#include <math.h>
Complex::Complex(double re, double im)
:r(sqrt( re*re + im*im )),
 theta(atan2(im,re)),
 real_cache_valid(0),
 imag_cache valid(0)
{}

在概念上来说,我们应该把那些用来获取(而不是设置)这些值的函数定义为常量成员函数,但是它们实际上也会修改对象中的缓冲值:

double
Complex::real_part() const{
   if(!real_cache_valid) {
    (double&)real_cache = r*sin(theta);
    (Boolean&)real_cache_valid = 1;
  }
  return real_cache;
}

为了在一个常量成员函数中对数据成员进行修改,我们必须使用类型转换来去除该成员的常量性。在C++中,这种做法不但合法,而且只要类中带有一个构造函数,它的行为就将正确无误。(如果类中没有构造函数,那么我们用来去除常量性的类型转换将得到未定义的结果;之所以有这样的规则是因为我们希望编译器能够将类似于const int这样的事物放置到ROM中去。)

相关文章
|
23天前
|
安全 算法 C++
【C/C++ 泛型编程 应用篇】C++ 如何通过Type traits处理弱枚举和强枚举
【C/C++ 泛型编程 应用篇】C++ 如何通过Type traits处理弱枚举和强枚举
46 3
|
25天前
|
安全 算法 编译器
【C++ 泛型编程 进阶篇】深入探究C++模板参数推导:从基础到高级
【C++ 泛型编程 进阶篇】深入探究C++模板参数推导:从基础到高级
240 3
|
25天前
|
设计模式 程序员 C++
【C++ 泛型编程 高级篇】C++模板元编程:使用模板特化 灵活提取嵌套类型与多容器兼容性
【C++ 泛型编程 高级篇】C++模板元编程:使用模板特化 灵活提取嵌套类型与多容器兼容性
241 2
|
2天前
|
编译器 C++
C++编程之美:探索初始化之源、静态之恒、友情之桥与匿名之韵
C++编程之美:探索初始化之源、静态之恒、友情之桥与匿名之韵
15 0
|
3天前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
|
9天前
|
编译器 C++
|
23天前
|
设计模式 安全 C++
【C++ const 函数 的使用】C++ 中 const 成员函数与线程安全性:原理、案例与最佳实践
【C++ const 函数 的使用】C++ 中 const 成员函数与线程安全性:原理、案例与最佳实践
70 2
|
24天前
|
存储 移动开发 安全
【C/C++ 口语】C++ 编程常见接口发音一览(不断更新)
【C/C++ 口语】C++ 编程常见接口发音一览(不断更新)
21 0
|
24天前
|
算法 编译器 C++
【C++ 模板编程 基础知识】C++ 模板类部分特例化的参数顺序
【C++ 模板编程 基础知识】C++ 模板类部分特例化的参数顺序
21 0
|
24天前
|
机器学习/深度学习 人工智能 算法
【C++ 职业方向】C++ 职业方向探索:工作职责、编程技能与MBTI人格匹配
【C++ 职业方向】C++ 职业方向探索:工作职责、编程技能与MBTI人格匹配
157 1