C/C++中CONST的意义

const 关键字来声明某个常量字段或常量局部变量。 常量字段和常量局部变量不是变量并且不能修改。 常量可以为数字、布尔值、字符串或 null 引用。 采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见用途是作为数组的界和switch分情况标号(也可以用枚举符代替)

常变量: const 类型说明符 变量名

常引用: const 类型说明符 &引用名

常对象: 类名 const 对象名

常成员函数: 类名::fun(形参) const

常数组: 类型说明符 const 数组名[大小]

常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名

首先提示的是:在常变量(**const 类型说明符 变量名)、**常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。如:

const int a=5; 与 int const a=5; 等同

​ 类名 const 对象名 与 const 类名 对象名 等同

用法1:常量
取代了C中的宏定义,声明时必须进行初始化(!c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
C标准中,const定义的常量是全局的,C++中视声明位置而定。

用法2:指针和常量
使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
所以出现在 * 之前的const是作为基础类型的一部分:

char *const cp; //到char的const指针
char const *pc1; //到const char的指针
const char pc2; //到const char的指针(后两个声明是等同的)
从右向左读的记忆方式:
cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容
pc2 is a pointer to const char. 故
pc2的内容不可以改变,但pc2可以指向别的字符串

且注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。

用法3:const修饰函数传入参数
将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:

void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数

用法4:修饰函数返回值
可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。

用法5:const修饰成员函数(c++特性)
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

具体展开来讲:
(一). 常量与指针

常量与指针放在一起很容易让人迷糊。对于常量指针和指针常量也不是所有的学习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;
  }

(二):常量与引用

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

int i = 10;

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

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;

(三):常量函数

常量函数是C++对常量的一个扩展,它很好的确保了C++中类的封装性。在C++中,为了防止类的数据成员被非法访问,将类的成员函数分成了两类,一类是常量成员函数(也被称为观察着);另一类是非常量成员函数(也被成为变异者)。在一个函数的签名后面加上关键字const后该函数就成了常量函数。对于常量函数,最关键的不同是编译器不允许其修改类的数据成员。例如:

 class Test  {
 public:
	 void func() const;

private:
  int intValue;

  };
void Test::func() const  {
  intValue = 100;

  }

上面的代码中,常量函数func函数内试图去改变数据成员intValue的值,因此将在编译的时候引发异常。

当然,对于非常量的成员函数,我们可以根据需要读取或修改数据成员的值。但是,这要依赖调用函数的对象是否是常量。通常,如果我们把一个类定义为常量,我们的本意是希望他的状态(数据成员)不会被改变。那么,如果一个常量的对象调用它的非常量函数会产生什么后果呢?看下面的代码:

class Fred{
public:
  void inspect() const;
  void mutate();

  };
void UserCode(Fred& changeable, const Fred& unChangeable)  {
  changeable.inspect(); // 正确,非常量对象可以调用常量函数。
  changeable.mutate(); // 正确,非常量对象也允许修改调用非常量成员函数修改数据成员。
  unChangeable.inspect(); // 正确,常量对象只能调用常理函数。因为不希望修改对象状态。
  unChangeable.mutate(); // 错误!常量对象的状态不能被修改,而非常量函数存在修改对象状态的可能

  }

从上面的代码可以看出,由于常量对象的状态不允许被修改,因此,通过常量对象调用非常量函数时将会产生语法错误。实际上,我们知道每个成员函数都有一个隐含的指向对象本身的this指针。而常量函数则包含一个this的常量指针。如下:

void inspect(const Fred* this) const;

void mutate(Fred* this);

也就是说对于常量函数,我们不能通过this指针去修改对象对应的内存块。但是,在上面我们已经知道,这仅仅是编译器的限制,我们仍然可以绕过编译器的限制,去改变对象的状态。看下面的代码:

class Fred{
public:
  void inspect() const;
private:
  int intValue;

  };

