C++ 基础内容, 不值一提
Author:Jacky Wu 2006-4-17
引用该文章,必须注明其出处 http://blog.youkuaiyun.com/imwkj
一:常量 const
1:define 与 const
define用预处理器只能做源代码的文本替代,不能做类型检查,这给程序带来不稳定因素
#define ARRAY_SIZE 100
在预处理过程中,仅仅用值 100 替代在代码中出现的 ARRAY_SIZE,虽然我们在#define 的时候,已经设想它应该是个int型常量,但是,ARRAY_SIZE 并没有任何类型信息。
而用const 可以解决这个问题
const int ARRAY_SIZE = 100;
在此语句中,定义了一个整数型常量,在其作用域内,它是一个恒定不变的常量(这显然很好理解),对于所有内部类型,甚至包括自定义类的对象,都可以使用const 限定符将之表示为一个常量。而这个常量是有类型信息的,在赋值或者参数传递时,会做类型检查,这带来了额外的安全性。
因此,就仅凭这一点,在C++中就应当多用const 而少用 #define 来设置一个常量。
2:const 与 数组
当编译器遇到const 定义时,会根据语句的复杂性来决定是否分配空间给此常量,这有两种情况:
(1) 用extern 来制定该常量为外部连接,或者取const 常量的地址,或者将它作为引用参数传递给函数,这会引起编译器分配内存给该常量
(2) 用const 限定的简单常量,或者在运行期间通过计算获得的常量,编译器将会将它保存在符号表里,不会分配空间给该常量。
对于此特性,并不需要清楚的知道编译器的行为,在程序中,仅需要知道,两点:
(1) 常量在定义的时候是必须赋值的,通过编译时间直接给定或者在编译时间通过计算得到都可以
(2) const 限定的常量在其运行的生命期内是不可以改变的(不可以改变在定义时的初值)
对此,可以通过考察下面的程序来理解这两个特性:
void fun() { int m = 100; //此处 m 是变量,但是必须得赋初值,否则程序会出现致命错误 const int i = 100; //ok, 用const 限定,不会出现上述问题 const int j = i + 10; const int n = m + 10; //m 必须是已经赋值的变量
long address = (long)&j; //取得常量J的地址,这将会为j分配空间
const char ch = std::cin.get(); //常量c是通过计算得到,在此后的运行生命期内,是不可以改变的
int a[j]; //OK, j 在编译期间是常量,通过常量表达式计算获得 int b[j+10]; //此为常量表达式
int c[n]; //?? 这个定义完全可以 } |
上面指出了在定义数组时const 显示出的一些特性,但是在用 const限定数组时,又会怎样呢。
在用const限定数组时,仅仅代表“此数组内容是不可以改变的” ,对于数组保存的值,编译期间是无法获得的。看如下代码。
const int arr[] = { 1, 2, 3, 4, 5} float arr2[ arr[4] ] ; //Illegal, 非法语句,在编译期间,arr[]数组的内容是无法获得的,只能在运行时,通过地址运算获得,而数组在编译时,必须指定数组的大小! |
3:const 与指针
const 与指针之间的关系包括两方面:const 修饰指针指向的对象(此时表示对象时常量), 和const 修饰指针的地址(此时表示,指针的地址值是常量,该地址不可以作为左值进行运算)。
(1) 指向const对象的指针
定义方式:
const int *ptr;
或者 int const* ptr;
都表示ptr为普通指针,但是ptr所指向的对象是不可以改变的,也就是说,*ptr不可以用作左值操作。
看下面的语句:
const int x = 100;
int m = 200;
const int* ptr; //不需要赋处值
ptr = &m; //这是可以的,这仅仅代表*ptr 不可以改变
//*ptr = 300; //错误,不可以改变*ptr 内容
ptr = & x;
//*ptr = 200; //错误,不可以改变*ptr 内容
(2) 指针的地址是不可以改变的
int m[5] = {1, 2, 3, 4, 5};
int* const ptr = m; //ptr不可以改变,定义时必须赋值,但是*ptr时可以改变的。
*ptr = 200; //运行后,m [0]= 200;
// ptr++; //错误,ptr不可以改变,它仅能表示期定义时的地址。
(3) 把一个const 指针指向一个const 对象。
int m = 100;
const int* const ptr = &m;
//或者 int const* const ptr = &m 两个语句代表一个意思。
显然,*ptr 和ptr 都不可以做左值。
const 限定指针的关系基本就这三个类型,在阅读时你这样理解:在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量(Effective C++)。
这有几点是要注意的:
(1) 可以将一个非const 对象的地址 赋给一个const 指针,例如下面的语句是可以的:
int m = 200;
const int* ptr; //不需要赋处值
ptr = &m; //这是可以的,这仅仅代表*ptr 不可以改变
//*ptr = 300; //错误,不可以改变*ptr 内容
不能把一个const 对象的制止赋给非const 指针,但是可以通过强制类型转换做到这一点(不推荐):
const int m = 200;
//int* ptr = &m; //非法操作,不可以
int* ptr2 = (int*)&m; //合法操作,但是最好不用。
*ptr2 = 300; //合法操作,但完全破坏了常量的安全性检查!!
//运行后,*ptr2 = 300, m=200,
//而 *(int*)&m = 300; 不可理解.这里说明
//(int*)&m 和 &m 是不相等的两个地址!!!
4:const 与函数参数,返回值
(1) const 传递值
例如
void fun( const int i)
{
//这里仅代表i 在fun内时不可以变化的
}
为达到这样的效果,最好这样做:
void fun( int ip)
{
const int& i= ip;
//常量引用,异曲同工,效果更好
}
(2)返回const 值
对于内部类型(int, char,等)来说,返回const 值没有太多意义,但是对于用户自定义类型来说,用const限定返回值就保证了函数返回值不能用作左值操作。例如:
class X { X(int i = 0) { ii = i; } void modify() { i++; }
private: int ii; };
X Xfun1() { return X(); }
const X Xfun2() { return X(); }
void Xfun3(X& rx) { rx.modify(); }
int main() { Xfun1() = X(1); //ok,位拷贝给临时变量 Xfun1().modify(); //ok,但是仅对临时变量操作,语句结束后,临时变量被销毁 // Xfun3(Xfun1()); //引用临时对象,可能发生运行期间错误 // Xfun3(Xfun2()); //同上 // Xfun2() = X(1); //错误,试图修改返回的const 对象 // Xfun2().modify(); //同上; // Xfun3(Xfun2()) //引用传递错误,应当通过常量引用传递, //但是由于传递临时对象的引用,又可能导致运行时刻错误
} 关于临时对象,可以这样理解:编译时由编译器生成,临时对象的作用域仅仅时该对象产生的表达式,越过此表达式临时对象则被销毁,如果在运行时引用该临时对象,则会发生运行时刻一场。 |
(3)const 限定传递或返回地址和引用
void fun1(int* x) { *x = 100; //改变了传递过来的原参数值 }
void fun2( const int* x) { //*x = 100; //错误,*x为常量,不可改变 int i = *x; //OK const int* ptr = x; //OK //int *ptr2 = x; //语法错误,见常量与指针说明 }
//对于地址或者引用返回值, 一定要注意,返回对象的地址(引用),对象的生命域一定大于或者等于函数的生命域,不能返回 //生命域在函数内部的对象 //1:可以返回全局变量的地址 //2:可以返回静态变量的地址 //3:可以返回字符串常量的地址 //4:可以返回通过指针或者引用参数传递变量的地址
//5:不可以返回临时对象的地址 //6:不可以返回函数内局部对象的地址 //注意: //7:除非完全肯定返回的在函数内部通过new建立的堆内存上的对象在随后的操作中一定被delete, //否则,一定不要返回堆内存上的对象的地址,如果这样做可能会引起内存泄漏. //这样的函数仅能被自己使用,在类中最好只将之声明为private 函数
const char* fun3() { return "Hello World!"; //可以返回 }
const char* const fun4() { static int i; return &i; //可以返回静态变量,只要了解静态变量的存储区域就很好理解 }
|
对于参数传递,用const 限定指针或者引用传递,可以阻止在函数中修改原来对象的内容。而通过const 限定的引用传递也可以接收临时对象作为参数。
//const 引用接收临时对象 class X{};
X fun() { return X(); }
void g1(X&) {} void g2(const X&) {}
int main() { // g1(fun()); //错误,不能接受临时对象 g2(fun()); //正确,因为临时对象被自动作为常量处理! } 只要知道临时对象是被编译器处理为常量就可以很好的理解该行为 |
5:const 与类
(1)const 数据成员
对于类中的const 数据成员指的是“该类生成的对象生命期内,该成员是不可变的”,在对象生成之前必须已经赋值。对于const数据成员,唯一可以赋值的地方是“构造函数初始化列表 constructor initializer list),对于const数据成员使用方法如下:
class Array { public: Array(int size = 100) : sz(size) {} int size() { return sz; } private: const int sz; }; 显然这里的sz 在类中是不可变更的,但是在创建对象时,可以通过构造函数参数堆sz的之进行初始化,Array(int size = 100) : sz(size) ,其中sz(size) 相当于调用了sz的构造函数,对于内部数据类型是赋初值,对于自定义类类型是调用构造函数。 |
但是,这样的操作并不能获得和#define 相近的编译期间常量,如果想获得这样的编译期间常量,我们有两中方法:
(a) 使用static const
class Array { public: Array() {} int size() { return sz; } private: static const int sz = 100; int array[sz]; }; 下划线部分说明了 sz 是一个编译期间常量 |
在定义一个static const 常量的时候必须对其初始化,如下:
(b)使用 “enum hack”
使用枚举成员的特点,它在编译期间必须有值,因此,这恰恰获得了编译期间常量的效果,如下:
class Array { public: Array() {} int size() { return sz; } private: enum{ sz = 100}; int array[sz]; }; 下划线部分说明了 sz 是一个编译期间常量 这是语言本身所具有的一个特性 ,使用它不必担心编译器问题 |
(2)const 对象和 const 成员函数
对于const对象和const成员函数之间的关系可以这样理解:const 对象只可以调用由const 限定的类的成员函数,而const限定的成员函数不能修改类中的普通的数据成员(通过指针可以修改,但是会出现不必要的麻烦,除非必须,否则不要这样做)。
与const 对象和const成员函数相关的还有两个关键字,mutable(易变的),volatile(不稳定的,可变的)
class X { public: X(int m) : i(m) {} int f() const { //i++; //错误,不可以在const 成员函数中做改变普通数据成员的操作 return i; }
int f2() { i++; return i; } private: int i; };
int main() { X x1(100); const X x2(200); x1.f(); //ok,非const对象可以调用const 成员函数 x2.f(); //ok,const 对象只能调用const成员函数 //x2.f1(); //错误const 对象只能调用const成员函数 } 这样,编译器确保了const对象会修改数据成员,从而在逻辑上表示了一个const对象
但是,如果想在const成员函数中修改某些变量该怎么样做呢。有两种方法: (a)通过对this指针的强制类型转换来获得操作权限(粗体划线代码所示) (b)通过将变量声明为mutable,表示这是一个在任何时候可以改变的变量,这样允许在const成员函数中改变其值 class X { public: X(int m) : i(m) {} int f() const { //i++; //错误,不可以在const 成员函数中做改变普通数据成员的操作 ((X*)this)->i++; //强制指针类型转换,越过const成员函数检查,正确,但不可取 mi = 100; //正确,mi是mutable变量 return i; }
int f2() { i++; mi++; //正确,mi是mutable变量 return i; } private: int i; mutable int mi; }; |
对于volatile 关键字主要作用是提示编译器该对象的值可能在编译器未监测到的情况下被改变,因此编译器不能武断地对引用这些对象的代码作优化处理。在一般情况下,volatile变量不由用户改变,由系统驻留程序改变。对于此,就不多说明了。
C++中const用法总结
作者JuKevin
1. const修饰普通变量和指针
const修饰变量,一般有两种写法:
const TYPE value;
TYPE const value;
这两种写法在本质上是一样的。它的含义是:const修饰的类型为TYPE的变量value是不可变的。
对于一个非指针的类型TYPE,无论怎么写,都是一个含义,即value只不可变。
例如:
const int nValue; //nValue是const
int const nValue; // nValue是const
但是对于指针类型的TYPE,不同的写法会有不同情况,例如:
A. const char *pContent;
B. char * const pContent;
C. char const *pContent;
D. const char* const pContent;
对于前三种写法,我们可以换个方式,给其加上括号
A. const (char) *pContent;
B. (char*) const pContent;
C. (char) const *pContent;
这样就一目了然。根据对于const修饰非指针变量的规则,很明显,A=C.
- 对于A,C, const修饰的类型为char的变量*pContent为常量,因此,pContent的内容为常量不可变.
- 对于B, 其实还有一种写法: const (char*) pContent;
含义为:const修饰的类型为char*的变量pContent为常量,因此,pContent指针本身为常量不可变.
- 对于D, 其实是A和B的混合体,表示指针本身和指针内容两者皆为常量不可变
总结:
(1) 指针本身是常量不可变
(char*) const pContent;
const (char*) pContent;
(2) 指针所指向的内容是常量不可变
const (char) *pContent;
(char) const *pContent;
(3) 两者都不可变
const char* const pContent;
还有其中区别方法:
沿着*号划一条线,
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
2. const修饰函数参数
const修饰函数参数是它最广泛的一种用途,它表示函数体中不能修改参数的值(包括参数本身的值或者参数其中包含的值)。它可以很好
void function(const int Var); //传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)
void function(const char* Var); //参数指针所指内容为常量不可变
void function(char* const Var); //参数指针本身为常量不可变(也无意义, 因为char* Var也是形参)
参数为引用,为了增加效率同时防止修改。
修饰引用参数时:
void function(const Class& Var);//引用参数在函数内不可以改变
void function(const TYPE& Var); //引用参数在函数内为常量不可变
3. const 修饰函数返回值
const修饰函数返回值其实用的并不是很多,它的含义和const修饰普通变量以及指针的含义基本相同。
(1) const int fun1() 这个其实无意义,因为参数返回本身就是赋值。
(2) const int * fun2()
调用时 const int *pValue = fun2();
我们可以把fun2()看作成一个变量,那么就是我们上面所说的1.(1)的写法,即指针内容不可变。
(3) int* const fun3()
调用时 int * const pValue = fun2();
我们可以把fun2()看作成一个变量,那么就是我们上面所说的1.(2)的写法,即指针本身不可变。
4. const修饰类对象/对象指针/对象引用
const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。
const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。
例如:
class AAA
{
void func1();
void func2() const;
}
const AAA aObj;
aObj.func1(); ×
aObj.func2(); 正确
const AAA* aObj = new AAA();
aObj->func1(); ×
aObj->func2(); 正确
5. const修饰成员变量
const修饰类的成员函数,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。
class A
{
…
const int nValue; //成员常量不能被修改
…
A(int x): nValue(x) {}; //只能在初始化列表中赋值
}
6. const修饰成员函数
const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。
class A
{
…
void function()const; //常成员函数, 它不改变对象的成员变量. 也不能调用类中任何非const成员函数。
}
对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。
7. const常量与define宏定义的区别
(1) 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2) 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
const常量会在内存中分配(可以是堆中也可以是栈中)。