当前位置: 首页 > news >正文

C++Primer学习(7.1 定义抽象数据类型)

类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作:类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
类要想实现数据抽象和封装,需要首先定义一个抽象数据类型(abstract datatype)。在抽象数据类型中,由类的设计者负责考虑类的实现过程:使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。
7.1 定义抽象数据类型
在第1章中使用的sales_item 类是一个抽象数据类型,我们通过它的接口(例如1.5.1节(第17页)描述的操作)来使用一个Sales_item对象。我们不能访问Sales_item对象的数据成员,事实上,我们甚至根本不知道这个类有哪些数据成员。
与之相反,Sales_data类(参见2.6.1节,第64页)不是一个抽象数据类型。它允许类的用户直接访问它的数据成员,并且要求由用户来编写操作。要想把Sales_data变成抽象数据类型,我们需要定义一些操作以供类的用户使用。一旦Sales_data 定义了它自己的操作,我们就可以封装(隐藏)它的数据成员了。
7.1.1 设计Sales_data类
我们的最终目的是令Sales_data支持与 Sales_item 类完全一样的操作集合。Sales_item类有一个名为isbn的成员函数(member function),并且支持+、=、+=、<<和>>运算符。我们将在第14章学习如何自定义运算符。现在,我们先为这些运算定义普通(命名的)函数形式。由于14.1节(第490页)将要解释的原因,执行加法和I0的函数不作为Sales_data的成员,相反的,我们将其定义成普通函数;执行复合赋值运算的函数是成员函数。Sales_data类无须专门定义赋值运算,其原因将在7.1.5节(第239页)介绍。综上所述,Sales_data的接口应该包含以下操作:
一个isbn 成员函数,用于返回对象的ISBN编号
一个 combine 成员函数,用于将一个Sales_data对象加到另一个对象上
一个名为 add 的函数,执行两个Sales_data对象的加法
一个read函数,将数据从istream读入到Sales_data对象中
一个print函数,将Sales_data对象的值输出到ostream
关键概念:不同的编程角色
程序员们常把运行其程序的人称作用户(user)。类似的,类的设计者也是为其用户设计并实现一个类的人;显然,类的用户是程序员,而非应用程序的最终使用者。当我们提及“用户”一词时,不同的语境决定了不同的含义。如果我们说用户代码或者Sales_data类的用户,指的是使用类的程序员;如果我们说书店应用程序的用户,则意指运行该应用程序的书店经理。
Note:C++程序员们无须刻意区分应用程序的用户以及类的用户
在一些简单的应用程序中,类的用户和类的设计者常常是同一个人。尽管如此,还是最好把角色区分开来。当我们设计类的接口时,应该考虑如何才能使得类易于使用;而当我们使用类时,不应该顾及类的实现机理。
要想开发一款成功的应用程序,其作者必须充分了解并实现用户的需求。同样,优秀的类设计者也应该密切关注那些有可能使用该类的程序员的需求。作为一个设计良好的类,既要有直观且易于使用的接口,也必须具备高效的实现过程。
使用改进的 Sales_data 类
在考虑如何实现我们的类之前,首先来看看应该如何使用上面这些接口函数。举个例子,我们使用这些函数编写16节(第21页)书店程序的另外一个版本,其中不再使用Sales_item对象,而是使用Sales_data对象:

Sales data total; //保存当前求和结果的变量
if (read(cin,total))//读入第一笔交易
{Sales data trans;//保存下一条交易数据的变量while(read(cin,trans))//读入剩余的交易{if (total.isbn()== trans.isbn())//检查isbntotal.combine(trans);//更新变量total当前的值else{print(cout,total)<<endl;//输出结果total = trans;//处理下一本书}}print(cout,total)<<endl;//输出最后一条交易
}
else
{cerr <<"No data?!"<< endl;//没有输入任何信息//通知用户
}

一开始我们定义了一个 Sales data对象用于保存实时的汇总信息。在if条件内部,调用read 函数将第一条交易读入到total中,这里的条件部分与之前我们使用>>运算符的效果是一样的。read函数返回它的流参数,而条件部分负责检查这个返回值(参见 4.11.2节,第144页),如果read函数失败,程序将直接跳转到else语句并输出一条错误信息。如果检测到读入了数据,我们定义变量trans用于存放每一条交易。while语句的条件部分同样是检查read函数的返回值,只要输入操作成功,条件就被满足,意味着我们可以处理一条新的交易。
在 while循环内部,我们分别调用total和trans的isbn成员以比较它们的ISBN编号。如果 total和trans指示的是同一本书,我们调用combine 函数将 trans 的内容添加到 total表示的实时汇总结果中去。如果trans 指示的是一本新书,我们调用print函数将之前一本书的汇总信息输出出来。因为 print 返回的是它的流参数的引用,所以我们可以把 print 的返回值作为<<运算符的左侧运算对象。通过这种方式,我们输出 print 函数的处理结果,然后转到下一行。接下来,把trans 赋给 total,从而为接着处理文件中下一本书的记录做好了准备。
7.1.2定义改进的Sales_data类
改进之后的类的数据成员将与2.6.1节(第64页)定义的版本保持一致,它们包括:bookNo,string类型,表示ISBN编号;units_sold,unsigned 类型,表示某本书的销量;以及revenue,double类型,表示这本书的总销售收入。
如前所述,我们的类将包含两个成员函数:combine和isbn。此外,我们还将赋了Sales_data另一个成员函数用于返回售出书籍的平均价格,这个函数被命名为avg_price。因为avg_price 的目的并非通用,所以它应该属于类的实现的一部分,而非接口的一部分。
定义和声明成员函数的方式与普通函数差不多。成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,例如add、read和print等,它们的定义和声明都在类的外部。
由此可知,改进的Sales_data类应该如下所示:

