C++ primer | 第7章 类

来源:互联网 发布:儿童学英语软件 编辑:程序博客网 时间:2024/06/14 20:27

类的基本思想是数据抽象data abstraction 和 封装 encapsulation 。数据抽象是一种依赖于接口和实现分离的编程使用类时只需要 抽象的思考类型做了什么,而无需了解类型工作的细节。

7.1 定义抽象数据类型

定义和声明成员函数的方式与普通函数相似。成员函数的声明必须在类的内部,它的定义既可以再类的内部(inline)也可以再累的外部、作为接口组成的非成员函数如(add/read/print)定义和声明在类的外部。

//Sales_data 类struct Sales_data{std::string isbn() const {return bookNo;}//成员函数本身也是一个块,这里只有一条return 语句,调用isbn成员时传入了total的地址,通过this额外隐式参数开调用他的对象,this不允许自定义,默认情况下,this是指向类类型的非常量版本的常量指针,可以在成员函数内部使用this,我们可以将isbn定义成如下形式:std::string isbn() const {return this->bookNO;)this 总是指向这个对象,是一个常量指针,不允许改变this中的地址,Sales_data& combine(const Sales_data&);//仅仅声明,未定义,返回引用dobule avg_price() const;std::string bookNo;unsigned units_sold=0;double revenue=0.0;}//Sales_data 的非成员接口函数声明和定义在类外Sales_data add(const Sales_data&,const Sales_data&);std::ostream &print(std::ostream&,const Sales_data&);std::istream &read(std::istream&,Sales_data&);

常量指针const pointer vs 指向常量的指针pointer to const
见第二章!

 char  * const cp; ( * 读成 pointer to )  cp is a const pointer to char ,亦即常量指针,cp值(地址)不可改变,但*cp,也就是cp所指对象的值能够改变。 常量指针必须被赋予初始值,因为指针值是常量,所以不能再用指向其他变量,但是可以更改指向内存的值,*pInt = 20; *pInt = 50;等。const char * p;  p is a pointer to const char,pointer to const ***如果想存放常量对象的地址,必须只能使用指向常量的指针***。一般情况下指针的类型必须与其所指向的类型一致,但是有两种例外,第一就是可以使用指向常量的指针指向一个非常量,所谓的指向常量仅仅要求不能通过该指针改变指向对象的值!!!但是常量不可以用不普通指针如const double pi=3.14;double *ptr =π//错误!!       一般来说 如果你认定一个变量是常量表达式,就把他声明为constexpr

明白了上面,我们就可以判断:this是一个常量指针,这里使用const来修改this的类型,this是隐式的但是依然要遵循初始化规则,不能把常量绑定到一个非常量的表达式上

//下面是错误的代码,仅用于说明this指针是如何使用的std::string Sales_data::isbn(const Sales_data *const this){return this ->isbn;}//因为this不能自定义,所以此代码是错误的

另外,我们可以发现,bookNo是定义在isbn之后的但是这里isbn依然可以调用bookNo是因为,编译器会分两步来处理类:先编译成员的声明,然后在编译函数体,所以不用在意类中的成员的出现次序。

定义一个返回this对象的函数
这里定义的是combine,他设计的初衷是实现类似于符合复制运算符+=的效果,只传入一个对象,为了与+=保持一致,需要返回引用类型(**调用一个返回应用的函数得到左值,其他得到右值),Sales_data&

7.1.3 定义类相关的非成员函数

一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类咋同一个头文件内。
定义read和print 函数

//输入交易信息和输出总数及价格istream &read(istream &is, sales_data &item)//IO 类型不能被拷贝只能通过应用来传递他们,读取和写入会改变流的内容所以是普通引用{    double price =0;    is >>itme.bookNo>>item.units_sold>>price;    item.revenue=price*item.units_sold;    return is;}ostream &print (ostream &os, const Sales_data &item){    os <<item.isbn() <<" "<< item.units_sold<<" "    << item.revenue<< "" <<item.avg_price();//不负责换行,应该尽量减少对格式的控制    return os; }

