关于C/C++中const关键字的思考

const关键字不仅可以用来定义常量,还可以修饰函数的形参、返回值,以及成员函数的this指针。适当的使用const,可以有效避免由于程序员的疏忽造成的变量的改动,很好地提高程序的健壮性。下面分情况讨论const在不同场景下的作用:

一、定义常量

const int x = 1;   //定义常量x

const定义的常量和用宏定义#define定义的常量不同之处在于:const常量是有类型的;而宏定义的常量无类型,使用的时候仅仅是简单的字符串替换(这一操作由预处理器执行)。
在使用const常量时,应该注意和非const变量之间的赋值操作,具体情况如下:

1.1 简单的赋值(无引用或指针)

    const int x1 = 1;
    int y1 = x1;    //ok

    int x2 = 1;
    const int y2 = x2;   //ok

简单赋值(传值方式,无引用或指针)操作过程中,实际上是将右操作数的值拷贝一份至左操作数地址,因此两个操作数所指的数据是独立的,互不影响。

1.2 常量=变量(引用或指针)

    int x1 = 1;
    const int &y1 = x1;    //ok

    int value = 1;
    int *x2 = &value;
    const int *y2 = x2;    //ok

允许常量的引用(指针)指向一个变量。如此一来,不可以通过y1(或*y2)更改对应内存空间的值1,但仍可通过x1(或 *x2)更改。

1.3 变量=常量(引用或指针)(重要原则)

    const int x1 = 1;
    int &y1 = x1;    //error

    int value = 1;
    const int *x2 = &value;
    int *y2 = x2;    //error

不允许变量的引用(指针)指向一个常量,因为编译器会认为程序员可能通过这个变量更改常量的值,这是不被允许的。

tips: (以下观点是作者自己理解的,如有误,望指正)变量与常量的区别是在“语言层次”上而言的,在程序员设计程序时,通过适当地设计常量和变量,完成相应的功能。可变量或常量所指向的内存地址,没有常量或者变量之分,任何变量都可以对其进行更改。正如例1.2的代码示例中,x1和y1都指向同一内存地址,y1是常量,不能修改其值,但可以通过x1修改,实际上也对y1的值修改了。

    int x1 = 1;
    const int &y1 = x1;

    x1 = 2;   //ok

二、修饰函数形参

关于函数传参问题,请参照博客文章:C/C++中函数传参方式简述,此处不再赘述。

如果函数内部无修改形参操作,则最好将形参定义为const。

2.1 非const引用形参

如果函数形参定义为非const引用的话,则形参实际上是实参的一个别名,不产生实参的副本。但这种定义形参方式在使用时不太灵活,因为其即不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。(《C++ Primer》 P205)因此“应将不需修改的引用形参定义为const引用”。(类似上面1.3所述)

2.2 const引用形参

如果函数形参定义为const引用的话,不产生实参的副本的同时,函数不能修改形参。

void StringCopy(char *strDestination, const char *strSource);
void swap ( int * const p1 , int * const p2 );  //限制在函数swap内修改指针p1和p2的指向

三、修饰函数返回值

用const修饰函数返回值并不常用,在这里简单的列举一下几种情况:

3.1 “传值”返回值

如果返回值以“传值”方式返回给主调函数,这种情况下添加const修饰毫无意义。

如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。例如把函数int GetInt(void) 写成const int GetInt(void)是没有意义的。

3.2 “传址”返回值

如果返回值以“传址”方式返回给主调函数,即返回值为指针,那么返回值(指针指向内容)不能被修改,且只能被赋值给const修饰的常量指针。(常量指针与指针常量的区别,在文章末尾有解释)

const char * GetString(void);
char *str = GetString();   //error
const char *str = GetString();   //ok

3.3 “传引用”返回值

被调函数以“传引用”方式将返回值返回主调函数,类似3.2。

tips:对函数返回值使用 const 的另一个目的在于:限制不能将函数调用表达式作为左值使用。示例如下:

int & min ( int &i, int &j);
可以对函数调用进行赋值,因为它返回的是左值: min ( a , b )=4;
但是,如果对函数的返回值限定为 const 的,即定义:const int & min ( int & i, int &j );
那么,就不能对 min ( a, b ) 调用进行赋值了。

