C++ 中const关键字详解

为什么使用const? 采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见的用途是作为数组的边界和switch分情况标号。

const 用途分类


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

取代了C中的宏定义,声明时必须进行初始化(C++类中则不然)。const 限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间,但是可以保证类型安全(没有对修饰的常量进行取地址操作时,都是是放在符号表中的。所以说没有分配空间)。C标准中,const定义的常量是全局,C++中视声明位置而定。

  • 在C语言中是一个变量,但具有常属性,不能直接被改变;而 c++ 中是一个常量;

  • const定义常量从汇编的角度来看,只是给出了对应的内存地址,const定义的常量在程序运行过程中只有一份拷贝;

  • const修饰变量时,变量存放的位置与它没有太大的关系,一般而言,存放于栈上,若有static或者全局变量则放在数据段;

//C++中

intmain(intargc, char*argv[])

{

//c++中 对于基础类型 系统不会给c开辟空间 c放到符号表中

constintc=0;

// int* p = &c; 无法从const int* 转换为int*

// c++中当 对c 取地址的时候 系统就会给c开辟空间

int*p= (int*)&c; //可以正常转换

cout<<"Begin..."<<Qt::endl;

// 空间内容可以修改

*p=5;

//读取符号表的内容

cout<<c<<Qt::endl;

//读取空间内容

cout<<*p<<Qt::endl;

cout<<"End"<<Qt::endl;

inta=200;

constintb=a; //系统直接为b开辟空间,而不会把b放入符号表

// const 自定义数据类型系统会分配空间,空间可读可写。

constPersonper= {100, "tom"};

}

//输出结果

Begin...

0

5

end

如果上面的代码在C语言中 c 的输出 c=5;但在C++中 C依然为0。说明在C++中编译过程中若发现使用常量则直接以符号表中的值替换。

指针和常量

使用指针时涉及到两个对象:指针本身和指针所指对象。

1.将一个指针的声明用const修饰,是将所指对象成为常量而不是指针成为常量。

charconst*pc1; //指向const char 的指针

constchar*pc2; //指向const char 的指针

这两种声明方式是等同的

2.将指针本身声明为常量

char*constcp; //指向char 的const 指针 cp

记忆方法:从右向左读

“cp is a const pointer to char." 故cp不能指向别的字符串,但可以修改其指向的字符串的内容

"pc1 is a pointer to const char" 故*pc1 的内容不可以改变,但pc1可以指向别的字符串.

const修饰函数传入参数

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

voidFun(constA*in); //修饰指针型传入参数

voidFun(constA&in); //修饰引用型传入参数

voidfunc_02(int*constp)

{

int*pi=newint(100);

//错误!P是指针常量。不能对它赋值。

p=pi;

}

intmain()

{

int*p=newint(10);

func_02(p);

deletep;

return0;

}

我们可以使用这样的方法来防止函数调用者改变参数的值。但是,这样的限制是有限的,作为参数调用者,我们也不要试图去改变参数中的值。因此,下面的操作是在语法上是正确的,但是可以破坏参数的值

#include <iostream>

#include <string>

voidfunc(constint*pi)

{

//这里相当于重新构建了一个指针,指向相同的内存区域。当然就可以通过该指针修改内存中的值了。

int*pp= (int*)pi;

*pp=100;

}

intmain()

{

usingnamespacestd;

int*p=newint(10);

cout<<"*p = "<<*p<<endl;

func(p);

cout<<"*p = "<<*p<<endl;

deletep;

return0;

}

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

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

修饰函数返回值

可以阻止用户修改返回值。返回值也要相应的赋给一个常量或常指针。

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

  1. const 修饰类的数据成员。如:

classA

{

constintsize;

}

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

classA

{

constintsize=100; //错误

intarray[size]; //错误,未知的size

}

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

classA

{…

enum {size1=100, size2=200 };

intarray1[size1];

intarray2[size2];

}

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

  1. const修饰指针的情况,见下式:

intb=500;

constint*a=& [1]

intconst*a=& [2]

int*consta=& [3]

constint*consta=& [4]

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。

const A fun2( ); const A* fun3( )

这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。

constRationaloperator*(constRational&lhs, constRational&rhs)

{

returnRational(lhs.numerator() *rhs.numerator(),

lhs.denominator() *rhs.denominator());

}

返回值用const修饰可以防止允许这样的操作发生:Rational a,b;Radional c;(a*b) = c;

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

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

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

const char * GetString(void);

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

char *str=GetString();

正确的用法是:

const char *str=GetString();

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

classA

{…

A&operate= (constA&other); //赋值函数

}

Aa,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);

const修饰成员函数

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

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;

const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

classTest

{

public:

voidfunc() const;

private:

intintValue;

};

voidTest::func() const

{

intValue=100;

}

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

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

classFred

{

public:

voidinspect() const;

voidmutate();

};

voidUserCode(Fred&changeable, constFred&unChangeable)

{

changeable.inspect(); // 正确,非常量对象可以调用常量函数。

changeable.mutate(); // 正确,非常量对象也允许修改调用非常量成员函数修改数据成员。

unChangeable.inspect(); // 正确,常量对象只能调用常理函数。因为不希望修改对象状态。

unChangeable.mutate(); // 错误!常量对象的状态不能被修改,而非常量函数存在修改对象状态的可能

}

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

voidinspect(constFred*this) const;

voidmutate(Fred*this);

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

classFred

{

public:

voidinspect() const;

private:

intintValue;

};

voidFred::inspect() const

{

cout<<"At the beginning. intValue = "<<intValue<<endl;

// 这里,我们根据this指针重新定义了一个指向同一块内存地址的指针。

// 通过这个新定义的指针,我们仍然可以修改对象的状态。

Fred*pFred= (Fred*)this;

pFred->intValue=50;

cout<<"Fred::inspect() called. intValue = "<<intValue<<endl;

}

intmain()

{

Fredfred;

fred.inspect();

return0;

}

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

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

classFred

{

public:

voidinspect() const;

private:

mutableintintValue;

};

voidFred::inspect() const

{

intValue=100;

}

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

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

#include <iostream>

#include <string>

usingnamespacestd;

classFred

{

public:

voidfunc() const;

voidfunc();

};

voidFred::func() const

{

cout<<"const function is called."<<endl;

}

voidFred::func()

{

cout<<"non-const function is called."<<endl;

}

voidUserCode(Fred&fred, constFred&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();

}

intmain()

{

Fredfred;

UserCode(fred, fred);

return0;

}

输出结果为:

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.

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

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

常量与引用

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

inti=10;

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

constint&ri=i;

// 错误!不能这样写。语法错误

// int& const rci = i;

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

voidfunc(constint&i)

{

// 错误!不能通过i去改变它所代表的内存区域。

i=100;

}

intmain()

{

inti=10;

func(i);

return0;

}

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

constinti=10;

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

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

int*pi= (int*) &i;

猜你喜欢

转载自blog.csdn.net/allexw/article/details/129067767
今日推荐