C++ 入門学習ノート -----第 15 章: オブジェクト指向プログラミング

オブジェクト指向の章についてはあまり話さず、文法について簡単に話します。なぜなら、プログラミングは比較的大きな課題であり、より多くの問題は文法よりも設計の仕方にあるからです。

ここに画像の説明を挿入

継承:主に単一継承

每个类控制它自己的成员初始化过程,首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
我们可以将基类的指针或引用绑定到派生类对象上。
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会别拷贝、移动或赋值,派生类部分将被忽略掉。
当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用。
如果虚函数使用默认实参,则基类和派生类找那个定义的默认实参最好一致。
不能继承友元关系,每个类负责控制各自成员的访问权限。
默认派生运算符由定义派生类所用的关键字来决定。默认情况,使用class关键字定义的派生类是私有继承的;
使用struct关键字定义的派生类是公有继承的。


class Base					//基类定义所有类共同拥有的成员
{
    
    
public:						//都可访问
	Base() = default;		//默认构造函数
	Base(int m1,int m2,int m3):meb1(m1),meb2(m2),meb3(m3){
    
    }
	virtual ~Base() = default;		//一般需要给基类提供虚析构函数
	
	void fun(){
    
    };				//普通方法,直接继承
	virtual void fun1(){
    
    };	//虚函数,派生类可以覆盖
	virtual void fun2(){
    
    };	//Derived没有覆盖fun2(),和普通函数一样直接继承
	virtual void fun4(){
    
    }; 
	virtual void fun5() = 0;	//纯虚函数,无须定义,也可以在类的外部进行定义,有纯虚函数的类时抽象基类,不能实例化
	void fun6(){
    
    };
	void fun6(float f){
    
    };
	int meb1;

	static void fun3(){
    
    };	//静态成员在继承体系中只有唯一定义
protected:					//派生类可以访问(还有友元)
	int meb2;	
private:					//只有本类和友元可以访问
	int meb3;
};

class Derived;				//派生类声明,声明时不能有派生列表

class Derived : public Base	//类派生列表指出从哪个基类继承而来,可以定义各自特有的成员
{
    
    
publicDerive() = default;
	Derive(int m1,int m2,int m3,int dm1):Base(m1,m2,m3),dmeb1(dm1){
    
    }	//先初始化基类,再执行派生类初始化
	virtual void fun1() override{
    
    };	//virtual是可选的,override是C++11新标准,指示哪个成员函数改写基类的虚函数
	void fun4() final {
    
    }				//final不允许后续的其他类覆盖fun4
	void fun6(int i){
    
    }		//名字相同就会隐藏基类fun6所有重载成员,名字查找先于类型检查
	using Base::fun6;		//基类的所有fun6函数都是可见的

	int dmeb1;
protectedprivate};

class Derived2 final:public Derived{
    
    };		//添加final后,Derived2不能作为基类

通过在类的内部使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员(非私有成员)标记出来,
using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。例如:
class Derived3 : private Base	//私有继承,所有成员都是private
{
    
    
public:
	using Base::meb1;		//使用using可以改变成员的可访问性,用户可以访问meb1
protected:
	using Base::meb2;		//派生类可以访问meb2
};

finaloverride在形参列表以及尾置返回类型之后

访问静态成员:四种方式
void Test(const Derived &derivedObj)
{
    
    
	Base::fun3();			//Base定义了fun3
	Derived::fun3();		//Derived继承了fun3
	derivedObj.fun3();		//派生类对象能访问基类的静态成员
	fun3();					//通过this对象访问基类的静态成员
}

回避虚函数的机制:通常只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的机制
Derived d;
Base obj(d);
obj->Base::fun1();	//调用基类的fun1

派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:
public:保持与基类的权限一致
protected:基类的public成员在派生类中变为protected
private:基类的publicprotected成员在派生类中变为private

ここに画像の説明を挿入
ここに画像の説明を挿入
コンストラクターとコピー コントロール

之前的经验准则是如果一个类需要析构函数,它同样也需要拷贝和赋值操作。但基类的析构函数并不遵循上述准则。

