C++知识整理

导读:本篇文章讲解 C++知识整理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目录

类CLASS

类的介绍

特性

类的三大特性

类与结构体的区别

派生与继承

抽象类

示例

 总结

静态成员

public,private,protected以及friendly的区别

override,overload以及overwrite的区别

覆盖(override)

重载(overload)

重写(overwrite)

浅拷贝和深拷贝的区别

虚函数、纯虚函数

介绍

虚函数 vs 纯虚函数,如何选用?

Constexpr和Const关键字的区别(C++11)

Static关键字

不考虑类,static的作用

类中static的作用

抽象类和接口

1. 抽象类与接口是面向对象思想层面概念,不是程序设计语言层面概念

2. 抽象类是本体的抽象,接口是行为的抽象

参考资料


类CLASS

类的介绍

        类是面向对象语言程序设计中的概念,是面向对象编程的基础。

        类的实质是一种引用数据类型,类似于 byteshortint(char)、longfloatdouble 等基本数据类型,不同的是它是一种复杂的数据类型。因为它的本质是数据类型,而不是数据,所以不存在于内存中,不能被直接操作,只有被实例化为对象时,才会变得可操作。

        类是对现实生活中一类具有共同特征的事物的抽象。如果一个程序里提供的数据类型与应用中的概念有直接的对应,这个程序就会更容易理解,也更容易修改。一组经过很好选择的用户定义的类会使程序更简洁。此外,它还能使各种形式的代码分析更容易进行。特别地,它还会使编译器有可能检查对象的非法使用。 [2]

        类的内部封装属性方法,用于操作自身的成员。类是对某种对象的定义,具有行为behavior),它描述一个对象能够做什么以及做的方法(method),它们是可以对这个对象进行操作的程序和过程。它包含有关对象行为方式的信息,包括它的名称属性方法事件

        类的构成包括成员属性和成员方法(数据成员和成员函数)。数据成员对应类的属性,类的数据成员也是一种数据类型,并不需要分配内存。成员函数则用于操作类的各项属性,是一个类具有的特有的操作,比如“学生”可以“上课”,而“水果”则不能。类和外界发生交互的操作称为接口

特性

类的三大特性

  1. 封装性将数据和操作封装为一个有机的整体,由于类中私有成员都是隐藏的,只向外部提供有限的接口,所以能够保证内部的高内聚性和与外部的低耦合性。用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员,能够增强安全性和简化编程。

  2. 继承性更符合认知规律,使程序更易于理解,同时节省不必要的重复代码。

  3. 多态性是指同一操作作用于不同对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向父类(基类)的指针,来调用实现子类(派生类)中的方法。

类与结构体的区别

        在 C++C# 语言中,class 和 struct 都可以定义一个类,它们的区别如下:

  1. 从职能观点来看,class 表现为行为;而 struct 常用于存储数据。

  2. C# 中,class 是引用类型,继承自 System.Object 类;struct 是值类型,继承自 System.ValueType 类,不具多态性。但是注意,System.ValueType 是个引用类型。

  3. class 支持继承,可以继承自类和接口;而 struct 没有继承性,struct 不能从 class 继承,也不能作为 class 的基类,但 struct 支持接口继承

  4. class 可以声明无参构造函数,可以声明有参构造函数;而 struct 只能声明带参数构造函数,且不能声明无参构造函数。因此,struct 没有自定义的默认无参构造函数,默认无参构造函数只是简单地把所有值初始化为它们的 0 等价值。

  5. Java / C# 中,实例化时,class 要使用 new 关键字;而 struct 可以不使用 new 关键字,如果不以 new 来实例化 struct,则其所有的字段将处于未分配状态,直到所有字段完成初始化,否则引用未赋值的字段会导致编译错误。

  6. class 可实现抽象类abstract),可以声明抽象方法(抽象函数);而 struct 为抽象,也不能声明抽象函数

  7. class 可以声明 protected 成员、virtual 成员、sealed 成员和 override 成员;而 struct 不可以,但是值得注意的是,struct 可以重载 System.Object 的 3 个虚方法,equals( )、toString( ) 和 getHashTable( )。

  8. class 的对象复制分为浅拷贝深拷贝,必须经过特别的方法来完成复制;而 struct 创建的对象复制简单,可以直接以等号连接即可。

  9. class 实例由垃圾回收机制来保证内存的回收处理;而 struct 变量使用完后立即自动解除内存分配

  10. 作为参数传递时,class 变量是以按址方式传递;而 struct 变量是以按值方式传递的。

        我们可以简单的理解,class 是一个可以动的机器,有行为,有多态,有继承;而 struct 就是个零件箱,组合了不同结构的零件。其实,class 和 struct 最本质的区别就在于 class 是引用类型,内存分配于托管堆;而 struct 是值类型,内存分配于线程的堆栈上。由此差异,导致了上述所有的不同点。所以只有深刻的理解内存分配的相关内容,才能更好的驾驭。

        当然,使用 class 基本可以替代 struct 的任何场合,class 后来居上。虽然在某些方面 struct 有性能方面的优势,但是在面向对象编程里,基本是 class 横行的天下。

        那么,有人不免会提出,既然 class 几乎可以完全替代 struct 来实现所有的功能,那么 struct 还有存在的必要吗……答案是,至少在以下情况下,鉴于性能上的考虑,我们应该考虑使用 struct 来代替 class:

  1. 实现一个主要用于存储数据的结构时,可以考虑 struct。

  2. struct 变量占有堆栈的空间,因此只适用于数据量相对小的场合。

  3. struct 数组具有更高的效率。

