引言:
在C++中,用类来定义自己的抽象数据类型。通过定义类型来对应所要解决的问题中的各种概念,可以使我们更容易编写、调试和修改程序。可以使得自己定义的数据类型用起来与内置类型一样容易和直观。
一个前面曾经定义过的类:
class Sales_item { private: std::string isbn; unsigned units_sold; double revenue; public: double ave_price() const; bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } Sales_item():units_sold(0),revenue(0) {} }; double Sales_item::ave_price() const { if (!revenue) { return 0; } return revenue / units_sold; }
一、类定义:扼要重述
简单来说,类就是定义了一个新的类型和一个新的作用域。
1、类成员
访问标号:在public部分定义的成员可被使用该类型的所有代码访问:在private部分定义的成员可被其他类成员访问。
所有成员必须在类的内部声明,一旦类定义完成之后,就没有任何方式可以增加成员了。
2、构造函数
在创建一个类对象时,编译器会自动使用一个构造函数来初始化该对象,也就是说:构造函数用于给每个数据成员设定适当的值。
构造函数一般使用一个构造函数初始化列表,来初始化对象的数据成员。
Sales_item():units_sold(0),revenue(0) {}
构造函数初始化类表由成员名和带括号的初始值组成,跟在构造函数的形参表之后,并以冒号(:)开头。
3、成员函数
在类内部定义的函数默认为内联函数(inline)。
而在类的外部定义的函数必须指明它们是在类的作用域中。
就const关键字加在形参表之后,就可以将成员函数声明为常量:
bool same_isbn(const Sales_item &rhs) const;
const必须同时出现在声明和定义中,若只出现在一处,则会出现编译时错误!
//类内 bool same_isbn(const Sales_item &rhs) const; //类外 bool Sales_item::same_isbn(const Sales_item &rhs) //Error { return isbn == rhs.isbn; }
//P369 习题12.1、2、3、4 class Person { public: Person(const std::string &Name,const std::string &Address):name(Name),address(Address){} std::string getName() const; std::string getAddress() const; private: std::string name; std::string address; }; std::string Person::getName() const { return name; } std::string Person::getAddress() const { return name; }
二、数据抽象和封装
数据抽象是一种依赖于接口和实现分离的编程技术:类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。
封装是一项将低层次的元素组合起来形成新的、高层次实体的技术!函数和类都是封装的具体形式。其中类代表若干成员的聚集。大多数(设计良好的)类类型隐藏了实现该类型的成员。
1、访问标号实施抽象和封装
1)程序的所有部分都可以访问带有public标号的成员。类型的数据抽象视图由其 public成员定义。
2)使用类的代码不可以访问带有private标号的成员。private封装了类型的实现细节。
【class与struct的区别:】
如果类是用struct关键字定义的,则在第一个访问标号之前的成员是公有的;如果是用class定义,则在第一个访问标号之前的成员是私有的!
【建议:具体类型和抽象类型】
并非所有类型都必须是抽象的。标准库中的pair类就是一个实用的、设计良好的具体类而不是抽象类,具体类会暴露而非隐藏其实现细节。
但是具体类通常还有成员函数。特别地,如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。类的使用都也可以初始化或赋值数据成员,但由类来做更不易出错。
2、编程角色的不同类别
好的类的设计者会定义直观和易用的类接口,而用户(此处可以指程序员)则只需关心类中影响他们使用的那部分实现。
【注意,C++程序员经常会将应用程序的用户和类的使用者都称为“用户”。】
在简单的应用程序中,类的使用者和设计者也许是同一个人。即使在这种情况下,保持角色的区分也是有益的。设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类是如何工作的!
【关键概念:数据抽象和封装的好处】
1)避免类内部出现无意的、可能破坏对象状态的用户级错误。
2)随着时间的推移,可以根据需求改变或缺陷报告来完善类实现,而无需改变用户级代码。
【P371值得仔细品读!】
【注解:】
改变头文件中的类定义可有效地改变包含该头文件的每个源文件的程序文本,所以,当类发生改变时,使用该类的代码必须重新编译。
三、关于类定义的更多内容
C++语言为类提供了相当多的支持【这从C++的原名:CWith Class,就可看出...】
1、同一类型的多个数据成员
类的数据成员的声明类似于普通变量的声明:
class Screen { public: //... private: std::string contents; std::string::size_type cursor; std::string::size_type height,width; };
2、使用类型别名来简化类
出了定义数据和函数成员之外,类还可以定义自己的局部类型名字。如果为std::string::size_type提供一个类型别名,那么Screen类将是一个更好的抽象:
class Screen { public: //... typedef std::string::size_type index; private: std::string contents; index cursor; index height,width; };
将index定义放在public部分,是因为希望用户使用这个名字!
3、成员函数可以被重载
成员函数只能重载本类的其他成员函数。
重载的成员函数和普通函数应用相同的规则:两个重载成员的形参数量和类型不能完全相同。调用非成员重载函数所用到的函数匹配过程也应用于重载成员函数的调用。
4、定义重载函数
class Screen { public: typedef std::string::size_type index; //一下两个函数构成重载 char get() const { return contents[cursor]; } char get(index ht,index wd) const; private: std::string contents; index cursor; index height,width; };
使用:
Screen myScreen; char ch = myScreen.get(); ch = myScreen.get(0,0);
5、显式指定inline函数
在类内部定义的成员函数,默认就是inline函数。但是也可以显式的将成员函数指定为inline:
class Screen { public: typedef std::string::size_type index; //默认就是inline函数 char get() const { return contents[cursor]; } inline char get(index ht,index wd) const; index get_cursor() const; private: std::string contents; index cursor; index height,width; }; //已经在类体中声明为inline了,就没必要再次声明 char Screen::get(index r,index c) const { index row = r * width; return contents[row + c]; } //即使没有在类体中声明,也可以在外面补上... inline Screen::index Screen::get_cursor() const { return cursor; }
在声明和定义处指定inline都是合法的。在类的外部定义inline的一个好处是可以使得类比较容易阅读。
【最佳实践】
像其他inline一样,inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中。