第7章 类

       类的基本思想是数据抽象和封装。数据抽象是一种依赖接口和实现分离的编程技术。类的接口包括用户所能执行的所有操作,类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需要的各种私有私有函数。封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节。类要想实现数据抽象和封装,首先定义一个抽象数据类型。

7.1定义抽象数据类型

7.1.2定义改进的Sales_data类

       定义和声明成员函数的方式与普通函数差不多。成员函数的定义和声明成员函数的方式与普通函数差不多。成员函数的声明必须在类的内部,定义既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,定义和声明都在类的外部。定义在类内部的函数是隐式的inline函数。
        定义成员函数
       string isbn() const {return bookNo;}
       this形参是隐式定义的。任何自定义名为this的参数或者变量的行为都是非法的。
       const 作用是修改隐式this指针的类型。默认情况下,this的类型是类类型非常量版本的常量指针,例如在Sales_data中,this的类型是Sales_data * const.我们不能把this绑定到一个常量对象上,这一情况也使得我们不能在一个常量对象上调用普通的成员函数。紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称为 常量成员函数
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
        类作用域和成员函数
       首先编译成员的声明,然后才轮到成员函数体,因此成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
        在类的外部定义成员函数
       在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。返回类型、参数列表和函数名都得和类内部的声明保持一致。同时类外部定义的成员的名字必须包含它所属的类名。
       double Sales_data::avg_price() const{
}
        定义一个返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
    ......
    return *this;
}
       一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。

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

       我们定义非成员函数的方式与定义其他函数一样,通常把函数的声明和定义分离开来,如果函数在概念上属于类但不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内。一般来说,如果非成员函数是类接口的组成部门,则这些函数的声明应该与类在同一个头文件内。

7.1.4构造函数

       类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数被叫做构造函数。构造函数的任务是初始化类对象的成员函数,无论何时只要类的对象被创建,就会执行构造函数。
       构造函数的名字和类名字相同。和其他函数不一样的是,构造函数没有返回类型。类可以包含多个构造函数,不同构造函数必须在参数数量和参数类型上有所区别。
       构造函数不能被声明为const的,因为当我们创建一个const对象时,知道构造函数完成初始化过程,对象才能真正的取得“常量”属性。
       类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫默认构造函数。默认构造函数无任何实参。如果我们的类没有显式的定义构造函数,那么编译器就会为我们隐式的定义一个默认构造函数。
       某些类补充能依赖合成的默认构造函数。一是一旦定义了其他构造函数,除非再定义一个默认构造函数,否则没有默认构造函数。二是合成的默认构造函数可能执行错误的操作。三是有时候默认编译器为某些类合成默认的构造函数。
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){}
string bookNo;
unsigned units_sold  =0;
double revenue = 0.0;
}
       Sales_data() = default; 定义一个默认构造函数。C++11新标准中,规定=default为默认构造函数,=default既可以和声明一起出现在类内部,也可以作为定义出现在类外部。如果在类的内部,则默认构造函数是内联的,否则不是内联的。
       Sales_data(const string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n)把:之后的部门称为构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初始值。

7.1.5拷贝、赋值和析构

       如果我们不主动定义这些操作,则编译器将替我们合成他们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。

7.2 访问控制与封装

       定义在Public说明符之后的成员在整个程序内可被访问,public成员定义为类的接口。
定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
       一个类可以包含0个或多个访问说明符,每个访问说明指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者达到类的结尾处。
        使用class和struct关键字
       struct和class的默认访问权限不一样。如果struct则第一个访问说明符之前是public,如果class第一个访问说明符之前是private.

7.2.1 友元

       类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元,如果类想把一个函数作为它的友元,只需要增加一条friend关键字开始的函数声明即可。友元声明只能出现在类定义的内部,但在类内部的具体位置不限,友元不是类的成员也不受它所在区域访问控制级别的约数。
友元的声明
       友元的声明仅仅指定了访问权限,而非一个通常意义上的函数声明。如果希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。

7.3类的其他特性

7.3.1类成员再探

       类还可以自定义某种类型在类中的别名。一样可以存在访问权限,可以是public或者private中的一种。
class Screen(){
public:
 typedef std::string::size_type pos;
private:
  pos cursor =0;
  pos height =0,width = 0;
  std::string contents;
}
       一是可以用等价的using pos=std::string::size_type; 二是用来定义类型的成员必须先定义后使用。
       在类内部把inline作为声明的一部分显式的声明成员函数。同样的,在类的外部用inline关键字修饰函数的定义。
       重载成员函数,只要函数之间在参数的数量和、或类型上有所区别就行。
        可变数据成员
       有时我们希望修改类的某个数据成员,即使在一个const的成员函数内。可以通过在变量声明中加入mutalbe关键字做到这一点。
       一个可变数据成员永远不会是const,即使它是const对象的成员,因此一个const成员函数可以改变一个可变成员的值。
class Screen(){
public:
 void some_member() const;
private: 
    mutable size_t access_ctr;
}
void Screen:: some_member() const
{
  ++acess_ctr;
}
       尽管some_member是一个const成员函数,它仍能改变access_ctr的值。该成员是一个可变成员,因此任何成员函数,包括const函数在内都能改变它的值。
class Window_mgr{
private:
std::vector<Screen> screens{Screen(24,80,' '}
}
       Screen的构造函数接受两个尺寸参数和一个字符值,创建了一个给定大小的空白屏幕对象。
C++11新标准中,最好的方式就是把这个默认值声明成一个类内初始值。
       类内初始值,必须以符号=或者花括号表示。

7.3.2返回*this的成员函数

class Screen(){
public:
  Screen &set(char);
}
inline Screen &Screen::set(char c)
{
return *this
}
       如果我们定义的返回类型不是引用,则set的返回值将是*this的副本,调用set只能改变临时副本,不能改变原始值。
       一个const成员函数如果引用的形式返回*this,那么它的返回类型将是常量引用。

7.3.3类类型

       每个类定义了唯一的类型,对于两个类,即使他们的成员一样,两个类也是不同的类型。我们可以把类名作为类型的名字使用,从而直接指向类类型。或者也可以吧类名跟在关键字class和struct后面。
Sales_data item1;
class Sales_data item1;
       同样也可以仅声明类而不定义它。
class Screen
       这种声明被称作前向声明,在声明之后定义之前是一个不完全类型。可以定义指向这种类型的指针或者引用,也可以作为参数或者返回类型的函数。允许一个类的指针或引用是自己,但是不允许一个类的成员是自己。
       必须首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。

7.3.4友元再探

        类之间的友元关联
       如果一个类指定了友元,则友元类成员函数可以访问此类的所有成员。但是友元关系不具有传递性。
       成员函数作为友元, 必须指明函数属于哪个类。
        尽管重载函数名字相同,但他们依然是不同的函数,需要每个分别声明为友元。
        类和非成员函数的声明不是必须在它们的友元声明之前。
       友元是友元,声明是声明。要使用函数必须先声明。

7.4类的作用域

       当成员 函数定义在类的外部时候,返回类型中使用的名字位于类作用域之外,这时,返回类型必须指明它是哪个类的成员。

7.4.1名字查找与类的作用域

       类的定义分两步:编译成员的声明,直到类的全部可见后编译函数体。
       用于类成员声明的名字查找
       这两个阶段处理方式只适用于成员函数中使用的名字,声明中使用的名字、包括返回类型或者参数列表中使用的名字,都必须确保使用前可见。
        类型名要特殊处理
       内层作用域可以重新定义外层作用域中的名字,但是在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能再之后重新定义该名字。
typedef double Money;
class Account{
public:
  Money balance(){return bal;}//使用外层Money
  private: 
    typedef double Money;//错误不能重新定义;
}
        成员定义中的普通块作用域的名字查找
       首先,在成员函数内找,然后再类内找,最后在成员函数定义之前所有作用域中找。
       类作用域之后,在外围的作用域中查找
       如果要绕开,则可以this->,或者类名::变量名

7.5构造函数再探

7.5.1构造函数初始值列表

        构造函数的初始值有时必不可少
       成员是const或者是引用的话,必须将其初始化。当成员是某种类类型且该类没有定义默认构造函数时候,也必须将其初始化。
class ConstRef{
public:
   ConstRef(int ii);
private:
  int i;
  const int ci;
  int &ri;
}
       成员ci和ri必须初始化。我们初始化const或者引用的数据成员唯一的机会是通过构造函数初始值,因此正确的形式应该是
       ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}.
       如果成员是const、引用或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员初始化。
        成员初始化顺序
       构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
       成员的初始化顺序与他们定义中的出现顺序一致,第一个成员先被初始化,然后第二个,构造函数初始值的前后位置关系不会影响实际的初始化顺序。
        默认实参和构造函数
       默认实参构造函数使用string的默认构造函数初始化bookNo.
class Sales_data{
public:
Sales_data(std::string s= ""):bookNo(s){}
}
       如果一个构造函数为所有参数都提供了默认实参,则实际上也定义了默认构造函数

7.5.2委托构造函数

       委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。
class Sales_data{
public:
Sales_data(string s,unsigned cnt,double price):bookNo(s),units_sold(cnt),revenue(cnt*price){}
Sales_data():Sales_data("",0,0){}
Sales_data(istream &is):Sales_data(){ read(is,*this)}
}

7.5.3默认构造函数的作用

       当对象被默认初始化或值初始化时自动执行默认构造函数1.在块作用域内不使用任何初始值定义一个非静态变量或数组时;2当一个类本身含有类类型成员且使用合成的默认构造函数;3当类类型成员没有在构造函数初始值列表中显式的初始化时。在实际中定义了其他构造函数,最好也提供一个默认构造函数。
       Sales_data obj(),和 Sales_data obj是不一样的。前者表示函数,返回对象是Sales_data;后者表示定义了一个变量。

7.5.4隐式的类类型转换

       通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
string null_book = "9-9999-9";
item.combine(null_book);//因为Sales_data定义了string的构造函数,构造一个临时对象Sale_data,
        只允许一步类类型转换
       item.combine("9-9999-9")//错误,因为需要先把“9-9999-9”转为string ,然后转为Sale_data需要两步。
       可以item.combine(string("9-9999-9"));//正确
        抑制构造函数定义的隐式转换
       可以构造函数声明为explicit组织隐式的转换
       关键字explicit只对有一个实参的构造函数有效,需要多个实参的构造函数不能用于隐式转换。同时只能在类内构造函数时使用explicit,在类外部定义不应重复。
        explicit构造函数只能用于直接初始化
       执行拷贝初始化时候,只能使用直接初始化不能使用explicit构造函数。
       Sales_data item2=null_book;//错误
        为转换显式地使用构造函数
       可以item.combine(Sales_data(null_book));//正确
       item.combine(static_cast<Sales_data>(cin));

       7.5.5字面值常量

       constexpr函数的参数和返回值必须是字面值类型,除了算数、引用和指针外,某些类也是字面值类型。
        constexpr构造函数
       一个字面值常量类必须至少提供一个constexpr构造函数。constexpr函数必须初始化所有数据成员,初始值或者使用constexpr构造函数或者是一条常量表达式。

7.6类的静态成员

        声明静态成员
       可以在成员前面加上static使得其与类关联在一起。静态成员可以是public或private的。静态数据成员可以是常量、引用、指针或者类类型。
       静态成员函数不能声明为const的,也不能在static函数体内使用this指针。
        使用类的静态成员
       使用域运算符直接访问静态成员
        定义静态成员
       可以在类的内部也可以在类的外部定义静态成员,当在类的外部定义静态成员时候,不能重复static关键字,该关键字只出现在类内部声明语句中。
       一般来说不能在类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次。
       静态成员类内初始化,静态成员必须为constexpr类型,可以在类内提供初始值。
       静态数据成员的类型可以是它所属的类类型,非静态数据成员则受到限制,可以使用静态成员作为默认实参,非静态数据成员不能作为默认实参。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值