C++primer笔记——第七章【类】

【第七章】 类
    1、类的基本思想是数据抽象 和 封装。
    
    2、数据抽象是一种依赖于接口和实现分离的编程技术。
    
    3、类的接口包括用户所能执行的操作。类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。
    
    4、封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,即类的用户只能使用接口而无法访问实现部分。
    
    5、类要想实现数据抽象和封装,要先定义一个抽象数据类型。
    
7.1 定义抽象数据类型
    1、成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部
    
    2、非成员函数的定义和声明都在类的外部
    
    3、定义在类内部的函数是隐式的inline函数
    
引入this:
    4、成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
    例如,如果调用a.member_fun(),则编译器把对象a的地址传递给member_fun的隐式形参this。
    
    5、在成员函数内部,可以直接使用调用该函数的对象成员,而无须通过成员访问运算符。因为this所指的就是这个对象。任何对类成员的直接访问都被看做this的隐式引用。
    即:当成员函数使用数据成员num时,它隐式地使用this指向的成员,即this->num
    
    6、this形参是隐式定义的,任何自定义名为this的参数或变量的行为都是非法的。
    
    7、因为this的目的总是指向“这个“对象,所以this是一个常量指针,不允许改变this中保存的地址。
    
引入const成员函数:
    若有函数 string isbn() const { return num; }
    8、这里的const的作用是修改隐式this指针的类型。
    默认情况下,this的类型是指向类类型非常量版本的常量指针,即 类名 *const。这意味着我们不能将this绑定到一个常量对象上。这也导致不能在一个常量对象上调用普通的成员函数
    
    9、而this是隐式的且不出现在形参列表中,所以在哪将this声明成指向常量的指针呢?
    C++允许把const关键字放在成员函数的参数列表之后,此时紧跟在参数列表后的const表示this是一个指向常量的指针!!这样使用const的成员函数称为常量成员函数。
    
    10、常量成员函数不能改变调用它的对象的内容!
    
    11、常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
    
类的作用域和成员函数:
    12、类外定义的成员函数,要包含它所属的类名,代表该函数被声明在类的作用域内,一旦编译器看到这个函数名,就能理解剩余的代码是位于类的作用域内的。
    
定义一个返回this对象的函数:
    返回类型为类的引用,返回语句为return *this;    // 返回调用该函数的对象
    
7.1.3 定义类相关的非成员函数
    13、有些函数定义的操作从概念上来说属于类的接口的组成部分,但实际上它们并不属于类本身。
    
    14、定义非成员函数的方式和定义其他函数一样,通常把函数的声明和定义分离开。
    如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内。
    
    15、一般,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
    
7.1.4 构造函数
    16、每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数。
    
    17、构造函数的任务时初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
    
    18、构造函数的名字和类名相同,但没有返回类型。类可以包含多个构造函数,不同的构造函数间要在参数数量和类型上有区别,和重载函数一样
    
    19、构造函数不能声明为const的,当我们创建类的一个const对象时,知道构造函数完成初始化过程,对象才能真正取得常量属性。因此,构造函数在const对象的构造过程中可以向其写值
    
合成的默认构造函数:
    20、如果没有显式地定义构造函数,则编译器会隐式地定义一个构造函数,称为合成的默认构造函数。按如下规则初始化类的数据成员:
    1)如果存在类内初始值,用它来初始化成员
    2)否则,默认初始化该成员
    
    21、假如有一个类
    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;
    };

= default的含义:
    Sales_data() = default;
    该函数不接受任何实参,所以是一个默认构造函数,定义这个构造函数是因为我们既需要其他形式的构造函数,也需要默认的构造函数。希望这个函数的作用完全等同于之前使用的合成默认构造函数。
    如果需要默认的行为,则可以再参数列表后面写上=default来要求编译器生成构造函数。其中=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。
    在类的内部就是内联的。
    
构造函数初始值列表:
    构造函数中 冒号以及冒号和花括号之间的代码,这部分叫做构造函数初始值列表,负责为新创建的对象的一个或几个数据成员赋值。
    当某个数据成员被构造函数初始值列表忽略时,将以与合成默认构造函数相同的方式隐式初始化。
    
