C++ part two

面向对象与面向过程

  • 面向对象和面向过程是一个相对的概念。
  • 面向过程是按照计算机的工作逻辑来编码的方式,最典型的面向过程的语言就是c语言了,c语言直接对应汇编,汇编又对应电路。
  • 面向对象则是按照人类的思维来编码的一种方式,C++就完全支持面向对象功能,可以按照人类的思维来处理问题
    • 举个例子,要把大象装冰箱,按照人类的思路自然是分三步,打开冰箱,将大象装进去,关上冰箱。要实现这三步,我们就要首先有人,冰箱这两个对象。人有给冰箱发指令的能力,冰箱有能够接受指令并打开或关闭门的能力。
    • 但是从计算机的角度讲,计算机只能定义一个叫做人和冰箱的结构体。人有手这个部位,冰箱有门这个部位。然后从天而降一个函数,是这个函数让手打开了冰箱,又是另一个函数让大象进去,再是另一个函数让冰箱门关上。
    • 从开发者的角度讲,面向对象显然更利于程序设计。用面向过程的开发方式,程序一旦大了,各种从天而降的函数会非常繁琐,一些用纯c写的大型程序,实际上也是模拟了面向对象的方式。
    • 那么,如何用面向过程的c语言模拟出面向对象的能力呢?类就诞生了,在类中可以定义专属于类的函数,让类有了自己的动作。回到那个例子,人的类有了让冰箱开门的能力,冰箱有了让人打开的能力,不再需要天降神秘力量了。

构造函数、析构函数

  • 构造函数:C++提供的必须有的在对象创建时初始化对象的方法
  • 析构函数:当类对象被销毁时,就会调用析构函数。栈上对象的被销毁时机就是函数栈销毁时。堆上的对象销毁时机就是该堆内存被手动释放时,如果用new申请的这块堆内存,那调用delete销毁这块内存时就会调用析构函数。

this 常成员函数与常对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Car {
public:
int m_price; // 成员变量
void SetPrice(int p) // 成员函数
{
m_price = p;
}
};

int main()
{
Car car;
car.SetPrice(20000); // 给car对象m_price成员变量赋值

return 0;
}

  • c++需要转化成c语言执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 结构体Car
    struct Car
    {
    // price变量是属于Car结构体这个域里的变量
    int price;
    };


    // 参数1:结构体Car的指针
    // 参数2:要设置的价格变量
    void SetPrice(struct Car* this, int p)
    {
    this->price = p; // 将传入的Car结构体的price变量赋值
    }

  • 静态成员函数是不能使用 this 指针,因为静态成员函数相当于是共享的变量,不属于某个对象的变量。

  • 常成员函数就是无法修改成员变量的函数。可以理解为将this指针指向对象用const修饰的函数。

  • 常对象就是用const修饰的对象

inline,mutable,default,delete

  • inline关键字
    • 在函数声明或定义中函数返回类型前加上关键字inline就可以把函数指定为内联函数。关键字inline必须与函数定义放在一起才能使函数成为内联,仅仅将inline放在函数声明前不起任何作用。
    • 内联函数的作用,普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,于是C++提供了内联函数将函数体放到需要调用函数的地方,用空间换效率。
    • inline关键字只是一个建议,开发者建议编译器将成员函数当做内联函数,一般适合搞内联的情况编译器都会采纳建议。
    • 使用inline关键字就是一种提高效率,但加大编译后文件大小的方式,现在随着硬件性能的提高,inline关键字用的越来越少了。
  • mutable关键字
    • Mutable意为可变的,与const相对,被mutable修饰的成员变量,永远处于可变的状态,即便处于一个常函数中,该变量也可以被更改。
    • 这个关键字在现代C++中使用情况并不多,一般来说只有在统计函数调用次数时才会用到
    • mutable关键字是一种没有办法的办法,设计时应该尽量避免,只有在统计函数调用次数这类情况下才推荐使用。这个关键字也称不上是重点。
  • default关键字
    • default关键字的作用很简单。在编译时不会生成默认构造函数时便于书写。也可以对默认复制构造函数,默认的赋值运算符和默认的析构函数使用,表示使用的是系统默认提供的函数,这样可以使代码更加明显。
    • 现代C++中,哪怕没有构造函数,也推荐将构造函数用default关键字标记,可以让代码看起来更加直观,方便。
  • delete关键字
    • C++会为程序生成默认构造函数,默认复制构造函数,默认重载赋值运算符。在很多情况下,我们并不希望这些默认的函数被生成,在C++11以前,只能有将此函数声明为私有函数或是将函数只声明不定义两种方式。
    • C++11于是提供了delete关键字,只要在函数最后加上“=delete”就可以明确告诉编译器不要默认生成该函数。

