您的位置:首页 > 其它

const用法总结

2014-10-27 15:39 148 查看
先用一个比较接近的词来描述关键字const的含义:“只读”。
以下从修饰变量、函数、类三个角度进行分析与总结。

***************************************************

修饰变量:
1)const一般变量:
const int value = 10;

这是“C Primer Plus”中讲到的三种创建符号常量方法之一,另外两种是宏定义和枚举。

define宏定义的符号常量是明显常量,在编译时会直接用实际的值代替,在程序运行时所有的替代都已经完成。

作为常量限定符,const相比于#define的优点在于:

a.能够明确指定类型,可用于复杂/自定义的数据类型

b.有了第一条,const可以做类型安全检查,而宏定义不行

c.可将定义限定在特定的函数内或文件中

d.可以对const常量进行调试(实实在在的变量),不能对宏进行调试

在C++中,通常,定义一个const值来表示数组元素个数是个好方法,在后面声明数组和引用数组长度时可以使用该const值,替代原来C语言中宏定义的方法。

2)修饰于指针的特殊情况:两种区别通过“const后面的不能修改”来理解

a.常量指针(常指针):

int* const p = &a;
同上面一样,定义时必须赋初值,变量p的值不能修改,即定义后不能指向其他变量,但a可以是可修改的;

b.指针常量:
int a=2,b;
const int *p2;
p2 = &a;
//*p2 = 20;
a = 20;
p2 = &b;
指针所保存的地址可以改变,但指针所指向的值却不可以通过该指针改变。当添加*p2 = 20时,会发生编译错误!(*p2 不能修改
上面a不是常量,可以修改;p2是可以指向其他变量的。

这种用法是很常见的,其微妙之处在于指针只用来取值,不用来修改!常将这种方法用于向函数传递数组、字符串、类对象等参数。

c.当然了,还可以有:
int a = 0;
const int * const p
= &a;;
p是指向常量的常指针,即不能修改p的值,也不能通过p修改值,很好理解,没什么必要记。

3)const修饰全局变量
这个也是需要提一下的。
C语言中:外部链接,存放在只读数据段(.rdata)
C++中:内部链接,它可以被编译器放到符号表中作为编译期常量,(注意“可以”两个字,在该符号出现的某些地方,直接用常量代替),默认是不会进入导出符号表。

因此,在C++中,const会改变全局变量默认的作用域,使其仅在文件内有效;
如果不同源文件包含了同一个放const常量的头文件,就相当于每个文件有自己的一组常量。
(对于局部的,在函数或代码块内声明const时,其作用域为相应代码块)。
因此,不必担心与其他常量名冲突,也很容易理解下面这一点:
const int k = 2;
int a[k]; //在C++中是合法的,而在C中是非法的。

而在C语言中,文件之间共享有两个写法:“C Primer Plus” P361
a.只在一处定义,在另一处明确指明外部链接特性:
/* f1.c */
const int a = 1; //定义
/* f2.c */
extern const int a; //声明

b.在头文件中声明,在源文件中定义
/* constant.h */
static const int a = 1;
/* f1.c */

#include "constant.h"

/* f2.c */

#include "constant.h"

如前面所述,

如果是在C语言中,则头文件中必须声明static,限定作用域,因为C语言中的const全局变量默认是外部链接特性的,作用域为整个程序。

如果是在C++中,就可以不加static了。

补充:

从反汇编看
const int i =1;
00112FC3 mov dword ptr [ebp-98h],1 //将const局部变量存于栈中
int j = i;
00112FCD mov dword ptr [ebp-0A4h],1 //但这里是直接替换,编译器优化的结果,i的值不用运行就可以确定
int * const p = &j;
013D2FD7 lea eax,[ebp-0A4h]
013D2FDD mov dword ptr [ebp-0B0h],eax //将j的地址赋予p
int * k = p;
013D2FE3 mov eax,dword ptr [ebp-0B0h]
013D2FE9 mov dword ptr [ebp-0BCh],eax
const常量是实实在在存在于内存的“变量”,编译器处理时与宏定义有本质的区别。

***************************************************
修饰函数:
1)修饰形参,用于传递不被修改的参数,这是用得最多的一种用途
void foo(const int arr[], int n)
声明arr指向的是常量数据,从而保护数组,在C++中尽量使用这样的写法。

同时,编译器也禁止将常量数组的地址赋给非常量指针作为实参:
const int months[12]= {31,...};
int sum(int arr[],int n);
//int j = sum(months,12); //一个const变量(实参)不能随意转化为非const变量

对于这一用法总结两条理由: “C++ Primer Plus” P201
a. 可以避免无意间修改数据
b. 使得函数能够处理const和非const实参,否则将只能接受非const数据。(这二条容易漏掉,也很重要)

2)修饰返回值
a. const int f1() 没有什么意义
b. 常用的是用于返回自定义类对象的引用:
将函数返回值视为常量(临时变量),类似于修饰变量时进行分析即可。

除非有人写:
int a = (f1())++; //编译错误!
这类糟糕的代码,不然不用特意关注这一类用法。