7.1.5 拷贝、赋值和析构
    对象会在几种情况下被拷贝:
    1)初始化变量以及以值的方式传递或返回一个对象等
    2)当我们使用了赋值运算符时会发生对象的赋值操作
    
7.2 访问控制与封装
    1、C++中,使用访问说明符来加强类的封装性:
    1)定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口
    2)定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(隐藏了)类的实现细节
    
    2、作为接口的一部分,构造函数和部分成员函数紧跟在public说明符后。而数据成员和作为实现部分的函数跟在private说明符后面。
    
使用class或struct关键字:
    可以使用这两个关键字定义一个 类。唯一的区别就是他们的默认访问权限不一样。struct关键字默认是public的,class关键字的成员默认是private的
    
7.2.1 友元
    1、类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
    
    2、如果想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。
    
    3、友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限,一般放在类定义开始或结束前的位置集中声明友元。
    
友元的声明:
    4、为了使友元对类的用户可见,一般把友元的声明与类本身放在一个头文件中(类的外部)
    
7.3 类的其他特性
7.3.1 类成员再探

定义一个类型成员:
    1、除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或private
    
    2、类型成员通常出现在类开始的地方
    
令成员作为内联函数:
    3、建议只在类外部定义的地方说明inline,inline成员函数也应该与相应的类定义在同一个头文件中
    
可变数据成员:
    4、有时(不经常)会有这种情况,希望能修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中假如mutable关键字来实现。
    
    5、一个可变数据成员永远不会是const,即使它是const对象的成员。因此,任何成员函数,包括一个const成员函数都可以改变一个可变成员的值。

类数据成员的初始值:

    class 类1{
    private:
        vector<类2> s{类2(23, 80, ' ')};
    };
    6、类内初始值必须使用=的初始化形式或者用花括号括起来的直接初始化形式
    
7.3.2 返回*this的成员函数
    1、返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本
    如a.member_fun1(4,0).member_fun2('*');        这些操作将在同一个对象上进行
    
    2、假如定义的返回类型不是引用,则fun1的返回值将是*this的副本,因此调用fun2只能改变临时副本,不能改变对象a的值
    
从const成员函数返回*this:
    3、假如令一个函数是常成员函数,此时this将是一个指向const的指针而*this是const对象。所以该函数的返回类型应该是const 类名&
    
    4、一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
    
基于const的重载:
    5、通过区分成员函数是否是const的,我们可以对其进行重载,其原因与根据指针参数是否指向const而重载的原因差不多
    因为非常量版本的函数对于常量对象是不可用的,所以只能在一个常量对象上调用const成员函数。
    另外,虽然可以在非常量对象上调用常量版本或非常量版本,但是非常量版本显然是一个更好的函数匹配
    
7.3.3 类类型
    1、即使两个类的成员列表完全一致,它们也是不同的类型。
    
类的声明:
    2、仅声明类而暂时不定义它:class S;
    这种声明称为前向声明,它向程序中引入了名字S并且指明S是一种类类型。对于类型S,在它声明之后定义之前是一个不完全类型。
    
    3、不完全类型只能在下面情景使用:
    可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
    
    4、因为只有当类全部完成后类才算被定义,编译器才知道存储该数据成员需要多少空间,所以一个类的成员类型不能是自己
    然而,一旦一个类的 名字出现后,他就被认为是声明过了(但尚未定义),因此类允许包含指向他自身类型的引用或指针。
    
7.3.4 友元再探
    1、如果一个类的某个成员想访问另一个类的私有成员,则这个私有成员所属的类需要把想访问的类指定成它的友元
    
    2、如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员
    
    3、友元关系不存在传递性。也就是说,如果友元类有它自己的友元,则这些友元不能具有访问一个类的特权
    
令成员函数作为友元:
    4、除了令整个类作为友元之外,还可以只为需要访问某个私有成员的成员函数提供访问权限。
    假如W类的fun想访问S类的私有成员,需要指明该成员函数属于哪个类
    class S
    {
        friend void W::fun(si);
    };
    
    要想令某个成员函数作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系:
    1)首先定义W类,其中声明fun函数,但是不能定义它。在fun使用S的成员之前必须先声明S
    2)接下来定义S,包括对fun的友元声明
    3)最后定义fun,此时它才可以使用S的成员
    
函数重载和友元:
    5、如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明
    
