有关const的一些事

 

说说const,这也是我学习《thinking in c++》和《effective c++》中关于const内容的学习笔记。
一、最基本用法——定义常量
C++中编译器是尽量避免为const分配内存的,而是在进行类型检查之后将从定义时的初始化式(要求是常量表达式)中计算来的值折叠到代码里(即进行值替代),然后存放到符号表(symbol table)。
C++中const定义的时候必须进行初始化,这是因为:
初始化式用来区分定义和声明,而在C中,无论写
const int x;
还是
extern const int x;
编译器都将其看做是声明,所以不需要初始化。
而在C++中,这么写
const int x;
是不允许的,要声明必须加上extern,表明定义在别处(elsewhere)。
这个规定也是从const的不可赋值性得出的。只能初始化不能定义,又必须有值存在,而初始化又只能发生在定义出(初始化是从无到有,赋值是从有到有)。
前面提到,编译器是尽量避免为const分配内存的,那么什么情况会迫使编译器为const分配内存呢?
以下情况:
1.初始化式的值过于复杂使得编译器无法计算,这样编译器为const分配内存。
2.初始化表达式不是编译期常量,如const int i = cin.get();编译器无法在编译期确定常量的值,因此为其分配内存。
3.const数组,编译器不确定符号表足够盛放整个数组,于是一律分配内存,不在符号表里保留信息。
4.任何对一个const取地址的行为(包括将其传给一个形参为引用的函数这样的不自觉取地址行为)会导致内存分配,但是编译器仍然是知道其值的,符号表里仍然有其信息,编译器仍将它看做编译期常量。这和上面三种情况不同。
1-3情况,编译器在编译期无法得到const的值,也就不能作为常量来使用它,比如不能用其作为定义数组时的大小。

二、const与指针搭配使用
两种使用方式:使指针所指之物为const;使指针所存地址为const。
Pointer to const
如果想让指针所指之物为常量,而指针所含地址可以更改,在定义时将const写在*的左面。然而,这只是说不能通过指针更改其所指之物,此被指之物本身不必是const,也就是不通过指针还是可以更改的。又由于指针本身并不是const,所以定义指针时不必初始化。两种定义格式完全相等,如下:
const int* pi;
int const* pi;
const pointer
如果想让指针为const,即指针不能再指向其它之物,而指针所指之物可以被更改,当然也可以通过指针被更改,那么应将const写在*右面,由于指针是const的,所以定义时必须初始化,如下:
int d = 1;
int* const pi = &d;
也可以让指针同时具有两种属性,还是两种等价的定义格式:
int d = 1;
const int* const p1 = &d;
int const* const p2 = &d;
这样,指针和所指之物就都是const的了。仍然不需要所指之物本身是const的。
可以用non-const之物的地址给指向const之物的指针赋值,不可以把const之物的地址赋给指向non-const之物的指针,除非使用强制转换,那就破坏了const带来的常量性。
关于指向字符串字面值的指针
虽然指向字符串字面值的指针不必是pointer to const,但是字符串字面值是编译器创建的且不容许更改的,因此若通过non-const指针更改其内容,虽无任何编译链接错误,却会导致运行期错误。如下:
char* p = “string”; //可以通过编译,但技术上是错误的
*p = ‘a’;  //可以通过编译,但会引起运行期错误,实际上是未定义行为,有些机器上也会可以工作
编译器之所以允许non-const指针用字符串字面值初始化,是向大多数依赖于此的C代码的妥协。
要相对字符串字面值作出更改,请使用数组:
char p[] = “string”;

