C++设计模式由浅入深(一)—— 继承和多态

2023-10-30

一、继承和多态

C++首先是一个面向对象的语言,对象是C++程序的基石。通过类的继承和派生,软件工程师可以自由表达对软件系统中各个部分之间的关系与交互逻辑,定义各个组件之间的接口和实现,有秩序地组织起数据结构和代码。本书的目的不是为了教授C++语言,本章的要义是为了帮助读者理解C++语言特性中与类和继承相关的知识,有助于后续章节的展开。因此,我不会事无巨细地描述C++中类及其相关工具的具体用法,而是会介绍贯彻本书中的基础概念和语言结构。

本章我们将要讨论:

  • C++中的类是什么?它在C++语言中扮演什么样的角色?
  • 类的继承是什么?C++是如何利用继承的?
  • 什么是运行时多态?以及如何在C++中使用它。

1 类和对象

面向对象程序设计是一种把数据结构和算法有机结合起来的程序构建方式,其中算法运行于被称为“对象”的单一实体之上。大多数面向对象程序语言,包括C++,都是基于类的。类是用来定义对象的,它描述了对象的算法、数据结构、表现形式以及与其他类之间的关系。一个“对象”是类的具体实例,换言之,是一个变量。对象拥有地址,即其在内存中占有一席之地。类是一种用户定义的类型。总的来说,任意数量的对象可以通过类所提供的定义实例化(也有例外,某些类可能会限制对象的数量,但这不是常态)。

在C++中,类中包含的数据被组织成为类的数据成员,由不同类型的变量组成。算法,则由类的方法,也就是各种成员函数来实现。尽管从语言上来讲,没有要求类的数据成员必须与其提供的算法相关,但是作为一个良好的设计习惯,数据应当被妥善地封装在类的内部,同时类的方法提供给外部对于类内数据的有限访问。

封装的概念是C++中类的核心,语言标准允许我们通过将成员设置为public使其对外暴露,或者设置为private将它们隐藏。一个优秀的设计通常有且仅有private数据成员,仅通过public方法对外提供数据访问的接口。这些public接口就像是一个合约,类的设计者允诺这个类将提供某些操作和功能。类的private成员及方法则作为实现的一部分,并且只要public接口有效,可以任意修改。举个例子,下面的类表达了一个有理数,并且支持增量操作,其暴露出的public接口如:

class Rational {
public:
    Rational& operator+=(const Rational& rhs);
};

一个良好设计的类不会通过public接口暴露非必要的实现细节,因为实现的细节不是“合约”的一部分,不过接口的文档有时倒是会提醒你接口会有哪些限制罢了。比方说,如果我们承诺所有的有理数中,分子和分母不存在公倍数,那么当两个有理数相加时,需要一个约分的步骤。这个步骤最好用private方法来实现它,因为他可能还会调用其他的细节操作。因此类的客户不需要去执行这些复杂的操作,因为在这个方法的结果返回给调用者时,private方法已经完成了约分:

class Rational {
public:
	Rational& operator+=(const Rational& rhs);
private:
	long n_; // 分子
	long d_; // 分母
	void reduce();
};
Rational& Rational::operator+=(const Rational& rhs) {
	n_ = n_*rhs.d_ + rhs.n_*d_;
	d_ = d_*rhs.d_;
	reduce();
    return *this;
}
Rational a, b;
a += b;

类方法拥有数据成员的特殊访问权限,因此可以直接访问private数据成员。注意这里的operator+=是类的成员函数,它是由a对象调用并作用于自身的。如果一个成员函数通过“直呼其名”引用了一个数据成员,那么它访问的就是调用这个函数的对象中的那个数据成员,可以想象在这里编译器自动给我们显式地添加了this->n_this->d_。访问另一个对象的的成员则需要通过指向另一个对象的指针或者引用来进行,不过对于非成员函数而言,这样的访问可能会收到访问权限控制的限制。

顺便一提,C++也支持C风格的结构体struct。但是在C++中,struct并不仅仅局限为一个数据的聚合体,它可以有成员方法,private和public访问权限控制,以及任何作为类可以有的属性。从语言的层面来看,类和对象的区别仅仅在于默认访问权限。在类(class)中,所有的成员默认是private访问权限,而struct是public访问权限。除此之外,对于struct或者class的选择更多的是一种习惯或者约定俗成,通常我们用struct来表示C风格的结构体,它的表现能够和C的结构体一样好。例如,仅有构造函数的struct。当然,这样的界定并不准确,也取决于不同的项目或者团队的编码风格。

