【C++笔记】7.类-创新互联

7. 类
  1. 类的基本思想是数据抽象和封装。

    为龙井等地区用户提供了全套网页设计制作服务,及龙井网站建设行业解决方案。主营业务为网站设计、成都网站制作、龙井网站设计,以传统方式定制建设网站,并提供域名空间备案等一条龙服务,秉承以专业、用心的态度为用户提供真诚的服务。我们深信只要达到每一位用户的要求,就会得到认可,从而选择与我们长期合作。这样,我们也可以走得更远!
  2. 数据抽象是一种依赖于接口和实现分离的编程技术。
    类的接口包括用用户所能执行的操作;
    类的实现包括类的数据成员、负责接口实现的函数体、以及定义类所需的各种私有函数。

  3. 封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节。

7.1 定义抽象数据类型 7.1.1 设计Sales_data类
  1. 设计程序时就需要考虑到抽象类型的对象的各种操作,即接口。

  2. 通过各种操作来设计编写,各种操作在内部的实现。

7.1.2 定义改进的Sales_data类
  1. 成员函数的声明必须在类的内部,定义则既可以在内部也可以在外部。
    定义在类内部的函数是隐式的inline函数。

  2. 成员函数通过一个名为this的额外的隐式参数,来访问调用它的对象。
    因此,任何自定义名为this的参数或变量都是非法的。
    this是一个常量指针。

  3. 引入const成员函数:
    std::string isbn() const{ return bookNo; }
    这里const关键字的作用:成员函数不能改变调用它的对象的内容。
    常量对象、常量对象的引用或指针都只能调用const成员函数。
    不能在一个常量对象上调用普通的成员函数
    原因:默认情况下,this的类型是指向类类型非常量的指针,因此不能把this绑定到一个常量对象上。
    换句话说:this原本类型TYPE * const。即不能改变this本身的值,但可以改变*this的值。使用const成员函数,相当于const TYPE* const,即说明该成员函数不会改变对象原本的值。

  4. 类本身就是一个作用域。编译某一个类时,类内成员函数可以直接调用类内成员变量,且不必考虑其位置。
    但类外定义成员函数,应与类内声明匹配。

  5. 一般来说,定义函数类似于某个内置运算符时,应该令该函数的行为也尽量模仿这个运算符。如:
    内置的赋值运算符把它的左侧运算对象当成左值返回。为了与其保持一致,combine函数应返回引用类型。

    6.3.2节202页:调用返回引用的函数得到左值,其他返回类型得到右值。

  6. 定义一个返回this对象的函数,*this得到该对象。

7.1.3 定义类相关的非成员函数
  1. 一般来说,如果非成员函数是类接口的组成部分,则这些函数应该与类在同一个头文件内。

  2. IO类属于不能拷贝的类型,因此只能用引用来传递它们。
    且因读取和写入会改变流的内容,因此应该是普通引用。
    print函数不负责换行,一般来说,执行输出任务的函数应尽量减少对格式的控制,确保由用户代码来决定是否换行。

7.1.4 构造函数
  1. 构造函数的任务是初始化对象的数据成员,类的对象一旦被创建,就会执行构造函数。

  2. 构造函数与类名相同,且没有返回类型。可以重载,但必须区别明显。

  3. 构造函数不能被声明为const的。

  4. 没有构造函数时,编译器隐式的定义一个默认构造函数。

  5. 如果类里面有指针、数组等复合类型,一定要自己定义一个构造函数,
    且这个构造函数必须把内置类型的变量也都赋值。

  6. 定义一个默认构造函数:Sales_data() = default;
    该函数要求,编译器支持类内初始值。否则应使用初始值列表来初始化成员。

  7. 构造函数初始值列表:冒号和花括号之间的代码。
    当某个数据成员被构造函数初始化列表忽略时,它将以合成默认初始化的方式隐式初始化。

    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) {}
  8. 构造函数也可以在类内声明后,在类外定义。
    这个构造函数初始列表值虽然是空的,但是由于执行了构造函数体,所以对象成员仍然能被初始化。

    Sales_data::Sales_data(std::istream){read(is, *this);
    }
  9. 为避免报错,尽量把声明都写在前面。

