C++ Primer 学习笔记 第十四章 重载运算与类型转换

2023-10-27

C++允许我们自定义类类型之间的转换规则。

重载的运算符是具有特殊名字的函数,它的名字由operator和运算符构成。

重载的运算符函数参数数量与该运算符作用的运算对象的数量一样多,一元运算符有一个参数,二元运算符有两个参数,且二元运算符的左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。

除了重载的函数调用运算符()外,其他重载的运算符不能有默认实参。

如果运算符函数是一个成员函数,则它的左侧运算对象绑定到隐式的this指针上,因此,成员运算符函数的参数数量比运算符的运算对象总数少一个。

一个运算符函数或者是类的成员,或者至少拥有一个类类型的参数,这意味着我们不能为内置类型重定义运算符。

我们只能重载已有的运算符,而不能发明新的符号,如operator**是禁止的。

对于既是一元又是二元的运算符(+、-、&、*),编译器通过参数的数量推断是哪种运算符。

重载运算符不改变运算符原有的结合律:

x == z + y;
x == (z + y);    // 永远与上句等价

允许和禁止重载的运算符:
在这里插入图片描述
我们也可以显式调用重载的运算符:

// 对于非成员运算符函数
data1 + data2;
operator+(data1, data2);    // 与上句代码等价

// 对于成员运算符函数
data1 += data2;
data1.operator+=(data2);    // 与上句代码等价

某些运算符原始版本指定了运算对象的求值顺序,如逻辑运算符的短路求值、逗号运算符从左到右求值(函数调用时参数列表中的逗号不是逗号运算符),但使用重载的运算符相当于函数调用,因此,它的运算对象的求值顺序无法保留下来,因为它改变了以往的使用习惯,因此不建议重载它们。逗号和取地址符也不建议重定义,它们作用于类类型时已经有了内置的含义。

当某些类类型的操作在逻辑上与某些运算符相似时,才建议重载:
1.类如果执行IO操作,则定义移位运算符使其与内置类型IO一致。
2.如类某个操作检查相等性,则使用operator==,此时通常也定义operator!=。
3.如类存在比较,则定义operator<,此时通常也定义其他比较操作。
4.重载运算符的返回类型通常与内置的版本相同,如逻辑运算符返回bool类型,算术运算符返回类类型值,赋值和复合赋值运算符返回左侧运算对象的引用。

类如有重载的算术运算符,那么也应该有复合赋值运算符,如+和+=。

将重载运算符函数定义为成员函数还是非成员函数准则:
1.赋值=、下标[]、调用()、成员访问->运算符必须是成员函数。
2.复合赋值运算符两者都行,但最好是成员函数。
3.改变对象状态或与给定类型密切相关的运算符,如递增、递减、解引用,通常是成员函数。
4.有对称性的运算符,即左侧、右侧运算对象可以相互转换的运算符,如算术、相等性、关系、位运算符等,必须是非成员函数,因为当我们把具有对称性的运算符定义为成员函数时,我们在使用时左侧运算对象必须得是类类型,如:

// 如将string的+定义为成员函数
string s = "world";
string t = s + "!";    // 正确,相当于调用s.operator+()
string u = "hi" + s;    // 错误,相当于调用"hi".operaot+(),而"hi"类型为const char *,属于内置类型,没有成员函数

如上,因此string的+运算符被定义为非成员函数,此时上例中第三句代码相当于调用operator+("hi", s),两者都能转化为string。

"a" == "b";    // 使用的是C++内置的==,比较的是两个指针

重载输出运算符:

// 返回类型为ostream&,与内置的行为相似
ostream& operator<<(ostream& os, const Sales_data& item) {    // 第一个参数应该是ostream的非const引用,因为ostream不能被拷贝并且我们需要修改它的状态
                                                              // 第二个参数是const的类类型引用,是我们要输出的对象,不会改变它,它是类类型,引用效率高
    os << item.isbn();    // 不打印换行符,打印不应控制格式
    return os;
}