除了我们上面看到的数据成员和成员方法以外,C++的类也支持static数据成员和static成员方法。static成员方法非常接近与非成员函数,它不会由类的任何对象调用,仅能通过它的参数访问类的对象。然而,与非成员函数不同的是,类的static成员方法保留了对类的private成员的访问特权。

2 继承和派生

在C++中,类的派生有两个主要的目的。一方面,它允许我们表达对象间的关系;另一方面,它允许我们通过许多更简单的类型来构建复杂的类型。这两种目的都是通过类的继承达成的。

继承是C++中使用类和对象的核心概念。继承允许我们扩展已有类来定义新的类。当一个类派生自另一个类的时候,某些情况下,可以包含父类的所有数据成员和算法,并且添加它自己的算法和数据成员。在C++中,区分private继承和public继承非常重要。

Public继承方式继承了类的公有接口,并且继承了所有实现——基类的数据成员也会成为派生类的一部分。接口的继承是区分public继承和private继承的标志,因为公有继承会将基类的共有接口作为派生类共有接口的一部分对外暴露,就像上面讲过的合约一样。

通过public继承自基类,并且遵循基类接口的限制条件,派生类就可以将其绑定在与基类相同的合约上,同时可以对合约进行适当的扩展和增补。由于public继承的派生类遵循了合约的规范,因此所有能用到基类的地方都可以用派生类来代替,但是这样就无法用到派生类对于基类的扩展特性了。

这个特点通常被称为“is-a”法则,一个派生类的实例也是一个基类的实例。但是我们在C++中对于“is-a”关系的阐释不能全凭直觉。例如,正方形是一种矩形嘛?如果它是,那么我们就可以尝试从Rectangle类中派生出Square类:

class Rectangle {
public:
	double Length() const { return length_; }
	double Width() const { return width_; }
	//...
private:
	double l_;
	double w_;
};
class Square : public Rectangle {
	//...
};

但是我们马上就能发现有点不对头,派生类的正方形拥有两个数据成员,而现实中的正方形并不需要,因为只需要知道一条边长即可确定一个正方形。这个看起来并不是很糟糕,但是考虑一下如果我们在矩形类的“合约中”提供了任意缩放的功能:

class Rectangle {
public:
	void Scale(double sl, double sw) { // 对长和宽按不同比例缩放
		length_ *= sl;
		width_ *= sw;
	}
//...
};

现在回头看,通过public继承而来的正方形也具有了这样的任意缩放性质。事实上,通过public继承,我们相当于承诺在能用矩形的地方都可以用正方形代替。很显然,这样的承诺条件并不能够被满足。当我们派生类的客户准备调整一个正方形的比例时,这种操作显然不合法。也许我们可以忽略这个调用,或者抛出运行时异常。但不论如何,我们都违背了“合约”的精神。对于C++来说,这种情况只有一种解决方案,那就是:正方形不是矩形。反之亦然,我们无法通过正方形能够提供的接口来派生出合法的矩形。

类似地,在C++中,如果鸟类具有飞行的接口,那么企鹅也不能算作鸟类。正确的设计应该是设计一个更抽象的Bird类,它不会保证提供飞或者不飞的接口。然后我们再创建中间的基类,如FlyingBirdFlightLessBird,这样我们就能正常地为Eagle或者Penguin这样的类实现派生。最重要的是,在C++中对于企鹅算不算鸟类,取决于我们如何定义鸟,或者说,从C++语言的角度上讲,取决于鸟类的公有接口是如何。

由于public继承隐含了“is-a”的关系,语言层面就允许我们对引用和指针进行宽泛地转化。首先,派生类的指针是可以隐式转换为基类指针的,对于引用也是如此:

class Base { ... };
class Derived : public Base { ... };
Derived* d = new Derived;
Base* b = d; // 隐式转换

这样的转换总是合法的,因为派生类的实例(public继承)一定是基类的实例。反之则不一定,如果要转换必须显式转换:

Base* b = new Derived; // *b 是指向派生类的
Derived* d = b; // 无法通过编译
Derived* d = static_cast<Derived*>(b); // 显式强制转换

这样的转换无法通过隐式方式进行的原因是,如果要让这样的转换行为合法,那么基类的指针必须真实地指向一个派生类对象,否则这将会是未定义行为。作为程序员,必须显式地断言,使用static_cast,从逻辑上表示出程序员对于当前进行的转换是有意而为之并且确认合法的。如果不确定这样的转换是否合法,我们也有其他的途径可以尝试,下一小节将会介绍这样的转换(dynamic_cast)。

