原创
【博客大赛】《C++ Primer》学习笔记(23)类
为什么《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[cursor];}
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:
vector<Screen> screens{Screen(24, 80, ' ')};
};
这里使用using也是一样的效果。
另外,typedef必须要先声明,才能被使用,所以这里将它放置在了类的起始位置。
定义在类内部的函数,默认为inline函数。
inline关键字也可以用在类外部的成员函数定义上,但是它们得定义在header文件内。
类的成员函数也可以被重载,规则和非成员函数是一样的。
如果我们希望永远有能力更改某一个成员变量,即使在const成员函数里面,
可以将这个成员变量添加mutable关键字。
一个被mutable修饰的变量绝不会是const的,即使类object是const的。
当我们初始化一个class成员变量时,是在为它的构造函数提供参数,比如:
vector<Screen> 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是友元。
如果函数是可重载的,那么声明友元的时候,比如声明具体的那个。
类、类成员函数作为友元时,友元的声明可以在它们本身的声明之前。
但即使是类成员函数作为友元,它也必须在类外面被声明一次。
用户593939 2016-3-24 22:30