三、用于函数参数和返回值
如果以pass-by-value方式传递函数参数,那么将其声明为const对客户来说毫无意义,参数在函数内部本身就是一个临时对象,它是否改变不影响客户手里的参数原本,因此这个promise毫无意义。所以,这样做是对函数编写者的限制,而不是调用者。为了不迷惑调用者,可以把const的声明从参数列表挪到函数体内,用reference实现:
void f(int i)
{
 const int& j = i;
 j++; //Illegal — compile-time error
}
对于pass-by-value的函数返回值,存在同样的问题,当写:
const int f();
只是保证函数体内的原返回值不被改变,由于是按照pass-by-value返回的,所以这种保证毫无意义而只会让客户迷惑。
当处理以pass-by-value返回用户自定义类型的函数时,const才显得重要。
对于内置类型的pass-by-value返回的函数,编译器阻止函数调用结果作为左值,而对于用户自定义类型,编译器不做此组织行为,除非将返回值声明为const。用const修饰返回值从而阻止用户自定义类型的返回值作为左值,可以避免if(f()=a)这样的错误(本想做比较而不是赋值)。

如果以指针和引用作为函数参数,那么const参数比non-const参数版本的函数更具一般性:
即可以将non-const指针和变量传递给const参数的函数,反之则不可。
对于临时变量,可以将其传递给接受const引用的函数,而不能传递给接收non-const引用的函数,因为对临时变量的更改是不允许的。
临时变量是不能被取值用于指针传递的。

四、类中的使用
1)类中的编译期常量
首先,不能在类的定义里简单地使用const作为编译期常量定义数组大小了,因为const在每个对象中都会被分配内存,所以编译器在编译期间不知道它的值,这里的“const”只保证在此对象的生命期内该成员不被改变,而不能保证各个对象之间的const成员一致,也就不是编译期常量。
用于产生可以在类中可以使用的编译期常量,有两种方法:
1.使用enum:
enum {size = 10};
int buf[size];
枚举的所有值必须在编译期建立,且不会占用对象中的存储空间,枚举常量在编译期被全部求值。
2.使用static const:
classicA{
private:
 static const int size = 5;
 int buf[size];
};
由于这是类的定义,所以这仅仅是size的声明而非定义。
通常C++要求使用到的东西都有一份定义,但是如果是类中的专属常量,又是static且为整数类型(int,char,bool),则需特殊处理。只要不对其取地址就可以只提供声明而不需定义就能使用。
如果不得不定义,由于声明中已经给了初值,定义中不再需要初值,定义应放在.cpp中而非.h。

2)类中的const成员
如果一个类中有const成员,由于const变量必须在定义时被初始化,所以需要特殊处理:
首先const变量的定义及初始化肯定要发生在类的构造函数里,但是又不能到进入构造函数体内再进行,因为在进入构造函数体内之前,构造函数会调用类中各成员的构造函数来定义各个成员,成员真正的定义时刻发生在此时,构造函数体内发生的是赋值而非初始化。所以正确的做法是在构造函数的初始化列表里提供const成员的初值,用来指导对const变量构造函数的调用。

3)const对象与const成员函数
类和其它内置类型一样,可以声明const的对象。然而对于类这样复杂的类型,编译器如何保证其const性呢?
编译器的做法是只允许const对象调用被声明为const的成员函数,然后再对const成员函数的行为进行限制,从而保证对象的常量性。
声明一个成员函数为const的语法是在函数声明的末尾加上const:
int f() const;
定义时仍要重复此写法否则编译器不将其作为同一个函数。
int A::f() const{
 //do something…
}
构造函数和析构函数都肯定不是const的,因为它们的工作不可避免要更改对象。
const对象只能调用const成员函数,non-const对象则无限制。
如在一个const函数里,想做更改对象的事,怎么办?
还是有办法的:
1.强制转换:在函数内取this指针,这时它是const指针,将其强制转换成non-const指针,即可利用它进行有关操作,编译器不再保护此指针对应的对象的const性:
void Y::f() const{
 ((Y*)this)->j++;
}
2.使用关键字mutable,讲一个成员声明为mutable,说明它在const函数中可被改变而不影响对象的常量性。而且可以让用户心中有数。

--The End

你可能感兴趣的:(c,工作,String,table,reference,编译器)