1.常变量: const 类型说明符 变量名 const int a;
常引用: const 类型说明符 &引用名 const int &a;
常对象: 类名 const 对象名 (class)A const a;
常成员函数: 类名::fun(形参) const (class)A::fun(...)const
常数组: 类型说明符 const 数组名[大小] const int a[10];
常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名 const int* a(只想地址上的内容不变)或者int* const a(a自相的地址不变)
2.类型说明符和const的顺序可以互换
3.常变量:
取代了C中的宏定义,声明时必须进行初始化(注意:c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。(如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》(这句换暂时不太理解))
用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
C标准中,const定义的常量是全局的,C++中视声明位置而定。
3.cost修饰函数参数:以指明不想让调用函数修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
4.修饰函数返回值:阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。(即返回值不能作为作为左值使用,即他可以赋值给别人,别人不可向他赋值)
5.const修饰成员函数(c++特性): 类名::fun(形参) const (class)A::fun(...)const
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对却是实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。
6.常量与指针:其实,只要知道:const在星号左边,指针指向的内容不改变,const在星号右边,指针指向的地址不变
对于常量指针,不能通过该指针来改变所指的内容。即,下面的操作是错误的:
int i = 10;
const int *pi = &i;
*pi = 100;(错误)
因为你在试图通过pi改变它所指向的内容。但是,并不是说该内存块中的内容不能被修改。我们仍然可以通过其他方式去修改其中的值。例如:
// 1: 通过i直接修改。
i = 100;
// 2: 使用另外一个指针来修改。
int *p = (int*)pi;
*p = 100;
7.常量与引用
引用就是另一个变量的别名,它本身就是一个常量。也就是说不能再让一个引用成为另外一个变量的别名, 那么他们只剩下代表的内存区域是否可变。即:
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;
8.常量函数
在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 <iostream>
#include <string>
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.
从上面的输出结果,我们可以看出。当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。
总之,我们需要明白常量函数是为了最大程度的保证对象的安全。通过使用常量函数,我们可以只允许必要的操作去改变对象的状态,从而防止误操作对对象状态的破坏。但是,就像上面看见的一样,这样的保护其实是有限的。关键还是在于我们开发人员要严格的遵守使用规则。另外需要注意的是常量对象不允许调用非常量的函数。这样的规定虽然很武断,但如果我们都根据原则去编写或使用类的话这样的规定也就完全可以理解了。
9.常量返回值
很多时候,我们的函数中会返回一个地址或者引用。调用这得到这个返回的地址或者引用后就可以修改所指向或者代表的对象。这个时候如果我们不希望这个函数的调用这修改这个返回的内容,就应该返回一个常量。这应该很好理解,大家可以去试试。
10.c++ 中const
(1). const常量,如const int max = 100;
优点:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误(边际效应)
(2). 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];
}
枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
(3). 另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如下几种情况,以下会逐渐的说明用法:A&operator=(const A& a);
void fun0(const A* a );
void fun1( ) const; // fun1( ) 为类成员函数
const A fun2( );
修饰参数的const,如 void fun0(const A* a ); void fun1(const A& a);
调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const A*a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A&a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
[注意]:参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。
10.总结
(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); //负值函数
}
(4). 类成员函数中const的使用
一般放在函数体后,形如:void fun() const;
任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。如:
(5). 使用const的一些建议
1) 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2) 要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
3) 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
4) const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5) 不要轻易的将函数的返回值类型定为const;
6) 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
// ...
11. C++中,是否为const分配空间要看具体情况.如果加上关键字extern或者取const变量地址,则编译器就要为const分配存储空间.
12. C++中定义常量的时候不再采用define,因为define只做简单的宏替换,并不提供类型检查.