输入输出运算符必须是非成员函数,因为它第一个参数必须是i(o)stream类型,并且我们不能给标准库的iostream类添加接受Sales_data类的重载输入输出运算符,否则只能写为:

Sales_data data;
data << cout;

重载的输入输出运算符经常要读写类的私有成员,因此需要将此运算符声明为类的友元:

class a {
    friend ostream& operator<<(ostream&, const a&);
};

而输入运算符的第一个参数为istream&,第二个参数为类类型&:

istream& operator>>(istream& is, Sales_data& item) {
    double price;
    is >> item.bookNo >> item.units_sold >> price;
    if (is) {    // 检查输入状态
        item.revenue = item.units_sold * price;
    } else {
        item = Sales_data();    // 如输入失败,则对象被赋予默认状态,输入运算符应负责从错误中恢复
    }
    return is;
}

上例中,输入失败指:
1.读取到错误的输入类型。
2.读取到文件末尾或输入流出错。

如我们需要对输入如bookNo进行格式检查,在检查之后若不合法,也应该将流状态设为失败,可通过设置流的failbit实现。

算术和关系运算符一般定义为非成员函数以实现对称性,即交换左右两个运算对象效果不变。

算术运算符通常使用复合赋值运算符(如定义了算术运算符,则一般也会定义复合赋值运算符)来实现,这样可读性比较好:

Sales_data operator+(const Sales_data& lhs, const Sales_data& rhs) {    // 算术或关系运算符不改变两个运算对象,因此参数为const的,返回值也应该是一个临时对象
    Sales_data sum = lhs;
    sum += rhs;
    return sum;
}

相等运算符设计准则:
1.定义了==也要定义!=,在其中一个的设计中将工作委托给另一个即可。
2.如类的某操作在逻辑上有相等性的含义,则应该定义相等运算符。
3.相等应具有传递性。

设计了==和!=后可以更容易地使用标准库容器和算法。

一些关联容器和算法要用到小于运算符。

关系运算符应:
1.定义顺序关系,顺序关系是一个严格弱序。
2.如两个对象是!=的,则一个对象应<另一个。

Sales_data不应定义关系运算符,因为如果Sales_data定义的关系运算符比较的是isbn,但之前Sales_data定义的相等运算符比较的是revenue和units_sold,可能出现两个对象的units_sold不等(根据==运算符,两对象不等)而isbn相等的情况(根据关系运算符,两对象相等),不符合上述要求2,如将比较运算符改为每个成员依次比较大小或revenue和units_sold比较大小,就符合条件2了,但这没有意义。

除了拷贝赋值运算符和移动赋值运算符,还能定义其他的赋值运算符,如vector定义了接收花括号的元素列表作为参数的赋值运算符:

vector<int> ivec;
ivec = {0,1,2,3};

只要是赋值运算符(包括复合赋值运算符),它的返回类型就应该是类类型的引用,这样与内置类型的赋值运算符行为相似。

赋值运算符必须定义在类的内部,而复合赋值运算符不一定,但最好都写在类内部。

下标运算符通常用于容器类,它通常和内置类型的行为一致,都返回元素类型的引用,这样它可以出现在赋值运算符的任意一端。通常定义常量和非常量版本的重载下标运算符函数,常量版本的返回常量以禁止修改值。

迭代器类中通常定义递增和递减运算符,它并不一定是类的成员,但它会改变对象状态,那么最好定义为成员函数。一般应同时定义前置和后置版本的递增和递减运算符。

前置版本的递增和递减运算符行为应和内置版本的相似,返回类类型的引用。

前置版本:

// 以下check函数只检查超过最大值的下标,curr是无符号类型
StrBlobPtr& StrBolbPtr::operator++() {
    check(curr, "increment past end of StrBolbPtr");    // 最多可以自增到尾后元素位置,如果已经到尾后位置,则抛出异常
    ++curr;
    return *this;
}

