C++ 学习笔记之(7)-类

C++ 学习笔记之(7)-类

类的基本思想是数据抽象和封装。封装实现了类的接口和实现的分离。数据抽象是依赖于接口和实现分离的编程技术。

定义抽象数据类型

定义改进的Sales_data

struct Sales_data{
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double 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&);
  • 定义在类内部的函数是隐式的inline函数

  • 所有成员必须在类内部声明,但成员函数体定义可在类内或类外

  • this为成员函数的隐式参数,由函数对象地址初始化,为常量指针,指向对象本身,不可改变。

  • 常量成员函数(const member function):成员函数参数列表后加const关键字,作用是修改隐式this指针的类型,使其成为指向常量对象的常量指针const Sales_data *const。因为默认this类型为指向类类型非常量版本的常量指针Sales_data *const,故this无法绑定到常量对象。常量成员函数不能修改对象内容。

    std::string isbn() const { return bookNo; }
  • 常量对象以及常量对象的引用或指针都只能调用常量成员函数

  • 编译器分两步处理类,首先编译类成员声明,然后到成员函数体。故成员函数体可随意使用类其他成员。

定义类相关的非成员函数

类需要一些辅助函数比如上述add, read等,这些函数属于类的接口组成部分,实际不属于类本身

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

  • IO类属于不能被拷贝的类型,所以只能通过引用传递他们

    istream &read(istream &is, Sales_data &item)
    {
      double price = 0;
        is >> item.bookNo >> item.units_sold >> price;
        item.revenue = price * item.units_sold;
        return is;
    }

构造函数

构造函数是用来控制其对象的初始化过程。

扫描二维码关注公众号,回复: 153489 查看本文章
  • 构造函数名字和类名相同,没有返回类型

  • 构造函数不能被声明成const,当创建类的const对象时,知道构造函数完成初始化过程,对象才真正取得常量属性

  • 默认构造函数:类通过一个特殊的构造函数控制默认初始化过程,无需任何实参。当类没有显示定义构造函数时,编译器会隐式定义默认构造函数,又被称为合成的默认构造函数

  • 如果类包含内置类型或复合类型的成员,则应赋予其类内初始值,这样类才适合使用合成的默认构造函数,否则默认初始化,其值未定义。

  • C++11新标准定义在参数列表后使用= default要求编译器生成构造函数。

    struct Sales_data{
      Sales_data() = default;
    }
  • 构造函数初始值列表:负责为新创建对象的一个或几个数据成员赋初值

    Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenus(p*n) {}

访问控制与封装

  • C++使用访问说明符(access specifiers)加强累的封装性

    • 定义在public说明符之后的成员在整个程序内可被访问
    • 定义在private说明符后的成员可被类成员函数访问,但不能被使用该类的代码访问
  • structclass的默认访问权限不一样。class关键字的默认为private

  • 友元:类可以使其他类或函数成为该类友元,就可以使其他类或函数访问它的非公有成员

    friend Sales_data add(const Sales_data&, const Sales_data&);
  • 友元声明仅指定了范文权限,而非通常意义的函数声明,最好在友元声明外再对函数进行一次声明

类的其他特性

类成员再探

  • inline可用在类内部函数声明,也可用在类外部函数定义处

  • 可变数据成员(mutable):永远不会是const, 即使它是const对象的成员,即一个const成员函数可以改变一个可变成员的值

    class A
    {
        public:
            void add() const
          {
              ++b;  // 成立,因为是可变成员 mutable
          }
      private:
            mutable int b;  // 即使在一个`const`对象内也能被修改
    }

返回 *this的成员函数

  • 一个const成员函数如果以引用形式返回*this, 那么它的返回类型将是常量引用
  • 通过区分成员函数是否为const,可以对其进行重载,原因类似于函数参数中指针是否为const

类类型

  • 前向声明:函数声明和定义分离,在声明之后定义之前是一个不完全类型,即只知类类型,却不清楚包含那些成员
  • 不完全类型的使用非常有限:可以定义指向不完全类型的指针或引用个也可以声明(但不能定义)以不完全类型作为参数或返回类型的函数。

友元再探

类之间的友元关系

class Screen{
    friend class Window_mgr;  // Window_mgr 的成员可以访问 Screen 类的私有部分
};
  • 若类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员
  • 友元关系不存在传递性,即window_mgr的友元类不具有访问Screen的特权
  • 每个类负责控制自己的友元类或友元函数

令成员函数作为友元

class Screen{
    friend void Window_mgr::clear(ScreenIndex);  // Window_mgr::clear必须在Screen类之前被声明
}

友元函数设计规则

  • 首先定义Window_mgr类,其中声明clear函数,但不能定义。在clear使用Screen的成员之前必须先声明Screen
  • 接下来定义Screen, 包括对于clear的友元声明
  • 最后定义clear,此时它才可以使用Screen的成员

