映月读书网 > C语言解惑 > 14.5 使用const >

14.5 使用const

因为早期C版本不支持const,所以人们在过去编写了大量不支持const的代码。因此,旧的C代码有很大的改进潜力。此外,许多用惯了早期C版本的程序员在使用ANSI C时,仍不使用const,这将使他们失去建立良好软件工程的机会。当然也要注意,尽管ANSI C对const做了很好的定义,但仍有某些系统不支持const。

在讨论使用const之前,先来看看左值和右值问题。

14.5.1 左值和右值

变量是一个指名的存储区域,左值是指向某个变量的表达式。名字“左值”来源于赋值表达式“A=B”,其中左运算分量“A”必须能被计算和修改。左值表达式在赋值语句中既可以作为左操作数,也可以作为右操作数,例如“x=56”和“y=x”,x既可以作为左值(x=56),又可以作为右值(y=x)。但右值“56”只能作为右操作数,而不能作为左操作数。

某些运算符可以产生左值。例如,如果“p”是一个被正确初始化的指针类型的表达式,则“p”就是左值表达式,可以通过“p=”改变这个指针的指向。同理,“*p”也是一个左值表达式,它代表由p指向的变量,并且可以通过“*p=”改变这个变量的值。假如有变量a,因为可以执行“a=*p”的操作,所以“*p”又是一个右值表达式。当然,也可以把“p”的值作为右值赋给一个指针变量,所以“p”也是一个右值表达式。

【例14.12】改正下面程序中的错误。


#include <stdio.h>
void main 
( 
)
{
     char a=\"We are here
!\"
,b[16]
;
     b=a
;
     printf
(b
);
     printf
(\"n\"
);
}
  

编译给出出错信息:


error C2106
: \'=\' 
: left operand must be l-valu
  

值可以作为右值,例如整数、浮点数、字符串、数组的一个元素等。在C语言中,右值是以单一值的形式出现。字符数组a和b的每个元素均可以作为右值,即“a[0]=b[0]”是正确的,但在这里,a和b都不是字符串的单个元素,所以是错误的。可以将它们修改成如下的程序,即使用具体的元素作为左值和右值。


#include <stdio.h>
void main 
( 
)
{
      char a=\"We are here
!\"
,b[16]
;
      int i=0
;
     while
(b[i]=a[i]
)
           i++
;
     printf
(b
);
     printf
(\"n\"
);
}
  

可能有人会说,a和b是可以作为右值的啊!确实,a和b可以作为数组首地址的值赋给指针变量,但它们不能作为左值。请看下面的程序。

【例14.13】下面是一个使用数组首地址作为右值的例子,目的是将数组a的内容复制到b中,运行输出的结果是


We are here
!
We are here
!
  

请问这个程序正确吗?


#include <stdio.h>
void main 
( 
)
{
      char a=\"We are here
!\"
,b[16]
,*p1
,*p2
;
      int i=0
;
      p1=a
;  p2=b
;
      p2=p1
;
      printf
(p1
);
      printf
(\"n\"
);
      printf
(p2
);
}
  

【分析】“p2=b;p2=p1;”最终是将指针p2指向字符串a,所以输出的不是字符串b的内容。如果执行“printf(b);”,就可以验证这一点。将“p2=p1;”改为


while
(p2[i]=p1[i]
)
     i++
;
  

即可。其实,用一个指针即可验证这个问题。


//
将数组b
的首地址作为右值的例子。
#include <stdio.h>
void main 
( 
)
{
      char a=\"We are here
!\"
,b[16]
,*p
;
      int i=0
;
      p=b
;
      while
(p[i]=a[i]
)
          i++
;
      printf
(a
);  printf
(\"n\"
);
      printf
(b
);  printf
(\"n\"
);
}
  

由此可见,在C语言中,左值是一个具体的变量,右值一定是一个具体类型的值,所以有些既可以作为左值,也可以作为右值,但有些只能作为右值。

14.5.2 推荐使用const定义常量

在C语言中,宏定义是一个重要内容。无参数的宏作为常量,而带参数的宏则可以提供比函数调用更高的效率。但预处理只是进行简单的文本代替而不做语法检查,所以会存在一些问题。例如:


#define BUFSIZE 100
  