struct Sales_data
{//新成员:关于Sales_data对象的操作std::string isbn()const{return bookNo;}Sales_data& combine(const Sales data&);double avg price()const;//数据成员和2.6.1节(第64页)相比没有改变std::string bookNo;unsiqned units_sold=0double revenue=0.0;
}
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream &print(std::ostream&const Sales_data&);
std::istream &read(std::istream&,Sales_data&);

定义在类内部的函数是隐式的inline函数(参见6.5.2节,第214页)。
定义成员函数
尽管所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。对于 Sales_data类来说,isbn函数定义在了类内,而 combine和avg_price定义在了类外。
我们首先介绍isbn函数,它的参数列表为空,返回值是一个string对象:

std::string isbn() const {return bookNo};

和其他函数一样,成员函数体也是一个块。在此例中,块只有一条return语句,用于返sales_data对象的 bookNo 数据成员。关于 isbn 函数一件有意思的事情是:它是如何获得 bookNo 成员所依赖的对象的呢?
引入 this
让我们再一次观察对isbn 成员函数的调用:

total.isbn()

在这里,我们使用了点运算符来访问 total对象的 isbn 成员然后调用它。当我们调用成员函数时,实际上是在替某个对象调用它。如果isbn指向Sales_data的成员(例如 bookNo),则它隐式地指向调用该函数的对象的成员。在上面所示的调用中,当isbn返回bookNo 时,实际上它隐式地返回total.bookNo。
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如,如果调用

total.isbn()

则编译器负责把 total的地址传递给isbn 的隐式形参 this,可以等价地认为编译器将该调用重写成了如下的形式:

//伪代码,用于说明调用成员函数的实际执行过程
Sales_data::isbn(&total)

其中,调用Sales_data的isbn 成员时传入了total的地址。
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看作 this 的隐式引用,也就是说,当isbn 使用 bookNo 时,它隐式地使用 this 指向的成员,就像我们书写了this->bookNo一样。对于我们来说,this形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。我们可以在成员函数体内部使用this,因此尽管没有必要,但我们还是能把isbn定义成如下的形式:

std::string isbn()const {return this->bookNo;}

因为this的目的总是指向“这个”对象,所以this 是一个常量指针,我们不允许改变this中保存的地址.
引入const 成员函数
isbn 函数的另一个关键之处是紧随参数列表之后的const 关键字,这里,const的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针。例如在 Sales_data成员函数中,this的类型是 Sales_data *const。尽管 this 是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下)我们不能把this绑定到一个常量对象上。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。(在 C++ 中,常量对象(const object)不能调用普通成员函数,但可以调用常量成员函数(const member function)。这是 C++ 的一种安全机制,用于保证常量对象的状态不可被修改。)
补充知识:

class MyClass {
public:void modifyValue() { value = 42; } // 普通成员函数(可修改对象)int getValue() const { return value; } // 常量成员函数(不可修改对象)
private:int value;
};int main() {const MyClass obj; // 常量对象obj.modifyValue();  // 错误!普通成员函数不能被常量对象调用obj.getValue();     // 正确!常量成员函数可以被调用return 0;
}

补充:指针的类型必须与其所指对象的类型一致,非常量指针(普通指针)不能直接指向常量。但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。指向常量的指针是不能通过这个指针修改所指向的对象值。
如果 isbn 是一个普通函数而且 this 是一个普通的指针参数,则我们应该把 this声明成 const Sales_data*const。毕竟,在isbn 的函数体内不会改变 this 所指的对象,所以把 this 设置为指向常量的指针有助于提高函数的灵活性。
然而,this是隐式的并且不会出现在参数列表中,所以在哪儿将this声明成指向常量的指针就成为我们必须面对的问题。C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数(const member function)
可以把isbn 的函数体想象成如下的形式:

//伪代码,说明隐式的 this 指针是如何使用的
//下面的代码是非法的:因为我们不能显式地定义自己的this 指针
//谨记此处的 this 是一个指向常量的指针,因为isbn 是一个常量成员
std::string Sales_data::isbn(const Sales_data *const this)
{return this->isbn;}

因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容。在上例中,isbn 可以读取调用它的对象的数据成员,但是不能写入新值。
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

知识复习:常量指针和指向常量的指针
(一)指向常量的指针
定义和语法
指向常量的指针(Pointer to a constant)是指指针所指向的对象是常量,不能通过该指针来修改所指向对象的值,但指针本身可以指向其他对象。其语法形式通常有两种:

const 数据类型 *指针名;
数据类型 const *指针名;

特点
不能通过指针修改所指对象的值:一旦指向一个常量对象,就不能通过该指针来改变这个对象的值,即使该对象本身可能并不是常量。
指针本身可以改变指向:可以将该指针指向其他对象。

#include <iostream>
int main() 
{int num1 = 10;int num2 = 20;// 定义一个指向常量的指针const int *ptr = &num1;// 不能通过指针修改所指对象的值,以下语句会报错// *ptr = 30; // 指针本身可以改变指向ptr = &num2;std::cout << "Value pointed by ptr: " << *ptr << std::endl;return 0;
}

在上述代码中,const int *ptr 定义了一个指向常量的指针 ptr,它指向 num1。由于 ptr 是指向常量的指针,所以不能通过 *ptr 来修改 num1 的值。但可以将 ptr 重新指向 num2。
(二)常量指针
定义和语法
常量指针(Constant pointer)是指指针本身是常量,一旦初始化后就不能再指向其他对象,但可以通过该指针修改所指向对象的值。其语法形式为:

数据类型 * const 指针名;

特点
指针本身不能改变指向:初始化时必须指定所指向的对象,之后不能再将该指针指向其他对象。
可以通过指针修改所指对象的值:如果所指向的对象不是常量,那么可以通过该指针来修改对象的值。

#include <iostream>
int main() 
{int num1 = 10;int num2 = 20;// 定义一个常量指针int * const ptr = &num1;// 指针本身不能改变指向,以下语句会报错// ptr = &num2; // 可以通过指针修改所指对象的值*ptr = 30;std::cout << "Value of num1: " << num1 << std::endl;return 0;
}

(三)在函数参数中使用指向常量的常量指针
作用
指向常量的常量指针(const 数据类型 * const)结合了上述两者的特点,既不能改变指针的指向,也不能通过指针修改所指向的数据。用于函数既不需要修改数据,也不允许指针指向其他对象的场景。

#include <iostream>
// 函数接受一个指向常量的常量指针作为参数
void display(const int * const ptr) 
{// 可以读取所指向的值std::cout << "Value: " << *ptr << std::endl;// 不能通过指针修改所指向的值,以下语句会导致编译错误// *ptr = 200; // 不能改变指针本身的指向,以下语句会导致编译错误// int anotherNum = 30;// ptr = &anotherNum; 
}
int main() 
{int num = 50;display(&num);return 0;
}

代码解释
在 display 函数中,参数 const int * const ptr 是一个指向常量的常量指针。这意味着在函数内部,既不能通过 ptr 修改所指向的值,也不能让 ptr 指向其他对象。在 main 函数中,将 num 的地址传递给 display 函数,display 函数只能读取 num 的值,不能对其进行任何修改。
类作用域和成员函数
回忆之前我们所学的知识,类本身就是一个作用域(参见2.6.1节,第64页)。类的成员函数的定义嵌套在类的作用域之内,因此,isbn中用到的名字bookNo其实就是定义在 sales_data内的数据成员。
值得注意的是,即使bookNo定义在isbn之后,isbn也还是能够使用bookNo。就如我们将在7.4.1节(第254页)学习到的那样,编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
在类的外部定义成员函数
像其他函数一样,当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:

double Sales_data::avg_price()const
{if (units_sold)return revenue/units_sold;elsereturn 0;
}

函数名 Sales_data::avg_price使用作用域运算符来说明如下的事实:我们定义了一个名为avg_price的函数,并且该函数被声明在类sales_data的作用域内。一旦编译器看到这个函数名,就能理解剩余的代码是位于类的作用域内的。因此,当avg_price使用revenue和units_sold 时,实际上它隐式地使用了Sales_data的成员。
定义一个返回 this 对象的函数
函数combine的设计初衷类似于复合赋值运算符+=,调用该函数的对象代表的是赋值运算符左侧的运算对象,右侧运算对象则通过显式的实参被传入函数:

Sales_data& Sales_data::combine(const Sales_data &rhs)
{units_sold +=rhs.units_sold;//把rhs的成员加到this 对象的成员上revenue +=rhs.revenue;return *this;//返回调用该函数的对象
}

当我们的交易处理程序调用如下的函数时,

total.combine(trans);//更新变量total当前的值

total 的地址被绑定到隐式的 this 参数上,而rhs 绑定到了 trans 上。因此,当
combine 执行下面的语句时,

units_sold +=rhs.units sold;//把rhs的成员添加到this 对象的成员中

效果等同于求 total.units_sold和trans.unit_sold 的和,然后把结果保存到total.units_sold中。
该函数一个值得关注的部分是它的返回类型和返回语句。一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符把它的左侧运算对象当成左值返回,因此为了与它保持一致combine函数必须返回引用类型。因为此时的左侧运算对象是一个Sales_data的对象,所以返回类型应该是Sales_data&。
如前所述,我们无须使用隐式的this指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问:

return *this;//返回调用该函数的对象

其中,return语句解引用this指针以获得执行该函数的对象,换句话说,上面的这个调用返回 total 的引用。
7.1.3 定义类相关的非成员雨数
类的作者常常需要定义一些辅助函数,比如 add、read 和print 等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。
我们定义非成员函数的方式与定义其他函数一样,通常把函数的声明和定义分离开来。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入一个文件。
Note一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
定义 read 和 print 函数
下面的read和print 函数与 2.6.2节(第66页)中的代码作用一样,而且代码本身也非常相似:

//输入的交易信息包括ISBN、售出总数和售出价格
istream &read(istream &is,Sales_data &item)
{double price=0;is >>item.bookNo >>item.units_sold >> price;item.revenue =price *item.units_sold;return is;
}ostream &print(ostream &os,const Sales data &item)
{os<< item.isbn()<<" "<< item.units_sold <<" "<<item.revenue <<" "<< item.avg_price();return os;
}

read函数从给定流中将数据读到给定的对象里,print函数则负责将给定对象的内容打印到给定的流中。
除此之外,关于上面的函数还有两点是非常重要的。第一点,read和print分别接受一个各自IO类型的引用作为其参数,这是因为IO类属于不能被拷贝的类型,因此我们只能通过引用来传递它们。而且,因为读取和写入的操作会改变流的内容,所以两个函数接受的都是普通引用,而非对常量的引用。
第二点,print函数不负责换行。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。
定义 add 函数
add 函数接受两个 Sales_data对象作为其参数,返回值是一个新的 Sales_data,用于表示前两个对象的和:

Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{//把lhs的数据成员拷贝给sumSales_data sum =lhs;sum.combine(rhs);//把rhs的数据成员加到sum当中return sum;
}

