C++ 基础(十一)定义自己的数据类型

时间:2021-08-14
本文章向大家介绍C++ 基础(十一)定义自己的数据类型,主要包括C++ 基础(十一)定义自己的数据类型使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 类和面向对象编程

  • 类类型:组合基本类型和其他类型的结构。
  • 面向对象编程的核心:封装,继承和多态。
  • 封装:
    • 对象的定义包含一组属性和一组函数。把这些数值和函数打包到一个对象中,就是封装。
    • 数据隐藏:封装的意义在于数据隐藏,数据成员一般不希望被外部访问,对外的接口始终不发生太大的变化,内部的组成在变。
  • 继承:
    • 根据一个类定义另一个类。
    • 派生类继承基类的成员,并可以定义自己的新成员。
    • 派生类可以重写从基类继承的成员函数。
  • 多态性:
    • C++多态性总是涉及使用指针或者引用调用对象的成员函数。
    • 这种特性仅适用于派生于公共类型的对象。属于一组继承性相关的类的对象,可以通过基类指针和引用来传递和操作。
    • 派生类可以使用基类指针和引用传递,指针和引用可以调用它指向的对象继承的成员函数。
    • 多态性体现是函数调用实际上会跟着类实际指向的对象而定,调用哪个函数是在运行时决定的。

2. 定义类

  • 类成员默认是私有的。(struct 默认是公共的)。
  • 使用public 和 private修饰符。

3. 构造函数

  • 定义类的新实例时调用,创建新对象时初始化调用。
  • 构造函数与类同名。
  • 不定义构造函数时,编译器生成默认的构造函数。
  • default关键字:
    • 如果定义了一个有参构造函数,编译器就不再生成构造函数了,使用default关键字可以生成一个默认构造函数:Box() = default;
  • 类外部定义函数和构造函数:
    • 成员函数和构造函数都需要类名限定。
  • 默认构造函数的参数值:
    • 构造函数和成员函数默认值总放在类中,不放在外部构造函数和成员函数中。
  • 成员初始化列表:
    • 函数定义后,使用冒号分开。
    • 需要按照成员函数初始化列表中的顺序进行初始化。
  • explicit关键字:
    • 只有一个参数的构造函数可能会出现问题。
    • 这种类中有与类参数进行的操作时,该函数传递构造函数的参数,会发生隐式类型转换。
    • 防止这种情况的做法是使用explicit关键字修饰构造函数。
    • 编译器不会把explicit声明的构造函数用于隐式类型转换,只能在程序代码中显示创建对象,
  • 委托构造函数:
    • 类有多个构造函数,一个构造函数在初始化参数列表调用前一个构造函数,把构造工作交给另一个构造函数。
  • 副本构造函数:
    • 把类对象作为参数,编译器提供默认的副本构造函数。
    • 副本构造函数的实参必须是const引用。Type::Type (const Type& object);

4. 访问私有成员

  • 使用成员函数返回私有成员的值。通常定义在类内部,定义为内联函数。称为访问器函数,与之对应的有修改器函数。

5. this指针

  • 执行成员函数时,都会包含一个隐藏的指针,称为this指针。
  • 静态成员函数中不包含this指针。
  • 返回this指针,函数返回值类型是该类的指针。优点是可以使用一个成员函数的返回值调用另一个成员函数。
Box* Box::setLength(double lv)
{
    if(lv > 0) length = lv;
    return this;
}

Box* Box::setWidth(double wv)
{
    if(wv > 0) width = wv;
    return this;
}

Box* Box::setHeight(double hv)
{
    if(hv > 0) height = hv;
    return this;
}

Box myBox {3.0, 4.0, 5.0};
myBox.setLength(-20.0)->setWidth(40.0)->setHeight(10..0);

6. const 对象和const成员函数

  • 综述:
    • const修饰的对象成员变量都是const,都不能修改。
    • const指针指向非const对象,也不能通过指针修改成员。
    • 实参是非const,函数形参是const引用,不能在函数中修改对象内容。
    • 通过const指针或者const引用,与指直接访问const对象有相同的限制。
  •  const成员函数
    • 只使用const限定的对象没有任何用处,不能调用它的任何成员函数。
    • 解决办法是在类中,不修改成员的函数使用const修饰。
  double Box::volume() const
  {
      return length * length * length;
  }
  • const 正确性
    • 在const成员函数内部修改成员变量会失败。
    • 把成员函数指定为const,该成员函数的this指针会也会成为cosnt指针。
    • const成员函数不能调用非const成员函数。
  • 重载const
    • 由于const是函数签名的一部分,可以使用const版本重载一个非const版本的成员函数。
    • 对于返回某个对象封装的内部数据的指针或引用的函数,常常进行重载。