7.1.5 拷贝、赋值和析构
  1. 编译器将会主动帮我们进行对象的拷贝、赋值和析构。
    但,有些类不能自动完成上述任务,如管理动态内存的类。
    因此我们应尽量使用vector或string来管理。

  2. 在学习第13章之前,类中所有分配的资源都应该直接以类的数据成员的形式存储。

7.2 访问控制与封装
  1. 访问说明符:public:表示接口,private表示实现。

  2. class与struct的不同,仅仅是成员的默认权限不同。

7.2.1 友元
  1. 允许其他类或函数,即友元,访问自己的private成员。
    友元只能出现在类定义的内部,一般定义在类定义的开头或结尾。

  2. “我的友元的友元,不是我的友元。”

  3. 尽管当类的定义发生改变时无需更改用户代码,但是使用了该类的源文件必须重新编译。

  4. 友元的声明不等于函数的声明,也就是函数本身也要声明。
    因此,通常把友元的声明放在头文件中。

7.3 类的其他特性 7.3.1 类成员再探
  1. 类内可以定义其他类型在类中别名,一般放在类开始的地方。
    要求:使用自定义类型的成员必须先定义,后使用。

    public:
        typedef std::string::size_type pos;
  2. 类内,规模较小的函数适合于被声明为内联函数。
    定义在类内部的函数自动被声明为内联函数,我们也可以显式定义内联函数。

  3. 重载成员函数:应区别明显。

  4. 可变数据成员:关键字mutable
    尽管some_member()是一个const成员函数,它仍然能够改变access_ctr的值。该成员是个可变成员,因此任何成员函数,包括const函数在内,都能改变它的值。

    class screen {public:
        void some_member() const {++access_ctr; };
    private:
        mutable size_t access_ctr;
    }
  5. 类数据成员的初始值:可以使用={ }

    class Window_mgr {private:
        std::vectorscreens{Screen(24, 80, ' ')};
    }
7.3.2 返回*this的成员函数
  1. 返回*this的函数,返回的是对象的副本。
    使用成员函数调用符(.)不会改变对象本身。

  2. 返回*this引用的函数,返回的是对象本身。
    可以改变对象(以及对象的成员的值)。
    且,此时可以基于表达式,再使用成员函数调用符(.)。

  3. 返回*this的const引用的函数时,不能使用成员函数调用符(.)。

    Screen myScreen;
    Screen Screen::move0(int, int){...};
    myScreen.move0(4, 0);  // 不会移动原窗口,因为得到的是原窗口的副本
    Screen &Screen::move(int, int){...};
    myScreen.move(4, 0).move(4, 0);  // 移动原窗口
    const Screen &Screen::display(int, int){...};
    myScreen.display(cout).move(4, 0);  // 错误:不允许对const Screen进行move操作
  4. 基于const的重载:
    原理和上一章差不多,可以减少给函数起名的数量。
    非常量可以进入常量形参函数、非常量形参函数。
    常量只能进入常量形参函数。
    非常量*this可以进入常量*this成员函数、非常量*this成员函数。
    常量*this只能进入常量*this成员函数。

  5. 对于公共代码(display)使用私有功能函数(do_display):

    • 避免在多处使用同样的代码。
    • 随着类的规模发展,display函数可能愈发复杂。
    • 可以只在do_display处添加调试代码。
    • 类内部定义do_display会声明为内联函数,开销很小。
7.3.3 类类型
  1. 每个类定义了唯一的类型,即使成员完全一样,这两个类也是不同的类。

  2. 类也可以先声明,后定义,但定义必须在创建对象之前。

7.3.4 友元再探
  1. 类之间的友元关系:无传递性
    “我的友元的友元,不是我的友元。”
    必须有友元声明才能使用。

  2. 还可以令其他类的成员函数作为友元。
    必须有友元声明才能使用。

  3. 友元函数如果有重载函数,必须把对应的形参写完整,方以区别。

  4. 友元与作用域:建议声明友元在类的内部,声明友元在类的外部,友元函数定义在类的外部。

7.4 类的作用域
  1. 注意::.的区别。