void Fred::inspect() const  {
  cout << "At the beginning. intValue = "<< intValue << endl;

  // 这里,我们根据this指针重新定义了一个指向同一块内存地址的指针。
  // 通过这个新定义的指针,我们仍然可以修改对象的状态。
  Fred* pFred = (Fred*)this;
  pFred->intValue = 50;
  cout << "Fred::inspect() called. intValue = "<< intValue << endl;

  }
int main()  {
  Fred fred;
  fred.inspect();

  return 0;

  }

上面的代码说明,只要我们愿意,我们还是可以通过常量函数修改对象的状态。同理,对于常量对象,我们也可以构造另外一个指向同一块内存的指针去修改它的状态。这里就不作过多描述了。

另外,也有这样的情况,虽然我们可以绕过编译器的错误去修改类的数据成员。但是C++也允许我们在数据成员的定义前面加上mutable,以允许该成员可以在常量函数中被修改。例如:

class Fred{
public:
  void inspect() const;
private:
  mutable int intValue;

  };
void Fred::inspect() const  {
  intValue = 100;

  }

但是,并不是所有的编译器都支持mutable关键字。这个时候我们上面的歪门邪道就有用了。

关于常量函数,还有一个问题是重载。

#include 
#include 
using namespace std;

class Fred{
public:
  void func() const;
  void func();

  };

void Fred::func() const  {
  cout << "const function is called."<< endl;

  }
  void Fred::func()  {
  cout << "non-const function is called."<< endl;

  }
void UserCode(Fred& fred, const Fred& cFred)  {
  cout << "fred is non-const object, and the result of fred.func() is:" << endl;
  fred.func();
  cout << "cFred is const object, and the result of cFred.func() is:" << endl;
  cFred.func();
  }
int main()  {

  Fred fred;
  UserCode(fred, fred);
  return 0;

  }

输出结果为:

fred is non-const object, and the result of fred.func() is:

non-const function is called.

cFred is const object, and the result of cFred.func() is:

const function is called.

从上面的输出结果,我们可以看出。当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。

总之,我们需要明白常量函数是为了最大程度的保证对象的安全。通过使用常量函数,我们可以只允许必要的操作去改变对象的状态,从而防止误操作对对象状态的破坏。但是,就像上面看见的一样,这样的保护其实是有限的。关键还是在于我们开发人员要严格的遵守使用规则。另外需要注意的是常量对象不允许调用非常量的函数。这样的规定虽然很武断,但如果我们都根据原则去编写或使用类的话这样的规定也就完全可以理解了。
(四):常量返回值

很多时候,我们的函数中会返回一个地址或者引用。调用这得到这个返回的地址或者引用后就可以修改所指向或者代表的对象。这个时候如果我们不希望这个函数的调用这修改这个返回的内容,就应该返回一个常量。这应该很好理解,大家可以去试试。

const在C++中

\1.const修饰普通类型的变量

const常量,如下:

const int  a = 7; 
int  b = a; // 正确
a = 8;       // 错误,不能改变

a 被定义为一个常量,并且可以将 a 赋值给 b,但是不能给 a 再次赋值。对一个常量赋值是违法的事情,因为 a 被编译器认为是一个常量,其值不允许修改。

接着看如下的操作:

#include
 
using namespace std;
 
int main(void){
    const int  a = 7;
    int  *p = (int*)&a;
    *p = 8;
    cout<<a;
    system("pause");
    return 0;
}

对于 const 变量 a,我们取变量的地址并转换赋值给 指向 int 的指针,然后利用 *p = 8; 重新对变量 a 地址内的值赋值,然后输出查看 a 的值。

从下面的调试窗口看到 a 的值被改变为 8,但是输出的结果仍然是 7。

img

img

从结果中我们可以看到,编译器然后认为 a 的值为一开始定义的 7,所以对 const a 的操作就会产生上面的情况。所以千万不要轻易对 const 变量设法赋值,这会产生意想不到的行为。

