侯捷C++面向对象编程(上)课程打卡 Day4

8、 静态 模板 namespace

8.1 static

对于非静态的函数和数据:

非静态的成员函数通过this指针来处理不同的数据(一份函数–>多个对象)

img

image-20230731154548833

对于静态的函数和数据:

静态函数没有this,不能处理一般的数据,只能处理静态的数据。

例1:

class Account
{
public:
	static double m_rate;  //静态变量的声明
	static void set_rate(const double& x) { m_rate = x; } //静态函数
};
double Account::m_rate = 0; //静态变量的定义 一定要有

int main()
{
    //调用静态函数法1——by class name
	Account::set_rate(5.0);
    //调用静态函数法2——by object
	Account a;
	a.set_rate(7.0); //静态函数与a无关/无this
}

例2:设计模式 Singleton(单体)

image-20230731163117925

  • 构造函数放在private中,外界无法调用
  • 设计了getInstance静态函数,来生成并返回唯一的一份
8.2 template

可以参考菜鸟教程:

https://www.runoob.com/cplusplus/cpp-templates.html


模板是泛型编程的基础,泛型编程以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或者公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

8.2.1 函数模板

模板函数的定义的一般形式如下:

template <typename type> ret-type funcname(parameter list)
{
    //函数主体
}

在这里,type是函数所使用的数据类型的占位符名称。该名称可以在函数定义中使用。

接下来给出实例,返回两个数的最大值:

#include <iostream>
#include <string>
 
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
    return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World
8.2.2 类模板

正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:

template <class type> class class-name {
.
.
.
}

在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
 
using namespace std;
 
template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
 
template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 
 
template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 
 
template <class T>
T Stack<T>::top () const 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
    // 返回最后一个元素的副本 
    return elems.back();      
} 
 
int main() 
{ 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 
 
        // 操作 int 类型的栈 
        intStack.push(7); 
        cout << intStack.top() <<endl; 
 
        // 操作 string 类型的栈 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}

当上面的代码被编译和执行时,它会产生下列结果:

7
hello
Exception: Stack<>::pop(): empty stack
8.3 namespace

命名空间被用于作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

定义命名空间

命名空间的定义使用关键字namespace,后跟命名空间的名称。如下图所示:

namespace namespace_name{
//代码声明
}

为了调节带有命名空间的函数或变量,需要在前面加上命名空间的名称。如下图所示:

name::code;//code可以是变量或者函数

using 指令

可以使用using namespace指令,这样在使用命名空间的时候就可以不用在前面加上命名空间的名称。该指令会告诉编译器,后续的代码使用指定的命名空间的名称。

using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分,您可以使用如下的语句:

using std::cout;

随后的代码中,在使用 cout 时就可以不用加上命名空间名称作为前缀,但是 std 命名空间中的其他项目仍然需要加上命名空间名称作为前缀

using 指令引入的名称遵循正常的范围规则。名称从使用 using 指令开始是可见的,直到该范围结束。此时,在范围以外定义的同名实体是隐藏的。

不连续的命名空间

命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。

所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:

namespace namespace_name {   // 代码声明 }

嵌套的命名空间

命名空间可以嵌套,您可以在一个命名空间中定义另一个命名空间.

9、复合 委托

9.1 Composition 复合

类似于C语言中的结构里面可以有结构—class里面也可以有class

image-20230801093748678

deque 是一个已经存在的功能很多的类(两头进出的队列);利用deque的功能来实现queue的多种操作

该例只是复合的一种情况——设计模式 Adapter

9.1.1 复合下的构造和析构

image-20230801095529359

  • 构造是由内而外

    Container 的构造函数,编译器会自动先调用 Component 的 default 构造函数,再执行自己

    注意如果要调用 Component 的其他构造函数需要自己写出来Container::Container(): Component() {  };
    
  • 析构是由外而内

    Container 的析构函数会先执行自己,之后编译器调用 Component 的析构函数

9.2 Delegation 委托

委托就是 Composition by reference;即通过指针把任务委托给另一个类

img

复合中,内部和外部是一起出现的;而委托是不同步的

这是一个著名的设计模式——pimpl (pointer to implementation) 或者叫 “编译防火墙”

  • 右边怎么变动都不会影响左边

  • reference counting 多个指针共享一个 “Hello”;但当a要改变内容时, 系统会单独复制一份出来给a来改,b和c依然在共享

image-20230801101907977

10 继承与多态

10.1 Inheritance 继承

面向对象程序设计的最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序更加容易。也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。

代码如下:

// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};

//派生类
class Dog : public Animal {
    // bark() 函数
};
10.1.1 基类 & 派生类

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

class derived-class: access-specifier base-class

其中,访问修饰符 access-specifier 是 public、protectedprivate 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。

10.1.2 访问控制和继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

我们可以根据访问权限总结出不同的访问类型,如下所示:

访问publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。
10.1.3 继承类型

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。

我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

如下图所示:

基类 public 成员基类 protected 成员基类 private 成员
public 继承public 成员protected 成员不能访问
protected 继承protected 成员protected 成员不能访问
private 继承private 成员private 成员不能访问
10.1.4 多继承

多继承即一个子类可以有多个父类,它继承了多个父类的特性。

C++ 类可以从多个类继承成员,语法如下:

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,
{
<派生类类体>
};

其中,访问修饰符继承方式是 public、protectedprivate 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,如上所示。

示例:

#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;
}

当上面的代码被编译和执行时,它会产生下列结果:

Total area: 35
Total paint cost: $2450
10.2 多态与虚函数
10.2.1 多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

如下例:

#include <iostream> 
using namespace std;
 
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};
class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }
};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }
};
// 程序的主函数
int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
 
   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();
 
   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();
   
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Parent class area :
Parent class area :

导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

修改后,当编译和执行前面的实例代码时,它会产生以下结果:

Rectangle class area :
Triangle class area :

此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

10.2.2 虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

  • pure virtual 函数:

    derived class 一定要重新定义 (override 覆写) 它;其没有定义只有声明

    语法:virtual xxxxxx =0;

    = 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

  • virtual 函数:

    derived class 可以重新定义 (override, 覆写) 它,且它已有默认定义

    语法:virtual xxxxxx;

  • non-virtual 函数:

    不希望 derived class 重新定义 (override, 覆写) 它