double &length()
{
    return _length;
}

// 重载为
const double & length()const
{
    return _length;
}

// 但是double是基础类型,一般按值进行传递
double length() const
{
    return _length;
}

// 对象是否为const决定了哪个成员函数会被调用

// 错误情况,开头没有const。
// 因为const成员函数隐含的this指针是const,类成员的名称是对const引用,所以返回类型是const引用
double & length() const
{
    return _length;
}
  • 常量的强制转换
    • 把const对象转为非const:
      • const_cast<Type *> (expression);   expression 是 const Type * 或者Type*
      • const_cast<Type&> (expression);   expression 是 const Type *、const Type &、Type或者Type*
    • 但是不建议这样使用,可能会破坏对象的常量性。
  • 使用mutable关键字
    • 希望不修改const对象的特定成员,使用mutable关键字。
    • 修饰的成员即使是const对象,该成员也可被修改。
    • 典型用途是调试,记录日志,缓存或线程同步。

7. 友元

  • 综述:
    • 友元可以访问类对象的任意成员,无论这些成员的修饰符是什么。
    • 友元声明破坏了面向对象编程的数据隐藏性。
    • 运算符重载需要使用一些友元的场景。
  • 友元函数
    • 关键字friend,无法在类外将函数设置为类的友元。
    • 类的友元函数可以是一个全局函数,也可以是另一个类的成员。
    • 访问修饰符不能被用于类的友元函数。
    • 友元函数一般都需要传入类对象作为参数,因为友元不是类成员,不能直接引用成员变量。
    • 友元和一般函数一样,但可以不受限制地访问类中所有成员。
    • 友元应该是别无选择时的选择。
  • 友元类
    • 把整个类声明为另一个类的友元。友元所有函数可以访问原有类的所有成员。
    • 原有类不能访问友元类的私有成员。
    • 友元关系不能传递。
    • 嵌套类是类似场景下替代友元的一种选择。

8. 类的对象数组

  • 一般都是无参构造。
  • 类对象的大小,可以使用sizeof计算,存在边界对齐。

9. 类的静态成员:

  • 综述:

    • 类的成员可以声明为static。类的静态成员可以在类的范围内存储数据。
    • 独立于对象,可以由对象访问,属于类属性。
    • 静态成员函数独立于单个类对象,任何类对象都可以调用静态成员函数。
    • 静态成员函数如果是公共成员,还能从类的外部调用。
    • 静态成员函数的常见用法是无论是否声明了类对象,都能操作静态成员变量。
  • 静态成员变量
    • 关键字static,只定义一次。所有对象之间共享。
    • 一个用途是计算个类的实例对象存在。
    • C++17开始,支持内联变量。对静态成员初始化产生两种作用:inline声明时,可直接初始化;无inline时,在类外部初始化静态变量。相比之下,内联变量方便很多,可以在头文件中初始化。
    • 类中定义了静态成员,对象的大小不改变,静态成员不是任何对象的一部分。
  • 访问静态成员变量
    • 通过对象访问或者通过类名限定符访问。
  • 静态常量
    • 静态成员往往用来定义常量。
    • C++17之前静态常量的初始化工作复杂,C++17之后引入内联常量,初始化静态常量变得简单了。
    • 规则,static和const的成员变量都声明为inline。
    • 定义这类常量来包含函数参数的边界值。
  • 类类型的静态成员变量
    • 静态成员变量不是类对象的一部分,所以它可以与类具有相同的类型。即Box类包含一个Box类型的静态常量。
    • 仍需要在类的外部进行初始化。const Box Box::refBox {10.0, 10.0, 10.0};
    • 类的静态成员变量时在创建任何对象之前创建的。类对象的任何静态和非静态的成员都能访问refBox。
    • 不能使用constexpr声明一个Box静态常量。C++11新标准规定,允许将变量声明为constexpr类型让编译器来验证变量的值是否是一个常量表达式。
  • 静态成员函数
    • 独立于类对象,没有类对象,也能调用公共静态成员函数。
    • static关键字,使用对象调用或者使用类名限定符调用。
    • 静态函数不能访问调用它的对象。为了让静态函数访问类的对象,需要把它作为实参传递给该函数。这种情况下,该函数可以访问对象的私有成员和公共成员。
    • 静态成员函数时类的一个具有完全访问权限的成员。

