关于 const, inline, static, this 的基本知识
const
(1) const 基础
如果const关键字不涉及到指针,我们很好理解,下面是涉及到指针的情况:
int b = 500;
const int* a = &b; [1]
int const *a = &b; [2]
int* const a = &b; [3]
const int* const a = &b; [4]
如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。不知道,也没关系,我们可以参考《Effective c++》Item21上的做法, 如果 const 位于星号的左侧,则 const 就是用来修饰指针所指向的变量,即指针指向为常量;如果 const 位于星号的右侧,const 就是修饰指针本身,即指针本身是常量。
因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。
(2) 作为参数
void display(const double& r);
void display(const double* r);
说明:
- 在引用或者指针参数的时候使用 const 限制是有意义的,而对于值传递的参数使用 const 则没有意义
- 保证引用的变量的值不被改变
- const 在 double 前或者后面意思相同,只是不同的人的写法不同
(3) const对象
声明为 const 的对象只能访问类中声明为 const 的成员函数,不能调用其它成员函数。
(4) const成员函数
类型说明符 函数名(参数表) const;
void print(int i) const;
说明:
- const 是函数类型的一个组成部分,因此在实现部分也要带 const 关键字
- 常成员函数不能更新对象的数据成员,也不能调用该类中没有用 const 修饰的成员函数
(5) 使用const的一些建议
- 要大胆的使用 const,这将给你带来无尽的益处,但前提是你必须搞清楚原委
- 要避免最一般的赋值操作错误,如将 const 变量赋值,具体可见思考题
- 在参数中使用 const 应该使用引用或指针,而不是一般的对象实例,原因同上
- const 在成员函数中的三种用法(参数、返回值、函数)要很好的使用
- 不要轻易的将函数的返回值类型定为 const
- 除了重载操作符外一般不要将返回值类型定为对某个对象的 const 引用
inline
(1) 预处理宏
介绍内联函数之前,有必要介绍一下预处理宏。内联函数的功能和预处理宏的功能相似。相信大家都用过预处理宏,我们会经常定义一些宏,如:
#define TABLE_COMP(x) ((x)>0 ? (x) : 0)
就定义了一个宏。
为什么要使用宏呢?因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。而宏只是在预处理的地方把代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个函数更有效率。
但是宏也有很多的不尽人意的地方。
- 宏不能访问对象的私有成员。
- 宏的定义很容易产生二意性。
我们举个例子:
#define TABLE_MULTI(x) (x*x)
我们用一个数字去调用它,TABLE_MULTI(10),这样看上去没有什么错误,结果返回100,是正确的,但是如果我们用TABLE_MULTI(10+10)去调用的话,我们期望的结果是400,而宏的调用结果是(10+10*10+10),结果是120,这显然不是我们要得到的结果。避免这些错误的方法,一定要给宏的参数都加上括号。
#define TABLE_MULTI(x) ((x)*(x))
这样可以确保不会出错,但是,即使使用了这种定义,这个宏依然有可能出错,例如使用TABLE_MULTI(a++)调用它,他们本意是希望得到(a+1)(a+1)的结果,而实际上呢?我们可以看看宏的展开结果: (a++)(a++),如果a的值是4,我们得到的结果是56=30。而我们期望的结果是55=25,这又出现了问题。事实上,在一些C的库函数中也有这些问题。例如: Toupper(*pChar++)就会对pChar执行两次++操作,因为Toupper实际上也是一个宏。
(2) inline 函数
我们可以看到宏有一些难以避免的问题,怎么解决呢?
下面就是用我要介绍的内联函数来解决这些问题,我们可以使用内联函数来取代宏的定义。而且事实上我们可以用内联函数完全取代预处理宏。
内联函数和宏的区别在于,宏是由 预处理器 对宏进行替代,而内联函数是通过 编译器控制 来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
我们可以用 inline 来定义内联函数,不过,任何在类的声明部分定义的函数都会被自动的认为是内联函数。
在函数声明或定义中函数返回类型前加上关键字 inline 即把min()指定为内联。
inline int min(int first, int secend) {/****/};
我们可以把它作为一般的函数一样调用。但是执行速度确比一般函数的执行速度要快。
注意:
- inline 函数对编译器而言 必须是可见的,以便它能够在调用点内展开该函数。
- 与非inline函数不同的是,inline函数 必须在调用该函数的每个文本文件中定义。当然,对于同一程序的不同文件,如果C++ inline函数出现的话,其定义必须相同。
- 对于由两个文件compute.c和draw.c构成的程序来说,程序员不能定义这样的min()函数,它在compute.c中指一件事情,而在draw.c中指另外一件事情。如果两个定义不相同,程序将会有 未定义的行为 (undefined behavior)。
- 为保证不会发生这样的事情,建议把inline函数的定义 放到头文件中。在每个调用该inline函数的文件中包含该头文件。这种方法保证对每个inline函数只有一个定义,且程序员无需复制代码,并且不可能在程序的生命期中引起无意的不匹配的事情。
(3) inline 函数的编程风格
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。如下风格的函数 Foo 不能成为内联函数:
inline void Foo(int x, int y);
// inline 仅与函数声明放在一起
void Foo(int x, int y){}
而如下风格的函数 Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y)
// inline 与函数定义体放在一起{}
所以说,C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
定义在类声明之中的成员函数将自动地成为内联函数,例如:
class A
{
public:
void Foo(int x, int y) { }
// 自动地成为内联函数
}
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y){}
内联函数在C++类中,应用最广的,应该是用来定义存取函数。我们定义的类中一般会把数据成员定义成私有的或者保护的,这样,外界就不能直接读写我们类成员的数据了。对于私有或者保护成员的读写就必须使用成员接口函数来进行。如果我们把这些读写成员函数定义成内联函数的话,将会获得比较好的效率。
Class sample {
Private:
int nTest;
Public:
int readtest(){ return nTest;}
Void settest(int I) {nTest=I;}
}
当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。
static
static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。
(1)静态数据成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。
使用静态数据成员可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
静态数据成员的使用方法和注意事项如下:
- 静态数据成员在定义或说明时前面加关键字static。
- 静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下:
<数据类型><类名>::<静态数据成员名>=<值>
这表明:
- 初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆。 2.初始化时不加该成员的访问权限控制符private,public等。
- 初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员。
- 静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化。
- 引用静态数据成员时,采用如下格式:
<类名>::<静态成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员。
程序实例:
# include …
class Myclass{
public:
Myclass(int a, int b, int c);
void GetNumber();
void GetSum();
private:
int A, B, C;
static int Sum;
};
int Myclass::Sum = 0;
Myclass::Myclass(int a, int b, int c){
A = a;
B = b;
C = c;
Sum += A+B+C;
}
void Myclass::GetNumber(){
cout<<"Number="<<a<<","<<b<<","<<c<<endl;
}
void Myclass::GetSum(){
cout<<"Sum="<<sum<<endl;
}
void main(){
Myclass M(3, 7, 10),N(14, 9, 11);
M.GetNumber();
N.GetNumber();
M.GetSum();
N.GetSum();
}
从输出结果可以看到Sum的值对M对象和对N对象都是相等的。这是因为在初始化M对象时,将M对象的三个int型数据成员的值求和后赋给了Sum,于是Sum保存了该值。在初始化N对象时,对将N对象的三个int型数据成员的值求和后又加到Sum已有的值上,于是Sum将保存求和后的值。所以,不论是通过对象M还是通过对象N来引用的值都是一样的,即为54。
(2) 静态成员函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数中要引用非静态成员时,可通过对象来引用。下面通过例子来说明这一点。
#include …
class M{
public:
M(int a){
A=a;
B+=a;
}
static void f1(M m);
private:
int A;
static int B;
};
void M::f1(M m)
{
cout<<"A="<<m.a<<endl;
cout<<"B="<<b<<endl;
}
int M::B=0;
void main()
{
M P(5),Q(10);
M::f1(P); //调用时不用对象名
M::f1(Q);
}
读者可以自行分析其结果。从中可看出,调用静态成员函数使用如下格式:
<类名>::<静态成员函数名>(<参数表>);
this 指针
先要理解class的意思。class应该理解为一种类型,像int和char一样,是用户自定义的类型。(虽然比 int 和 char 这样 built-in 类型复杂的多,但首先要理解它们一样是类型)。用这个类型可以来声明一个变量,比如int x, myclass my等等。这样就像变量 x 具有 int 类型一样,变量 my 具有 myclass 类型。
理解了这个,就好解释 this 了,my 里的 this 就是指向 my 的指针。如果还有一个变量myclass mz,mz 的 this 就是指向 mz 的指针。这样就很容易理解this 的类型应该是 myclass *,而对其的解引用 *this 就应该是一个 myclass 类型的变量。
通常在class定义时要用到类型变量自身时,因为这时候还不知道变量名(为了通用也不可能固定实际的变量名),就用this这样的指针来使用变量自身。
(1) this指针的用处
一个对象的 this 指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上 this 指针,编译器在编译的时候也是加上 this 的,它作为非静态成员函数的隐含形参,对各成员的访问均通过 this 进行。
例如,调用 date.SetMonth(9) <===> SetMonth(&date, 9),this帮助完成了这一转换。
(2) this指针的使用
一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;另外一种情况是当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
(3) this指针程序示例
this指针是存在与类的成员函数中,指向被调用函数所在的类实例的地址。根据以下程序来说明 this 指针:
#include<iostream.h>
class Point
{
int x, y;
public:
Point(int a, int b) { x=a; y=b;}
Void MovePoint( int a, int b){ x+=a; y+=b;}
Void print(){ cout<<"x="<<x<<"y="<<y<<endl;}
};
void main( )
{
Point point1( 10,10);
point1.MovePoint(2,2);
point1.print( );
}
当对象 point1 调用 MovePoint(2,2) 函数时,即将 point1 对象的地址传递给了 this 指针。
MovePoint 函数的原型应该是 void MovePoint( Point *this, int a, int b);第一个参数是指向该类对象的一个指针,我们在定义成员函数时没看见是因为这个参数在类中是隐含的。这样point1的地址传递给了 this,所以在 MovePoint 函数中便显式的写成:
void MovePoint(int a, int b) { this->x +=a; this-> y+= b;}
即可以知道,point1调用该函数后,也就是point1的数据成员被调用并更新了值。即该函数过程可写成 point1.x+= a; point1. y + = b;
(4) 关于this指针的一个精典回答
当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this 是一个指针,它时时刻刻指向你这个实例本身。