类、类定义、类声明、类的数据成员、类的成员函数的一些注意点

一、信息隐藏

      信息隐藏是把类的内部表示和实现声明为私有的,而把类对象上执行的操作声明为公有的。私有内部表示被称为是封装的,而类的公有部分被称为类接口。

二、类的数据成员

      类的数据成员可以分为静态数据成员和非静态数据成员。除了静态数据成员外,数据成员不能再类体中被显示地的初始化。例如:

class First
{
private:
	int memi = 0 ; //错误
	double memd = 0.0 ; //错误
};


三、访问修饰符有public,private,protected。如果没有指定访问限定符,则缺省情况下,在类体的开始左括号后面的区是private区。

四、友元

       在某些情况下,允许某个函数而不是整个程序可以访问类的私有成员,这样做会比较方便。友元机制允许一个类授权其他的函数访问它的非公有成员。注意是非公有成员,即也就是说可以访问私有成员和受保护的成员。一个友元或许是一个名字空间函数,另一个前面定义的类的一个成员函数,也可能是一个完整的类,在使一个类成为友元时,友元类的所有成员函数都被给予访问“授权友谊的类的非公有成员”的权力。

五、类声明和类定义

我们可以声明一个类但不定义它。例如:

class Screen ;  //Screen类的声明

这个声明向程序引入了一个名字Screen,指示Screen为一个类类型。

注意一点:

      我们只能以有限的方式使用已经被声明但还没有被定义的类类型。如果没有定义类,那么我们就不能定义该类类型的对象,因为类类型的大小不知道,编译器不知道为这种类类型的对象预留多少存储空间。

      但是,我们可以声明指向该类类型的指针或引用。允许指针和引用是因为它们都有固定的大小,这与它们指向的对象的大小无关。但是,因为该类的大小和类成员都是未知的,所以要等到完全定义了该类,我们才可能将解引用操作符(*)应用在这样的指针上,或者使用指针或引用来指向某一个类成员。

特别注意一点:

       当我们只是声明了一个类时,还没有定义它,我们只能使用该类来定义类型为该类的指针或引用。例如:

class Screen ;  //Screen类的声明
class StackScreen
{
private:
	int topStack ;
	//正确:指向一个Screen对象,这里只能用类Screen来定义类型为该类的指针或引用
	Screen *stack ;
	//错误:类Screen不能有自身类型的数据成员,因为它只是被声明没有被定义
	Screen scr ;  


       因为只有当一个类的类体已经完整时,它才被视为被定义,所以一个类不能有自身类型的数据成员。但是,当一个类的类头被看到时,它被视为已经被声明了,所以一个类可以用指向自身类型的指针或引用作为数据成员。例如:

class LinkScreen
{
	LinkScreen *next ;    //正确
	LinkScreen &link ;    //正确
	LinkScreen new_link ; //错误
};

类声明和类定义的区别:

类声明:class的声明并不提供class body。因此,虽然class的名称被导入程序中,但用途有限,一旦声明,唯一可用的是该class类型的pointer和reference。由于class的大小是未知的,所以我们无法定义其objects。class的声明最常用于“某个class需要声明一个指针指向另一个class,而后者是没有定义或无法定义”的情况

类定义:class的定义提供class body,因而提供class的所有成员的声明,由于此后class的大小已知,于是能够定义其objects。

六、类对象

       类的定义,不会引起存储区分配。只有当定义一个类的对象时,系统才会分配存储区。例如:

class Screen
{
public:
	//成员函数
private:
	string _screen ;
	string::size_type _cursor ;
	short _height ;
	short _width ;
};


       上面类Screen的定义没有分配存储区。如下定义:

Screen myScreen ;  //分配了存储区


       上面的类Screen定义了一个对象myScreen,所以为对象myScreen分配了存储区。分配的存储区是含有Screen类的四个数据成员。名字myScreen引用到这块存储区每个类对象都有自己的类数据成员拷贝。修改myScreen的数据成员不会改变任何其他Screen对象的数据成员。

       在缺省的情况下,当一个类对象被指定为函数实参或函数返回值时,它就被按值传递。

       再介绍一下常量对象。什么是常量对象呢?例如Screen() ; (假设该类有默认构造函数),则这样就建立了一个常量对象。

       常量对象的特点:

       常量对象的作用域不是整个main函数,而是仅限于包含该常量的值表达式,表达式一旦计算完成,其中的对象就按构造构造的逆序析构。

七、类成员函数

1、inline和非inline成员函数

       如果一个类的成员函数的定义是在类体内完成的,这些函数称为“在类定义中定义的内联函数”。这些函数自动被作为inline函数处理。同时,我们也可以通过在成员函数的返回类型前显示的指定关键字inline,在类体内将这些成员函数声明为inline的。例如:

class Screen
{
public:
	//成员函数
	void home() {_cursor = 0 ;}            //默认为内联函数
	void move(int,int) ;
	char get() {return _screen[_cursor] ;} //默认为内联函数
	//...
};