如果不想让编译器察觉到上面到对 const 的操作,我们可以在 const 前面加上 volatile 关键字。

Volatile 关键字跟 const 对应相反,是易变的,容易改变的意思。所以不会被编译器优化,编译器也就不会改变对 a 变量的操作。

#include
using namespace std;
int main(void) {
	volatile const int  a = 7;
	int  *p = (int*)&a;
	*p = 8;
	cout<<a;
    system("pause");
    return 0;
    }

输出结果如我们期望的是 8。

img

优点:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)

\2. const 修饰类的数据成员函数。

const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。

**注意:**const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。如:

class A
{

const int size;

}

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。如

class A

{

const int size = 100; //错误

int array[size]; //错误,未知的size

}

const数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现。如

class A

{…

enum {size1=100, size2 = 200 };

int array1[size1];

int array2[size2];

}

枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

下面以get_cm()const为例, 函数用到了 const 成员函数:

#include 
using namespace std; 
class Test {
public:
    Test(){}
    Test(int _m):_cm(_m){}
    int get_cm()const    {
        return _cm;    }
private:
    int _cm;
};
void Cmf(const Test& _tt) {
    cout<<_tt.get_cm();
} 
int main(void) {
    Test t(8);
    Cmf(t);
    system("pause");
    return 0;
}

如果 get_cm() 去掉 const 修饰,则 Cmf 传递的 const _tt 即使没有改变对象的值,编译器也认为函数会改变对象的值,所以我们尽量按照要求将所有的不需要改变对象内容的函数都作为 const 成员函数。

如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用 mutable 关键字修饰这个成员,mutable 的意思也是易变的,容易改变的意思,被 mutable 关键字修饰的成员可以处于不断变化中,如下面的例子。

#include
using namespace std;
class Test { 
public:
    Test(int _m,int _t):_cm(_m),_ct(_t){} 
    void Kf()const    {
        ++_cm; // 错误
        ++_ct; // 正确 
    } 
private: 
    int _cm; 
    mutable int _ct;
}; 
int main(void) { 
    Test t(8,7);
    return 0;
}

这里我们在 Kf()const 中通过 ++_ct; 修改 _ct 的值,但是通过 ++_cm 修改 _cm 则会报错。因为 ++_cm 没有用 mutable 修饰。

\3. const修饰指针

const 修饰指针变量有以下三种情况。

  • A: const 修饰指针指向的内容,则内容为不可变量。
  • B: const 修饰指针,则指针为不可变量。
  • C: const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。

对于A:

const int *p = 8; ==> const int* a = &;  [1]
int const *a = & ;        [2]

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量; 所以指针指向的内容 8 不可改变。简称左定值,因为 const 位于 * 号的左边。