7.4.1 名字查找与类的作用域
  1. 目前为止我们编写的程序中,名字查找的过程:

    • 首先,在名字所在的块中寻找声明语句,只考虑在名字的使用之前出现的声明。
    • 如果没找到,继续查找外层作用域。
    • 如果最终没找到匹配的声明,则程序报错。
  2. 类的定义过程、特点:

    • 首先,编译成员的声明,直到类全部可见后才编译函数体。
    • 编译器处理完类中的全部声明后才会处理成员函数的定义。
    • 成员函数可以使用类中定义的任何名字。
    • 如果函数的定义和成员的声明被同时处理,则只能使用那些已经出现的名字。
  3. 类成员声明的名字查找:
    该作用域内查找,若无则去外层,在该作用域插线之前查找。

  4. 一般说来,内层作用域可以重新定义外层作用域中定义过的名字,即使在内层使用过。
    但是在类中,如果成员使用了外层作用域的某个名字,则类不能重新定义该名字。

  5. 类型名的定义通常出现在类的开始处,这样就能确保所有使用该类的成员都出现在类名的定义之后。

  6. 成员函数中使用的名字按照如下方式戒子:

    • 首先,在成员函数内查找该名字的声明。且要求名字的声明出现在使用之前。
    • 如果函数成员内没有找到,则在类内查找,这时所有成员都被考虑而无关类内位置。
    • 如果类内也没找到该名字的声明,在成员函数定义之前的作用域继续查找。
  7. 如果出现了上面的情况,可以使用作用域说明符(::)来说明,函数体里的某个名字是外层作用域的名字。

  8. 当成员函数在类的外部,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还要考虑在成员函数定义之前的全局作用域的声明。

    typedef string Type;
    Type initVal();  // string
    class Exercise {public:
        typedef double Type;  // double
        Type setVal(Type);  // double double
        Type initVal();  // double
    
    private:
        int val;
    };
    // string double
    Type Exercise::setVal(Type parm) {val = parm + initVal();
        return val;  // 无法从int转换到string
    }
7.5 构造函数再探 7.5.1 构造函数初始值列表
  1. 定义变量时应立即初始化,而非先定义在赋值。构造数据成员时,也应如此。
    若无显式初始化成员,则进行默认初始化。
    若成员是const或引用,则必须进行初始化。
    若成员是类类型且无默认初始化规则时,应显式初始化。

    class ConstRef {public:
            ConstRef(int ii);
        private:
            int i;
            const int ci;
            int &ri;
    };
    ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}
  2. 成员初始化的顺序:按照类定义中成员出现的顺序进行。
    因此,应尽量避免一个成员初始化其他成员。

  3. 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
    Sales_data(std::s = ""): bookNo(s){ }

7.5.2 委托构造函数
  1. 委托构造函数:用其他构造函数来定义自己的构造函数。
    写法上,就是把构造函数初始值列表(冒号和花括号之间的代码)嵌入另一个构造函数即可。
7.5.3 默认构造函数的作用
  1. 在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
7.5.4 隐式的类类型转换
  1. 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数。具体内容将在14.9节展开。

  2. 允许我们通过一个实参调用的构造函数定义一条从构造函数的参数类型向类类型隐式转换的规则。
    在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则。因此,在需要使用Sales_data的地方,我们可以使用string或istream作为替代。

    class Sales_data{Sales_data(string s) : bookNo(s) {}
    Sales_data(istream &is) {read(is, *this); }
    };
    
    // 错误方式:
    item.combine("9-999-99999-9");
    
    // 正确方式:
    // 隐式地将null_book转换为Sales_data
    string null_book = "9-999-99999-9";
    item.combine(null_book);
    
    // 正确方式:
    // 显式转换为string,隐式转换为Sales_data
    item.combine(string("9-999-99999-9"));
    // 隐式转换为string,显式转换为Sales_data
    item.combine(Sales_data("9-999-99999-9"));
  3. 类类型转换不一定总是有效。比如用cin来的输入转换后,可能因输入不规范导致无法转换。

  4. 抑制构造函数的隐式转换:
    在类的定义内,将构造函数声明前加上explicit关键字。
    explicit构造函数只能用于直接初始化,而非拷贝初始化。
    使用时,只能把explicit关键字放在类定义内,构造函数的声明处。

  5. 编译器不会将explicit的构造函数用于隐式转换过程,但用户可以显式初始化调用explicit函数。

  6. 标准库中已有显式构造函数的类

    • 接受一个单参数的const char*的string构造函数不是explicit的。
    • 接受一个容量参数的vector构造函数是explicit的。
