const 用法详解
const
The const keyword can be used to tell the compiler that a certain variable should not be modified once it has been initialized. It can also be used to declare functions of a class that do not alter any class data.
Usage
It is considered good practice to use const wherever appropriate to protect data from being unintentionally overwritten. Attempting to shoehorn const into a program after it has been written will create a cascade effect. It is best to implement const early in the code development cycle. This brings us to the proper declaration and usage of const.
The const keyword can take on multiple meanings and be used in a variety of locations (even nonsensical places).
Declarations
To understand what the const is protecting, read from right to left.
const char * str;
// pointer to characters that cannot be changed
(although the pointer can be redirected)
char const * str;
// same as above (just an alternate way of writing it)
char * const str;
// cannot change the pointer to characters
(although the characters themselves can be changed)
Similarly for C++ references (you cannot apply const to a reference,
since a reference cannot be redirected):
const char & str;
// reference to character that cannot be changed
char const & str;
// same as above
This seems easy enough, however more complicated resolutions can be
more difficult to interpret. Consider:
char * const * data;
// pointer to unchangeable pointer of characters
char const ** data;
// pointer to pointer of unchangeable characters
const char ** data;
// pointer to pointer of unchangeable characters
char ** const data;
// unchangeable pointer to pointer of characters
char * const * const data;
// unchangeable pointer to unchangeable
pointer of characters
There are some who would have you believe that you MUST
place the const after the type, however you are free to place
it either before or after the type, if it is a regular non-pointer
type (e.g. “char”). Use the format that matches your existing
code or your organizations coding standards. Like anything else,
just be consistent. If you want the const to apply to the pointer,
you must place const after the asterisk.
It is also good practice to declare certain fields of an object to be
const if it is a property of the object that does not change over
the life of the object.
Parameters
The most common usage of const is to protect data that is pointed to or referenced:
void func ( const MyObject * data );
// MyObject cannot be changed in func
void func ( const MyObject & data );
// MyObject cannot be changed in func
Note that the placement of the const before or after the type
is irrelevant. The following is equivalent:
void func ( MyObject const * data );
// same as ( const MyObject * )
void func ( MyObject const & data );
// same as ( const MyOjbect & )
However, placement of the const after the pointer or reference
changes what is “const”. A const following a pointer protects
only the pointer, not the data to which it points.
void func ( MyObject * const data );
// unnecessary protection of the copied pointer to MyObject
Inside func, you are free to manipulate MyObject, but not the pointer
to MyObject. However, since the pointer value is a local to the
function (the pointer was passed by value when the function was
called), this isn't helpful, as nobody outside the function will be
affected by whether you change the pointer or not anyway.
Placing the const after a reference is entirely useless and should be
avoided. References cannot be redirected (i.e. they are already implicitly const).
void func ( MyObject & const data );
// useless protection of the reference to MyObject
Here, the const is protecting the reference which can never be manipulated anyway.
Sometimes it is useful to return private data from an object. However, we don't want the private data manipulated outside the class.
const MyObject & MyClass::func ( MyObject & data );
// the MyObject returned by func cannot be changed
Methods
Often instance methods do not manipulate any data in the objects. These methods, which are also known as accessors, should be declared const. The effect of this is that the this pointer inside the method, instead of being a “MyClass *”, will now be a “const MyClass *”, so that they cannot modify the object through the this pointer.
void MyClass::func ( MyOjbect & data ) const;
// this function does not manipulate class data
Note that if you have a const object, you may only call const methods on that object. (The reason is that when you call a method, you need to pass a pointer to the object as the this pointer. But if you have a “const MyClass” object, then you can only get a “const MyClass *”, not a “MyClass *”, so you can only call const methods.)
void MyClass::const_func() const;
void MyClass::func();
const MyClass object;
object.const_func(); // ok
object.func(); // can't call non-const function in a const object
For the same reason, inside a const method, you can only call other const methods of the object.
1、什么是const?
常类型是指使用类型修饰符const说明的类型,常类型的变量或对
2、为什么引入const?
const 推出的初始目的,正是为了取代预编译指令,消除它的缺点
3、cons有什么主要的作用?
(1)可以定义const常量,具有不可变性。
例如:
const int Max=100;
int Array[Max];
(2)便于进行类型检查,使编译器对处理内容有更多了解
例如:
void f(const int i) { .........}
编译器就会知道i是一个常量,不允许修改;
(3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调
同宏定义一样,可以做到不变则已,一变都变!如(1)中
只需要:const int Max=you want;即可!
(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
还是上面的例子,如果在函数体内修改了i,编译器就会报错;
例如:
void f(const int i) { i=10;//error! }
(5) 为函数重载提供了一个参考。
class A
{
......
void f(int i) {......} file://一个函数
void f(int i) const {......} file://上一个函数的重载
......
};
(6) 可以节省空间,避免不必要的内存分配。
例如:
#define PI 3.14159 file://常量宏
const doulbe Pi=3.14159; file://此时并未将Pi放入ROM中
......
double i=Pi; file://此时为Pi分配内存,以后不再分配!
double I=PI; file://编译期间进行宏替换,分配内存
double j=Pi; file://没有内存分配
double J=PI; file://再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址
(7) 提高了效率。
编译器通常不为普通const常量分配存储空间,而是将它们保存在
3、如何使用const?
(1)修饰一般常量
一般常量是指简单类型的常量。这种常量在定义时,修饰符const
例如:
int const x=2; 或 const int x=2;
(2)修饰常数组
定义或说明一个常数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
(3)修饰常对象
常对象是指对象常量,定义格式如下:
class A;
const A a;
A const a;
定义常对象时,同样要进行初始化,并且该对象不能再被更新
(4)修饰常指针
const int *A; file://const修饰指向的对象,A可变,A指向的对象不可变
int const *A; file://const修饰指向的对象,A可变,A指向的对象不可变
int *const A; file://const修饰指针A, A不可变,A指向的对象可变
const int *const A; file://指针A和A指向的对象都不可变
(5)修饰常引用
使用const修饰符也可以说明引用,被说明的引用为常引用
const double & v;
(6)修饰函数的常参数
const修饰符也可以修饰函数的传递参数,格式如下:
void Fun(const int Var);
告诉编译器Var在函数体中的无法改变,从而防止了使用者的一些无
(7)修饰函数的返回值:
const修饰符也可以修饰函数的返回值,是返回值不可被改变
const int Fun1();
const MyClass Fun2();
(8)修饰类的成员函数:
const修饰符也可以修饰类的成员函数,格式如下:
class ClassName
{
public:
int Fun() const;
.....
};
这样,在调用函数Fun时就不能修改类里面的数据
(9)在另一连接文件中引用const常量
extern const int i; file://正确的引用
extern const int j=10; file://错误!常量不可以被再次赋值
另外,还要注意,常量必须初始化!
例如:
const int i=5;
4、几点值得讨论的地方:
(1)const究竟意味着什么?
说了这么多,你认为const意味着什么?一种修饰符?接口抽象
也许都是,在Stroustup最初引入这个关键字时
(2)位元const V.S. 抽象const?
对于关键字const的解释有好几种方式,最常见的就是位元con
class A
{
public:
......
A f(const A& a);
......
};
如果采用抽象const进行解释,那就是f函数不会去改变所引用对
我们可以看到位元解释正是c++对const问题的定义
为什么这样呢?因为使用位元const有2个好处:
最大的好处是可以很容易地检测到违反位元const规定的事件
当然,位元const也有缺点,要不然,抽象const也就没有产
首先,位元const的抽象性比抽象const的级别更低
其次,使用位元const的库接口会暴露库的一些实现细节
有时,我们可能希望对const做出一些其它的解释,那么
(3)放在类内部的常量有什么限制?
看看下面这个例子:
class A
{
private:
const int c3 = 7; // ???
static int c4 = 7; // ???
static const float c5 = 7; // ???
......
};
你认为上面的3句对吗?呵呵,都不对!使用这种类内部的初始化语法
那么,我们的标准委员会为什么做这样的规定呢?一般来说
(4)如何初始化类内部的常量?
一种方法就是static 和 const 并用,在内部初始化,如上面的例子;
另一个很常见的方法就是初始化列表:
class A
{
public:
A(int i=0):test(i) {}
private:
const int i;
};
还有一种方式就是在外部初始化,例如:
class A
{
public:
A() {}
private:
static const int i; file://注意必须是静态的!
};
const int A::i=3;
(5)常量与数组的组合有什么特殊吗?
我们给出下面的代码:
const int size[3]={10,20,50};
int array[size[2>;
有什么问题吗?对了,编译通不过!为什么呢?
const可以用于集合,但编译器不能把一个集合存放在它的符号表
你再看看下面的例子:
class A
{
public:
A(int i=0):test[2]({1,2}) {} file://你认为行吗?
private:
const int test[2];
};
vc6下编译通不过,为什么呢?
关于这个问题,前些时间,njboy问我是怎么回事?我反问他:
呵呵,看了这一段冠冕堂皇的话,真让我笑死了!njboy别怪我揭
这里我们看到,常量与数组的组合没有什么特殊!一切都是数组惹的祸
(6)this指针是不是const类型的?
this指针是一个很重要的概念,那该如何理解她呢
(7)const到底是不是一个重载的参考对象?
先看一下下面的例子:
class A
{
......
void f(int i) {......} file://一个函数
void f(int i) const {......} file://上一个函数的重载
......
};
上面是重载是没有问题的了,那么下面的呢?
class A
{
......
void f(int i) {......} file://一个函数
void f(const int i) {......} file://?????/
......
};
这个是错误的,编译通不过。那么是不是说明内部参数的const不
class A
{
......
void f(int& ) {......} file://一个函数
void f(const int& ) {......} file://?????/
......
};
这个程序是正确的,看来上面的结论是错误的。为什么会这样呢
(8)什么情况下为const分配内存?
以下是我想到的可能情况,当然,有的编译器进行了优化
A、作为非静态的类成员时;
B、用于集合时;
C、被取地址时;
D、在main函数体内部通过函数来获得值时;
E、const的 class或struct有用户定义的构造函数、析构函数或基类时
F、当const的长度比计算机字长还长时;
G、参数中的const;
H、使用了extern时。
不知道还有没有其他情况,欢迎高手指点:)
(9)临时变量到底是不是常量?
很多情况下,编译器必须建立临时对象。像其他任何对象一样
10)与static搭配会不会有问题?
假设有一个类:
class A
{
public:
......
static void f() const { ......}
......
};
我们发现编译器会报错,因为在这种情况下static不能够与co
为什么呢?因为static没有this指针,但是const修饰
(11)如何修改常量?
有时候我们却不得不对类内的数据进行修改,但是我们的接口却被声明
1)标准用法:mutable
class A
{
public:
A(int i=0):test(i) { }
void Setvalue(int i)const { test=i; }
private:
mutable int test; file://这里处理!
};
2)强制转换:const_cast
class A
{
public:
A(int i=0):test(i) { }
void Setvalue(int i)const
{ const_cast <int>(test)=i; }//这里处理!
private:
int test;
};
3)灵活的指针:int*
class A
{
public:
A(int i=0):test(i) { }
void Setvalue(int i)const
{ *test=i; }
private:
int* test; file://这里处理!
};
4)未定义的处理
class A
{
public:
A(int i=0):test(i) { }
void Setvalue(int i)const
{ int *p=(int*)&test; *p=i; }//这里处理!
private:
int test;
};
注意,这里虽然说可以这样修改,但结果是未定义的,避免使用!
5)内部处理:this指针
class A
{
public:
A(int i=0):test(i) { }
void Setvalue(int i)const
{ ((A*)this)->test=i; }//这里处理!
private:
int test;
};
6)最另类的处理:空间布局
class A
{
public:
A(int i=0):test(i),c('a') { }
private:
char c;
const int test;
};
int main()
{
A a(3);
A* pa=&a;
char* p=(char*)pa;
int* pi=(int*)(p+4);//利用边缘调整
*pi=5; file://此处改变了test的值!
return 0;
}
虽然我给出了6中方法,但是我只是想说明如何更改
(12)最后我们来讨论一下常量对象的动态创建。
既然编译器可以动态初始化常量,就自然可以动态创建,例如:
const int* pi=new const int(10);
这里要注意2点:
1)const对象必须被初始化!所以(10)是不能够少的。
2)new返回的指针必须是const类型的。
那么我们可不可以动态创建一个数组呢?
答案是否定的,因为new内置类型的数组,不能被初始化
在C中,处理器可以不受限制的建立宏并用它来代替值,因为预处理器只做文本代替,没有类型检查,所以会产生一些问题。CPP中的const直接可以取代c中的#define。
1. 限定符声明变量只能被读
const int i=5;
int j=0;
i=j; //非法,导致编译错误
j=i; //合法
2. 必须初始化
const int i=5; //合法
const int j; //非法,导致编译错误
3. 在另一连接文件中引用const常量
file1.cpp: const int i=200;
file2.cpp :
#include "file1.cpp"
extern const int i; //合法
extern const int j=10; //非法,常量不可以被再次赋值
4. 便于进行类型检查(???)
用const方法可以使编译器对处理内容有更多了解。
#define I=10
const long &i=10;
/* 提醒:由于编译器的优化,使
得在const long i=10; 时 i 不被分配内存,而是以10直接代入
以后的引用中,以致在以后的代码中没有错误,为达到说教效
果,特别地用&i明确地给出了i的内存分配。不过一旦你关闭所
有优化措施,即使const long i=10;也会引起后面的编译错误。*/
char h=I; // 没有错
char h=i; // 编译警告,可能由于数的截短带来错误赋值。
5. 可以避免不必要的内存分配
#define STRING "abcdefghijklmn\n"
const char string[]="abcdefghijklm\n";
...
printf(STRING); //为STRING分配了第一次内存
printf(string); //为string一次分配了内存,以后不再分配
...
printf(STRING); //为STRING分配了第二次内存
printf(string);
...
由于const定义常量从汇编的角度来看,只是给出了对应的内存地址,
而不是象#define一样给出的是立即数,所以,const定义的常量在
程序运行过程中只有一份拷贝,而#define定义的常量在内存中有
若干个拷贝。
6. 可以通过函数对常量进行初始化
int value();
const int i=value();
dapingguo说:假定对ROM编写程序时,由于目标代码的不可改写,
本语句将会无效,不过可以变通一下:
const int &i=value();
只要令i的地址处于ROM之外,即可实现:i通过函数初始化,而其
值有不会被修改。
7. 是不是const的常量值一定不可以被修改呢?
观察以下一段代码:
const int &i=10;
int *p=(int*)&i;
*p=100;
经验证 : i=100;
若: const int i = 10 ;
int *p = (int *) & i ;
*p = 100 ;
此时: i = 10 没有发生改变
常引用的值可改变,但是常量的值没法改变
通过强制类型转换,将地址赋给变量,再作修改即可以改变const常量值。
8. 请分清数值常量和指针常量,以下声明颇为玩味:
int ii=0;
const int i=0; //i是常量,i的值不会被修改
const int *p1i=&i; //指针p1i所指内容是常量,可以不初始化
int * const p2i=ⅈ //指针p2i是常量,所指内容可修改
const int * const p3i=&i; //指针p3i是常量,所指内容也是常量
p1i=ⅈ //合法
*p2i=100; //合法
1. const常量,如const int max = 100;
优点:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)
2. const 修饰类的数据成员。如:
class A
{
const int size;
…
}
const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。如
class A
{
const int size = 100; //错误
int array[size]; //错误,未知的size
}
const数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。如
class A
{…
enum {size1=100, size2 = 200 };
int array1[size1];
int array2[size2];
}
枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
3. const修饰指针的情况,见下式:
int b = 500;
const int* a = &b [1]
int const *a = &b [2]
int* const a = &b [3]
const int* const a = &b [4]
如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。不知道,也没关系,我们可以参考《Effective c++》Item21上的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的 右侧,const就是修饰指针本身,即指针本身是常量。因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无 关),这种情况下不允许对内容进行更改操作,如不能*a = 3 ;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常 量。
4. const的初始化
先看一下const变量初始化的情况
1) 非指针const常量初始化的情况:A b;
const A a = b;
2) 指针const常量初始化的情况:
A* d = new A();
const A* c = d;
或者:const A* c = new A();
3)引用const常量初始化的情况:
A f;
const A& e = f; // 这样作e只能访问声明为const的函数,而不能访问一
般的成员函数;
[思考1]: 以下的这种赋值方法正确吗?
const A* c=new A();
A* e = c;
[思考2]: 以下的这种赋值方法正确吗?
A* const c = new A();
A* b = c;
5.另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const 可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如下几种情况,以下会逐渐的说明用法:
A& operator=(const A& a);
void fun0(const A* a );
void fun1( ) const; // fun1( ) 为类成员函数
const A fun2( );
1) 修饰参数的const,如 void fun0(const A* a ); void fun1(const A& a);
调 用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const A* a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A& a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
[注意]:参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。
[总结]: 对于非内部数据类型的输入参数,因该将“值传递”的方式改为“const引用传递”,目的是为了提高效率。例如,将void Func(A a)改为void Func(const A &a)
对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x)不应该改为void Func(const int &x)
2) 修饰返回值的const,如const A fun2( ); const A* fun3( );
这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
返回值用const修饰可以防止允许这样的操作发生:Rational a,b;
Radional c;
(a*b) = c;
一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
[总结]
1. 一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对 某个对象引用的情况。原因如下:如果返回值为某个对象为const(const A test = A 实例)或某个对象的引用为const(const A& test = A实例) ,则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少 用到。
2. 如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。如:
const char * GetString(void);
如下语句将出现编译错误:
char *str=GetString();
正确的用法是:
const char *str=GetString();
3. 函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。如:
class A
{
…
A &operate = (const A &other); //负值函数
}
A a,b,c; //a,b,c为A的对象
…
a=b=c; //正常
(a=b)=c; //不正常,但是合法
若负值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。
[思考3]: 这样定义赋值操作符重载函数可以吗?
const A& operator=(const A& a);
6. 类成员函数中const的使用
一般放在函数体后,形如:void fun() const;
任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。如:
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; //const 成员函数
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++m_num; //编译错误,企图修改数据成员m_num
Pop(); //编译错误,企图调用非const函数
Return m_num;
}
7. 使用const的一些建议
1 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2 要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
3 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
4 const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5 不要轻易的将函数的返回值类型定为const;
6除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
[思考题答案]
1 这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
2 这种方法正确,因为声明指针所指向的内容可变;
3 这种做法不正确;
在const A::operator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
A a,b,c:
(a=b)=c;
因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。