*****移动操作与继承*****
如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
而且在它的派生类中也没有合成的移动操作。因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当
我们确实需要执行移动操作时应该首先在基类中进行定义。基类要使用合成的版本,必须显示地定义这些成员。
一旦基类定义了自己的移动操作,那么它必须同时显示地定义拷贝操作。
class Base
{
    
    
public:
	Base() = default;							//默认构造函数
	Base(const Base&) = default;				//拷贝构造函数
	Base(Base&&) = default;						//移动构造函数
	Base& operator=(const Base&) = default;		//拷贝赋值运算符
	Base& operator=(Base&&) = default;			//移动赋值运算符
	virtual ~Base() = default;					//虚析构函数
};

注:如果需要移动操作,那么基本上也需要拷贝操作,反之不然。
基本上有四种情况:
1.只需要虚析构函数
2.虚析构函数 + 拷贝操作(拷贝构造函数、拷贝赋值运算符)
3.虚析构函数 + 拷贝操作 + 移动操作(移动构造函数、移动赋值运算符)
4.虚析构函数 + 移动操作  : 只能移动对象,不能拷贝

派生クラスのコピー コントロール メンバー

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。
派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。
派生类赋值运算符也必须为其基类部分的成员赋值。

析构函数只负责销毁派生类自己分配的资源。对象的成员时被隐式销毁的;派生类对象的基类部分也是自动销毁的

派生クラスのコピーまたは移動コンストラクターを定義する

class Base{
    
    /*.....*/};
class D:public Base
{
    
    
public:
	//默认情况,基类的默认构造函数初始化对象的基类部分,要想使用拷贝或移动构造函数
	//我们必须在构造函数初始值列表中显示地调用该构造函数
	D(const D& d):Base(d)			//拷贝基类成员
	/*D成员的初始值*/{
    
    /*......*/}
	D(D&& d):Base(std::move(d))		//移动基类成员
	/*D成员的初始值*/{
    
    /*......*/}

	D(const D& d)					//没有显示调用基类的拷贝可能时不正确的定义
	/*D成员的初始值*/{
    
    /*......*/}
	上面的问题是,拷贝时基类部分被默认初始化,而非拷贝
};

注:在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝或移动基类部分,
则必须在派生类的构造函数初始值列表中显示地使用基类的拷贝或移动构造函数。

派生クラス代入演算子

派生类的赋值运算符与拷贝和移动构造函数一样也必须显示地位其基类部分赋值:
D& D::operator=(const D& rhs)
{
    
    
	Base::operator=(rhs);	//为基类部分赋值
	/*****安装过去的方式为派生类的成员赋值*******/
	//还需处理自赋值及释放已有资源等情况
	return *this;
}

派生クラスのデストラクタ

派生类析构函数只负责销毁有派生类自己分配的资源:对象销毁的顺序正好与其创建的顺序相反
class D:public Base
{
    
    
public:
	//Base::~Base被自动调用执行
	~D(){
    
    /*由用户定义清楚派生类成员的操作*/}
};

コンストラクタとデストラクタでの仮想関数の呼び出し

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

例如:当基类构造函数调用虚函数的派生类会发生什么?
这个虚函数可能会访问派生类的成员,毕竟,如果它不需要访问派生类成员的话,则派生类直接使用基类的虚函数
就可以了,然而,当执行基类构造函数时,它要用到的派生类成员尚未初始化,如果我们运行这样的访问,则程序
很可能会崩溃。

継承されたコンストラクタ

在C++11新标准中,派生类能够重用其直接基类定义的构造函数。这些构造函数并非以常规的方式继承而来,为了方便称其为“继承”。
一个类只能初始化它的直接基类,一个类也只继承其直接基类的构造函数。
类不能继承默认、拷贝和移动构造函数。如果派生类没有定义这些构造函数,则编译器将为派生类合成它们。

派生类继承基类构造函数的方式是提供一条注明了直接基类名的using声明语句:
class Derived:public Base
{
    
    
public:
	using Base::Base;	//继承基类的构造函数
};
通常情况下,using声明语句只是令某个名字在当前作用域内可见。而当用于构造函数时,using声明语句将令编译器产生代码。
对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。如果派生类含有自己的数据成员,会被默认初始化。

*****继承的构造函数的特点*****
和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。
例如:不管using声明出现在哪儿,基类的私有构造函数在派生类中还是一个私有构造函数;
受保护的构造函数和公有构造函数也是同样的规则。