友元声明和作用域:
    6、见P252例子
    
7.4 类的作用域
    1、在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。
    
作用域和定义在类外部的成员:
    2、在类的外部,成员的名字被隐藏。所以一旦遇到类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。
    
7.4.1 名字查找和类的作用域
    1、名字查找:
    1)首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明
    2)如果没找到,继续查找外层作用域
    3)如果最终没有找到匹配的声明,程序报错
    
    2、对于类的定义,分两步处理:
    1)首先,编译成员的声明
    2)直到类全部可见后才编译函数体
    即:编译器处理完类中的全部声明后才会处理成员函数的定义。所以,因为成员函数体直到整个类可见后才会被处理,所以它能使用类中定义的任何名字
    
成员定义中的普通块作用域的名字查找:
    3、成员函数中使用的名字按照如下方式解析:
    1)首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前的声明才被考虑。
    2)如果在成员函数内没有找到,则在类内继续查找,此时类的所有成员都可以被考虑。
    3)如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找
    一般来说,不建议使用其他成员的名字作为某个成员函数的参数。具体见P256例子,因为你不确定你所访问的是参数列表中的变量还是类的数据成员
    但可以用作用域运算符或this指针显式表示访问的是数据成员,否则就是访问的参数
    
类作用域之后,在外围的作用域中查找:
    如果需要的是外层作用域中的名字,可以显式地通过作用域运算符来进行请求
    
7.5 构造函数再探
    当定义变量时习惯于立即对其进行初始化,而非先定义,再赋值:
    string foo = "hello";    // 定义并初始化
    string bar;                // 默认初始化成空string对象
    bar = "hello";            // 为bar附一个新值
    就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。如:
    // Sales_data构造函数的一种写法,虽然合法但是比较草率:没有使用构造函数初始值
    Sales_data::Sales_data(const string &s, unsigned cnt, double price)
    {
        bookNo = s;
        units_sold = cnt;
        revenue = cnt * price;
    }
    这段代码和初始值列表的效果是一样的:当构造函数完成后,数据成员的值相同。
    区别是:原来的初始值列表初始化了它的数据成员,而这个版本是对数据成员执行了赋值操作。
    
构造函数的初始值有时必不可少:
    1、有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。
    如果成员是const或者是引用的话,必须将其初始化!类似地,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。如:
    class ConstRef{
    public:
        ConstRef(int ii);
    private:
        int i;
        const int ci;
        int &ri;
    };
    和其他常量对象或者引用一样,成员ci和ri都必须被初始化。因此,如果没有为它们提供构造函数初始值的话将引发错误:
    ConstRef::ConstRef(int ii)
    {
        // 赋值
        i = ii;            // 正确
        ci = ii;        // 错误,不能给const赋值
        ri = i;            // 错误,ri没有被初始化
    }
    随着构造函数体一开始执行,初始化就完成了。我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值,因此该构造函数的正确形式应该是:
    ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}
    
    总结:如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值
    在很多类中,初始化和赋值的区别事关底层效率的问题:前者直接初始化数据成员,后者则先初始化再赋值
    
成员初始化的顺序:
    1、构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序!成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,以此类推
    构造函数初始值列表中的初始值的前后位置关系不会影响实际的初始化顺序,如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。
    总结:最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免用某些成员初始化其他成员。
    
默认实参和构造函数:
    2、如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数
    
7.5.2 委托构造函数
    一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数
    1、一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身
    如Sales_data():Sales_data("", 0, 0){}
    Sales_data(std::string s):Sales_data(s, 0, 0){}
    当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
    假如函数体包含有代码的话,将先执行这些代码!!然后控制权才会交还给委托者的函数体。
    
7.5.3 默认构造函数的作用
    1、当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况发生:
    1)当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时
    2)当一个类本身含有类类型的成员且使用合成的默认构造函数时
    3)当类类型的成员没有在构造函数初始值列表中显式地初始化时
    
    2、值初始化在以下情况发生:
    1)在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时
    2)当我们不使用初始值定义一个局部静态变量时
    3)当我们通过书写形如T()的表达式显式地请求值初始化时,其中T是类型名
    
7.5.4 隐式的类类型转换
    1、如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时把这种构造函数称作 转换构造函数
    
    2、能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
    
    3、只允许一步类类型转换
    