派生与继承

class子类类名:
public父类类名,private父类类名,protected父类类名
{
public:公有成员

private:私有成员

protected:保护成员

};
  1. 子类即是继承而来的类,父类即是被继承的类,或者称之为基类

  2. public 修饰的为公有继承,private 修饰的为私有继承protected 修饰的为保护继承。

  3. 父类可以只有一个,也可以有多个。只有一个父类称为单继承,多个父类称为多继承。C++ 支持多继承的机制,Java 则只具有单继承功能,并增加了 “ 接口 ” 的概念,一个类可以实现多个接口。

  4. 如果不标明继承方式,默认为私有继承

        派生和继承是类的重要特性,继承是由抽象到具体的体现。通过继承,子类可以使用父类的成员。

        但要注意的是,派生和继承在带来方便的同时,也会使类与类之间的关系变得复杂,尤其是涉及到私有继承和保护继承时,类中成员的关系可能会变得难以理解。所以在涉及类时,尽量避免过多层次的继承,私有继承和保护继承的使用也要慎重。

        继承来的成员和自身声明的成员并无本质区别,也有公有成员、私有成员、保护成员之分。继承时,父类中成员类型(公有成员 / 私有成员 / 保护成员)和继承方式(公有继承 / 私有继承 / 保护继承)不同,情况不同。可以归纳为:

        三种类型的继承,父类的成员均被子类继承(之前的百科关于这点的描述是错误的),只是由类实例化的对象对其继承的成员的访问权限会有所变化。三种不同方式的继承,描述的是子类实例化对象对其成员的访问权限,并非是描述子类时,子类对继承自父类的成员的访问权限。

  1. 公有继承继承自父类的成员保持不变。

  2. 私有继承继承自父类的成员全部变为私有成员。

  3. 保护继承继承自父类的公有成员变为保护成员,其余不变。

抽象类

        并不是所有种类的事物都可以被实例化,换而言之,有的种类只是一种抽象概念,现实中并没有实际存在的对应物。比如:假设所有的动物都会叫,我们可以定义一个类——“ 动物 ”,定义类中的一个成员函数——“ 叫 ”,我们知道猫的叫声,也知道狗的叫声,然而 “ 动物 ” 是如何 “ 叫 ” 的——我们根本无法实现它。所以,我们引入了抽象类的概念,抽象类是无法被实例化的,无法声明抽象类的对象。

        通常,用 abstract 修饰的类是抽象类;C++ 中包含纯虚函数的类是抽象类;Java 中含有抽象方法的类是抽象类;继承了纯虚函数而没有实现它的类也是抽象类。

        抽象类只能被用作基类,作用体现于:

  1. 约束派生类必须实现的成员函数或方法。

  2. 不同派生类中同名的成员函数实现不同,体现了多态性