[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3;

对于 B:

int a = 8;
int* const p = &a;
*p = 9; // 正确
int  b = 7;
p = &b; // 错误
int* const a = &         [3]

如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量; 对于 const 指针 p 其指向的内存地址不能够被改变,但其内容可以改变。简称,右定向。因为 const 位于 * 号的右边。

[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;

对于 C: 则是 A 和 B的合并

int a = 8;
const int * const  p = &a;  [4]

[4]为指针本身和指向的内容均为常量; 这时,const p 的指向的内容和指向的内存地址都已固定,不可改变。

对于 A,B,C 三种情况,根据 const 位于 * 号的位置不同,我总结三句话便于记忆的话:“左定值,右定向,const修饰不变量”

\4. const的初始化

先看一下const变量初始化的情况

  1. 非指针const常量初始化的情况:A b;
    const A a = b;

  2. 指针const常量初始化的情况:

A* d = new A();
const A* c = d;
或者:const A* c = new A();
3)引用const常量初始化的情况:
A f;
const A& e = f; // 这样作e只能访问声明为const的函数,而不能访问一般的成员函数;

[思考1]: 以下的这种赋值方法正确吗?
const A* c=new A();
A* e = c;
[思考2]: 以下的这种赋值方法正确吗?
A* const c = new A();
A* b = c;

\5. const参数传递和函数返回值

另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如下几种情况,以下会逐渐的说明用法:

A&operator=(const A& a);

void fun0(const A* a );
void fun1( ) const; // fun1( ) 为类成员函数
const A fun2( );
  1. 修饰参数的const,可以防止指针被意外篡改。如 void fun0(const A* a ); void fun1(const A& a);
    调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const A*a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A&a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。

[注意]:参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。

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

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

#include
 
using namespace std;
 
void Cpf(int *const a) {
    cout<<*a<<" ";
    *a = 9;
}
 
int main(void) {
    int a = 8;
    Cpf(&a);
    cout<<a; // a 为 9
    system("pause");
    return 0;
}
  1. 修饰返回值的const,const修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是 使得函数调用表达式不能作为左值。 如下面的例子。
#include   
using namespace std;  
  
class A {
private:
	int i;
public:
	A(){i=0;}
	int & get(){
		return i;
	}
};

void main(){
	A a;
	cout<<a.get()<<endl; //数据成员值为0
	a.get()=1; //尝试修改a对象的数据成员为1,而且是用函数调用表达式作为左值。
	cout<<a.get()<<endl; //数据成员真的被改为1了,返回指针的情况也可以修改成员i的值,所以为了安全起见最好在返回值加上const,使得函数调用表达式不能作为左值
}

一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
[总结]

\1. 一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回值为某个对象为const(const A test = A实例)或某个对象的引用为const(const A& test = A实例),则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。

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

const char * GetString(void);

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

*char str=GetString();

正确的用法是:

*const char str=GetString();

\3. 函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赙值函数中,目的是为了实现链式表达。如:

class A

{…

A &operate = (const A &other); //负值函数

}
A a,b,c; //a,b,c为A的对象

a=b=c; //正常

(a=b)=c; //不正常,但是合法

若负值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。
[思考3]: 这样定义赋值操作符重载函数可以吗?
const A& operator=(const A& a);

\6. 类成员函数中const的使用
一般放在函数体后,形如:void fun() const;
任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。如:

class Stack{
 public:
   void Push(int elem);
   int Pop(void);
   int GetCount(void) const;  //const 成员函数
 private:
   int m_num;
   int m_data[100];
};

int Stack::GetCount(void) const{
 ++m_num;       //编译错误,企图修改数据成员m_num
 Pop();          //编译错误,企图调用非const函数
 Return m_num;

}

\7. 使用const的一些建议

  1. 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
  2. 要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
  3. 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
  4. const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
  5. 不要轻易的将函数的返回值类型定为const;
  6. 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;

[思考题答案]

  1. 这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
  2. 这种方法正确,因为声明指针所指向的内容可变;
  3. 这种做法不正确;
    在const A::operator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
    A a,b,c:
    (a=b)=c;
    因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。
const在C语言中

const 在实际编程中用得并不多,const 是 constant 的缩写,意思是“恒定不变的”!它是定义只读变量的关键字,或者说 const 是定义常变量的关键字。

关键字const用来定义常量,但又有变量的属性,所以叫常变量。用 const 定义常变量的方法很简单,就在通常定义变量时前面加 const 即可,如:

const  int  a = 10;

const 和变量类型 int 可以互换位置,二者是等价的,即上条语句等价于:

int  const  a = 10; <===>const int a = 10;

那么用 const 修饰后和未修饰前有什么区别呢?它们不都等于 10 吗?

用 const 定义的变量的值是不允许改变的,即不允许给它重新赋值,即使是赋相同的值也不可以。所以说它定义的是只读变量。这也就意味着必须在定义的时候就给它赋初值。

如果定义的时候未初始化,我们知道,对于未初始化的局部变量,程序在执行的时候会自动把一个很小的负数存放进去。这样后面再给它赋初值的话就是“改变它的值”了,即发生语法错误。

用 const 修饰的变量,无论是全局变量还是局部变量,生存周期都是程序运行的整个过程。全局变量的生存周期为程序运行的整个过程这个是理所当然的。而使用 const 修饰过的局部变量就有了静态特性,它的生存周期也是程序运行的整个过程。我们知道全局变量是静态的,静态的生存周期就是程序运行的整个过程。但是用const修饰过的局部变量只是有了静态特性,并没有说它变成了静态变量。

我们知道,局部变量存储在栈中,静态变量存储在静态存储区中,而经过 const 修饰过的变量存储在内存中的“只读数据段”中。只读数据段中存放着常量和只读变量等不可修改的量。

前面说过,数组的长度不能是变量。虽然 const 定义的是只读变量,就相当于是定义一个常量。但是只读变量也是变量,所以 const 定义的变量仍然不能作为数组的长度。但是需要注意的是,在 C++ 中可以!C++ 扩展了 const 的含义,在 C++ 中用 const 定义的变量也可作为数组的长度。

const 与 define

很多人在学习 const 的时候都会混淆它与 define 的区别。从功能上说它们确实很像,但它们又有明显的不同:

  1. define是预编译指令,而const是普通变量的定义。define定义的宏是在预处理阶段展开的,而const定义的只读变量是在编译运行阶段使用的。
  2. const定义的是变量,而define定义的是常量。define定义的宏在编译后就不存在了,它不占用内存,因为它不是变量,系统只会给变量分配内存。但const定义的常变量本质上仍然是一个变量,具有变量的基本属性,有类型、占用存储单元。可以说,常变量是有名字的不变量,而常量是没有名字的。有名字就便于在程序中被引用,所以从使用的角度看,除了不能作为数组的长度,用const定义的常变量具有宏的优点,而且使用更方便。所以编程时在使用const和define都可以的情况下尽量使用常变量来取代宏。
  3. const定义的是变量,而宏定义的是常量,所以const定义的对象有数据类型,而宏定义的对象没有数据类型。所以编译器可以对前者进行类型安全检查,而对后者只是机械地进行字符替换,没有类型安全检查。这样就很容易出问题,即“边际问题”或者说是“括号问题”。

所以说const的存在一定有它的合理性,与预编译指令相比,const修饰符有以下的优点:

​ 1、预编译指令只是对值进行简单的替换,不能进行类型检查

​ 2、可以保护被修饰的东西,防止意外修改,增强程序的健壮性

​ 3、编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

const 和指针

const 也可以和指针变量一起使用,这样可以限制指针变量本身,也可以限制指针指向的数据。const 和指针一起使用会有几种不同的顺序,如下所示:

const int *p1;int const *p2;int * const p3;

在最后一种情况下,指针是只读的,也就是 p3 本身的值不能被修改;在前面两种情况下,指针所指向的数据是只读的,也就是 p1、p2 本身的值可以修改(指向不同的数据),但它们指向的数据不能被修改。

当然,指针本身和它指向的数据都有可能是只读的,下面的两种写法能够做到这一点:

const int * const p4;int const * const p5;

const 和指针结合的写法多少有点让初学者摸不着头脑,大家可以这样来记忆:const 离变量名近就是用来修饰指针变量的,离变量名远就是用来修饰指针指向的数据,如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。

const 和函数形参

在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。

在C语言标准库中,有很多函数的形参都被 const 限制了,下面是部分函数的原型:

size_t strlen ( const char * str );
int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );
char * strcpy ( char * destination, const char * source );
int system (const char* command);
int puts ( const char * str );
int printf ( const char * format, ... );