抑制构造函数定义的隐式转换:
    4、可以通过将构造函数声明为explicit加以阻止
    关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于隐式转换!所以无需将这些构造函数指定为explicit的。
    只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复
    
explicit构造函数只能用于直接初始化:
    5、发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(=)。此时,我们只能使用直接初始化而不能使用explicit构造函数:
    Sales_data item1(null_book);    // 正确,直接初始化
    // 错误,不能将explicit构造函数用于拷贝形式的初始化过程
    Sales_data item2 = null_book;
    
    总结:当我们使用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器不会在自动转换过程中使用该构造函数。
    
为转换显式地使用构造函数:
    6、尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显式地强制进行转换:
    // 正确,实参是一个显式构造的Sales_data对象
    item.combine(Sales_data(null_book));
    // 正确,static_cast可以使用explicit的构造函数
    item.combine(static_cast<Sales_data>(cin));
    
7.5.5 聚合类
    聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件,就是聚合类:
    1)所有成员都是public的
    2)没有定义任何构造函数
    3)没有类内初始值
    4)没有基类,也没有virtual函数
    
    1、可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致。
    
    2、如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化
    
7.5.6 字面值常量类
    1、数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但符合下面要求的,也是一个字面值常量类:
    1)数据成员必须都是字面值类型
    2)类必须至少含有一个constexpr构造函数
    3)如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须用成员自己的constexpr构造函数
    4)类必须使用析构函数的默认定义,该成员负责销毁类的对象。
    
    2、尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。
    
    3、constexpr构造函数可以声明成=default的形式。
    否则constexpr构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又符合constexpr函数的要求(意味着它能拥有的唯一可执行语句就是返回语句)
    综合这两点,constexpr构造函数体一般来说应该是空的。通过前置关键字constexpr就可以声明一个constexpr构造函数了。
    
7.6 类的静态成员
    希望静态成员与类关联,而非与类的每个对象关联。更重要的是,一旦静态成员发生变化,我们希望所有的对象都能使用新值。
    
声明静态成员:
    1、通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样可以是public或private的。
    
    2、静态数据成员的类型可以是常量、引用、指针、类类型等。
    
    3、类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
    
    4、类似地,静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,而且也不能再static函数体内使用this指针。
    
使用类的静态成员:
    5、我们使用作用域运算符直接访问静态成员,表示其属于哪一个类
    
    6、虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或指针来访问静态成员
    A a1;
    A *a2 = &a1;
    // 调用静态成员函数rate的等价形式
    double r = a1.rate();    // 通过A的对象或引用
    r = a2->rate();
    
    7、成员函数不需要通过作用域运算符就能直接使用静态成员
    
定义静态成员:
    8、和其他成员函数一样,既可以在类内也可以在类外定义静态成员函数。当在类外定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
    
    9、和类的所有成员一样,当我们只想类外部的静态成员时,必须指明成员所属的类名。
    
    10、因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。
    这就意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员!!
    相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。
    
    11、类似于全局变量,静态数据成员定义在任何函数之外。因此一旦它被定义,就将一直存在于程序的整个生命周期中。
    
    12、定义静态数据成员的方式和在类的外部定义成员函数差不多。我们需要制定对象的类型名,然后是类名、作用域运算符以及成员自己的名字:
    // 定义并初始化一个静态成员
    double A::static_member = init_member();
    这条语句定义了名为static_member的对象,该对象是类A的静态成员,类型是double。从类名开始,这条定义语句剩余部分就都位于类的作用域之内了。
    虽然init_member是私有的,我们也能用它初始化静态成员变量
    
    13、要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。

静态成员的类内初始化:
    14、通常,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。
    初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。
    
静态成员能用于某些场景,而普通成员不能:
    如:静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受限制,只能声明成它所属类的指针或引用:
    class Bar{
    private:
        static Bar mem1;        // 正确,静态成员可以是不完全类型
        Bar *mem2;                // 正确,指针成员可以是不完全类型
        Bar mem3;                // 错误,数据成员必须是完全类型
    };
    
    静态成员和普通成员的另一个区别是:可以使用静态成员作为默认实参
    非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终引发错误。
    
    

猜你喜欢

转载自blog.csdn.net/CSDN_dzh/article/details/81172223