1.2 文字常量和常变量
1、文字常量 p3
程序中的特殊标识符或表达式,由于同时满足:
(1)不可寻址(放在代码区)
(2)值不可变
所以可视为文字常量。他们是 静态数组名、枚举变量、全局(静态变量)首地址、#define定义的常量。
整型文字常量:
(1)前加0表示 八进制
(2)前加0x表示 十六进制
(3)后加L(推荐)或l,表示long类型
(4)后加U(或u)表示无符号数
eg.1024UL
2、常变量Const
同其他变量一样被分配空间,可寻址。
const是在高级语言的语义层面上定义的,是编译器在编译期做语法检测来保证,但是运行时,const变量不是在只读内存中,而是和一般变量一样放在数据区,所以一样可以对其进行修改。
所以:常变量是一种加了特殊限制的变量,理解成“只读”变量
即使是const修饰,也是可以修改的
#include <iostream>
using namespace std;
void ShowValue(const int &i) {
cout<<i<<endl;
}
int main()
{
const int j=5;
void *p=(void *)&j;
int *ptr=(int *)p;
(*ptr)++;
//cout<<j<<endl; //还是会显示5,因为编译器优化的时候将j替换为文字常量5
//但如果是int i=5; const int j=i; 则无法替换,直接输出j为6
ShowValue(j); //显示6
return 0;
}
3、常变量替换
如果常变量有初始化赋初值,那编译器将该常变量在其他地方替换成文字常量
但是如果开始不初始化就会错误
如:
void DefineArray(const int n){
int B[n]={}; //error,数组大小在编译期确定
}
int main(){
const int m=5;
int A[m]={}; //ok
}
4、文字常量和常变量寻址
int &r=5; //error,无法寻址文字常量,无法建立引用
const int &r=5; //ok,在数据区开辟一个值为5的无名整数量,然后将引用r与这个整形两绑定
1.3 const用法
1、const的位置
int const *p; //指向常量的指针(即常指针,const修饰的是int),指向的对象是const型,不可以修改,但是指针p的指向可以修改
int *const p; //指针常量(const修饰的是int*),指针变量p是const型,它的指向不可修改,但是指向的对象可以修改
const和数据类型结合在一起 —>“常类型”。(看成一个整体)
修饰类型时,既可以放在放前面,也可以放在后面;用常类型声明 or 定义变量,const只出现在变量前。
const和被修饰类型间不能有其他标识符存在。
引用本身可以理解成一个指针常量
故在引用前使用const没有意义
int & const r4=i; //const是多余的,编译器warning后忽略const存在
const配合二重指针,此例子中const在不同位置,结果不同
#include <iostream>
using namespace std;
int main()
{
int const **p1; //不是指针常量,指向 int count*(“int const*”是一个 指向整型常量的指针)
int *const *p2; //不是指针常量,但所指的变量是指针常量(int *const,即指向整型的指针常量,指向不能修改)
int i=5;
int j=6;
const int *ptr1=&i;
int *const ptr2=&j;
p1=&ptr1;
p2=&ptr2;
cout<<**p1<<endl;
cout<<**p2<<endl;
return 0;
}
输出:
5
6
上述p1和p2 赋值有讲究,如果 p1=&ptr2 或 p2=ptr1 就会编译错误
2、const修饰某个类 —> 常对象 和 常函数
const修饰对象–>常对象
const修饰成员函数—>常函数
在常函数中,不允许对任何成员变量进行修改
通过常对象,只能调用该对象的常函数
#include <iostream>
using namespace std;
class A
{
int num;
public:
A() {num=5;}
void disp();
void disp() const;
void set(int n) {num=n;}
};
void A::disp() const {
cout<<num<<endl;
}
void A::disp() {
cout<<"non-const version of disp()"<<endl;
}
int main()
{
A a1;
a1.set(3);
a1.disp();
A const a2;
a2.disp();
}
以上注意:
(1)如果常函数声明和定义分开,都需要加const,否则编译错误
只有类的非静态成员函数可以被声明为常函数
(2)如果一个类的两个成员函数,返回值、函数名、参数列表完全相同,其中之一是const,则重载。因为 常成员函数的参数传入this指针是const Class*类型的,参数不同,导致函数签名不同。
非只读对象(如a1)调用某个函数(如 disp()),先找非const版本,如果没有,再调用const版本。而常对象,只能调用类中定义的常函数,否则编译器报错。
如果一个非const对象(如a1)调用函数,同时有const和非const版本的函数,我们希望其调用const函数。就必须建立该对象的常引用,或指向该对象的常指针来达到目的。如: (const A&)a1.disp(); 或 (const A *)&a1->disp();
(3)常对象创建后,其数据成员不允许在修改。所以显示构造函数来初始化该对象非常重要。
常对象,全体成员数据成员都是常量看待。
类对象的非静态常量成员必须在构造函数中初始化,且只能借助初始化列表进行。
3、const修饰函数参数+函数返回值
#include <iostream>
using namespace std;
void disp1(const int &ri){
cout<<ri<<endl;
}
void disp2(const int i){
cout<<i<<endl;
}
const int disp3(const int& ri){
cout<<ri<<endl;
return ri;
}
int& disp4(int& ri){
cout<<ri<<endl;
return ri;
}
const int& disp5(int& ri){
cout<<ri<<endl;
return ri;
}
int main(int argc,char* argv[])
{
int n=5;
disp1(n);
disp2(n);
disp3(n);
disp4(n)=6; //修改引用返回值
disp5(n);//disp5(n)=6;是错误的
}
注意:
(1)const修饰参数,主要作用是被引用对象或被指向对象,如果只是形参,就没有多少意义。如:void disp2(const int i),这里的i在函数中改不改变,加不加const没什么影响。
不但如此,同时定义一个相似的用const修饰参数和不用const修饰参数的函数,会引起重定义错误。比如:任何整型表达式的值,都可以传给int型参变量,也可以传给const int型参变量,故不重载。
(2)当返回值是一个普通数据,而非引用,const修饰也没多少意义。因为函数返回值是一个非左值,本来就不能改变其值。故其上 const int disp3(const int& ri),对返回值修饰然并卵。
(3)如果返回值为引用,用const修饰可以阻止对被引用对象修改,disp5(n)=6;是错误的
(4)常见的对const的误解。
误解一:用const修改的变量值一定是不能改变的。const修饰的变量可通过指针可间接修改。
如:
const int j=5;
void *p=(void *)&j;
int *ptr=(int *)p;
(*ptr)++;
误解二:常引用或常指针,只能指向常变量,这是一个极大的误解。常引用或者常指针只能说明不能通过该引用(或者该指针)去修改被引用的对象,至于被引用对象原来是什么性质是无法由常引用(常指针)决定的。
1.4 const_cast 的用法
1、作用
const_cast 是 C++ 运算符,作用是去除符合类型中的const或者volatile
当大量使用const_cast是不明智的,只能说程序存在设计缺陷。使用方法见下例:
void constTest(){
int i;
cout<<"please input a integer:";
cin>>i;
const int a=i;
int& r=const_cast<int&>(a);//若写成int& r=a;则发生编译错误
++r;
cout<<a<<endl;
}
int main(int argc,char* argv[])
{
constTest();
return 0;
}
输入:
5
输出:
6
总结:
(1)const_cast运算符的语法形式是const_cast< type> (expression)。 括号不可省略
(2)const_cast只能去除目标的const或者volatile属性,不能进行不同类型的转换。只能将 const type* 转换为 type*,或者 const type & 转换为 type &。
如下转换就是错误的:
cons tint A={1,2,3};
char* p=const_cast< char*>(A); //不能由const int[]转换为char*
(3)一个变量被定义为只读变量(常变量),那么它永远是常变量。cosnt_cast取消的是间接引用时的改写限制,而不能改变变量本身的const属性。 如下就是错误的:
int j = const_cast< int> (i);
(4)利用传统的C语言中的强制类型转换也可以将 const type* 类型转换为 type* 类型,或者将 const type& 转换为 type& 类型。但是使用 const_cast 会更好一些,因为 const_cast 写法复杂(提醒程序猿不要轻易转换),转换能力较弱,目的明确,不易出错,易查bug;而C风格的强制类型转换能力太强,风险较大。
1.5 mutable 的用法
1、作用
mutable 用来解决常函数中不能修改对象的数据成员的问题。
如果在一些情况下,希望在常函数中仍然可以修改某个成员变量的值,就在该变量前加上mutable。能在保证常量对象大部分数据成员仍然“只读”情况下,实现对个别成员的修改。
#include <iostream>
#include <string>
using namespace std;
class Student
{
string Name; //默认为private
int times_of_getname;
public:
Student(char *name)
:Name(name),times_of_getname(0) {
}
string get_name() {
times_of_getname++;
return Name;
}
void PrintTimes() const {
cout<<times_of_getname<<endl;
}
};
int main()
{
const Student s("Bill王");
cout<<s.get_name()<<endl;
cout<<s.get_name()<<endl;
s.PrintTimes();
return 0;
}
如上程序会报错,因为常量对象s(信息不能被修改的学生实体),调用 非const 函数 get_name(),但是如果将 get_name() 改为 const,有无法修改 times_of_getname。
但如果修改为:
mutable int times_of_getname;
string get_name() const{
}
即可
2、使用注意事项
使用关键字mutable要注意以下几点:
(1)mutable 只用于类的非静态和非常量数据成员。
(2)mutable 关键字提示编译器该变量可以被类的const函数修改。
(3)一个类中,用mutable修饰的变量只能是少数,或者根本不使用,大量使用代表程序设计上的缺陷。
1.6 求余运算符
1、概览
%用于求余数,优先级与*和/相同,结合律也是从左至右。
要求两个操作数均为整数(或可以隐式转换成整数的类型),故:14.2%3就是错误的,因为double不能隐士转换为整形。
#include <iostream>
using namespace std;
int main()
{
char c=253;
int i=5;
cout<<c%2<<endl;
cout<<i%c<<endl;
cout<<19%10%5<<endl;
return 0;
}
输出:
-1
2
4
在c/c++中,char可视作单字节整形,取值范围-128~127,故可以参与求余。
253对应的二进制是0xFD,即-3的补码表示,C99标准规定:
如果%是正数,有 a%(-b) == a%b;如果%左边是负数,有(-a)%b == -(a%b)
有因为%是从左向右结合,所以19%10%5相当于(19%10)%5,结果是4
1.7 sizeof 的用法
C++基本数据类型的变量占据内存字节数的多少跟运行的平台有关。
int i = 9;
sizeof(i); // 4
sizeof(i=5); // 4
cout<<i; //9
因为 sizeof 是在编译时进行的运算,而与运行时无关,即可执行代码中不包含sizeof。sizeof 真正关心的是变量或表达式的类型,而不是值。在sizeof看来,i 和 int 是一样的。
类或结构体的sizeof
(1)不允许有长度为0的数据类型存在,至少占用1自己;
(2)类的成员函数(虚函数不是函数占空间)不占类的大小;
(3)内存对齐是为了提高cpu的存储速度。需要满足如下原则
1、VC++ 规定各成员变量存放的起始地址相对于对象的其实地址的偏移量必须为该变量的类型所占字节数倍数。
2、整个对象的大小必须是其成员变量最大尺寸的整数倍。
(4)如果一个类包含虚函数,那么编译器会在该类对象中插入一个指向虚函数表的指针。
1.8 引用与指针常量
引用变量是c++引入的重要机制。
错误观念:引用本质只是别名,在符号表中ri和i对应于相同的变量地址
int i=5;
0100437E mov dword ptr [i],5
int &ri=i;
01004385 lea eax,[i]
01004388 mov dword ptr [ri],eax
ri=8;
0100438B mov eax,dword ptr [ri]
ri=8;
0100438E mov dword ptr [eax],8
在底层实现上,引用是用指针常量实现的,如果用指针常量实现,反汇编是一毛一样的。引用和指针常量关系如下:
(1)内存中占用都是4字节,存放都是被引用对象的地址,都必须在定义的同时进行初始化。
(2)指针常量本身允许寻址;引用变量不允许寻址,&r返回的是被引用对象的地址,就是变量r中的值,而不是变量r的地址,r的地址由编译器掌握,程序猿无法直接对其进行存取。
(3)凡用引用的代码,都可以用指针常量实现;反之不然,因为引用本身限制较多,不能完成指针所有的操作。
例如下面的代码是合法的:
int i=5,j=6;
int *const array[] = {&i,&j}; //指针数组
但是如下代码却是非法的:
int i=5,j=6;
int &array[] = {i,j}; //不可能有引用的数组
虽然引用在初始化时会绑定一个变量,不过也是可以有特殊手段可以改变引用绑定关系:
int main()
{
//freopen("input.txt","r",stdin);\
int i=5,j=6;
int &r = i;
void *pi = &i,*pj = &j;
int* addr;
int dis = (int)pj-(int)pi;
addr = (int *)((int)pj+dis); //精确计算r的地址
cout<<"&i == "; PRINT(pi); //i的地址
cout<<"&j == "; PRINT(pj); //j的地址
cout<<"&pi == "; PRINT(&pi); //pi的地址
cout<<"&pj == "; PRINT(&pj); //pj的地址
cout<<"dis == "; PRINT(dis);
PRINT(*addr);
//(*add)+=dis;
(*addr)=(int)&j; PRINT(*addr); //将r指向j
r=666;
cout<<"i == "; PRINT(i);
cout<<"j == "; PRINT(j);
return 0;
}
答案输出:
&i == 003CF9D0
&j == 003CF9C4
&pi == 003CF9AC
&pj == 003CF9A0
dis == -12
3996112
3996100
i == 5
j == 666
因为在内存中排布方式是:
dis 低地址
addr
pj
pi
r
j
i 高地址
在
&j == 003CF9C4
&pi == 003CF9AC
中隔了2个dis,其实就是夹了一个r
1.9 左值的概念
左值是c++中的一个基本概念,凡是可以出现在赋值运算左边的表达式都是左值。右值跟左值相对,凡是可以出现在赋值运算右边的表达式都是右值。
左值一定可以作为右值,而反过来不一定成立。
左值概念有:
(1)必须可寻址
(2)非只读单元
(3)不能是临时无名对象 如: i+1 =5 不行
(4)如果表达式运算结果是一个引用,可以作为左值。 如:int &fun() 函数可以,(i+=1)=5 可以,i+=运算结果是对i的引用
由此可知:
(1)并不是只有单个变量才能作为左值
(2)也不能仅由表达式的外在形式判断是否为左值。要根据一个表达式的运算结果的性质判断。
结合引用的性质可知:
(1)能建立普通引用的表达式一定是左值;
(2)不能作为左值的表达式只能建立常引用,而不能建立普通引用。
函数的参数声明为引用,这样在发生函数调用时可以减少运行时开销。将函数的参数声明为一般的引用还是声明为常引用很有讲究。
int Max(int &a,int &b) {
return (a>b)?a:b;
}
int main()
{
//freopen("input.txt","r",stdin);
int i=2;
cout<<Max(i,5)<<endl;
return 0;
}
以上代码编译不通过,显示“非常引用的初始值必须为左值”,由于5不是左值,不能为它建立普通引用,所以编译错误。于是在此时修改函数定义
int Max(int &a,const int &b);
可见:
将函数的参数声明为常引用,不完全是因为参数的值在函数体内不能修改,还可能是接受非左值作为函数参数的情况。
常引用类型转换
对某个变量(或表达式)建立常引用,允许发生类型转换,而一般的引用不允许
int Max(const int &a,const int &b) {
return (a>b)?a:b;
}
int main()
{
//freopen("input.txt","r",stdin);
char c='a';
const int &rc = c;
PRINT((void*)&c);
PRINT((void*)&rc);
int i=7;
const int &ri = i;
PRINT((void*)&i);
PRINT((void*)&ri);
cout<<Max(rc,5.5)<<endl;
return 0;
}
输出:
如果将下句的const去掉则会报错。原因是普通引用只能建立在相同的数据类型变量上。同样,能允许 Max(rc,5.5) 这样的函数调用也是因为函数 Max() 的第二个参数是常引用,因此将实参 5.5 先转换为 int 型无名变量,然后再建立对该无名变量的常引用。
const int &rc = c;
对表达式建立常引用:
(1)首先要考虑该表达式结果是否能寻址
(2)其次还要考虑表达式结果的数据类型与引用数据类型是否一致
否则只能另外建立一个无名变量,该变量中存放非左值表达式的运算结果,然后再建立对该无名变量的常引用。
不过既然这里建立的是一个临时的无名变量,但这个变量同时也占用栈空间,那就可以相办法改变,而且改变这个值不会改变原有的变量:
int main()
{
//freopen("input.txt","r",stdin);
char c='a';
const int &rc = c;
PRINT(c);
PRINT(rc);
PRINT((void*)&c);
PRINT((void*)&rc);
int dis = (int)&rc-(int)&c;
PRINT(dis);
int* addr = (int*)((int)&c+dis); //精确计算rc指向的无名变量的地址
*addr = 666;
PRINT(c);
PRINT(rc);
return 0;
}
输出:
1.10 goto语句
goto是一种无条件跳转语句。
1974年,D. E. Knuth 对于 goto 语句的争论作了全面公正的评述:
(1)不加限制的使用 goto,特别是使程序往回跳,会使程序的结构难于理解
(2)为提高程序效率(主要指的是跳出多层循环),同时又不破坏程序良好结构,有控制的使用一些 goto 是有必要的。
如此我可以总结一下 goto 语句的一般使用情景:
(1)跳出多层循环
(2)不往回跳
注:
(1)一个带冒号的“标号”,代表的是程序中的某条语句在内存中的位置,必须跟在某条可执行语句之前。
(2)goto 语句只能跳转到同一个函数体内的标号。因为每个函数有一个建立函数栈的过程,而直接跳过去,代表什么呢?显然没有一个建立函数栈的过程。
1.11 volatile的用法
volatile 是“易变的”、“不稳定”的意思。volatile是 c++ 的一个关键字,用来解决在“共享”环境下容易出现的读取错误的问题。
在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。
这虽然在单任务环境下是一个优化过程,但是却是多任务环境下问题的起因。
多任务环境中,虽然在一个函数体内部,在两次读取变量之间没有对变量的值进行修改,但是该变量仍然有可能被其他的程序(如中断程序、另外的线程等)所修改。如果还是从寄存器而不是从RAM中读取变量的值,就会出现被修改了的比阿郎的之不能及时的反应的问题。如下程序对这一现象进行了模拟:
#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
int i=10;
int a=i;
cout<<a<<endl;
_asm{
mov dword ptr [ebp-4],80
}
int b=i;
cout<<b<<endl;
return 0;
}
程序在VS2012环境下生成 release 版本(一定要极端优化,vs编译环境下选择优化 速度最大化 /O2),输出结果也是:
10
10
顺便说一下,ebp是扩展基址指针寄存器(extended base pointer) 其内存放一个指针,该指针指向系统栈最上面一个栈帧的底部。
本来事实上已经通过内联汇编,修改过的值,为什么打印出来还是10呢
但是如果:
将 int i=10; 前加 volatile 就不会发生这种情况了。
跟踪汇编代码可以发现,凡是声明为 volatile 的变量,每次拿到的值都是从内存中直接读取的。
以下实验在 vs2012 release 环境下进行。
不加 volatile
int i=10;
int a=i;
tmp(a);
00D71273 push dword ptr ds:[0D73024h]
00D71279 mov ecx,dword ptr ds:[0D7303Ch]
00D7127F push 0Ah
00D71281 call dword ptr ds:[0D7302Ch]
00D71287 mov ecx,eax
00D71289 call dword ptr ds:[0D73028h]
_asm{
mov dword ptr [ebp-4],80
00D7128F mov dword ptr [ebp-4],50h
}
int b=i;
tmp(b);
00D71296 push dword ptr ds:[0D73024h]
00D7129C mov ecx,dword ptr ds:[0D7303Ch]
00D712A2 push 0Ah
00D712A4 call dword ptr ds:[0D7302Ch]
00D712AA mov ecx,eax
00D712AC call dword ptr ds:[0D73028h]
加了 volatile
tmp(a);
01201274 push dword ptr ds:[1203024h]
volatile int i=10;
0120127A mov dword ptr [i],0Ah
int a=i;
01201281 mov eax,dword ptr [i]
tmp(a);
01201284 mov ecx,dword ptr ds:[120303Ch]
0120128A push eax
0120128B call dword ptr ds:[120302Ch]
01201291 mov ecx,eax
01201293 call dword ptr ds:[1203028h]
_asm{
mov dword ptr [ebp-4],80
01201299 mov dword ptr [i],50h
}
int b=i;
012012A0 mov eax,dword ptr [i]
tmp(b);
012012A3 push dword ptr ds:[1203024h]
012012A9 mov ecx,dword ptr ds:[120303Ch]
012012AF push eax
tmp(b);
012012B0 call dword ptr ds:[120302Ch]
012012B6 mov ecx,eax
012012B8 call dword ptr ds:[1203028h]
由于编译器的极端优化,可以很明显的看到,在没有加 volatile 的情况下,甚至编译器是直接使用操作数 0Ah 进行运算的。
而在加了 volatile 的情况下,每次都是从 ptr [i] 中读取。
而且在速度极端优化的情况下,
void tmp(int t) {
cout<<t<<endl;
}
也自动 inline 处理了。
但是这里也抛出一个问题,为什么是 [ebp-4] 修改的就是i的值,更奇怪的是,我如果如下这样写代码,那改的会是哪个变量的值呢:
#include <iostream>
using namespace std;
void tmp(int t) {
cout<<t<<endl;
}
int main(int argc,char* argv[])
{
volatile int ic=12;
volatile int i=10;
int a=i;
volatile int ib=11;
tmp(a);
tmp(ib);
tmp(ic); //必须使用,如果不使用,编译器优化为使用同一块内存地址
_asm{
mov dword ptr [ebp-4],80
}
int b=i;
tmp(b);
return 0;
}
为什么分配的总是 [ebp-4] 是复制给 a 的值呢?试验过,如果将 ic 赋值给 a,那 [ebp-4] 存放的值将会是 ic
阅读以上程序,注意以下几个要点:
(1)以上代码必须在release模式下考查,因为只有Release模式(严格说需要在速度最大优化 /O2)下才会对程序代码进行优化,而这种优化在变量共享的环境下容易引发问题。
(2)凡是需要被多个任务共享的变量(如可能被中断服务程序访问的变量、被其他线程访问的变量等),都应声明为 volatile 变量。而且为了提高执行效率,要减少对 volatile 不必要的使用。
(3)由于优化可能会将一些“无用”的代码彻底去除,所以,如果确实希望在可执行文件中保留这部分代码,也可以将其中的变量声明为 volatile:
int main(int argc,char* argv[])
{
int s,i,j;
for(i=0;i<100;++i)
for(j=0;j<100;++j)
s=5;
return 0;
}
在生成 release 版本的程序时,由于循环体每次给 s 的值不变(简化为执行1次),或者说没有使用(1次都没有),但如果此时程序猿是希望循环拖延时间,写成 volatile 就可以了。