友元声明和作用域

  • 友元声明的作用是影响访问权限,并非普通意义的声明

    struct X{
      friend void f() { /* 友元函数可以定义在类内部 */}
        X() { f(); }  // 错误:f 还没有被声明
        void g();
        void h();
    };
    void X::g() { return f(); }  // 错误:f 还没有被声明
    void f();  // 声明那个定义在 X 类中的函数
    void X::h() { return f(); }  // 正确:现在 f 的声明在作用域中了

类的作用域

  • 一个类就是一个作用域,故外部定义类成员函数需要提供类名和函数名,在类外部,成员的名字会被隐藏

  • 编译器处理完类中的全部声明后才会处理成员函数的定义

  • 外层对象若被隐藏,可使用作用域运算符访问

    void Screen::dummy_fcn(pos height){
      cursor = width * ::height;   // 全局变量height
    }

构造函数再探

构造函数初始值列表

  • 若在构造函数的初始值列表中未显示初始化成员,则成员会在构造函数体之前执行默认初始化
  • 构造函数初始值是进行初始化,构造函数体中执行的是赋值
  • 若成员是const、引用,或者某种未提供默认构造函数的类类型,必须通过构造函数初始值列表进行初始化。
  • 初始化和赋值的区别事关底层效率问题
    • 初始化是直接初始化数据成员
    • 赋值是先初始化后赋值
  • 成员初始化顺序和在类定义中的出现顺序一致,并且尽量避免使用某些成员初始化其他成员
  • 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数

委托构造函数

C++11定义了 委托构造函数(delegating constuctor):即可使用其所属类的其他构造函数执行它的初始化过程

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) {}
}
  • 先执行被委托的构造函数,再执行委托者的函数体

隐式的类类型转换

  • 转换构造函数:若构造函数只接受一个实参,则实际上定义了转换为此类类型的隐式转换机制,这种构造函数被称为转换构造函数

    string null_book = '9-999=99999-9';
    // 构造临时Sales_data对象,对象的units_sold 和 revenue 为0, bookNo 等于 null_book
    item.combine(null_book);
  • 编译器只会自动执行一步类型转换

    // 错误:需要两步转换,先将字符串字面值转为string,再将临时的string对象转换成Sales_data对象
    item.combine("9-999-99999-9");
    item.combine(string("9-999-99999-9"));  // 正确:显示转换成 string,隐式转换成 Sales_data
  • 可以通过将构造函数声明为explicit阻止隐式转换,explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换。

    class Salse_data{
      public:
            explicit Sales_data(const std::string &s): bookNo(s) {}
    }
  • 只能在类内声明构造函数时使用explicit关键字,类外部定义时不应重复

  • explicit构造函数只能用于直接初始化,不能用于拷贝形式初始化

聚合类

聚合类是指用户可以直接访问其成员,并且有特殊的初始化语法形式,条件如下

  • 所有成员都是public
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有virtual函数

字面值常量类

字面值常量类的要求

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

构造函数不能为const,但字面值常量类的构造函数可以使constexpr函数

constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {}
  • constexpr构造函数必须初始化所有数据成员,初始值使用constexpr构造函数或者是常量表达式

类的静态成员

与类而非对象关联的成员为静态成员,可在成员声明之前加上关键字static, 静态成员可以是publicprivate的,类型可以是常量、引用、指针或类类型等

  • 静态成员函数不能声明成const

  • static函数体内不能使用this指针,因为静态成员函数不与对象绑定,没有this指针

  • 可以使用作用域运算符直接访问静态成员,可以使用类对象、引用或指针访问静态成员

    double r;
    r = Account::rate();  // 使用作用域运算符访问静态成员
    Account ac1, *arc = &ac1;
    // 调用静态成员函数 rate 的等价形式
    r = ac1.rate();  // 通过 Account 的对象或引用
    r = ac2->rate();  // 通过指向 Account 对象的指针
  • 成员函数不需要通过作用域运算符就能直接使用静态成员

  • static关键字只能出现在类内部的声明语句中

  • 因为静态数据成员不属于累的任何一个对象,所以不能再类内部初始化静态成员,必须在类外部定义和初始化每个静态成员,静态数据成员只能定义一次

  • 若静态成员为字面值常量类型的constexpr,可以为静态成员提供const整数类型的类内初始值,初始值必须是常量表达式

  • 静态数据成员可以使不完全类型,特别的,可以就它所属的类类型。而非静态成员则收到限制,只能声明成它所属类的指针或引用

  • 静态成员可以做默认实参,而普通成员不行

结语

类是C++语言中最基本的特性,有两项基本能力:数据抽象,即定义数据成员和函数成员的能力;二是封装,即保护类的成员不被随意访问的能力。通过将类的实现细节设为private,就可以实现封装。类可以将其他类或者函数设为友元,这样它们就能访问类的非公有成员

类可以定义构造函数,用来初始化对象。构造函数可以重载,切应该使用构造函数初始值列表来初始化所有数据成员

类还能定义可变或静态成员,一个可变成员永远不会是const, 即使在const成员函数内也能修改它的值;一个静态成员可以使函数也可以是数据,静态成员存在于所有对象之外。

猜你喜欢

转载自blog.csdn.net/u011221820/article/details/80071755