Const 作用
1. const类型定义:指明变量或对象的值是不能被更新,引入目的是为了取代预编译指令
2. 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
3. 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
4. 可以节省空间,避免不必要的内存分配。
例如:
#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定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
对于基本声明
1. const int r=100; //标准const变量声明加初始化,因为默认内部连接所以必须被初始化,其作用域为此文件,编译器经过类型检查后直接用100在编译时替换
2. extend const int r=100; //将const改为外部连接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义
但是如果外部想链接r,不能这样用
extern const int r=10; //错误!常量不可以被再次赋值
3. const int r[ ]={1,2,3,4};
struct S {int a,b;};
const S s[ ]={(1,2),(3.4)}; //以上两种都是常量集合,编译器会为其分配内存,所以不能在编译期间使用其中的值,例如:int temp[r[2]];这样的编译器会报告不能找到常量表达式
但是
const int Max=100;
int Array[Max];
正确。
还有
定义数组必须用常量,可以用const或者#define定义。 Static 虽然是编译时确定,也不能用来声明数组。
对于指针和引用
1. const int *r=&x; //声明r为一个指向常量的x的指针,r指向的对象不能被修改,但他可以指向任何地址的常量
pointer const 可以指定普通变量,用改指针不能修改它指向的对象,并不表示指向的对象是const不能被改变,例如:
int i = 10;
const int * p = &i;
*p = 11; //wrong
i = 11 ; //correct
自己的一个经验:一个具体的概念可以用范型的概念来赋值,但是一个范型的概念不能用具体的概念来赋值。
我们可以把const指针当成普通指针的父类,因为普通指针改写了const属性,而具有比const指针更多的功能。 这样的话只有父类指针可以指向子类,而子类指针不能指向父类。
2. int const *r=&x; //与用法1完全等价,没有任何区别
3. int * const r=&x; //声明r为一个常量指针,他指向x,r这个指针的指向不能被修改,但他指向的地址的内容可以修改
4. const int * const r=&x; //综合1、3用法,r是一个指向常量的常量型指针
5. const double & v; 该引用所引用的对象不能被更新
引用必须定义是初始话,而且初始化后这个引用不能指向其他的对象。但是这里加的const声明不是这个意思,它是指不能改变v引用对象本身,也就是只能调用该对象里面的const成员函数。
对于类型检查
可以把一个非const对象赋给一个指向const的指针,因为有时候我们不想从这个指针来修改其对象的值;但是不可以把一个const对象赋值给一个非const指针,因为这样可能会通过这个指针改变指向对象的值,但也存在使这种操作通过的合法化写法,使用类型强制转换可以通过指针改变const对象:
const int r=100;
int * ptr = const_cast<int*>(&r); //C++标准,C语言使用:int * ptr =(int*)&r;
对于字符数组
如char * name = “china”; 这样的语句,在编译时是能够通过的,但是”china”是常量字符数组,任何想修改他的操作也能通过编译但会引起运行时错误,如果我们想修改字符数组的话就要使用char name[ ] = “china”; 这种形式。
对于函数
1. void Fuction1 ( const int r ); //此处为参数传递const值,意义是变量初值不能被函数改变
2. const int Fuction1 (int); //此处返回const值,意思指返回的原函数里的变量的初值不能被修改,但是函数按值返回的这个变量被制成副本,能不能被修改就没有了意义,它可以被赋给任何的const或非const类型变量,完全不需要加上这个const关键字。但这只对于内部类型而言(因为内部类型返回的肯定是一个值,而不会返回一个变量,不会作为左值使用),对于用户自定义类型,返回值是常量是非常重要的,见下面条款3。
3. Class CX; //内部有构造函数,声明如CX(int r =0)
CX Fuction1 () { return CX(); }
const CX Fuction2 () { return CX(); }
如有上面的自定义类CX,和函数Fuction1()和Fuction2(),我们进行如下操作时:
Fuction1() = CX(1); //没有问题,可以作为左值调用
Fuction2() = CX(1); //编译错误,const返回值禁止作为左值调用。因为左值把返回值作为变量会修改其返回值,const声明禁止这种修改。
4. 函数中指针的const传递和返回:
int F1 (const char * pstr); //作为传递的时候使用const修饰可以保证不会通过这个指针来修改传递参数的初值,这里在函数内部任何修改*pstr的企图都会引起编译错误。
const char * F2 (); //意义是函数返回的指针指向的对象是一个const对象,它必须赋给一个同样是指向const对象的指针。
const char * const F3(); //比上面多了一个const,这个const的意义只是在他被用作左值时有效,它表明了这个指针除了指向const对象外,它本身也不能被修改,所以就不能当作左值来处理。
5. 函数中引用的const传递:
void F1 ( const X& px); //这样的一个const引用传递和最普通的函数按值传递的效果是一模一样的,他禁止对引用的对象的一切修改,唯一不同的是按值传递会先建立一个类对象的副本,然后传递过去,而它直接传递地址,所以这种传递比按值传递更有效。
**另外只有引用的const传递可以传递一个临时对象,因为临时对象都是const属性,且是不可见的,他短时间存在一个局部域中,所以不能使用指针,只有引用的const传递能够捕捉到这个家伙。
6. 有一点可以注意一下
const为函数重载提供了一个参考。
class A
{
......
void f(int i) {......} file://一个函数
void f(int i) const {......} file://上一个函数的重载
......
};
关于函数overloading, 不能根据返回值类型来确定
double max( int a, int b);
int max( int a, int b);
也不能根据参数的默认值来判断
int max( int a, int b);
int max( int a, int b, int c=12);
一句话不能让编译器有多个选择就ok了
对于类
1. 首先,对于const的成员变量,只能在构造函数里使用初始化成员列表来初始化,试图在构造函数体内进行初始化const成员变量会引起编译错误。初始化成员列表形如:
X:: X ( int ir ): r(ir) {} //假设r是类X的const成员变量
2. const成员函数。提到这个概念首先要谈到const对象,正象内置类型能够定义const对象一样(const int r=10;),用户自定义类型也可以定义const对象(const X px(10);),编译器要保证这个对象在其生命周期内不能够被改变。如果你定义了这样的一个const对象,那么对于这个对象的一切非const成员函数的调用,编译器为了保证对象的const特性,都会禁止并在编译期间报错。所以如果你想让你的成员函数能够在const对象上进行操作的话,就要把这个函数声明为const成员函数。假如f( )是类中的成员函数的话,它的声明形如:
int f( ) const; //const放在函数的最后,编译器会对这个函数进行检查,在这个函数中的任何试图改变成员变量和调用非const成员函数的操作都被视为非法
**类的构造和析构函数都不能是const函数。
3. 建立了一个const成员函数,但仍然想用这个函数改变对象内部的数据。这样的一个要求也会经常遇到,尤其是在一个苛刻的面试考官那里。首先我们要弄清楚考官的要求,因为有两种方法可以实现,如果这位考官要求不改变原来类的任何东西,只让你从当前这个const成员函数入手,那么你只有使用前面提到的类型强制转换方法。实例如下:
//假如有一个叫做X的类,它有一个int成员变量r,我们需要通过一个const成员函数f( )来对这个r进行++r操作,代码如下
void X::f( ) const
{ (const_cast<X*>(this)) -> ++r; } //通过this指针进行类型强制转换实现
另外一种方法就是使用关键字:mutable。如果你的成员变量在定义时是这个样子的:
mutable int r ;
那么它就告诉编译器这个成员变量可以通过const成员函数改变。编译器就不会再理会对他的检查了。
关于const一些问题
[思考1]: 以下的这种赋值方法正确吗?
const A_class* c=new A_class();
A_class* e = c;
这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
[思考2]: 以下的这种赋值方法正确吗?
A_class* const c = new A_class();
A_class* b = c;
这种方法正确,因为声明指针所指向的内容可变;
[思考3]: 这样定义赋值操作符重载函数可以吗?
const A_class& operator=(const A_class& a);
不正确;在const A_class::operator=(const A_class& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:A_class a,b,c:(a=b)=c;因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。
说了这么多,你认为 const 意味着什么?一种修饰符?接口抽象?一种新类型?
也许都是,在 Stroustup 最初引入这个关键字时,只是为对象放入 ROM 做出了一种可能,对于 const 对象, C++ 既允许对其进行静态初始化,也允许对他进行动态初始化。理想的 const 对象应该在其构造函数完成之前都是可写的,在析够函数执行开始后也都是可写的,换句话说, const 对象具有从构造函数完成到析够函数执行之前的不变性,如果违反了这条规则,结果都是未定义的!虽然我们把 const 放入 ROM 中,但这并不能够保证 const 的任何形式的堕落,我们后面会给出具体的办法。无论 const 对象被放入 ROM 中,还是通过存储保护机制加以保护,都只能保证,对于用户而言这个对象没有改变。换句话说,废料收集器(我们以后会详细讨论,这就一笔带过)或数据库系统对一个 const 的修改怎没有任何问题。
class A
{
public:
......
A f(const A& a);
......
};
最大的好处是可以很容易地检测到违反位元 const 规定的事件:编译器只用去寻找有没有对数据成员的赋值就可以了。另外,如果我们采用了位元 const ,那么,对于一些比较简单的 const 对象,我们就可以把它安全的放入 ROM 中,对于一些程序而言,这无疑是一个很重要的优化方式。(关于优化处理,我们到时候专门进行讨论)
看看下面这个例子:
class A
{
private:
const int c3 = 7; // ???
static int c4 = 7; // ???
static const float c5 = 7; // ???
......
};
那么,我们的标准委员会为什么做这样的规定呢?一般来说,类在一个头文件中被声明,而头文件被包含到许多互相调用的单元去。但是,为了避免复杂的编译器规则, C++ 要求每一个对象只有一个单独的定义。如果 C++ 允许在类内部定义一个和对象一样占据内存的实体的话,这种规则就被破坏了。
一种方法就是 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;
我们给出下面的代码:
const int size[3]={10,20,50};
int array[size[2]];
const 可以用于集合,但编译器不能把一个集合存放在它的符号表里,所以必须分配内存。在这种情况下, const 意味着 “ 不能改变的一块存储 ” 。然而,其值在编译时不能被使用,因为编译器在编译时不需要知道存储的内容。自然,作为数组的大小就不行了:)
你再看看下面的例子:
class A
{
public:
A(int i=0):test[2]({1,2}) {} file://你 认为行吗?
private:
const int test[2];
} ;
vc6 下编译通不过,为什么呢?
这里我们看到,常量与数组的组合没有什么特殊!一切都是数组惹的祸!
( 6 ) this 指针是不是 const 类型的?
this 指针是一个很重要的概念,那该如何理解她呢?也许这个话题太大了,那我们缩小一些: this 指针是个什么类型的?这要看具体情况:如果在非 const 成员函数中, this 指针只是一个类类型的;如果在 const 成员函数中, this 指针是一个 const 类类型的;如果在 volatile 成员函数中 ,this 指针就是一个 volatile 类类型的。
先看一下下面的例子:
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://?????
......
};
这 个程序是正确的,看来上面的结论是错误的。为什么会这样呢?这要涉及到接口的透明度问题。按值传递时,对用户而言,这是透明的,用户不知道函数对形参做了 什么手脚,在这种情况下进行重载是没有意义的,所以规定不能重载!当指针或引用被引入时,用户就会对函数的操作有了一定的了解,不再是透明的了,这时重载 是有意义的,所以规定可以重载。
以下是我想到的可能情况,当然,有的编译器进行了优化,可能不分配内存。
A 、作为非静态的类成员时;
B 、用于集合时;
C 、被取地址时;
D 、在 main 函数体内部通过函数来获得值时;
E 、 const 的 class 或 struct 有用户定义的构造函数、析构函数或基类时;。
F 、当 const 的长度比计算机字长还长时;
G 、参数中的 const ;
H 、使用了 extern 时。
不知道还有没有其他情况,欢迎高手指点:)
( 9 )临时变量到底是不是常量?
假设有一个类:
class A
{
public:
......
static void f() const { ......}
......
};
我们发现编译器会报错,因为在这种情况下 static 不能够与 const 共存!
为什么呢?因为 static 没有 this 指针,但是 const 修饰 this 指针,所以 ...
( 11 )如何修改常量?
有时候我们却不得不对类内的数据进行修改,但是我们的接口却被声明了 const ,那该怎么处理呢?我对这个问题的看法如下:
1 )标准用法: mutable
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const { test=i; }
private:
mutable int test; file://这 里处理!
} ;
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const
{ const_cast <int>(test)=i; }// 这里处理!
private:
int test;
}
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;
}
既然编译器可以动态初始化常量,就自然可以动态创建,例如:
const int* pi=new const int(10);
1 ) const 对象必须被初始化!所以 (10) 是不能够少的。
2 ) new 返回的指针必须是 const 类型的。
那么我们可不可以动态创建一个数组呢?答案是否定的,因为 new 内置类型的数组,不能被初始化。
这里我们忽视了数组是类类型的,同样对于类内部数组初始化我们也做出了这样的忽视,因为这涉及到数组的问题,我们以后再讨论。