原创 【博客大赛】《C++ Primer》学习笔记(23)类

2016-3-24 20:40 836 12 13 分类: MCU/ 嵌入式 文集: Qt和Cpp
为什么《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是友元。
如果函数是可重载的,那么声明友元的时候,比如声明具体的那个。
类、类成员函数作为友元时,友元的声明可以在它们本身的声明之前。
但即使是类成员函数作为友元,它也必须在类外面被声明一次。

文章评论1条评论)

登录后参与讨论

用户593939 2016-3-24 22:30

参考和学习了
相关推荐阅读
DiracFatCat 2018-09-05 12:14
【博客大赛】卡尔曼滤波学习笔记(11)从位置估计速度
卡尔曼滤波器,不仅仅是一个低通滤波器,否则也不会持续发展50年。 示例:桑先生需要测试高速列车的性能。测试的目的是判断列车在直线上能否保持80m/s的速度。速度和位置每0.1秒测量一次,但是由于...
DiracFatCat 2018-08-31 19:32
【博客大赛】卡尔曼滤波学习笔记(10)一个简单的示例
《Kalman Filtering: Theory and Practice Using MATLAB》第三章,看不懂,暂时略过。《Kalman Filtering: Theory and Pract...
DiracFatCat 2018-07-19 15:09
对sed命令的练习
sed是流编辑器。它每次处理一个输入,因此很有效率。官方手册:https://www.gnu.org/software/sed/manual/sed.html学习Linux命令,当然要阅读官方手册,所...
DiracFatCat 2018-06-19 15:10
【博客大赛】卡尔曼滤波学习笔记(八)可观测性和可控制性 ...
可观测性是指,在给定模型的情况下,动力学系统的状态是否由它的输入输出唯一确定。可观测性是系统模型的特征。如果传感器矩阵H是可逆的,则本系统可观测,因为有:如果传感器矩阵H某些时候是不可逆的,则本系统仍...
DiracFatCat 2018-06-19 10:56
【博客大赛】卡尔曼滤波学习笔记(七)Z变换
如果我们仅仅对离散线性系统感兴趣,那么就使用下面这个表达式:如果u是常量,那么可以写成:为了简化表达式,我们可以将上面写成:离散线性时不变系统的Φ求解,可以使用Z变换。(* 由于本人已经忘记了Z变换的...
DiracFatCat 2018-06-19 10:54
【博客大赛】卡尔曼滤波学习笔记(六)拉普拉斯变换
对于线性时变/时不变系统,求解Φ(t)有多种方式,拉普拉斯变换是其中一种。(* 由于本人已经忘记了拉普拉斯变换的内容,因此本节待续。)...
我要评论
1
12
关闭 站长推荐上一条 /2 下一条