frankfan的胡思乱想

学海无涯,回头是岸

c++面向对象基本

默认构造 抽象类 多继承

本章讲授的主要内容依然围绕C++中的面向对象世界观的构建,其中牵涉到构造函数、抽象类与多重继承的概念。

默认构造

编译器生成的构造函数,被称为默认构造函数,该构造函数不带参数。

C++中类的构造分为不带参的构造函数与带参构造函数,带参构造函数需要用户自己定义并实现,而不带参的构造函数并不一定需要用户自己实现,在『某些』情况下,编译器会为类生成默认的无参构造函数。

也就是说,若没有手动实现类的构造函数,那么该类有可能存在默认的构造函数,有可能没有默认的构造函数。

那么,编译器是否生成默认构造函数的原则有哪些呢?

总的原则:若用户手动实现了构造函数,无论其是否带参,编译器都不再生成默认构造函数

编译器是倾向于不给用户默认实现构造函数的,但是,当满足以下3条的任意一条情况时,编译器会生成默认构造函数

1、父类已经实现不带参构造函数

2、类中存在虚函数

3、类中存在对象成员,且该成员存在无参构造(无论是自定义还是默认)

#include <iostream>
using namespace std;

class Node{
public:
  Node(int v):value(v){}
private:
  int value;
};

int main(){
  
  //Node node;编译报错,因为Node类自定义了构造函数,编译器不会再生成默认的构造函数,因此无法通过不带参构造函数创建对象(因为Node并没有不带参的构造函数)
  return 0 ;
}

父类已经实现不带参构造函数时,当子类没有实现构造函数式,编译器会生成默认构造函数

#include <iostream>
using namespace std;

class Node{
public:
  Node(){}
};

class TTNode:public Node{
};
  
int main(){
  TTNode ttnode;//可以使用不带参构造函数,尽管用户没有实现不带参的构造函数,但是编译器生成了默认构造函数
  return 0;
}

父类已经实现带参构造函数时,无论如何子类都不会生成默认构造函数

#include <iostream>
using namespace std;

class Node{
public:
  Node(int v){}
};

class TTNode:public Node{
};

int main(){
  
  //TTNode ttnode;编译报错,父类实现了带参构造函数后,编译器并不会给子类生成默认构造函数
  return 0;
}

当类中存在虚函数时,无论虚函数是通过继承父类而来还是原本就定义在本类中,编译器都会尝试生成默认构造函数

#include <iostream>
using namespace std;
class Node{
public:
  virtual void showValue(){}
};
int main(){
  
  Node node;//类中定义了虚函数,但是又没有现实构造函数,编译器会为Node类生成默认构造函数
  return 0;
}
#include <iostream>
#include "Node.h"
using namespace std;
class TTNode:public Node{
public:
  void showValue(){}
};
int main(){
  TTNode ttnode;//同样会生成默认的构造函数,从父类继承了虚函数
  return 0;
}

当类中存其他类的成员对象时,编译器也会为本类生成默认构造函数

#include <iostream>
using namespace std;
class Node{

};

class TTNode{
public:
  Node node;
};

int main(){
  
  TTNode ttnode;//编译器会为TTNode创建默认的构造函数,因为其成员对象Node类存在无参构造函数(无论其是否是编译器生成还是自定义)
  return 0;
}

抽象类、纯虚函数

抽象类的本质是定义接口

面向接口编程也是一种区别于面向对象编程的一种编程范式。当然,接口本身也在面向对象编程中有大量运用,既能规范接口调用又能增加灵活性

所谓抽象类,就是类中存在纯虚函数的类

#include <iostream>
using namespace std;

class NodeInterface{
public:
  //通过特殊语法定义纯虚函数
  virtual void showvalue() = 0;
};

class TTNode:public NodeInterface{
public:
  void showvalue(){
    cout<<"ttnode show value"<<endl;
  }
}; 

int main(){
  
  //NodeInterface nodeinterface;编译报错,含有纯虚函数的类被称为抽象类。抽象类无法创建对象,仅作为接口规范存在
  
  TTNode ttnode;
  ttnode.showvalue();//ttnode show value
  
  NodeInterface *node = new Node;
  node->showvalue();//ttnode show value
  
  //子类必须重写其所继承的抽象类的所有纯虚函数,否则,子类本身也会称为抽象类,无法被实例化
  return 0;
}

纯虚函数没有自己的实现,只是提供函数签名与接口,所有的实现都交由其子类,而子类又必须实现其父类的纯虚函数

既然是接口,那么就可以有多个接口,并且一个类可以同时实现多个接口,这就涉及到多重继承的问题

#include <iostream>
using namespace std;

class NodeInterface{
public:
  virtual void showNodeValue() = 0;
};

class NodeActionInterface{
public:
  virtual void doAction() = 0;
};

class NodeItem:public NodeInterface,public NodeActionInterface{
  
public:
  void showNodeValue(){
    //
  }
  
  void doAction(){
    //
  }
};

int main(){
  
  NodeItem item;
  item.showNodeValue();
  item.doAction();
  return 0;
}

一个类可以同时继承多个父类,这就是多重继承。多重继承的最常用场合就是继承虚基类,实现虚函数接口。

当然,也可以从面相对象编程的角度上去多重继承,但是这种方式极不推荐,不仅存在各种隐藏的问题,同时也会使得类与类之间存在混乱的关系。

#include <iostream>
using namespace std;
class A{
public:
  int value;
};
class B:public A{};
class C:public A{};
class D:public B,public C{};

int main(){
  
  //这就是多重继承中非常容易出现的『菱形继承』,他的问题在于当类B类C同时存在同名的成员名以及同名的成员函数时,子类D该用哪个父类的成员函数与成员,这样就出现了二义性,这是多重继承无法避免的问题。
  
  //为了解决这个问题,C++提出了『虚继承』的概念,专门用来解决多重继承中可能出现的这个问题
  D d;
 	//d.value = 11;编译报错
  return 0;
}

用虚继承解决承菱形继承中可能出现的问题

#include <iostream>
using namespace std;
class A{
public:
  int value;
};
class B:virtual public A{};//虚继承
class C:virtual public A{};
class D:public B,public C{};

int main(){
  
	//使用虚继承解决菱形继承中成员的二义性问题
  D d;
 	d.value = 11;
  return 0;
}

此外,多重继承与普通继承在内存机制上并没有不同。

最先构造的是祖父A,然后是父类B,然后是父类C,最后是自己D。

多个父类的构造顺序取决于源码中的继承顺序(从左往右)

关于父类指针与子类指针的互转问题

父类指针指向子类对象,无疑是安全的。

因为父类指针的接口能够访问到的成员范围小于等于子类,因此无论如何都不会越界。

而当子类指针指向父类时,则有可能存在访问越界的风险,因为子类的成员范围有可能大于父类,因此当通过子类指针接口进行访问时,极有可能导致过度偏移造成访问越界。

#include <iostream>
using namespace std;

class Node{
public:
  int value;
};

class TTNode:public Node{
public:
  int tid;
};

int main(){
  
  Node *pnode = new TTNode;
  pnode->value = 11;//安全的,通过Node指针能够访问到的对象只有value,其偏移为0
  
  //不安全的,TTNode能访问的范围比Node大,当通过pnode对象访问成员tid时,偏移为4,此时已经超出了Node对象的内存范围,越界访问导致出错
  TTNode *pnode = (TTNode*)new Node;//WARING
  
  return 0;
}

总之,不要尝试使用子类指针访问父类对象,除非你明确知道自己的行为

posted on 2021-12-28 00:23  shadow_fan  阅读(23)  评论(0编辑  收藏  举报

导航