声明,所有的朋友,如果要转我的帖子,务必注明"作者:黑啤来源:CSDN博客"和 具体的网络地址http://blog.csdn.net/nx500/archive/2007/10/24/1842453.aspx,并且我的所有 博客内容,禁止任何形式的商业用途,请大家尊重我的劳动.谢谢!
目 录
十二.类.类类型通常被称为抽象数据类型,抽象数据类型是面向对象编程和泛型编程的基础.
001 类就是定义了一个新的类型和新的作用域.
所有类成员必须在类的内部声明,一旦类定义完成后,就没有任何方式可以增加成员了.
创建一个类类型对象时,编译器会自动使用一个构造函数来初始化该对象.
构造函数一般应使用一个构造函数初始化列表来初始化对象的数据成员.
Sales_item():units_sold(0), revenue(0.0){ }
在类的内部,声明成员函数是必需的,而定义成员函数则是可选的.在类内部定义的函数默认为inline类型.
在类外部定义的成员函数必须指明他们是在类的作用域中.
Sales_item::avg_price...
成员函数有个附加的隐含实参(this),将函数绑定到调用函数的对象上.
比如trans.avg_price()函数调用,avg_price()函数内部对Sales_item类成员的引用,就是对trans成员的引用.
将关键字const加在形参表后,就可以将成员函数声明为常量,不能改变起所操作的对象的数据成员.
002 类的基本思想是数据抽象与封装.
数据抽象是一种依赖于接口和实现分离的编程技术.设计者关心实现,使用者关心接口.
封装是一项将底层次的元素组合起来形成新的,高层次实体的技术.函数是封装的一种形式.类也是封装的一个实体.
数据抽象和封装有两个重要优点:
1.避免类内部出现无意的,可能破坏对象状态的用户级别错误.
2.可以根据'需求改变'或'缺陷报告'来完善类实现,而无须改变用户级代码.
并非所有类都必须是抽象的,标准库中的pair类就是一个具体类,而不是抽象类.
003 访问标号.
public部分定义的成员可被使用该类型的所有代码访问;private部分定义的成员可被类内其他成员访问.
在类的左花括号之后,第一个访问标号之前定义成员的访问级别,其值依赖于类是如何定义的.
对于class关键字定义的类,这些成员是private类型的,如果是用struct关键字定义的,则这些成员是pulbic类型.
004 类定义的杂项.
使用类型别名来简化类.
// 类所定义的类型名,遵循任何其他成员的标准访问控制.
class Screen {
public:
// 将index的定义放在类的public部分,是希望用户也可以使用.
typedef std::string::size_type index;
private:
std::string contents;
index cursor;
index height, width;
};
成员函数可以重载,具体要求与普通函数重载一致,有不同的行参表.
可以在类定义体内部指定一个成员为inline,作为其声明的一部分;也可以在类定义体外部的函数上指定inline.
005 不完全类型只能以有限方式使用.不能定义该类型的对象.
不完全类型只能用于定义指向该类型的指针及引用,或用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数.
在创建类的对象之前,必须完整的定义该类,而不只是声明类,同样,在使用引用或指针访问类的成员之前,必须已经定义类.
类的前向声明一般用来编写互相依赖的类.
006 定义一个类时,也就是定义了一个类型.一旦定义了类,就可以定义该类型的对象,定义对象时,将为其分配存储空间.
一般而言,定义类型时不进行存储空间分配.
在类的定义中,分号是必需的,因为在类定义后可以接一个对象定义列表.所以定义必须以分号结束.
007 隐含的this指针.
成员函数不能定义this形参,而是由编译器隐含的定义.成员函数的函数体可以显式的使用this指针.
必须使用this指针的情况:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时,例如返回对调用该函数的对象的引用.
myScreen.move(4,0).set('#');
// 上句代码等价于.
myScreen.move(4,0);
myScreen.set('#');
在单个表达式中调用move和set操作时,必须每个操作都返回一个引用,该引用指向执行操作的对象.
class Screen{
public:
Screen& move(index r, index c);
Screen& set(char);
Screen& set(index, index, char);
};
Screen& Screen::set(char c){
contents[cursor] = c;
return *this;
}
Screen& Screen::move(index r, index c){
index row = r * width;
cursor = row + c;
return *this;
}
在这些函数中,this是一个指向非常量Screen的指针,可以通过对this指针解引用来访问this指向的对象.
在普通的非const成员函数中,this的类型是一个指向类类型的const指针,可以改变this指向的值,但不能改变this所保存的地址.
在const成员函数中,this的类型是一个指向const类类型对象的const指针.
由此,产生了一个问题,对于一些const成员函数将不能出现在下面的表达式中.
myScreen.display(cout).set('#');
这里display是一个const成员,则它的返回类型必须是const Screen&,将无法支持set操作.
为解决这个问题,必须重载display,一个是const,一个不是const类型.在添加一个const的do_display()操作.
class Screen{
public:
Screen& display(std::ostream &os){
do_display(os);
return *this;
}
Screen& display(std::ostream &os) const {
do_display(os);
return *this;
}
private:
void do_display(std::ostream &os) const{
os << contents;
}
};
Screen myScreen(5, 3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); // 调用非const版本.
blank.display(cout); // 调用const版本.
额外话题:增加一个do_display,看似增加了一个函数,其实它是一个内联函数,开销仅限于coding.
如果要在const成员函数中修改某个成员,必须将其声明为mutable.
private:
mutable int curSize;
008 类的作用域.
即使两个类具有完全相同的成员列表,它们也是不同的类型.每个类的成员不同于任何其他类型的成员.
尽管成员是在类的定义体之外定义的,但成员定义就好像他们是在类的作用域中一样.
在定义于类外部的成员函数中,行参表和成员函数体都出现在成员名之后.这些都是在类作用域中定义,可以直接引用类的其他成员.
返回类型不一定在类作用域中.如果返回类型使用由类定义的类型,必须使用完全限定名.
class Screen{
public:
typedef std::string::size_type index;
index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const {
return cursonr;
}
必须在类中先定义类型名字,才能将他们用作数据成员的类型,或者成员函数的返回类型或形参类型.
一旦一个名字被用作类型,该名字就不能重复定义.
009 类成员定义中的名字查找.
首先检查成员函数局部作用域中的声明.
如果在成员函数中找不到该名字的声明,则检查类中所有成员的声明.
如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明.
int height;
class Screen{
public:
void dummy_fcn(index height){
cursor = width * height;
}
private:
index cursor;
index height, width;
}
height形参屏蔽了height成员,如果想引用成员height,可以 cursor = width * this->height.
想使用类外边的那个height,则要 cursor = width * ::height;
当成员函数定义在类的外部时,名字查找的第三步不仅要考虑在Screen类定义之前的全局作用域中的声明,而且要考虑在成员函数定义之前出现的全局作用域声明.
Screen::index verify(Screen::index);
void Screen::setHeight(index var){
height = verify(var);
}
010 构造函数:保证每个对象的数据成员具有合适的初始值.
构造函数没有返回值类型;构造函数可以被重载;实参决定使用哪个构造函数;构造函数不能声明为const类型.
构造函数可以包含一个构造函数初始化列表.列表以一个冒号开始,接着是逗号分割的数据成员列表,数据成员用园括号初始化.
Sales_item::Sales_item(const string &book):isbn(book), unit_sold(0), revenue(0.0){ }
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化.初始化发生在构造函数体开始之前.
没有定义初始化列表的构造函数在构造函数的函数体中对数据成员赋值.
如果没有为类成员提供初始化式,则编译器会隐式地使用数据成员类型的默认构造函数.
如果那个成员类型没有默认构造函数,则编译失败,此时必须使用初始化列表,显式的初始化那个数据成员.
必须对任何const或引用类型成员以及没有默认构造函数的类类型的任何成员使用"初始化列表".
// 下列代码有两个问题,没有初始化ri绑定的对象,没有对const类型元素初始化具体值.
class ConstRef{
public:
ConstRef(int ii);
private:
int i;
const int c1;
int &ri;
}
ConstRef::ConstRef(int ii){
i = ii;
ci = ii; // error,ci是const,不能赋值的
ri = i // error,ri还没有帮定到具体的对象上
}
// 正确的构造函数应该这样操作.
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
注意成员初始化的次序:不是按照初始化列表的次序,而是按照定义成员的次序.
class X {
int i;
int j;
public:
X(int val): j(val), i(j) {} // i是一个为被初始化的值
}
一般来讲,应该按照成员声明的次序去初始化,此外,因该避免使用成员来初始化其他成员.
初始化,可以使用任意的表达式.
对于类类型数据成员,要指定实参并传递给成员类型的一个构造函数.
一般的,我们更喜欢使用默认实参,来减少重复代码.
011 默认构造函数.
只要定义一个对象时没有提供初始化,就使用默认构造函数.
为所有行参提供默认实参的构造函数也定义了默认构造函数.
如果类没有定义构造函数,编译器会自动生成一个默认构造函数;如果定义了至少一个构造函数,编译器就不会再生成默认构造函数.
包含内置数据类型和复合类型成员的类应该定义构造函数,而不是依靠默认构造函数.
通常情况下,任何类都应该自己定义一个默认构造函数(没有形参表,或者所有形参都有默认值).
如果一个类定义了构造函数确没有默认构造函数,那么:
1.该类的对象必须显式初始化.
2.编译器也不能给该类构造默认的构造函数.
3.该类对象不能作为动态分配数组的元素.
4.该类的静态分配数组必须为每个元素提供显式的初始化.
5.对于一些容器,如vector,将不能接受只有容器大小而没有初始值的构造函数.
012 使用默认构造函数时注意,不要变成函数声明.
Sales_item myObj(); // 这个语句就生命了一个返回值类型为Sales_item,没有形参的函数,而不是定义.
Sales_item myObj; // ok.
013 小心通过隐式类型转换构造的对象.
class Sales_item {
public:
Sales_item(const std::string &book = ""):isbn(book),units_sold(0),revenue(0.0){}
Sales_item(std::istream &is);
};
string null_book "9-999-99999-9";
item.same_isbn(null_book); // 1.表示不存在的ISBN
item.same_isbn(cin) // 2
1.编译器使用接受一个string的Sales_isbn够找函数,从null_book生成一个新的Sales_item对象,我们可以用它来检测item是不是一个空对象.
2.通过cin产生一个临时对象,same_isbn结束,这临时对象就消失了,这肯定不是我们想要的.
为了避免以上问题,抑制有构造函数定义的隐式转换,可以将构造函数声明为explicit.
class Sales_item {
public:
explicit Sales_item(const std::string &book = ""):
isbn(book),units_sold(0),revenue(0.0){}
explicit Sales_item(std::istream &is);
};
item.same_isbn(Sales_item(null_book));
explicit关键字只用于类内部的构造函数声明上,在类的定义体外部所做的定义上不再重复.
通常,除非有明显的理由想要定义隐式的转换,否则,单形参的构造函数都应该为explicit.
014 友元.在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问,如,被重载的操作符,这些操作符经常需要访问类的私有元素,但又不能作为类的成员.
友元机制允许一个类将其非公有成员的访问权授予指定的函数或类.
比如,下面这个例子,Window_Mgr可以像访问自己元素一样访问Screen类中的数据成员.
class Screen{
friend class Window_Mgr;
// ..
}
Window& Window_Mgr::relocate(Screen::index r, Screen::index c, Screen& s){
s.height += r;
s.width += c;
return *this;
}
下面这个例子,使其他函数可以成为一个类的友元.
class Screen {
friend Window_Mgr& Window_Mgr::relocate(Screen::index , Screen::index , Screen& );
// ..
}
为了支持如上这些友元的定义,Window_Mgr必须先定义,否则Screen类就不能将一个Window_Mgr函数指定为友元.
一般来讲,必须先定义包含成员函数的类,才能将成员函数设为友元.另方面,不必预先声明类和非成员函数来将它们设为友元.
友元声明将已命名的类或非成员函数引入到外围作用域中.此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域.
class X{
friend class Y; // 相当于同时声明了类Y,不需要提前声明.
friend void f() { // 与类成员函数不同,f不需要提前声明;与类成员函数相同,可以在此类中定义f函数.
//.. // f的作用域,与类X是相同的.
}
};
class Z{
Y *ymem; // 已经在X中声明过Y了.
void g() {
return ::f();
}
类必须将重载函数中每一个希望设为友元的函数都声明为友元,注意:只有friend声明了的才是友元.
extern std::ostream& storeOn(std::ostream&, Screen&);
extern BitMap& storeOn(BitMap&, Screen&);
class Screen{
// 类外的两个storeOn函数中,只有参数为(std::ostream&, Screen&)的函数stroeOn是Screen类的友元.
friend std::ostream& storeOn(std::ostream&, Screen&);
// ..
}
015 Static类成员.
全局对象的利弊.
1.可以在程序的任意点统计已创建类对象的数量.
2.可以作为指向类错误处理例程的一个指针.
3.可以是指向类类型对象的内存自由存储区的指针.但是全局对象破坏封装,一般用户代码就可能修改这个值.
定义static类成员,独立于该类的任意对象,每个static数据成员是与类关联的对象.
定义static类的成员函数,这样的成员函数没有this形参,它可以直接访问所属类的static成员,而不能直接使用非static成员.
使用static成员的优点:
1.static成员的名字是在类的作用域中,可以避免与其他类的成员或全局对象名字冲突.
2.可是实施封装,static成员可以设置为私有成员,而全局对象不行.
3.static类成员与特定类关联,可以清晰的显式程序员的意图.
staitc成员遵循正常的公有/私有访问规则.
class Account{
public:
void applyint() {
amount += amount * interestRate;
}
static double rate(){
return interestRate;
}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
可以通过作用域操作符直接调用static成员,或者通过对象/引用/或指向该类类型对象的指针间接调用
Account ac1;
Account *ac2 = &ac1;
double rate;
rate = Account::rate(); // ok.
rate = ac1.rate(); // ok.
rate = ac2->rate(); // ok.
因为static成员不是任何对象的组成部分,所以static成员函数不能被声明为const,因为const声明的是不会修改该函数所属的对象.
static成员函数也不能被声明为虚函数.
static数据成员必须在类的定义体外部定义/同时被初始化.
static数据成员不是通过构造函数进行初始化的,而且static数据成员的定义应该是放在包含该类非内联成员函数定义的源程序文件中,切记:不要放在头文件里.
double Account::interestRate = initRate();
特殊的:const static数据成员是在类的定义体中初始化的.但是这个数据成员依旧需要在类的定义体外定义.