7.1.4 构造函数

每个类都分别定义了它的对象被初始化的方式,通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数就是构造函数 constructor。它们不能被声明成const的,而且在const对象的构造过程中可以向其写值。
对于一个普通的类来说,必须要定义它自己的默认的构造函数
定义Sales_data的构造函数:

//包含构造函数的Sales——datastruct Sales_data{// new constructorSales_data()=default;//可以在类内也可以再类外、、没有返回值类型Sales_data(const std::string &s):bookNo(s){}//不需要加分号,加了分号相当于有了一条空语句Sales_data(const std::string &s,unsigned n,double p):            bookNo(s),units_sold(n),revenue(p*n){}            //:和{}之间的部分是构造函数初始化列表,不同成员的初始化列表用逗号分割,每个名字后面紧跟(),()内是成员的初始值,初始值也可以放到花括号内。Sales_data(std::istream &);// 之前的已有成员……}

在类的外部定义构造函数

Sales_data::Sales\_data(std::istream &is)//没有术赤化列表或者说初始化列表是空的,但是由于执行了函数体,所以对象的成员仍然被初始化{    read(is,*this);    }

第一部分Sales_data::Sales_data没有返回值类型,并且他的名字与类名相同,所以他是一个构造函数。

7.2 访问控制和封装

到目前为止,我们已经定义了类的接口,但是没有任何机制强制使用这些接口,而且用户可以直接控制对象的具体实现细节,这里我们可以使用访问说明符 access specifiers加强类的封装性:
- public之后的成员可以在整个程序内被访问,定义类的接口
- private 可以被类的成员函数访问,也就是封装了类的实现细节
这里写图片描述

上面程序中除了多了访问说明符之外还将struct关键字改为了class,他们基本一样,但是权限不太一样,在class中出现第一个访问说明符之前默认是private,struct默认是public,好了,现在我们的之前的read、print、add尽管是类的接口,但是不是类的成员,所以不能正常编译了,为了可以正常编译,我们需要在类中(一般在类定义开始或者结束的时候)添加友元声明,以friend为关键字。类内的声明不是一个通常意义的函数声明,只是制订了访问权限,所以通常还是要将friend 的声明与类本身放在同一个头文件中,提供独立的声明。

7.3 类的其他特性

7.3.1 类成员再探

  1. 类型成员 typedef std::string :: size_type pos;
    using pos = std::string::size_type;
    类型成员必须先定义后使用,一般通常出现在类开始的地方
  2. 令成员函数作为内联函数
    可以在类的内部把inline作为声明的一部分显示地声明成员函数
    也可以在类的外部用inline关键字修饰函数的定义(推荐)
  3. 重载成员函数
  4. 可变数据成员
    可变数据成员mutable data member 可以在变量声明中加入mutable 关键字
    即使在一个const对象内也可以被修改
  5. 类数据成员的初始值
    我们提供一个类内初始值时,必须以符号=或者花括号表示

7.3.2 返回*this的成员函数

一个const成员函数如果以引用的形式返回*this那么返回的将是一个常引用量,通过区分成员函数是否是 const的,可以进行const的重载,原因与const的形参与重载的关系一样:

//顶层const 不影响传入Record lookup(phone);Record lookup(const phone);//redeclaresRecord lookup(phone*);Record lookup(phone* const);//redeclaresRecord lookup(Account&);Record lookup(const Account&);//new functionRecord lookup(Account*);Record lookup(const Account*);//new function

7.3.3 类类型

我们为每个类都起了个名字,这个名字就是他们的类型,及时成员完全一样,也不是同一类。
类的声明
class Screen;//向前声明 forward declaration
在创建类的对象之前,该类必须被定义过,不能仅仅是声明。类也必须首先被定义然后才能用引用或者指针访问其成员;但是但是但是有一种特殊情况:类可以包含指向它自身类型的引用或者指针

7.3.4 友元再探

  1. 友元不存在传递性
  2. 如果相对重载函数设为友元,必须对所有的函数分别声明
  3. 可以在友元声明中定义友元函数,但是必须在外部提供相应的声明来使得函数可见

7.4 类的作用域

在类的作用域外,只能由对象、引用或者指针使用成员访问运算符。

Screen ::pos ht=24,wd=80;Screen scr(ht,wd,'');Screen *p=&scr;char c=scr.get();//访问 scr对象的get成员c=p->get();//访问p所指向对象的get成员

一旦在作用域外遇到类名,在定义时剩余部分就在作用域之内了。
另外如果成员函数定义在类的外度时,返回类型中使用的名字都位于类的外部,所以定义之前要首先处理返回类型,之后才进入作用域。

//类函数中的声明省略//直接定义外部函数window_mgr::ScreenIndexwindow_mgr::addScreen(const Screen &s){//省略了}

声明中使用的名字,包括返回类型或者参数类标中使用的名字,都必须在使用前确保可见。
如果成员使用了外层作用域中的某个名字,而名字代表一种类型,则类不能在之后重新定义该名字。
不建议使用其他成员的名字作为参数
可以使用作用域访问符明确变量的位置

7.5 构造函数再探

7.5.1 构造函数初始值列表

如果成员时const 引用或者属于某粥未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值
初始化比赋值高效
成员初始化顺序与他们在类定义中出现的顺序一致,但是最好令构造函数初始值的顺序与成员成名的顺序保持一致,尽量避免使用某些成员初始化其他成员。

7.5.2 委托构造函数

class Sales_data{public: //非委托Sales_data(std::string s,unsigned cnt,double price):bookNo(s),units_sold(cnt),revenue(cnt*price){}//其余都是委托Sales_data():Sales_data("",0,0){}Sales_data(std::string s):Sales_data(s,0,0){}Sales_data(std::istream &is):Sales_data()        {read(is,*this);}......}

当一个构造函数委托给领一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行,再sales_data类中受委托的构造函数恰好是空的,加入函数体包含有代码,将首先执行这些代码,然后控制权才会交换给委托者的函数体。

7.5.4 隐式的类类型转换

item.combine("99-99");//错误,只能使用一步转换,需要两种转换99-99转为为string string换为Sales_dataitem.combine(string("99-99"));//正确,显示的转换为string,隐式的转换为Sales_dataitem.combine(Sales_data("99-99"));//正确,隐式的转换为string,显式的转换为Sales_data

explicit
explicit可以阻止构造函数的隐式转换,但是只对一个实参的构造函数有效,只能在类内声明构造函数是使用。当我们以explicit关键字声明构造函数时,只能以直接初始化的形式使用,而且不会在自动转换的过程中使用该构造函数。
可以使用static_cast显示的强制转换
item.combine(static_cast<Sales_data>(cin));

7.5.5 聚合类

aggregate class使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
他满足以下条件:
- 所有成员都是public的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,没有virtual 函数

7.5.6 字面值常量类

  1. 数据成员都是字面值类型的聚合类

  2. 2.1 数据成员都是字面值类型
    2.2 至少含有一个constexpr构造函数
    2.3 如果一个数据成员含有类内初始值,则内置的类型成员的初始值必须是一条常量表达式;或者如果成员输入某种类类型,则初始值必须使用成员自己的constexpr 构造函数
    2.4 类必须使用析构函数的默认定义,该成员负责销毁类的对象

constexpr 构造函数
尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数,事实上,一个字面值常量类必须提供至少一个constexper构造函数,constexpr构造函数函数体一般是空的,constexpr函数唯一可以执行的语句就是返回语句。

7.6 类的静态成员

在类外定义时不能重复使用static关键字,只在类内声明即可;
通常类的静态成员不应该在类的内部初始化,然而,我们可以为静态成员提供const整数类型的类内初始值,但是静态成员必须是constexpr,初始值必须是常量表达式。
即使一个常量静态数据成员在类内部被初始化了也应该在类外定义一下。
静态成员能用于某些普通成员不可以的场景
1. 静态成员可以是不完全类型
2. 可以使用静态成员作为默认实参

0 0
原创粉丝点击