C++的另一种类型的继承则是private继承。当我们声明一个private继承时,派生类并不会对基类的公有接口进行扩展,相反,所有基类的方法都变成派生类的私有方法了。派生类的所有public接口,都要从零开始编写。这样一来,派生类的对象就不能顺理成章替代基类原有的位置了。派生类所能得到的则是基类的所有内部实现,即算法和数据成员。这种继承方式通常来说也被成为“has-a”模式,也就是派生类中“拥有”一个基类的实例。

对于私有继承的派生类而言,基类之于派生类的关系就如同数据成员之于类的关系。这种实现技巧被称为组合方式,一个对象由多个其他的对象组成,这些其他对象作为它的数据成员而存在。通常如果没有特别的其他原因,类的组合是比私有继承更最合适的方式。那么,还有什么其他的理由会用到私有继承呢?还是有一些可能性的。首先,在派生类中,可以通过using声明将一些基类的public成员方法重新暴露出去:

class Container : private std::vector<int> {
public:
	using std::vector<int>::size;
	//...
};

在某些罕见场景下这种用法可能会有奇效,但这也等价于类组合中使用inline的转发函数:

class Container {
private:
	std::vector<int> v_;
public:
	size_t size() const { return v_.size(); }
//...
};

其次,指向派生类的指针或引用可以被转换为基类指针或引用,但仅在派生类的成员函数中可用。到目前为止,我们还没看到使用私有继承的充足理由。当然,通常来说我们建议使用组合的方式设计类。但是接下来的两个理由却非常充分,任意一个都可以被作为使用私有继承的动机。

其中之一就是当我们对于对象大小比较敏感的时候,我们可以通过私有继承的方式缩小对象的尺寸。仅拥有方法而没有数据成员的基类并不罕见,这样的类没有数据成员,因此他也不应该占据空间。但是在C++中,这种类必须拥有非0的大小。因为语言要求任意两个不同的对象必须由不同且唯一的地址。一个典型的例子就是,当我们先后依次声明两个变量,那么第二个变量的地址就是第一个变量的地址加上第一个变量的大小:

int x; // 地址为 0xffff0000, sizeof x 为 4
int y; // 地址为 0xffff0004

为了避免处理0大小对象的难题,C++中一个空的对象至少需要占据1个字节的大小。如果一个类中拥有这样的空对象,他也会占据至少1个字节的空间(由于对第二个数据成员的布局字节填充可能会导致这个值变大)。这种情况是对内存的浪费,因为这个空间完全没有被任何东西使用。一方面,如果一个空类被当作基类,对象中的基类成分不要求为非0值。对于派生类而言,整个对象的大小必须非0。这种情况下,派生类对象的地址,派生类中基类对象的地址以及派生类首个数据成员的地址三者可以为同一地址。因此,在C++中不给空基类分配内存其实是合法的,尽管sizeof这个空基类的大小是1字节。因此,大多数现代编译器都会做如下的优化:

class Empty {
public:
	void useful_function();
};
class Derived : private Empty {
    int i;
}; // sizeof(Derived) == 4 私有继承仅占据4字节
class Composed {
	int i;
	Empty e;
}; // sizeof(Composed) == 8 类组合占据8字节

如果应用场景中需要创造大量这样的派生类对象,那么通过私有继承方式可以节省下大量的内存空间。另一个使用私有继承的原因可能和虚函数有关,我们将在下一节中介绍。

3 多态与虚函数

我们讨论public继承时提到了,派生类可以在任何期望可以用到基类的地方做替换。即使存在这个要求,知晓实际对象的真实类型也是很有用的:

Derived d;
Base& b = d;
//...
b.some_method(); // b其实是派生类

some_method()是基类提供的public接口的一部分,因此在派生类中也必须有效。但是在基类接口提供的合约的灵活性规范内,我们可以对它进行适当的修改。例如,之前我们做过对飞行类鸟类的类型派生设计。假设FlyingBird类提供了fly()接口,每一种派生自该类的鸟类都同样具有这样的接口。但是,老鹰和秃鹫飞行的方式略有不同,也就是说,老鹰和秃鹫对fly()的实现可以有差异。任何对于任意FlyingBird类调用fly()接口的代码可能会得到不同的效果:

class FlyingBird : public Bird {
public:
	virtual void fly(double speed, double direction) {
		//... 某个飞行速度,高度等 ...
	}
    //...
};

