C++ primer 第十五章
1.OPP:概述
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
通过继承联系在一起的类构成一种层次关系,在层次关系的根部的是基类,基类下面的类是派生类
基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
虚函数:由基类来声明,但基类希望它的派生类各自重新定义适合自身的版本的函数。
class quote{
public:virtual double net_price(std::size_t n) const; //基类quote声明的虚函数
};
派生类必须通过使用类派生列表明确指出它是从哪个基类继承而来。
class bulk_quote : public quote{ //在基类前写上访问说明符...
};
派生类必须在其内部对所有重新定义的虚函数进行声明。
新标准允许派生类使用关键字override来显式地注明它将使用哪个成员函数来改写基类的虚函数。
double net_price(std::size_t) const override; //派生类重新定义的虚函数
通过动态绑定,我们能用同一段代码分别处理基类和派生类的对象。
在C++语言中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定。
double print_total(ostream& os,const quote& item, size_t n)
{auto ret = item.net_price();
}//basic是基类类型,bulk是派生类类型
print_total(cout,basic,20); //调用基类的net_price
print_total(cout,bulk,20); //调用派生类的net_price
函数的执行版本由实参决定,在运行时选择函数的版本,动态绑定又称为运行时绑定。
2.定义基类和派生类
2.1、定义基类
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
派生类可以继承其基类的成员。
在C++中,基类必须将它的两种成员函数区分开来:一种是希望派生类进行覆盖的函数,另一种是希望派生类直接继承而不要改变的函数。
基类通过在其成员函数语句前加上关键字virtual,使得该函数执行动态绑定,成为虚函数。
任何构造函数之外的非静态函数都可以是虚函数。
关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
若基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。
派生类能访问基类的公有成员,而不能访问基类的私有成员。
若基类希望它的派生类有权访问该成员,同时禁止其他用户访问,可使用protected访问运算符来说明该成员。
class quote{ //基类
public:quote() = default; //默认构造函数quote(const std::string& book,double sales_price) : books(book),price(sales_price) {} //接受两个参数的构造函数virtual double net_price(std::size_t n) const {return n*price;} //虚函数virtual ~quote() = default; //虚析构函数
private:std::string books;
protected: //派生类能够访问的成员double price = 0.0;
};
2.2、定义派生类
派生类在每个基类前面可以有三种访问说明符中的一种:public、protected、private。
访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类用户可见。
若一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。(在任何需要基类的引用或指针的地方我们都可以使用派生类的对象)
若派生类没有覆盖基类中的某个虚函数,则派生类会直接继承其在基类中的版本。
新标准允许派生类使用关键字override显式地注明它使用某个成员函数覆盖了它所继承的虚函数。
一个派生类对象包含多个组成部分:一个含有派生类自己定义的非静态成员的子对象,一个与该派生类继承的基类对应的子对象。
由于在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当作基类对象来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分。
这种转换称为派生类到基类的类型转换,编译器会隐式地执行派生类到基类的转换。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
派生类只能通过使用基类的构造函数来初始化它的基类部分,每个类控制他自己的成员的初始化过程。
派生类构造函数是通过构造函数初始化列表来将实参传递给基类构造函数的。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
bulk_quote(const std::string& book,double p,std::size_t qty,double disc) :quote(book,p),qtys(qty),discount(disc) {}
//将实参book,p传给quote的构造函数,剩余的实参依次初始化派生类的成员
派生类的作用域嵌套在基类的作用域之内,派生类可以直接访问基类的公有成员和受保护成员。
若基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
静态成员遵循通用的访问控制规则,若基类中的成员是private的,则派生类无权访问该成员。
派生类的声明包含类名但不包含它的派生列表。
class bulk_quote : public quote; //错误,派生列表不能出现
class bulk_quote; //正确
派生列表以及与定义有关的其他细节必须与类的主体一起出现。
若我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
一个类可以是基类,同时也可以是派生类。
class base{...};
class b1 : public base {...};
class b2 : public b1 {...};
//base是b1的直接基类,同时也是b2的间接基类
每个类都会继承直接基类的所有成员。
新标准中提供了一种防止继承发生的方法,即使用关键字final来阻止继承。
class base{...};
class b1 final : base{...}; //b1不能被继承
class b2 : b1{...}; //错误,b1是final的
2.3、类型转换与继承
存在继承关系的类能将基类的指针或引用绑定到派生类的对象上。
当使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定的对象的真实类型。
智能指针类也支持派生类向基类的类型转换,这意味着允许将一个派生类对象的指针存储在基类的智能指针内。
当我们使用存在继承关系的类型时,应该区分开一个变量或表达式的静态类型与该表达式表示对象的动态类型。
基类的指针或引用的静态类型可能与其动态类型不一致。
静态类型在编译时就是已知的,动态类型直到运行时才可知,由实参类型决定。
double ret = item.net_price(n);
//item是quote类型的引用,在函数定义时就已经确定了,是静态类型
//由于item是quote类型的引用,可以使用基类版本或派生类版本的net_price,
//具体执行哪个函数由传入实参决定,所以net_price的类型要在运行时才可知,它的类型是动态类型
由于每个派生类对象都包含一个基类部分,基类的引用或指针可以绑定到该基类部分上。
存在派生类向基类的类型转换,不存在从基类向派生类的类型转换。
即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换。
bulk_quote bulk;
quote* item = &bulk; //正确,派生类向基类的转换
bulk_quote* bulkp = item; //错误,不存在基类向派生类的转换,哪怕基类中存储的是派生类也不行
若我们已知某个基类向派生类的转换是安全,可通过static_cast来强制覆盖掉编译器的检查工作。
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型中不存在。
当我们初始化或赋值一个类类型的对象时,实际上在调用它们的构造函数或赋值运算符函数,因为这些成员都接受引用作为参数,所以派生类向基类的转换允许我们给基类的拷贝/移动操作传递一个派生类的对象。
bulk_quote bulk; //派生类对象
quote item(bulk); //使用quote::quote(const quote&)构造函数
item = bulk; //调用quote::operator=(const quote&);
但上述操作会忽略掉bulk_quote部分,只将派生类中的quote部分保存在item中。
3.虚函数
当我们使用基类的引用或指针调用一个虚成员函数时会进行动态绑定,因此必须为每个虚函数都提供定义,而不管它是否被用到。
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该匹配哪个版本。
通过一个具有普通类型的表达式调用虚函数时,在编译时就会将调用的版本确定下来,对象的动态类型与静态类型相同。
quote base;
base.net_price(20); //调用quote::net_price
一旦某个函数被声明成虚函数,则在所以派生类中它都是虚函数。
派生类的函数若覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
派生类中的虚函数的返回类型也必须与基类函数匹配,但当类的虚函数返回类型是类本身的指针或引用时,上述规则则无效。
若派生类定义了一个与基类中虚函数的名字相同但形参列表不同的函数,那该函数与基类中原有的函数是相互独立的。
新标准中我们可以使用override关键字来说明派生类中的虚函数,若该函数没有覆盖已存在的虚函数,则编译器会报错。
struct a{virtual void f1(int) const;
};struct b : public a{void f1(int) const override; //使用override来标记虚函数
};
和其他函数一样,虚函数可以拥有默认实参。
若某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定的。
若虚函数使用默认实参,则基类和派生类中定义的默认实参最好保持一致。
若希望对虚函数的调用不要进行动态绑定,而是强制让其执行虚函数的某个特定版本,可以使用作用域运算符来实现。
double ret = base->quote::net_price(22);
//强制调用quote的net_price版本,不管base实际指向的对象类型是什么
通常情况下,只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的机制。
如果一个派生类虚函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
4.抽象基类
抽象基类是含有不完整的内容的基类,类似于模板,用于生产特定条件的类。
纯虚函数是一种特殊的虚函数,该函数没有具体的定义,由子类来提供实现。
通过在函数体的位置(即声明语句)书写=0就可以将一个虚函数说明为纯虚函数,只能在类内部进行声明。
double net_price(std::size_t) const = 0; //纯虚函数
我们可以为纯虚函数提供定义,不过函数体必须定义在类的外部。
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。
抽象基类负责定义接口,而后续的派生类可以覆盖该接口。
由于纯虚函数是不完整的,因此不能直接创建一个抽象基类的对象。
抽象基类的派生类必须给出自己的纯虚函数的定义,否则它们将是抽象基类。
5.访问控制与继承
每个类分别控制着其成员对于派生类来说是否可访问。
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。
某个类对其继承而来的成员的访问权限受到基类中该成员的访问说明符和在派生类列表中的访问说明符的影响。
派生访问说明符是控制派生类从基类所继承的成员在派生类中的访问权限。
派生访问说明符可以控制继承自派生类的新类的访问权限,这无关于基类的访问权限。
class base{
protected:int prot;
};struct priv : private base{int f1() const { return prot; } //prot继承于base,是priv的私有成员
};struct priv1 : public priv{int use() { return prot; } //错误,prot在priv中是私有的
};
无论派生类以什么方式继承基类,派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
只有当派生类公有地继承基类时,用户代码才能使用派生类向基类的转换。
友元关系不能传递,也不能继承。
派生类的友元不能随意访问基类类的成员,但基类的友元能访问派生类的基类部分。
我们可以通过使用using声明来改变派生类继承的某个名字的访问级别。
using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
class base{
public:std::size_t size() const { return n;}
protected:std::size_t n;
};struct priv : private base{
public:using base::size; //size的访问权限为public
protected:using base::n; //n的访问权限为protected
}
派生类只能为那些它可以访问的名字提供using声明。
默认派生运算符由定义派生类所用的关键字来决定(class是私有的,struct是公有的)。
6.继承中的类作用域
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
class disc : public quote{ //disc是bulk_quote的直接基类
public:std::pair<size_t,double> discont() const { return {quantity,discount};}
};bulk_quote bulk;
bulk_quote* bulkp = &bulk; //直接基类指针
quote* itemp = &bulk; //间接基类指针
bulkp->discont(); //正确,搜索从bulk_quote开始
itemp->discont(); //错误,搜索从quote开始
派生类能逐级向上查找匹配项,而基类不能向下寻找。
派生类也能重用定义在其直接基类或间接基类中的名字,此时外层作用域的名字将被隐藏。
可以通过作用域运算符来使用一个被隐藏的基类成员。
除了覆盖继承而来的虚函数以外,派生类最好不要重用其他定义在基类中的名字。
声明在内层作用域的函数并不会重载声明在外层作用域的函数。
被隐藏的成员或函数将无法调用。
假如基类与派生类的虚函数接受的实参不同,则我们无法通过基类的引用或指针来调用派生类的虚函数。
成员函数无论是否是虚函数都能被重载。
7.构造函数与拷贝控制
7.1、虚析构函数
基类通常应该定义一个虚析构函数,这样就能动态分配继承体系中的对象。
若基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
基类的析构函数并不遵循若定义析构函数,则要定义拷贝和赋值操作的经验准则。
若类定义了析构函数,则会阻止合成移动操作。
7.2、合成拷贝控制与继承
基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似。
某些定义基类的方式可能导致有的派生类成员成为被删除的函数:
1、若基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数是被删除的或不可访问的,则派生类中对应的成员将是被删除的。
2、若基类中有一个不可访问的或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将被删除的。
上述现象的发生原因在于派生类对象中含有基类部分。
在实际编程过程中,如果在基类中没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相关的操作。
基类缺少移动操作会阻止派生类拥有自己的合成移动操作。
7.3、派生类的拷贝控制成员
派生类的析构函数只负责销毁派生类自己分配的资源,它的基类部分会被自动销毁的。
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动派生类自己的成员和基类部分成员。
在派生类定义拷贝或移动构造函数时,通常使用对应的的基类构造函数初始化对象的基类部分。
派生类的赋值运算符必须显式地为其基类部分赋值。
基类的运算符能正确地处理自赋值的情况。
对象销毁的顺序与其创建的顺序相反。
若构造函数或析构函数调用了某个虚函数,则我们应该执行与调用函数所属类型相对应的虚函数版本。
7.4、继承的构造函数
新标准允许派生类能够重用其直接基类定义的构造函数。
类不能继承默认、拷贝和移动构造函数,若派生类没有直接定义这些构造函数,则编译器会合成。
派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。
class bulk_quote : public disc{
public:using disc::disc; //继承disc的构造函数
};
当using作用域构造函数时,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。
一个构造函数的using声明并不会改变该构造函数的访问级别。
using声明语句不能指定explicit或constexpr。
若基类的构造函数是explicit或constexpr,则继承的构造函数也拥有相同的属性。
当一个基类构造函数含有默认实参时,这些实参并不会被继承,但派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
若基类含有几个构造函数,大多数时候派生类会继承所有的构造函数。
8.容器与继承
由于不允许在容器中保存不同类型的元素,当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。
当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的是基类的指针。
9.文本查询程序再探
class queryresult; //未定义,但使用到,先声明
class textquery //保存输入文件
{
public:using line_no = std::vector<std::string>::size_type; //类型别名textquery(std::ifstream&); //构造函数,接受一个文件流为参数queryresult query(const std::string&) const; //
private:std::shared_ptr<std::vector<std::string>> file; //输入文件std::map<std::string, std::shared_ptr<std::set<line_no>>> wm; //用查找的单词做关键字,保存行号的set做值
};textquery::textquery(std::ifstream& is) : file(new std::vector<std::string>)
{std::string text;while (getline(is, text)){file->push_back(text); //保存每一行,file是指针,需解引用int n = file->size() - 1; //当前行号std::istringstream line(text); //分解成单个单词std::string word;while (line >> word){auto& line = wm[word]; //line与wm[word]绑定,line是智能指针if (!line) //若单词不在wm中,返回一个空指针line.reset(new std::set<line_no>); //指针指向新创建的setline->insert(n); //将行号插入set中}}
}queryresult textquery::query(const std::string& sought) const //查找单词
{static std::shared_ptr<std::set<line_no>> nodata(new std::set<line_no>); //空setauto loc = wm.find(sought);if (loc == wm.end())return queryresult(sought, nodata, file); //未找到elsereturn queryresult(sought, loc->second, file);
}class queryresult{friend std::ostream & printis(std::ostream&, const queryresult&); //友元声明
public:typedef std::set<textquery::line_no>::const_iterator line_it;queryresult(std::string s,std::shared_ptr<std::set<textquery::line_no>> p,std::shared_ptr<std::vector<std::string>> f) : //构造函数sought(s), lines(p),file(f) {}line_it begin() const { return lines->cbegin(); }line_it end() const { return lines->cend(); }std::shared_ptr<std::vector<std::string>> get_file() { return file; }
private:std::string sought; //查找的单词std::shared_ptr<std::set<textquery::line_no>> lines; //指向保存单词出现的行号的setstd::shared_ptr<std::vector<std::string>> file; //指向保存文件的vector
};//抽象基类,具体的查询类型从query_base派生,成员都是private的
class query_base {friend class query;
protected:using line_no = textquery::line_no; //类型别名virtual ~query_base() = default; //析构函数
private://返回与当前query匹配的queryresultvirtual queryresult eval(const textquery&) const = 0; //rep是表示查询的一个stringvirtual std::string rep() const = 0;
};class query {//运算符需要访问接受shared_ptr的构造函数,来构造新的query对象,构造函数是私有的friend query operator~(const query&);friend query operator|(const query&, const query&);friend query operator&(const query&, const query&);
public:query(const std::string&); //构造一个新的wordquery//接口函数:调用对应的query_base操作queryresult eval(const textquery& t) const { return q->eval(t);}std::string rep() const { return q->rep(); }
private://接受一个指向query_base的shared_ptr指针作为参数,存储给定的指针query(std::shared_ptr<query_base> query) : q(query) {} std::shared_ptr<query_base> q;
};inline query::query(const std::string& s) : q(new wordquery(s)) {} //分配一个新的wordqueryclass wordquery:public query_base { //查找单个单词friend class query;wordquery(const std::string& s) : query_word(s) {} //构造函数queryresult eval(const textquery& t) const {return t.query(query_word); //调用textquery的query成员,在文件中查找}std::string rep() const { return query_word; } //返回查找的单词std::string query_word; //要查找的单词
};class notquery :public query_base {friend query operator~(const query&);notquery(const query& q) : query(q) {}std::string rep() const { return "~(" + query.rep() + ")"; }queryresult eval(const textquery&) const;query query;
};inline query operator~(const query& operand)
{return std::shared_ptr<query_base>(new notquery(operand));//将新分配的notquery指针绑定到shared_ptr<query_base>//return 负责将shared_ptr<query_base>类型转换成query(调用query的构造函数)
}//抽象基类,保存操作两个运算对象的查询类型所需的数据
class binaryquery : public query_base {
protected:binaryquery(const query& l,const query& r,std::string s) : lhs(l),rhs(r),opsym(s) {}std::string rep() const { return "(" + lhs.rep() + " " + opsym + " " + rhs.rep() + ")"; }query lhs, rhs; //左侧和右侧运算对象std::string opsym; //运算符名称
};class andquery : public binaryquery {friend query operator&(const query&, const query&);andquery(const query& left, const query& right) :binaryquery(left, right, "&") {}//从基类binaryquery继承了rep并定义了eval的虚函数queryresult eval(const textquery&) const override;
};inline query operator&(const query& lhs, const query& rhs)
{return std::shared_ptr<query_base>(new andquery(lhs, rhs));
}class orquery : public binaryquery {friend query operator|(const query&, const query&);orquery(const query &left,const query& right) : binaryquery(left,right,"|") {}queryresult eval(const textquery&) const;
};inline query operator|(const query& lhs, const query& rhs)
{return std::shared_ptr<query_base>(new orquery(lhs, rhs));
}queryresult orquery::eval(const textquery& text) const
{//????auto right = rhs.eval(text), left = lhs.eval(text);//含有左侧单词的文本插入到ret_lines指向的set中auto ret_lines = std::make_shared<std::set<line_no>>(left.begin(), left.end()); //含有右侧单词的文本插入到set中ret_lines->insert(right.begin(), right.end());//返回一个queryresult,表示并集return queryresult(rep(), ret_lines, left.get_file());
}queryresult andquery::eval(const textquery& text) const
{auto left = lhs.eval(text), right = rhs.eval(text);//保存left和right交集的setauto ret_lines = std::make_shared<std::set<line_no>>();//该算法将输入序列中共同出现的元素写入目的位置中std::set_intersection(left.begin(), left.begin(), right.begin(), right.end(), std::inserter(*ret_lines, ret_lines->begin()));return queryresult(rep(), ret_lines, left.get_file());
}queryresult notquery::eval(const textquery& text) const
{//包含运算对象出现的行号集合setauto result = query.eval(text);auto ret_lines = std::make_shared<std::set<line_no>>(); //存放的行号auto beg = result.begin(), end = result.end();auto sz = result.get_file()->size();for (size_t n = 0; n != sz; ++n){if (beg == end || *beg != n) //若不在result中,则插入ret_lines->insert(n);else if (beg != end) //获取result的下一行++beg;}return queryresult(rep(), ret_lines, result.get_file());
}
相关文章:

C++ primer 第十五章
1.OPP:概述 面向对象程序设计的核心思想是数据抽象、继承和动态绑定。 通过继承联系在一起的类构成一种层次关系,在层次关系的根部的是基类,基类下面的类是派生类 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有…...

【数据结构与算法】直接插入排序和希尔排序
引言 进入了初阶数据结构的一个新的主题——排序。所谓排序,就是一串记录,按照其中的某几个或某些关键字的大小(一定的规则),递增或递减排列起来的操作。 排序的稳定性:在一定的规则下,两个值…...

HQL,SQL刷题,尚硅谷
目录 相关表数据: 题目及思路解析: 多表连接 1、课程编号为"01"且课程分数小于60,按分数降序排列的学生信息 2、查询所有课程成绩在70分以上 的学生的姓名、课程名称和分数,按分数升序排列 3、查询该学生不同课程的成绩…...

随机生成用户名、密码、注册时间【Excel】
1.1简介 最近想虚拟一些数据,看下有没有自动生成的工具。百度看了下,大概有这么几种方法 1.excel内置公式函数处理 2.使用使用VBA宏生成随机 3.下载方方格子,emm工具是个好工具,蛮多功能的,每月8块 4.Java函数实现…...

C++函数模板详解(结合代码)
目录 1. 模板概念 2. 函数模板语法 3. 函数模板注意事项 4. 函数模板案例 5. 普通函数与函数模板的区别 6. 普通函数与函数模板的调用规则 7. 模板的局限性 1. 模板概念 在C中,模板是一种通用的程序设计工具,它允许我们处理多种数据类型而不是固…...