7.5.5 聚合类
  1. 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
    当一个类满足如下条件时,我们说它是聚合的:

    • 所有成员都是public的。
    • 没有定义任何构造函数。
    • 没有类内初始值。
    • 没有基类,也没有vitual函数(第15章)
  2. 我们可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。
    其中初始值的顺序必须与声明的顺序一致,否则是错误的。
    和初始化数组的规则一样。如果初始值列表元素个数比类的成员少,则缺靠后的成员被默认初始化。
    不允许初始值列表元素个数比类的成员多。

    struct Data{int ival;
        string s;
    }
    Data val1 = {0, "Anna" };
  3. 三个明显的缺点:

    • 要求类的所有成员必须是public的。
    • 初始化乏味且容易出错。
    • 添加或删除一个成员,所有的初始化语句都要更新。
7.5.6 字面值常量类
  1. 除了算术类型、引用、指针外,某些类也可以是字面值类型。

    类型一般比较简单,值也显而易见、容易得到,就把它们称为字面值类型。
    和其他类不同,字面值类型的类可能含有constexpr函数成员,这样的成员必须符合const的所有要求,它们是隐式const的。

  2. 数据成员都是字面值类型的聚合类,是字面值常量类。
    若非聚合类,但符合下述要求,它仍是字面值常量类:

    • 数据成员必须都是字面值类型。
    • 类必须至少含有一个constexpr构造函数。
    • 若一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式。
    • 若成员始于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
    • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
  3. constexpr构造函数
    尽管构造函数不能是const的,但字面值类型常量类的构造函数可以是constexpr函数。
    事实上,一个字面值常量类必须至少提供一个constexpr构造函数。

  4. constexpr构造函数可以声明成=default的形式、或者删除函数的形式。
    否则,constexpr构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又符合constexpr函数的要求(意味着它拥有的唯一语句是返回语句)。
    综合以上两点可知,constexpr构造函数的函数体一定是空的。我们通过前置关键字constexpr就可以声明一个constexpr构造函数了。

  5. constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。

7.6 类的静态成员
  1. 有时,类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联,此时可声明一个静态成员。
    关键字static,其可以是public的,也可以是private的。
    其可以是常量、引用、指针、类类型等。

    class Account{public:
        void calculate(){amount += amount * interestRate;}
        static double rate() {return interestRate;}
        static void rate(double);
    private:
        std::string owner;
        double amount;
        static double interestRate;
        static double initRate();
    };
  2. 类的静态变量存在于任何对象之外,且被所有同类型对象共享。
    静态成员函数也不与任何对象绑定,它们不包含this指针。
    作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。
    这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效。

  3. 定义静态类型成员,既可以在类的内部,也可以在类的外部。
    在类的外部定义静态成员时,不能重复static关键字,该关键字只能出现在类的内部声明处。
    定义静态成员变量或函数时,必须使用作用域运算符。

    void Account::rate(double newRate){interestRate = newRate;
    }
  4. 因为静态函数成员不属于任何一个对象,所以它们并不是在创建类的对象时被定义的。
    换句话说,它们不是由类的构造函数初始化的。

  5. 不能在类的内部初始化静态成员,要在类的外部初始化静态成员。
    一个静态变量成员只能定义一次。
    类似于全局变量,静态数据成员定义在任何函数之外,只要它已被定义,就将一直存在于程序的整个声明周期中。
    虽然initRate()是私有的,但也可以用它来初始化interestRate。

    double Account::interestRate = initRate();
  6. 静态成员的类内初始化:
    通常情况下,类的静态成员不应该在类的内部初始化。
    我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。
    初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。例如,我们可以用一个初始化了的静态数据成员指定数组成员的维度。

  7. 如果某个静态成员的应用场景仅限于编译器可以替换它的值的情况,则一个初始化的const或constexpr不需要分别定义。相反,如果我们将它用于值不能替换的场景中,则该成员必须有一条定义语句。
    即使一个常量静态数据成员在类内被初始化了,通常情况下也应该在类的外部定义一下该成员。

  8. 静态成员能用于某些场景,而普通成员不能。
    比如,静态数据成员可以是不完全类型。
    (声明之后定义之前,此时为不完全类型)
    特别的,静态数据成员的类型可以是它所属的类类型,而非静态数据成员则受到限制,只能声明成它所属的类的指针或引用。
    静态成员和普通成员的另外一个区别是,我们可以使用静态成员作为默认实参。
    而非静态成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


当前名称:【C++笔记】7.类-创新互联
URL标题:http://scyanting.com/article/pjiph.html