派生类继承了这个成员函数的声明和实现,如果这个实现满足了派生类的要求,那么就无需改动。但如果派生类需要修改实现,它可以通过覆写基类的实现来达成:

class Vulture : public FlyingBird {
public:
	virtual void fly(double speed, double direction) {
    //... move the bird but accumulate exhaustion if too fast ...
	}
};

当一个虚函数被调用的时候,C++的运行时系统就会判定对象的真实类型。通常,这类信息在编译时不可知,而必须在运行时决定:

void hunt(FlyingBird& b) {
b.fly(...); // Could be Vulture or Eagle
	...
};
Eagle e;
hunt(e); // hunt参数b的真实类型是Eagle, FlyingBird::fly() 被调用
Vulture v;
hunt(v); // hunt参数b的真实类型是Vulture, Vulture::fly() 被调用

这种在多个基类对象上调用同样方法但其运行结果不同的编程技巧,称为“运行时多态”。在C++中,对象要具有多态性质必须至少拥有一个虚函数,并且只有提供了虚函数的接口才能实现多态的特性。

显然,对于虚函数接口的声明和其派生类对其进行的覆写必须具有同样的形式,程序员调用了基类中的方法,但是实际运行了派生类中的版本。这种情况当且仅当基类和派生类对同一个虚函数拥有完全一致的名称,返回值和参数列表(有一个例外,当虚函数返回基类的指针或者引用时,派生类覆写虚函数的时候可以返回派生类的指针或引用)。

关于多态派生的一个非常常见的场景就是基类没有对虚函数的默认实现。例如,所有的鸟类都可以飞,但是他们有不同的速度,因此对于鸟类的基类来说,没有必要为他们设置一个默认的速度。在C++中,我们可以在基类中拒绝为虚函数提供实现。这样的函数我们称他为纯虚函数,任何包含纯虚函数的类都被称为抽象类:

class FlyingBirt {
public:
	virtual void fly(...) = 0; // 纯虚函数
};

抽象类仅仅定义了接口,实现接口的任务就落在了派生类的身上。如果一个派生类的父类包含了纯虚函数,那么任何在此派生链上的类都必须对纯虚函数进行实现。换言之,拥有纯虚函数的类无法被实例化。然而,我们可以拥有指向真实派生类对象的基类指针或引用。

C++语法中有一些小技巧,当我们覆写虚函数的时候,不要求明确写出virtual关键字。如果基类声明了一个虚函数,那么子类的同名、同参数的函数就自动具备了虚函数的属性,并且会覆盖基类的实现。注意,如果派生类中的同名函数参数列表与基类同名虚函数不同,那么这个函数不会覆盖任何东西,但是会遮蔽(shadow)基类中的同名函数。当程序员意图覆盖基类虚函数但没有抄对声明的时候,可能会导致很难发现的bug:

class Eagle : public FlyingBird {
public:
	virtual void fly(int speed, double direction);
};

这里的参数类型与基类中不相同。尽管Eagle中的fly也是虚函数,但是它并没有覆写基类的虚函数。而由于基类中的fly()是纯虚函数,这个bug可以被发现,因为纯虚函数没有被实现。然而,如果基类提供了fly()的实现,那么编译器就无法发现这个隐秘的bug。好在C++11中提供了override关键字,它可以极大地方便我们甄别出这类隐秘的bug,任何意图覆写基类的函数中都可以在声名时加入override关键词:

class Eagle : public FlyingBird {
public:
	void fly(int speed, double direction) override;
};

这里的virtual关键字是可选的,但是如果基类中不存在名为fly的虚函数,那么这样的代码也会触发编译错误。

虚函数的最常见用法就是通过公有继承进行派生。由于所有派生类对象都是基类对象(is-a关系),一个程序可以同时操作一堆派生自统一基类的不同派生类的集合,就好像他们是同一个类型一样。虚函数的覆写保证了对每个对象的处理都能按照正确的方式进行:

void MakeLoudBoom(std::vector<FlyingBird*> birds) {
	for (auto bird : birds) {
		bird->fly(...); // Same action, different results
	}
}

不过,即便是私有继承也可以利用到虚函数的特性。只不过它的用法比较晦涩,甚至比较罕见。毕竟,通过私有继承的派生类不能通过其基类指针对其访问(被指涉的私有基类是不可访问基类,此时将派生类指针转换为基类的操作会失败)。然而, 在一种场景下这种转换是允许的,就是在派生类的成员函数中。这就是通过私有继承基类的方法调用派生类方法的方式(感觉很别扭):