Nest学习随笔
一、Middleware(中间件)、Interceptor(拦截器)、ExceptionFilter(异常过滤器) 执行顺序 接口调用正常:Middleware > Interceptor接口调用异常:Middleware > ExceptionFilter 二、访问静态文件 使用 nestjs/serve-static 依赖 配置方法&#x…...

二十二、软考-系统架构设计师笔记-真题解析-2018年真题
软考-系统架构设计师-2018年上午选择题真题 考试时间 8:30 ~ 11:00 150分钟 1.在磁盘调度管理中,应先进行移臂调度,再进行旋转调度。假设磁盘移动臂位于21号柱面上,进程的请求序列如下表所示。如果采用最短移臂调度算法,那么系统…...

2024最新最全Selenium自动化测试面试题!
1、什么是自动化测试、自动化测试的优势是什么? 通过工具或脚本代替手工测试执行过程的测试都叫自动化测试。 自动化测试的优势: 1、减少回归测试成本 2、减少兼容性测试成本 3、提高测试反馈速度 4、提高测试覆盖率 5、让测试工程师做更有意义的…...

Docker 搭建Redis集群
目录 1. 3主3从架构说明 2. 3主3从Redis集群配置 2.1关闭防火墙启动docker后台服务 2.2 新建6个docker容器实例 2.3 进去任意一台redis容器,为6台机器构建集群关系 2.4 进去6381,查看集群状态 3. 主从容错切换迁移 3.1 数据读写存储 3.1.1 查看…...