StrBlobPtr& StrBolbPtr::operator--() {
    --curr;    // 先减它,当curr已经是0时将产生一个无效下标
    check(curr, "decrement past begin of StrBolbPtr");    // 检查减后的下标是否合法,0再减时,由于curr是无符号数,会产生非常大的数,从而使下标失效
    return *this;
}

区分前置和后置版本的递增和递减运算符的方法是,后置版本有一个int形参,它只起区分作用,后置版本:

StrBlobPtr StrBlobPtr::operator++(int) {    // 返回值不是引用,使用后置版本运算符时,编译器提供0作为int的实参,由于我们用不到此参数,因此未命名
    StrBolbPtr ret = *this;    // 记录当前值
    ++*this;    // 使用前置版本完成功能
    return ret;
}

显式调用递增和递减运算符:

StrBlobPtr p(a1);
p.operator++(0);    // 调用后置版本的递增运算符,传递一个整型值即可
p.operator++();    // 调用前置版本的递增运算符

成员访问运算符*(通常是类的成员):

std::string& StrBlobPtr::operator*() const {    // 解引用返回的是元素的引用
    auto p = check(curr, "dereference past end");    // check函数返回值类型为std::shared_ptr<std::vector<std::string>>
    return (*p)[curr];    // *p是curr所指的vector
}

成员访问运算符->(必须是类的成员),箭头运算符永远不能丢掉成员访问的最基本的含义,重载它的类在使用它时,如返回值是一个指向类类型对象的内置指针类型,则在这个指针上再调用->来获取该类对象的成员即可,如返回值是一个定义了->运算符的类的对象,则在这个返回的对象上再调用它本身的->运算符,直到返回值是指向类类型对象的内置指针类型:

std::string* operator->() const {    // 此处箭头运算符返回内置指针类型,此外它还能返回定义了->运算符类的对象,此外的任何返回类型都不允许
    return &(this->operator*());    // 将实际工作委托给解引用运算符
}

使用StrBlobPtr的->运算符:

StrBlobPtr p;
p->append("c");    // 由于StrBlobPtr中保存的是指向vector<string>的智能指针,因此箭头运算符右边应该访问string类型的成员
                   // 具体的调用过程为:由于p是定义了->的类类型对象,因此访问的是p的->运算符,它返回string *,之后对string *再调用->,由于string *是内置指针类型,因此按内置指针上->的访问规则访问string的成员即可
                   // p的->运算符右边要访问的成员必须是最后递归到内置指针类型时,该指针指向的对象的成员,因此,递归最后返回的一定是指向类类型的指针,而非指向内置类型的指针,否则内置类型没有成员可以访问,->也就不能用了

以上两个成员访问函数都是const的,因为获取一个对象的成员并没有改变这个对象(const成员函数中不能改变数据成员的值,const的对象只能调用const成员函数,非const对象两者都能调用,但会优先调用非const版本)。它们都返回非const的返回值,因为StrBlobPtr只能绑定到非常量StrBlob对象上,因为StrBlobPtr在实现时只有接受非常量StrBlob的引用的构造函数。

如果类定义了调用运算符,则该类的对象称为函数对象,因为可以像函数一样“调用”这种对象,实际上是在运行重载的调用运算符。函数对象通常用作泛型算法的实参:

class PrintString {
public:
    PrintString(ostream& o = cout, char c = ' ') : os(o), sep(c) { }
    void operator()(const string& s) const {
        os << s << sep;
    }
private:
    ostream& os;
    char sep;
};

int main() {
    vector<string> vs = { "aaa", "bbb" };
    for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));    // 打印每个vs中的内容
}

以上用法与lambda用法相似,我们编写一个lambda后,编译器将其翻译成一个未命名类的未命名对象,在这个类中只含有一个重载的调用运算符成员,它的形参列表和函数体与lambda表达式完全一样:

sort(words.begin(),  words.end(), [] (const string& a, const string& b) { return a.size() < b.size(); });
//等价于:
class ShorterString {
public:
    bool operator()(const string& s1, const string& s2) const {
        return s1.size() < s2.size();
    }
};

int main() {
    sort(words.begin(),  words.end(), ShorterString());
}

当lambda以引用捕获参数时,可以直接使用该引用而不用存储它,而如果使用值捕获时,那么lambda生成的类会多出数据成员存储该值,并且会生成一个构造函数,此构造函数会使用捕获的值传入形参用来初始化数据成员,此时由于定义了构造函数,默认构造函数将不会合成,因此在生成该类的对象时必须提供构造函数的参数,即想要捕获的值。lambda表达式生成的类不含默认构造函数(不能构造)、赋值运算符(不能赋值)和默认析构函数,而是否含默认的移动/拷贝构造函数视捕获的数据成员类型而定。

lambda捕获的参数默认不能改变,因此,默认它的调用运算符是const的,如将lambda改为mutable的,那么调用运算符为非const的。

C++ 11引入lambda的意义就在此,当需要一个只在此处会使用到的类时,这样可以简化写法,并且这个类是匿名的。

标准库定义了表示算术运算的函数对象类,如plus+、modulus%、equal_to==、negate-等,都定义在头文件functional中:
在这里插入图片描述

plus<int> intAdd;
int sum = intAdd(10, 20);    // sum为30

这些函数对象类通常用于替换算法中的默认运算符,如sort默认使用<,我们可以传给它一个greater<int>的对象:

sort(ivec.begin(), ivec.end(), greater<int>());    // 第三个参数是一个greater<int>的未命名对象,它使用greater<int>的默认构造函数

当我们对一个存放int指针的vector使用sort时,我们实际使用<比较两个指针,这是未定义行为,但如果我们使用标准库函数对象类less<int*>来替代默认的<,这样比较的结果就是指针在内存中地址的排序了。实际上,在关联容器中,就是使用的less<key_type>对元素排序的,因此我们可以直接定义指针作为关键字的set或map,这样在关联容器中对指针的排序是按其在内存中的地址排序的。

C++中可调用对象:函数、函数指针、lambda表达式、bind函数的返回对象、重载了调用运算符的类的对象。它们也有各自的类型,它们可能有相同的调用形式,调用形式指返回类型和传递的参数类型,一种调用形式对应着一种函数类型。

如我们想实现一个计算器,通过表示加减乘除的string和函数map实现:

int add(int i, int j) {
    return i + j;
}

int (*mod)(int, int) = [](int i, int j) {
        return i % j;
};

struct divide {
    int operator()(int denominator, int divisor) {
        return denominator / divisor;
    }
};

int main() {
    map<string, int (*)(int, int)> binops;
    binops.insert({"+", add});    // 正确
    binops.insert({"%", mod});    // 错误,mod类型不匹配(但我测试时可以,应该是编译器优化或新版本)
} 

为解决以上类型不匹配问题,C++ 11引入了名为function的类型,它定义在头文件functional中:
在这里插入图片描述

function<int(int, int)> f1 = add;    // 函数名用作函数指针
function<int(int, int)> f2 = divide();    // 调用默认构造函数生成函数对象类对象
function<int(int, int)> f3 = [](int i, int j) { return i * j };

map<string, function<int(int, int)>> binops =     
    {"+", add},
    {"-", std::minus<int>}
    {"/", divide()}
};    // 此时就可以全部放入map中了

binops["+"](1, 2);    // 调用add(1, 2)

二义性问题:

int add(int i, int j) {
    return i + j;
}

Sales_data add(const Sales_data&, const Sales_data&);

map<string, function<int(int, int)>> binops;
binops.insert({"+", add});   // 错误,有两个add(但我测试时是可以识别出来哪个add的,应该是编译器优化或新版本特性)

解决方法:

// 函数指针解法:
int (*fp)(int, int) = add;    // 函数指针
binops.insert({"+", fp});
// lambda解法:
binops.insert({"+", [](int i, int j) { return add(i, j); }});