class Base {
public:
	virtual void f() { std::cout << "Base::f()" << std::endl; }
	void g() { f(); }
};
class Derived : private Base {
	virtual void f() { std::cout << "Derived::f()" << std::endl; }
	void h() { g(); }
};
Derived d;
d.h(); // 打印 Derived::f()

由于基类的public接口在派生类中是private属性,我们不能直接调用它。但我们可以通过派生类的另一个方法,如h()去调用基类中的接口。如果我们直接在h()中调用f()那么毫无疑问它调用的肯定是派生类的f()。相反,我们通过h()调用了基类中的g(),并让g()去调用基类中的f()。此时,我们可以看到,派生类中的虚函数f()正确地覆写了基类中的虚函数f(),在这个上下文中,我们通过h()调用到的基类f()会正确地调用派生类覆写后的版本。

前面的小节我们讨论过,类的组合通常比私有继承更好用,这里就是第二个使用私有继承的原因。当我们确实需要用到虚函数的多态性质,但又不想暴露public接口,那么只能使用私有继承来实现。

当我们不确定基类指针指涉对象的真实类型时,使用static_cast是危险的。但是我们可以利用dynamic_cast来帮助判断。当我们用尝试转换一个基类指针到派生类指针时,如果转换的类型正确,那么我们就可以得到一个正确的派生类指针;否则,dynamic_cast就会返回一个空指针:

class Base { ... };
class Derived : public Base { ... };
Base* b1 = new Derived; // 真的派生类
Base* b2 = new Base; // 不是派生类
Derived* d1 = dynamic_cast<Derived*>(b1); // 成功转换
Derived* d2 = dynamic_cast<Derived*>(b2); // 转换失败 d2 == nullptr

需要注意的是,当dynamic_cast作用在引用身上时,由于不存在所谓的“空引用”,如果转换失败,dynamic_cast就会抛出异常,通常是std::bad_cast

到目前为止,我们把自己局限在了一个基类的情况下。这样其实也有助于我们思考类的继承和派生关系,就好比一棵树的许多枝叶很根系。下面我们将要讨论多重继承的情况。

4 多重继承

在C++中,一个类可以从多个基类中派生而来。回到我们上面提到的鸟类,可以观察到一个问题,尽管可飞行的鸟类之间有许多相似之处,但是它们也与其他的可飞行动物由相似之处,即“可以飞行”的属性。由于飞行的属性并不局限于鸟类,我们也许希望把处理飞行相关的数据结构和算法移动到单独的基类之中。毫无疑问的是,老鹰是属于鸟类的,因此为了表达这种关系,我们可以使用两个基类来构造老鹰类:

class Eagle : public Bird, public FlyingAnimal {...};

在这个例子中,从两个基类的派生方式都是public继承,这就意味着派生类继承的接口必须满足两组“合约”。想象一下,如果两个接口都定义了同名的方法会怎样?如果这个同名方法不是虚函数,那么派生类对于该方法的调用是有歧义的,并且程序无法通过编译。如果该方法是虚函数,并且派生类对其进行了覆写,那么这样就不会产生歧义。此外,Eagle类也同时是Bird和FlyingAnimal了:

Eagle* e = new Eagle;
Bird* b = e;
FlyingAnimal* f = e;

这两种从派生类向基类进行的指针转换是允许的。反之,则必须显式调用dynamic_cast或者static_cast来进行。此外也有一个有趣的转换,如果一个指向FlyingAnimal的类同时也是Bird类,这个指针可以在两者之间互相转换吗?答案是肯定的,只是需要用dynamic_cast

Bird* b = new Eagle; // Eagle同时也是FlyingAnimal
FlyingAnimal* f = dynamic_cast<FlyingAnimal*>(b);

在这个上下文中,dynamic_cast有时也被称为交叉转换(cross-cast),即我们不在继承架构中向上或者向下转换(即在基类和派生类间),而是在同一个继承层次上互相转换。

多重继承是C++最被人所诟病的和讨厌的特性。网上看到的建议多数是过时的,因为当时的编译器对多重继承的优化和效率都非常低下。现如今,在现代编译器的加持之下,这已经不是个问题了。又有很多人说,多重继承让类的继承体系更难理解和分析。也许更准确的说法应该是,设计一个良好的多重继承体系并且精确地反映对象之间的不同属性之关系是很难的,因此一个设计糟糕的多重继承体系确实难以理解和分析。

