代码测试平台:Window 7 Server + Microsoft Visual Studio 10 Visual C++
某些说明问题的汇编代码: Debug模式下的汇编代码
1.const变量声明
const int n1; // 错误,声明的同时必须要伴随着初始化.
const int n1 = 3; // 正确.
extern const int n2; // 正确.
const int* pn1; // 正确.
int* const pn2; // 错误,声明的同时必须要伴随着初始化.
int n2 = 5;
int* const pn2 = &n2; // 正确.
const int nArray1[]; // 错误,声明的同时必须要伴随着初始化.
const int nArray2[] = { 1 } ; // 正确.
int n3 = 5;
int nArray3[n3]; // 错误.
const int n4 = 5;
int nArray4[n4]; // 正确.
const int n5 = 5;
const int nArray5[n5]; // 错误,声明的同时必须要伴随着初始化.
const int n6 = 1;
const int nArray6[n6] = { 1 }; // 正确.
const int nArray7[] = { 1,2 };
int nArray8[nArray7[1]]; // 错误.
2. const int* pn(int
const* pn)与int* const
pn的区别.
(1)const int* pn;这样声明是可行的;int* const pn;错误的,声明必须伴随着初始化.
(2) const int* pn(int
const* pn)
// pn为常量指针;const修饰的是*pn,*pn表示指针指向的值,用const修饰*pn,*pn是常量,是不能改变的.
int a = 5;
int b = 8;
const int* pn = &a; // 正确.
pn = &b; // 正确.
*pn = 3 // 错误.
//-------------------------------------------------------
int* const pn
// pn为指针常量;const修饰的是pn,pn是不能改变的,但是pn指向的内容是可以改变的.
int a = 5;
int b = 8;
int* const pn = &a; // 正确.
pn = &b; // 错误.
*pn = 3 // 正确.
(3)对于const int* const pn
int a = 5;
int b = 8;
const int* const pn = &a; // 正确.
pn = &b; // 错误.
*pn = 6; // 错误.
3.const int& n
// 引用必须初始化,而且初始化后这个引用不能在引用其它的变量.
int a = 5;
int b = 8;
const int& n = a; // 正确.
n = b; // 错误.
4.类型检查
// 可以把一个non const指针赋给一个const指针,不可以把const指针赋给non
const指针.
// 但是可以使用某些手段进行转换.
const int a = 5;
const int* pn = &a; // 正确.
int* pn2 =
&a; // 错误.
int* pn3 = (int*)&a; // 正确.
int* pn4 = const_cast<int*>(&a); // 正确.
pn = pn3; // 正确.
pn = pn4; // 正确.
5. char p1[] = "abcdefghijklmn";
*p1 = 'T'; // 正确.
char* p2 = "abcdefghijklmn";
*p2 = 'T'; // 错误.
问题1:*p1为什么是正确的?*p2为什么是错误的?
答:因为p1是一个数组,*p1
= 'T'相当于p[0]
= 'T';
p2是一个指针,它指向字符串"abcdefghijklmn",字符串"abcdefghijklmn"是不可以修改的.
问题2:"abcdefghijklmn"是不是常量???如果是常量,怎么能赋给一个non const指针呢???
答:个人理解"abcdefghijklmn"只是放在了不可写入的区域;只能读,不能写.所以赋给一个non const指针没什么问题.即使它是const的,编译器实现时就可以控制转换.
char p1[] = "abcdefghijklmn";
00A63498 mov eax,dword ptr [string
"abcdefghijklmn" (0A65838h)]
00A6349D mov dword ptr [ebp-18h],eax
00A634A0 mov ecx,dword ptr ds:[0A6583Ch]
00A634A6 mov dword ptr [ebp-14h],ecx
00A634A9 mov edx,dword ptr ds:[0A65840h]
00A634AF mov dword ptr [ebp-10h],edx
00A634B2 mov ax,word ptr ds:[00A65844h]
00A634B8 mov word ptr [ebp-0Ch],ax
00A634BC mov cl,byte ptr ds:[0A65846h]
00A634C2 mov byte ptr [ebp-0Ah],cl
*p1 = 'T';
00A634C5 mov byte ptr [ebp-18h],54h
char* p2 = "abcdefghijklmn";
00A634C9 mov dword ptr [ebp-24h],offset string "abcdefghijklmn" (0A65838h) // 这里把"abcdefghijklmn"地址赋给p2
*p2 = 'T';
00A634D0 mov eax,dword ptr [ebp-24h]
00A634D3 mov byte ptr [eax],54h // 试图向不可写的地址写入新的值,错误.
貌似你还可以看到"abcdefghijklmn"只有一份,地址为(0A65838h)…呵呵
6.常量折叠
示例1:
void testModifyConstInt()
{
const int Pi = 3;
int* pn = const_cast<int*>(&Pi);
*pn =
2;
cout << "&Pi
= " << &Pi << endl;
cout << "pn
= " << pn << endl;
cout << "Pi
= " << Pi << endl;
cout << "*pn
= " << *pn << endl;
}
输出结果为:
&Pi = 0045F8F0
pn = 0045F8F0
Pi = 3
*pn = 2
为什么输出地址一样而值不一样呢?
// 汇编码如下:
const int Pi = 3;
000F14CE mov dword ptr [Pi],3
int* pn = const_cast<int*>(&Pi);
000F14D5 lea eax,[Pi]
000F14D8 mov dword ptr [pn],eax
*pn = 2;
000F14DB mov eax,dword ptr [pn]
000F14DE mov dword ptr [eax],2 // 这里看来,Pi地址的值确实被修改了
cout << "&Pi
= " << &Pi << endl;
000F14E4 mov esi,esp
000F14E6 mov eax,dword ptr [__imp_std::endl (0FA31Ch)]
000F14EB push eax
000F14EC mov edi,esp
000F14EE lea ecx,[Pi]
000F14F1 push ecx // 这里压入栈的是[Pi]
000F14F2 push offset string
"&Pi = " (0F7848h)
......
cout << "pn = " << pn << endl;
000F1524 mov esi,esp
000F1526 mov eax,dword ptr [__imp_std::endl
(0FA31Ch)]
000F152B push eax
000F152C mov edi,esp
000F152E mov ecx,dword ptr [pn]
000F1531 push ecx // 这里压入栈的是[pn],这里的[pn]的值和[Pi]是一样的
000F1532 push offset string
"pn = " (0F7840h)
......
cout << "Pi = " << Pi << endl;
000F1564 mov esi,esp
000F1566 mov eax,dword ptr [__imp_std::endl (0FA31Ch)]
000F156B push eax
000F156C mov edi,esp
000F156E push 3 // 但是这里直接压入栈的数字是3,所以后面输出3
000F1570 push offset string
"Pi = " (0F7838h)
......
cout << "*pn = " << *pn << endl;
000F15A2 mov esi,esp
000F15A4 mov eax,dword ptr [__imp_std::endl (0FA31Ch)]
000F15A9 push eax
000F15AA mov edi,esp
000F15AC mov ecx,dword ptr [pn]
000F15AF mov edx,dword ptr [ecx]
000F15B1 push edx // 这里压入栈的是[pn],所以后面输出的是2
000F15B2 push offset string
"*pn = " (0F7830h)
......
这下知道为什么地址一样,而值不一样了吧!
示例2:
我们把const int Pi = 3;放在函数之外
const int Pi = 3;
void testModifyConstInt()
{
int* pn = const_cast<int*>(&Pi);
*pn =
2;
cout << "&Pi = " << &Pi << endl;
cout << "pn = " << pn << endl;
cout << "Pi = " << Pi << endl;
cout << "*pn = " << *pn << endl;
}这时会输出什么呢?
答案:尝试写入数据到只读的内存,程序出错.
把const int Pi = 3;放在函数内部就可以,为什么放在全局区域就不行了呢???
// 汇编代码如下:
int* pn = const_cast<int*>(&Pi);
011514AE mov dword ptr [pn],offset Pi (11578A8h)
*pn = 2;
011514B5 mov eax,dword ptr [pn]
011514B8
mov dword ptr [eax],2 // 就是这里出错了.
我们先了解一点汇编指令的小知识:
lea ax,A 执行时才会将A的地址放入ax;
offset 是伪指令 mov ax,offset A在编译时就已经计算A的地址.
假设A地址为0x1234,则mov ax,offset A 编译后为 mov ax,0x1234
所以:
011514AE mov dword ptr [pn],offset Pi (11578A8h)
获取Pi的地址是没问题的.
Pi放在全局,又是const类型的,编译期间已经把Pi分配到一个只读的地址区域,这时候你若要向此地址进行写入操作,就出错了.
那么const int Pi = 3;放在函数内部就可以呢?
答:可能因为此时的Pi是的地址空间属于函数栈,只有在运行期间的时候才知道把testModifyConstInt函数装载到某个地址空间,这个地址空间是可以进行写入操作的.
7.函数和成员函数
(1)void testConstParam(const int n); // 参数与给const变量赋值类似
(2)void testConstParam(const int* pn); // 在函数内部不能修改*pn
(3) const调用,const用于成员函数重载.
对于成员函数,如果想让const对象操作它,就要把这个成员函数声明为const.
class A
{
public:
A() { cout <<
"this = " << this <<
" -> A::A()" << endl; }
A(const A&) { cout
<< "this = " << this
<< " -> A::A(const A&)" << endl; }
A& operator=(const A& ){ cout << "this = " << this << " -> A::A(const A&)"
<< endl; return
*this;}
~A() { cout <<
"this = " << this <<
" -> A::~A()" << endl; }
public:
void ShowA1() { cout << "this = " << this << " -> A::ShowA1()" <<
endl; }
void ShowA2() { cout << "this = " << this << " -> A::ShowA2()" <<
endl; }
void ShowA2() const { cout
<< "this = " << this
<< " -> A::ShowA2() const" << endl;
}
void SetX(int x) { this->x = x;}
private:
int x;
};
A returnA()
{
return A();
}
const A returnConstA()
{
return A();
}
const A* returnConstAPoint()
{
return new A;
}const A returnConstA()
{
return A();
}
const A* returnConstAPoint()
{
return new A;
}
int _tmain(int argc, _TCHAR* argv[])
{
A* pA1 = returnConstAPoint(); // 错误.
const A* pA2 = returnConstAPoint(); // 正确.
returnConstAPoint()->ShowA1(); // 错误.
returnConstAPoint()->ShowA2(); // 正确.调用的是void A::ShowA2() const returnA() = A(); // 正确.
returnConstA() = A(); // 错误.returnConstA()返回值为const,禁止修改.
}
(4)对于const成员变量,只能用使用构造函数初始化列表来初始化.
class B
{
public:
B() : b(0) { }
private:
const int b;
};
那么,对于const成员数组,又怎么初始化呢?
例如:
class B
{
public:
B(){ }
public:
const int arrayB[5]; // 这个数组怎么初始化呢???
};
// 既然数组const int array1[5] = { 1,2,3,4,5};可以这样初始化.
// 那么是否可以这样呢???
B() : arrayB({1,2,3,4,5})
{ } // 这样是不行的
// 可行的办法,这里我不知道为什么,请高人解答
B() : arrayB() { }
(5)const成员函数中如何修改成员变量.
class B
{
public:
B() : b(0) { }
void SetB(int b) const
{
((B*)this)->b = b;
// const_cast<B*>(this)->b = b;
}
private:
int b; // 如果有必要,最好把此变量声明为mutable.那么就不用强制转换欺骗编译器.
};
(6) 动态创建const对象
int* pn = new const int; // 错误.动态创建返回的是const int*,不能赋int*
const int* pn = new const int; // 可以.动态创建未初始化.
const int* pn = new const int(5); // 正确.动态创建并初始化.
const int* pn = new const int[5]; // 可以.动态创建一个数组,但是未初始化.
class D
{
public:
D() {}
};
const D* pD = new
const D[5]; // 可以.动态创建一个D数组
(7)在类中,const和static可以一起使用吗?
class C
{
public:
C() { }
void ShowC() const{ cout <<
"C::ShowC()" << endl; }
static void StaticShowC() { cout
<< "C::StaticShowC()" << endl;
}
// 错误.
static void StaticConstShowC() const
{ cout << "C::StaticConstShowC()
const" << endl; }
static const int c; // 正确.
static const int arrayC[5]; // 正确.
};
const int C::c
= 5;
const int C::arrayC[]
= { 1,2,3,4,5};
为什么static和const不能用在一起修饰类函数呢?
答:可能是因为static并不会关联任何一个C类对象,而const用于修饰this指针,既然本身不存在对象,const修饰谁呢?我就认为无法修饰,所以出错.
在Microsoft Visual Studio 10 Visual C++提示信息是静态成员函数上不允许修饰符.
具体详细信息,还可以参看Bjarne Stroustrup博士《The Design and Evolution of C++》一书中对static修饰符的解释.你还可以知道命名空间的来源.
我们在来看看:
int _tmain(int argc, _TCHAR* argv[])
{
C c;
c.ShowC();
C::StaticShowC();
return 0;
}
对于c.ShowC()汇编代码:
0014176E lea ecx,[c]
00141771 call C::C (141131h)
......
00141800 push ebp
00141801 mov ebp,esp
00141803 sub esp,0CCh
00141809 push ebx
0014180A push esi
0014180B push edi
0014180C push ecx // 这里实际上要把c对象压入.
......
为什么普通成员函数要压入自己???
其实c.ShowC()相当于void ShowC(const
C& c);具体怎么回事,这里不讨论.
c.StaticShowC()和C::StaticShowC()生成的汇编代码都为:
c.StaticShowC();
0014177E call C::StaticShowC
(141271h)
C::StaticShowC();
00141783 call C::StaticShowC
(141271h)
c.StaticShowC()和C::StaticShowC()最终调用其实是一样的.
Bjarne Stroustrup博士给const的定义:
引用Bjarne Stroustrup博士《The Design and Evolution of C++》13.3.2节
为了保证某些(并不是全部)const对象能够被放进只读存储器(ROM)里,我原来采纳了这样的一个规则:任何具有构造函数的对象(它需要做运行时的初始化)都不能放进ROM,其他的const可以放。这种做法与我对什么能够做初始化,怎样做以及什么时候做的长期关注有密切关系。C++语言提供了静态(连接时的)初始化和动态(运行时的)初始化。这个规则既允许对const对象做动态的初始化,也允许对不需要做动态初始化的对象使用ROM。后一种情况的典型例子是简单对象的大数组,例如YACC的分析表。
将const概念与构造函数联系起来也是一种折衷,考虑了我对const的理想与可用硬件的现实,以及应该相信程序员在写明显的类型转换时知道自己正在做什么的观点。在Jerry Schwarz的推动下,这个规则现在已经被另一个更接近我原来理想的规则取代了。将一个对象声明为const,就是认为它具有从其构造函数完成到析构函数开始之间的不变性。在这两点之间对这个对象进行写人,其结果应该认为是无定义。
我还记得在开始设计const机制时提出过这样的论点,当时说理想的const应该是这样的一种对象,直到其建构函数完成之前它都是可以写的,而后通过某种硬件的法术就变成了只读的,而最后到析构函数的人口点它又重新变成可以写的。你可以设想一种实际上就是这样工作的带标记的系统结构,对于这种实现,如果某人企图向定义为const的对象做写人就会引起一个运行错误。在另一方面,人则可以去写一个本身并没有定义为const,但却是经过const指针或者引用传递过来的对象。在这两种情况下,用户都必须首先强制去掉这个对象的const。这个观点意味着强制去掉一个原来就定义为const的对象的const,而后对它做写操作最好的情况就是无定义;而对一个原来并没有定义为const的对象同样做这些事情则是合法的,有清楚定义的。
请注意,按照这个精炼了的规则,const的意义将不仅依赖于这个类型是否有建构函数;从原则上说任何类都可能有建构函数。任何被声明为const的对象都可以放进ROM,放到代码段里,或者通过存储控制进行保护,等等,以保证它在接受了初始值之后不再发生变化。这种保护并不是必须要求的东西,无论如何当前的系统还不能保护每个const,使它们避免任何形式的堕落。
对一个实现而言,在如何处理const上它还是可以有很大程度的变化。让废料收集程序或者数据库系统修改一个const对象的值(例如将它移到磁盘或者移回来)不存在任何逻辑问题,只要能保证对于用户而言这个对象并没有变化。
以上内容不对的地方,请大家指正.
我的邮箱:weiwutan@gmail.com