自定义类类型转换可以通过只有一个参数的非explicit构造函数完成,还可以通过类型转换运算符完成,前者把另一个类型转换为我们定义的类类型,后者将我们定义的类类型转换为其他类型,类型转换运算符是一种特殊的成员函数,它将一个类类型的值转换成其他类型:

operator type() const;    // 它既没有显式的返回类型,也没有形参,且必须定义为类的成员函数,它通常不改变待转换对象的内容,因此通常是const的

如上,type表示除void外的能作为函数的返回类型的某种类型,因此不能转换成数组或函数类型。

例子:

class SmallInt {
public:
    SmallInt(int i = 0) : val(i) {    // 将int类型转换成SmallInt类型
        if (i < 0 || i > 255) {
            throw std::out_of_range("Bad SmallInt Value");
        }
    }

    operator int() const {    // 将SmallInt类型转换为int类型
        return val;    // val必须是int或能转换成int
    }
private:
    std::size_t val;
};

SmallInt si;
si = 4;    // 4隐式地转换成SmallInt,实际上是直接用4拷贝构造si
si + 3;    // si隐式转换为int,然后执行整数加法

用户定义的类型转换一次只能执行一步,但隐式的类内定义的类型转换可以在一个内置类型转换之前或之后进行:

SmallInt si = 3.14;    // 内置类型转换先将double转换成int,用户定义的隐式类型转换再将int转换为SmallInt
si + 3.14;    // 用户定义的隐式类型转换先将SmallInt转换为int,内置类型转换再将int转换成double

类型转换运算符隐式执行,这也是类型转换运算符没有形参列表的原因。

类很少有类型转换运算符,用户使用时发生隐式转换会感到困惑,但类经常有转换成bool的类型转换符,但可能引起意外结果:

int i = 42;
cin << i;   

C++早期版本中,上例是IO类型遇到的问题之一,尽管它将输出运算符作用到了cin上,但cin没有输出运算符,但cin有转换为bool的类型转换运算符,因此,cin会先转换为bool值,再提升为int类型,最后执行左移操作。

为防止以上情况发生,C++11引入了显式的类型转换运算符:

class SmallInt {
public:
     explicit operator int() const {
         return val;
     }
};

如上的类型转换运算符不会隐式地发生类型转换:

SmallInt si = 3;
si + 3;    // 错误,此处会发生隐式的类型转换
static_cast<int>(si) + 3;    // 正确,显式地请求类型转换

但如果如上所述,那么cin也就不能用于条件判断了,因为cin不会隐式地发生向bool类型的转换。因此,表达式被用于一个条件判断语境向bool类型转换时是一个例外,此时向bool类型的隐式类型转换也会发生,而不受explicit的约束,但即使是在条件判断语境中,也不能使用向其他类型的explicit的转换符转换,并且通常向bool类型的转换也是用于条件部分的,因此向bool转换的类型转换运算符通常带有explicit。

事实上,早期版本的C++也不会通过cin << 42这种代码,因为早期的IO类型定义了向void*的转换规则,这样它就不会在算术运算中被转换,并且可以在条件判断时转换。而新版IO库使用显式的类型转换运算符达到同样的目的。

二义性类型转换:
1.A类定义了接受B类型的构造函数,B中有向A转换的类型转换运算符时。
2.类中定义了多个转换源或转换目标可以相互转换的转换,尤其是算术类型,因为算术类型能相互转换。

例子:

struct B;

struct A {
    A() = default;
    A(const B&);    // 可以将B转换成A
};

struct B {
    operator A() const;    // 将B转换成A
};

A f(const A&);    // 以A为参数的返回A类型的函数

B b;
A a = f(b);    // 二义性错误,f(B::operator A())(用B中定义的类型转换运算符将B转换为A)还是f(A::A(const B&))(用A中的构造函数将B转换成A)?

