C++ Primer Plus “阅读”笔记(六)

学校里面开始上陈国师的oop了呢。但是国师上课超快,继续自学 CPPPP 还是十分有必要的2333.

本篇内容包括:

  • 运算符重载
  • 友元函数
  • 重载 << 运算符,以便用于输出
  • 状态成员
  • 使用 rand() 生成随机值
  • 类的自动转换和强制类型转换
  • 类转换函数
  • 对类成员使用动态内存分配
  • 隐式和显式复制构造函数
  • 隐式和显式重载赋值运算符
  • 在构造函数中使用 new 所必须完成的工作
  • 使用静态类成员
  • 将定位 new 运算符用于对象
  • 使用指向对象的指针
  • 实现队列抽象数据类型(ADT

第11章 使用类

11.1 运算符重载

  • 运算符重载是一种形式的C++多态

11.2 计算时间:一个运算符重载示例

  • 将参数声明为引用的目的是为了提高效率

  • 不要返回指向局部变量或临时对象的引用。函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据

11.2.1 添加加法运算符

  • 重载后的运算符可以像普通函数一样调用
    1
    const Foo sum = a.operator+(b);

11.2.2 重载限制

  • 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。
  • 使用运算符时不能违反运算符原来的句法规则(如参数个数已经优先级)
  • 不能创建新运算符
  • 不能重载下面的运算符
    • sizeofsizeof 运算符
    • .:成员运算符
    • .*:成员指针运算符
    • :::作用域解析运算符
    • ?::条件运算符
    • typeid:一个 RTTI 运算符
    • const_cast:强制类型转换运算符
    • dynamic_cast:强制类型转换运算符
    • reinterpret_cast:强制类型转换运算符
    • static_cast:强制类型转换运算符
  • 下面的运算符只能通过成员函数进行重载
    • =:赋值运算符
    • ():函数调用运算符
    • []:下标运算符
    • ->:通过指针访问类成员的运算符
    • e.g.
      1
      2
      int operator+(const int &lhs, const Foo &rhs) { return lhs + rhs.x; }  // valid
      int operator[](const Foo &lhs, const int &rhs) { return lhs + rhs.x; } // invalid
  • 下列运算符可以重载
    • +-*/%^&|~=!=<>+=-=*=/=%=^=&=|=<<>>>>=<<===!=<=>=&&||++−−,−>*−>()[]newdeletenew []delete []
    • 但我好像有几个不认识诶233,以后有空了解一下。(Flag)

11.3 友元

  • 友元有3种
    • 友元函数
    • 友元类(在第15章介绍)
    • 友元成员函数(在第15章介绍)

11.3.1 创建友元

  • 第一步:将其原型放在类声明中,并在原型声明前加上关键字 friend。这意味着以下两点
    • 虽然该函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
    • 虽然该函数不是成员函数,但它与成员函数的访问权限相同
  • 第二步:编写函数定义。因为它不是成员函数,所以不要使用限定符如 Foo::。另外,不要在定义中使用关键字 friend

11.3.2 常用的友元:重载 << 运算符

  • e.g.
    1
    2
    3
    4
    5
    6
    7
    8
    class Foo {
    /* details omitted */
    friend std::ostream & operator<<(std::ostream &, const Foo &);
    };
    std::ostream & operator<<(std::ostream &os, const Foo &rhs) {
    os << rhs.value;
    return os;
    }

11.6 类的自动转换和强制类型转换

  • 使用关键字 explicit 于构造函数之前,可以避免构造函数导致的隐式类型转换(见 10.3.2

11.6.1 转换函数

  • 转换函数可以讲类对象转换为其他的类型
    1
    2
    3
    4
    5
    6
    7
    class Foo {
    int bar;

    public:
    operator int() const { return bar; }
    operator double() const { return double(bar); }
    };
  • 应注意以下几点
    • 转换函数必须是类方法
    • 转换函数不能指定返回类型
    • 转换函数不能有参数
  • 当语句有二义性时,请使用显式强制类型转换。如对于上面定义的 Foo
    1
    2
    3
    4
    Foo x;
    int a = x; // valid
    long b = x; // invalid
    long c = int(x); // valid
  • 为防止不必要的隐式类型转换,可对转换函数使用 explicit 限定符

11.6.2 转换函数和友元函数

  • 运算符的 lhs 是不会被隐式类型转换的,所以此时用友元函数重载的运算符可以编译通过,但用方法重载的不行

第12章 类和动态内存分配

12.1 动态内存和类

12.1.2 特殊成员函数

  • C++自动提供了下面这些成员函数
    • 默认构造函数,如果没有定义构造函数
    • 默认析构函数,如果没有定义
    • 复制构造函数,如果没有定义
      • 新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用,如(其中第二行和第三行也有可能调用的是赋值运算符)
        1
        2
        3
        4
        5
        6
        Foo bar(bar_);
        Foo bar = bar_;
        Foo bar = Foo(bar_);
        Foo *bar = new Foo(bar_);
        void DoSomething(Foo bar) { /* details omitted */ }
        DoSomething(bar_);
      • 原型为
        1
        classname(const classname &);
      • 默认的复制构造函数的功能
        • 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象
        • 如果成员中有指针,则浅复制容易出锅(有可能在别处把空间释放掉)。为了防止这个问题,可以考虑深复制,即复制构造函数应当复制指针指向的内容并将副本的地址赋给成员,而不仅仅是复制指针指向的地址
    • 赋值运算符,如果没有定义
      • 原型为
        1
        classname & classname::operator=(const classname &);
      • 一般自我赋值直接返回 *this,防止深拷贝时把原始数据 delete
    • 地址运算符,如果没有定义
  • C++11 提供了另外两个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(move assignment operator),这将在第18章讨论

12.2 改进后的新 String

12.2.1 修订后的默认构造函数

  • 在 C++98 中,字面值 0 有两个含义:可以表示数字值零,也可以表示空指针,这使得阅读程序的人和编译器难以区分。有些程序员使用 (void*)0 来标识空指针(空指针本身的内部表示可能不是零),还有些程序员使用 NULL,这是一个表示空指针的 C 语言宏。C++11 提供了更好的解决方案:引入新关键字 nullptr,用于表示空指针。

12.2.3 使用中括号表示法访问字符

  • 下列代码合法,是因为 operator[] 是类的一个方法,因此能够修改内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Pair {
    int x, y;

    public:
    int& operator[](const int &i) {
    return i ? y : x;
    }
    } pair;
    pair[0] = 1;
    int &second = pair[1];
    second = 1;
  • 注意到 int& operator[](const int &)int& operator[](const int &i) const 是不同的特征标,所以可以重载它
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Pair {
    int x, y;

    public:
    Pair(const int &x, const int &y) : x(x), y(y) {}
    int &operator[](const int &i) { return i ? y : x; }
    const int &operator[](const int &i) const { return i ? y : x; }
    };
    Pair foo(1, 2);
    std::cin >> foo[0]; // using non-const version of operator[]
    std::cout << foo[0]; // using non-const version of operator[]
    const Pair bar(1, 2);
    std::cin >> bar[0]; // invalid
    std::cout << bar[0]; // using const version of operator[]

12.2.4 静态类成员函数

  • 可以将成员函数声明为静态的(函数声明必须包含关键字 static,但如果函数定义是独立的,则其中不能包含关键字 static
  • 效果
    • 不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用 this 指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它
    • 由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员
  • 可以使用静态成员函数设置类级(classwide)标记,以控制某些类接口的行为。例如,类级标记可以控制显示类内容的方法所使用的格式

12.2.5 进一步重载赋值运算符

  • 为提高处理效率,经常重载赋值运算符以防止只有一个参数的构造函数被用作转换函数,这样就不用创建和删除临时对象了

12.3 在构造函数中使用new时应注意的事项

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
  • newdelete 必须相互兼容。new 对应于 deletenew[] 对应于 delete[]
  • 如果有多个构造函数,则必须以相同的方式使用 new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。(比如使用 new int [1]。)然而,可以在一个构造函数中使用 new 初始化指针,而在另一个构造函数中将指针初始化为空,这是因为 delete(无论是带中括号还是不带中括号)可以用于空指针
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象
    • 具体地说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。

12.4 有关返回对象的说明

12.4.1 返回指向 const 对象的引用

  • 使用 const 引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制

12.4.2 返回指向非 const 对象的引用

  • 两种常见的返回非 const 对象情形是,重载赋值运算符以及重载与 cout 一起使用的 << 运算符。前者这样做旨在提高效率,而后者必须这样做

12.4.3 返回对象

  • 如果被返回的对象是被调用函数中的局部变量,则应返回对象而不是引用。通常,被重载的算术运算符属于这一类

12.4.4 返回 const 对象

  • 如果被重载的运算符返回的是对象,则可能引发下列问题
    1
    2
    foo1 + foo2 = bar;  // dyslectic programming
    std::cout << (foo1 + foo2 = bar).value() << std::endl; // demented programming
    这种代码之所以可行,是因为复制构造函数将创建一个临时对象来表示返回值。因此,在前面的代码中,表达式 foo1 + foo2 的结果为一个临时对象。如果担心这种行为可能引发的误用和滥用,有一种简单的解决方案:将返回类型声明为 const 对象
  • 总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如 ostream 类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高

12.5 使用指向对象的指针

12.5.3 再谈定位 new 运算符

  • delete 可与常规 new 运算符配合使用,但不能与定位 new 运算符配合使用
  • delete 释放一块缓冲区时,在缓冲区里面的使用定位 new 分配的对象的析构函数不会被调用。这种问题的解决方案是,显式地为使用定位 new 运算符创建的对象调用析构函数
    1
    p->~Foo();
    需要注意的一点是正确的删除顺序。对于使用定位 new 运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区

12.7 队列模拟

12.7.1 队列类

  • 可以对类的常量成员变量进行初始化,但不能给它赋值。因此,对于 const 数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。C++ 提供了一种特殊的语法来完成上述工作,它叫做成员初始化列表(member initializer list)。同样地,对于被声明为引用的类成员,也必须使用这种语法
    1
    2
    3
    4
    5
    6
    class Foo {
    const int bar;

    public:
    Foo(const int &bar) : bar(bar) {}
    };
  • 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关
  • C++11 允许以更直观的方式进行初始化
    1
    2
    3
    class Foo {
    const int bar = 10;
    };
  • 如果要防止默认函数中的浅拷贝造成问题,又懒得写深拷贝的代码,可以这将所需的方法定义为伪私有方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Foo {
    private:
    int x;
    Foo(const Foo &rhs) {}
    Foo &operator=(const Foo &rhs) { return *this; }
    public:
    Foo(const int &x) : x(x) {}
    };
    Foo a(1);
    Foo b = a; // invalid
    C++11 提供了另一种禁用方法的方式——使用关键字 delete,这将在第18章介绍。

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×