我们自己在定义函数时也可以使用 const 对形参加以限制,例如查找字符串中某个字符出现的次数:

#include 
size_t strnchr(const char *str, char ch){
    int i, n = 0, len = strlen(str);
    for(i=0; i<len; i++){
        if(str[i] == ch){
            n++; 
        } 
    } 
    return n;
}
int main(){
    char *str = "http://c.biancheng.net";
    char ch = 't';
    int n = strnchr(str, ch); 
    printf("%d\n", n); 
    return 0;
}

运行结果:
3

根据 strnchr() 的功能可以推断,函数内部要对字符串 str 进行遍历,不应该有修改的动作,用 const 加以限制,不但可以防止由于程序员误操作引起的字符串修改,还可以给用户一个提示,函数不会修改你提供的字符串,请你放心。

const 和非 const 类型转换

当一个指针变量 str1 被 const 限制时,并且类似const char *str1这种形式,说明指针指向的数据不能被修改;如果将 str1 赋值给另外一个未被 const 修饰的指针变量 str2,就有可能发生危险。因为通过 str1 不能修改数据,而赋值后通过 str2 能够修改数据了,意义发生了转变,所以编译器不提倡这种行为,会给出错误或警告。

也就是说,const char *char *是不同的类型,不能将const char *类型的数据赋值给char *类型的变量。但反过来是可以的,编译器允许将char *类型的数据赋值给const char *类型的变量。