一个using声明语句不能指定explicitconstexpr。
如果基类的构造函数时explicit或者constexpr,则继承的构造函数也拥有相同的属性。

当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别
省略掉一个含有默认实参的形参。

如果基类含有几个构造函数,则除了两个例外情况,大多数时候派生类会继承所有这些构造函数。
第一个例外是派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数
具有相同的参数列表,则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。
第二个例外是默认、拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的
构造函数来使用,因此,如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。

コンテナと継承

当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。因为不允许在容器中保存不同类型的元素,所以不能把
具有继承关系的多种类型的对象直接存放在容器中。
vector<Base> vec;
vec.push_back(Base());	
vec.push_back(Derived());	//正确,但只能把对象的Base部分拷贝给vec,只能拷贝基类的部分,派生类会被忽略掉

コンテナー内のオブジェクトの代わりに (スマート) ポインターを配置する

在容器中存放具有继承关系的对象时,实际上存放的通常是基类的指针(更好的选择是只能指针)。
vector<shared_ptr<Base>> vec;
vec.push_back(make_shared<Base>());
vec.push_back(make_shared<Deried>());	//自动把派生类的指针转换成基类的智能指针,vec中存储类型是一样的

访问元素:
cout<<vec.back()->meb1;

举例:
class Test
{
    
    
public:
	void add_item(const shared_ptr<Base> & base) {
    
     items.insert(base);}
private:
	static bool compare(const shared_ptr<Base> &lhs,const shared_ptr<Base> &rhs)
	{
    
     return lhs->meb1 < rhs->meb1; }
	
	multiset<shared_ptr<Base>,decltype(compare)*> items{
    
    compare};	//自定义比较回调:因为shared_ptr没有定义小于运算符
};

Test的用户仍然必须处理动态内存,原因是add_item需要接受一个shared_ptr参数。因此,用户不得不安装如下形式编写代码:
Test test;
test.add_item(make_shared<Base>());
test.add_item(make_shared<Derived());
我们可以重新定义add_item,使得它接受一个Base对象而非shared_ptr。
新版本的add_item将负责处理内存分配,这样它的用户就不必再受困于此了。
我们定义两个版本:
一个拷贝它给定的对象,另一个则采取移动操作。
void add_item(const Base& base);		//拷贝给定的对象
void add_item(Base&& base);				//移动给定的对象
唯一的问题是add_item不知道要分配的类型。当add_item进行内存分配时,它将拷贝(或移动)它的参数。
在某处可能会有一条如下形式的new表达式:
new Base(data);
不幸的是,这条表达式所做的工作可能是不正确的:new为我们请求的类型分配内存,一次这条表达式将分配一个Base类型的对象
并且拷贝data的Base部分。然而,data实际指向的可能是Derived对象,此时,该对象将被迫切掉一部分。

*****模拟虚拷贝*****
为了解决上述问题,我们给Base类添加一个虚函数,该函数将申请一份当前对象的拷贝。
class Base
{
    
    
public:
	virtual Base* clone() const & {
    
     return new Base(*this); }
	virtual Base* clone() && {
    
     return new Base(std::move(*this)); }
	/*其他成员*/
};

class Derived:public Base
{
    
    
	Derived* clone() const & {
    
     return new Derived(*this); }
	Derived* clone() && {
    
     return new Derived(std::move(*this)); }
	/*其他成员*/
};

因为我们拥有add_item的拷贝和移动版本,所以我们分别定义clone的左值和右值版本

使用clone很容易地写出新版本的add_item:
class Test
{
    
    
publci: 	//add_item也提供左值和右值两个重载版本
	void add_item(const Base& data){
    
    items.insert(shared_ptr<Base>(data.clone()));}
	void add_item(Base&& data){
    
    items.insert(shared_ptr<Base>(std::move(data).clone()));}
};
在上面的第二个的右值版本中,尽管data的类型是右值引用类型,但实际上data本身(和任何其他变量一样)是个左值,
因此,我们调用move把一个右值引用绑定到data上。

具体执行哪个clone,根据data的动态类型决定运行Base的函数还是Derived的函数。

後ろにテキストクエリプログラムの例もあります。この例は非常に優れており、設計方法を理解してからコードを入力してください

おすすめ

転載: blog.csdn.net/weixin_41155760/article/details/126100427