       成员函数必须先在其类体内被声明,而且类体必须在成员函数被定义之前出现。通常,在类体外定义的成员函数不是inline的,但是,这样的函数也可以被声明为inline,可以通过显式的在类体中出现的函数声明上使用关键字inline,或者通过在类体外出现的函数定义上显式的使用关键字inline,或两者都使用。例如:

inline void Screen::move(int r,int c) //在类体外定义,显式使用inline关键字
{
	if (checkRange(r,c))
	{
		int row = (r-1)*_width ;
		_cursor = row + c - 1 ;
	}
}

或者如下使用关键字inline

class Screen
{
public:
	
	inline char get(int,int) ;//声明的时候添加了关键字inline
	//其他函数声明不变
};

char Screen::get(int r,int c) //在类体内声明使用了关键字inline,在类体外定义时就不用使用关键字inline
{
	move(r,c) ;
	return get() ;
}

       由于内联函数必须在调用它的每个文本文件中被定义,所以没有在类体中定义的内联成员函数必须被放在类定义出现的头文件中。例如,前面出现的move()和get()的定义应该被放在头文件Screen.h中,且跟在类Screen后面。也就是说,内联函数声明和定义必须在同一个文件中,不能将内联函数的声明和定义放在不同的文件中。例如,不能讲内联函数的声明放在.h文件中,而起定义或实现放在.cpp文件中,这样做是不允许的。

2、私有成员函数

类的私有成员函数只能被类的其他成员函数(和友元)调用,程序不能直接调用他们。


class Screen
{
public:
	
	inline char get(int,int) ;//声明的时候添加了关键字inline
	//其他函数声明不变
};

char Screen::get(int r,int c) //在类体内声明使用了关键字inline,在类体外定义时就不用使用关键字inline
{
	move(r,c) ;
	return get() ;
}


3、const成员函数

       把一个成员函数声明为const可以保证这个成员函数不修改类的数据成员。但是如果该类含有指针,那么在const成员函数中就能修改指针指向的对象。编译器不会把这种修改检测为错误。例如:

class Text
{
public:
	void bad(const string& parm) const ;
private:
	char *_text ;  //定义了类型我char的指针
};

void Text::bad(const string& parm) const
{
	_text = parm.c_str() ;//错误:不能修改_text
	for(int i=0;i<parm.size();++i)
		_text[i] = parm[i] ; //可以修改_text指针指向的对象的值,不好的风格,但不是错误的
}

       从上面的程序我们可以发现,我们在const成员函数bad里面修改_text指针指向的对象,所以const函数并不能阻止程序员可能做到的所有修改动作。
       const对象只能调用const函数。但是非const对象可以强制调用const函数。 例如:

#include <iostream>
using namespace std ;

class Screen
{
public:
	char getValue() {return _value ;}
private:
	int _value ;
};

int main()
{
	const Screen s ;  //s被定义成const对象
	cout << s.getValue() << endl  ; //错误:用const对象s去调用非const函数
	return 0 ;
}

上述程序会出现编译错误:

        error C2662: “Screen::getValue”: 不能将“this”指针从“const Screen”转换为“Screen &”

       这是由于对象s是const对象,而成员函数getValue是非const成员函数。导致二者的类型不一致。这说明const对象只能调用const成员函数。解决的办法有两个:第一个,将对象s声明为非const对象。第二个,将成员函数getValue声明为const成员函数。

      对于非const对象,也可以调用const成员函数,不过必须进行类型转换。

#include <iostream>
using namespace std ;

class Screen
{
public:
	char getValue() {cout << "调用非const的getValue函数\n" ;return _value ;}
	char getValue() const {cout << "调用const的getValue函数\n" ;return _value ;}
private:
	int _value ;
};

int main()
{
	Screen s ;  //s被定义成非const对象
	cout << s.getValue() << endl  ; //正确:用非const对象s去调用非const函数
	cout << const_cast<const Screen&>(s).getValue() << endl  ; //正确:用非const对象s去调用const函数
	return 0 ;
}

      使用const_cast进行常量对象和非常量对象之间的转换。"<>"里面的类型必须是指针类或引用类型。

      还要注意一点的就是,const对象对于构造函数和析构函数的调用,不管构造函数和析构函数是否是是const函数,const对象都会调用这两个函数。

4、volatile成员函数

