热度 13
2016-3-24 20:40
836 次阅读|
1 个评论
为什么《C++ Primer》是一本经典的C++书籍? 因为直到第7章才讲到了类的概念。 之前的那么多准备工作,并不是浪费时间,而是透彻的讲述了C++的特点,它们是构建C++的基石。 所以不要跳过它们,而是要踏踏实实的学习前6章。 现在,才到了解类的时候。 7.1 Defining Abstract Data Types ---------------------------------------------------------------- 对于类来说,interface和implementation一般是分开的。 使用类的人,只关心interface; 编写类的人,才负责implementation。 一般来说,类的抽象封装了类的数据,对外可见的只有interface。 虽然设计类和使用类的很可能是同一个人,但还是要把它们的角色定位分开。 设计类的接口的时候,应该使得类容易使用;而使用类的时候,应该了解接口的特点。 举个例子: struct Sales_data { string isbn() const {return bookNo;} Sales_data combine(const Sales_data); double avg_price() const; string bookNo; unsigned units_sold; double revenue; }; Sales_data add(const Sales_data, const Sales_data); ostream print(ostream, const Sales_data); istream read(istream, Sales_data); double Sales_data::avg_price() const { if (units_sold) return revenue / units_sold; else return 0; } Sales_data Sales_data::combine(const Sales_data rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; } ---------------- 函数里可以定义在类的内部,比如isbn()函数。 isbn()函数返回的是调用它的对象里的bookNo,因为参数默认会使用this指针,也就是对象本身。 this是一个const类型的指针,它不可被修改。 为什么isbn()函数后面要添加const关键字? 因为this的类型是Sales_data *const,它意味着我们不能使用this来绑定一个const的对象。 鉴于isbn()不需要修改Sales_data的成员,使用const告诉编译器,this指向了不能被修改的const对象。 isbn()也称之为const成员函数。 另外,如果对象(或其引用/指针)是const的,那么它只能通过const成员函数访问。 编译类分两步,先是声明,然后是定义,因此类里面的变量可以在类的任何位置被使用。 类的成员函数可以定义在类的外面,比如avg_price()。它必须要加上类的名字,并且保留const。 类的返回值可以是*this,比如combine()。如果要定义具有操作符功能的函数,那么它应该表现得像操作符。 因此combine()函数返回了类对象的引用,作为lvalue。 再举个例子: struct Contacts { string name; string address; string tel; string get_name() const {return name;} string get_address() const {return address;} string get_tel() const {return tel;} }; ---------------- 非类成员的相关函数 add/print/read函数虽然不是类的成员函数,但是它们却是接口。 它们和其他普通函数一样,通常在源文件中定义,在头文件中声明。且它们一般和类放置在一起。 IO类不能被复制,所以这里使用了引用。 IO类会随着读写而改变,因此这里也不能使用const来修饰它。 ---------------- Constructors 构造函数和类有着相同的名字,并且它没有返回值。 类可以有多个构造函数,因为函数可以重载,它们只能通过参数来区分。 构造函数不能声明为const。当成员变量中有const变量时,也可以在构造函数里被修改。 如果类不提供构造函数,那么它的成员变量会被默认的构造出来。 默认的构造函数称之为default constructor,它没有参数; 编译器提供的默认的构造函数称之为synthesized default constructor。 如果成员变量有默认值,那么默认的构造函数就会使用它们。 如果我们定义了任何构造函数,那么编译器都不会再生成默认的构造函数了,除非我们自己再定义它。 为什么需要自己的构造函数呢? • 编译器会生成默认的构造函数,仅仅是在没有定义任何构造函数的情况下。 • 默认的构造函数可能并不能正确的初始化成员变量,比如指针和引用。 • 编译器无法生成默认的构造函数,比如成员变量中有不能被默认构造的类时。 在Sales_data例中,怎么编写自己的构造函数?(根据实际情况的四种途径) • istream,读取输入 • const string表示isbn,unsigned表示卖出本数,double表示价格 • const string表示isbn,其它采用默认 • 空参数列表(默认构造函数) 仍然是Sales_data的例子: struct Sales_data { Sales_data() = default; Sales_data(const string s): bookNo(s) {} Sales_data(const string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {} Sales_data(istream); string isbn() const {return bookNo;} Sales_data combine(const Sales_data); double avg_price() const; string bookNo; unsigned units_sold; double revenue; }; Sales_data() = default; 我们提供这个构造函数,仅仅是因为我们想使用其他的构造函数。 这个构造函数做了synthesized version的事情,因此我们不需要编写它。 = default关键字告诉编译器替我们做这件事情。 = default关键字可以出现在类内部,也可以放置在外部。 = default关键字在内部,说明函数inline;在外部,默认不是inline。 Sales_data(const string s): bookNo(s) {} Sales_data(const string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {} 这两个构造函数,都是通过Initializer List来实现初始化。 这里没有指定值的变量,会根据synthesized default的步骤进行初始化。 构造函数不应该覆盖in-class初始化,除非使用不同的值;不能使用in-class时,再显式的提供。 Sales_data(istream); 这个函数的定义在类的外部,它会从istream读取数据来初始化Sales_data。 值得注意的是,在函数体执行之前变量会先被in-class初始化,然后再由istream初始化。 再举个例子: struct Contacts { Contacts(const string name, const string address, const string tel): name(name), address(address), tel(tel) {} string name; string address; string tel; string get_name() const {return name;} string get_address() const {return address;} string get_tel() const {return tel;} }; ---------------- 类的复制、赋值和析构 如果我们不定义这些操作,编译器会替我们做这些工作。 但是编译器替我们做的这些systhesized工作,不一定会正确的运行,尤其申请对象外的资源的时候。 动态内存的管理,一般不能依赖sysnthesized versions。 However,需要动态内存管理的类,一般会使用vector或者string来管理必要的存储, 因为vector和string的复制、赋值和析构操作可以保证是安全的。 除非我们知道如何定义copy操作,否则动态申请的资源应该直接存储在类的成员里。 7.2 Access Control and Encapsulation ---------------------------------------------------------------- 目前定义的类,任何人都可以对它的成员变量进行操作,都可以调用它的任何函数。 要控制用户对它的访问,需要对它进行封装。 public:可以在程序的任何位置访问它。 private:仅仅能够被类的成员函数访问。 比如上面的Sales_data类: struct Sales_data { public: Sales_data() = default; Sales_data(const string s): bookNo(s) {} Sales_data(const string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {} Sales_data(istream); string isbn() const {return bookNo;} Sales_data combine(const Sales_data); private: double avg_price() const; string bookNo; unsigned units_sold; double revenue; }; public和private可以多次出现,它们的作用域是直到下一个specifier为止。 既然已经学到了封装,我们再做一个更精细的更改,将struct更改为class。 这仅仅是格式的变化,因为使用它们中的任何一个都是可以的。 它们的区别仅仅是,class默认的访问控制是private,而struct默认的访问控制是public。 作为通用的编程习惯,如果我们定义一个所有成员皆可访问的class,那就使用struct; 如果其中有private成员,那就使用class。 ---------------- Friends 不属于类成员的interface函数,无法访问类的private变量,即使它需要访问。 我们可以将这些函数(或类)设置为Sales_data的friend,这样它们就能访问私有成员了。 方法是添加带有friend关键字的声明: struct Sales_data { friend istream read(istream is, Sales_data item); friend ostream print(ostream os, const Sales_data item); public: Sales_data() = default; Sales_data(const string s): bookNo(s) {} Sales_data(const string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {} Sales_data(istream); string isbn() const {return bookNo;} Sales_data combine(const Sales_data); private: double avg_price() const; string bookNo; unsigned units_sold; double revenue; }; friend关键字可以出现在类的任何地方,它不受访问控制的影响。 Friends并不是类的成员。 friend关键字告诉类谁可以访问它的private成员,但它并不能替代声明。 7.3 Additional Class Features ---------------------------------------------------------------- 上面所述的Sales_data类非常简单,friend的特点也非常简单。 类还包含很多上述未涉及到的特征。 我们首先定义两个会互相访问的类Screen和Window_mgr: class Screen { public: typedef string::size_type pos; Screen() = default; Screen(pos ht, pos wd, char c): height(ht), width(wd), contents(ht * wd, c) {} char get() const {return contents ;} inline char get(pos ht, pos wd) const; Screen move(pos r, pos c); Screen display() const; private: pos cursor= 0; pos height = 0, width = 0; string contents; mutable size_t access_ctr; }; class Window_mgr { private: vectorScreen screens{Screen(24, 80, ' ')}; }; 这里使用using也是一样的效果。 另外,typedef必须要先声明,才能被使用,所以这里将它放置在了类的起始位置。 定义在类内部的函数,默认为inline函数。 inline关键字也可以用在类外部的成员函数定义上,但是它们得定义在header文件内。 类的成员函数也可以被重载,规则和非成员函数是一样的。 如果我们希望永远有能力更改某一个成员变量,即使在const成员函数里面, 可以将这个成员变量添加mutable关键字。 一个被mutable修饰的变量绝不会是const的,即使类object是const的。 当我们初始化一个class成员变量时,是在为它的构造函数提供参数,比如: vectorScreen screens{Screen(24, 80, ' ')}; ---------------- return *this 这个return语句说明成员函数返回的是调用它的对象,不是副本,而是对象本身。 这个return语句返回的是lvalue,意味着它可以被赋值,因此可以有: myScreen.move(4, 0).set('#'); 需要说明的是,如果返回的是Screen或者Screen的引用,它们的表现是完全不一样的。 返回的是Screen的话,对象就会被复制一份副本,操作的就不是原来这个对象了。 如果成员函数是const的,比如display(),意味着它的返回值也是const的,即*this是const的, 这样就不能连锁使用了。 为了解决这个问题,我们可以定义两个版本的函数,通过重载实现const和nonconst的返回: public: Screen display(ostream os) {do_display(os); return *this;} const Screen display(ostream os) const {do_display(os); return *this;} private: void do_display(ostream os) const {os contents;} 这里推荐将公共的部分放置在私有成员中。 原因是: • 避免在不同的地方编写两份同样的代码。 • 公共部分的函数,有可能在未来变得更加复杂。 • 公共部分的函数,有可能会增加调试代码。 • 公共部分的函数,定义在类的内部,默认是inline的,不会带来额外的资源开销。 ---------------- class types 两个不同名称的类,即使成员完全一样,也是不同的类型,不能相互赋值。 以下两种方式都正确: Sales_data item1; class Sales_data item1; 类和函数一样,可以在定义前先声明。 我们可以为这样的类声明定义指针或引用,也可以将它们作为函数声明(非定义)的参数和返回值。 类必须先定义,才能使用它的成员,因此类不能有它自身做其成员的情况,但可以是引用或指针。 ---------------- Friendship Revisited 除了函数可以作为类的友元外,其他的类或类的成员函数也可以作为类的友元: class Screen { friend class Window_mgr; /* ...... */ }; 或者只声明其中的某个函数是友元: class Screen { friend void Window_mgr::clear(ScreenIndex); /* ...... */ }; 声明类的成员函数为友元时,需要仔细处理声明和定义之间的关系。 比如Window_mgr和Screen在代码中必须以这样的顺序定义或声明: • 定义Window_mgr类,声明clear但不能定义它。 • 定义Screen类,包括对clear友元的声明。 • 最后定义clear函数。 如果B和C都是A的友元,不意味着B和C是友元。 如果函数是可重载的,那么声明友元的时候,比如声明具体的那个。 类、类成员函数作为友元时,友元的声明可以在它们本身的声明之前。 但即使是类成员函数作为友元,它也必须在类外面被声明一次。