示例

1、虚函数 
        在基类中将一个函数声明为虚函数,使该函数具有虚属性,那么其所有派生函数中该函数的重写都具备了虚属性,也就使得基类指针可以调用派生类实例中继承自该基类的所有成员函数,且若有重写,调用的都是重写后的函数。

2、纯虚函数
        声明纯虚函数可使当前类变成抽象类,禁止该类被实例化,并要求其非抽象类的派生类必须实现该函数。

        下面展示虚函数和纯虚函数的代码示例,注意观察注释内容:

class Base
{
public:
	virtual void print() = 0;//纯虚函数,可以有函数体,可实例化的派生类必须重写它
	virtual void play() {//虚函数,有函数体,若派生类没有重写它,就原样继承下来
		cout << "Base play!" << endl;
	}
	virtual ~Base() = 0;//纯虚析构函数,必须要有函数体
	//virtual ~Base(){}	//很多情况下,基类的虚函数都会有函数体,将析构函数声明为纯虚函数是一种抽象化基类、不许其实例化的一种方法。
};
void Base::print() {
    //被声明为纯虚函数,仍可以有函数体
    //但派生类若想实例化,必须重写纯虚函数,派生类中可以调用基类有函数体的纯虚函数(Base::print())
	cout << "Base print!" << endl;
}
Base::~Base(){
    //被声明为纯虚析构函数,根据C++类的规则,因为其派生类析构函数会调用基类的析构函数,所以必须有函数体,否则无法析构基类

 总结

1、纯虚函数对类最大的束缚就是:类中一旦出现纯虚函数,就不可实例化了;

2、纯虚函数还是可以有函数体,只不过最终还是要被派生类重写,在派生类的函数中可以调用基类中有函数体的纯虚函数;

3、为使派生类能完全释放资源,基类析构函数必须声明为虚函数,否则,在用基类指针new一个派生类对象后,delete该指针就只能回收与基类相关的资源,造成内存泄漏;

4、纯虚析构函数必须要有函数体,根据C++析构函数的调用规则,派生类会调用基类析构函数,如果基类析构函数没有函数体会造成函数调用失败而报错,这是纯虚析构函数与普通纯虚函数不同之处。

静态成员

        用static修饰的成员是静态成员,可以是成员函数或数据成员。静态成员是所有对象共有的,只分配一次内存,产生新的对象时不会产生副本。

        成员的初始化必须在类外进行,使用静态成员时必须使用类名和域操作符

public,private,protected以及friendly的区别

作用域 类内部 本包 子类 外部包
public
protected ×
friendly(default) × ×
private × × ×

override,overload以及overwrite的区别

覆盖(override)

继承了父类的同名无參函数:当子类从父类继承一个无參方法,而又定义了一个同样的无参数的方法时,则子类新写的方法覆盖父类的方法,成为覆盖。

class Super{
    public void NoParametersMethod(){
        System.out.println("Super Method....");
    }
}

class subClass extends Super {
    public void NoParametersMethod(){
        System.out.println("subClacc Method....");
    }   
}

class override{
    public static void main(String[] args) {
        subClass sub = new subClass();
        sub.NoParametersMethod();
    }
}
output:
subClacc Method....

重载(overload)

继承了父类的同名有參函数:当子类继承了父类的一个同名参数,且方法参数表不同,则称为重载,通过方法重载,子类可以重新实现父类的某些方法,使其具有自己的特征。

class Super{
    public void WithParametersMethod(int a){
        System.out.println("Super Method:"+a);
    }
}

class subClass extends Super {
    public void WithParametersMethod(int a,int b){
        System.out.println("subClacc Method:"+a+","+b);
    }   
}

class overload{
    public static void main(String[] args) {
        subClass sub = new subClass();
        sub.WithParametersMethod(0);
        sub.WithParametersMethod(1,2);
    }
}
output:
Super Method:0
subClacc Method:1,2

重写(overwrite)

当前类的同名方法:通过方法重写,一个类中可以有多个具有相同名字的方法,由传递给他们的不同个数和类型的参数,甚至不同参数顺序(不推荐)来决定使用哪种方法

class Curr{
    public void fun(int i) {
        System.out.println("int");
    }
    public void fun(int i,String s){
        System.out.println("int,String");
    }
    public void fun(String s,int i){
        System.out.println("String,int");
    }
}

class override{
    public static void main(String[] args) {
        Curr curr = new Curr();
        curr.fun(0);
        curr.fun(0, "");
        curr.fun("", 0);
    }
}
output:
int
int,String
String,int

浅拷贝和深拷贝的区别

        浅拷贝只是一种简单的拷贝,让几个对象公用一个内存。拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。然而当内存销毁的时候,指向这个内存空间的所有指针需要重新定义,不然会造成野指针错误。

        深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

        深拷贝和浅拷贝其实真正的区别在于是否还需申请新的内存空间。

        举个例子:你想一套房子。潜拷贝指的是你爸就去配了把钥匙给你,你拥有了一套房。深拷贝指的是你爸重新买了套房给你,里面的布置也全和原来那间做的一模一样,这时候有两间一模一样的房子。

虚函数、纯虚函数

介绍

        多态(polymorphism)是面向对象编程语言的一大特点,而虚函数是实现多态的机制。其核心理念就是通过基类访问派生类定义的函数。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的。使用一个基类类型的指针或者引用,来指向子类对象,进而调用由子类复写的个性化的虚函数,这是C++实现多态性的一个最经典的场景。

  • 虚函数,在类成员方法的声明(不是定义)语句前加“virtual”, 如 virtual void func()
  • 纯虚函数,在虚函数后加“=0”,如 virtual void func()=0
  • 对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为称之为复写Override。
  • 对于纯虚函数,子类必须提供纯虚函数的个性化实现。

        在派生子类中对虚函数和纯虚函数的个性化实现,都体现了“多态”特性。但区别是:

  • 子类如果不提供虚函数的实现,将会自动调用基类的缺省虚函数实现,作为备选方案;
  • 子类如果不提供纯虚函数的实现,编译将会失败。尽管在基类中可以给出纯虚函数的实现,但无法通过指向子类对象的基类类型指针来调用该纯虚函数,也即不能作为子类相应纯虚函数的备选方案。(纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路。)

虚函数 vs 纯虚函数,如何选用?

  1. 当基类中的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供缺省备选方案的时候,该方法应该设计为虚函数。
  2. 当基类中的某个成员方法,必须由子类提供个性化实现的时候,应该设计为纯虚函数。

Constexpr和Const关键字的区别(C++11)

        对于修饰Object来说,

        const并未区分出编译期常量和运行期常量;

        constexpr限定在了编译期常量;

        但对修饰函数来说,constexpr修饰的函数,返回值不一定是编译期常量。

#include <iostream>
#include <array>
using namespace std;

constexpr int foo(int i)
{
    return i + 5;
}

int main()
{
    int i = 10;
    std::array<int, foo(5)> arr; // OK
    
    foo(i); // Call is Ok
    
    // But...
    std::array<int, foo(i)> arr1; // Error

    // sizeof(ia) / sizeof(*ia) 求ia中元素个数
    constexpr size_t sz = sizeof(ia) / sizeof(*ia);
    int arr[sz]; //OK
   
}

        所以,对于constexpr需要两方面看待。

        constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr的条件,可以尽量加上constexpr。

        而检测constexpr函数是否产生编译时期值的方法很简单,就是利用std::array需要编译期常值才能编译通过的小技巧。这样的话,即可检测你所写的函数是否真的产生编译期常值了。

Static关键字

不考虑类,static的作用

1、对其他文件隐藏

当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都有全局可见性。同时编译两个源文件:a.cpp和main.cpp,在a.cpp中有以下内容:

#include<cstdio>

char a='A';         //全局变量
void msg() printf("Hello\n");

                则在a.cpp中定义的全局变量a和函数msg能在main.cpp中使用。这是因为未加static前缀的全局变量都具有全局可见性,其他源文件也能访问。若加了static,就会对其他源文件隐藏了。如在a和msg的定义前加上static,main.cpp就不能访问了。

        这样可以在不同的文件中定义同名函数和同名变量,而不用担心命名冲突。

        这个也可以通过namespace实现。

2、static的第二作用是默认初始化为0

        包括未初始化的全局静态变量和局部静态变量。另外未初始化的全局变量也具备之一属性,因为为初始化的全局变量与未初始化的静态变量存储在同一区域内(BSS,全局(静态)存储区,BSS的特点是在程序执行之前BSS会自动清0)

3、static的第三个作用是保持局部变量内容的持久

        函数内的局部变量,当调用时就存在,退出函数时就销毁,但静态局部变量虽然在函数内定义,但静态局部变量始终存在,也就是说它的生存期为整个源程序,其特点是只进行一次初始化且具有“记忆性”。

        值得注意的是:虽然局部静态变量的生存周期为整个源程序,但其作用域仍与局部变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量嗨继续存在,但不能使用它了。

类中static的作用

        有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联,我们可以通过在成员的声明之前加上关键字static使得其与类关联在一起。即,表示属于一个类而不是属于此类的任何特定对象的变量和函数。

        类的静态成员存在任何对象之外,对象中不包含任何与静态成员函数有关的数据。和其他成员一样,静态成员可以是public的或private的。什么意思了?下面结合例子我们分静态数据成员和静态成员函数来说

class Account
{
public:
    void calculate() {amount+=amount*interestRate;}
    static double rate() {return interestRate;}
    static void rate(double);
private:
    std::string owner;
    double amount;
    static double interestRate;
    static double initRate();
}

1、静态数据成员

        通常,非static数据成员存在与类类型的每个对象中,而static数据成员独立与该类的任意对象而存在,即每个static数据成员是与类关联的对象,并不是与该类的对象相关联,通俗讲,静态数据成员不属于类的任一对象。也就是说,当某个类的实例修改了该静态成员变量,其修改值为该类的其他所有实例所见。

        因为静态数据成员不属于类的任一对象,所以它们并不是创建类对象时被定义,这意味着不是由类的构造函数初始化。一般来说,static数据成员要在类定义体的外部定义(如对应的cpp中),和其他对象一样,一个静态数据成员只能定义一次。另外,静态数据成语定义在任何函数之外,因为一旦它被定义,就将一直存在于程序的整个生命周期。然而我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。

2、静态成员函数

        因为普通的成员函数总是具体的属于某个类的具体对象,所以普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身。但是静态成员函数由于不予任何的对象相关联,因此它不具有this指针。因而无法访问类对象的非静态数据成员,也无法访问非静态成员函数,只能调用其余的静态数据成员与访问静态成员函数。另外,因为static成员不是任何对象的组成部分,所以static成员函数不能被声明为const。

        静态类成员函数既可以定义在类的内部可以在内的外部,当定义在类的外部时,不能重复使用static关键字,该关键字只能出现在类内部的声明语句中。另外,static成员函数也不能被声明为虚函数、volatile。

1)静态函数的主要目的是为了方便调用,他与类对象无关,因此不需要生成对象就可以使用,可以用来封装一些算法,比如写一个Math类,包含算法sin(),使用静态函数就可以直接Math::sin()调用,否则需要先Math math;math.sin();麻烦且需要为一个纯算法增加构造和析构过程显然是不合理的。

