虽然数据隐藏保护了数据的安全性,但各种形式的数据共享却又不同程度地破坏了数据的安全。因此,对于既需要共享有需要防止改变的数据应该声明为常量。因为常量在程序运行期间不可改变,所以可以有效保护数据。
1.常对象
常对象:它的数据成员值在对象的整个生存期内不能被改变。也就是说,常对象必须进行初始化,而且不能被更新。
声明常对象的语法形式:
const 类型说明符 对象名;
例如:
class A
{
public:
A(int i,int j):x(i),y(j){}
private:
int x, y;
};
const A a(3, 4);//a是常对象,不能被更新
与基本数据类型的常量相似,常对象的值不能被改变。在C++语法中,对基本数据类型的常量提供了可靠的保护。如果程序中出现了类似下面这样的语句,编译时是会出错的。也就是说,语法检查时确保了常量不能被赋值。
const int n=10;//正确,用10对常量n进行初始化
n=20;//错误,不能对常量赋值
【注意】在定义一个变量或者常量时为它指定初值叫做初始化,而在定义一个变量或常量以后使用赋值运算符修改它的值叫做赋值。
语法保障类类型的常对象的值不被改变:
改变对象的数据成员值有两个途径,一是通过对象名访问其成员对象,由于常对象的数据成员都被视为常量,这时语法限制不能赋值。二是在类的成员函数中改变数据成员的值,然而几乎无法预料和统计哪些成员函数会改变数据成员的值,对此语法只好规定不能通过常对象调用普通的成员函数。因此,为常对象定义了常成员函数。
【注意】基本数据类型的常量也可以看作一种特殊的常对象。
2.常成员函数
使用关键字const修饰的函数称为常成员函数,常成员函数的声明格式如下:
类型说明符 函数名(参数表)const;
【注意】
(1)const是函数类型的一个组成部分,因此在函数的定义部分也要带const关键字。
(2)如果将一个对象说明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数(这就是C++从语法机制上对常对象的保护,也是常对象唯一的对外接口方式)。
(3)无论是否通过常对象调用常成员函数,在常成员函数调用期间,目的对象都被视为常对象,因此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有用const修饰的成员函数(这就保证了在常成员函数中不会更改目的对象的数据成员值)。
(4)const关键字可以用于对重载函数的区分。例如,如果在类中这样声明:
void print();
void print() const;
这是对print函数的有效重载。
【注意】如果仅以const关键字区分对成员函数的重载,那么通过非const的对象调用该函数,两个重载函数都可以与之匹配,这是编译器将选择最近的函数重载——不带const关键字的函数。
【例】在R类中声明了一个常成员函数。
class R
{
public:
R(int r1, int r2) :r1(r1), r2(r2) {}
void print();
void print()const;
private:
int r1, r2;
};
void R::print()
{
cout << r1 << ":" << r2 << endl;
}
void R::print()const
{
cout << r1 << ";" << r2 << endl;
}
int main()
{
R a(5, 4);
a.print();//调用void print()
const R b(20, 32);//调用void print()const
b.print();
return 0;
}
运行结果及分析:
在R类中声明了两个同名函数print,其中一个是常成员函数。在主函数中定义了两个对象a和b,其中a是普通的R类类型的对象,b是R类类型的常对象。通过对象a调用的是没有const修饰的print函数,而通过对象b调用的是用const修饰的print常成员函数。
(1)如果将程序做以下修改:
class R
{
public:
R(int r1, int r2) :r1(r1), r2(r2) {}
void print();
//void print()const;
private:
int r1, r2;
};
void R::print()
{
cout << r1 << ":" << r2 << endl;
}
//void R::print()const
//{
// cout << r1 << ";" << r2 << endl;
//}
int main()
{
R a(5, 4);
a.print();//调用void print()
const R b(20, 32);//调用void print()const
b.print();//错误
return 0;
}
这样会导致编译不通过,因为b在主函数中被定义为常对象,常对象不可以调用普通的成员函数。
(2)如果将程序做以下修改:
class R
{
public:
R (int r1,int r2):r1(r1),r2(r2){}
//void print();
void print()const;
private:
int r1, r2;
};
//void R::print()
//{
// cout << r1 << ":" << r2 << endl;
//}
void R::print()const
{
cout << r1 << ";" << r2 << endl;
}
int main()
{
R a(5, 4);
a.print();
const R b(20, 32);
b.print();
return 0;
}
运行结果及分析:
如果R类中没有普通成员函数print,只有const修饰的常成员函数void print()const;
,那么在主函数中,普通的R类类型的对象a和常对象b都会调用常成员函数,所以普通对象和常对象都可以调用常成员函数。
3.常数据成员
就像一般数据一样,类的数据成员也可以是常量,使用const说明的数据成员为常数据成员。如果在一个类中说明了常数据成员,那么任何函数中都不能对该成员赋值。构造函数对常数据成员进行初始化,就只能通过初始化列表。
【例】在类A中声明了一个常数据成员。
class A
{
public:
A(int i);
void print();
private:
const int a;
static const int b;//静态常数据成员
};
const int A::b = 10;//静态常数据成员在类外声明和初始化。
//常数据成员只能通过初始化列表来获得初值
A::A(int i) :a(i){}
void A::print()
{
cout << a << ":" << b << endl;
}
int main()
{
/*建立对象a和b,并以100和1作为初值,分别调用构造函数,
通过构造函数的初始化列表给对象的常数据成员赋值*/
A m(100);
A n(1);
m.print();
n.print();
return 0;
}
运行结果:
分析:
类数据成员中的静态变量和常量都应当在类定义之外加以定义,但C++标准规定了一个例外:类的静态常量如果具有整型或枚举类型,那么可以直接在类中定义为它指定常量值,例如上面代码可以改写为如下所示,在类中定义时直接给b指定常量值10:
class A
{
public:
A(int i);
void print();
private:
const int a;
static const int b=10;//静态常数据成员
};
//const int A::b = 10;//静态常数据成员在类外声明和初始化。
//常数据成员只能通过初始化列表来获得初值
A::A(int i) :a(i){}
void A::print()
{
cout << a << ":" << b << endl;
}
int main()
{
/*建立对象a和b,并以100和1作为初值,分别调用构造函数,
通过构造函数的初始化列表给对象的常数据成员赋值*/
A m(100);
A n(1);
m.print();
n.print();
return 0;
}
这时不必在类外定义A::b,因为编译器会将程序中对A::b的所有引用都替换成数值10,一般无须再为A::b分配空间。但也有例外,例如如果程序中出现了对b取地址的情况,则必须通过专门的定义为A::b分配空间。由于已经在类中为它指定了初值,就不能再在类定义之外为它指定初值,即使两处给的值一样也不行。
但是对于类数据成员中的常量,必须在构造函数的初始化列表对其进行赋初值,即初始化。
4.常引用
如果在声明引用时用const修饰,被声明的引用就是常引用。常引用所引用的对象不能被更新。如果常引用作为形参,便不会意外地发生对实参的更改。常引用的声明形式如下:
const 类型说明符 &引用名;
非const的引用只能绑定到普通的对象,而不能绑定到常对象,但常引用可以绑定到常对象。一个常引用,无论绑定到一个普通对象,还是一个常对象,通过该引用访问该对象时,都只能把该对象当作常对象。这意味着,对于基本数据类型的引用,则不能为数据赋值,对于类类型的引用,则不能修改它的数据成员,也不能调用它的非const成员函数。
【例】常引用做形参
class Point//Point类的定义
{
public://外部接口
Point(int x = 0, int y = 0) :x(x), y(y) {}
int getX() { return x; }
int getY() { return y; }
friend float dist(const Point& p1, const Point& p2);//友元函数声明
private://私有数据成员
int x, y;
};
float dist(const Point& p1, const Point& p2)//常引用作为形参
{
double x = p1.x - p2.x;//通过对象访问Point类的私有数据成员
double y = p1.y - p2.y;
return static_cast<float>(sqrt(x * x + y * y));
}
int main()//主函数
{
const Point myp1(1, 1), myp2(4, 5);//定义Point类的对象
cout << "两点之间的距离为:";
cout << dist(myp1, myp2) << endl;//计算两点之间的距离
return 0;
}
运行结果及分析:
由于dist函数中,无须修改两个传入对象的值,因此将传参方式改为传递常引用更合适,这样,调用dist函数时,就可以用常对象作为参数。
【注意】对于在函数中无须改变其值的参数,不宜使用普通引方式传递,因为那会使得对象无法被传入,采用传值方式或传递常引用方式可避免这一问题。对于大对象来说,传值耗时较多,因此传递常引用为宜。拷贝构造函数的参数一般也宜采用常引用传递。
例如将上述代码改写为:
class Point//Point类的定义
{
public://外部接口
Point(int x = 0, int y = 0) :x(x), y(y) {}
int getX() { return x; }
int getY() { return y; }
friend float dist(Point& p1, Point& p2);//友元函数声明
private://私有数据成员
int x, y;
};
float dist(Point& p1, Point& p2)//常引用作为形参
{
double x = p1.x - p2.x;//通过对象访问Point类的私有数据成员
double y = p1.y - p2.y;
return static_cast<float>(sqrt(x * x + y * y));
}
int main()//主函数
{
const Point myp1(1, 1), myp2(4, 5);//定义Point类的对象
cout << "两点之间的距离为:";
cout << dist(myp1, myp2) << endl;//error
return 0;
}
此时,dist函数并不是以常引用的方式进行传参,但是,因为主函数中定义的对象为常对象,不可以采用普通的引用进行传参,所以编译不会通过。