在函数体中,我们定义了一个新的Sales_data对象并将其命名为sum。sum 将用于存放两笔交易的和,我们用lhs的副本来初始化sum。默认情况下,拷贝类的对象其实拷贝的是对象的数据成员。在拷贝工作完成之后,sum的bookNo、units_sold和revenue将和 lhs 一致。接下来我们调用combine函数,将rhs的units_sold和revenue添加给 sum。最后,函数返回sum 的副本。
7.1.4 构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
在这一节中,我们将介绍定义构造函数的基础知识。构造函数是一个非常复杂的问题,我们还会在7.5节(第257页)、15.7节(第551页)、18.1.3节(第689页)和第13 章介绍更多关于构造函数的知识。
构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型:除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
不同于其他成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
合成的默认构造函数
我们的 sales_data类并没有定义任何构造函数,可是之前使用了sales_data对象的程序仍然可以正确地编译和运行。举个例子,第229页的程序定义了两个对象:

Sales_data total;//保存当前求和结果的变量
Sales_data trans;//保存下一条交易数据的变量

这时我们不禁要问:total和trans是如何初始化的呢?
我们没有为这些对象提供初始值,因此我们知道它们执行了默认初始化。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(default constructor)。默认构造函数无须任何实参。
如我们所见,默认构造函数在很多方面都有其特殊性。其中之一是,如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。
编译器创建的构造函数又被称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:
(1)如果存在类内的初始值,用它来初始化成员。
(2)否则,默认初始化该成员。
因为 Sales_data为units_sold和revenue 提供了初始值,所以合成的默认构造函数将使用这些值来初始化对应的成员;同时,它把bookNo默认初始化成一个空字符串。
某些类不能依赖于合成的默认构造函数
合成的默认构造函数只适合非常简单的类,比如现在定义的这个sales_data 版本。对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:
第一个原因也是最容易理解的一个原因就是编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。这条规则的依据是,如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制。只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。
第二个原因是对于某些类来说,合成的默认构造函数可能执行错误的操作。回忆我们之前介绍过的,如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的。该准则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
第三个原因是有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。在13.1.6节(第449页)中我们将看到还有其他一些情况也会导致编译器无法生成一个正确的默认构造函数。
定义 Sales_data 的构造函数
对于我们的 Sales_data类来说,我们将使用下面的参数定义4个不同的构造函数:
(1)一个istream&,从中读取一条交易信息。一个unsigned,表示售出的图书数量:一个const string&,表示ISBN编号;一个 double,表示图书的售出价格。
(2)一个const string&,表示ISBN编号;编译器将赋予其他成员默认值。
(3)一个空参数列表(即默认构造函数),正如刚刚介绍的,既然我们已经定义了其他构造函数,那么也必须定义一个默认构造函数。
给类添加了这些成员之后,将得到

struct Sales_data
{//新增的构造函数Sales_data()=default;Sales_data(const std::string &s):bookNo(s){ }Sales_data(const std::string &s,unsigned n,double p):bookNo(s)units_sold(n)revenue(p*n) { }Sales_data(std::istream &);//之前已有的其他成员std::string isbn()const {return bookNo;}Sales_data& combine(const Sales_data&);double avg_price()const;std::string bookNo;unsiqned units_sold=0;double revenue =0.0
}

= default 的含义
我们从解释默认构造函数的含义开始:
Sales data()=default;
首先请明确一点:因为该构造函数不接受任何实参,所以它是一个默认构造函数。我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数
在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中,=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default在类的内部,则默认构造函数是内联的:如果它在类的外部,则该成员默认情况下不是内联的。
WARNING:上面的默认构造函数之所以对 Sales_data 有效,是因为我们为内置类型的数据成员提供了初始值。如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表(马上就会介绍)来初始化类的每个成员
构造函数初始值列表
接下来我们介绍类中定义的另外两个构造函数:

Sales_data(const std::string &s):bookNo(s){ }
Sales_data(const std::string &s,unsigned n,double p):bookNo(s)units_sold(n)revenue(p*n) { }

这两个定义中出现了新的部分,即冒号以及冒号和花括号之间的代码,其中花括号定义了(空的)函数体。我们把新出现的部分称为**构造函数初始值列表(constructor initialize list),**它负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来。
含有三个参数的构造函数分别使用它的前两个参数初始化成员bookNo和units_sold,revenue的初始值则通过将售出图书总数和每本书单价相乘计算得到。只有一个 string类型参数的构造函数使用这个string对象初始化bookNo,对于units_sold和revenue则没有显式地初始化。当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。在此例中,这样的成员使用类内初始值初始化,因此只接受一个string参数的构造函数等价于

//与上面定义的那个构造函数效果相同
Sales data(const std::string &s):bookNo(s),units_sold(0),revenue(0){}

通常情况下,构造函数使用类内初始值不失为一种好的选择,因为只要这样的初始值存在我们就能确保为成员赋予了一个正确的值。不过,如果你的编译器不支持类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。
BestPractices:构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。如果你不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。有一点需要注意,在上面的两个构造函数中函数体都是空的。这是因为这些构造函数的唯一目的就是为数据成员赋初值,一旦没有其他任务需要执行,函数体也就为空了。
在类的外部定义构造函数
与其他几个构造函数不同,以istream 为参数的构造函数需要执行一些实际的操作。在它的函数体内,调用了read函数以给数据成员赋以初值:

Sales_data::Sales_data(std::istream &is)
{read(is,*this);//read函数的作用是从is中读取一条交易信息然后//存入this 对象中
}

构造函数没有返回类型,所以上述定义从我们指定的函数名字开始。和其他成员函数一样,当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。因此,Sales_data::Sales_data的含义是我们定义 Sales_data 类的成员,它的名字是Sales_data。又因为该成员的名字和类名相同,所以它是一个构造函数。
这个构造函数没有构造函数初始值列表,或者讲得更准确一点,它的构造函数初始值列表是空的。尽管构造函数初始值列表是空的,但是由于执行了构造函数体,所以对象的成员仍然能被初始化。
没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化。对于sales_data来说,这意味着一旦函数开始执行则bookNo将被初始化成空string对象,而units_sold和revenue 将是0。
为了更好地理解调用函数read的意义,要特别注意read 的第二个参数是一个Sales_data对象的引用。在7.1.2节(第232页)中曾经提到过,使用this 来把对象当成一个整体访问,而非直接访问对象的某个成员。因此在此例中,我们使用 * this将“this”对象作为实参传递给read 函数。
7.1.5 拷贝、赋值和析构
除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等。当我们使用了赋值运算符时会发生对象的赋值操作。当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁,当vector对象(或者数组)销毁时存储在其中的对象也会被销毁。
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。例如在7.1.1节(第229页)的书店程序中,当编译器执行如下赋值语句时,

total=trans;//处理下一本书的信息

它的行为与下面的代码相同

//sales_data的默认赋值操作等价于:
total.bookNo =trans.bookNo;
total.units_sold =trans.units_sold;
total.revenue =trans.revenue;

我们将在第13章中介绍如何自定义上述操作。
某些类不能依赖于合成的版本
尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是必须要清楚的一点是,对于某些类来说合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。举个例子,第12章将介绍C++程序是如何分配和管理动态内存的。而在13.14节(第447页)我们将会看到,管理动态内存的类通常不能依赖于上述操作的合成版本。
不过值得注意的是,很多需要动态内存的类能(而且应该)使用vector对象或者string对象管理必要的存储空间。使用vector或者string的类能避免分配和释放内存带来的复杂性。
进一步讲,如果类包含vector或者string成员,则其拷贝、赋值和销毁的合成版本能够正常工作。当我们对含有vector成员的对象执行拷贝或者赋值操作时,vector类会设法拷贝或者赋值成员中的元素。当这样的对象被销毁时,将销毁vector 对象,也就是依次销毁 vector中的每一个元素。这一点与string是非常类似的。
WARNING:在学习第 13 章关于如何自定义操作的知识之前,类中所有分配的资源都应该直接以类的数据成员的形式存储。

相关文章:

C++Primer学习(7.1 定义抽象数据类型)

类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作:类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。 封…...

Vue 3 Diff 算法深度解析:与 Vue 2 双端比对对比

文章目录 1. 核心算法概述1.1 Vue 2 双端比对算法1.2 Vue 3 快速 Diff 算法 2. 算法复杂度分析2.1 时间复杂度对比2.2 空间复杂度对比 3. 核心实现解析3.1 Vue 2 双端比对代码3.2 Vue 3 快速 Diff 代码 4. 性能优化分析4.1 性能测试数据4.2 内存使用对比 5. 使用场景分析5.1 Vu…...

启动桌面Docker提示虚拟服务未启动

在启动 Docker Desktop 时&#xff0c;可能会遇到以下提示&#xff1a; Docker Desktop - Virtual Machine Platform not enabled Virtual Machine Platform not enabled该错误通常是由于 Windows 未启用 “Virtual Machine Platform” 功能导致的&#xff0c;这是运行 Docker…...

【SpringBoot】实现登录功能

在上一篇博客中&#xff0c;我们讲解了注册页面的实现。在此基础上会跳转到登录页面&#xff0c;今天给大家带来的是使用 SpringBoot&#xff0c;MyBatis&#xff0c;Html&#xff0c;CSS&#xff0c;JavaScript&#xff0c;前后端交互实现一个登录功能。 目录 一、效果 二、…...

DataWhale 速通AI编程开发:(进阶篇)第3章 提示词(Prompts)配置项

学习网址&#xff1a;Datawhale-学用 AI,从此开始 3.1 Roo Code提示词配置了什么 众所周知&#xff0c;提示词&#xff08;Prompt&#xff09;是用户向大语言模型输入的一段文本&#xff0c;用于指导大语言模型生成符合用户要求的输出。在ai编程领域更是如此&#xff0c;提示…...

VUE中VNode(虚拟节点)是个啥?

用 JavaScript 生成 Virtual DOM&#xff08;VNode&#xff09; 在 Vue 中&#xff0c;Virtual DOM&#xff08;虚拟 DOM&#xff09;是一个用 JavaScript 对象表示真实 DOM 结构的抽象层。通过这种方式&#xff0c;Vue 可以通过比较 Virtual DOM 与真实 DOM 的差异来最小化更…...

力扣:3. 无重复字符的最长子串(滑动窗口)

3. 无重复字符的最长子串 - 力扣&#xff08;LeetCode&#xff09;3. 无重复字符的最长子串 - 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长 子串 的长度。 示例 1:输入: s "abcabcbb"输出: 3 解释: 因为无重复字符的最长子串是 "abc"…...

注解+AOP实现权限控制

注解与AOP实战&#xff1a;实现权限控制 在现代Java开发中&#xff0c;注解&#xff08;Annotation&#xff09;和面向切面编程&#xff08;AOP&#xff09;是两种强大的技术&#xff0c;它们能够帮助我们实现代码的解耦&#xff0c;提高代码的可读性和可维护性。本文将通过一…...