2)静态成员函数不需要实例化就可以被调用,不会也不可以调用或操纵非静态成员。静态成员之间可以相互访问,非静态成员函数可以任意地访问静态成员函数和静态数据成员。

3)它跟类的对象无关,只跟类有关,不需要this指针,可以用来封装系统API的回调函数,通常这些函数也是没有this指针的。

抽象类和接口

1. 抽象类与接口是面向对象思想层面概念,不是程序设计语言层面概念

        如若想正确认识抽象类与接口,首先要弄清楚的一点是,这两个概念均属于面向对象思想层面,而不属于某种程序设计语言。例如,C#中用interface关键字声明的语言元素,我们叫它“接口”,其实这是不准确的,准确来说,这应该叫做“接口在C#语言中的实现机制”。

        面向对象思想包含许多概念,而不同面向对象语言对这些概念的具体实现机制各有不同。例如,C++中并没有一种关键字对应于C#中的interface,那么C++中就没有接口的概念了吗?非也!在C++中,如果想定义一个接口,可以通过将一个类中所有方法定义为纯虚方法[①]来做到。

        这里可以看到,同样是接口,C#中用interface关键字来定义,而C++通过创建一个只包含纯虚方法的类来定义,这就是同一种概念在不同具体语言中具有不同的实现机制。类似的,C++中也没有abstract关键字用于定义抽象类,而是如果一个类中至少含有一个纯虚方法且它的方法不全为纯虚方法,则这个类被称为抽象类。

        通过上面的分析可以看出,如果仅仅停留在语言层面去认知抽象类与接口,是无法准确理解两者的真谛的,因为不同语言对同一概念的实现机制有很大差别。如果一个C#初学者简单将两者理解为“用abstract修饰的类是抽象类,用interface定义的语言元素是接口”,那么当他接触C++时一定会感到困惑,因为C++里既没有abstract也没有interface,而是通过类中纯虚方法的情况确定这是个类、是个抽象类还是个接口。

        明确了上面的问题,我们就可以给出抽象类与接口的真正定义了。