这些问题主要发生在多重继承使用public继承的情况下。但是我们要知道,多重继承也可以通过私有继承的方式来实现的。也许多重继承中使用私有继承的理由相较于单继承场景下更加少,然而,对于多个空类的场景下,多重私有继承产生的开销优化效果就足够充分了:

class Empty1 {};
class Empty2 {};
class Derived : private Empty1, private Empty2 {
	int i;
}; // sizeof(Derived) == 4
class Composed {
	int i;
	Empty1 e1;
	Empty2 e2;
}; // sizeof(Composed) == 8

当一个派生类代表了一个由许多相互之间无关联且不重叠的属性组成的系统时,多重继承就尤为高效。在本书探索由C++表示的诸多设计模式时,我们会遇到上述例子类似的情况。

总结

尽管本章无意作为对类和对象使用的权威指南或者参考,我们仍然向读者介绍了在后续章节中会需要理解的例子和解释。由于我们的目标是探索C++的设计模式,本章的重点就聚焦在了如何正确地使用类和继承。我们对C++中不同的关系是如何表达的这一特点进行了深入探讨。借由这些特性,我们得以在设计模式中对不同组件的关系和交互进行良好的表达。

下一章我们会用同样的基调去理解C++中对于设计模式所需的模板的特性,这些特性有助于我们更好地理解后续的章节。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++设计模式由浅入深(一)—— 继承和多态 的相关文章

  • C++ std::accumulate 没有给出预期的总和

    double numbers 1 0 5 0 333333 0 25 0 2 0 166667 0 142857 0 125 0 111111 0 1 std vector
  • 为 DocumentDb 设置自定义 json 转换器

    我正在使用类型化 DocumentQuery 从 Azure DocumentDb 集合中读取文档 from f in client CreateDocumentQuery
  • 无法在 QGLWidget 中设置所需的 OpenGL 版本

    我正在尝试在 Qt 4 8 2 中使用 QGLWidget 我注意到 QGLWidget 创建的默认上下文不显示 OpenGL 3 1 以上的任何输出 Qt wiki 有一个教程 http qt project org wiki How t
  • 在 LINQ 查询中进行转换

    是否可以在 LINQ 查询中进行强制转换 为了编译器的缘故 下面的代码并不糟糕 但最好将其放入一个查询中 Content content dataStore RootControl as Controls Content List
  • SOAP Web 服务:多台服务器,一个接口

    我有一个场景 需要任意数量的服务器来提供相同的 SOAP Web 服务 我想生成一组代理类 并能够为它们提供一个位置 以便在运行时将它们指向不同的服务器 不幸的是 看起来好像wsdl port节点 子节点wsdl service 要求对特定
  • 如何使用T4从一个模板同时生成两个文件?

    我遇到的情况是 我需要生成两个 CSharp 代码文件 它们的代码几乎相同 但方法的输入和输出类型的命名空间不同 事实上 每个文件都针对特定国家 地区 并且类型来自特定国家 地区的 WSDL 我正在围绕服务编写一些包装器 逻辑完全相同 但从
  • 如何从经过身份验证的 SecurityToken 中获取声明

    我将令牌作为字符串传递到 SOAP 服务中 并验证了该令牌是否有效 我现在有一个 SecurityToken 在调试模式下我可以看到所有声明 特别是我想传递到另一个方法的 userId 声明 我似乎不知道如何获得这些索赔 现在 我解码了令牌
  • 大量互斥体对性能的影响

    假设我有一个包含 1 000 000 个元素的数组 以及多个工作线程 每个线程都操作该数组中的数据 工作线程可能会使用新数据更新已填充的元素 但每个操作仅限于单个数组元素 并且独立于任何其他元素的值 使用单个互斥锁来保护整个数组显然会导致高
  • 重载算术运算符

    赋值运算符可以声明为 T 运算符 const t 在类中 但不能以这种方式定义算术运算符 它必须是友元函数 我不明白为什么 你能解释一下吗 算术运算符不必须是友元 那么你可以这样定义 MyClass MyClass operator con
  • MINIX内部碎片2

    我正在用 C 语言编写一些软件 它递归地列出给定目录中的所有文件 现在我需要计算出内部碎片 我花了很长时间研究这个问题 发现 ext2 上的内部碎片只发生在最后一个块中 我知道理论上你应该能够从索引节点号获得第一个和最后一个块地址 但我不知
  • 如何在VS2005中使用从.bat而不是.exe启动的外部程序进行调试?

    在我的 c 项目的调试属性中 我选择了 启动外部程序 并选择了我希望将调试器附加到的程序的 exe 但是 现在我需要从 bat 文件而不是 exe 启动程序 但 VS2005 似乎不允许这样做 这可能吗 编辑 为了澄清 我需要调试从 bat
  • 具有多个父项的 Qt 树模型

    我想构建一棵树 其中一个元素可以引用另一个元素 我想要构建的树是 像这样的东西 A B C D E F P this is a pointer to C D first child of C E second child of C I fo
  • 在 Visual Studio 2012 Express 中设置 C++ 调试环境

    我需要调试的应用程序需要设置环境变量 这在 Visual Studio 2012 中似乎非常复杂 我想做类似的事情 set path c foo c bar c windows c program files application set
  • g++ / gcc 是否支持 C++20 新的atomic_flag 功能?

    根据参考参数 https en cppreference com w cpp atomic atomic flag c 20 有丰富的 对我来说有用的 支持atomic flag运营 然而 目前尚不清楚 gcc 是否支持这些功能 它们在任何
  • 查找数组中的多个索引

    假设我有一个像这样的数组 string fruits watermelon apple apple kiwi pear banana 是否有一个内置函数可以让我查询 apple 的所有索引 例如 fruits FindAllIndex ap
  • 在多线程环境中捕获信号

    我有一个大型程序 需要尽可能具有弹性 并且有大量线程 我需要捕获所有信号SIGBUS SIGSEGV 并在必要时重新初始化有问题的线程 或者禁用该线程以继续减少功能 我的第一个想法是做一个setjump 然后设置信号处理程序 可以记录问题
  • 异步/等待 - 是*并发*吗?

    我一直在考虑 C 5 中新的异步内容 并且出现了一个特殊问题 据我了解 await关键字是一个简洁的编译器技巧 语法糖来实现连续传递 http en wikipedia org wiki Continuation passing style
  • 尝试后终于没有被调用

    由于某种原因 在我的控制台应用程序中 我无法运行我的finally 块 我编写这段代码是为了测试finally块是如何工作的 所以它非常简单 static void Main int i 0 try int j 1 i Generate a
  • 使用空的weak_ptr作为参数调用map::count安全吗?

    打电话安全吗map count http www cplusplus com reference map map count on an 未初始化因此为空weak ptr http en cppreference com w cpp mem
  • 将同步 zip 操作转换为异步

    我们有一个现有的库 其中一些方法需要转换为异步方法 但是我不确定如何使用以下方法执行此操作 错误处理已被删除 该方法的目的是压缩文件并将其保存到磁盘 请注意 zip 类不公开任何异步方法 public static bool ZipAndS