这里的BUFSIZE只是一个名字,并不占用存储空间并且能被放在一个头文件中。在编译期间编译器将用字符串“100”来代替所有的BUFSIZE。这种简单的置换常常会隐藏一些很难发现的错误,并且这种方法还存在类型问题。比如这个BUFSIZE究竟是整数还是浮点数?而使用const,则把值代入编译过程即可解决这些问题,和上面宏定义等效的语句如下:


const int BUFSIZE=100
;
  

这样就可以在任何编译器需要知道这个值的地方使用BUFSIZE。并且编译器在编译过程中可以通过必要的计算把一个复杂的常量表达式缩减成简单的,这在定义数组时尤其突出。但是对于某些更复杂的情况来说,宏定义往往不如常量来得简洁清楚,可以用const来完全代替无参数的宏。

对基本数据类型的变量,一旦加上const修饰符,编译器就将其视为一个常量,不再为它分配内存,并且每当在程序中遇到它时,都用在说明时所给出的初始值取代它。使用const可以使编译器对处理内容有更多的了解,从而允许对其进行类型检查,同时还能避免对常量的不必要的内存分配并可改善程序的可读性。

因为被const修饰的变量的值在程序中不能被改变,所以在声明符号常量时,必须对符号常量进行初始化,除非这个变量是用extern修饰的外部变量。例如:


const int i=8
;          //
正确
const int d
;          //
错误
extern const int d
;     //
正确
  

const的用处不仅仅是在常量表达式中代替宏定义。如果一个变量在生存期中的值不会改变,就应该用const来修饰这个变量,以提高程序的安全性。

【例14.14】找出下面程序中的错误。


#include <stdio.h>
#define NUM1 2
#define NUM2 3
#define NUM3 6
void mul2
()
{
     int i
;
     for
(i=1
;i<NUM3
;i++
)
    { printf
(\"NUM1*%d=%dn\"
,i
,i*NUM1
);  }
}
 void mul3
()
 {
     int i
;
     for
(i=1
;i<NUM3
;i++
)
     { printf
(\"NUM2*%d=%dn\"
,i
,i*NUM2
); }
 }
 void main
()
 {
   mul2
();
   mul3
();
 }
  

程序引用宏定义的方法不对,语句


printf
(\"NUM1*%d=%dn\"
,i
,i*NUM1
);
  

中的“\"”表示后面是字符,所以NUM1作为字符串,这个函数将会输出为:


NUM1*1=2
NUM1*2=4
… 
…
NUM2*5=15
  

应改为


printf
(\"%d*%d=%dn\"
, NUM1
,i
,i*NUM1
);
printf
(\"%d*%d=%dn\"
, NUM2
,i
,i*NUM1
);
  

因为都是常数,推荐的做法是使用const,下面是修改后的程序。


#include <stdio.h>
int const NUM1= 2
;
int const NUM2 =3
;
int const NUM3 =6
;
void mul2
()
{
     int i
;
     for
(i=1
;i<NUM3
;i++
)
      { printf
(\"%d*%d=%dn\"
,NUM1
,i
,i*NUM1
);  }
}
void mul3
()
{
     int i
;
     for
(i=1
;i<NUM3
;i++
)
      { printf
(\"%d*%d=%dn\"
,NUM2
,i
,i*NUM2
);  }
}
void main
()
{
   mul2
();
   mul3
();
}
  

输出结果如下。


2*1=2
2*2=4
2*3=6
2*4=8
2*5=10
3*1=3
3*2=6
3*3=9
3*4=12
3*5=15
  

推荐使用const代替无参数的宏来定义常量。在编程中注意不要再使用宏定义常量。当然,常量是不能被改变的,它只能作为右值。

14.5.3 对函数传递参数使用const限定符

采用const声明传递函数参数,可以避免被调用函数修改实参的值。

【例14.15】演示在被调函数中不可改变传递参数的例子。


#include <stdio.h>
void swap
(int*
, const int
);
void main
( 
)
{
     int a=23
, b=85
;
     b=a+b
;     //
在主程序里可以修改变量b
,将它作为参数传递
     swap
(&a
,b
);     //
只允许被调函数使用b
,但不允许修改b
的值
     printf
(\"
返回调用函数:a=%d
, b=%dn\"
, a
, b
);
     b=b-a
;     //
可以继续修改变量b
     printf
(\"
可改变b
的值:a=%d
, b=%dn\"
, a
, b
);
}
void swap
(int* a
, const int b
)
{
   // b=a+b
;     //
这个语句修改了b
的值,编译出错
   *a=*a+b
;
   printf
(\"
在调用函数中:a=%d
, b=%dn\"
, *a
, b
);
}
  

