Daily-C-Study(7):C语言const关键字

Daily-C-Study(7):C语言const关键字

成于坚持,败于止步

const 是constant 的缩写,是恒定不变的意思,也翻译为常量、常数等。很不幸,正是因为这一点,很多人都认为被const 修饰的值是常量。这是不精确的,精确的说应该是只读的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容(这里你该怀疑我是不是在说梦话了,不知他存储的内容那我们怎么使用啊,我会对说话的话负责的,稍等O(∩_∩)O~),精确的说应该是不能被write,不能被修改。或许当初这个关键字应该被替换为readonly。

const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。我们看看它与define 宏的区别。很多人误以为define 是关键字,但是事实上32 个关键字里是没有define的一席之地的。

const 修饰的只读变量

定义const 只读变量,具有不可变性。例如:

const int Max=100;

int Array[Max];

这里请在Visual C++6.0 里分别创建.c 文件和.cpp 文件测试一下。你会发现在.c 文件中,编译器会提示出错,而在.cpp 文件中则顺利运行。为什么呢?我们知道定义一个数组必须指定其元素的个数。这也从侧面证实在C 语言中,const 修饰的Max 仍然是变量,只不过是只读属性罢了;而在C++里,扩展了const 的含义,在后面在讨论这一部分吧。

注意:const 修饰的只读变量必须在定义的同时初始化,因为过了这个村就没这个店了,之后你被禁止改变这个变量了难道不是吗?
这里给大家个实例便于理解,请动手测试一下。
#include 
void main(void)
{
	int const dat = 2;
	int x = 0,z;
	switch (dat)
	{
		case 0:
		  x += 1;
		  break;
		case 1:
		  x += 2;
		  break;
		case 2:
		  x += 3;
		  break;
		default:break;
	}
	z = x;
}

这个代码是无法编译通过的,当然首先你得知道case这家伙有点洁癖,case 语句后面只能是常量,那么为什么上面报错也就不言而喻了,case抱怨自己后面跟的不是常量,自然就拒绝编译通过啊,这里也就验证了我们上面说过的话,const修饰的变量只是readonly,但是他本身还是变量,跟常量没有一毛钱关系O(∩_∩)O~

节省空间,避免不必要的内存分配,同时提高效率

编译器通常不为普通const 只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。这里就为我上面的话好好解释解释,我没有大放厥词,是的,const修饰的变量定义时根本没有分配存储空间,他只是暂时保存在符号表里面的,你用的时候直接用就好了,不用经过内存区read,即使想read也是无法read的,更不用说write了,可以理解了吧?O(∩_∩)O~例如:

#define M 3 //宏常量

const int N=5; //此时并未将N 放入内存中

......

int i=N; //此时为N 分配内存,以后不再分配!

int I=M; //预编译期间进行宏替换,分配内存

int j=N; //没有内存分配

int J=M; //再进行宏替换,又一次分配内存!

const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const 定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个拷贝。

#define 宏是在预编译阶段进行替换,而const 修饰的只读变量是在编译的时候确定其值。

#define 宏没有类型,而const 修饰的只读变量具有特定的类型。

修饰一般变量

一般常量是指简单类型的只读变量,最好我们还是不要把它叫做常量比较妥当。这种只读变量在定义时,修饰符const 可以用在类型说明符前,也可以用在类型说明符后。例如:

int const i=2; 或const int i=2;

修饰数组

定义或说明一个只读数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};或

const int a[5]={1, 2, 3, 4, 5};

修饰指针

const int *p; // p 可变,p 指向的对象不可变

int const *p; // p 可变,p 指向的对象不可变

int *const p; // p 不可变,p 指向的对象可变

const int *const p; //指针p 和p 指向的对象都不可变

这里很容易混淆这几种情况。这里给出一个记忆和理解的方法:先忽略类型名(编译器解析的时候也是忽略类型名),我们看const 离哪个近。“近水楼台先得月”,离谁近就修饰谁。

const int *p; //const 修饰*p,p 是指针,*p 是指针指向的对象,不可变

int const *p; //const修饰*p,p 是指针,*p 是指针指向的对象,不可变

int *const p; //const修饰p,p 不可变,p 指向的对象可变

const int *const p; //前一个const 修饰*p,后一个const 修饰p,指针p 和p 指向的对象都不可变

修饰函数的参数
const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使用。例如:

void Fun(const int i);

告诉编译器i 在函数体中的不能改变,从而防止了使用者的一些无意的或错误的修改。

这里还是要多花些时间了,const配合指针,引用使用一直是个难点:

(a)常量与指针

常量与指针放在一起很容易让人迷糊。对于常量指针和指针常量也不是所有的学习C/C++的人都能说清除。例如:

const int *m1 = new int(10);

int* const m2 = new int(20);

在上面的两个表达式中,最容易让人迷惑的是const到底是修饰指针还是指针指向的内存区域?其实,只要知道:const只对它左边的东西起作用,唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。根据这个规则来判断,m1应该是常量指针(即,不能通过m1来修改它所指向的内容。);而m2应该是指针常量(即,不能让m2指向其他的内存模块)。由此可见:

1. 对于常量指针,不能通过该指针来改变所指的内容。即,下面的操作是错误的:

int i = 10;

const int *pi = &i;

*pi = 100;

因为你在试图通过pi改变它所指向的内容。但是,并不是说该内存块中的内容不能被修改。我们仍然可以通过其他方式去修改其中的值。例如:

// 1: 通过i直接修改。

i = 100;

// 2: 使用另外一个指针来修改。

int *p = (int*)pi;

*p = 100;

实际上,在将程序载入内存的时候,会有专门的一块内存区域来存放常量。但是,上面的i本身不是常量,是存放在栈或者堆中的。我们仍然可以修改它的值。而pi不能修改指向的值应该说是编译器的一个限制。

2. 根据上面const的规则,const int *m1 = new int(10);我们也可写作:

int const *m1 = new int(10);

这是,理由就不须作过多说明了。

3. 在函数参数中指针常量时表示不允许将该指针指向其他内容。

void func_02(int* const p)
{
	int *pi = new int(100);
	//错误!P是指针常量。不能对它赋值。
	p = pi;
}

int main()
{
	int* p = new int(10);
	func_02(p);
	delete p;
	return 0;
}
4. 在函数参数中使用常量指针时表示在函数中不能改变指针所指向的内容。
void func(const int *pi)
{
	//错误!不能通过pi去改变pi所指向的内容!
	*pi = 100;
}

int main()
{
	int* p = new int(10);
	func(p); 
	delete p;
	return 0;
}
我们可以使用这样的方法来防止函数调用者改变参数的值。但是,这样的限制是有限的,作为参数调用者,我们也不要试图去改变参数中的值。因此,下面的操作是在语法上是正确的,但是可能破还参数的值:
#include 
#include 
void func(const int *pi)
{
	//这里相当于重新构建了一个指针,指向相同的内存区域。当然就可以通过该指针修改内存中的值了。
	int* pp = (int*)pi;
	*pp = 100;
}

int main()
{
	using namespace std;
	int* p = new int(10);
	cout << "*p = " << *p << endl;
	func(p);
	cout << "*p = " << *p << endl;
	delete p;
	return 0;
}

(b)常量与引用

常量与引用的关系稍微简单一点。因为引用就是另一个变量的别名,它本身就是一个常量。也就是说不能再让一个引用成为另外一个变量的别名, 那么他们只剩下代表的内存区域是否可变。即:

int i = 10;

// 正确:表示不能通过该引用去修改对应的内存的内容。

const int& ri = i;

// 错误!不能这样写。

int& const rci = i;

const int& ri = i;

// 错误!不能这样写。

int& const rci = i;

由此可见,如果我们不希望函数的调用者改变参数的值。最可靠的方法应该是使用引用。下面的操作会存在编译错误:

void func(const int& i)
{
	// 错误!不能通过i去改变它所代表的内存区域。
	i = 100;
}

int main()
{
	int i = 10;
	func(i);
	return 0;
}

这里已经明白了常量与指针以及常量与引用的关系。但是,有必要深入的说明以下。在系统加载程序的时候,系统会将内存分为4个区域:堆区 栈区 全局区(静态)和代码区。从这里可以看出,对于常量来说,系统没有划定专门的区域来保护其中的数据不能被更改。也就是说,使用常量的方式对数据进行保护是通过编译器作语法限制来实现的。我们仍然可以绕过编译器的限制去修改被定义为“常量”的内存区域。看下面的代码:

const int i = 10;

// 这里i已经被定义为常量,但是我们仍然可以通过另外的方式去修改它的值。

// 这说明把i定义为常量,实际上是防止通过i去修改所代表的内存。

int *pi = (int*) &i;

*pi=100;这种方法就可以实现修改这个内存地址的数据了

七、修饰函数的返回值

const 修饰符也可以修饰函数的返回值,返回值不可被改变。例如:

const int Fun (void);

在另一连接文件中引用const 只读变量:

extern const int i; //正确的声明

extern const int j=10; //错误!只读变量的值不能改变。

注意这里是声明不是定义,关于声明和定义的区别,不知道还是要搞搞清楚的

这里还是接着再花点时间说说C++陷阱对const的讲解吧

看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。

const是constant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。

1.用const修饰函数的参数

如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。

2.const只能修饰输入参数:
如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。

例如StringCopy函数:

void StringCopy(char *strDestination, const char *strSource);

其中strSource是输入参数,strDestination是输出参数。给strSource加上const修饰后,如果函数体内的语句试图改动strSource的内容,编译器将指出错误。

3. 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。

例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A为用户自定义的数据类型。

4. 对于非内部数据类型的参数而言,像void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。

为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。
以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。

说一下,内部数据类型基本包括以下类型:Byte、Boolean、Integer、Long、Single、Double、Currency、Decimal、Date、Object 以及 String
是不是开始纠结了啊,我只好将“const &”修饰输入参数的用法总结一下:

a.对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。

b.对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。

5. 如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。

例如函数

const char * GetString(void);

如下语句将出现编译错误:

char *str = GetString();

正确的用法是

const char *str = GetString();

6. 如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值。

例如不要把函数int GetInt(void) 写成const int GetInt(void)。

同理不要把函数A GetA(void) 写成const A GetA(void),其中A为用户自定义的数据类型。

如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出 错。见6.2节“返回值的规则”。

函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。这个地方其实还是很难于理解的

就先到这里,O(∩_∩)O~

我的专栏地址:http://blog.csdn.net/column/details/c-daily-study.html

待续。。。。。。

你可能感兴趣的:(C-Daily-Study,C语言每天一小步)