1.1.1. 定义普通常量
使用#define来定义常量也是常用方法,但const也可以用来定义常量,在[Effective C++]中建议使用const代替#define来定义常量,因为const定义的常量具有类型信息,而宏没有,所以使用const定义的常量在进行赋值操作时编译器会进行更严格的类型检查,是类型安全的。
const double PI = 3.1414926;
const int POOL_SIZE = 20;
定义常量有三种方法:宏、const、enum,其中宏应该尽量避免,而const与enum也各有优缺点,最大的区别就是enum只能用于定义整数,而不能定义浮点数;而对于定义逻辑关系较近的一组整数时比较适合使用enum,也可以考虑使用类代替enum(参见[??])。
常量必须在定义时进行初始化,之后便不能再赋值。说它不能被赋值并不是说常量的值是绝对不会改变的,只是说不能直接赋值,但可以通过指针及强制类型转换、const_cast是可以改变常量的值的。
#include
using namespace std;
int main( void )
{
const int ci = 5;
const int* cpci = &ci;
int *pci = (int*)&ci;
cout<
return 0;
}
输出结果:
cpci = 002DFAC8, pci = 002DFAC8
ci=5, *cpci=1, *pci=1
ci=5, *cpci=2, *pci=2
ci != *cpci
之所以使用ci直接输出变量的值时显示其值始终没有改变,但通过指针间接显示出来的值是改变了,而且输出结果的最后一行很奇怪,ci的值与*cpci的值居然不相等,只因为编译器在编译时进行了优化,将代码中的ci直接替换成了5,与宏替换是相同的效果,而指针的值则是实际内存中的值。
所以,千万不要试图使用指针强行改变const变量的值,否则程序可能表现出错误的行为,而且查找起来这种错误非常困难。在gcc 4.3.4和visual C++ 2010中均默认打开了对常量的优化选项,目前还没找到关闭该优化的命令行选项,一定不要自作聪明去改const变量的值。
1.1.2. 修饰指针
把const与指针放到一起,很多人便会想到一个绕口令“指针常量与常量指针。“指针常量”即一个指针变量,该变量不能被赋值,而指针指向的内存单元的内容是可以改变的;“常量指针”即一个指向常量的指针,指针变量本身可以赋值,而指针指向的内存单元的内容是不可以被重新赋值的。
char a = 'A', b = 'B';
const char* ptoc = &a; // 常量指针
*ptoc = 'C'; // 改变指针指向内存单元的内容,不可以
ptoc = &b; // 改变指针的值,可以
char* const cp = &a; // 指针常量
cp = &b; // 改变指针的值,不可以
*cp = 'D'; // 改变指针指向内存单元的内容,可以
const char* const cptoc = &a; // 指向常量的指针常量
*cptoc = 'E'; // 不可以
cptoc = &b; // 不可以
const是修饰类型还是修饰指针,要看const的位置,放在*前就是修饰数据类型,放到*后就是修饰指针,const char和char const是一样的。
建议:在不打算修改数据内容的时候都将指针定义成常量指针,不打算指针本身被修改的场合都定义成指针常量。尽可能地多用const,用错了没关系,编译器会提示你的,只要能够编译通过,就不会因为用错const而导致程序逻辑错误,应该说const负作用极小。
1.1.3. 修饰类成员常量
当使用const修饰类成员变量时便定义了常数据成员,它的使用与使用类外定义的常量本质上并没有什么区别,在这里只想指出一点:有网友提到const数据成员只能被const修饰的函数使用这是没有根据的,是错误的。
1.1.4. 修饰类成员函数
const修饰成员函数语法:
class Socket
{
public:
typedef unsigned short socket_port_t;
socket_port_t LocalPort( void ) const
{
++readCount;
return _port;
}
private:
socket_port_t _port;
mutable int _readCount;
};
使用const修饰的成员函数不能修改类的成员变量,如成员_port,而且只能调用成员类对象const函数,但有个例外,就是mutable修饰的成员变量可以在const修饰的成员函数中被修改,如_readCount。
另外,const只能修饰非静态函数。
建议:将所有不改变对象状态的函数都使用const修饰符标识,以提高程序的可读性。其实,头文件就是最好的类接口的说明文档,越多的提供信息就能使程序的可读性越好,越利于维护。看到成员函数的const修饰符,读者便立即明白该函数不会改变程序的状态,这也有利于当程序状态出现异常时的问题定位。
1.1.5. 修饰类对象、对象引用或对象指针(常量指针)
当const修饰自定义的类对象时,与修饰C++内置类型的变量的思想是一致的,但稍有不同,除了不能被赋值外,还不能调用没有使用const修饰的非静态成员函数。当const修饰类对象引用、指针时限制是一样的,因为引用本身与直接使用该变量实质上没有区别,而使用指针只是将.操作符改为了->本质上还是一样的
const std::string hello = “Hello from Noock Tian;
std::cout<
hello = "Hi"; // 不可以赋值
hello.push_back("!"); // 不可以
1.1.6. 修饰函数参数
const修饰函数的例子是很常见了,表示函数的参数在函数体内不会被意外修改,一般用于修饰输入参数,例如标准库中的字符串连接函数。str1是输出参数,其内容会被修改,而str2为输入参数,其内容不会修改。
char* strcat(char* str1, const char* str2);
实际上在说到const用法一开始就提到,const只是一种声明,但并不能保证,例如strcat函数虽然声明了str2为const char*型,但并不能保证内部绝对不会修改str2的内容。但const从语言本身提供了一种编写自描述性代码的方法,只要使用函数与实现函数的双方都达成一致的约定,按照契约编程,我们就可以认为const修饰的类型在函数体内不会被修改,这与const修饰类成员函数一样,可以提高软件的可读性。
1.1.7. 修饰函数返回值
const可以用于修饰任何类型,只要返回值类型不是void,const就可以用来修饰返回值的类型。但实际上const用于修饰非引用的返回值类型是没有意义的,因为返回值一般都会被赋值给另一个变量,此时用于传递返回值的对象已经被销毁,修饰返回值类型的const的作用也就终结了。
当返回值是引用类型时,如果该引用的值不希望被修改是可以声明为常引用的,例如:
class Socket
{
public:
const string& IP( void ) const{ return _ip; }
private:
string _ip;
};
Socket sock;
string& ip = sock.IP(); // 不可以
const string& ip2 = sock.IP(); // 可以
string ip3 = sock.IP(); // Socket::_ip被复制,可以
此处,为了减少构造临时变量,将IP函数返回值定义为引用类型以提高程序运行效率,但为了保护内部状态不会被客户端代码意外,返回值使用const修饰为常引用。但是,如果对于软件安全性较高的场合,最好不要定义为引用,因为恶意的客户端代码是有可能修改Socket::_ip的值的。
在C++中赋值运算符反默认返回值都是引用,但笔者认为定义为常引用更为合适,例如:
int main( void )
{
int a = 1,b = 2,c = 3,d = 4;
((a=b)=c)=d;
cout<
return 0;
}
输出结果:
a=4, b=2, c=3, d=4
显然,在实际工程中谁也不会写出这样的代码,这段代码却是合法的,无疑这给程序员多了一种出错的可能,如果把赋值运算符的返回值定义为常引用,则会减少程序员出错的机会,例如[??]:
class Object
{
public:
const Object& operator=(const Object& a) { return *this; }
};
int main( void )
{
Object a, b, c, d;
a = b = c = d; // 可以
((a=b)=c)=d; // 不可以
return 0;
}
在gcc 编译时则会出现错误提示:
error: passing ‘const Object’ as ‘this’ argument of ‘const Object& Object::operator=(const Object&)’ discards qualifiers.
当然,不同的编译器可能错误提示不同。