运行结果如下。


在调用函数中:a=131
, b=108
在调用函数后:a=131
, b=108
可以改变b
值:a=131
, b=-23
  

在主程序声明的变量b,可以改变它的值。将它作为函数swap的参数传递时,为了保证在函数swap中只使用这个传递的值而不修改它,可以将这个参数声明为“const int b”,在被调函数swap中,语句“b=a+b;”企图改写b,则编译系统就会报错。

退出swap函数,保证了b的原来值。当然,这时候就可以改变b的值。

用const修饰传递参数,意思是通知函数,它只能使用参数而无权修改它。这主要是为了提高系统的自身安全。

例如设计的数组参数,不是供一个函数调用,所以要求任何函数调用时,都不能改变数组的内容。这时就要把数组参数用const限定。

【例14.16】不允许改变作为参数传递的数组内容的例子。


#include <stdio.h>
int add
(const int a
)
{
    int i=0
, sum=0
;
    for
(i=0
; i<5
; i++
)
         sum=sum+a[i]
;
    return sum
;
}
int mul
(const int a
)
{
    int i=0
, mul=1
;
    for
(i=0
; i<5
; i++
)
        mul=mul*a[i]
;
    return mul
;
}
void main
( 
)
{
     int i
, a={1
,2
,3
,4
,5}
;
     printf
(\"add=%d
,mul=%dn\"
,add
(a
),mul
(a
));
     for
(i=0
; i<5
; i++
)
          printf
(\"%d\"
,a[i]
);
     printf
(\"n\"
);
}
 

程序运行结果如下。


add=15
,mul=120
1 2 3 4 5
  

主函数使用同一个数组分别作为add和mul函数的参数,当然add和mul函数只能使用这个数组的内容,而不允许改变数组的内容。

14.5.4 对指针使用const限定符

可以用const限定符强制改变访问权限。用const正确地设计软件可以大大减少调试时间和不良的副作用,使程序易于修改和调试。

1.指向常量的指针

如果想让指针指向常量,就要声明一个指向常量的指针,声明的方式是在非常量指针声明前面使用const,例如:


const int *p
;     //
声明指向常量的指针
  

因为目的是用它指向一个常量,而常量是不能修改的,即“*p”是常量,不能将“*p”作为左值进行操作,这其实是限定了“*p=”的操作,所以称为指向常量的指针。当然,这并不影响P既可作为左值,也可作为右值,因此可以改变常量指针指向的常量。下面是在定义时即初始化的例子。


const int y=58
;          //
常量y
不能作为左值
const int *p=&y
;          //
因为 y
是常量,所以*p
不能作为左值
  

指向常量的指针p指向常量y,*p和y都不能作为左值,但可以作为右值。

如果使用一个整型指针p1指向常量y,则编译系统就要给出警告信息。这时可以使用强制类型转换。例如:


const int y=58
;          //
常量y
不能作为左值
int *p1
;               //*p1
既可以作为左值,也可以作为右值
p1=
(int *
)&y
;          //
因为 y
是常量,p1
不是常量指针,所以要将&y
进行强制转换
  

如果在声明p1时用常量初始化指针,也要进行转换。例如:


int *p1=
(int *
)&y
;     //
因为 y
是常量,p1
不是常量指针,所以要将&y
进行强制转换
  

在使用时,对于常量,要注意使用指向常量的指针。

2.指向常量的指针指向非常量

因为指向常量的指针可以先声明,后初始化,所以也会出现在使用时将它指向了非常量。所以这里用一个例子演示一下如果指向常量的指针p是指向普通变量,会发生什么情况。

【例14.17】使用指向常量的指针和非常量指针的例子。