这种限制很容易理解,char *指向的数据有读取和写入权限,而const char *指向的数据只有读取权限,降低数据的权限不会带来任何问题,但提升数据的权限就有可能发生危险。

C语言标准库中很多函数的参数都被 const 限制了,但我们在以前的编码过程中并没有注意这个问题,经常将非 const 类型的数据传递给 const 类型的形参,这样做从未引发任何副作用,原因就是上面讲到的,将非 const 类型转换为 const 类型是允许的。

下面是一个将 const 类型赋值给非 const 类型的例子:

#include 
void func(char *str){
 }
int main(){
    const char *str1 = "c.biancheng.net";
    char *str2 = str1;
    func(str1);   
    return 0;
    }

第7、8行代码分别通过赋值、传参(传参的本质也是赋值)将 const 类型的数据交给了非 const 类型的变量,编译器不会容忍这种行为,会给出警告,甚至直接报错。

const 在c和c++中的区别

\1. C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中.所以,以下代码:

#include
using namespace std;
int main()
{
const int a = 1;
const int b = 2;
int array[ a + b ] = {0};
for (int i = 0; i < sizeof array / sizeof *array; i++)
{
cout << array << endl;
}
}

在可以通过编译,并且正常运行.但稍加修改后,放在C编译器中,便会出现错误:

int main()
{
int i;
const int a = 1;
const int b = 2;
int array[ a + b ] = {0};
for (i = 0; i < sizeof array / sizeof array; i++)
{
printf("%d",array);
}
}


错误消息:
c:\test1\te.c(8): error C2057: 应输入常数表达式
c:\test1\te.c(8): error C2466: 不能分配常数大小为 0 的数组

出现这种情况的原因是:在C中,const是一个不能被改变的普通变量,既然是变量,就要占用存储空间,所以编译器不知道编译时的值.而且,数组定义时的下标必须为常量.

\2. 在C语言中: const int size; 这个语句是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间.但在C++中这样写是不正确的.C++中const默认是内部连接,如果想在C++中达到以上的效果,必须要用extern关键字.即C++中,const默认使用内部连接.而C中使用外部连接.
(1) 内连接:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量.C/C++中内连接使用static关键字指定.
(2) 外连接:所有被编译过的文件创建一片单独存储空间.一旦空间被创建,连接器必须解决对这片存储空间的引用.全局变量和函数使用外部连接.通过extern关键字声明,可以从其他文件访问相应的变量和函数.