    如果一个类对象的值可能被修改的方式是编译器无法控制或检测到的,那么我们可以把这个对象声明为volatile对象。volatile修饰符的主要目的是提示编译器,该对象的值可能在编译器未监测到的情况下被改变。因此编译器不能武断的对引用这些对象的代码做优化处理。

5、mutable数据成员

为了允许修改一个类的数据成员,即使它是一个const对象的数据成员,我们也可以把该数据成员声明为mutable(易变的)。mutable数据成员永远不会是const成员,即使它是一个const对象的数据成员。mutable成员总是可以被更新,即使是在一个const成员函数中。我们使用关键字mutable的格式如下:

mutable int a ;

下面请看使用mutable的例子:

class Screen
{
public:
	//成员函数
private:
	string _screen ;
	mutable  int _value ;
};

我们可以使用const成员函数;来修改mutable成员_value的值:

inline void Screen::move(int r,int c) const //const成员函数
{
	//...
	//正确:const成员函数可以修改mutable成员
	_value = _value + 1 ;
	//...
}

      我们可以看到我们可以在const成员函数中修改数据成员_value的值。

八、静态数据成员

       我们用关键字static来定义静态数据成员。静态数据成员被当做该类类型的全局对象。对与非静态数据成员,每个对象都有自己的拷贝,而静态数据成员对每个类类型的只有一个拷贝。静态数据成员只有一份,由该类类型的所有对象共享访问。

       同全局对象相比,静态数据成员有两个优势:

1、静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其他全局名字相冲突的可能性

2、可以实现信息隐藏。静态数据成员可以是private,而全局对象不能

我们可以看下下面的例子:

class Account
{
public:
	Account(double amount,const string &owner) ;
	string owner() {return _owenr ;}
private:
	static double _interestRate ; //_interestRate为静态数据成员
	double _amount ;
	string _owner ;
};

       我们可以看到_interestRate声明为static,而_amount和_owner不呢?这是因为每个Account对应不同的主人,有不同数目的金额,而所有的Account的利率却是相同的。所以将_interestRate设置为static,为类Account的所有对象共享。

       一般地,静态数据成员在该类定义之外被初始化。如同一个成员函数被定义在类定义之外一样,在这种定义中的静态数据成员的名字必须被其类名限定修饰。例如:

//静态数据成员的显示初始化,且是在类定义之外,即不在类定义时初始化static数据成员
#include "account.h"
double Account::_interestRate = 0.0589 ;

      注意静态数据成员的名字前加上了类名限定修饰(Account::)。一定要注意这一点。

      当这个static变量是const变量时,该变量在类体内初始化,但该成员仍要在类定义之外进行定义。且类外定义时不能再指定初始值了。例如:

//头文件
class Account
{
public:
	//...
private:
	static const int nameSize = 16 ;
	static const char name[nameSize] ;
};

//文本文件
const int Account::nameSize ; //必需的成员定义
const char Account::name[nameSize] = "Saving Account" ;

       因为name是一个数组(不是整型),所以它不能在类体内被初始化。还有一点要注意的是,静态数据成员name的定义是在它的类的域内,当限定修饰名Account::name被看到之后,它就可以引用Account的私有成员数据成员,比如nameSize。任何试图这么做的行为都会导致编译时刻错误。例如:

//头文件
class Account
{
public:
	//...
private:
	static const int nameSize = 16 ;
	static const char name[nameSize] = "Saving Account" ;
};

编译时会出现错误:

error C2864: “Account::name”: 只有静态常量整型数据成员才可以在类中初始化

       从上面的错误我们可以看到:只有数据类型是整型的静态数据成员才可以在类中初始化。即使有些数据成员是静态的,但是其类型不是整型,也不可以在类中被初始化。所以name必须在类定义之外被初始化。

       静态数据成员一些独特的特性:

       (1)、静态数据成员的类型可以是其所属类,而非static数据成员只能被声明为该类的对象的指针或引用。例如:

class Bar
{
public:
	// ...
private:
	static Bar mem1 ;  //静态数据成员的类型可以是其所属的类
	Bar *mem2 ;        //非静态数据成员的类型可以该类的指针
	Bar &mem3 ;        //非静态数据成员的类型可以该类的引用
	Bar mem4 ;         //非静态数据成员的类型不可以是其所属的类
};

      (2)、静态数据成员可以被作为类成员函数的缺省,而非static成员不能。例如:

extern int var ;

class Foo
{
private:
	int var ;
	static int stcvar ;
public:
	//错误:被解析的非static的Foo::var 
	//没有相关的类对象
	int mem1(int = var) ;    //错误

	//ok:解析为static的Foo::stcvar
	int mem2(int = stcvar) ; //正确

	//ok:int var的全局实例
	int mem3(int = ::var) ;  //正确
};

九、静态成员函数

       静态成员函数的声明除了在类体中的函数声明前加上关键字static,以及不能声明为const或volatile之外的,与非静态成员函数相同。出现在类体外的函数定义不能指定关键字static。