随机推荐

  • 浅析数据库case when 用法

    背景 今天在做一个需求 大致就是根据卡的logo去匹配 卡片的主卡数量 附属卡数量 激活卡数量 未激活卡数量 销卡数量等 当时以为要写很多sql 后来问了下同事说可以用case when写一条sql就能搞定 当时那个开心啊就是这样的O O哈
  • YoloV8改进策略:SPD-Conv加入到YoloV8中,让小目标无处遁形

    摘要 SPD Conv是一种新的构建块 用于替代现有的CNN体系结构中的步长卷积和池化层 它由一个空间到深度 SPD 层和一个非步长卷积 Conv 层组成 空间到深度 SPD 层的作用是将输入特征图的每个空间维度降低到通道维度 同时保留通道
  • arxiv国内镜像——快速下载

    arXiv org是一个收录科学文献预印本的在线数据库 目前包含了超过50万篇文章 并且以每个月5000篇的速度增长着 目前 这个数据库包含 数学 物理 计算机 非线性科学 定量生物学 定量财务以及统计学几大分类 其最重要的特点就是 开放式
  • php结合验证码实现登陆,thinkPHP实现的验证码登录功能示例

    本文实例讲述了thinkPHP实现的验证码登录功能 分享给大家供大家参考 具体如下 使用thinkphp自带的验证 实现登录页面的账号密码 验证码的验证 namespace Admin Controller use Think Contro
  • hashmap的扩容机制

    1 hashmap扩容的代码实现 hashMap的扩容机制 final Node
  • springboot整合IJPay实现微信支付-V3---微信小程序

    前言 微信支付适用于许多场合 如小程序 网页支付 但微信支付相对于其他支付方式略显麻烦 我们使用IJpay框架进行整合 一 IJpay是什么 JPay 让支付触手可及 封装了微信支付 支付宝支付 银联支付常用的支付方式以及各种常用的接口 不
  • Flutter

    看到有不少人在用 CustomScrollView 我实在心痛 杀鸡焉用牛刀 好好看代码 一个小小的 Column 就能解决问题 class AddHeaderFooterListPageState extends State
  • OpenCV3特征提取与目标检测之HOG(一)——HOG的概述与原理

    1 HOG Histogram of Oriented Gradient 是方向梯度直方图的意思 是一种特性描述子 通过计算与统计图像局部区域的梯度方向直方图来构成特征 边缘是图像颜色剧变的区域 在一副图像中 局部目标的表象与形状能够被梯度
  • PDU+远控,企业如何应用工业级智能PDU远程赋能业务?

    在很多企业级业务场景下 如何保障相关业务设备的稳定供电非常重要 插座也就成为了这些业务体系中的核心基建 为了保证相关设备供电的稳定 并且实现高效的远程管理 很多企业级的业务场景会部署专业的智能PDU 而在众多智能PDU设备中 向日葵智能PD
  • AI实战:钉钉 AI 的魔法棒,“小二” 帮你打工

    钉钉作为企业办公领域巨头般的存在 市场规模远超企微和飞书 随着阿里 AI 大模型的能力提升 AI 能力也在慢慢嵌入到阿里各个产品生态中去 钉钉 AI 就是在这样的一个背景下产生的 官方网站 https workspace dingtalk
  • Web项目如何做单元测试

    你可能会用单元测试框架 python的unittest pytest Java的Junit testNG等 那么你会做单元测试么 当然了 这有什么难的 test demo py def inc x return x 1 def test a
  • 在WebStorm中使用Vue的v-bind,v-on等内置指令时报命名空间的错误

    报错详情 Namespace v bind is not bound Namespace v on is not bound 等 问题说明 出现这个错误不是代码本身的问题 而是 WebStorm 这个编辑器的问题 因为 WebStorm 不
  • B+ Tree

    B Tree 什么是B B 树的时间复杂度和高度 Insert 简单的insert 复杂的Insert Delete 简单的delete 复杂的delete 时间复杂度 什么是B B tree是平衡二叉树 每个节点包含k个元素 k的范围在
  • SmartBuffer-让你不再为共享与私有缓存处理逻辑费神

    一 带着问题 找思路 有这样一个需求 有3个算法类 我们分别称为TestAlgo1 TestAlgo2 TestAlgo3 每个算法可以设置使用buffer类型是共享或私有 补充 共享是指在同类型算法的多个实例中使用同一块buffer 比如
  • Python-mock

    文章目录 一 接口测试中Mock的用处 二 使用步骤 1 Moco框架搭建Mock服务 Moco说明 2 Python unittest模块自带的mock 一 接口测试中Mock的用处 前后端开发 后端接口未开发完成 前端调用mock数据进
  • 网易云音乐评论抓取(js逆向)

    网易云音乐评论抓取 js逆向 本文通过分析网易云音乐的js加密 通过构造相关的参数 获取网易云音乐评论 所用语言和相关模块 python3 6 requests 网站特点分析 通过分析网站可知 评论获取的url https music 16
  • JavaScript和C++的一些显著差异和学习tips

    之前学习JS来做Vue Node js前后端 最近开始重新学习C 显然这两者的差异巨大 主要注意的点 C 的语法结构 while for JS中没有数据类型 而C 的数据类型需要学习 例如DWORD等等 面向对象 JavaScript的对象
  • 人工智能基础篇

    人工智能基础篇 本篇目录 一 人工智能 机器学习 深度学习的关系 1 关系图 2 人工智能 3 人类智能过程 4 机器学习 5 深度学习 二 人工智能研究的领域 三 人工智能的应用场景 1 计算机视觉 2 语音技术 3 自然语言处理 4 决
  • C++学习第二弹之整数数据类型

    本文主要介绍C 数 整数 包括创建变量和编写各种类型的常量 整数 不同C 数据类型使用不同的内存来存储整数 内存越大能够表示的数值就越大 同时有的类型可以表示正值和负值 有些不能表示负值 C 基本整型有char short int long
  • C++设计模式由浅入深(一)—— 继承和多态

    一 继承和多态 C 首先是一个面向对象的语言 对象是C 程序的基石 通过类的继承和派生 软件工程师可以自由表达对软件系统中各个部分之间的关系与交互逻辑 定义各个组件之间的接口和实现 有秩序地组织起数据结构和代码 本书的目的不是为了教授C 语