spring boot商城、商城源码 欢迎交流
一个基于spring boot、spring oauth2.0、mybatis、redis的轻量级、前后端分离、防范xss攻击、拥有分布式锁,为生产环境多实例完全准备,数据库为b2b2c设计,拥有完整sku和下单流程的商城 联系: V-Tavendor...

全面解析“通义千问”:功能、优势与使用指南
引言: “通义千问”是由阿里云研发的一款先进的人工智能语言模型,以其强大的自然语言处理能力与广泛的知识覆盖面,在教育、咨询、信息检索等领域发挥着重要作用。本文将详细介绍“通义千问”的核心功能、显著优势以及具体使用方法。 一、“…...

【第三方登录】Google邮箱
登录谷歌邮箱开发者 https://console.developers.google.com/ 先创建项目 我们用的web应用 设置回调 核心主要: 1.创建应用 2.创建客户端ID 3.设置域名和重定向URL 4.对外公开,这样所有的gmail邮箱 都能参与测试PHP代码实现 引入第三方包 h…...

oslo_config学习小结
2.配置文件加载方法 2.1基础 配置文件指的是文件以.conf,.ini结尾等内容为配置项的文件,配置文件内容格式一般为 [DEFAULT] option value [sectiona] optiona valuea optionb valueb [sectionb] optionc valuec optiond valued 2.2加载方法…...