四、修饰成员函数(函数体/对象/*this)

C++中,const还可以用来修饰成员函数,即修饰该类实例化的对象的this指针。

关于this指针,《C++ Primer》P377中有如下描述:

在普通的非const成员函数中,this的类型是一个指向类类型对象的const指针:可以改变this所指向的值,但不能改变this所保存的地址。
在const成员函数中,this的类型是一个指向const类类型对象的const指针:既不能改变this所指向的对象,也不能改变this所保存的地址。

上述描述的意思就是,const修饰的成员函数实际上可以理解为Type const *this,即此成员函数中的对象被视为const对象,不能更改数据成员。

不过const的位置很特殊,在声明的尾部,估计是语言设计者发现,其它的地方都被占用了吧~~
const修饰的成员函数,不能修改类/对象的数据成员。因此,任何不会修改数据成员的成员函数都应该声明为const 类型

如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。

class MyClass {
    int member ;
    public:
    int getCount ( ) const;
};

classname :: getCount( )
{   member =4 ;   //error,“只读成员函数”,不能修改数据成员
    return member;
}

关于const成员函数须注意以下几点:
a. const成员函数不可以修改对象的数据,不管对象是否具有const性质。它在编译时,以是否修改成员数据为依据,进行检查;
b. const成员函数不能在函数中调用其它非const成员函数;(非const成员函数可能修改数据成员)
c. const对象只能访问const成员函数;而非const对象既可以访问非const成员函数,也可以访问const成员函数;(const对象的数据成员不能被修改,而非const成员函数可能对其修改)

五、关于const的其它知识点

5.1 const的链接属性

关于变量或函数的链接属性,请转至:链接属性与存储类型

const对象默认的链接属性为internal,即文件的局部变量。

a. 在全局作用域里定义非const变量时,它在整个程序中都可以访问。

//《C++ Primer》 P50
//全局变量具有外部链接属性
//file1.cc
int counter;  //define
//file2.cc
extern int counter;   //uses counter from file1
++counter;    //increments counter defined in file1

b. 在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量值存在于那个文件中,不能被其它文件访问。通过执行const变量为extern(显式修改链接属性),就可以在整个程序中访问const对象。

//又如(《C++ Primer》 P50例子)
//const变量具有内部链接属性,默认值存在于定义的文件中,不能被其它文件访问
//file_1.cc
//defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();   //用extern修改const变量的链接属性

//file_2.cc
extern const int bufSize;  //uses bufSize from file_1
for(int index=0;index!=bufSize;++index);    //uses bufSize defined int file_1
    //...

5.2 const定义的常量转换至非const变量

我们先来看一段示例代码:

    int i = 0;
    const int &j = i;  //ok,隐式转换,int -> const int

    const int x = 1;
    int &y = x;     //error
    int &y = const_cast<int&>(x);    //ok,显式转换,const int -> int

通过上述代码可见,const引用变量可以与非const变量绑定,反之则报错(非const引用变量可能修改const变量的值)。此时需要借助C++中的const_cast进行强制类型转换。
const_cast,顾名思义,就是修改表达式的const属性。const_cast仅能去掉(或添加,不常用)变量的const属性,如果用它来完成其它类型的强制转换,都会引起编译错误。

C++几种强制类型转换可见另一篇文章:C++四种强制类型转换运算符的联系与区别

tips:
a. const_cast不会改变原变量的const属性,引用C++类型转换详解–const_cast的一段话:

使用const_cast去掉const属性,其实并不是真的改变原类类型(或基本类型)的const属性,它只是又提供了一个接口(指针或引用),使你可以通过这个接口来改变类型的值。也许这也是const_case只能转换指针或引用的一个原因吧。

    int i = 0;
    const int &j = const_cast<const int&>(i);
    i = 1;    //ok

    const int x = 1;
    int &y = const_cast<int&>(x);
    x = 0;   //error

b. const_cast可能出现“常量重叠”问题,即同一个地址具有两个不同的值(关于const变量与const_cast),使用时要注意。

5.3 常量指针与指针常量

从字面意思看,“常量指针”侧重指针:指向常量的指针;而“指针常量”侧重常量:指针类型的常量。

5.3.1 常量指针

const int *p;
int const *p;

这个被指向对象不能通过指针更改,但可以通过原来的声明更改;可以改变该指针指向的对象(对象地址,指针变量的值)。

int a=1;
const int* p=&a;
*p=3;   //error
a=3;  //ok

5.3.2 指针常量

int* const p;

可以通过该指针修改指向对象的值,但不能更改指针指向的对象。

int b=2;
int* const q=&b;
q=&a;  //error
*q=3;   //ok

tips: const修饰谁,谁不能变

参考资料:
[C/C++] const 详解(修饰变量、输入参数、返回值、成员函数)
修饰函数和函数返回值的const的差别
const参数,const返回值与const函数
const修饰符总结
const的内部链接属性

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章