/* C++代码 header.h /
const int test = 1;
/
C++代码 test1.cpp /
#include “header.h”
using namespace std;
int main() { cout << “in test1 :” << test << endl; }
/
C++代码 test2.cpp /
#include “header.h”
using namespace std;
void print() { cout << “in test2:” << test << endl;}
以上代码编译连接完全不会出问题,但如果把header.h改为:
extern const int test = 1;
在连接的时候,便会出现以下错误信息:
test2 error LNK2005: “int const test” (?test@@3HB) 已经在 test1.obj 中定义
因为extern关键字告诉C++编译器test会在其他地方引用,所以,C++编译器就会为test创建存储空间,不再是简单的存储在名字表里面.所以,当两个文件同时包含header.h的时候,会发生名字上的冲突.
此种情况和C中const含义相似:
/
C代码 header.h /
const int test = 1;
/
C代码 test1.c /
#include “header.h”
int main() { printf(“in test1:%d\n”,test); }
/
C代码 test2.c */
#include “header.h”
void print() { printf(“in test2:%d\n”,test); }

错误消息:
test3 fatal error LNK1169: 找到一个或多个多重定义的符号
test3 error LNK2005: _test 已经在 test1.obj 中定义

在可以通过编译,并且正常运行.但稍加修改后,放在C编译器中,便会出现错误:

int main()
{
int i;
const int a = 1;
const int b = 2;
int array[ a + b ] = {0};
for (i = 0; i < sizeof array / sizeof array; i++)
	{
		printf("%d",array);
	}
}

错误消息:
c:\test1\te.c(8): error C2057: 应输入常数表达式
c:\test1\te.c(8): error C2466: 不能分配常数大小为 0 的数组

出现这种情况的原因是:在C中,const是一个不能被改变的普通变量,既然是变量,就要占用存储空间,所以编译器不知道编译时的值.而且,数组定义时的下标必须为常量.
\2. 在C语言中: const int size; 这个语句是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间.但在C++中这样写是不正确的.C++中const默认是内部连接,如果想在C++中达到以上的效果,必须要用extern关键字.即C++中,const默认使用内部连接.而C中使用外部连接.
(1) 内连接:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量.C/C++中内连接使用static关键字指定.
(2) 外连接:所有被编译过的文件创建一片单独存储空间.一旦空间被创建,连接器必须解决对这片存储空间的引用.全局变量和函数使用外部连接.通过extern关键字声明,可以从其他文件访问相应的变量和函数.

/* C++代码 header.h /
const int test = 1;
/
C++代码 test1.cpp /
#include “header.h”
using namespace std;
int main() { cout << “in test1 :” << test << endl; }
/
C++代码 test2.cpp /
#include “header.h”
using namespace std;
void print() { cout << “in test2:” << test << endl;}
以上代码编译连接完全不会出问题,但如果把header.h改为:
extern const int test = 1;
在连接的时候,便会出现以下错误信息:
test2 error LNK2005: “int const test” 已经在 test1.obj 中定义
因为extern关键字告诉C++编译器test会在其他地方引用,所以,C++编译器就会为test创建存储空间,不再是简单的存储在名字表里面.所以,当两个文件同时包含header.h的时候,会发生名字上的冲突.
此种情况和C中const含义相似:
/
C代码 header.h /
const int test = 1;
/
C代码 test1.c /
#include “header.h”
int main() { printf(“in test1:%d\n”,test); }
/
C代码 test2.c */
#include “header.h”
void print() { printf(“in test2:%d\n”,test); }

错误消息:
test3 fatal error LNK1169: 找到一个或多个多重定义的符号
test3 error LNK2005: _test 已经在 test1.obj 中定义

也就是说:在c++ 中const 对象默认为文件的局部变量。与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象:
// file_1.cc
// defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_2.cc
extern const int bufSize; // uses bufSize from file_1
// uses bufSize defined in file_1
for (int index = 0; index != bufSize; ++index)
// …

\3. C++中,是否为const分配空间要看具体情况.如果加上关键字extern或者取const变量地址,则编译器就要为const分配存储空间.
\4. C++中定义常量的时候不再采用define,因为define只做简单的宏替换,并不提供类型检查.

你可能感兴趣的:(c++,C,c++,c语言,开发语言)