SpringBoot2.6.3 + knife4j-openapi3
1.引入项目依赖: <dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-spring-boot-starter</artifactId><version>4.5.0</version> </dependency> 2.新增配置文件 import io.swag…...

PostgreSQL FDW(外部表) 简介
1、FDW: 外部表 背景 提供外部数据源的透明访问机制。PostgreSQL fdw(Foreign Data Wrapper)是一种外部访问接口,可以在PG数据库中创建外部表,用户访问的时候与访问本地表的方法一样,支持增删改查。 而数据则是存储在外部,外部可以是一个远程的pg数据库或者其他数据库(…...

Java项目:75 springboot房产销售系统
作者主页:源码空间codegym 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 使用房产销售系统分为管理员和用户、销售经理三个角色的权限子模块。 管理员所能使用的功能主要有:首页、个人中心、用户管理、销…...

2.6 IDE(集成开发环境)是什么
IDE(集成开发环境)是什么 IDE 是 Integrated Development Environment 的缩写,中文称为集成开发环境,用来表示辅助程序员开发的应用软件,是它们的一个总称。 通过前面章节的学习我们知道,运行 C 语言&…...

tomcat和web服务器是什么??
一、什么是服务器 1.服务器是计算机的一种,它比普通计算机运行更快、负载更高。服务器拥有独立IP地址,并且运行了服务器软件。 2.服务器由服务器软件和服务器硬件组成。服务器硬件就是拥有独立ip的计算机,服务器软件是一个被动的软件&#…...

鸿蒙Harmony跨模块交互
1. 模块分类介绍 鸿蒙系统的模块一共分为四种,包括HAP两种和共享包两种 HAP(Harmony Ability Package) Entry:项目的入口模块,每个项目都有且只有一个。feature:项目的功能模块,内部模式和En…...

由浅到深认识Java语言(30):集合
该文章Github地址:https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.c…...

Python学习笔记(二)
一:异常: 1.1:异常处理: 1.2:异常捕获: 1.3:异常传递: 二:模块: 2.1:模块的定义: 2.2:模块的导入: 2.3&…...

5.域控服务器都要备份哪些资料?如何备份DNS服务器?如何备份DHCP服务器?如何备份组策略?如何备份服务器状态的备份?
(2.1) NTD(域控数据库)备份 (2.2)DNS备份 (2.3)DHCP备份 (2.4)组策略备份 (2.5)CA证书备份 (2.6)系统状态备份 (2.1)…...

TCP与UDP:网络协议的技术原理与要点
文章目录 1. TCP(传输控制协议)1.1 面向连接1.1.1 三次握手1.1.2 为什么需要三次握手?1.1.3 四次挥手1.1.4 为什么需要四次挥手? 1.2 可靠性1.3 有序传输1.4 流量控制1.5 拥塞控制 2. UDP(用户数据报协议)2…...

vue-office/docx插件实现docx文件预览
1.下包 //预览docx文件 npm install vue-office/docx vue-demi//如果是vue2.6版本或以下还需要额外安装 vue/composition-api2.引入 <template><div>//在src填入文档地址<VueOfficeDocx srchttp://...../xx.docx style"width:80%" rendered"re…...

STM32—控制蜂鸣器(定时器)
目录 1 、 电路构成及原理图 2 、编写实现代码 main.c tim_irq.c 3、代码讲解 4、烧录到开发板调试、验证代码 5、检验效果 此笔记基于朗峰 STM32F103 系列全集成开发板的记录。 1 、 电路构成及原理图 定时器中断是利用定时器的计数功能(向上计数或向下计…...

【React】使用 JSX 为 JavaScript 添加标签
使用 JSX 为 JavaScript 添加标签实际上是将 JSX 语法与 JavaScript 代码结合使用,以描述用户界面。JSX 允许你在 JavaScript 中编写类似 HTML 的结构,并最终由 React 库将其转换为真正的 DOM 元素。以下是将标签引入 JavaScript 以及将 HTML 转化为 JSX…...

Docker构建多平台(x86,arm64)构架镜像
这里写自定义目录标题 背景配置buildx开启experimental重启检查 打包 背景 docker镜像需要支持不同平台架构 配置buildx 开启experimental vi /etc/docker/daemon.json {"experimental": true }或者 重启检查 # 验证buildx版本 docker buildx version# 重启do…...

python爬虫基础-----运算符(第三天)
🎈🎈作者主页: 喔的嘛呀🎈🎈 🎈🎈所属专栏:python爬虫学习🎈🎈 ✨✨谢谢大家捧场,祝屏幕前的小伙伴们每天都有好运相伴左右,一定要天天…...

Itextpdf电子签章
印章 印章是我国特有的历史文化产物,古代主要用作身份凭证和行驶职权的工具。它的起源是由于社会生活的实际需要。早在商周时代,印章就已经产生。如今的印章已成为一种独特的,融实用性和艺术性为一体的艺术瑰宝。传统的印章容易被坏人、小人…...

两台电脑简单的通信过程详解(经过两个路由器,不同网段)
一、eNSP拓扑图 二、配置4台电脑的IP地址、子网掩码、网关地址。 三、配置路由器 注意拓扑图的接口与本博客是否相符,判断以下命令中的ip是否需要修改。 1.AR1-接口对应IP <Huawei>sys #进入系统视图 [Huawei]int g0/0/0 #进入0/0/0接口 [Huawei-GigabitE…...