抽象类是不能实例化的类,但是其中的方法可以包含具体实现代码。

接口是一组方法声明的集合,其中应仅包含方法的声明,不能有任何实现代码。

以上对抽象类和接口的定义与任何具体语言无关,而是从面向对象思想角度进行的定义,不同语言可以有不同的实现机制。

从上面的定义中,我们可以发现两者在思想层面上的一项重大区别:抽象类是类(Class),接口是集合(Set,两者从本质上不是一种东西。

2. 抽象类是本体的抽象,接口是行为的抽象

        在开始这一节之前,我想先请问各位一个问题,“我是一个人”和“我能呼吸”分别表达了“我”和“人”以及“我”和“呼吸”的关系,那么这两句话表达的是一种关系吗?如果你能很容易区分前者表示“是一个”的关系,而后者表示“能”的关系,那么恭喜你,你一定也能很容易区分抽象类和接口。

        在阅读这一节时,请读者务必谨记上面这个问题以及下面这句话:

        抽象类表示“是一个(IS-A关系的抽象,接口表示能(CAN-DO关系的抽象。

        从上面粗体字中我们可以看出,抽象类和接口有一个共性——它们都是“某种关系的抽象”,只不过类型不同罢了。其实如果将上面那句话的前半句中的“抽象类”改为“类”也是正确的,这并不奇怪,上文我们说过,抽象类只不过是一种特殊的类罢了。

        下面我们先来解释IS-A关系。其实英语中的IS-A关系在汉语中可以解释为两种情况,当IS-A用在一个对象和一个类之间时,意思是“这个对象是类的一个实例”,例如关羽是一个对象,我们可以说“GuanYu IS-A General”,其中General(将军)是个类,这表示关羽是将军类的一个实例。而当IS-A用在两个类之间时,我认为叫做IS-A-KIND-OF更为准确,表示汉语中的“是一种”,如“General IS-A Person”,表示将军这个类是人这个类的一种,换用面向对象术语可以如下表述:General是Person的子类(Sub Type),Person是General的父类或超类(Super Type),General继承自Person。

        这后一种IS-A关系,就是抽象类所表达的关系。分析到这里可以看出,抽象类所表达的关系其实就是面向对象三大特性之一——继承(Inheritance),也就是说,抽象类所表达的关系,与一般类与类之间的继承并无区别,而抽象类相比普通类,除了不能实例化外,也并无区别。之所以出现抽象类,是因为在较高抽象层次上,某些方法(往往是纯虚方法)无法实现,必须由其子类按照各自不同的情况具体实现。因为它含有纯虚方法,所以将这种类实例化在道理上讲不通,但我们又希望将这些子类中共有的部分抽象出来减少代码重复,于是就有了抽象类——它包含可复用部分,但又不允许实例化。

        因此,抽象类的使用动机是在不允许实例化的限制下复用代码。请牢记这个动机。

        接着再说说接口和CAN-DO关系。

        我们知道,面向对象编程的基本思想就是通过对象间的相互协作,完成程序的功能。具体来说,在面向对象编程中,要求每个类都隐藏内部细节(这叫封装性),仅对外暴露一组公共方法,对象间就通过互相调用彼此的公共方法完成程序功能。

        可以看到,面向对象思想中,对象和对象间根本不需要了解,调用者甚至可以完全不知道被调用者是谁,只要知道被调用者“能干什么”就行了。这就如同拨打110报警一样,你根本不知道对方长什么样、穿什么衣服、结没结婚、有没有孩子,你也不知道对方在哪,对象是谁,但是你知道对方一定“能接警”,所以你可以顺利完成报警。

        这种“能干什么”就是CAN-DO关系,当我们把这种CAN-DO关系抽象出来,形成一个CAN-DO关系的集合,这就是接口了。那么使用接口的动机又是什么呢?动机之一是松散耦合。我们知道“低耦合”是面向对象程序设计中一个重要原则,而很大一部分耦合就是调用关系,面向对象中术语叫“依赖”。如果没有接口,调用者就要紧依赖于被调用者,就如同在没有110报警的年代,你只认识一个接警员,不知道其他接警员的电话,那么当你报警时,你必须给这个接警员打电话才行,如果哪天这个接警员休假或病了,你就无法报警了,除非你再去认识一个接警员。这时,我们说你紧依赖于这个接警员,也叫紧耦合。但有了110报警后就不一样了,我们将“可接警”看作一个接口,接口中有一个方法“接警”,而拨通110后,电话那头的人一定是实现了这个接口的,这时报警人不再依赖于具体接警员,而是依赖于“可接警”接口,这就叫做松依赖。

        所以说,接口又可以看作一组规则的集合,它是对调用者的保证,对被调用者的约束。如上例中,可接警对报警人(调用者)保证调用对象可接警,同时约束接警部门必须把一个实现了这个接口的人安排在接警电话前面。哪怕这是个机器人或刚进行了两个小时接警培训的保洁员都没关系。

        使用接口的另一个动机就是实现多态性

        下面想象你被分配到一个全新的研发小组做主管,第一天上班的早晨,一群人站在你面前等着你训话,你完全不认识他们,也不知道他们各自的职务,但是你可以说一句“都去工作吧”,于是大家作鸟兽散,程序员去写程序,会计去核对账目,业务员出门联系客户……当你这样做的时候,你就利用接口实现了多态性。因为你知道,他们都实现了“可工作”这个接口,虽然各个人员对“工作”具体的实现不一样,但这不要紧,你只要调用他们的“工作”方法,他们就各自做自己的事情了。如果你不能面向接口去利用多态性,你就要一个个说:“程序员去写程序,会计去核账,业务员快出门联系客户……”,这实在非常的费劲。

        对这一节的内容做一个总结:

        抽象类表示“是一个(IS-A关系的抽象,它抽象了类的本体,其使用动机是在不允许实例化的限制下复用代码。接口表示能(CAN-DO关系的抽象,它抽象了类的行为,其使用动机是松散对象间的耦合以及实现程序多态性。

        经过上面的分析,我想你已经可以很容易在抽象类与接口间做出选择了。如果你是为了将一系列类的公共代码抽出,减少代码的重复,并且这些类与抽象出来的类可以表述为IS-A关系,就用抽象类;如果你是为了将一个或一组行为抽象出来,用以松散对象间耦合或实现多态性,那就用接口吧。

参考资料

类(编程术语)_百度百科 (baidu.com)

public、private、protected、Friendly的区别与作用域,以及不写时的区别?_公众号:算法攻城狮-CSDN博客

方法的覆盖(override)、重载(overload)和重写(overwrite) – 佳佳牛 – 博客园 (cnblogs.com)

C++中static用法 – 王大咩的图书馆 – 博客园 (cnblogs.com)

[转]抽象类与接口的区别及应用 – zjp – 博客园 (cnblogs.com)

C++ 纯虚函数_过百的博客-CSDN博客_纯虚函数

C++ const 和 constexpr 的区别? – 知乎 (zhihu.com)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100792.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!