#include <stdio.h>
void main
( 
)
{
     int x=45
;          //
变量x
能作为左值和右值
     const int y=58
;     //
常量y
不能作为左值,但可以作为右值
     const int *p
;          //
声明指向常量的指针
     int *p1
;          //
声明指针
     p=&y
;               //
用常量初始化指向常量的指针,*p
不能作为左值
     printf
(\"%d\"
,*p
);
     p=&x
;               //p
作为左值,使常量指针改为指向变量x
,*p
不能作为左值
     printf
(\"%d\"
,*p
);
     x=256
;          //
用x
作为左值间接改变*p
的值,使*p=x=256
     printf
(\"%d\"
,*p
);
     p1=
(int *
)&y
;          //
非常量指针指向常量需要强制转换
     printf
(\"%dn\"
,*p1
);
}
  

运行结果如下。


58 45 256 58
  

使用指向常量的指针指向变量时,虽然“*p”不能作为左值,但可以使用“x=”改变x的值,x改变,则也改变了*p的值,也就相当于把“*p”间接作为左值。所以说,这个const仅是限制直接使用“*p”作为左值,但可以间接使用“*p”作为左值,而“*p”仍然可以作为右值使用。

与使用非常量指针一样,也可以使用运算符“&”改变常量指针的指向,这当然也同时改变了“*p”的值。

必须使用指向常量的指针指向常量,否则就要进行强制转换。当然也要避免使用指向常量的指针指向非常量,以免产生操作限制,除非是有意为之。

以上结论可以从程序的运行结果中得到验证。

3.常量指针

把const限定符放在*号的右边,就使指针本身成为一个const指针。因为这个指针本身是常量,所以编译器要求给它一个初始化值,即要求在声明的同时必须初始化指针,这个值在指针的整个生存期中都不会改变。编译器把“p”看作常量地址,所以不能作为左值(即“p=”不成立)。也就是说,不能改变p的指向,但“*p”可以作为左值。

【例14.18】使用常量指针的例子。


#include <stdio.h>
void main
( 
)
{
      int x=45
,y=55
,*p1
;               //
变量x
和y
均能作为左值和右值
      int const sum=100
;               //
常量sum
只能作为右值
      int * const p=&x
;               //
声明常量指针并使用变量初始化
      int * const p2=
(int *
)&sum
;          //
使用常量初始化常量指针,需要强制转换
      printf
(\"%d %d\"
,*p
,*p2
);
      x=y
;                         //
通过左值x
间接改变*p
的值,使*p=55
      printf
(\"%d\"
,*p
);
      *p=sum
;                    //
直接用*p
作为左值,使*p=100
      printf
(\"%d\"
,*p
);
      *p2=*p2+sum+*p
;               //*p2
作为左值,使*p2=300
      printf
(\"%d\"
,*p2
);
      p1=p
;                         //p
作为左值,使指针p1
与常量指针p
的指向相同
      printf
(\"%dn\"
,*p1
);
}
 

运行结果如下。


45 100 55 100 300 100
  

语句“x=y;”和“*p=sum;”,都可以改变x的值,但p指向的地址不能改变。

显然,常量指针是指这个指针p是常量,既然p是常量,当然p不能作为左值,所以定义时必须同时用变量对它初始化。对常量而言,需用使用指向常量的指针指向它,含义是这个指针指向的是不能做左值的常量。不要使用常量指针指向常量,使用常量指针就需要进行强制转换。

4.指向常量的常量指针

也可以声明指针和指向的对象都不能改动的“指向常量的常量指针”,这时也必须初始化指针。例如:


int x=2
;
const int* const p=&x
;
  

告诉编译器,*p和p都是常量,都不能作为左值。这种指针限制了“&”和“*”运算符,所以在实际的应用中很少用到这种特殊指针。

5.void指针

一般情况下,指针的值只能赋给相同类型的指针。Void类型不能声明变量,但可以声明void类型的指针,而且void指针可以指向任何类型的变量。

【例14.19】演示void指针的例子。


#include <stdio.h>
void main
( 
)
{
      int x=56
, y=65
,*p=&x
;
      void *vp=&x
;                    //void
指针指向x
      printf
(\"%d
,%d
,%dn\"
,vp
,p
,x
);
      vp=&y
;                    //void
指针改为指向y
      p=
(int *
)vp
;                    //
强制将void
指针赋值给整型指针
      printf
(\"%d
,%d
,%dn\"
,vp
,p
,*p
);
}
  

虽然void指针指向整型变量对象x,但不能使用*vp引用整型对象的值。要引用这个值,必须强制将void指针赋值给与值相对应的整型指针类型。程序输出如下。


1245052
,1245052
,56
1245048
,1245048
,65