3)const成员函数:注意“成员”两个字
void stock::show() const
保证成员函数不会修改调用对象;

如果是一般函数呢?
int add(int *a, int *b) const
将会报错:“非成员函数上不允许修饰符”!!

***************************************************
修饰类:
1)const成员变量
class Stock
{
public:
Stock( int x):Len(x){}
private:
const int Len;
//使用时要注意
//const int Len = 30;
//char company[Len];
}
与一般的const变量不同,const成员变量并不是在声明处定义与初始化的,因为类声明只是描述对象的形式,并没有创建对象,创建对象之前是没有存储空间的。
const成员变量对应于具体的对象才有意义:在类的每一次实例化时被初始化,在这个对象的生存周期中不可改变。
而且由于是const的变量,初始化是必须的,所以形如上面利用初始化列表对Len进行初始化的构造函数是必不可少的,否则编译不能通过(“没有合适的默认构造函数可用”)。

注意上面注释掉的方法是行不通的,因为需要为const变量分配内存。
但是如果确实需要在类中声明一个数组,希望将数组长度定义成了一个常量,方便一次性修改呢?
“C++ Primer Plus” P327给出了一种方法:
class Stock
{
private:
static const int Len
= 5;
char company[Len];
};

补充:简单测试不同类型存放的内存区域
class A
{
public:
static const int m
= 1;
};

class B
{
public:
static int m;
};
int B::m = 0;

static const int x = 1; //只读数据段(.rdata)
static int y = 0; //初始化数据段(.data)

int main(void )
{
A a;
cout<<&a.m<< " "<<&x<<endl;
B b;
cout<<&b.m<< " "<<&y<<endl;
return 0;
}
结果为:
a.m地址比x地址低4个字节,b.m地址比y地址低4个字节,各自在不同区域。

2)const成员函数:

上面已经提到了。
这里补充:
在重载时,C++区分常量和非常量函数的特征标,即可以:

class A{

int foo(int &x){}

int foo(int &x) const{}

};

***************************************************
补充1:const引用,在C++非常常用,也是一种好的编程习惯!
作形参时:引用属于“传地址”的一种参数传递方式,而指针却是属于“传拷贝”的一种方式,二者是有本质差别的,但不是这里的讨论重点。

对于对传递的值不作修改的函数,指导原则: “C++ Primer Plus” P240

a.内置类型或小型结构,则值传递

b.数组,则使用const指针,这是唯一方法

c.较大的结构 ,则使用const指针或const引用,避免复制

d.类对象,则使用const引用。类设计的语义常常要求使用引用,这也是C++新增这一特性的主要原因,传递类对象的标准方式是按引用传递。

const Stock & Stock::topval(const Stock
& s) const
{
if (s.total_val > total_val) //能够读取私有变量
return s;
else
return *this ;
//如何返回对象的引用
}
这里有三个const。(返回const引用通常是为了满足连续操作的需要,如连续赋值)。

***************************************************
补充2:const_cast
const int a=6;
int *p=&a;
不能通过编译,因为它消除了a的const属性。
在C++中,对于这样的情况,某个值大多数情况是常量,而有时又是可以修改的。这种情况使用const_cast实现:
const_cast:要求目标类型和表达式的类型完全一致,返回值删除了原来的const特性

int main(void )
{
const int a
= 1;
int *p = const_cast <int*>(&a);
int &q = const_cast <int&>(a);
*p = 2;
cout << "value a="<< a << endl; //输出1,这是因为a变量编译期常量,被编译器优化了,直接用1代替
cout << "value *p=" <<*p
<< endl; //输出2,a所在内存单元(在栈中)存放的数据已通过p更改为2
return 0;
}

***************************************************
补充3:volatile

“C++ Primer Plus” P277

volatile(不稳定的, ['vɒlətaɪl]):

与const合起来称为cv限定符,即使程序代码没有对内存单元进行修改,其值也可能发生变化(如硬件导致、两个程序共享内存)。

作用:改善编译器的优化能力。未声明的话,编译器在两次读取某个变量值时,可能并非查找两次,而是先将值缓存到寄存器中,使得该变量值再这两次取值间不会变化。volatile阻止这种优化。

用volatile声明的变量,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

几个例子(嵌入式程序员要熟悉):

1). 并行设备的硬件寄存器(如:状态寄存器)

2). 一个中断服务子程序中会访问到的非自动变量

3). 多线程应用中被几个任务共享的变量

曾有网友利用下面的问题来考查对这两个关键字的理解(对不起忘记出处了)。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。

2). 一个指针可以是volatile 吗?解释为什么。

3). 下面的函数有什么错误:

int square(volatile int *ptr)

{return *ptr * *ptr;}

下面是答案:

1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2). 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

3). 这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)

{int a,b;

a = *ptr;

b = *ptr;

return a * b;}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)

{int a;

a = *ptr;

return a * a;}

***************************************************
补充4:mutable(易变的):
即使结构或类变量为const,其某个成员也可以被修改。
struct data{

char name[10];

mutable int accesses;



};
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: