第 7 章 类

来源:互联网 发布:log4j flume源码 编辑:程序博客网 时间:2024/05/19 18:13

  类的实现包括类的数据成员负责接口实现的函数体以及定义类所需的各种私有函数
  

7.1 定义抽象数据类型

定义成员函数
  成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部。
  作为接口组成部分的非成员函数,定义和声明都可以在类的外部。
  注意:定义在类内部的函数是隐式inline函数。
引入this
  在成员函数内部可直接使用该函数的对象成员,而无须通过成员访问运算符。
  this 是一个常量指针。
引入const成员函数
  const放在成员函数的参数列表之后,表示this是一个指向常量的指针,这样的成员函数称为常量成员函数
  const 的作用就是修改隐式this指针的类型。默认情况下,this的类型是指向类类型非常量版本的常量指针。意味着(默认情况下)不能把this绑定到一个常量对象上。也就使得不能在一个常量对象上调用普通的成员函数。所以设置this为指向常量的指针有助于提高函数的灵活性。
  注意:常量对象以及常量对象的引用或指针都只能调用常量成员函数。
类作用域和成员函数
  成员函数可以随意使用类中的其他成员而无序在意这些成员出现的次序。
在类的外部定义成员函数
  必须包含它所属的类名 :: 作用域
定义一个返回this对象的函数
  return *this; //返回调用该函数的对象
  

7.1.3 定义相关的非成员函数

  一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件中。
  

7.1.4 构造函数

  类通过一个或几个特殊的成员函数来控制其对象的初始过程。
  无论何时,只要类的对象被创建,就会使执行构造函数。
构造函数
  名字和类名相同,无返回类型;
  不同的构造函数必须在参数数量或参数类型上有所区别;
  不能声明const的。
合成的默认构造函数
  默认构造函数:若类没有显式地定义构造函数,那么编译器会隐式地定义一个默认构造函数。
  对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员规则
  -如果存在类内的初始值,用它来初始成员;
  -否则,默认初始化该成员。
某些类不能依赖于合成的默认构造函数
合成的默认构造函数只适合非常简单的类,对一个普通的函数,必须定义它自己的默认构造函数。
定义构造函数
  构造函数不接受任何实参,则是默认构造函数。
  =default;//要求编译器生成默认构造函数。
构造函数初始值列表
  sales_data(congst string &s): bookNo(s){}
  当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐士初始化。
  构造函数使用类内初始值是好的选择,若编译器不支持类内初始值,则所有构造函数都应该显式地初始化每个内置类型成员。
在类的外部定义构造函数
   必须使用 “::“”

7.1.5 拷贝、赋值和析构

  若不定义这些操作,则编译器将替我们合成。
  某些类不能依赖于合成的版本。特别是,当类需要分配类对象之外的资源时。
  

7.2 访问控制与封装

  在c++语言中,使用访问说明符加强类的封装性:
  

  • 定义在public之后的成员在整个程序内可被访问,public成员定义类的接口
  • 定义在private之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

使用class或struct关键字
  唯一区别时就是默认的访问权限。
  

7.2.1 友元

  类允许其他类或者函数访问它的非公有成员。只需增加一条以friend关键字开始的函数声明语句即可。
  友元声明可以定义在类定义的内部,但是在类内出现的位置不限。
Note:一般来说,最好在类定义开始或结束前的位置集中声明友元。

友元的声明
  友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。
  为了使友元对类的用户可见,通常把友元的声明与类本身放置在同一个文件中(类的外部)(除了类内部的友元声明之外)。
  

7.3 类的其他特性

定义一个类型成员
  除了函数和函数成员之外,类还可以定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种。
  

typedef std::string::size_type pos;     //等价使用using pos = std::string::size_type

令类作为内联函数
  定义在类内部的成员函数是自动inline的。
  可以在类类内部把inline作为声明的一部分显式地声明成员函数。
  可以在类的外部用inline关键字修饰函数的定义。
注:虽然无须在声明和定义的地方同时说明inline,但是这么做时合法的。不过,最好只在类的外部定义的地方说明inline。
Note:inline成员函数应该与相应的类定义在同一个文件中。
重载成员函数
可变数据成员
  有时,希望修改类的某个数据成员,即使是在一个const成员函数内,可以通过在变量的声明中加入mutable。
  一个可变数据成员永远不会是const,即使它是const对象的成员。一个const成员函可以改变一个可变成员的值。
类数据成员的初始值
  类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化。

7.3.2 返回*this的成员函数

  返回*this的成员函数的类型若为 类的引用,可以直接进行多不操作。
  

//其中 move, set,都返回Screen类的引用char ch = myscreen.move(4,0).set('#').get(4,0);

从const成员函数返回*this
  一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
基于const的重载 
  通过区分成员函数是否时const的,可以对其重载。
  虽然可以在非常量对象上调用常量版本或非常量版本,但显然此时非常量版本是一个更好的匹配。常量对象只能调用在常量成员函数。
  

7.3.3 类类型

Note:即使两个类的成员列表完全一致,它们也是不同的类。
  可以直接把类名作为类型的名字使用,或者也可以把类名更在关键字class或者struct后面。
类的声明
  可以仅声明而暂时不定义类:
  

class Screen;

  这种声明称作前向声明,对于Screen来说,在它声明之后定义之前是一个不完全类型
  不完全类型只能在非常有限的情景下使用:
  (1)可以定义指向这种类型的指针或引用;
  (2)可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
注:一个类的成员类型不能是该类自己。然而,一旦这个类的名字出现后,它就被声明了,因此类允许包含指向它自身类型的引用或指针。

7.3.4 友元再探

  类还可以把其他类定义成友元,也可以把其他类(之前已经定义过得)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数时隐式内联的。
类之间的友元关系
  如果一个类指定了一个友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
  注意:友元关系不存在传递性。每个类负责控制自己的友元类或友元函数。
令成员函数作为友元
  当把一个成员函数声明为友元时,必须明确指出该成员函数属于那个类。
  要想令某个成员作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。
函数重载和友元
  如果一个类想把一组重载函数声明为它的友元,需要对这组函中的每一个分别声明。
  友元声明和作用域
  类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。
  甚至就算在类的内部定义该函数,也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员函数调用该友元函数,它也必须是被声明过的:
  

#include <iostream>using namespace std;struct X{    friend void f(){cout << "友元函数运行" << endl;}      //友元函数可以定义在类的内部    x() {f();}              //错误:f还没有声明    void g();    void h();};void X::g(){return f();}            //错误:f还没有声明void f();                           //声明那个定义在X中的函数void X::h(){return f();}            //正确:现在的f的声明在作用域中了

7.4 类的作用域

作用域和定义在类外部的成员
  一个类就是一个作用域。在类的外部,成员的名字被隐藏起来了。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。
  另一方面,函数的返回类型在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。
  

7.4.1 名字查找与类的作用域

  名字查找过程:
  -首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
  -如果没有找到,继续查找外层作用域。
  -如果最终没有找到匹配的声明,则程序报错。
  类的定义分两步处理:
  -首先,编译成员的声明;
  -知道类全部可见后才编译函数体。
用于类成员声明的名字查找
  这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中(类的外层作用域,而不是类内)中继续查找。
类型名要特殊处理
  在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
  Note:类型名定义通常出现在类的开始处,这样就能确保所有使用该类的成员都出现在类名的定义之后。
成员定义中的普通块作用域的名字查找
  一般来说,不建议使用其他成员的名字作为某个成员函数的参数。
  Note:在类的成员函数中,尽管类的成员别隐藏了,但是可以通过加上类的名字或显式地使用this指针来强制访问类的成员。
类作用域之后,在外围的作用域中查找
  如果需要的时外层作用域中的名字,可以显式地通过作用域运算符来进行请求。
  

int height = 100;class Screen{public:    typedef std::string::size_type pos;    void dummy_fcn(pos height)        {cout << ::height << endl;}private:    pos height = 10;};

在文件中名字的出现处对其进行解析
  当成员定义在类的外部时, 名字查找不仅要考虑定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。
  

7.5 构造函数

7.5.1 构造函数初始值列表

  就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始成员,则该成员将在构造函数之前执行默认初始化。
构造函数的初始值优势必不可少
  如果成员是const、引用或者属于某种类类型且该类型没有定义默认构造函数时,必须通过构造函数初始值列表为这些成员提供初值。
成员初始化的顺序
  成员的初始化顺序与它们在类定义中出现的顺序一致,构造函数初始值列表中初始值的前后位置不会影响实际的初始顺序。
  NOte:最好令构造函数初始值的顺序与成员声明的顺序保持一致。如果可能的化,尽量避免使用某些成员初始化其他成员。
默认实参和构造函数
  Note:如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认的构造函数。
  

7.5.2 委托构造函数

  一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。
  一个委托构造函数也有一个成员初值列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员函数一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配
  

class Sales_data{public:    //非委托的构造函数使用对应的实参初始化成员    Sales_data(std::string s, unsigned cnt, double pirce):        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)}};

  当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。

7.5.3 默认构造函数的作用

  当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生:
  -当在块作用域内不适用任何初始值定义一个非静态变量或者数组时;
  -当一个类本身含有类类型的成员且使用合成的默认构造函数时;
  -当类类型的成员没有在构造函数初始值列表中显式地初始化时。
值初始化在以下情况下发生:
  -在数组初始化的过程中如果提供的初始值数量少于数组的大小时;
  -当不使用初始值定义一个局部变量时;
  -当通过书写形如T()的表达式显式地请求值初始化时,其中T时类型名。
类必须包含一个默认构造函数以便在上述情况下使用。
Note:在实际中,如果定义了其他构造函数,最好也提供一个默认构造函数。
使用默认构造函数
  默认构造函数的参数列表比一定为空,也可能参数列表中的形参都提供了默认实参。
  

7.5.4 隐式的类类型转换

Note:能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

string null_book = "9-999-99999-9";//构建一个临时的Sales_data对象//该对象的units_sold和revenue等于0,bookNo等于null_bookitem.combine(null_book);

只允许一步类型转换
  编译器只会自动地执行一步类型转换。
  

//错误:需要用户定义的两种转换:(1)把“9-999-9999-9”转换成string2)再把这个(临时的)string转换成Sales_dataitem.combine(“9-999-9999-9”);

类类型转换不是总有效的
抑制构造函数定义的隐式转化
  通过把构造函数声明为explicit加以阻止。
  关键字explicit只对一个实参的构造函数有效。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
explicit构造函数只能用于直接初始化
  发生隐式转换的一种情况是 当我们执行拷贝形式的初始化时(使用=)。此时,只能使用直接初始化而不能使用explicit构造函数。
  

Sales_data item1(null_book);        //正确:直接初始化//错误:不能将explicit构造函数用于拷贝形式的初始化过程Sales_data item2 = null_book;

为转换显式地使用构造函数
  尽管编译器不会将explicit构造函数用于隐式转换过程,但是可以使用这样的构造函数显式地强制进行转换。
标准库中含有显示构造函数的类
  -接受一个单参数的const char *的string构造函数不是explicit的;
  -接受一个容量参数的vector构造函数时explicit的。
  

7.5.5 聚合类

  聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,说它时聚合的:
  

  • 所有成员都是public的;
  • 没有定义任何构造函数;
  • 没有类内初始值
  • 没有基类,也没有virtual函数
    可以提过一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。

7.5.6 字面值常量类

  constexpr函数的参数和返回值必须是字面值类型。
  数据成员都是字面值类型的聚合类时字面值常量类。如果一个类不是聚合类,符合以下条件,也是一个字面值常量类:
  

  • 数据成员都必须是字面值类型;
  • 类必须至少含有一个constexpr构造函数;
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数;
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
    constexpr构造函数
      constexpr构造函数可以声明=default的形式。否则,constexpr构造函数就必须既符合构造函数的要求,又符合constexpr函数的要求。总和这两点,constexpr构造函数体一般来说应该是空的。通过前置关键字constexpr就可以声明一个constexpr构造函数了。
      constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。
      

7.6 类的静态成员

  有时候需类一些成员与类本省相关,而不是与类的各个对象保持关联。
声明静态成员
  通过在成员的声明之前加static使得其与类关联在一起。静态成员可以是public的或是private的。
  类的静态成员存在与任何对象之外,对象中不包含任何与静态数据成员有关的数据。
  类似的,静态成员函数也不与任何对象绑定在一起,它们不包含任何this指针。
  使用类的静态成员
  使用作用域运算发直接访问静态成员。
  虽然静态成员不属于类的某个对象,但是仍然可以使用类的对象、引用或者指针来访问静态成员。
  成员函数不用通过作用域运算符就能直接使用静态成员。
  定义静态成员
  既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能够重复static,该关键字只出现在类的内部声明语句。
  静态成员必须在类的外部定义和初始化每个静态成员。
  类似于全局变量,静态数据成员定义在任何函数之外。因为一旦它被定义,就将一直存在于程序的整个生命周期中。
  

//定义并初始化一个静态成员double Account::interestRate = initRate();

Note :要想确保对象只定义一次,最好的办法时把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。
静态数据成员类内初始化
  通常不应该在类的内部初始化。然后,可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须时字面值常量类型的constexpr。
  只用静态常量成员才能在类内初始化。
如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了。
Note:即使一个常量静态数据成员在类的内部被初始化了,通常情况下也应该在类的外部定义以下该成员。
静态成员能由于某些场景,而普通成员不能
  在某些非静态数据成员可能非法的场合,静态成员却可以正常使用。举例:静态成员可以是不完全类型,特别地,静态成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用。
  

class Bar{public:    //...private:            static Bar mem1;//正确:静态成员可以是不完全类型            Bar *mem2;//正确:指针成员可以是不完全类型            Bar *mem3;//错误:数据成员必须是完全类型

  另一个区别是可以用静态成员作为默认实参,但非静态成员不能。

原创粉丝点击