c++多重继承
1.概论
多重继承是否有必要吗?
这个问题显然是一个哲学问题,正确的解答方式是根据情况来看,有时候需要,有时候不需要,这显然是一句废话,有点像上马克思主义哲学或者中庸思。
但是这个问题和那些思想一样,被一知半解的人所误解和扭曲,最后变成了和稀泥的借口。下面来谈一谈我的个人看法。
假设有两个类A,B,A类提供了一种我们需要的容器,B类提供了我们需要的算法,这两个类是不同途径提供而来,现在创造一种满足这两个要求的类,就可以使用继承
类c可以同时继承两个类,获得他们的特征和行为。
但是,c++中的模板提供了新的思路,它可以通过自动识别来使用对应的方法,这样就不需要使用多重继承才能使用相关的功能了。
在c++标准库中iostream类,有一个多重继承
ios
istream ostream
iostream
这种继承类似于代码分解机制,ios是istream和ostream的共有类,他们继承了ios之后各种扩展了功能,然后再由iostream继承。
当我们使用继承的时候,无论是公有继承还是保护继承。或者是私有继承,派生类都继承了基类的全部成员,没有例外。
2.接口继承
这是一种只继承接口的继承,基类没有成员变量,也没用实现方法,只有一个接口声明,这样的类也被称为虚基类。就像下面这个类
class Base{public:virtual ~Base(){}virtual void interface() = 0;};假设有三个这样的类,他们各种声名了不同的功能接口,让一个类来继承他们,实现他们的接口,最后可以通过不同的基类指针或者引用调用不同的接口。
class Base1{public:virtual ~Base1(){}virtual void interface1() = 0;};class Base2{public:virtual ~Base2(){}virtual void interface2() = 0;};class Base3{public:virtual ~Base3(){}virtual void interface3() = 0;};class deriver: public Base1, public Base2, public Base3{public:实现三个基类的接口}void interface1(const Base1& b1){b1.interface1}void interface2(const Base2& b2){b2.interface2}void interface3(const Base3& b3){b3.interface3}当我们只需要基类的每一个功能时,我们只需要知道基类是谁,而不需要了解派生类的实现。
但是这种继承,可以通过模板来化解,写出来的代码也简单很多。
只需要通过一个基类,写出三种方法,然后通过泛型编程调用不同的函数
class Base{public:void interface1(){}void interface2(){}void interface3(){}};template<class Base1>void interface1(const Base1& b1){b1.interface1();}template<class Base2>void interface2(const Base2& b2){b2.interface2();}template<class Base3>void interface3(const Base3& b3){b3.interface3();}两种方式有何不同?
第一种通过继承,使得对象更为明确了,有一种各司其职的感觉。
第二种是一种集合程度较高的写法,类型较弱。
但是两种方式没有高下之别,适用于不同的场景。
3.实现继承
c++的实现继承意味着所有内容都来自于基类,这样就不用实现继承来的方法了,直接调用即可,多重继承的一个用途包括混入类,混入类的存在是为了通过继承来增加
其他类的功能,它自己不能实例化自己,而是通过其他类来实例化。以数据库操作为例:
//数据库异常类struct DatabaseError:std::runtime_error{DatabaseError(const std::string& msg){}};//数据库类,连接数据库和关闭数据库,还有一些其他功能,比如检索,删除。。。这里不写出来class Database{std::string dbid;public:Database(const std::string* dbStr) : dbid(dbStr){}virtual ~Database(){}void open() throw(DatabaseError){std::cout << "connected to" << dbid << std::endl;}void close(){std::cout << dbid << "close" <<std::endl;}};在一个服务器=客户端的模式下,客户拥有多个对象,这些对象分享一个连接的数据库,只有当所有客户都断开连接的时候,数据库才调用close关闭自己。
为了记录连接数据库的客户数量,创建一个新的特殊的类,也就是混入类
class Countable{long count;protected:Countable(){count = 0;}virtual ~Countable(){assert(count == 0);}public:long attach(){return count++;}long detach(){return --count > 0 ? count : (delete this, 0);}long refcount() const{return count;}};这个类的构造函数和析构函数都是保护成员,所有它无法自己生成,必须有一个友元类或者派生类来使用它;
析构函数这一点很重要,因为只有detach使用delete它的时候才会被正确的销毁
下面这个类会继承上面的两个类
class DBConnection : public Database, public Countable{private:DBConnection(const DBConnection&);DBConnection& operator=()(const DBConnection&);//不允许赋值或者赋值protected://构造函数--打开数据库DBConnection(const string& dbStr)throw(DatabaseError) : Database(dbStr){open();//}//析构函数,关闭数据库--无法从外面关闭~DBConnection(){ close();}public://静态方法static DBConnection* create(const string& dbStr)throw(DatabaseError){DBConnection* con = new DBConnection(dbStr);}con->attach();assert(con->refCount() == 1);return con;};这个类只能通过静态成员创建自己,在创建自己的同时,另外两个类也调用了自己的构造函数
不用修改Database类就做到了记录连接数据库的数量,然后根据数量来调用 DBConnection的析构函数关闭数据库
class DBClient{DBConnection* db;public:DBClient (DBConnection* dbCon){db = dbCon;db->attach();}~DBClient(){ db->detach();}};客户端使用RAII(the Resource Acquisition Is Initation)的方法实现数据库的打开和关闭。int main(){DBConnection* db = DBConnection::create("database");assert(db->refCount()==1);return 0;}整合上面的代码,并具体运行
//整合上面的代码#include <iostream>
#include<stdexcept>
#include<string>
#include<cassert>
using namespace std;//数据库异常类
struct DatabaseError:std::runtime_error{DatabaseError(const std::string& msg) : std::runtime_error(msg){}
};
//数据库类,连接数据库和关闭数据库,还有一些其他功能,比如检索,删除。。。这里不写出来
class Database{
std::string dbid;
public:Database(const std::string& dbStr) : dbid(dbStr){}virtual ~Database(){}void open() throw(DatabaseError){std::cout << "connected to" << dbid << std::endl;}void close(){std::cout << dbid << "close" <<std::endl;}
};
// 在一个服务器=客户端的模式下,客户拥有多个对象,这些对象分享一个连接的数据库,只有当所有客户都断开连接的时候,数据库才调用close关闭自己。
// 为了记录连接数据库的客户数量,创建一个新的特殊的类,也就是混入类
class Countable{
long count;
protected:Countable(){count = 0;}virtual ~Countable(){assert(count == 0);}
public:long attach(){return count++;}long detach(){return --count > 0 ? count : (delete this, 0);}long refCount() const{return count;}
};
// 这个类的构造函数和析构函数都是保护成员,所有它无法自己生成,必须有一个友元类或者派生类来使用它;
// 析构函数这一点很重要,因为只有detach使用delete它的时候才会被正确的销毁
//
// 下面这个类会继承上面的两个类
class DBConnection : public Database, public Countable{
private:DBConnection(const DBConnection&);DBConnection& operator=(const DBConnection&);//不允许赋值或者赋值protected:
//构造函数--打开数据库DBConnection(const string& dbStr)throw(DatabaseError) : Database(dbStr){open();//}//析构函数,关闭数据库--无法从外面关闭~DBConnection(){ close();}public://静态方法static DBConnection* create(const string& dbStr)throw(DatabaseError){DBConnection* con = new DBConnection(dbStr);con->attach();assert(con->refCount() == 1);return con;}
};
// 这个类只能通过静态成员创建自己,在创建自己的同时,另外两个类也调用了自己的构造函数
//
// 不用修改Database类就做到了记录连接数据库的数量,然后根据数量来调用 DBConnection的析构函数关闭数据库class DBClient{DBConnection* db;
public:DBClient (DBConnection* dbCon){db = dbCon;db->attach();}~DBClient(){ db->detach();}};
// 客户端使用RAII(the Resource Acquisition Is Initation)的方法实现数据库的打开和关闭。int main(){//创建数据库 DBConnection* db = DBConnection::create("database");assert(db->refCount()==1);DBClient c1(db);assert(db->refCount()==2);DBClient c2(db);assert(db->refCount()==3);DBClient c3(db);assert(db->refCount()==4);db->detach();assert(db->refCount() == 3);return 0;
}在c++模板我曾经介绍过一种使用模板来计数的方式,这里将混入类设定模板,可以指定混入类的类型
template<class Counter>
class DBConnection : public Database, public Counter{
private:DBConnection(const DBConnection&);DBConnection& operator=(const DBConnection&);//不允许赋值或者赋值protected:
//构造函数--打开数据库DBConnection(const string& dbStr)throw(DatabaseError) : Database(dbStr){open();//}//析构函数,关闭数据库--无法从外面关闭~DBConnection(){ close();}public://静态方法static DBConnection* create(const string& dbStr)throw(DatabaseError){DBConnection* con = new DBConnection(dbStr);con->attach();assert(con->refCount() == 1);return con;}
};
这里只有一个改变,就是加入了template<class Counter>,也可以把数据库作为模板类,这样就允许使用不同的数据库,如果这个类的混入类足够多,则可以指定更多的模板类。
4.重复子对象
当派生类继承基类的时候,它会继承基类所有的成员,下面程序说明了多个基类子对象在内存中的布局情况。
#include <iostream>
#include<stdexcept>
#include<string>
#include<cassert>
using namespace std;class A{ int x;};
class B{int y;};
class C : public A, public B{ int z;};int main(){cout << "sizeof(A) = " << sizeof(A) << endl;cout << "sizeof(B) = " << sizeof(B) << endl;cout << "sizeof(C) = " << sizeof(C) << endl;C c;cout << "&c == " << &c << endl;A* ap = &c;B* bp = &c;cout << "ap == " << static_cast<void*>(ap) << endl;cout << "bp == " << static_cast<void*>(bp) << endl;C* cp = static_cast<C*>(bp);//强制向下转换类型 cout << "cp == " << static_cast<void*>(cp) << endl;return 0;
}
//输出
sizeof(A) = 4
sizeof(B) = 4
sizeof(C) = 12
&c == 0x6ffde0
ap == 0x6ffde0
bp == 0x6ffde4
cp == 0x6ffde0
bp == cp true
0
从程序输出来看,对象C的布局如下
A的数据
B的数据
C自己新增的数据
以对象A开头,依次向下偏移四个字节。
ap指向了对象C开头的位置,所以它的输出和&a一样
bp必须指向B对应的位置,所以它必须偏移四个字节,到达B的位置
而把bp向下转为C*的时候,它就需要后退四个字节,来到对象C的初始位置,但是如果bp指向一个独立的B对象,这种转化就是不合法了。
当使用bp == cp时,cp隐式转化为bp了,这种转化说明,向上转总是允许的
可以得出一个结论,子对象和完整类型间来回转换,要用到适当的偏移量
下面这个程序有点像菱形继承,但是其实不是,根据继承,子类会继承基类的全部成员,因此Left和Right都有一份Top,而Bottom则有两份Top了
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class Top{
public:int x{10};//4 bit
public:Top(int n): x(n){}
}; class Left : public Top{
public:int y{20};//4
public:Left(int n, int m):Top(n),y(m){}
};class Right : public Top{
public:int z{30};// 4
public:Right(int n, int m):Top(n), z(m){}
};class Bottem: public Left, public Right{
public:int w;//4
public:Bottem(int i, int j, int k, int m):Left(i,k), Right(j,k), w(m){}
};
int main(){Bottem b(1,2,3,4);//Top + Top + Right + Left + int wcout << sizeof(b) << endl;Top* ptr = &b;//错误,因为存在两个Top因此产生二义性 cout << ptrreturn 0;
}5. 虚基类
想要真正实现菱形继承,就要用到虚函数了,让Left和Right共享一份Top
另外对象的空间大小是由非静态成员变量和支持虚函数所产生的虚函数表所共同决定的,还有不要忘了字节对齐,至于虚函数表占据多大的空间,要看编译器的实现
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class Top{
public:int x; //4字节
public:virtual ~Top(){}//8字节 Top(int n): x(n){}friend ostream& operator<<(ostream& os, const Top& t){return cout << t.x;}
};
//按照字节对其,Top占16字节 //继承父类的虚指针和数据成员16 + 4字节
class Left :virtual public Top{
public:int y;
public:Left(int n, int m):Top(n),y(m){}
};class Right :virtual public Top{
public:int z;
public:Right(int n, int m):Top(n), z(m){}
};//继承了Left和Right的虚指针以及数据 32
class Bottem: public Left, public Right{
public:int w;
public:Bottem(int i, int j, int k, int m):Left(i,j), Right(j,k),Top(m),w(m){}friend ostream& operator<<(ostream& os, const Bottem& b){return cout << b.x << " " << b.y << " " << b.z << " " << b.w << endl;}
};class A{int a;
};
int main(){cout << sizeof(Top) << endl;//16cout << sizeof(Left) << endl;//32cout << sizeof(Right) << endl;//32cout << sizeof(Bottem) << endl;//48Bottem b(1,2,3,4);cout << sizeof(b) << endl;cout << b << endl;cout <<static_cast<void*>(&b) << endl;Top* p = static_cast<Top*>(&b); cout << *p << endl;cout << static_cast<void*>(p) << endl;cout << dynamic_cast<void*>(p) << endl;return 0;
}虚继承这方面的事情讲得有点多了,感觉没必要一一详细列举,只需要说一个大概的情况即可
首先,使用虚继承可以保证基类在后续的直接多重继承中只被继承一个副本。
其次,Left和Right还有孙子类Bottem都继承了基类Top,且都有对Top的初始化,如果都起作用了,就会导致二义性,那么对继承的基类进行初始化
的任务就交给了Bottem,这时Left和Right都将会失去对Top初始化的作用,他们对Top的初始化会被忽略,只有最高派生类Bottem的初始化才有效
最后是关于虚继承的虚指针与虚函数表,编译器会给每一个派生类创建一个虚指针,它会指向一张虚函数表,里面有各种数据成员或者方法,在后面需要使用的时候,
它会根据这张虚函数表调用不同的函数,来实现多态。这一点很多文章都有分析,这里只是将一个大概的。以后有空再详细重复吧。
还有一个需要谈到的问题,子类对父类的对象的调用,用下面这个程序作为总结,子类会重复调用父类,最好的解决办法是调用一个特殊的处理,避免重复的工作
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class Top{
private:int x; //4字节
public:virtual ~Top(){}//8字节 Top(int n): x(n){}friend ostream& operator<<(ostream& os, const Top& t){return cout << t.x;}
};
//按照字节对其,Top占16字节 //继承父类的虚指针和数据成员16 + 4字节
class Left :virtual public Top{
private:int y;
public:Left(int n, int m):Top(n),y(m){}friend ostream& operator<<(ostream& os, const Left& l){return cout <<static_cast<const Top&>(l) << " " << l.y;}
};class Right :virtual public Top{
private:int z;
public:Right(int n, int m):Top(n), z(m){}friend ostream& operator<<(ostream& os, const Right& r){return cout <<static_cast<const Top&>(r) << " " << r.z;}
};//继承了Left和Right的虚指针以及数据 32
class Bottem: public Left, public Right{
private:int w;
public:Bottem(int i, int j, int k, int m):Left(i,i), Right(j,j),Top(k),w(m){}friend ostream& operator<<(ostream& os, const Bottem& b){return cout<< static_cast<const Left&>(b) << " "<< static_cast<const Right&>(b) << " "<< b.w;}
};class A{int a;
};
int main(){Bottem b(1,2,3,4);//3 1 3 2 4cout << b << endl;return 0;
}问题在于Left和Right都调用了Top的输出,最好导致重复输出Top
所以,可以再Left和Right的保护成员中添加一个打印函数,这个函数就只会让派生类调用。
总结初始化的顺序:
(1) 所有虚基类子对象,按照他们在类定义中出现的位置,从上往下,从左往右初始化。
(2)然后非虚基类按通常顺序初始化
(3)所有的成员对象按声名的顺序初始化
(4)完整的对象的构造函数执行
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class M{
public:M(const string& s){cout << " M " << s << endl;}
}; class A{
private:M m;
public:A(const string& s) : m("in A"){cout << " A " << s << endl;}virtual ~A(){}
};class B{M m;
public:B(const string& s) : m("in B"){cout << " B " << s << endl;}virtual ~B(){}
};class C{M m;
public:C(const string& s) : m("in C"){cout << " C " << s << endl;}virtual ~C(){}
};class D{M m;
public:D(const string& s) : m("in D"){cout << " D " << s << endl;}virtual ~D(){}
};class E: public A, virtual public B, virtual public C{M m;
public:E(const string& s): A("from E"), B("from E"),C("from E"), m("in E"){cout << " E " << s << endl;}
};class F: virtual public B, virtual public C, public D{M m;
public:F(const string& s): B("from F"),C("from F"),D("from F"),m("in F"){cout << " F " << s << endl;}
};class G: public E, public F{M m;
public:G(const string& s):B("from G"), C("from G"), E("from G"), F("from G"), m("in G"){cout << " G " << s << endl;}
};int main(){G g("main start");return 0;
}总体来看,先对E,再对F,然后G自己
细分下去:先初始化虚基类B C,然后是A
然后是E本身
接下来的F也是如此,初始化自己的虚基类,这个任务是G 完成,而且已经完成,也就是B C的初始化,所以就初始化D
最后是G自己,最后输出了m in G
B::m
B
C::m
C
A::m
A
E::m
E
D::m
D
F::m
F
G::m
G
//输出
M in B
B from G
M in C
C from G
M in A
A from E
M in E
E from G
M in D
D from F
M in F
F from G
M in G
G main start
6.名字查找问题
如果一个派生类同时继承了多个类,其中有的类有两个相同名字的函数,在调用这个函数,则会出错。当然,这几个类需要在统一层次的继承的。
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class Top{
public:virtual ~Top(){}
};class Left :virtual public Top{
public:void fun(){}
};class Right : virtual public Top{
public:void fun(){}
};class Bottem : public Left, public Right{};int main(){Bottem b;b.fun();//错误,产生了二义性 }修改的方法是用基类名称来限定
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class Top{
public:virtual ~Top(){}
};class Left :virtual public Top{
public:void fun(){cout << "Left::fun()" << endl; }
};class Right : virtual public Top{
public:void fun(){cout << "Right::fun()" << endl;}
};class Bottem : public Left, public Right{
public:
using Right::fun;};int main(){Bottem b;b.fun(); }正如上面提到的,必须是同一层次的继承才会有名字冲突,如果是垂直继承下来的,就不会有二义性了。
#include <iostream>
#include<string>
#include<cassert>
using namespace std;class Top{
public:virtual ~Top(){}virtual void fun(){cout << "Top::fun()" << endl;}
};class Left :virtual public Top{
public:void fun(){cout << "Left::fun()" << endl; }
};class Right : virtual public Top{};class Bottem : public Left, public Right{};int main(){Bottem b;b.fun(); }虽然Bottem继承了Left和Right还有Top,其中Top和Left都有fun函数,但是b调用fun的时候,会直接调用Left的fun而不是基类Top的fun
这说明,继承中调用同名函数的时候,会优先调用派生级别高一些的。
7.避免使用多重继承
多重继承是一个很复杂的东西,我们应该尽量避免
如果下面两个条件有一个不满足,就不要多重继承
(1)是否需要通过一个新的派生类来显示接口?
(2)需要向上类型转化为基类吗?
尽量使用组合,而不是继承
8. 使用多重继承的一个案例:扩充一个接口
假设有这么一个库,只有一些头文件和接口,具体实现方法都看不见,这个库是一个带有虚函数的类层次接口,并且有全局函数,函数的参数是基类的引用,它利用这个类继承的多态
现在程序员需要用到一些功能,需要这些类的函数是虚函数,但是提供的库并不是,现在怎么办?
//1.提供的头文件和类声名
//Base.h文件
class Base{
public:virtual void v() const;void f() const;//假设我们想要这个函数是虚函数~Base();
}; class Base1 : public Base{
public:void v() const;void f() const;~Base1();
};void fun1(const Base&);//全局函数,调用类中的某些接口
void fun2(const Base&);//同上 //上面这些d东西已经固定了,我们无法更改,现在我们想要添加一个功能g做某些事情,还想让Base1里的函数是虚函数,
//我们也不能令全局函数的功能发生变化
//为了解决问题可以使用多重继承
class MyProject{
public:virtual ~MyProject(){cout << "~MyProject()" << endl;}virtual void f() const = 0;//令这些函数全都为虚函数 virtual void v() const = 0;virtual void g() const = 0;//我们需要增加的功能
}; class MyWay: public MyProject, public Base1{
public:~MyWay(){cout << "~MyWay()" << endl;}void f()const{cout << "MyWay::f()" << endl;Base1::f();}void v()const{cout << "MyWay::v()" << endl;Base1::v();}void g()const{cout << "MyWay::g()\n";}
};//2下面的文件对用户不可见,这里写出来是为了更好的说明
//Base.cpp文件
void Base::f()const {cout << "Base::fun()" << endl;
} void Base::v()const{cout << "Base::v()" << endl;
}Base::~Base(){cout << "Base::~Base()" << endl;
}void Base1::f()const{cout << "Base1::fun()" << endl;
} void Base1::v()const{cout << "Base1::v()" << endl;
}Base1::~Base1(){cout << "Base1::~Base1()" << endl;
}void fun1(const Base& b){b.f();b.v();
}void fun2(const Base& b){b.f();b.v();
}int main(){MyWay& pw = *new MyWay;cout << "____________\n";pw.f();cout << "____________\n";pw.v();cout << "____________\n";pw.g();cout << "____________\n";fun1(pw);cout << "____________\n";fun2(pw);cout << "____________\n";delete &pw;return 0;
}我们通过多重继承解决了接口问题,而且也没用改变原来的库的性质。
相关文章:
c++多重继承
1.概论多重继承是否有必要吗?这个问题显然是一个哲学问题,正确的解答方式是根据情况来看,有时候需要,有时候不需要,这显然是一句废话,有点像上马克思主义哲学或者中庸思。但是这个问题和那些思想一样&#…...
15_FreeRtos计数信号量优先级翻转互斥信号量
目录 计数型信号量 计数型信号量相关API函数 计数型信号量实验源码 优先级翻转简介 优先级翻转实验源码 互斥信号量 互斥信号量相关API函数 互斥信号量实验源码 计数型信号量 计数型信号量相当于队列长度大于1的队列,因此计数型信号量能够容纳多个资源,这在…...
二叉树(一)
二叉树(一)1.树的概念2.树的相关概念3.树的表示4.树在实际中的运用5.二叉树概念及结构6.特殊的二叉树7.二叉树的性质🌟🌟hello,各位读者大大们你们好呀🌟🌟 🚀🚀系列专栏…...
【SCL】1200案例:天塔之光数码管显示液体混合水塔水位
使用scl编写天塔之光&数码管显示&液体混合&水塔水位 文章目录 目录 文章目录 前言 一、案例1:天塔之光 1.控制要求 2.编写程序 3.效果 二、案例2:液体混合 1.控制要求 2.编写程序 三、案例3:数码管显示 1.控制要求 2.编写程序 3…...
5.1配置IBGP和EBGP
5.2.1实验1:配置IBGP和EBGP 实验目的 熟悉IBGP和EBGP的应用场景掌握IBGP和EBGP的配置方法 实验拓扑 实验拓扑如图5-1所示: 图5-1:配置IBGP和EBGP 实验步骤 IP地址的配置 R1的配置 <Huawei>system-view Enter system view, return …...
c++中超级详细的一些知识,新手快来
目录 2.文章内容简介 3.理解虚函数表 3.1.多态与虚表 3.2.使用指针访问虚表 4.对象模型概述 4.1.简单对象模型 4.2.表格驱动模型 4.3.非继承下的C对象模型 5.继承下的C对象模型 5.1.单继承 5.2.多继承 5.2.1一般的多重继承(非菱形继承) 5.2…...
[答疑]经营困难时期谈建模和伪创新-长点心和长点良心
leonll 2022-11-26 9:53 我们今年真是太难了……(此处删除若干字)……去年底就想着邀请您来给我们讲课,现在也没有实行。我想再和我们老大提,您觉得怎么说个关键理由,这样的形势合适引进UML开发流程? UML…...
计算机基础知识
计算机网络的拓扑结构 一、OSI 7层网络模型是指什么? 7层分别是什么?每层的作用是什么? OSI7层模型是 国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系。 每层功能:(自底向上) 物理层:建立、…...
Java爬虫—WebMagic
一,WebMagic介绍WebMagic企业开发,比HttpClient和JSoup更方便一),WebMagic架构介绍WebMagic有DownLoad,PageProcessor,Schedule,Pipeline四大组件,并有Spider将他们组织起来…...
[软件工程导论(第六版)]第2章 可行性研究(复习笔记)
文章目录2.1 可行性研究的任务2.2 可行性研究过程2.3 系统流程图2.4 数据流图概念2.5 数据字典2.6 成本/效益分析2.1 可行性研究的任务 可行性研究的目的 用最小的代价在尽可能短的时间内确定问题是否能够解决。 可行性研究的3个方面 (1)技术可行性&…...
Mac下安装Tomcat以及IDEA中的配置
安装brew 打开终端输入以下命令: /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 搜索tomcat版本,输入以下命令: brew search tomcat 安装自己想要的版本,例…...
【Linux详解】——文件基础(I/O、文件描述符、重定向、缓冲区)
📖 前言:本期介绍文件基础I/O。 目录🕒 1. 文件回顾🕘 1.1 基本概念🕘 1.2 C语言文件操作🕤 1.2.1 概述🕤 1.2.2 实操🕤 1.2.3 OS接口open的使用(比特位标记)…...
HomMat2d
1.affine_trans_region(区域的任意变换) 2.hom_mat2d_identity(创建二位变换矩阵) 3.hom_mat2d_translate(平移) 4.hom_mat2d_scale(缩放) 5.hom_mat2d_rotate(旋转 &…...
Python3 JSON 数据解析
Python3 JSON 数据解析 JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。 Python3 中可以使用 json 模块来对 JSON 数据进行编解码,它包含了两个函数: json.dumps(): 对数据进行编码。json.loads(): 对数据进行解码。 在 json 的编解码…...
Homebrew 安装遇到的问题
Homebrew 安装遇到的问题 例如:第一章 Python 机器学习入门之pandas的使用 文章目录Homebrew 安装遇到的问题前言一、安装二、遇到的问题1.提示 zsh: command not found: brew三、解决问题前言 使用 Homebrew 能够 安装 Apple(或您的 Linux 系统&#…...
Metasploit框架基础(二)
文章目录前言一、Meatsplooit的架构二、目录结构datadocumentationlibmodulesplugins三、Measploit模块四、Metasploit的使用前言 Metasploit是用ruby语言开发的,所以你打开软件目录,会发现很多.rb结尾的文件。ruby是一门OOP的语言。 一、Meatsplooit的…...
c++容器
1、vector容器 1.1性质 a)该容器的数据结构和数组相似,被称为单端数组。 b)在存储数据时不是在原有空间上往后拓展,而是找到一个新的空间,将原数据深拷贝到新空间,释放原空间。该过程被称为动态拓展。 vec…...
Vue.js如何实现对一千张图片进行分页加载?
目录 vue处理一千张图片进行分页加载 分页加载、懒加载---概念介绍: 思路: 开发过程中,如果后端一次性返回你1000多条图片或数据,那我们前端应该怎么用什么思路去更好的渲染呢? 第一种:我们可以使用分页…...
计算机网络复习(六)
考点:MIME及其编码(base64,quoted-printable)网络协议http是基于什么协议,应用层到网络层基于什么协议6-27.试将数据 11001100 10000001 00111000 进行 base64 编码,并得到最后传输的 ASCII 数据。答:先将 24 比特的二…...
Redis进阶:布隆过滤器(Bloom Filter)及误判率数学推导
1 缘起 有一次偶然间听到有同事在说某个项目中使用了布隆过滤器, 哎呦,我去,我竟然不知道啥是布隆过滤器, 这我哪能忍?其实,也可以忍,但是,可能有的面试官不能忍!&#…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...
【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...