解决方法:

A a = f(b.operator A());
A a = f(A(b));

我们也不能使用强制类型转换解决二义性问题,因为强制类型转换本身也面临二义性。

二义性类型转换的另一个例子:

struct A {
    A(int = 0);
    A(double);
    operator int() const;
    operator double() const;
};

void f2(long double);

A a;
f2(a);    // 二义性转换,将a转换为int还是double?两者都不能精确匹配

long lg;
A a2(lg);    // 二义性转换,将lg转换成int还是double?两者都不能精确匹配

以上二义性产生的原因与函数匹配时产生二义性的原因相似,都是因为转换级别相同而不能区分哪种转换更好造成的,两者的转换级别都是算术转换级别的,如转换级别不同就不会产生二义性了:

short s = 42;
A a3(s);    // s整型提升为int,此优先级高于s类型转换为double,因此会使用A::A(int)

因此:
1.不要令两个类完成相同的类型转换。如A有一个接受B的非explicit构造函数,且B有一个向A转换的类型转换函数。
2.如类有一个转换成算术类型的类型转换函数时:
(1)不要再定义接受算术类型的非explicit构造函数,即不需再将数字转换成类类型,而是让该类转换成算术类型,之后再进行算术类型的内置转换。
(2)不要再定义转换成其他算术类型的类型转换函数,即使用已定义的类型转换函数将该类转换成算术类型,再自动进行内置类型转换。

另一个二义性问题:

struct C {
    C(int);    // int可转换为C
};

struct D {
    D(int);    // int可转换为D
};

void manip(const C&);
void manip(const D&);    // 一对重载的函数

manip(10);    // 二义性错误,10转换成C或D一样好
manip(C(10));    // 一种解决方案,但这种解决方案意味着程序设计存在不足

上例的另一种情况:

struct E {
    E(double);    // double能转换成E
};

void manip2(const C&);
void manip2(const E&);

manip2(10);    // 依然有二义性错误,即使manip2(C(10))是精确匹配,在向不同的类类型转换过程中,如两种类型都提供了可行的转换,那么认为这两种转换一样好,而忽略其他因素,只考虑是否可行

函数匹配例子:

struct LongDouble {
    LongDouble(double = 0.0);
    operator double();
    operator float();
};

LongDouble ldObj;
int ex1 = ldObj;    // 错误,因为double向int和float向int的转换级别相同
float ex2 = ldObj;    // 正确,将LongDouble转换成float

void calc(int);
void calc(LongDouble);
double dval;
calc(dval);    // 调用calc(int),因为算术类型转换的优先级高于有类类型的转换

运算符的二义性:运算符也是一种函数,因此在调用时会将所有重载的运算符考虑在内,包括成员函数版本和非成员函数版本的。

运算符二义性的例子:

class SmallInt {
    friend SmallInt operator+(const SmallInt&, const SmallInt&);    
public:
    SmallInt(int = 0);    // 可以将int转换为SmallInt
    operator int() const {    // 可以将SmallInt转换为int
        return val;
    }
private:
    std::size_t val;
};

SmallInt s1, s2;
SmallInt s3 = s1 + s2;    // 使用operator+(const SmallInt&, const SmallInt&),这是精确匹配的
int i = s3 + 0;    // 二义性错误,两种选择,一是将s3转换成int,二是将0转换成SmallInt

如上,当我们为类同时定义了转换源和目标相同的函数且定义了这两种类型的二元运算符时,会产生二义性运算符调用。

二义性运算符调用的另一个例子:

SmallInt si;    // SmallInt类可以完成int向SmallInt的转换和SmallInt向int的转换
si + 3.14;    // 二义性调用,一是si可以转换为int,int再转换为double,二是3.14转换为int,int再转换为SmallInt,两者都是类类型转换,优先级相同
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ Primer 学习笔记 第十四章 重载运算与类型转换 的相关文章

  • java基础错题总结

    1 解析 首先乘法的优先级高于加法 所以先进行y z 然后在这里 是连接符 因为头一个是字符串所以系统就认为是连接符 就成 10202 0 输出这个字符串 但如果第一个不是字符串类型的 就像 10 20 a 这个会输出 30 a 2 解析
  • gis中的加权求和工具在哪里_因果推理初探(5)——干预工具(上)

    本节将延续上一节学习的干预的有关概念 开始深入介绍几种干预的工具 后门调整 前门调整 逆概率加权等 本节将有大量公式来袭 请准备草稿纸或提前绕道 在上一节最后 我们推导出有关干预的重要公式 调整公式 它的形式如下 这个公式让我们可以通过观测
  • 为什么使用代理后不能上网了?

    在使用完代理服务器之后 有的用户可能会遇到这样的问题 明明网络正常 为什么我的浏览器不能打开网页了 今天就给大家说下具体解决方法 这里我们以IE浏览器为例 1 先打开浏览器 点击右上角的 工具 图标 然后点击下拉中的 Internet选项
  • elasticsearch中文分词器插件elasticsearch-analysis-ik远程自定义词典热更新

    IK简介 IK分词器基于词库进行分词 analysis ik内置了一些词典 主词典main dic 姓氏词典surname dic 量词词典quantifier dic 后缀词典suffix dic 介词词典preposition dic
  • vue项目创建

    默认3 默认2 自定义配置 js语法编辑器 ts 渐进式web应用程序 路由 状态管理器 css处理器 代码检查 单元测试 端对端测试 选择版本 路由是否选择历史模式 选择css预处理器 配置放在哪里 保存这个项目作为一个模版使用 npm
  • 【网络】Linux网络问题汇总(一)

    网卡设置了静态获取 仍然获取动态IP的解决方法 问题展示 网卡配置静态方式获取 仍然通过dhcp获取到了ip 且每次分配的ip都一样 root senlian cat etc sysconfig network scripts ifcfg
  • OAUTH之 钉钉第三方授权登录

    文章目录 OAUTH之钉钉第三方授权登录 前期用到的工具 获取access token 请求地址 请求方法 响应 扫码 使用账号密码 获取 临时 code 参数重要说明 直接访问 扫码登录 使用账号密码登录第三方网站 根据 sns 临时授权
  • 性能测试度量指标

    1 响应时间 响应时间指从用户或事务在客户端发起一个请求开始 到客户端接收到从服务器端返回的响应结束 这整个过程所消耗的时间 在性能测试实践中 为了使响应时间更具代表性 响应时间通常是指事务的平均响应时间ART 在实践中要注意 不同行业 不
  • node+koa2+mongodb搭建RESTful API风格后台

    RESTful API风格 在开发之前先回顾一下 RESTful API 是什么 RESTful 是一种 API 设计风格 并不是一种强制规范和标准 它的特点在于请求和响应都简洁清晰 可读性强 不管 API 属于哪种风格 只要能够满足需要
  • Unity之URP开启PostProcessing后使用RenderTexture渲染模型背景为不透明

    项目需要在UI界面显示角色模型 使用一个模型相机投射到RT上然后放在Raw Image上 现在这个模型相机需要开启后处理Post Processing 只针对模型添加了后处理效果 问题是开启后 Raw Image背景变了 把UI背景图遮住了
  • TensorFlow找不到cudart64_110.dll not found的解决方案

    问题描述 当我写了两句小程序准备开启我的TensorFlow之路时候 import tensorflow as tf hello tf constant hello tensorflow print Hello python sess tf
  • 安防监控视频云存储平台EasyNVR对接EasyNVS时,一直不上线该如何解决?

    视频安防监控平台EasyNVR可支持设备通过RTSP Onvif协议接入 并能对接入的视频流进行处理与多端分发 包括RTSP RTMP HTTP FLV WS FLV HLS WebRTC等多种格式 近期有用户在使用安防视频平台EasyNV

随机推荐