2.5 python接口编程

在现代软件开发的复杂生态系统中&#xff0c;不同系统、模块之间的交互协作至关重要。接口编程作为一种关键机制&#xff0c;定义了组件之间的通信规范与交互方式。Python 凭借其卓越的灵活性、丰富的库资源以及简洁易读的语法&#xff0c;在接口编程领域占据了重要地位&#x…...

睡不着运动锻炼贴士

在快节奏的现代生活中&#xff0c;失眠似乎已成为许多人的“夜间伴侣”。夜晚辗转反侧&#xff0c;白天精神不振&#xff0c;这样的恶性循环让许多人苦不堪言。其实&#xff0c;除了调整作息和饮食习惯&#xff0c;适当的运动也是改善睡眠的一剂良药。今天&#xff0c;就让我们…...

【Python入门】一篇掌握Python中的字典(创建、访问、修改、字典方法)【详细版】

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;《Python/PyTorch极简课》_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目…...

深入理解 HTML 表单与输入

在网页开发的广袤领域中&#xff0c;HTML 表单如同搭建用户与服务器沟通桥梁的基石。它是收集用户输入信息的关键渠道&#xff0c;承载着交互的重任。今天&#xff0c;就让我们一同深入探索 HTML 表单与输入的奥秘。​ HTML 表单在文档中划定出一片独特的区域&#xff0c;这片…...

宝塔docker切换存储目录