10. 析构函数

  • 释放类对象时,自动执行析构函数。
  • 如果析构的函数体为空,最好加上default关键字。
  • 需要明确调用析构的场景很少。
  • 用户不定义析构函数时,编译器每次给每个类添加默认析构函数。
  • 实际上编译器会默认添加的有:默认构造函数,默认副本构造函数,默认析构函数。

11. 使用指针作为类成员

  • 确保技术删除所有对象,智能指针很有帮助。
  • unique_ptr不会忘记对自由存储区分配的对象应用delete运算符。
  • 多个对象指向并使用同一个对象时,且无法判断何时全部使用完时,std::shared_ptr<>非常有用。
  • 应该使用智能指针来管理动态内存分配的对象,这种原则是RAII,资源获取即初始化。
  • delete运算符收到nullptr时,不做任何动作,比较安全。

12. 嵌套类

  • 嵌套类的名称被限定为包含类的作用域,并受到包含类的成员访问修饰符的影响。
  • 不能在外部类外部创建,使用被内部类对象。
  • 所有的Package成员都能在Truckload类内部直接访问,前提是内部类的成员都是public,不能在外部访问。
  • 嵌套类 == 内部类    包含类 == 外部类
  • 嵌套类的成员可以直接引用包含类的静态成员,类型或枚举类型。
  • 包含类的其他成员访问嵌套类需要通过类对象,类对象的指针和引用。
  • 嵌套类的成员函数访问外层类的成员时,与外层类成员函数权限相同。嵌套类成员函数可以访问外层类私有成员。
  • public修饰符的嵌套类:
    • Package定义放在Truckload公共部分。
    • 可以在外部定义嵌套类的对象: Truckload::Package  aPackage(Box);
  • 迭代器模式:
    • 问题由来:getxx()和getNextxx()允许遍历Truckload中所有的Box对象。遍历的机制是内部的一个指针。
SharedBox findLargestBox(const Truckload& truckload)
{
    SharedBox largestBox { truckolad.getFirstBox() };

    SharedBox nextBox { truckload.getNextBox() };
    while(nextBox) {
        if(nextBox->compare(*largestBox) > 0)
            largestBox = nextBox;
        nextBox = truckload.getNextBox();
    }

    return latgestBox;
}
// 存在的问题:getFirstBox() 和 getNextBox()都在更新内部的指针pCurrent,即它们必须是非const成员,但实参是引用const值的实参。
// 解决方法一是mutable,但存在缺陷,并发问题,会对同一集合使用嵌套循环。每个遍历都需要一个pCurrent指针。
// 解决方法二是为遍历Truckload而设计和创建另一个对象。这种对象叫做“迭代器”
    • 使用迭代器的类定义(部分):
class Truckload
{
private:
    class Package
    {
    public:
        SharedBox pBox;
        Package* pNext;

        Package(SharedBox pb):pBox{pb}, pNext{nullptr} {}
        ~Package() { delete pNext; }
    };

    Package* pHead {};
    Package* pTail {};

public:
    class Iterator
    {
    private:
        Package* pHead;
        Package* pCurrent;

        friend class Truckload;
        explicit Iterator(Package* head) : pHead{head}, pCurrent{nullptr} {}

    public:
        SharedBox getFirstBox();
        SharedBox getNextBox();
    };

    Iterator getIterator() const { return Iterator{phead}; }
    // other
};
    • 特点分析
      • Iterator声明为Truckload的嵌套类,故具有与Truckload成员函数相同的访问权限。这样,可以访问私有成员Package类。
      • Iterator必须是公共的,保证类外代码可以访问。
      • Iterator的主构造是私有的,外部不能创建Iterator。
      • 为了外层类能访问Iterator的私有构造,外部类声明为Iterator的友元。
SharedBox findLargestBox(const Truckload& truckload)
{
    auto iterator = truckload.getIterator();
    SharedBox largestBox { iterator.getFirst(); };

    SharedBox nextBox { iterator.getNextBox() };
    while(nextBox) {
        if (nextBox->compare(*largestBox) > 0)
            largestBox = nextBox;
        nextBox = iterator.getNextBox();
    }

    return largestBox;
}

原文地址:https://www.cnblogs.com/yangdadahome/p/15115662.html