友元函数

  • 友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #include <iostream>
    using namespace std;
    class Box
    {
    private:
    double width;
    public:
    friend void printWidth( Box box );
    void setWidth( double wid );
    };

    // 成员函数定义
    void Box::setWidth( double wid )
    {
    width = wid;
    }

    // 请注意:printWidth() 不是任何类的成员函数
    void printWidth( Box box )
    {
    /* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
    cout << "Width of box : " << box.width <<endl;
    }

    // 程序的主函数
    int main( )
    {
    Box box;

    // 使用成员函数设置宽度
    box.setWidth(10.0);

    // 使用友元函数输出宽度
    printWidth( box );
    return 0;
    }

    Width of box : 10

重载运算符

  • 很多时候我们想让类对象也能像基础类型的对象一样进行作基础操作,比如“+”,“-”,“ * ”,“”,也可以使用某些运算符“=”,“()”,“[]”,“<<”,“>>”。但是一般的类即使编译器可以识别这些运算符,类对象也无法对这些运算符做出应对,我们必须对类对象定义处理这些运算符的方式。
  • C++提供了定义这些行为的方式,就是“operator 运算符”来定义运算符的行为,operator是一个关键字,告诉编译器我要重载运算符了。
  • C++重载运算符不能改变运算符的元数,“元数”这个概念就是指一个运算符对应的对象数量,比如“+”必须为“a + b”,也就是说“+”必须有两个对象,那么“+”就是二元运算符。比如“++”运算符,必须写为“a++”,也就是一元运算符。
  • 重载运算符/不可重载运算符(分两类,可以上网看看)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class test
    {
    friend ostream& opeartor<<(ostream& os,Test& test);
    friend istream& opeartor>>(istream& is,Test& test);
    public:
    void operator++()
    {
    ++count;
    }

    void operator--()
    {
    --count;
    }

    int operator[](int i)
    {
    return a[i];
    }

    void operator()()
    {
    count << "hello,world"<<endl;
    }
    Test opeartor*(Test& test)
    {
    count *= test.count
    return *this
    }

    private:
    int cout = 0;
    vector<int> a = {1,2,3,4,5};
    string name;
    };

    friend ostream& opeartor<<(ostream& os,Test& test)
    {
    os<< test.name << endl;
    return os;
    }
    friend istream& opeartor<<(ostream& is,Test& test)
    {
    os<< test.name << endl;
    return is;
    }

继承

  • C++子类对象的构造过程:
  • 先调用父类的构造函数,再调用子类的构造函数,也就是说先初始化父类的成员,再初始化子类的成员。
  • 若父类没有默认的构造函数,子类的构造函数又未调用父类的构造函数,则无法编译。
  • C++子类对象的析构过程。先调用父类的析构函数,再调用子类的析构函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #include <iostream>
    using namespace std;
    // 基类
    class Shape
    {
    public:
    void setWidth(int w)
    {
    width = w;
    }
    void setHeight(int h)
    {
    height = h;
    }
    protected:
    int width;
    int height;
    };

    // 派生类
    class Rectangle: public Shape
    {
    public:
    int getArea()
    {
    return (width * height);
    }
    };

    int main(void)
    {
    Rectangle Rect;

    Rect.setWidth(5);
    Rect.setHeight(7);

    // 输出对象的面积
    cout << "Total area: " << Rect.getArea() << endl;

    return 0;
    }

  • 一个派生类继承了所有的基类方法,但下列情况除外:
    • 基类的构造函数、析构函数和拷贝构造函数。
    • 基类的重载运算符。
    • 基类的友元函数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      //多继承
      #include <iostream>

      using namespace std;

      // 基类 Shape
      class Shape
      {
      public:
      void setWidth(int w)
      {
      width = w;
      }
      void setHeight(int h)
      {
      height = h;
      }
      protected:
      int width;
      int height;
      };

      // 基类 PaintCost
      class PaintCost
      {
      public:
      int getCost(int area)
      {
      return area * 70;
      }
      };

      // 派生类
      class Rectangle: public Shape, public PaintCost
      {
      public:
      int getArea()
      {
      return (width * height);
      }
      };

      int main(void)
      {
      Rectangle Rect;
      int area;

      Rect.setWidth(5);
      Rect.setHeight(7);

      area = Rect.getArea();

      // 输出对象的面积
      cout << "Total area: " << Rect.getArea() << endl;

      // 输出总花费
      cout << "Total paint cost: $" << Rect.getCost(area) << endl;

      return 0;
      }

多态

  • 多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。

  • 虚函数:通过在基类中使用 virtual 关键字声明虚函数,派生类可以重写这个函数,从而使得在运行时根据对象类型调用正确的函数。

  • 虚函数是在基类中使用关键字 virtual 声明的函数。虚函数允许子类重写它,从而在运行时通过基类指针或引用调用子类的重写版本,实现动态绑定。

  • 纯虚函数是没有实现的虚函数,在基类中用 = 0 来声明。纯虚函数表示基类定义了一个接口,但具体实现由派生类负责。纯虚函数使得基类变为抽象类(abstract class),无法实例化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    #include <iostream>
    using namespace std;

    // 基类 Animal
    class Animal {
    public:
    // 虚函数 sound,为不同的动物发声提供接口
    virtual void sound() const {
    cout << "Animal makes a sound" << endl;
    }

    // 虚析构函数确保子类对象被正确析构
    virtual ~Animal() {
    cout << "Animal destroyed" << endl;
    }
    };

    // 派生类 Dog,继承自 Animal
    class Dog : public Animal {
    public:
    // 重写 sound 方法
    void sound() const override {
    cout << "Dog barks" << endl;
    }

    ~Dog() {
    cout << "Dog destroyed" << endl;
    }
    };

    // 派生类 Cat,继承自 Animal
    class Cat : public Animal {
    public:
    // 重写 sound 方法
    void sound() const override {
    cout << "Cat meows" << endl;
    }

    ~Cat() {
    cout << "Cat destroyed" << endl;
    }
    };

    // 测试多态
    int main() {
    Animal* animalPtr; // 基类指针

    // 创建 Dog 对象,并指向 Animal 指针
    animalPtr = new Dog();
    animalPtr->sound(); // 调用 Dog 的 sound 方法
    delete animalPtr; // 释放内存,调用 Dog 和 Animal 的析构函数

    // 创建 Cat 对象,并指向 Animal 指针
    animalPtr = new Cat();
    animalPtr->sound(); // 调用 Cat 的 sound 方法
    delete animalPtr; // 释放内存,调用 Cat 和 Animal 的析构函数

    return 0;
    }

    结果
    Dog barks
    Dog destroyed
    Animal destroyed
    Cat meows
    Cat destroyed
    Animal destroyed

  • 当父类指针指向子类对象,且子类重写父类某一函数时。父类指针调用该函数,就会产生以下的可能

    • 该函数为虚函数:父类指针调用的是子类的成员函数。
    • 该函数不是虚函数:父类指针调用的是父类的成员函数。
  • 动态绑定和静态绑定:

    • 静态绑定:程序在编译时就已经确定了函数的地址,比如非虚函数就是静态绑定。
    • 动态绑定:程序在编译时确定的是程序寻找函数地址的方法,只有在程序运行时才可以真正确定程序的地址,比如虚函数就是动态绑定。
  • 虚函数是如何实现动态绑定的呢?

    • 每个有虚函数的类都会有一个虚函数表,对象其实就是指向虚函数表的指针,编译时编译器只告诉了程序会在运行时查找虚函数表的对应函数。
  • C++编程三剑客:模板、多态与泛型编程的交织与差异(可以看看)

RTII

移动语义

右值引用的出现恰好解决了传统 C++没有区分『移动』和『拷贝』的概念的问题。 例如下面这段代码使用右值引用避免无意义拷贝以提升性能。

1
2
3
4
5
6
std::vector<string> cmds;
std::string s;
while (std::cin >> s) {
cmds.emplace_back(std::move(s));
s.clear();
}
- 默认移动构造函数和默认移动赋值运算符会默认生成移动构造函数和移动赋值运算符的条件: - 只有一个类没有定义任何自己版本的拷贝操作(拷贝构造,拷贝赋值运算符),且类的每个非静态成员都可以移动,系统才能为我们合成。 - 可以移动的意思就是可以就行移动构造,移动赋值。所有的基础类型都是可以移动的,有移动语义的类也是可以移动的。