1、 停止 Docker 服务 sudo systemctl stop docker2、迁移 Docker 数据目录 sudo mkdir -p /newpath/docker sudo rsync -avz /var/lib/docker/ /newpath/docker/3、修改 Docker 配置文件 vi /etc/docker/daemon.json 内容 {"data-root": "/newpath/docker&q…...

IPoIB驱动中RSS与TSS技术的深度解析:多队列机制与性能优化

在高速网络通信中,IP over InfiniBand(IPoIB) 是实现低延迟、高吞吐的关键技术之一。为了充分发挥多核处理器的性能潜力,IPoIB驱动通过 接收侧扩展(RSS) 和 发送侧扩展(TSS) 技术,实现了数据包处理的多队列并行化。本文结合源码实现与性能优化策略,深入解析其核心机制…...

目前人工智能的发展,判断10年、20年后的人工智能发展的主要方向,或者带动的主要产业

根据2025年的最新行业研究和技术演进趋势&#xff0c;结合历史发展轨迹&#xff0c;未来10-20年人工智能发展的主要方向及带动的产业将呈现以下六大核心趋势&#xff1a; 一、算力革命与底层架构优化 核心地位&#xff1a;算力将成为类似“新能源电池”的基础设施&#xff0c;…...

DeepSeek-prompt指令-当DeepSeek答非所问,应该如何准确的表达我们的诉求?

当DeepSeek答非所问&#xff0c;应该如何准确的表达我们的诉求&#xff1f;不同使用场景如何向DeepSeek发问&#xff1f;是否有指令公式&#xff1f; 目录 1、 扮演专家型指令2、 知识蒸馏型指令3、 颗粒度调节型指令4、 时间轴推演型指令5、 极端测试型6、 逆向思维型指令7、…...

并发编程面试题二

1、java线程常见的基本状态有哪些&#xff0c;这些状态分别是做什么的 &#xff08;1&#xff09;创建&#xff08;New&#xff09;&#xff1a;new Thread()&#xff0c;生成线程对象。 &#xff08;2&#xff09;就绪&#xff08;Runnable&#xff09;:当调用线程对象的sta…...

【NLP】 8. 处理常见词(Stopwords)的不同策略

处理常见词&#xff08;Stopwords&#xff09;的不同策略 在自然语言处理 (NLP) 和信息检索 (IR) 任务中&#xff0c;常见词&#xff08;Stopwords&#xff09; 是指在文本中频繁出现但通常对主要任务贡献较小的词&#xff0c;例如 “the”、“is”、“in”、“and” 等。这些…...

【Java基础】java中的lambda表达式

Java Lambda表达式深度解析&#xff1a;语法、简化规则与实战 前言 Java 8的Lambda表达式通过简化匿名内部类和引入函数式编程&#xff0c;极大提升了代码的简洁性和可读性。 一、Lambda表达式的核心语法 Lambda表达式由参数列表、->符号和表达式主体组成&#xff0c;其基…...

【RS】OneRec快手-生成式推荐模型

note 本文提出了一种名为 OneRec 的统一生成式推荐框架&#xff0c;旨在替代传统的多阶段排序策略&#xff0c;通过一个端到端的生成模型直接生成推荐结果。OneRec 的主要贡献包括&#xff1a; 编码器-解码器结构&#xff1a;采用稀疏混合专家&#xff08;MoE&#xff09;架构…...

DQN 玩 2048 实战|第一期!搭建游戏环境(附 PyGame 可视化源码)

视频讲解&#xff1a; DQN 玩 2048 实战&#xff5c;第一期&#xff01;搭建游戏环境&#xff08;附 PyGame 可视化源码&#xff09; 代码仓库&#xff1a;GitHub - LitchiCheng/DRL-learning: 深度强化学习 2048游戏介绍&#xff0c;引用维基百科 《2048》在44的网格上进行。…...

练习题:87

目录 Python题目 题目 题目分析 代码实现 代码解释 列表推导式部分&#xff1a; 变量赋值和输出&#xff1a; 运行思路 结束语 Python题目 题目 使用列表推导式生成一个包含 1 到 100 中所有偶数的列表。 题目分析 本题要求使用 Python 的列表推导式生成一个包含 …...

二叉树的层序遍历(102)

102. 二叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* …...

NVMe集群:加速数据处理

随着大数据和云计算的快速发展&#xff0c;企业面临着前所未有的数据处理挑战。传统的存储技术和架构已经难以满足现代应用对高性能和低延迟的需求。在这种背景下&#xff0c;NVMe&#xff08;Non-Volatile Memory Express&#xff09;集群应运而生&#xff0c;它以其卓越的性能…...

JUC并发编程:共享模型之管程

一、共享带来的问题 &#xff08;1&#xff09;Java的体现 两个线程对初始值为 0 的静态变量一个做自增&#xff0c;一个做自减&#xff0c;各做 5000 次&#xff0c;结果是 0 吗&#xff1f; &#xff08;2&#xff09;问题分析 以上的结果可能是正数、负数、零。为什么呢…...

Java构造方法详解:从入门到实战

目录 一、什么是构造方法&#xff1f; 二、构造方法的作用 三、构造方法分类与使用 1. 默认构造方法 2. 有参构造方法 3. 构造方法重载 四、注意事项&#xff08;避坑指南&#xff09; 五、经典面试题解析 六、实战应用场景 七、总结 一、什么是构造方法&#xff1f; …...

Uniapp 字体加载问题(文件本地存储)

项目场景&#xff1a; 在最近公司开发一款小程序&#xff0c;但是小程序的文字需要用艺术字&#xff0c;就是那种不能用切图绕开的那种&#xff01; 问题描述 我们在使用uni.loadfontface Api请求数据字体文件的时候总是会报错&#xff0c;就是那种网上也找不到解决方法的那种…...

HTML 新手入门:从零基础到搭建第一个静态页面(一)

开启 HTML 学习之旅 在互联网的广袤世界中&#xff0c;网页是我们与信息交互的主要窗口。而 HTML&#xff0c;作为构建网页的基石&#xff0c;就像是搭建房屋的砖块&#xff0c;是网页开发中不可或缺的基础。无论你是对网页开发充满好奇的小白&#xff0c;还是渴望系统学习前端…...

使用multiprocessing实现进程间共享内存

在 Python 中,可以使用多种方法来实现几个进程之间的通信。 简单消息传递:使用 multiprocessing.Queue 或 multiprocessing.Pipe。 共享简单数据:使用 multiprocessing.Value 或 multiprocessing.Array。 共享复杂数据:使用 multiprocessing.Manager。 进程间信号控制:使用…...

在IDEA中连接达梦数据库:详细配置指南

达梦数据库&#xff08;DM Database&#xff09;作为国产关系型数据库的代表&#xff0c;广泛应用于企业级系统开发。本文将详细介绍如何在IntelliJ IDEA中配置并连接达梦数据库&#xff0c;助力开发者高效完成数据库开发工作。 准备工作 1. 下载达梦JDBC驱动 访问达梦官方资…...

docker无法正常拉取镜像问题的解决

目录 1.前言 2.解决方案 1.前言 安装docker后拉取镜像&#xff0c;遇见了如下问题&#xff1a; Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded whil…...

如何在保持安全/合规的同时更快地构建应用程序:DevOps 指南

随着敏捷思维方式的兴起&#xff0c;开发和 DevOps 团队都面临着持续的压力&#xff0c;他们需要以迭代方式缩短发布周期并加快部署速度&#xff0c;以满足不断增长的客户期望。随着这种对速度的追求越来越强烈&#xff0c;维护安全性和合规性标准的复杂性也随之增加。 当今 D…...

SQL Server查询优化

最常用&#xff0c;最有效的数据库优化方式 查询语句层面 避免全表扫描 使用索引&#xff1a;确保查询条件中的字段有索引。例如&#xff0c;查询语句 SELECT * FROM users WHERE age > 20&#xff0c;若 age 字段有索引&#xff0c;数据库会利用索引快速定位符合条件的记…...

iOS底层原理系列04-并发编程

在移动应用开发中&#xff0c;流畅的用户体验至关重要&#xff0c;而并发编程是实现这一目标的关键技术。本文将深入探讨iOS平台上的并发编程和多线程架构&#xff0c;帮助你构建高性能、响应迅速的应用程序。 1. iOS线程调度机制 1.1 线程本质和iOS线程调度机制 线程是操作…...

企业数字化转型数据治理解决方案(119页PPT)(文末有下载方式)

资料解读&#xff1a;企业数字化转型数据治理解决方案 详细资料请看本解读文章的最后内容。 在当今数字化时代&#xff0c;数据已经成为企业最宝贵的资产之一。然而&#xff0c;随着数据量的激增和数据来源的多样化&#xff0c;如何有效管理和利用这些数据成为了企业面临的一…...

git报错:“fatal:refusing to merge unrelated histories“

新建仓库&#xff0c;克隆本地项目到新仓库&#xff0c;首次同步本地已提交的代码到远程时&#xff0c;报错&#xff1a;"fatal:refusing to merge unrelated histories" 。 报错意思是&#xff1a;致命的&#xff1a;拒绝合并无关的历史。 一、问题背景&#xff…...

Jmeter下载及环境配置

Jmeter下载及环境配置 java环境变量配置配置jdk环境变量检查是否配置成功JMeter下载 java环境变量配置 访问地址&#xff1a; https://www.oracle.com/cn/java/technologies/downloads/ 注意&#xff1a;需要自己注册账号 下载完成&#xff0c;解压后的目录为&#xff1a; …...

K8S学习之基础二十四:k8s的持久化存储之pv和pvc

K8S的存储之pv和pvc 在 Kubernetes (k8s) 中&#xff0c;持久化存储是通过 PersistentVolume (PV) 和 PersistentVolumeClaim (PVC) 来实现的。PVC 是用户对存储资源的请求&#xff0c;而 PV 是集群中的实际存储资源。PVC 和 PV 的关系类似于 Pod 和 Node 的关系。 Persisten…...

1.5、Java构造方法重载

构造方法重载的实现 &#xff08;1&#xff09;定义多个构造方法 class Person {private String name;private int age;// 无参构造方法public Person() {this.name "Unknown";this.age 0;}// 带一个参数的构造方法public Person(String name) {this.name name;…...

领域驱动设计(DDD)技术分享:从三层架构到DDD的进化之旅

一、开篇话&#xff1a;我们为什么要聊DDD&#xff1f; 如果你像我一样有着Java开发背景&#xff0c;那Spring的三层架构可能是你的老朋友了。Controller-Service-DAO这种模式简直就像我们编程的"家常便饭"。但是&#xff0c;随着业务越来越复杂&#xff0c;你是否也…...

LeetCode - #227 基于 Swift 实现基本计算器

摘要 在这篇文章中&#xff0c;我们将实现一个基于 Swift 语言的基本计算器。该计算器能够解析和计算包含 、-、* 和 / 的数学表达式&#xff0c;并且遵循运算符的优先级规则。整数除法仅保留整数部分&#xff0c;不能使用 eval() 这样的内置解析方法。 描述 给你一个字符串表…...

Elasticsearch Java High Level Client [7.17] 使用

es 的 HighLevelClient存在es源代码的引用&#xff0c;结合springboot使用时&#xff0c;会存在es版本的冲突&#xff0c;这里记录下解决冲突和使用方式&#xff08;es已经不建议使用这个了&#xff09;。 注意es服务端的版本需要与client的版本对齐&#xff0c;否则返回数据可…...

[多线程]基于环形队列(RingQueue)的生产者-消费者模型的实现

标题&#xff1a;[多线程]基于环形队列&#xff08;RingQueue&#xff09;的生产者-消费者模型 水墨不写bug 一、模型实现 接下来我们要实现一个基于环形队列&#xff08;RingQueue&#xff09;的生产者-消费者模型。该模型使用信号量和互斥锁来保证生产者和消费者之间的同步与…...

HAL库STM32常用外设—— CAN通信(一)

文章目录 一、CAN是什么&#xff1f;1.1 CAN应用场景1.2 CAN通信优势 二、CAN基础知识介绍2.1 CAN总线结构2.2 CAN总线特点2.2.1 CAN总线的数据传输特点2.2.2 位时序和波特率 2.3 CAN位时序和波特率2.3 CAN物理层2.3.1 CAN 物理层特性2.3.2 CAN 收发器芯片介绍 2.4 CAN协议层2.…...

分页查询的实现

目录 前言 一.问题描述 二.后端实现步骤 2.1配置PageHelper插件 ①导入依赖 ②在application.yml配置文件中添加相关配置 2.2编写一个入门的程序&#xff0c;体验分页过程 2.3定义一个vo&#xff0c;用来收集分页后的所有信息 2.4修改serviceImpl层的代码 2.5动态设…...

Sourcetree——使用.gitignore忽略文件或者文件夹

一、为何需要文件忽略机制&#xff1f; 1.1 为什么要会略&#xff1f; 对于开发者而言&#xff0c;明智地选择忽略某些文件类型&#xff0c;能带来三大核心优势&#xff1a; 仓库纯净性&#xff1a;避免二进制文件、编译产物等污染代码库 安全防护&#xff1a;防止敏感信息&…...

Thinkphp的belongsToMany(多对多) 和 hasManyThrough(远程一对多)的区别是什么?

虽然 belongsToMany&#xff08;多对多&#xff09; 和 hasManyThrough&#xff08;远程一对多&#xff09; 都会使用 JOIN 查询&#xff0c;但它们的核心区别在于 关联关系的本质不同&#xff0c;具体如下&#xff1a; 1️⃣ belongsToMany&#xff08;多对多&#xff09; &a…...

DataWhale 大语言模型 - 大模型技术基础

本课程围绕中国人民大学高瓴人工智能学院赵鑫教授团队出品的《大语言模型》书籍展开&#xff0c;覆盖大语言模型训练与使用的全流程&#xff0c;从预训练到微调与对齐&#xff0c;从使用技术到评测应用&#xff0c;帮助学员全面掌握大语言模型的核心技术。并且&#xff0c;课程…...

Docker+Flask 实战:打造高并发微服务架构

DockerFlask 实战&#xff1a;打造高并发微服务架构 今天我们要深入探讨一个非常热门且实用的主题&#xff1a;基于 Docker 部署 Python Flask 应用。Docker 作为当下最流行的容器化技术&#xff0c;已经广泛应用于各种开发和部署场景&#xff0c;尤其是在微服务架构中。而 Fl…...

前端跨域如何调试,以及相关概念梳理【环境变量 本地代理 正向代理 反向代理 OPTIONS请求 CDN 等】

跨域报错 一 前端日常开发时&#xff0c;项目的部署地址和接口请求的地址一般是同源的&#xff0c;不会跨域。 例如项目的测试环境部署在https://my-dev.BeatingWorldLine.com/xxx, 测试环境的访问接口域名也要相同来保证不跨域https://my-dev.BeatingWorldLine.com/api/xxx, …...