       静态成员函数没有this指针,因此在静态成员函数中隐式或显示地引用之歌指针将导致编译时刻错误。试图访问隐式引用this指针的非静态数据成员也会导致编译时刻错误。例如:

inline double Account::dailyReturn()
{
	return(_interestRate / 365 * _amount) ;//_interestRate为静态数据成员,_amount为非静态数据成员
}

        成员函数dailyReturn不能声明为静态成员函数,因为它访问了非静态数据成员_amount。

        那么,我们如何调用静态数据成员函数呢?我们可以通过成员访问操作符(.)个箭头(->)为一个类对象或指针类对象的指针调用静态数据成员函数,也可以用限定修饰符直接访问或调用静态成员函数,而无需声明类对象。

十、指向类成员的指针

1、类成员的类型

      函数指针不能被赋值为成员函数的地址,即使返回类型和参数表完全匹配。例如,下面的pfi是一个函数指针,该函数没有参数,返回类型为int:

int (*pfi)() ;

      给出两个全局函数,HeightIs()和WidthIs():

int HeightIs() ;
int WidthIs() ;

     把HeightIs()和WidthIs()的任何一个或两个赋值给指针pfi都是合法的:

pfi = HeightIs ;
pfi = WidthIs ;

      类的Screen也定义了两个访问函数——height()和width()——它们也没有参数,返回类型也是int:

inline int Screen::height(){return _height ;}
inline int Screen::width() {return _width ;}

      但是把height和width任何一个赋给指针pfi都是类型违例,都将导致编译时刻错误:

pfi = &Screen::height ;
pfi = &Screen::width ;

      那么,为什么会出现这样的错误呢?那是因为成员函数有一个非成员函数不具有的属性——它的类。那么,成员函数指针和(普通)函数指针之间的区别是什么呢?函数指针存放函数的地址,可以被用来指针调用那个函数。成员函数指针首先必须被绑定在一个对象或者一个指针上,才能得到被调用对象的this指针,然后才调用指针所指的成员函数。下面,我们修改函数指针的声明,使其可以用成员函数来初始化。

int (Screen::* pfi)() ;//定义了一个指向类Screen的成员函数的指针

      上面的定义在指针类型符“*”前面加上了限定修饰符(Screen::),加上这个以后,该指针表示指向类Screen的成员函数的指针。这样我们使用类Screen的成员函数为pfi赋值时就不会出现错误了。例如:

pfi = &Screen::height ;
pfi = &Screen::width ;

       这样就不会出现错误了。同样的,对于类的数据成员也是一样的。

//指针p是指向short型的Screen类的数据成员的指针
short Screen::* p;    //前面加上了限定修饰符Screen::
p = &Screen::_height  //_height前面加上了限定修饰符Screen::

2、如何使用指向类成员的指针

      将指向类成员的指针绑定到一个对象或一个指向对象的指针上。例如:

//直接调用成员函数
if (myScreen.height() == bufScreen->height())
	bufScreen->copy(myScreen) ;
//通过成员指针的等价调用
if ((myScreen.*pmfi)() == (bufScreen->*pmfi)()) //*pmfi相当于height,*pmfs相当于copy
	(bufScreen->*pmfs)(myScreen) ;

3、静态类成员的指针

      在非静态类成员的指针和静态类成员的指针之间有一个区别。指向类成员的指针语法不能被用来引用类的静态成员。静态类成员是属于该类的全局对象和函数。它们的指针是普通指针。(记住:静态成员函数没有this指针)。

      指向静态类成员的指针的声明看起来与非静态成员的指针相同。解引用该指针不需要类对象。例如:

class Account
{
public:
	static void raiseInterest(double incr) ;         //静态成员函数
	static double interest(){return _interestRate;}  //静态成员函数
	double amount(){return _amount;}
private:
	static double _interestRate ;                    //静态数据成员
	double _amount ;
	double _owner ;
};

inline void Account::raiseInterest(double incr)
{
	_interestRate += incr ;
}

      &_interestRate的类型是double*,而不是double Account::*。因为_interestRate是静态成员函数。指向该静态成员变量的指针定义如下:

//OK:是double*,而不是double Account::*
double *pd = &Account::_interestRate ;

      它被解引用的方式与普通指针一样,不需要相关的类对象。例如:

Account uint ;
//用普通的解引用操作符
double daily = *pd / 365 * uint._amount ;

      由于_interestRate和_amount都是私有成员,所以我们需要使用静态成员函数interest()和非静态成天函数amount()。

      指向interest()的指针的类型是一个普通函数指针:

                 double (*)() ;  //正确

      而不是类Account的成员函数的指针:

                 double (Account::*)() ; //错误

      这个指针的定义和对interest函数的间接调用处理方式与非类的指针相同:

//注意这里是double(*pf)()不是double(Account::*pf)() 
double (*pf)() = &Account::interest ;
double daily = pf() / 365 * uint.amount() ;


 


 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值